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 }