astro/execution.go (98 lines of code) (raw):

/* * Copyright (c) 2018 Uber Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package astro import ( "fmt" "sort" "strings" "github.com/uber/astro/astro/conf" ) // MissingRequiredVarsError is an error type that is returned from plan or // apply when there are variables that need to be provided at run time that are // missing. type MissingRequiredVarsError struct { missing []string } func (e *MissingRequiredVarsError) plural() string { if len(e.missing) > 0 { return "s" } return "" } // Error is the error message, so this satisfies the error interface. func (e MissingRequiredVarsError) Error() string { return fmt.Sprintf("missing required variable%s: %s", e.plural(), strings.Join(e.missing, ", ")) } // MissingVars returns a list of the missing user variables. func (e MissingRequiredVarsError) MissingVars() []string { return e.missing } // terraformExecution is an interface that covers both bound and unbound // executions. type terraformExecution interface { ID() string ModuleConfig() conf.Module Variables() map[string]string TerraformParameters() []string } // execution represents the execution of a module with some variable // values. This type is never used directly: instead, the types // unboundExecution and boundExecution are used. type execution struct { moduleConf *conf.Module // variables is a map of variables to be passed during the // execution. variables map[string]string // terraformParameters is a list of additional Terraform parameters for this execution terraformParameters []string } // Name is an alias for ID; so that terraform/dag trace output makes // sense func (e *execution) Name() string { return e.ID() } // ID returns a unique ID for this execution. func (e *execution) ID() string { // For boundExecutions, the ID should be: // {modulename}-{variableValue1}-{variableValue2}-{and so on...} // Where variableValues are the values of the runtime variables. values := []string{} // Since runtime variables may have values that don't directly // pertain to this module/execution, we need to extract only the // variable names that are relevant to this module. keys := []string{} for _, v := range e.ModuleConfig().Variables { keys = append(keys, v.Name) } sort.Strings(keys) for _, key := range keys { values = append(values, e.variables[key]) } // construct the ID id := e.ModuleConfig().Name if len(values) > 0 { id = fmt.Sprintf("%s-%s", id, strings.Join(values, "-")) } return id } // ModuleConfig returns a copy of the configuration of the module // associated with this execution. func (e *execution) ModuleConfig() conf.Module { return *e.moduleConf } // Variables returns a reference to the variables set for this execution func (e *execution) Variables() map[string]string { return e.variables } // TerraformParameters returns reference to the Terraform parameters set for this execution func (e *execution) TerraformParameters() []string { return e.terraformParameters } // unboundExecution represents a module execution before runtime // variables have been provided by the user and template strings // replaced in the variable values. // An unboundExecution should never be actually executed. Instead, // bind() should be called with user variables supplied first. type unboundExecution struct { *execution } // bind takes a map of user-specified variables and returns a // boundExecution with variable values replaced. An error is returned if // not all required user values were provided. func (e *unboundExecution) bind(userVars map[string]string) (*boundExecution, error) { // boundVars is the map of execution variables bound to the values provided by user boundVars := make(map[string]string) for key, val := range e.Variables() { boundVars[key] = val if userVal, ok := userVars[key]; ok { boundVars[key] = userVal } } missingVars := []string{} // Check that the user provided variables replace everything that // needs to be replaced. for _, val := range boundVars { if err := assertAllVarsReplaced(val); err != nil { missingVars = append(missingVars, extractMissingVarNames(val)...) } } if len(missingVars) > 0 { return nil, MissingRequiredVarsError{missing: missingVars} } // Create a copy of the config and search attributes for placeholders // to replace with values from the bound vars. boundConfig := e.ModuleConfig() // TODO: Loop over all module configuration using reflection boundBackendConfig, err := replaceAllVarsInMapValues(boundConfig.Remote.BackendConfig, boundVars) if err != nil { return nil, fmt.Errorf("unable to bind execution: %v; %v", e.ID(), err) } boundConfig.Remote.BackendConfig = boundBackendConfig return &boundExecution{ &execution{ moduleConf: &boundConfig, variables: boundVars, terraformParameters: e.TerraformParameters(), }, }, nil } // boundExecution represents a module execution that is ready to be // executed. type boundExecution struct { *execution }