mirror of
				https://github.com/3bbbeau/tfvars-atlantis-config.git
				synced 2025-10-27 11:13:55 +00:00 
			
		
		
		
	
						commit
						bd4221af3e
					
				
							
								
								
									
										62
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								README.md
									
									
									
									
									
								
							| @ -8,14 +8,14 @@ | ||||
| 
 | ||||
| ## Quick start | ||||
| ### CLI | ||||
| 	tfvars-atlantis-config generate --automerge --autoplan --parallel --output=atlantis.yaml | ||||
| 	tfvars-atlantis-config generate --use-workspaces --automerge --autoplan --parallel --output=atlantis.yaml | ||||
| 
 | ||||
| ### Atlantis Server Side Config | ||||
| ```yaml | ||||
| repos: | ||||
| - id: /.*/ | ||||
|   pre_workflow_hooks: | ||||
|     - run: tfvars-atlantis-config generate --automerge --autoplan --parallel --output=atlantis.yaml | ||||
|     - run: tfvars-atlantis-config generate --use-workspaces --automerge --autoplan --parallel --output=atlantis.yaml | ||||
| ``` | ||||
| 
 | ||||
| 
 | ||||
| @ -51,7 +51,7 @@ parallel_apply: true | ||||
| projects: | ||||
| - name: my-terraform-dev | ||||
|   dir: my-terraform | ||||
|   workflow: my-terraform-dev | ||||
|   workspace: dev | ||||
|   autoplan: | ||||
|     when_modified: | ||||
|     - '*.tf' | ||||
| @ -59,33 +59,12 @@ projects: | ||||
|     enabled: true | ||||
| - name: my-terraform-prod | ||||
|   dir: my-terraform | ||||
|   workflow: my-terraform-prod | ||||
|   workspace: prod | ||||
|   autoplan: | ||||
|     when_modified: | ||||
|     - '*.tf' | ||||
|     - prod.tfvars | ||||
|     enabled: true | ||||
| workflows: | ||||
|   my-terraform-dev: | ||||
|     plan: | ||||
|       steps: | ||||
|       - init | ||||
|       - plan: | ||||
|           extra_args: | ||||
|           - -var-file=dev.tfvars | ||||
|     apply: | ||||
|       steps: | ||||
|       - apply | ||||
|   my-terraform-prod: | ||||
|     plan: | ||||
|       steps: | ||||
|       - init | ||||
|       - plan: | ||||
|           extra_args: | ||||
|           - -var-file=prod.tfvars | ||||
|     apply: | ||||
|       steps: | ||||
|       - apply | ||||
| ``` | ||||
| 
 | ||||
| ## Why you should use it? | ||||
| @ -107,24 +86,49 @@ runtime. | ||||
| | `--autoplan`                  | Enable auto plan.                                                                                                | false         | | ||||
| | `--default-terraform-version` | Default terraform version to run for Atlantis. Default is determined by the Terraform version constraints.       | ""            | | ||||
| | `--debug`                     | Enable debug logging.                                                                                            | false         | | ||||
| | `--multienv`                  | Enable injection of environment specific environment variables to each workflow.                                 | false         | | ||||
| | `--output`                    | Path of the file where configuration will be generated, usually `atlantis.yaml`. Default is to write to `stdout` | `stdout`      | | ||||
| | `--parallel`                  | Enables plans and applys to happen in parallel.                                                                  | false         | | ||||
| | `--root`                      | Path to the root directory of the git repo you want to build config for. Default is current dir.                 | `.`           | | ||||
| | `--use-workspaces`            | Whether to use Terraform workspaces for projects.                                                                | false         | | ||||
| 
 | ||||
| ## Multienv | ||||
| When `--multienv` is enabled, prefixed environment variables will be | ||||
| ## Workflows | ||||
| This utility does not generate workflows. You can use use the `$WORKSPACE` | ||||
| environment variable as part of a generic plan step to use the generated | ||||
| configuration. | ||||
| 
 | ||||
| See the [multienv](#multienv--provider-configuration) for a working example. | ||||
| 
 | ||||
| ## Multienv / Provider configuration | ||||
| You can run the `multienv` command in a workflow. Prefixed environment variables will be | ||||
| stripped of their prefix and injected into each workflow for the duration | ||||
| the workflow is run during plan/apply stages. | ||||
| 
 | ||||
| ### Example | ||||
| This is useful when you want to configure providers via environment variables | ||||
| on a per-workspace basis. | ||||
| 
 | ||||
| ``` | ||||
|   workflows: | ||||
|     default: | ||||
|       plan: | ||||
|         steps: | ||||
|         - init | ||||
|         - multienv: tfvars-atlantis-config multienv | ||||
|         - plan: | ||||
|             extra_args: ["-var-file", "$WORKSPACE.tfvars"] | ||||
|       apply: | ||||
|         steps: | ||||
|         - multienv: tfvars-atlantis-config multienv | ||||
|         - apply | ||||
| ``` | ||||
| 
 | ||||
| ### Example | ||||
| Workspace `dev`: | ||||
| _dev.tfvars_: | ||||
| 
 | ||||
| - `DEV_FOO_VAR="BAR"` -> `FOO_VAR="BAR"` | ||||
| - `DEV_AWS_ACCESS_KEY="..."` -> `AWS_ACCESS_KEY="..."` | ||||
| 
 | ||||
| Workspace `stg`: | ||||
| _stg.tfvars_: | ||||
| 
 | ||||
| - `STG_FOO_VAR="BAR"` -> `FOO_VAR="BAR"` | ||||
|  | ||||
| @ -38,7 +38,6 @@ func NewFlags() (*Flags, error) { | ||||
| 		AutoPlan:                false, | ||||
| 		DefaultTerraformVersion: "", | ||||
| 		Root:                    pwd, | ||||
| 		MultiEnv:                false, | ||||
| 		Output:                  "", | ||||
| 		Parallel:                false, | ||||
| 		UseWorkspaces:           false, | ||||
| @ -52,7 +51,6 @@ func (flags *Flags) AddFlags(cmd *cobra.Command) { | ||||
| 	cmd.Flags().BoolVar(&flags.Parallel, "parallel", flags.Parallel, "Enables plans and applys to happen in parallel. Default is disabled") | ||||
| 	cmd.Flags().StringVar(&flags.Output, "output", flags.Output, "Path of the file where configuration will be generated. Default is stdout") | ||||
| 	cmd.Flags().StringVar(&flags.Root, "root", flags.Root, "Path to the root directory of the git repo you want to build config for. Default is current dir") | ||||
| 	cmd.Flags().BoolVar(&flags.MultiEnv, "multienv", flags.MultiEnv, "Enable injection of environment specific environment variables to each workflow. Default is disabled") | ||||
| 	cmd.Flags().StringVar(&flags.DefaultTerraformVersion, "terraform-version", flags.DefaultTerraformVersion, "Default terraform version to run for Atlantis. Default is determined by the Terraform version constraints.") | ||||
| 	cmd.Flags().BoolVar(&flags.UseWorkspaces, "use-workspaces", flags.UseWorkspaces, "Use workspaces for projects. Default is disabled") | ||||
| 
 | ||||
| @ -67,7 +65,6 @@ func (flags *Flags) toOptions() repocfg.Options { | ||||
| 		Automerge:               flags.AutoMerge, | ||||
| 		Autoplan:                flags.AutoPlan, | ||||
| 		DefaultTerraformVersion: flags.DefaultTerraformVersion, | ||||
| 		MultiEnv:                flags.MultiEnv, | ||||
| 		Parallel:                flags.Parallel, | ||||
| 		UseWorkspaces:           flags.UseWorkspaces, | ||||
| 	} | ||||
|  | ||||
							
								
								
									
										98
									
								
								cmd/multienv.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								cmd/multienv.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | ||||
| package cmd | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"github.com/3bbbeau/tfvars-atlantis-config/logger" | ||||
| 	"github.com/spf13/cobra" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// ATLANTIS_WORKSPACE is the environment variable that contains the name of
 | ||||
| 	// the workspace that Atlantis is currently running for.
 | ||||
| 	ATLANTIS_WORKSPACE = "WORKSPACE" | ||||
| ) | ||||
| 
 | ||||
| // NewMultiEnvCmd creates a new `multienv` command
 | ||||
| func NewMultiEnvCmd() *cobra.Command { | ||||
| 	cmd := &cobra.Command{ | ||||
| 		Use:   "multienv", | ||||
| 		Short: "Returns a string representing the multienv for Atlantis", | ||||
| 		Long: `Returns a string representing the multienv for Atlantis, e.g.: | ||||
| 		EnvVar1Name=value1,EnvVar2Name=value2,EnvVar3Name=value3 | ||||
| 
 | ||||
| 		Reference: https://www.runatlantis.io/docs/custom-workflows.html#multiple-environment-variables-multienv-command
 | ||||
| 
 | ||||
| 		This is useful when you want to configure providers via environment variables | ||||
| 		on a per-workspace/environment basis.`, | ||||
| 		RunE: func(cmd *cobra.Command, args []string) error { | ||||
| 			workspace := os.Getenv(ATLANTIS_WORKSPACE) | ||||
| 			if len(workspace) == 0 { | ||||
| 				return fmt.Errorf("environment variable %s is not set. no workspace for multienv", ATLANTIS_WORKSPACE) | ||||
| 			} | ||||
| 
 | ||||
| 			multienv, err := multienv(os.Getenv(ATLANTIS_WORKSPACE)) | ||||
| 			if err != nil { | ||||
| 				if errors.Is(err, ErrNoEnvVars) { | ||||
| 					logger.FromContext(cmd.Context()).Debug("no matching prefixed environment variables found") | ||||
| 					// Return nil to indicate success, but no multienv string
 | ||||
| 					return nil | ||||
| 				} | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			// Atlanis expects the multienv string to be written to stdout
 | ||||
| 			fmt.Print(multienv) | ||||
| 			return nil | ||||
| 		}, | ||||
| 	} | ||||
| 	return cmd | ||||
| } | ||||
| 
 | ||||
| var ErrNoEnvVars error = fmt.Errorf("no matching prefixed environment variables found") | ||||
| 
 | ||||
| // Generates the Atlantis multienv string for multi-environment
 | ||||
| // Terraform projects, e.g:
 | ||||
| //
 | ||||
| //	EnvVar1Name=value1,EnvVar2Name=value2,EnvVar3Name=value3
 | ||||
| //
 | ||||
| // Given a prefix for the workspace name, strips the
 | ||||
| // environment name from existing environment vars while keeping their value.
 | ||||
| //
 | ||||
| // This is useful when you want to configure providers via environment variables
 | ||||
| // on a per-environment basis.
 | ||||
| //
 | ||||
| // Example:
 | ||||
| //
 | ||||
| //	DEV_AWS_ACCESS_KEY_ID="foo"
 | ||||
| //	DEV_AWS_SECRET_ACCESS_KEY="bar"
 | ||||
| //	->
 | ||||
| //	AWS_ACCESS_KEY_ID=$DEV_AWS_ACCESS_KEY_ID
 | ||||
| //	AWS_SECRET_ACCESS_KEY=$DEV_AWS_SECRET_ACCESS_KEY
 | ||||
| func multienv(prefix string) (string, error) { | ||||
| 	prefix = strings.ToUpper(prefix) | ||||
| 	if !strings.HasSuffix(prefix, "_") { | ||||
| 		prefix += "_" | ||||
| 	} | ||||
| 
 | ||||
| 	strippedEnviron := []string{} | ||||
| 	for _, v := range os.Environ() { | ||||
| 		if strings.HasPrefix(v, prefix) { | ||||
| 			// Limits the split to only two parts, separating the key from the first
 | ||||
| 			// occurrence of '=', otherwise if the value contains '=' character(s) the
 | ||||
| 			// string would be split into more than two parts.
 | ||||
| 			split := strings.SplitN(v, "=", 2) | ||||
| 
 | ||||
| 			// Strips the prefix from the environment variable name, e.g. "DEV_" from
 | ||||
| 			// "DEV_AWS_ACCESS_KEY_ID" and let it equal to the original environment variable
 | ||||
| 			strippedEnviron = append(strippedEnviron, fmt.Sprintf("%s=%s", strings.TrimPrefix(split[0], prefix), split[1])) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(strippedEnviron) == 0 { | ||||
| 		return "", ErrNoEnvVars | ||||
| 	} | ||||
| 	return strings.Join(strippedEnviron, ","), nil | ||||
| } | ||||
| @ -35,6 +35,7 @@ func New() (*cobra.Command, error) { | ||||
| 		return nil, fmt.Errorf("creating generate command: %w", err) | ||||
| 	} | ||||
| 	cmd.AddCommand(gCmd) | ||||
| 	cmd.AddCommand(NewMultiEnvCmd()) | ||||
| 
 | ||||
| 	return cmd, nil | ||||
| } | ||||
|  | ||||
| @ -40,8 +40,7 @@ func ProjectsFrom(c Component, opts Options) ([]ExtRawProject, error) { | ||||
| 				Name: ptr(friendlyName(c.Path, v)), | ||||
| 
 | ||||
| 				// The directory of this project relative to the repo root.
 | ||||
| 				Dir:      ptr(c.Path), | ||||
| 				Workflow: ptr(friendlyName(c.Path, v)), | ||||
| 				Dir: ptr(c.Path), | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
|  | ||||
| @ -32,7 +32,6 @@ func Test_ProjectsFrom(t *testing.T) { | ||||
| 					Project: raw.Project{ | ||||
| 						Name:             ptr("test-env"), | ||||
| 						Dir:              ptr("test"), | ||||
| 						Workflow:         ptr("test-env"), | ||||
| 						Workspace:        ptr("env"), | ||||
| 						TerraformVersion: ptr("8.8.8"), | ||||
| 						Autoplan: &raw.Autoplan{ | ||||
|  | ||||
| @ -15,7 +15,6 @@ type Options struct { | ||||
| 	Autoplan                bool | ||||
| 	DefaultTerraformVersion string | ||||
| 	Parallel                bool | ||||
| 	MultiEnv                bool | ||||
| 	UseWorkspaces           bool | ||||
| } | ||||
| 
 | ||||
| @ -58,19 +57,6 @@ func NewRepoCfg(components []Component, opts Options) (*ExtRawRepoCfg, error) { | ||||
| 
 | ||||
| 	repoCfg.Projects = append(repoCfg.Projects, projects...) | ||||
| 
 | ||||
| 	workflows := map[string]raw.Workflow{} | ||||
| 	for _, c := range components { | ||||
| 		generated, err := WorkflowsFrom(c, opts) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("failed while creating workflows with component %+v: %w", c, err) | ||||
| 		} | ||||
| 		for _, wf := range generated { | ||||
| 			workflows[wf.Name] = wf.Workflow | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	repoCfg.Workflows = workflows | ||||
| 
 | ||||
| 	return repoCfg, nil | ||||
| } | ||||
| 
 | ||||
| @ -80,20 +66,9 @@ func (rc *ExtRawRepoCfg) MarshalYAML() (interface{}, error) { | ||||
| 		{Key: "automerge", Value: rc.Automerge}, | ||||
| 		{Key: "parallel_plan", Value: rc.ParallelPlan}, | ||||
| 		{Key: "parallel_apply", Value: rc.ParallelApply}, | ||||
| 
 | ||||
| 		{Key: "projects", Value: rc.Projects}, | ||||
| 	} | ||||
| 
 | ||||
| 	workflows := yaml.MapSlice{} | ||||
| 	for name, wf := range rc.Workflows { | ||||
| 		workflows = append(workflows, yaml.MapItem{ | ||||
| 			Key: name, | ||||
| 			Value: yaml.MapSlice{ | ||||
| 				{Key: "plan", Value: wf.Plan}, | ||||
| 				{Key: "apply", Value: wf.Apply}, | ||||
| 			}, | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| 	m = append(m, yaml.MapItem{Key: "workflows", Value: workflows}) | ||||
| 	return m, nil | ||||
| } | ||||
|  | ||||
| @ -29,60 +29,14 @@ func Test_NewFrom(t *testing.T) { | ||||
| 					Automerge:     ptr(false), | ||||
| 					ParallelPlan:  ptr(false), | ||||
| 					ParallelApply: ptr(false), | ||||
| 					Workflows: map[string]raw.Workflow{ | ||||
| 						"test-dev": { | ||||
| 							Plan: &raw.Stage{ | ||||
| 								Steps: []raw.Step{ | ||||
| 									{ | ||||
| 										Key: ptr("init"), | ||||
| 									}, | ||||
| 									{ | ||||
| 										Map: map[string]map[string][]string{ | ||||
| 											"plan": {"extra_args": []string{"-var-file=vars/dev.tfvars"}}, | ||||
| 										}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							Apply: &raw.Stage{ | ||||
| 								Steps: []raw.Step{ | ||||
| 									{ | ||||
| 										Key: ptr("apply"), | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 						"test-stg": { | ||||
| 							Plan: &raw.Stage{ | ||||
| 								Steps: []raw.Step{ | ||||
| 									{ | ||||
| 										Key: ptr("init"), | ||||
| 									}, | ||||
| 									{ | ||||
| 										Map: map[string]map[string][]string{ | ||||
| 											"plan": {"extra_args": []string{"-var-file=vars/nested/stg.tfvars"}}, | ||||
| 										}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 							Apply: &raw.Stage{ | ||||
| 								Steps: []raw.Step{ | ||||
| 									{ | ||||
| 										Key: ptr("apply"), | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 					Projects: []raw.Project{ | ||||
| 						{ | ||||
| 							Name:     ptr("test-dev"), | ||||
| 							Dir:      ptr("test"), | ||||
| 							Workflow: ptr("test-dev"), | ||||
| 							Name: ptr("test-dev"), | ||||
| 							Dir:  ptr("test"), | ||||
| 						}, | ||||
| 						{ | ||||
| 							Name:     ptr("test-stg"), | ||||
| 							Dir:      ptr("test"), | ||||
| 							Workflow: ptr("test-stg"), | ||||
| 							Name: ptr("test-stg"), | ||||
| 							Dir:  ptr("test"), | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
|  | ||||
| @ -1,8 +1,6 @@ | ||||
| package repocfg | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| ) | ||||
| @ -10,7 +8,7 @@ import ( | ||||
| // Ptr returns a pointer to type T
 | ||||
| func ptr[T any](v T) *T { return &v } | ||||
| 
 | ||||
| // friendlyName creates a contextual name used for Atlantis projects and workflows
 | ||||
| // friendlyName creates a contextual name used for Atlantis projects
 | ||||
| func friendlyName(path, environment string) string { | ||||
| 	name := []string{strings.ReplaceAll(path, "/", "-"), pathWithoutExtension(filepath.Base(environment))} | ||||
| 	return strings.TrimSuffix(strings.Join(name, "-"), "-") | ||||
| @ -21,48 +19,3 @@ func friendlyName(path, environment string) string { | ||||
| func pathWithoutExtension(path string) string { | ||||
| 	return strings.TrimSuffix(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)), filepath.Base(path)) | ||||
| } | ||||
| 
 | ||||
| var ErrNoEnvVars error = fmt.Errorf("no matching prefixed environment variables found") | ||||
| 
 | ||||
| // Generates the Atlantis multienv string within stages for multi-environment
 | ||||
| // Terraform projects, e.g:
 | ||||
| //
 | ||||
| //	EnvVar1Name=value1,EnvVar2Name=value2,EnvVar3Name=value3
 | ||||
| //
 | ||||
| // Given a prefix for the environment name of the workflow, strips the
 | ||||
| // environment name from existing environment vars while keeping their value.
 | ||||
| //
 | ||||
| // This is useful when you want to configure providers via environment variables
 | ||||
| // on a per-environment basis.
 | ||||
| //
 | ||||
| // Example:
 | ||||
| //
 | ||||
| //	DEV_AWS_ACCESS_KEY_ID="foo"
 | ||||
| //	DEV_AWS_SECRET_ACCESS_KEY="bar"
 | ||||
| //	->
 | ||||
| //	AWS_ACCESS_KEY_ID=$DEV_AWS_ACCESS_KEY_ID
 | ||||
| //	AWS_SECRET_ACCESS_KEY=$DEV_AWS_SECRET_ACCESS_KEY
 | ||||
| func prefixedEnviron(prefix string) (*string, error) { | ||||
| 	prefix = strings.ToUpper(prefix) | ||||
| 	if !strings.HasSuffix(prefix, "_") { | ||||
| 		prefix += "_" | ||||
| 	} | ||||
| 
 | ||||
| 	strippedEnviron := []string{} | ||||
| 	for _, v := range os.Environ() { | ||||
| 		if strings.HasPrefix(v, prefix) { | ||||
| 			// Limits the split to only two parts, separating the key from the first
 | ||||
| 			// occurrence of '=', otherwise if the value contains '=' character(s) the
 | ||||
| 			// string would be split into more than two parts.
 | ||||
| 			name := strings.SplitN(v, "=", 2)[0] | ||||
| 
 | ||||
| 			// Strips the prefix from the environment variable name, e.g. "DEV_" from
 | ||||
| 			// "DEV_AWS_ACCESS_KEY_ID" and let it equal to the original environment variable
 | ||||
| 			strippedEnviron = append(strippedEnviron, fmt.Sprintf("%s=$%s", strings.TrimPrefix(name, prefix), name)) | ||||
| 		} | ||||
| 	} | ||||
| 	if len(strippedEnviron) == 0 { | ||||
| 		return nil, ErrNoEnvVars | ||||
| 	} | ||||
| 	return ptr(fmt.Sprintf("echo %s", strings.Join(strippedEnviron, ","))), nil | ||||
| } | ||||
|  | ||||
| @ -27,7 +27,7 @@ func Test_Ptr(t *testing.T) { | ||||
| } | ||||
| 
 | ||||
| // Tests the friendlyName helper function. Given a path and an environment it
 | ||||
| // should provide a contextual name to be used for Atlantis projects and workflows.
 | ||||
| // should provide a contextual name to be used for Atlantis projects.
 | ||||
| func Test_FriendlyName(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 
 | ||||
|  | ||||
| @ -1,175 +0,0 @@ | ||||
| package repocfg | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/runatlantis/atlantis/server/core/config/raw" | ||||
| ) | ||||
| 
 | ||||
| // ExtRawWorkflow extends the raw.Workflow type to add additional methods
 | ||||
| // and fields
 | ||||
| type ExtRawWorkflow struct { | ||||
| 	Name      string | ||||
| 	Args      *ExtraArgs | ||||
| 	Workspace string | ||||
| 	raw.Workflow | ||||
| } | ||||
| 
 | ||||
| // ExtraArgs is a type aliased map of extra arguments to be passed within a workflow
 | ||||
| type ExtraArgs map[string][]string | ||||
| 
 | ||||
| // hasArgs returns true if the component has a target (i.e. a Terraform variable file)
 | ||||
| func hasArgs(c Component) bool { | ||||
| 	return len(c.VarFiles) > 0 | ||||
| } | ||||
| 
 | ||||
| // Add generates the extra_args for the stage and adds them to the stage's Args
 | ||||
| func (args *ExtraArgs) Add(v []string) { | ||||
| 	(*args)["extra_args"] = v | ||||
| } | ||||
| 
 | ||||
| // relativeVarFile returns the path of the variable file v relative to
 | ||||
| // the component's path.
 | ||||
| func relativeVarFile(c, v string) (*string, error) { | ||||
| 	absComponent, err := filepath.Abs(c) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("absolute path for component %s: %s", c, err) | ||||
| 	} | ||||
| 
 | ||||
| 	absVarFile, err := filepath.Abs(v) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("absolute path for %s: %s", v, err) | ||||
| 	} | ||||
| 
 | ||||
| 	rel, err := filepath.Rel(absComponent, absVarFile) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("relative path for %s: %s", v, err) | ||||
| 	} | ||||
| 	return &rel, nil | ||||
| } | ||||
| 
 | ||||
| // WorkflowsFrom creates new Atlantis workflows from a Terraform component
 | ||||
| // and calls the stage methods to generate all of its fields.
 | ||||
| func WorkflowsFrom(c Component, opts Options) ([]ExtRawWorkflow, error) { | ||||
| 	var workflows []ExtRawWorkflow | ||||
| 	for _, v := range c.VarFiles { | ||||
| 
 | ||||
| 		wf := ExtRawWorkflow{ | ||||
| 			Name: friendlyName(c.Path, v), | ||||
| 			Workflow: raw.Workflow{ | ||||
| 				Plan:  &raw.Stage{}, | ||||
| 				Apply: &raw.Stage{}, | ||||
| 			}, | ||||
| 			Args:      &ExtraArgs{}, | ||||
| 			Workspace: pathWithoutExtension(v), | ||||
| 		} | ||||
| 
 | ||||
| 		if hasArgs(c) { | ||||
| 			rel, err := relativeVarFile(c.Path, v) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("relative variable file for %s: %s", v, err) | ||||
| 			} | ||||
| 
 | ||||
| 			wf.Args.Add([]string{"-var-file=" + *rel}) | ||||
| 		} | ||||
| 
 | ||||
| 		if opts.MultiEnv { | ||||
| 			err := wf.AddMultiEnv(v) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("adding multienv for %s: %s", v, err) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if opts.UseWorkspaces { | ||||
| 			wf.AddWorkspace() | ||||
| 		} | ||||
| 
 | ||||
| 		wf.PlanStage(opts) | ||||
| 		wf.ApplyStage(opts) | ||||
| 
 | ||||
| 		err := wf.Validate() | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("workflow validation failed for %s: %s", wf.Name, err) | ||||
| 		} | ||||
| 		workflows = append(workflows, wf) | ||||
| 	} | ||||
| 	return workflows, nil | ||||
| } | ||||
| 
 | ||||
| // PlanStage generates the plan stage for the workflow
 | ||||
| func (wf *ExtRawWorkflow) PlanStage(opts Options) { | ||||
| 	if wf.Plan == nil { | ||||
| 		wf.Plan = new(raw.Stage) | ||||
| 	} | ||||
| 
 | ||||
| 	init := raw.Step{ | ||||
| 		Key: ptr("init"), | ||||
| 	} | ||||
| 	wf.Plan.Steps = append(wf.Plan.Steps, init) | ||||
| 
 | ||||
| 	if len(*wf.Args) > 0 { | ||||
| 		wf.Plan.Steps = append(wf.Plan.Steps, raw.Step{ | ||||
| 			Map: map[string]map[string][]string{ | ||||
| 				"plan": *wf.Args, | ||||
| 			}, | ||||
| 		}) | ||||
| 	} else { | ||||
| 		wf.Plan.Steps = append(wf.Plan.Steps, raw.Step{ | ||||
| 			Key: ptr("plan"), | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ApplyStage generates the apply stage for the workflow
 | ||||
| //
 | ||||
| // Unlike the plan stage, the apply stage doesn't need extra args for -var-file,
 | ||||
| // as it uses the planfile generated by the plan stage.
 | ||||
| func (wf *ExtRawWorkflow) ApplyStage(opts Options) { | ||||
| 	if wf.Apply == nil { | ||||
| 		wf.Apply = new(raw.Stage) | ||||
| 	} | ||||
| 
 | ||||
| 	wf.Apply.Steps = append(wf.Apply.Steps, raw.Step{ | ||||
| 		Key: ptr("apply"), | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // AddWorkspace sets TF_WORKSPACE in the plan and apply stages
 | ||||
| // to the value of the workspace from the project configuration.
 | ||||
| //
 | ||||
| // This is needed because Atlantis will use the default workspace
 | ||||
| // which doesn't exist.
 | ||||
| func (wf *ExtRawWorkflow) AddWorkspace() { | ||||
| 	for _, stg := range []*raw.Stage{wf.Plan, wf.Apply} { | ||||
| 		stg.Steps = append(stg.Steps, raw.Step{ | ||||
| 			EnvOrRun: map[string]map[string]string{ | ||||
| 				"env": { | ||||
| 					"name":  "TF_WORKSPACE", | ||||
| 					"value": wf.Workspace, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // AddMultiEnv adds the multienv step to the plan and apply stages
 | ||||
| func (wf *ExtRawWorkflow) AddMultiEnv(env string) error { | ||||
| 	environ, err := prefixedEnviron(pathWithoutExtension(env)) | ||||
| 	if errors.Is(err, ErrNoEnvVars) { | ||||
| 		return nil | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("adding multienv for environment %s: %w", env, err) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, stg := range []*raw.Stage{wf.Plan, wf.Apply} { | ||||
| 		stg.Steps = append(stg.Steps, raw.Step{ | ||||
| 			StringVal: map[string]string{ | ||||
| 				"multienv": *environ, | ||||
| 			}, | ||||
| 		}) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @ -1,96 +0,0 @@ | ||||
| package repocfg | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"github.com/runatlantis/atlantis/server/core/config/raw" | ||||
| ) | ||||
| 
 | ||||
| // Tests creating a new workflow from a Terraform component
 | ||||
| func Test_WorkflowsFrom(t *testing.T) { | ||||
| 	t.Parallel() | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name      string | ||||
| 		component Component | ||||
| 		want      []ExtRawWorkflow | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "new-workflow", | ||||
| 			component: Component{ | ||||
| 				Path:     "test", | ||||
| 				VarFiles: []string{"test/vars/dev.tfvars", "test/vars/stg.tfvars"}, | ||||
| 			}, | ||||
| 			want: []ExtRawWorkflow{ | ||||
| 				{ | ||||
| 					Name: "test-dev", | ||||
| 					Args: &ExtraArgs{ | ||||
| 						"extra_args": []string{"-var-file=vars/dev.tfvars"}, | ||||
| 					}, | ||||
| 					Workspace: "dev", | ||||
| 					Workflow: raw.Workflow{ | ||||
| 						Plan: &raw.Stage{ | ||||
| 							Steps: []raw.Step{ | ||||
| 								{ | ||||
| 									Key: ptr("init"), | ||||
| 								}, | ||||
| 								{ | ||||
| 									Map: map[string]map[string][]string{ | ||||
| 										"plan": {"extra_args": []string{"-var-file=vars/dev.tfvars"}}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 						Apply: &raw.Stage{ | ||||
| 							Steps: []raw.Step{ | ||||
| 								{ | ||||
| 									Key: ptr("apply"), | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 				{ | ||||
| 					Name: "test-stg", | ||||
| 					Args: &ExtraArgs{ | ||||
| 						"extra_args": []string{"-var-file=vars/stg.tfvars"}, | ||||
| 					}, | ||||
| 					Workspace: "stg", | ||||
| 					Workflow: raw.Workflow{ | ||||
| 						Plan: &raw.Stage{ | ||||
| 							Steps: []raw.Step{ | ||||
| 								{ | ||||
| 									Key: ptr("init"), | ||||
| 								}, | ||||
| 								{ | ||||
| 									Map: map[string]map[string][]string{ | ||||
| 										"plan": {"extra_args": []string{"-var-file=vars/stg.tfvars"}}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 						Apply: &raw.Stage{ | ||||
| 							Steps: []raw.Step{ | ||||
| 								{ | ||||
| 									Key: ptr("apply"), | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tc := range tests { | ||||
| 		got, err := WorkflowsFrom(tc.component, Options{}) | ||||
| 		if err != nil { | ||||
| 			t.Errorf("WorkflowsFrom(): %s", err) | ||||
| 		} | ||||
| 		if !cmp.Equal(got, tc.want) { | ||||
| 			t.Errorf(`WorkflowFrom() | ||||
| 				diff %s`, cmp.Diff(got, tc.want)) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Beau
						Beau