mirror of
https://github.com/3bbbeau/tfvars-atlantis-config.git
synced 2025-12-05 22:01:43 +00:00
v0.0.1
This commit is contained in:
105
repocfg/project.go
Normal file
105
repocfg/project.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package repocfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/core/config/raw"
|
||||
)
|
||||
|
||||
// ExtRawProject extends the raw.Project type to add additional methods
|
||||
type ExtRawProject struct {
|
||||
raw.Project
|
||||
}
|
||||
|
||||
// ErrProjectFrom represents an error when creating an Atlantis project from a
|
||||
// Terraform component along with some useful contextual information.
|
||||
type ErrNewProject struct {
|
||||
Err error
|
||||
Component Component
|
||||
Project *ExtRawProject
|
||||
}
|
||||
|
||||
// Stringer implementation for ErrProjectFrom
|
||||
func (e ErrNewProject) Error() string {
|
||||
return fmt.Sprintf(`failed to create project:
|
||||
component: %+v,
|
||||
project: %+v,
|
||||
error: %s`, e.Component, e.Project, e.Err)
|
||||
}
|
||||
|
||||
// ProjectsFrom creates an Atlantis project from a Terraform component and
|
||||
// its associated Terraform variable files.
|
||||
func ProjectsFrom(c Component, opts Options) ([]ExtRawProject, error) {
|
||||
var projects []ExtRawProject
|
||||
|
||||
for _, v := range c.VarFiles {
|
||||
p := ExtRawProject{
|
||||
Project: raw.Project{
|
||||
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)),
|
||||
},
|
||||
}
|
||||
|
||||
if opts.UseWorkspaces {
|
||||
// Terraform workspaces are represented by the Terraform variable file
|
||||
// names.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// dev.tfvars -> dev
|
||||
// stg.tfvars.json -> stg
|
||||
p.Workspace = ptr(pathWithoutExtension(v))
|
||||
}
|
||||
|
||||
// Generate a default Terraform version for the project if enabled
|
||||
if opts.DefaultTerraformVersion != "" {
|
||||
p.DefaultTerraformVersion(opts.DefaultTerraformVersion)
|
||||
}
|
||||
|
||||
// Generate autoplan configuration for the project if enabled
|
||||
if opts.Autoplan {
|
||||
p.AutoPlan(v)
|
||||
}
|
||||
|
||||
// We can validate the project using the Atlantis validate method
|
||||
err := p.Validate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to validate project: %w", err)
|
||||
}
|
||||
projects = append(projects, p)
|
||||
}
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
// AutoPlan sets the autoplan configuration for the project
|
||||
//
|
||||
// This will trigger a plan when the Terraform files or the variable files
|
||||
// are modified.
|
||||
func (p *ExtRawProject) AutoPlan(v string) {
|
||||
autoplan := &raw.Autoplan{
|
||||
Enabled: ptr(true),
|
||||
|
||||
// Paths are relative to the project directory
|
||||
WhenModified: []string{
|
||||
"*.tf",
|
||||
filepath.Base(v),
|
||||
},
|
||||
}
|
||||
p.Autoplan = autoplan
|
||||
}
|
||||
|
||||
// DefaultTerraformVersion sets the default Terraform version for the project
|
||||
// if the version is valid.
|
||||
func (p *ExtRawProject) DefaultTerraformVersion(v string) {
|
||||
ver, err := version.NewSemver(v)
|
||||
if err != nil {
|
||||
p.TerraformVersion = new(string)
|
||||
} else {
|
||||
p.TerraformVersion = ptr(ver.String())
|
||||
}
|
||||
}
|
||||
118
repocfg/project_test.go
Normal file
118
repocfg/project_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package repocfg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/runatlantis/atlantis/server/core/config/raw"
|
||||
)
|
||||
|
||||
// Tests the ProjectFrom method for a project
|
||||
func Test_ProjectsFrom(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
component Component
|
||||
options Options
|
||||
want []ExtRawProject
|
||||
}{
|
||||
{
|
||||
component: Component{
|
||||
Path: "test",
|
||||
VarFiles: []string{"env.tfvars"},
|
||||
},
|
||||
options: Options{
|
||||
Autoplan: true,
|
||||
DefaultTerraformVersion: "8.8.8",
|
||||
Parallel: true,
|
||||
UseWorkspaces: true,
|
||||
},
|
||||
want: []ExtRawProject{
|
||||
{
|
||||
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{
|
||||
Enabled: ptr(true),
|
||||
WhenModified: []string{
|
||||
"*.tf",
|
||||
"env.tfvars",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
got, err := ProjectsFrom(tc.component, tc.options)
|
||||
if err != nil {
|
||||
t.Errorf("ProjectFrom() error: %s", err)
|
||||
}
|
||||
|
||||
if !cmp.Equal(got, tc.want) {
|
||||
t.Errorf(`ProjectFrom()
|
||||
diff %s`, cmp.Diff(got, tc.want))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the AutoPlan method for a project
|
||||
func Test_AutoPlan(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want := &raw.Autoplan{
|
||||
Enabled: ptr(true),
|
||||
WhenModified: []string{
|
||||
"*.tf",
|
||||
"env.tfvars",
|
||||
},
|
||||
}
|
||||
|
||||
got := new(ExtRawProject)
|
||||
|
||||
got.AutoPlan("env.tfvars")
|
||||
|
||||
if !cmp.Equal(got.Autoplan, want) {
|
||||
t.Errorf(`AutoPlan()
|
||||
diff %s`, cmp.Diff(got, want))
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the DefaultTerraformVersion method for a project
|
||||
func Test_DefaultTerraformVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
version string
|
||||
want raw.Project
|
||||
}{
|
||||
{
|
||||
version: "8.8.8",
|
||||
want: raw.Project{
|
||||
TerraformVersion: ptr("8.8.8"),
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "invalid",
|
||||
want: raw.Project{
|
||||
TerraformVersion: new(string),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
new := new(ExtRawProject)
|
||||
new.DefaultTerraformVersion(tc.version)
|
||||
got := new.Project.TerraformVersion
|
||||
|
||||
if !cmp.Equal(got, tc.want.TerraformVersion) {
|
||||
t.Errorf(`DefaultTerraformVersion()
|
||||
diff %s`, cmp.Diff(got, tc.want))
|
||||
}
|
||||
}
|
||||
}
|
||||
99
repocfg/repocfg.go
Normal file
99
repocfg/repocfg.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package repocfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/core/config/raw"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var ErrNoExistingConfig = fmt.Errorf("no existing config found")
|
||||
|
||||
// Options represents the top-level configuration for a new Atlantis RepoCfg
|
||||
type Options struct {
|
||||
Automerge bool
|
||||
Autoplan bool
|
||||
DefaultTerraformVersion string
|
||||
Parallel bool
|
||||
MultiEnv bool
|
||||
UseWorkspaces bool
|
||||
}
|
||||
|
||||
// Component represents a Terraform component and its associated Terraform variable files
|
||||
type Component struct {
|
||||
Path string
|
||||
VarFiles []string
|
||||
}
|
||||
|
||||
// ExtRawRepoCfg is an embedded type for a raw.RepoCfg
|
||||
type ExtRawRepoCfg struct {
|
||||
raw.RepoCfg `yaml:",inline"`
|
||||
}
|
||||
|
||||
// NewRepoCfg returns a new Atlantis RepoCfg from a slice of components
|
||||
// and options.
|
||||
//
|
||||
// Reference: https://www.runatlantis.io/docs/repo-level-atlantis-yaml.html
|
||||
func NewRepoCfg(components []Component, opts Options) (*ExtRawRepoCfg, error) {
|
||||
repoCfg := &ExtRawRepoCfg{
|
||||
RepoCfg: raw.RepoCfg{
|
||||
Version: ptr(3),
|
||||
Automerge: &opts.Automerge,
|
||||
ParallelPlan: &opts.Parallel,
|
||||
ParallelApply: &opts.Parallel,
|
||||
},
|
||||
}
|
||||
|
||||
var projects []raw.Project
|
||||
for _, c := range components {
|
||||
generated, err := ProjectsFrom(c, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed while creating projects with component %+v: %w", c, err)
|
||||
}
|
||||
|
||||
for _, p := range generated {
|
||||
projects = append(projects, p.Project)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (rc *ExtRawRepoCfg) MarshalYAML() (interface{}, error) {
|
||||
m := yaml.MapSlice{
|
||||
{Key: "version", Value: rc.Version},
|
||||
{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
|
||||
}
|
||||
100
repocfg/repocfg_test.go
Normal file
100
repocfg/repocfg_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package repocfg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/runatlantis/atlantis/server/core/config/raw"
|
||||
)
|
||||
|
||||
func Test_NewFrom(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
components []Component
|
||||
want *ExtRawRepoCfg
|
||||
}{
|
||||
{
|
||||
name: "WithNestedVars",
|
||||
components: []Component{
|
||||
{
|
||||
Path: "test",
|
||||
VarFiles: []string{"test/vars/dev.tfvars", "test/vars/nested/stg.tfvars"},
|
||||
},
|
||||
},
|
||||
want: &ExtRawRepoCfg{
|
||||
RepoCfg: raw.RepoCfg{
|
||||
Version: ptr(3),
|
||||
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-stg"),
|
||||
Dir: ptr("test"),
|
||||
Workflow: ptr("test-stg"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
got, _ := NewRepoCfg(tc.components, Options{})
|
||||
if !cmp.Equal(got, tc.want) {
|
||||
t.Errorf(`NewFrom()
|
||||
diff %s`, cmp.Diff(got, tc.want))
|
||||
}
|
||||
}
|
||||
}
|
||||
68
repocfg/util.go
Normal file
68
repocfg/util.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package repocfg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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
|
||||
func friendlyName(path, environment string) string {
|
||||
name := []string{strings.ReplaceAll(path, "/", "-"), pathWithoutExtension(filepath.Base(environment))}
|
||||
return strings.TrimSuffix(strings.Join(name, "-"), "-")
|
||||
}
|
||||
|
||||
// pathWithoutExtension removes the file extension from a base path.
|
||||
// if the path has no extension ("."), it returns an empty 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
|
||||
}
|
||||
73
repocfg/util_test.go
Normal file
73
repocfg/util_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package repocfg
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// Tests the ptr helper function. Given a generic value it should return a
|
||||
// pointer to that value.
|
||||
func Test_Ptr(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
intV := 3
|
||||
strV := "test"
|
||||
boolV := true
|
||||
|
||||
for _, T := range []any{intV, strV, boolV} {
|
||||
got := ptr(T)
|
||||
if *got != T {
|
||||
t.Errorf(`ptr()
|
||||
got %v
|
||||
want %v
|
||||
diff %s`, got, T, cmp.Diff(got, 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.
|
||||
func Test_FriendlyName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := "my/path/to/some/terraform/component"
|
||||
environment := "my/path/to/some/terraform/component/dev.tfvars"
|
||||
|
||||
want := "my-path-to-some-terraform-component-dev"
|
||||
got := friendlyName(path, environment)
|
||||
if got != want {
|
||||
t.Errorf(`friendlyName()
|
||||
got %v
|
||||
want %v
|
||||
diff %s`, got, want, cmp.Diff(got, want))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_PathWithoutExtension(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
path: "dev.tfvars",
|
||||
want: "dev",
|
||||
},
|
||||
{
|
||||
path: "dev",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
got := pathWithoutExtension(tc.path)
|
||||
if got != tc.want {
|
||||
t.Errorf(`pathWithoutExtension()
|
||||
got %v
|
||||
want %v
|
||||
diff %s`, got, tc.want, cmp.Diff(got, tc.want))
|
||||
}
|
||||
}
|
||||
}
|
||||
175
repocfg/workflow.go
Normal file
175
repocfg/workflow.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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
|
||||
}
|
||||
96
repocfg/workflow_test.go
Normal file
96
repocfg/workflow_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user