rm: workflows

This commit is contained in:
Beau
2024-03-20 13:48:16 +00:00
parent f3d3731057
commit 6c539fd28f
13 changed files with 141 additions and 432 deletions

View File

@@ -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),
},
}

View File

@@ -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{

View File

@@ -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
}

View File

@@ -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"),
},
},
},

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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))
}
}
}