From 20e0cc31400abf5f3ca16894161c1505a78952b4 Mon Sep 17 00:00:00 2001 From: Andrew Borg Date: Sat, 14 Dec 2024 12:08:44 -0500 Subject: [PATCH] Support project-level Terraform distribution selection Add support for terraform_distribution config value in project config. This config value behaves similarly to terraform_version whereby defaults taken from the server config will be overridden by project-level values. Also refactor to prevent terraform client slightly to prevent cyclical dependencies. Signed-off-by: Andrew Borg --- cmd/server.go | 66 ++++++++----- cmd/server_test.go | 1 + .../events/events_controller_e2e_test.go | 17 ++-- server/core/config/parser_validator_test.go | 25 +++++ server/core/config/raw/project.go | 13 +++ server/core/config/valid/global_cfg.go | 3 + server/core/config/valid/repo_cfg.go | 1 + server/core/runtime/apply_step_runner.go | 25 +++-- server/core/runtime/apply_step_runner_test.go | 41 +++++--- server/core/runtime/env_step_runner_test.go | 7 +- server/core/runtime/import_step_runner.go | 21 ++-- .../core/runtime/import_step_runner_test.go | 26 +++-- server/core/runtime/init_step_runner.go | 13 ++- server/core/runtime/init_step_runner_test.go | 97 +++++++++++-------- .../core/runtime/mocks/mock_async_tfexec.go | 31 +++--- .../runtime/models/shell_command_runner.go | 2 +- .../core/runtime/multienv_step_runner_test.go | 9 +- server/core/runtime/plan_step_runner.go | 38 +++++--- server/core/runtime/plan_step_runner_test.go | 58 +++++++---- .../core/runtime/policy_check_step_runner.go | 3 +- .../runtime/post_workflow_hook_runner_test.go | 7 +- .../runtime/pre_workflow_hook_runner_test.go | 7 +- server/core/runtime/run_step_runner.go | 55 ++++++----- server/core/runtime/run_step_runner_test.go | 12 ++- server/core/runtime/runtime.go | 7 +- server/core/runtime/show_step_runner.go | 18 +++- server/core/runtime/show_step_runner_test.go | 19 ++-- server/core/runtime/state_rm_step_runner.go | 21 ++-- .../core/runtime/state_rm_step_runner_test.go | 26 +++-- server/core/runtime/version_step_runner.go | 12 ++- .../core/runtime/version_step_runner_test.go | 13 ++- .../runtime/workspace_step_runner_delegate.go | 31 +++--- .../workspace_step_runner_delegate_test.go | 45 ++++++--- .../{events => core}/terraform/ansi/strip.go | 0 .../terraform/ansi/strip_test.go | 0 server/core/terraform/distribution.go | 8 ++ .../mocks/mock_terraform_client.go | 59 ++++++----- .../{ => tfclient}/terraform_client.go | 47 +++++---- .../terraform_client_internal_test.go | 32 ++++-- .../{ => tfclient}/terraform_client_test.go | 55 +++++------ server/events/command/project_context.go | 4 + server/events/command/scope_tags.go | 13 +-- server/events/mock_workingdir_test.go | 5 +- server/events/project_command_builder.go | 8 +- .../project_command_builder_internal_test.go | 12 +-- server/events/project_command_builder_test.go | 28 +++--- .../events/project_command_context_builder.go | 11 ++- .../project_command_context_builder_test.go | 4 +- server/events/project_command_runner_test.go | 6 +- server/server.go | 48 ++++----- server/user_config.go | 48 ++++----- 51 files changed, 728 insertions(+), 430 deletions(-) rename server/{events => core}/terraform/ansi/strip.go (100%) rename server/{events => core}/terraform/ansi/strip_test.go (100%) rename server/core/terraform/{ => tfclient}/mocks/mock_terraform_client.go (79%) rename server/core/terraform/{ => tfclient}/terraform_client.go (91%) rename server/core/terraform/{ => tfclient}/terraform_client_internal_test.go (88%) rename server/core/terraform/{ => tfclient}/terraform_client_test.go (86%) diff --git a/cmd/server.go b/cmd/server.go index 5722b38cfa..895e3b84b9 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -72,6 +72,7 @@ const ( CheckoutStrategyFlag = "checkout-strategy" ConfigFlag = "config" DataDirFlag = "data-dir" + DefaultTFDistributionFlag = "default-tf-distribution" DefaultTFVersionFlag = "default-tf-version" DisableApplyAllFlag = "disable-apply-all" DisableAutoplanFlag = "disable-autoplan" @@ -141,21 +142,22 @@ const ( SSLCertFileFlag = "ssl-cert-file" SSLKeyFileFlag = "ssl-key-file" RestrictFileList = "restrict-file-list" - TFDistributionFlag = "tf-distribution" - TFDownloadFlag = "tf-download" - TFDownloadURLFlag = "tf-download-url" - UseTFPluginCache = "use-tf-plugin-cache" - VarFileAllowlistFlag = "var-file-allowlist" - VCSStatusName = "vcs-status-name" - IgnoreVCSStatusNames = "ignore-vcs-status-names" - TFEHostnameFlag = "tfe-hostname" - TFELocalExecutionModeFlag = "tfe-local-execution-mode" - TFETokenFlag = "tfe-token" - WriteGitCredsFlag = "write-git-creds" // nolint: gosec - WebBasicAuthFlag = "web-basic-auth" - WebUsernameFlag = "web-username" - WebPasswordFlag = "web-password" - WebsocketCheckOrigin = "websocket-check-origin" + // TFDistributionFlag is deprecated for DefaultTFDistributionFlag + TFDistributionFlag = "tf-distribution" + TFDownloadFlag = "tf-download" + TFDownloadURLFlag = "tf-download-url" + UseTFPluginCache = "use-tf-plugin-cache" + VarFileAllowlistFlag = "var-file-allowlist" + VCSStatusName = "vcs-status-name" + IgnoreVCSStatusNames = "ignore-vcs-status-names" + TFEHostnameFlag = "tfe-hostname" + TFELocalExecutionModeFlag = "tfe-local-execution-mode" + TFETokenFlag = "tfe-token" + WriteGitCredsFlag = "write-git-creds" // nolint: gosec + WebBasicAuthFlag = "web-basic-auth" + WebUsernameFlag = "web-username" + WebPasswordFlag = "web-password" + WebsocketCheckOrigin = "websocket-check-origin" // NOTE: Must manually set these as defaults in the setDefaults function. DefaultADBasicUser = "" @@ -421,8 +423,8 @@ var stringFlags = map[string]stringFlag{ description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag), }, TFDistributionFlag: { - description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu), - defaultValue: DefaultTFDistribution, + description: "[Deprecated for --default-tf-distribution].", + hidden: true, }, TFDownloadURLFlag: { description: "Base URL to download Terraform versions from.", @@ -437,6 +439,10 @@ var stringFlags = map[string]stringFlag{ " Only set if using TFC/E as a remote backend." + " Should be specified via the ATLANTIS_TFE_TOKEN environment variable for security.", }, + DefaultTFDistributionFlag: { + description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu), + defaultValue: DefaultTFDistribution, + }, DefaultTFVersionFlag: { description: "Terraform version to default to (ex. v0.12.0). Will download if not yet on disk." + " If not set, Atlantis uses the terraform binary in its PATH.", @@ -840,12 +846,13 @@ func (s *ServerCmd) run() error { // Config looks good. Start the server. server, err := s.ServerCreator.NewServer(userConfig, server.Config{ - AllowForkPRsFlag: AllowForkPRsFlag, - AtlantisURLFlag: AtlantisURLFlag, - AtlantisVersion: s.AtlantisVersion, - DefaultTFVersionFlag: DefaultTFVersionFlag, - RepoConfigJSONFlag: RepoConfigJSONFlag, - SilenceForkPRErrorsFlag: SilenceForkPRErrorsFlag, + AllowForkPRsFlag: AllowForkPRsFlag, + AtlantisURLFlag: AtlantisURLFlag, + AtlantisVersion: s.AtlantisVersion, + DefaultTFDistributionFlag: DefaultTFDistributionFlag, + DefaultTFVersionFlag: DefaultTFVersionFlag, + RepoConfigJSONFlag: RepoConfigJSONFlag, + SilenceForkPRErrorsFlag: SilenceForkPRErrorsFlag, }) if err != nil { @@ -921,8 +928,11 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig, v *viper.Viper) { if c.RedisPort == 0 { c.RedisPort = DefaultRedisPort } - if c.TFDistribution == "" { - c.TFDistribution = DefaultTFDistribution + if c.TFDistribution != "" && c.DefaultTFDistribution == "" { + c.DefaultTFDistribution = c.TFDistribution + } + if c.DefaultTFDistribution == "" { + c.DefaultTFDistribution = DefaultTFDistribution } if c.TFDownloadURL == "" { c.TFDownloadURL = DefaultTFDownloadURL @@ -953,7 +963,7 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { return fmt.Errorf("invalid log level: must be one of %v", ValidLogLevels) } - if userConfig.TFDistribution != TFDistributionTerraform && userConfig.TFDistribution != TFDistributionOpenTofu { + if userConfig.DefaultTFDistribution != TFDistributionTerraform && userConfig.DefaultTFDistribution != TFDistributionOpenTofu { return fmt.Errorf("invalid tf distribution: expected one of %s or %s", TFDistributionTerraform, TFDistributionOpenTofu) } @@ -1172,6 +1182,10 @@ func (s *ServerCmd) deprecationWarnings(userConfig *server.UserConfig) error { // } // + if userConfig.TFDistribution != "" { + deprecatedFlags = append(deprecatedFlags, TFDistributionFlag) + } + if len(deprecatedFlags) > 0 { warning := "WARNING: " if len(deprecatedFlags) == 1 { diff --git a/cmd/server_test.go b/cmd/server_test.go index c14e43cdd6..af6c5eb136 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -73,6 +73,7 @@ var testFlags = map[string]interface{}{ CheckoutStrategyFlag: CheckoutStrategyMerge, CheckoutDepthFlag: 0, DataDirFlag: "/path", + DefaultTFDistributionFlag: "terraform", DefaultTFVersionFlag: "v0.11.0", DisableApplyAllFlag: true, DisableMarkdownFoldingFlag: true, diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 4f456b8a2f..0f5e87b443 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -29,6 +29,7 @@ import ( mock_policy "github.com/runatlantis/atlantis/server/core/runtime/policy/mocks" "github.com/runatlantis/atlantis/server/core/terraform" terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/mocks" @@ -1319,7 +1320,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers mockDownloader := terraform_mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - terraformClient, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler) + terraformClient, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler) Ok(t, err) boltdb, err := db.New(dataDir) Ok(t, err) @@ -1346,6 +1347,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers } } + defaultTFDistribution := terraformClient.DefaultDistribution() defaultTFVersion := terraformClient.DefaultVersion() locker := events.NewDefaultWorkingDirLocker() parser := &config.ParserValidator{} @@ -1429,7 +1431,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers terraformClient, ) - showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFVersion) + showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion) Ok(t, err) @@ -1440,6 +1442,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers conftextExec.VersionCache = &LocalConftestCache{} policyCheckRunner, err := runtime.NewPolicyCheckStepRunner( + defaultTFDistribution, defaultTFVersion, conftextExec, ) @@ -1451,11 +1454,13 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers Locker: projectLocker, LockURLGenerator: &mockLockURLGenerator{}, InitStepRunner: &runtime.InitStepRunner{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTFVersion, + TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTFDistribution, + DefaultTFVersion: defaultTFVersion, }, PlanStepRunner: runtime.NewPlanStepRunner( terraformClient, + defaultTFDistribution, defaultTFVersion, statusUpdater, asyncTfExec, @@ -1465,8 +1470,8 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers ApplyStepRunner: &runtime.ApplyStepRunner{ TerraformExecutor: terraformClient, }, - ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFVersion), - StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFVersion), + ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion), + StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion), RunStepRunner: &runtime.RunStepRunner{ TerraformExecutor: terraformClient, DefaultTFVersion: defaultTFVersion, diff --git a/server/core/config/parser_validator_test.go b/server/core/config/parser_validator_test.go index c21187bc47..05299aa725 100644 --- a/server/core/config/parser_validator_test.go +++ b/server/core/config/parser_validator_test.go @@ -610,6 +610,31 @@ workflows: }, }, }, + { + description: "project field with terraform_distribution set to opentofu", + input: ` +version: 3 +projects: +- dir: . + workspace: myworkspace + terraform_distribution: opentofu +`, + exp: valid.RepoCfg{ + Version: 3, + Projects: []valid.Project{ + { + Dir: ".", + Workspace: "myworkspace", + TerraformDistribution: String("opentofu"), + Autoplan: valid.Autoplan{ + WhenModified: raw.DefaultAutoPlanWhenModified, + Enabled: true, + }, + }, + }, + Workflows: make(map[string]valid.Workflow), + }, + }, { description: "project dir with ..", input: ` diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go index fe0e656a8c..4fbb2811cc 100644 --- a/server/core/config/raw/project.go +++ b/server/core/config/raw/project.go @@ -26,6 +26,7 @@ type Project struct { Dir *string `yaml:"dir,omitempty"` Workspace *string `yaml:"workspace,omitempty"` Workflow *string `yaml:"workflow,omitempty"` + TerraformDistribution *string `yaml:"terraform_distribution,omitempty"` TerraformVersion *string `yaml:"terraform_version,omitempty"` Autoplan *Autoplan `yaml:"autoplan,omitempty"` PlanRequirements []string `yaml:"plan_requirements,omitempty"` @@ -86,6 +87,7 @@ func (p Project) Validate() error { validation.Field(&p.PlanRequirements, validation.By(validPlanReq)), validation.Field(&p.ApplyRequirements, validation.By(validApplyReq)), validation.Field(&p.ImportRequirements, validation.By(validImportReq)), + validation.Field(&p.TerraformDistribution, validation.By(validDistribution)), validation.Field(&p.TerraformVersion, validation.By(VersionValidator)), validation.Field(&p.DependsOn, validation.By(DependsOn)), validation.Field(&p.Name, validation.By(validName)), @@ -118,6 +120,9 @@ func (p Project) ToValid() valid.Project { if p.TerraformVersion != nil { v.TerraformVersion, _ = version.NewVersion(*p.TerraformVersion) } + if p.TerraformDistribution != nil { + v.TerraformDistribution = p.TerraformDistribution + } if p.Autoplan == nil { v.Autoplan = DefaultAutoPlan() } else { @@ -202,3 +207,11 @@ func validImportReq(value interface{}) error { } return nil } + +func validDistribution(value interface{}) error { + distribution := value.(*string) + if distribution != nil && *distribution != "terraform" && *distribution != "opentofu" { + return fmt.Errorf("%q is not a valid terraform_distribution, only %q and %q are supported", *distribution, "terraform", "opentofu") + } + return nil +} diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index a930ef22bc..c472311bfe 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -105,6 +105,7 @@ type MergedProjectCfg struct { AutoplanEnabled bool AutoMergeDisabled bool AutoMergeMethod string + TerraformDistribution *string TerraformVersion *version.Version RepoCfgVersion int PolicySets PolicySets @@ -412,6 +413,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro DependsOn: proj.DependsOn, Name: proj.GetName(), AutoplanEnabled: proj.Autoplan.Enabled, + TerraformDistribution: proj.TerraformDistribution, TerraformVersion: proj.TerraformVersion, RepoCfgVersion: rCfg.Version, PolicySets: g.PolicySets, @@ -438,6 +440,7 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo Workspace: workspace, Name: "", AutoplanEnabled: DefaultAutoPlanEnabled, + TerraformDistribution: nil, TerraformVersion: nil, PolicySets: g.PolicySets, DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge, diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index e42e60158b..6096c66b14 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -147,6 +147,7 @@ type Project struct { Workspace string Name *string WorkflowName *string + TerraformDistribution *string TerraformVersion *version.Version Autoplan Autoplan PlanRequirements []string diff --git a/server/core/runtime/apply_step_runner.go b/server/core/runtime/apply_step_runner.go index 2e223f2996..35a864cfc8 100644 --- a/server/core/runtime/apply_step_runner.go +++ b/server/core/runtime/apply_step_runner.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" version "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/utils" @@ -17,10 +18,11 @@ import ( // ApplyStepRunner runs `terraform apply`. type ApplyStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version - CommitStatusUpdater StatusUpdater - AsyncTFExec AsyncTFExec + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version + CommitStatusUpdater StatusUpdater + AsyncTFExec AsyncTFExec } func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { @@ -39,11 +41,19 @@ func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pa ctx.Log.Info("starting apply") var out string + tfDistribution := a.DefaultTFDistribution + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } + tfVersion := a.DefaultTFVersion + if ctx.TerraformVersion != nil { + tfVersion = ctx.TerraformVersion + } // TODO: Leverage PlanTypeStepRunnerDelegate here if IsRemotePlan(contents) { args := append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...) - out, err = a.runRemoteApply(ctx, args, path, planPath, ctx.TerraformVersion, envs) + out, err = a.runRemoteApply(ctx, args, path, planPath, tfDistribution, tfVersion, envs) if err == nil { out = a.cleanRemoteApplyOutput(out) } @@ -51,7 +61,7 @@ func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pa // NOTE: we need to quote the plan path because Bitbucket Server can // have spaces in its repo owner names which is part of the path. args := append(append(append([]string{"apply", "-input=false"}, extraArgs...), ctx.EscapedCommentArgs...), fmt.Sprintf("%q", planPath)) - out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, ctx.TerraformVersion, ctx.Workspace) + out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, tfDistribution, tfVersion, ctx.Workspace) } // If the apply was successful, delete the plan. @@ -115,6 +125,7 @@ func (a *ApplyStepRunner) runRemoteApply( applyArgs []string, path string, absPlanPath string, + tfDistribution terraform.Distribution, tfVersion *version.Version, envs map[string]string) (string, error) { // The planfile contents are needed to ensure that the plan didn't change @@ -133,7 +144,7 @@ func (a *ApplyStepRunner) runRemoteApply( // Start the async command execution. ctx.Log.Debug("starting async tf remote operation") - inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfVersion, ctx.Workspace) + inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfDistribution, tfVersion, ctx.Workspace) var lines []string nextLineIsRunURL := false var runURL string diff --git a/server/core/runtime/apply_step_runner_test.go b/server/core/runtime/apply_step_runner_test.go index 2a31040c81..b3bb99545c 100644 --- a/server/core/runtime/apply_step_runner_test.go +++ b/server/core/runtime/apply_step_runner_test.go @@ -14,7 +14,9 @@ import ( "github.com/runatlantis/atlantis/server/core/runtime" runtimemocks "github.com/runatlantis/atlantis/server/core/runtime/mocks" runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -59,17 +61,20 @@ func TestRun_Success(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) o := runtime.ApplyStepRunner{ - TerraformExecutor: terraform, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, nil, "workspace") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -91,17 +96,19 @@ func TestRun_AppliesCorrectProjectPlan(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) o := runtime.ApplyStepRunner{ - TerraformExecutor: terraform, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, } - - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, nil, "default") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -123,17 +130,19 @@ func TestRun_UsesConfiguredTFVersion(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) o := runtime.ApplyStepRunner{ - TerraformExecutor: terraform, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, } - - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, tfVersion, "workspace") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -197,7 +206,7 @@ func TestRun_UsingTarget(t *testing.T) { planPath := filepath.Join(tmpDir, "workspace.tfplan") err := os.WriteFile(planPath, nil, 0600) Ok(t, err) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() step := runtime.ApplyStepRunner{ TerraformExecutor: terraform, } @@ -361,7 +370,7 @@ type remoteApplyMock struct { } // RunCommandAsync fakes out running terraform async. -func (r *remoteApplyMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) { +func (r *remoteApplyMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ tf.Distribution, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) { r.CalledArgs = args in := make(chan string) diff --git a/server/core/runtime/env_step_runner_test.go b/server/core/runtime/env_step_runner_test.go index 0fe86f77f0..7772d56c5f 100644 --- a/server/core/runtime/env_step_runner_test.go +++ b/server/core/runtime/env_step_runner_test.go @@ -5,7 +5,9 @@ import ( "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/core/runtime" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" @@ -38,12 +40,15 @@ func TestEnvStepRunner_Run(t *testing.T) { }, } RegisterMockTestingT(t) - tfClient := mocks.NewMockClient() + tfClient := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, err := version.NewVersion("0.12.0") Ok(t, err) projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() runStepRunner := runtime.RunStepRunner{ TerraformExecutor: tfClient, + DefaultTFDistribution: tfDistribution, DefaultTFVersion: tfVersion, ProjectCmdOutputHandler: projectCmdOutputHandler, } diff --git a/server/core/runtime/import_step_runner.go b/server/core/runtime/import_step_runner.go index 0d5787a8ad..7f3a22b9b4 100644 --- a/server/core/runtime/import_step_runner.go +++ b/server/core/runtime/import_step_runner.go @@ -5,25 +5,32 @@ import ( "path/filepath" version "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/utils" ) type importStepRunner struct { - terraformExecutor TerraformExec - defaultTFVersion *version.Version + terraformExecutor TerraformExec + defaultTFDistribution terraform.Distribution + defaultTFVersion *version.Version } -func NewImportStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version) Runner { +func NewImportStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version) Runner { runner := &importStepRunner{ - terraformExecutor: terraformExecutor, - defaultTFVersion: defaultTfVersion, + terraformExecutor: terraformExecutor, + defaultTFDistribution: defaultTfDistribution, + defaultTFVersion: defaultTfVersion, } - return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner) + return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner) } func (p *importStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfDistribution := p.defaultTFDistribution tfVersion := p.defaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } @@ -31,7 +38,7 @@ func (p *importStepRunner) Run(ctx command.ProjectContext, extraArgs []string, p importCmd := []string{"import"} importCmd = append(importCmd, extraArgs...) importCmd = append(importCmd, ctx.EscapedCommentArgs...) - out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), importCmd, envs, tfVersion, ctx.Workspace) + out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), importCmd, envs, tfDistribution, tfVersion, ctx.Workspace) // If the import was successful and a plan file exists, delete the plan. planPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)) diff --git a/server/core/runtime/import_step_runner_test.go b/server/core/runtime/import_step_runner_test.go index b10f182de9..21c8d054d1 100644 --- a/server/core/runtime/import_step_runner_test.go +++ b/server/core/runtime/import_step_runner_test.go @@ -8,7 +8,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -29,17 +31,19 @@ func TestImportStepRunner_Run_Success(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.15.0") - s := NewImportStepRunner(terraform, tfVersion) + s := NewImportStepRunner(terraform, tfDistribution, tfVersion) - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) commands := []string{"import", "-var", "foo=bar", "addr", "id"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace) _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -59,23 +63,25 @@ func TestImportStepRunner_Run_Workspace(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() tfVersion, _ := version.NewVersion("0.15.0") - s := NewImportStepRunner(terraform, tfVersion) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + s := NewImportStepRunner(terraform, tfDistribution, tfVersion) - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) // switch workspace - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfVersion, workspace) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfDistribution, tfVersion, workspace) // exec import commands := []string{"import", "-var", "foo=bar", "addr", "id"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace) _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") diff --git a/server/core/runtime/init_step_runner.go b/server/core/runtime/init_step_runner.go index 0c6de1b013..c8da3ffa48 100644 --- a/server/core/runtime/init_step_runner.go +++ b/server/core/runtime/init_step_runner.go @@ -5,14 +5,16 @@ import ( version "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/core/runtime/common" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/utils" ) // InitStep runs `terraform init`. type InitStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version } func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { @@ -33,6 +35,11 @@ func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pat } } + tfDistribution := i.DefaultTFDistribution + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } + tfVersion := i.DefaultTFVersion if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion @@ -56,7 +63,7 @@ func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pat terraformInitCmd := append(terraformInitVerb, finalArgs...) - out, err := i.TerraformExecutor.RunCommandWithVersion(ctx, path, terraformInitCmd, envs, tfVersion, ctx.Workspace) + out, err := i.TerraformExecutor.RunCommandWithVersion(ctx, path, terraformInitCmd, envs, tfDistribution, tfVersion, ctx.Workspace) // Only include the init output if there was an error. Otherwise it's // unnecessary and lengthens the comment. if err != nil { diff --git a/server/core/runtime/init_step_runner_test.go b/server/core/runtime/init_step_runner_test.go index 45927591a6..989ca6a169 100644 --- a/server/core/runtime/init_step_runner_test.go +++ b/server/core/runtime/init_step_runner_test.go @@ -12,7 +12,9 @@ import ( "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/runtime" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -20,6 +22,8 @@ import ( func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { RegisterMockTestingT(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) cases := []struct { version string expCmd string @@ -44,7 +48,7 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { for _, c := range cases { t.Run(c.version, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -55,10 +59,11 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { tfVersion, _ := version.NewVersion(c.version) iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) @@ -71,7 +76,7 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { if c.expCmd == "get" { expArgs = []string{c.expCmd, "-upgrade", "extra", "args"} } - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") }) } } @@ -79,15 +84,17 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { func TestRun_ShowInitOutputOnError(t *testing.T) { // If there was an error during init then we want the output to be returned. RegisterMockTestingT(t) - tfClient := mocks.NewMockClient() + tfClient := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) - When(tfClient.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(tfClient.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", errors.New("error")) - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.11.0") iso := runtime.InitStepRunner{ - TerraformExecutor: tfClient, - DefaultTFVersion: tfVersion, + TerraformExecutor: tfClient, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } output, err := iso.Run(command.ProjectContext{ @@ -118,14 +125,16 @@ func TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() - + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.14.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, []string{"extra", "args"}, repoDir, map[string]string(nil)) @@ -134,27 +143,29 @@ func TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) { Equals(t, "", output) expectedArgs := []string{"init", "-input=false", "extra", "args"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") } func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) { tmpDir := t.TempDir() RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ Workspace: "workspace", RepoRelDir: ".", Log: logger, } - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.14.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) @@ -163,7 +174,7 @@ func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) { Equals(t, "", output) expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") } func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing.T) { @@ -173,7 +184,7 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -181,13 +192,15 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing RepoRelDir: ".", Log: logger, } - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.13.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) @@ -196,7 +209,7 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing Equals(t, "", output) expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") } func TestRun_InitExtraArgsDeDupe(t *testing.T) { @@ -240,7 +253,7 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) { for _, c := range cases { t.Run(c.description, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -248,13 +261,15 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) { RepoRelDir: ".", Log: logger, } - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := iso.Run(ctx, c.extraArgs, "/path", map[string]string(nil)) @@ -262,7 +277,7 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) { // When there is no error, should not return init output to PR. Equals(t, "", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", c.expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", c.expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") }) } } @@ -276,17 +291,19 @@ func TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() logger := logging.NewNoopLogger(t) - + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.14.0") iso := runtime.InitStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) ctx := command.ProjectContext{ @@ -300,7 +317,7 @@ func TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) { Equals(t, "", output) expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") } func runCmd(t *testing.T, dir string, name string, args ...string) string { diff --git a/server/core/runtime/mocks/mock_async_tfexec.go b/server/core/runtime/mocks/mock_async_tfexec.go index 662571ed0b..453c80012d 100644 --- a/server/core/runtime/mocks/mock_async_tfexec.go +++ b/server/core/runtime/mocks/mock_async_tfexec.go @@ -7,6 +7,7 @@ import ( go_version "github.com/hashicorp/go-version" pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/core/runtime/models" + terraform "github.com/runatlantis/atlantis/server/core/terraform" command "github.com/runatlantis/atlantis/server/events/command" "reflect" "time" @@ -27,11 +28,11 @@ func NewMockAsyncTFExec(options ...pegomock.Option) *MockAsyncTFExec { func (mock *MockAsyncTFExec) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } func (mock *MockAsyncTFExec) FailHandler() pegomock.FailHandler { return mock.fail } -func (mock *MockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (chan<- string, <-chan models.Line) { +func (mock *MockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) (chan<- string, <-chan models.Line) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockAsyncTFExec().") } - _params := []pegomock.Param{ctx, path, args, envs, v, workspace} + _params := []pegomock.Param{ctx, path, args, envs, d, v, workspace} _result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandAsync", _params, []reflect.Type{reflect.TypeOf((*chan<- string)(nil)).Elem(), reflect.TypeOf((*<-chan models.Line)(nil)).Elem()}) var _ret0 chan<- string var _ret1 <-chan models.Line @@ -91,8 +92,8 @@ type VerifierMockAsyncTFExec struct { timeout time.Duration } -func (verifier *VerifierMockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockAsyncTFExec_RunCommandAsync_OngoingVerification { - _params := []pegomock.Param{ctx, path, args, envs, v, workspace} +func (verifier *VerifierMockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) *MockAsyncTFExec_RunCommandAsync_OngoingVerification { + _params := []pegomock.Param{ctx, path, args, envs, d, v, workspace} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandAsync", _params, verifier.timeout) return &MockAsyncTFExec_RunCommandAsync_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -102,12 +103,12 @@ type MockAsyncTFExec_RunCommandAsync_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, *go_version.Version, string) { - ctx, path, args, envs, v, workspace := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], v[len(v)-1], workspace[len(workspace)-1] +func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, terraform.Distribution, *go_version.Version, string) { + ctx, path, args, envs, d, v, workspace := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], d[len(d)-1], v[len(v)-1], workspace[len(workspace)-1] } -func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []*go_version.Version, _param5 []string) { +func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []terraform.Distribution, _param5 []*go_version.Version, _param6 []string) { _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(_params) > 0 { if len(_params) > 0 { @@ -135,15 +136,21 @@ func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArgu } } if len(_params) > 4 { - _param4 = make([]*go_version.Version, len(c.methodInvocations)) + _param4 = make([]terraform.Distribution, len(c.methodInvocations)) for u, param := range _params[4] { - _param4[u] = param.(*go_version.Version) + _param4[u] = param.(terraform.Distribution) } } if len(_params) > 5 { - _param5 = make([]string, len(c.methodInvocations)) + _param5 = make([]*go_version.Version, len(c.methodInvocations)) for u, param := range _params[5] { - _param5[u] = param.(string) + _param5[u] = param.(*go_version.Version) + } + } + if len(_params) > 6 { + _param6 = make([]string, len(c.methodInvocations)) + for u, param := range _params[6] { + _param6[u] = param.(string) } } } diff --git a/server/core/runtime/models/shell_command_runner.go b/server/core/runtime/models/shell_command_runner.go index 50b9f7760f..cd613bf450 100644 --- a/server/core/runtime/models/shell_command_runner.go +++ b/server/core/runtime/models/shell_command_runner.go @@ -10,8 +10,8 @@ import ( "github.com/pkg/errors" "github.com/runatlantis/atlantis/server/core/config/valid" + "github.com/runatlantis/atlantis/server/core/terraform/ansi" "github.com/runatlantis/atlantis/server/events/command" - "github.com/runatlantis/atlantis/server/events/terraform/ansi" "github.com/runatlantis/atlantis/server/jobs" ) diff --git a/server/core/runtime/multienv_step_runner_test.go b/server/core/runtime/multienv_step_runner_test.go index 360adce3f5..326307fdea 100644 --- a/server/core/runtime/multienv_step_runner_test.go +++ b/server/core/runtime/multienv_step_runner_test.go @@ -7,7 +7,9 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/runtime" - "github.com/runatlantis/atlantis/server/core/terraform/mocks" + "github.com/runatlantis/atlantis/server/core/terraform" + terraformmocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" @@ -45,12 +47,15 @@ func TestMultiEnvStepRunner_Run(t *testing.T) { }, } RegisterMockTestingT(t) - tfClient := mocks.NewMockClient() + tfClient := tfclientmocks.NewMockClient() + mockDownloader := terraformmocks.NewMockDownloader() + tfDistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, err := version.NewVersion("0.12.0") Ok(t, err) projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() runStepRunner := runtime.RunStepRunner{ TerraformExecutor: tfClient, + DefaultTFDistribution: tfDistribution, DefaultTFVersion: tfVersion, ProjectCmdOutputHandler: projectCmdOutputHandler, } diff --git a/server/core/runtime/plan_step_runner.go b/server/core/runtime/plan_step_runner.go index b1cb66c1e4..3d0060c194 100644 --- a/server/core/runtime/plan_step_runner.go +++ b/server/core/runtime/plan_step_runner.go @@ -9,6 +9,7 @@ import ( version "github.com/hashicorp/go-version" "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" ) @@ -26,34 +27,40 @@ var ( ) type planStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version - CommitStatusUpdater StatusUpdater - AsyncTFExec AsyncTFExec + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version + CommitStatusUpdater StatusUpdater + AsyncTFExec AsyncTFExec } -func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner { +func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner { runner := &planStepRunner{ - TerraformExecutor: terraformExecutor, - DefaultTFVersion: defaultTfVersion, - CommitStatusUpdater: commitStatusUpdater, - AsyncTFExec: asyncTFExec, + TerraformExecutor: terraformExecutor, + DefaultTFDistribution: defaultTfDistribution, + DefaultTFVersion: defaultTfVersion, + CommitStatusUpdater: commitStatusUpdater, + AsyncTFExec: asyncTFExec, } - return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner) + return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner) } func (p *planStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfDistribution := p.DefaultTFDistribution tfVersion := p.DefaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } planFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)) planCmd := p.buildPlanCmd(ctx, extraArgs, path, tfVersion, planFile) - output, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), planCmd, envs, tfVersion, ctx.Workspace) + output, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), planCmd, envs, tfDistribution, tfVersion, ctx.Workspace) if p.isRemoteOpsErr(output, err) { ctx.Log.Debug("detected that this project is using TFE remote ops") - return p.remotePlan(ctx, extraArgs, path, tfVersion, planFile, envs) + return p.remotePlan(ctx, extraArgs, path, tfDistribution, tfVersion, planFile, envs) } if err != nil { return output, err @@ -72,14 +79,14 @@ func (p *planStepRunner) isRemoteOpsErr(output string, err error) bool { // remotePlan runs a terraform plan command compatible with TFE remote // operations. -func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []string, path string, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) { +func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []string, path string, tfDistribution terraform.Distribution, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) { argList := [][]string{ {"plan", "-input=false", "-refresh", "-no-color"}, extraArgs, ctx.EscapedCommentArgs, } args := p.flatten(argList) - output, err := p.runRemotePlan(ctx, args, path, tfVersion, envs) + output, err := p.runRemotePlan(ctx, args, path, tfDistribution, tfVersion, envs) if err != nil { return output, err } @@ -193,6 +200,7 @@ func (p *planStepRunner) runRemotePlan( ctx command.ProjectContext, cmdArgs []string, path string, + tfDistribution terraform.Distribution, tfVersion *version.Version, envs map[string]string) (string, error) { @@ -205,7 +213,7 @@ func (p *planStepRunner) runRemotePlan( // Start the async command execution. ctx.Log.Debug("starting async tf remote operation") - _, outCh := p.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), cmdArgs, envs, tfVersion, ctx.Workspace) + _, outCh := p.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), cmdArgs, envs, tfDistribution, tfVersion, ctx.Workspace) var lines []string nextLineIsRunURL := false var runURL string diff --git a/server/core/runtime/plan_step_runner_test.go b/server/core/runtime/plan_step_runner_test.go index f05336637c..c6e0b0e497 100644 --- a/server/core/runtime/plan_step_runner_test.go +++ b/server/core/runtime/plan_step_runner_test.go @@ -13,7 +13,9 @@ import ( "github.com/runatlantis/atlantis/server/core/runtime" runtimemocks "github.com/runatlantis/atlantis/server/core/runtime/mocks" runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -24,7 +26,7 @@ import ( func TestRun_AddsEnvVarFile(t *testing.T) { // Test that if env/workspace.tfvars file exists we use -var-file option. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() @@ -36,10 +38,12 @@ func TestRun_AddsEnvVarFile(t *testing.T) { err = os.WriteFile(envVarsFile, nil, 0600) Ok(t, err) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) // Using version >= 0.10 here so we don't expect any env commands. tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger(t) - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) expPlanArgs := []string{"plan", "-input=false", @@ -78,14 +82,14 @@ func TestRun_AddsEnvVarFile(t *testing.T) { Name: "repo", }, } - When(terraform.RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil) + When(terraform.RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("output", nil) output, err := s.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil)) Ok(t, err) // Verify that env select was never called since we're in version >= 0.10 - terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, tmpDir, []string{"env", "select", "workspace"}, map[string]string(nil), tfVersion, "workspace") - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, tmpDir, []string{"env", "select", "workspace"}, map[string]string(nil), tfDistribution, tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") Equals(t, "output", output) } @@ -93,12 +97,14 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { // Test that if running for a project, uses a different path for the plan // file. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger(t) - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) ctx := command.ProjectContext{ Log: logger, Workspace: "default", @@ -115,7 +121,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { Name: "repo", }, } - When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil) + When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("workspace\n", nil) expPlanArgs := []string{"plan", "-input=false", @@ -137,7 +143,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) { "comment", "args", } - When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")).ThenReturn("output", nil) + When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default")).ThenReturn("output", nil) output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) @@ -173,16 +179,19 @@ Terraform will perform the following actions: - aws_security_group_rule.allow_all ` RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) When(terraform.RunCommandWithVersion( Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), + Any[tf.Distribution](), Any[*version.Version](), Any[string]())). Then(func(params []Param) ReturnValues { @@ -223,11 +232,13 @@ Terraform will perform the following actions: // Test that even if there's an error, we get the returned output. func TestRun_OutputOnErr(t *testing.T) { RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) expOutput := "expected output" expErrMsg := "error!" When(terraform.RunCommandWithVersion( @@ -235,6 +246,7 @@ func TestRun_OutputOnErr(t *testing.T) { Any[string](), Any[[]string](), Any[map[string]string](), + Any[tf.Distribution](), Any[*version.Version](), Any[string]())). Then(func(params []Param) ReturnValues { @@ -287,7 +299,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() asyncTfExec := runtimemocks.NewMockAsyncTFExec() When(terraform.RunCommandWithVersion( @@ -295,11 +307,14 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { Any[string](), Any[[]string](), Any[map[string]string](), + Any[tf.Distribution](), Any[*version.Version](), Any[string]())).ThenReturn("output", nil) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) ctx := command.ProjectContext{ Workspace: "default", RepoRelDir: ".", @@ -319,7 +334,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) { Ok(t, err) Equals(t, "output", output) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default") }) } @@ -385,11 +400,13 @@ locally at this time. }, } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() commitStatusUpdater := runtimemocks.NewMockStatusUpdater() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) asyncTf := &remotePlanMock{} - s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTf) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTf) absProjectPath := t.TempDir() // First, terraform workspace gets run. @@ -398,6 +415,7 @@ locally at this time. absProjectPath, []string{"workspace", "show"}, map[string]string(nil), + tfDistribution, tfVersion, "default")).ThenReturn("default\n", nil) @@ -438,7 +456,7 @@ locally at this time. planErr := errors.New("exit status 1: err") planOutput := "\n" + c.remoteOpsErr asyncTf.LinesToSend = remotePlanOutput - When(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfVersion, "default")). + When(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default")). ThenReturn(planOutput, planErr) output, err := s.Run(ctx, []string{"extra", "args"}, absProjectPath, map[string]string(nil)) @@ -543,7 +561,7 @@ type remotePlanMock struct { CalledArgs []string } -func (r *remotePlanMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) { +func (r *remotePlanMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ tf.Distribution, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) { r.CalledArgs = args in := make(chan string) out := make(chan runtimemodels.Line) diff --git a/server/core/runtime/policy_check_step_runner.go b/server/core/runtime/policy_check_step_runner.go index 98e4408bcb..2987875f18 100644 --- a/server/core/runtime/policy_check_step_runner.go +++ b/server/core/runtime/policy_check_step_runner.go @@ -3,6 +3,7 @@ package runtime import ( "github.com/hashicorp/go-version" "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" ) @@ -13,7 +14,7 @@ type policyCheckStepRunner struct { } // NewPolicyCheckStepRunner creates a new step runner from an executor workflow -func NewPolicyCheckStepRunner(defaultTfVersion *version.Version, executorWorkflow VersionedExecutorWorkflow) (Runner, error) { +func NewPolicyCheckStepRunner(defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, executorWorkflow VersionedExecutorWorkflow) (Runner, error) { policyCheckStepRunner := &policyCheckStepRunner{ versionEnsurer: executorWorkflow, executor: executorWorkflow, diff --git a/server/core/runtime/post_workflow_hook_runner_test.go b/server/core/runtime/post_workflow_hook_runner_test.go index bbf22b4394..353ba26fe3 100644 --- a/server/core/runtime/post_workflow_hook_runner_test.go +++ b/server/core/runtime/post_workflow_hook_runner_test.go @@ -8,7 +8,8 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/runtime" - "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tf "github.com/runatlantis/atlantis/server/core/terraform" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" "github.com/runatlantis/atlantis/server/logging" @@ -142,8 +143,8 @@ func TestPostWorkflowHookRunner_Run(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() - When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())). + terraform := tfclientmocks.NewMockClient() + When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())). ThenReturn(nil) logger := logging.NewNoopLogger(t) diff --git a/server/core/runtime/pre_workflow_hook_runner_test.go b/server/core/runtime/pre_workflow_hook_runner_test.go index ad8659cfa2..0bf1e637bc 100644 --- a/server/core/runtime/pre_workflow_hook_runner_test.go +++ b/server/core/runtime/pre_workflow_hook_runner_test.go @@ -9,7 +9,8 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/runtime" - "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tf "github.com/runatlantis/atlantis/server/core/terraform" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" "github.com/runatlantis/atlantis/server/logging" @@ -162,8 +163,8 @@ func TestPreWorkflowHookRunner_Run(t *testing.T) { Ok(t, err) RegisterMockTestingT(t) - terraform := mocks.NewMockClient() - When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())). + terraform := tfclientmocks.NewMockClient() + When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())). ThenReturn(nil) logger := logging.NewNoopLogger(t) diff --git a/server/core/runtime/run_step_runner.go b/server/core/runtime/run_step_runner.go index 76629ba460..20d55caee6 100644 --- a/server/core/runtime/run_step_runner.go +++ b/server/core/runtime/run_step_runner.go @@ -9,14 +9,16 @@ import ( "github.com/hashicorp/go-version" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/runtime/models" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/jobs" ) // RunStepRunner runs custom commands. type RunStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version // TerraformBinDir is the directory where Atlantis downloads Terraform binaries. TerraformBinDir string ProjectCmdOutputHandler jobs.ProjectCommandOutputHandler @@ -31,12 +33,16 @@ func (r *RunStepRunner) Run( streamOutput bool, postProcessOutput valid.PostProcessRunOutputOption, ) (string, error) { + tfDistribution := r.DefaultTFDistribution tfVersion := r.DefaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } - err := r.TerraformExecutor.EnsureVersion(ctx.Log, tfVersion) + err := r.TerraformExecutor.EnsureVersion(ctx.Log, tfDistribution, tfVersion) if err != nil { err = fmt.Errorf("%s: Downloading terraform Version %s", err, tfVersion.String()) ctx.Log.Debug("error: %s", err) @@ -45,27 +51,28 @@ func (r *RunStepRunner) Run( baseEnvVars := os.Environ() customEnvVars := map[string]string{ - "ATLANTIS_TERRAFORM_VERSION": tfVersion.String(), - "BASE_BRANCH_NAME": ctx.Pull.BaseBranch, - "BASE_REPO_NAME": ctx.BaseRepo.Name, - "BASE_REPO_OWNER": ctx.BaseRepo.Owner, - "COMMENT_ARGS": strings.Join(ctx.EscapedCommentArgs, ","), - "DIR": path, - "HEAD_BRANCH_NAME": ctx.Pull.HeadBranch, - "HEAD_COMMIT": ctx.Pull.HeadCommit, - "HEAD_REPO_NAME": ctx.HeadRepo.Name, - "HEAD_REPO_OWNER": ctx.HeadRepo.Owner, - "PATH": fmt.Sprintf("%s:%s", os.Getenv("PATH"), r.TerraformBinDir), - "PLANFILE": filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)), - "SHOWFILE": filepath.Join(path, ctx.GetShowResultFileName()), - "POLICYCHECKFILE": filepath.Join(path, ctx.GetPolicyCheckResultFileName()), - "PROJECT_NAME": ctx.ProjectName, - "PULL_AUTHOR": ctx.Pull.Author, - "PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num), - "PULL_URL": ctx.Pull.URL, - "REPO_REL_DIR": ctx.RepoRelDir, - "USER_NAME": ctx.User.Username, - "WORKSPACE": ctx.Workspace, + "ATLANTIS_TERRAFORM_DISTRIBUTION": tfDistribution.BinName(), + "ATLANTIS_TERRAFORM_VERSION": tfVersion.String(), + "BASE_BRANCH_NAME": ctx.Pull.BaseBranch, + "BASE_REPO_NAME": ctx.BaseRepo.Name, + "BASE_REPO_OWNER": ctx.BaseRepo.Owner, + "COMMENT_ARGS": strings.Join(ctx.EscapedCommentArgs, ","), + "DIR": path, + "HEAD_BRANCH_NAME": ctx.Pull.HeadBranch, + "HEAD_COMMIT": ctx.Pull.HeadCommit, + "HEAD_REPO_NAME": ctx.HeadRepo.Name, + "HEAD_REPO_OWNER": ctx.HeadRepo.Owner, + "PATH": fmt.Sprintf("%s:%s", os.Getenv("PATH"), r.TerraformBinDir), + "PLANFILE": filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)), + "SHOWFILE": filepath.Join(path, ctx.GetShowResultFileName()), + "POLICYCHECKFILE": filepath.Join(path, ctx.GetPolicyCheckResultFileName()), + "PROJECT_NAME": ctx.ProjectName, + "PULL_AUTHOR": ctx.Pull.Author, + "PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num), + "PULL_URL": ctx.Pull.URL, + "REPO_REL_DIR": ctx.RepoRelDir, + "USER_NAME": ctx.User.Username, + "WORKSPACE": ctx.Workspace, } finalEnvVars := baseEnvVars diff --git a/server/core/runtime/run_step_runner_test.go b/server/core/runtime/run_step_runner_test.go index 4672fa2bb0..a2a6fb8a28 100644 --- a/server/core/runtime/run_step_runner_test.go +++ b/server/core/runtime/run_step_runner_test.go @@ -10,7 +10,9 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/runtime" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" @@ -103,8 +105,9 @@ func TestRunStepRunner_Run(t *testing.T) { defaultVersion, _ := version.NewVersion("0.8") RegisterMockTestingT(t) - terraform := mocks.NewMockClient() - When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())). + terraform := tfclientmocks.NewMockClient() + defaultDistribution := tf.NewDistributionTerraformWithDownloader(mocks.NewMockDownloader()) + When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())). ThenReturn(nil) logger := logging.NewNoopLogger(t) @@ -113,6 +116,7 @@ func TestRunStepRunner_Run(t *testing.T) { r := runtime.RunStepRunner{ TerraformExecutor: terraform, + DefaultTFDistribution: defaultDistribution, DefaultTFVersion: defaultVersion, TerraformBinDir: "/bin/dir", ProjectCmdOutputHandler: projectCmdOutputHandler, @@ -157,8 +161,8 @@ func TestRunStepRunner_Run(t *testing.T) { expOut := strings.Replace(c.ExpOut, "$DIR", tmpDir, -1) Equals(t, expOut, out) - terraform.VerifyWasCalledOnce().EnsureVersion(logger, projVersion) - terraform.VerifyWasCalled(Never()).EnsureVersion(logger, defaultVersion) + terraform.VerifyWasCalledOnce().EnsureVersion(logger, defaultDistribution, projVersion) + terraform.VerifyWasCalled(Never()).EnsureVersion(logger, defaultDistribution, defaultVersion) }) } diff --git a/server/core/runtime/runtime.go b/server/core/runtime/runtime.go index 52fc5180eb..35e571262b 100644 --- a/server/core/runtime/runtime.go +++ b/server/core/runtime/runtime.go @@ -11,6 +11,7 @@ import ( version "github.com/hashicorp/go-version" "github.com/pkg/errors" runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -26,8 +27,8 @@ const ( // TerraformExec brings the interface from TerraformClient into this package // without causing circular imports. type TerraformExec interface { - RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error) - EnsureVersion(log logging.SimpleLogging, v *version.Version) error + RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error) + EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error } // AsyncTFExec brings the interface from TerraformClient into this package @@ -43,7 +44,7 @@ type AsyncTFExec interface { // Callers can use the input channel to pass stdin input to the command. // If any error is passed on the out channel, there will be no // further output (so callers are free to exit). - RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan runtimemodels.Line) + RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (chan<- string, <-chan runtimemodels.Line) } // StatusUpdater brings the interface from CommitStatusUpdater into this package diff --git a/server/core/runtime/show_step_runner.go b/server/core/runtime/show_step_runner.go index ba89479b56..ed346bc184 100644 --- a/server/core/runtime/show_step_runner.go +++ b/server/core/runtime/show_step_runner.go @@ -6,15 +6,17 @@ import ( "github.com/hashicorp/go-version" "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" ) const minimumShowTfVersion string = "0.12.0" -func NewShowStepRunner(executor TerraformExec, defaultTFVersion *version.Version) (Runner, error) { +func NewShowStepRunner(executor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTFVersion *version.Version) (Runner, error) { showStepRunner := &showStepRunner{ - terraformExecutor: executor, - defaultTFVersion: defaultTFVersion, + terraformExecutor: executor, + defaultTfDistribution: defaultTfDistribution, + defaultTFVersion: defaultTFVersion, } remotePlanRunner := NullRunner{} runner := NewPlanTypeStepRunnerDelegate(showStepRunner, remotePlanRunner) @@ -23,12 +25,17 @@ func NewShowStepRunner(executor TerraformExec, defaultTFVersion *version.Version // showStepRunner runs terraform show on an existing plan file and outputs it to a json file type showStepRunner struct { - terraformExecutor TerraformExec - defaultTFVersion *version.Version + terraformExecutor TerraformExec + defaultTfDistribution terraform.Distribution + defaultTFVersion *version.Version } func (p *showStepRunner) Run(ctx command.ProjectContext, _ []string, path string, envs map[string]string) (string, error) { + tfDistribution := p.defaultTfDistribution tfVersion := p.defaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } @@ -41,6 +48,7 @@ func (p *showStepRunner) Run(ctx command.ProjectContext, _ []string, path string path, []string{"show", "-json", filepath.Clean(planFile)}, envs, + tfDistribution, tfVersion, ctx.Workspace, ) diff --git a/server/core/runtime/show_step_runner_test.go b/server/core/runtime/show_step_runner_test.go index 9803efb9ff..87c717d8d6 100644 --- a/server/core/runtime/show_step_runner_test.go +++ b/server/core/runtime/show_step_runner_test.go @@ -9,7 +9,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -20,6 +22,8 @@ func TestShowStepRunnner(t *testing.T) { path := t.TempDir() resultPath := filepath.Join(path, "test-default.json") envs := map[string]string{"key": "val"} + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.12") context := command.ProjectContext{ Workspace: "default", @@ -29,17 +33,18 @@ func TestShowStepRunnner(t *testing.T) { RegisterMockTestingT(t) - mockExecutor := mocks.NewMockClient() + mockExecutor := tfclientmocks.NewMockClient() subject := showStepRunner{ - terraformExecutor: mockExecutor, - defaultTFVersion: tfVersion, + terraformExecutor: mockExecutor, + defaultTfDistribution: tfDistribution, + defaultTFVersion: tfVersion, } t.Run("success", func(t *testing.T) { When(mockExecutor.RunCommandWithVersion( - context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace, + context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfDistribution, tfVersion, context.Workspace, )).ThenReturn("success", nil) r, err := subject.Run(context, []string{}, path, envs) @@ -57,6 +62,8 @@ func TestShowStepRunnner(t *testing.T) { t.Run("success w/ version override", func(t *testing.T) { v, _ := version.NewVersion("0.13.0") + mockDownloader := mocks.NewMockDownloader() + d := tf.NewDistributionTerraformWithDownloader(mockDownloader) contextWithVersionOverride := command.ProjectContext{ Workspace: "default", @@ -66,7 +73,7 @@ func TestShowStepRunnner(t *testing.T) { } When(mockExecutor.RunCommandWithVersion( - contextWithVersionOverride, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, v, context.Workspace, + contextWithVersionOverride, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, d, v, context.Workspace, )).ThenReturn("success", nil) r, err := subject.Run(contextWithVersionOverride, []string{}, path, envs) @@ -83,7 +90,7 @@ func TestShowStepRunnner(t *testing.T) { t.Run("failure running command", func(t *testing.T) { When(mockExecutor.RunCommandWithVersion( - context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace, + context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfDistribution, tfVersion, context.Workspace, )).ThenReturn("success", errors.New("error")) _, err := subject.Run(context, []string{}, path, envs) diff --git a/server/core/runtime/state_rm_step_runner.go b/server/core/runtime/state_rm_step_runner.go index 3b4a08f102..42af97c006 100644 --- a/server/core/runtime/state_rm_step_runner.go +++ b/server/core/runtime/state_rm_step_runner.go @@ -5,25 +5,32 @@ import ( "path/filepath" version "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/utils" ) type stateRmStepRunner struct { - terraformExecutor TerraformExec - defaultTFVersion *version.Version + terraformExecutor TerraformExec + defaultTFDistribution terraform.Distribution + defaultTFVersion *version.Version } -func NewStateRmStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version) Runner { +func NewStateRmStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version) Runner { runner := &stateRmStepRunner{ - terraformExecutor: terraformExecutor, - defaultTFVersion: defaultTfVersion, + terraformExecutor: terraformExecutor, + defaultTFDistribution: defaultTfDistribution, + defaultTFVersion: defaultTfVersion, } - return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner) + return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner) } func (p *stateRmStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfDistribution := p.defaultTFDistribution tfVersion := p.defaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } @@ -31,7 +38,7 @@ func (p *stateRmStepRunner) Run(ctx command.ProjectContext, extraArgs []string, stateRmCmd := []string{"state", "rm"} stateRmCmd = append(stateRmCmd, extraArgs...) stateRmCmd = append(stateRmCmd, ctx.EscapedCommentArgs...) - out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), stateRmCmd, envs, tfVersion, ctx.Workspace) + out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), stateRmCmd, envs, tfDistribution, tfVersion, ctx.Workspace) // If the state rm was successful and a plan file exists, delete the plan. planPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)) diff --git a/server/core/runtime/state_rm_step_runner_test.go b/server/core/runtime/state_rm_step_runner_test.go index df5e1036e8..5de1f63c5f 100644 --- a/server/core/runtime/state_rm_step_runner_test.go +++ b/server/core/runtime/state_rm_step_runner_test.go @@ -8,7 +8,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -29,17 +31,19 @@ func TestStateRmStepRunner_Run_Success(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() tfVersion, _ := version.NewVersion("0.15.0") - s := NewStateRmStepRunner(terraform, tfVersion) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + s := NewStateRmStepRunner(terraform, tfDistribution, tfVersion) - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, "default") _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } @@ -59,23 +63,25 @@ func TestStateRmStepRunner_Run_Workspace(t *testing.T) { } RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() tfVersion, _ := version.NewVersion("0.15.0") - s := NewStateRmStepRunner(terraform, tfVersion) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + s := NewStateRmStepRunner(terraform, tfDistribution, tfVersion) - When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())). + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())). ThenReturn("output", nil) output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) Ok(t, err) Equals(t, "output", output) // switch workspace - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfVersion, workspace) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfDistribution, tfVersion, workspace) // exec state rm commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"} - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace) _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") diff --git a/server/core/runtime/version_step_runner.go b/server/core/runtime/version_step_runner.go index c75c5396fb..db1f525743 100644 --- a/server/core/runtime/version_step_runner.go +++ b/server/core/runtime/version_step_runner.go @@ -4,22 +4,28 @@ import ( "path/filepath" "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" ) // VersionStepRunner runs a version command given a ctx type VersionStepRunner struct { - TerraformExecutor TerraformExec - DefaultTFVersion *version.Version + TerraformExecutor TerraformExec + DefaultTFDistribution terraform.Distribution + DefaultTFVersion *version.Version } // Run ensures a given version for the executable, builds the args from the project context and then runs executable returning the result func (v *VersionStepRunner) Run(ctx command.ProjectContext, _ []string, path string, envs map[string]string) (string, error) { + tfDistribution := v.DefaultTFDistribution tfVersion := v.DefaultTFVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } versionCmd := []string{"version"} - return v.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), versionCmd, envs, tfVersion, ctx.Workspace) + return v.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), versionCmd, envs, tfDistribution, tfVersion, ctx.Workspace) } diff --git a/server/core/runtime/version_step_runner_test.go b/server/core/runtime/version_step_runner_test.go index 55c4fc05f4..242059c586 100644 --- a/server/core/runtime/version_step_runner_test.go +++ b/server/core/runtime/version_step_runner_test.go @@ -5,7 +5,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -33,18 +35,21 @@ func TestRunVersionStep(t *testing.T) { }, } - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.15.0") tmpDir := t.TempDir() s := &VersionStepRunner{ - TerraformExecutor: terraform, - DefaultTFVersion: tfVersion, + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, } t.Run("ensure runs", func(t *testing.T) { _, err := s.Run(context, []string{}, tmpDir, map[string]string(nil)) - terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"version"}, map[string]string(nil), tfVersion, "default") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"version"}, map[string]string(nil), tfDistribution, tfVersion, "default") Ok(t, err) }) } diff --git a/server/core/runtime/workspace_step_runner_delegate.go b/server/core/runtime/workspace_step_runner_delegate.go index 9d77db44d0..5628a6a351 100644 --- a/server/core/runtime/workspace_step_runner_delegate.go +++ b/server/core/runtime/workspace_step_runner_delegate.go @@ -5,33 +5,40 @@ import ( "strings" "github.com/hashicorp/go-version" + "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/events/command" ) // workspaceStepRunnerDelegate ensures that a given step runner run on switched workspace type workspaceStepRunnerDelegate struct { - terraformExecutor TerraformExec - defaultTfVersion *version.Version - delegate Runner + terraformExecutor TerraformExec + defaultTfDistribution terraform.Distribution + defaultTfVersion *version.Version + delegate Runner } -func NewWorkspaceStepRunnerDelegate(terraformExecutor TerraformExec, defaultTfVersion *version.Version, delegate Runner) Runner { +func NewWorkspaceStepRunnerDelegate(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, delegate Runner) Runner { return &workspaceStepRunnerDelegate{ - terraformExecutor: terraformExecutor, - defaultTfVersion: defaultTfVersion, - delegate: delegate, + terraformExecutor: terraformExecutor, + defaultTfDistribution: defaultTfDistribution, + defaultTfVersion: defaultTfVersion, + delegate: delegate, } } func (r *workspaceStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) { + tfDistribution := r.defaultTfDistribution tfVersion := r.defaultTfVersion + if ctx.TerraformDistribution != nil { + tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution) + } if ctx.TerraformVersion != nil { tfVersion = ctx.TerraformVersion } // We only need to switch workspaces in version 0.9.*. In older versions, // there is no such thing as a workspace so we don't need to do anything. - if err := r.switchWorkspace(ctx, path, tfVersion, envs); err != nil { + if err := r.switchWorkspace(ctx, path, tfDistribution, tfVersion, envs); err != nil { return "", err } @@ -40,7 +47,7 @@ func (r *workspaceStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs // switchWorkspace changes the terraform workspace if necessary and will create // it if it doesn't exist. It handles differences between versions. -func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext, path string, tfVersion *version.Version, envs map[string]string) error { +func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext, path string, tfDistribution terraform.Distribution, tfVersion *version.Version, envs map[string]string) error { // In versions less than 0.9 there is no support for workspaces. noWorkspaceSupport := MustConstraint("<0.9").Check(tfVersion) // If the user tried to set a specific workspace in the comment but their @@ -63,7 +70,7 @@ func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext // already in the right workspace then no need to switch. This will save us // about ten seconds. This command is only available in > 0.10. if !runningZeroPointNine { - workspaceShowOutput, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "show"}, envs, tfVersion, ctx.Workspace) + workspaceShowOutput, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "show"}, envs, tfDistribution, tfVersion, ctx.Workspace) if err != nil { return err } @@ -78,11 +85,11 @@ func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext // To do this we can either select and catch the error or use list and then // look for the workspace. Both commands take the same amount of time so // that's why we're running select here. - _, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "select", ctx.Workspace}, envs, tfVersion, ctx.Workspace) + _, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "select", ctx.Workspace}, envs, tfDistribution, tfVersion, ctx.Workspace) if err != nil { // If terraform workspace select fails we run terraform workspace // new to create a new workspace automatically. - out, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "new", ctx.Workspace}, envs, tfVersion, ctx.Workspace) + out, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "new", ctx.Workspace}, envs, tfDistribution, tfVersion, ctx.Workspace) if err != nil { return fmt.Errorf("%s: %s", err, out) } diff --git a/server/core/runtime/workspace_step_runner_delegate_test.go b/server/core/runtime/workspace_step_runner_delegate_test.go index 2ef3032d50..ae1d85a772 100644 --- a/server/core/runtime/workspace_step_runner_delegate_test.go +++ b/server/core/runtime/workspace_step_runner_delegate_test.go @@ -6,7 +6,9 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" + tf "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/logging" @@ -16,7 +18,9 @@ import ( func TestRun_NoWorkspaceIn08(t *testing.T) { // We don't want any workspace commands to be run in 0.8. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.8") workspace := "default" logger := logging.NewNoopLogger(t) @@ -24,7 +28,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { Log: logger, Workspace: workspace, } - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) @@ -36,6 +40,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { "select", "workspace"}, map[string]string(nil), + tfDistribution, tfVersion, workspace) terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, @@ -44,6 +49,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) { "select", "workspace"}, map[string]string(nil), + tfDistribution, tfVersion, workspace) } @@ -52,11 +58,13 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) { // If they attempt to use a workspace other than default in 0.8 // we should error. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.8") logger := logging.NewNoopLogger(t) workspace := "notdefault" - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) _, err := s.Run(command.ProjectContext{ Log: logger, @@ -67,6 +75,8 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) { func TestRun_SwitchesWorkspace(t *testing.T) { RegisterMockTestingT(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) cases := []struct { tfVersion string @@ -92,14 +102,14 @@ func TestRun_SwitchesWorkspace(t *testing.T) { for _, c := range cases { t.Run(c.tfVersion, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() tfVersion, _ := version.NewVersion(c.tfVersion) logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ Log: logger, Workspace: "workspace", } - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) @@ -111,6 +121,7 @@ func TestRun_SwitchesWorkspace(t *testing.T) { "select", "workspace"}, map[string]string(nil), + tfDistribution, tfVersion, "workspace") }) @@ -145,7 +156,9 @@ func TestRun_CreatesWorkspace(t *testing.T) { for _, c := range cases { t.Run(c.tfVersion, func(t *testing.T) { - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion(c.tfVersion) logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -163,20 +176,20 @@ func TestRun_CreatesWorkspace(t *testing.T) { Name: "repo", }, } - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) // Ensure that we actually try to switch workspaces by making the // output of `workspace show` to be a different name. - When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil) + When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil) expWorkspaceArgs := []string{c.expWorkspaceCommand, "select", "workspace"} - When(terraform.RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist")) + When(terraform.RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist")) _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) // Verify that env select was called as well as plan. - terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace") }) } } @@ -185,7 +198,9 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) { // Tests that if workspace show says we're on the right workspace we don't // switch. RegisterMockTestingT(t) - terraform := mocks.NewMockClient() + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) tfVersion, _ := version.NewVersion("0.10.0") logger := logging.NewNoopLogger(t) ctx := command.ProjectContext{ @@ -203,12 +218,12 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) { Name: "repo", }, } - s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{}) - When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil) + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) + When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("workspace\n", nil) _, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) Ok(t, err) // Verify that workspace select was never called. - terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, "/path", []string{"workspace", "select", "workspace"}, map[string]string(nil), tfVersion, "workspace") + terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, "/path", []string{"workspace", "select", "workspace"}, map[string]string(nil), tfDistribution, tfVersion, "workspace") } diff --git a/server/events/terraform/ansi/strip.go b/server/core/terraform/ansi/strip.go similarity index 100% rename from server/events/terraform/ansi/strip.go rename to server/core/terraform/ansi/strip.go diff --git a/server/events/terraform/ansi/strip_test.go b/server/core/terraform/ansi/strip_test.go similarity index 100% rename from server/events/terraform/ansi/strip_test.go rename to server/core/terraform/ansi/strip_test.go diff --git a/server/core/terraform/distribution.go b/server/core/terraform/distribution.go index 0fd781765d..dbeaf6a46b 100644 --- a/server/core/terraform/distribution.go +++ b/server/core/terraform/distribution.go @@ -18,6 +18,14 @@ type Distribution interface { ResolveConstraint(context.Context, string) (*version.Version, error) } +func NewDistribution(distribution string) Distribution { + tfDistribution := NewDistributionTerraform() + if distribution == "opentofu" { + tfDistribution = NewDistributionOpenTofu() + } + return tfDistribution +} + type DistributionOpenTofu struct { downloader Downloader } diff --git a/server/core/terraform/mocks/mock_terraform_client.go b/server/core/terraform/tfclient/mocks/mock_terraform_client.go similarity index 79% rename from server/core/terraform/mocks/mock_terraform_client.go rename to server/core/terraform/tfclient/mocks/mock_terraform_client.go index 279de1a751..9dca6ffd4b 100644 --- a/server/core/terraform/mocks/mock_terraform_client.go +++ b/server/core/terraform/tfclient/mocks/mock_terraform_client.go @@ -1,11 +1,12 @@ // Code generated by pegomock. DO NOT EDIT. -// Source: github.com/runatlantis/atlantis/server/core/terraform (interfaces: Client) +// Source: github.com/runatlantis/atlantis/server/core/terraform/tfclient (interfaces: Client) package mocks import ( go_version "github.com/hashicorp/go-version" pegomock "github.com/petergtz/pegomock/v4" + terraform "github.com/runatlantis/atlantis/server/core/terraform" command "github.com/runatlantis/atlantis/server/events/command" logging "github.com/runatlantis/atlantis/server/logging" "reflect" @@ -42,11 +43,11 @@ func (mock *MockClient) DetectVersion(log logging.SimpleLogging, projectDirector return _ret0 } -func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) error { +func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *go_version.Version) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") } - _params := []pegomock.Param{log, v} + _params := []pegomock.Param{log, d, v} _result := pegomock.GetGenericMockFrom(mock).Invoke("EnsureVersion", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) var _ret0 error if len(_result) != 0 { @@ -57,11 +58,11 @@ func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.V return _ret0 } -func (mock *MockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (string, error) { +func (mock *MockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") } - _params := []pegomock.Param{ctx, path, args, envs, v, workspace} + _params := []pegomock.Param{ctx, path, args, envs, d, v, workspace} _result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandWithVersion", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var _ret0 string var _ret1 error @@ -148,8 +149,8 @@ func (c *MockClient_DetectVersion_OngoingVerification) GetAllCapturedArguments() return } -func (verifier *VerifierMockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) *MockClient_EnsureVersion_OngoingVerification { - _params := []pegomock.Param{log, v} +func (verifier *VerifierMockClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *go_version.Version) *MockClient_EnsureVersion_OngoingVerification { + _params := []pegomock.Param{log, d, v} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "EnsureVersion", _params, verifier.timeout) return &MockClient_EnsureVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -159,12 +160,12 @@ type MockClient_EnsureVersion_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockClient_EnsureVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, *go_version.Version) { - log, v := c.GetAllCapturedArguments() - return log[len(log)-1], v[len(v)-1] +func (c *MockClient_EnsureVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, terraform.Distribution, *go_version.Version) { + log, d, v := c.GetAllCapturedArguments() + return log[len(log)-1], d[len(d)-1], v[len(v)-1] } -func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []*go_version.Version) { +func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []terraform.Distribution, _param2 []*go_version.Version) { _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(_params) > 0 { if len(_params) > 0 { @@ -174,17 +175,23 @@ func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() } } if len(_params) > 1 { - _param1 = make([]*go_version.Version, len(c.methodInvocations)) + _param1 = make([]terraform.Distribution, len(c.methodInvocations)) for u, param := range _params[1] { - _param1[u] = param.(*go_version.Version) + _param1[u] = param.(terraform.Distribution) + } + } + if len(_params) > 2 { + _param2 = make([]*go_version.Version, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(*go_version.Version) } } } return } -func (verifier *VerifierMockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification { - _params := []pegomock.Param{ctx, path, args, envs, v, workspace} +func (verifier *VerifierMockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification { + _params := []pegomock.Param{ctx, path, args, envs, d, v, workspace} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandWithVersion", _params, verifier.timeout) return &MockClient_RunCommandWithVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } @@ -194,12 +201,12 @@ type MockClient_RunCommandWithVersion_OngoingVerification struct { methodInvocations []pegomock.MethodInvocation } -func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, *go_version.Version, string) { - ctx, path, args, envs, v, workspace := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], v[len(v)-1], workspace[len(workspace)-1] +func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, terraform.Distribution, *go_version.Version, string) { + ctx, path, args, envs, d, v, workspace := c.GetAllCapturedArguments() + return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], d[len(d)-1], v[len(v)-1], workspace[len(workspace)-1] } -func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []*go_version.Version, _param5 []string) { +func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []terraform.Distribution, _param5 []*go_version.Version, _param6 []string) { _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(_params) > 0 { if len(_params) > 0 { @@ -227,15 +234,21 @@ func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArg } } if len(_params) > 4 { - _param4 = make([]*go_version.Version, len(c.methodInvocations)) + _param4 = make([]terraform.Distribution, len(c.methodInvocations)) for u, param := range _params[4] { - _param4[u] = param.(*go_version.Version) + _param4[u] = param.(terraform.Distribution) } } if len(_params) > 5 { - _param5 = make([]string, len(c.methodInvocations)) + _param5 = make([]*go_version.Version, len(c.methodInvocations)) for u, param := range _params[5] { - _param5[u] = param.(string) + _param5[u] = param.(*go_version.Version) + } + } + if len(_params) > 6 { + _param6 = make([]string, len(c.methodInvocations)) + for u, param := range _params[6] { + _param6[u] = param.(string) } } } diff --git a/server/core/terraform/terraform_client.go b/server/core/terraform/tfclient/terraform_client.go similarity index 91% rename from server/core/terraform/terraform_client.go rename to server/core/terraform/tfclient/terraform_client.go index d01525704b..603153c249 100644 --- a/server/core/terraform/terraform_client.go +++ b/server/core/terraform/tfclient/terraform_client.go @@ -14,7 +14,7 @@ // Modified hereafter by contributors to runatlantis/atlantis. // // Package terraform handles the actual running of terraform commands. -package terraform +package tfclient import ( "context" @@ -32,9 +32,11 @@ import ( "github.com/mitchellh/go-homedir" "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/core/terraform" + "github.com/runatlantis/atlantis/server/core/runtime/models" + "github.com/runatlantis/atlantis/server/core/terraform/ansi" "github.com/runatlantis/atlantis/server/events/command" - "github.com/runatlantis/atlantis/server/events/terraform/ansi" "github.com/runatlantis/atlantis/server/jobs" "github.com/runatlantis/atlantis/server/logging" ) @@ -47,10 +49,10 @@ type Client interface { // RunCommandWithVersion executes terraform with args in path. If v is nil, // it will use the default Terraform version. workspace is the Terraform // workspace which should be set as an environment variable. - RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error) + RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error) // EnsureVersion makes sure that terraform version `v` is available to use - EnsureVersion(log logging.SimpleLogging, v *version.Version) error + EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error // DetectVersion Extracts required_version from Terraform configuration in the specified project directory. Returns nil if unable to determine the version. DetectVersion(log logging.SimpleLogging, projectDirectory string) *version.Version @@ -58,7 +60,7 @@ type Client interface { type DefaultClient struct { // Distribution handles logic specific to the TF distribution being used by Atlantis - distribution Distribution + distribution terraform.Distribution // defaultVersion is the default version of terraform to use if another // version isn't specified. @@ -102,7 +104,7 @@ var versionRegex = regexp.MustCompile("(?:Terraform|OpenTofu) v(.*?)(\\s.*)?\n") // NewClientWithDefaultVersion creates a new terraform client and pre-fetches the default version func NewClientWithDefaultVersion( log logging.SimpleLogging, - distribution Distribution, + distribution terraform.Distribution, binDir string, cacheDir string, tfeToken string, @@ -189,7 +191,7 @@ func NewClientWithDefaultVersion( func NewTestClient( log logging.SimpleLogging, - distribution Distribution, + distribution terraform.Distribution, binDir string, cacheDir string, tfeToken string, @@ -227,7 +229,7 @@ func NewTestClient( // Will asynchronously download the required version if it doesn't exist already. func NewClient( log logging.SimpleLogging, - distribution Distribution, + distribution terraform.Distribution, binDir string, cacheDir string, tfeToken string, @@ -256,6 +258,10 @@ func NewClient( ) } +func (c *DefaultClient) DefaultDistribution() terraform.Distribution { + return c.distribution +} + // Version returns the default version of Terraform we use if no other version // is defined. func (c *DefaultClient) DefaultVersion() *version.Version { @@ -326,14 +332,14 @@ func (c *DefaultClient) DetectVersion(log logging.SimpleLogging, projectDirector } // See Client.EnsureVersion. -func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, v *version.Version) error { +func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error { if v == nil { v = c.defaultVersion } var err error c.versionsLock.Lock() - _, err = ensureVersion(log, c.distribution, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed) + _, err = ensureVersion(log, d, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed) c.versionsLock.Unlock() if err != nil { return err @@ -343,9 +349,9 @@ func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, v *version.Vers } // See Client.RunCommandWithVersion. -func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (string, error) { +func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error) { if isAsyncEligibleCommand(args[0]) { - _, outCh := c.RunCommandAsync(ctx, path, args, customEnvVars, v, workspace) + _, outCh := c.RunCommandAsync(ctx, path, args, customEnvVars, d, v, workspace) var lines []string var err error @@ -362,7 +368,7 @@ func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path s output = ansi.Strip(output) return fmt.Sprintf("%s\n", output), err } - tfCmd, cmd, err := c.prepExecCmd(ctx.Log, v, workspace, path, args) + tfCmd, cmd, err := c.prepExecCmd(ctx.Log, d, v, workspace, path, args) if err != nil { return "", err } @@ -388,8 +394,8 @@ func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path s // prepExecCmd builds a ready to execute command based on the version of terraform // v, and args. It returns a printable representation of the command that will // be run and the actual command. -func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, v *version.Version, workspace string, path string, args []string) (string, *exec.Cmd, error) { - tfCmd, envVars, err := c.prepCmd(log, v, workspace, path, args) +func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, d terraform.Distribution, v *version.Version, workspace string, path string, args []string) (string, *exec.Cmd, error) { + tfCmd, envVars, err := c.prepCmd(log, d, v, workspace, path, args) if err != nil { return "", nil, err } @@ -401,7 +407,8 @@ func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, v *version.Versio // prepCmd prepares a shell command (to be interpreted with `sh -c `) and set of environment // variables for running terraform. -func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, workspace string, path string, args []string) (string, []string, error) { +func (c *DefaultClient) prepCmd(log logging.SimpleLogging, d terraform.Distribution, v *version.Version, workspace string, path string, args []string) (string, []string, error) { + if v == nil { v = c.defaultVersion } @@ -413,7 +420,7 @@ func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, w } else { var err error c.versionsLock.Lock() - binPath, err = ensureVersion(log, c.distribution, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed) + binPath, err = ensureVersion(log, d, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed) c.versionsLock.Unlock() if err != nil { return "", nil, err @@ -446,8 +453,8 @@ func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, w // Callers can use the input channel to pass stdin input to the command. // If any error is passed on the out channel, there will be no // further output (so callers are free to exit). -func (c *DefaultClient) RunCommandAsync(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (chan<- string, <-chan models.Line) { - cmd, envVars, err := c.prepCmd(ctx.Log, v, workspace, path, args) +func (c *DefaultClient) RunCommandAsync(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, d terraform.Distribution, v *version.Version, workspace string) (chan<- string, <-chan models.Line) { + cmd, envVars, err := c.prepCmd(ctx.Log, d, v, workspace, path, args) if err != nil { // The signature of `RunCommandAsync` doesn't provide for returning an immediate error, only one // once reading the output. Since we won't be spawning a process, simulate that by sending the @@ -486,7 +493,7 @@ func MustConstraint(v string) version.Constraints { // It will download this version if we don't have it. func ensureVersion( log logging.SimpleLogging, - dist Distribution, + dist terraform.Distribution, versions map[string]string, v *version.Version, binDir string, diff --git a/server/core/terraform/terraform_client_internal_test.go b/server/core/terraform/tfclient/terraform_client_internal_test.go similarity index 88% rename from server/core/terraform/terraform_client_internal_test.go rename to server/core/terraform/tfclient/terraform_client_internal_test.go index f92a3fd2d2..9cde70e399 100644 --- a/server/core/terraform/terraform_client_internal_test.go +++ b/server/core/terraform/tfclient/terraform_client_internal_test.go @@ -1,4 +1,4 @@ -package terraform +package tfclient import ( "fmt" @@ -10,6 +10,8 @@ import ( version "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models" + "github.com/runatlantis/atlantis/server/core/terraform" + terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" @@ -120,7 +122,9 @@ func TestDefaultClient_RunCommandWithVersion_EnvVars(t *testing.T) { "DIR=$DIR", } customEnvVars := map[string]string{} - out, err := client.RunCommandWithVersion(ctx, tmp, args, customEnvVars, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + out, err := client.RunCommandWithVersion(ctx, tmp, args, customEnvVars, distribution, nil, "workspace") Ok(t, err) exp := fmt.Sprintf("TF_IN_AUTOMATION=true TF_PLUGIN_CACHE_DIR=%s WORKSPACE=workspace ATLANTIS_TERRAFORM_VERSION=0.11.11 DIR=%s\n", tmp, tmp) Equals(t, exp, out) @@ -163,7 +167,9 @@ func TestDefaultClient_RunCommandWithVersion_Error(t *testing.T) { "exit", "1", } - out, err := client.RunCommandWithVersion(ctx, tmp, args, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + out, err := client.RunCommandWithVersion(ctx, tmp, args, map[string]string{}, distribution, nil, "workspace") ErrEquals(t, fmt.Sprintf(`running 'echo dying && exit 1' in '%s': exit status 1`, tmp), err) // Test that we still get our output. Equals(t, "dying\n", out) @@ -209,7 +215,9 @@ func TestDefaultClient_RunCommandAsync_Success(t *testing.T) { "ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION", "DIR=$DIR", } - _, outCh := client.RunCommandAsync(ctx, tmp, args, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + _, outCh := client.RunCommandAsync(ctx, tmp, args, map[string]string{}, distribution, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -261,7 +269,9 @@ func TestDefaultClient_RunCommandAsync_BigOutput(t *testing.T) { _, err = f.WriteString(s) Ok(t, err) } - _, outCh := client.RunCommandAsync(ctx, tmp, []string{filename}, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + _, outCh := client.RunCommandAsync(ctx, tmp, []string{filename}, map[string]string{}, distribution, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -301,7 +311,9 @@ func TestDefaultClient_RunCommandAsync_StderrOutput(t *testing.T) { overrideTF: "echo", projectCmdOutputHandler: projectCmdOutputHandler, } - _, outCh := client.RunCommandAsync(ctx, tmp, []string{"stderr", ">&2"}, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + _, outCh := client.RunCommandAsync(ctx, tmp, []string{"stderr", ">&2"}, map[string]string{}, distribution, nil, "workspace") out, err := waitCh(outCh) Ok(t, err) @@ -341,7 +353,9 @@ func TestDefaultClient_RunCommandAsync_ExitOne(t *testing.T) { overrideTF: "echo", projectCmdOutputHandler: projectCmdOutputHandler, } - _, outCh := client.RunCommandAsync(ctx, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + _, outCh := client.RunCommandAsync(ctx, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, distribution, nil, "workspace") out, err := waitCh(outCh) ErrEquals(t, fmt.Sprintf(`running 'sh -c' 'echo dying && exit 1' in '%s': exit status 1`, tmp), err) @@ -383,7 +397,9 @@ func TestDefaultClient_RunCommandAsync_Input(t *testing.T) { projectCmdOutputHandler: projectCmdOutputHandler, } - inCh, outCh := client.RunCommandAsync(ctx, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, nil, "workspace") + mockDownloader := terraform_mocks.NewMockDownloader() + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + inCh, outCh := client.RunCommandAsync(ctx, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, distribution, nil, "workspace") inCh <- "echo me\n" out, err := waitCh(outCh) diff --git a/server/core/terraform/terraform_client_test.go b/server/core/terraform/tfclient/terraform_client_test.go similarity index 86% rename from server/core/terraform/terraform_client_test.go rename to server/core/terraform/tfclient/terraform_client_test.go index c60a5fb085..50afd698a7 100644 --- a/server/core/terraform/terraform_client_test.go +++ b/server/core/terraform/tfclient/terraform_client_test.go @@ -11,7 +11,7 @@ // limitations under the License. // Modified hereafter by contributors to runatlantis/atlantis. -package terraform_test +package tfclient_test import ( "context" @@ -28,6 +28,7 @@ import ( "github.com/runatlantis/atlantis/cmd" "github.com/runatlantis/atlantis/server/core/terraform" "github.com/runatlantis/atlantis/server/core/terraform/mocks" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/events/command" jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks" "github.com/runatlantis/atlantis/server/logging" @@ -42,12 +43,12 @@ func TestMustConstraint_PanicsOnBadConstraint(t *testing.T) { } }() - terraform.MustConstraint("invalid constraint") + tfclient.MustConstraint("invalid constraint") } func TestMustConstraint(t *testing.T) { t.Log("MustConstraint should return the constrain") - c := terraform.MustConstraint(">0.1") + c := tfclient.MustConstraint(">0.1") expectedConstraint, err := version.NewConstraint(">0.1") Ok(t, err) Equals(t, expectedConstraint.String(), c.String()) @@ -78,15 +79,15 @@ is 0.11.13. You can update by downloading from developer.hashicorp.com/terraform defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))() mockDownloader := mocks.NewMockDownloader() - distibution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logger, distibution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{"test": "123"}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{"test": "123"}, distribution, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -117,13 +118,13 @@ is 0.11.13. You can update by downloading from developer.hashicorp.com/terraform mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -141,7 +142,7 @@ func TestNewClient_NoTF(t *testing.T) { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - _, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + _, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) ErrEquals(t, "terraform not found in $PATH. Set --default-tf-version or download terraform from https://developer.hashicorp.com/terraform/downloads", err) } @@ -167,13 +168,13 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, false, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, false, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -198,13 +199,13 @@ func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logging.NewNoopLogger(t), distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logging.NewNoopLogger(t), distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "") Ok(t, err) Equals(t, fakeBinOut+"\n", output) } @@ -232,7 +233,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) { return []ReturnValue{binPath, err} }) distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Ok(t, err) @@ -243,7 +244,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) { // Reset PATH so that it has sh. Ok(t, os.Setenv("PATH", orig)) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "") Ok(t, err) Equals(t, "\nTerraform v0.11.10\n\n", output) } @@ -255,7 +256,7 @@ func TestNewClient_BadVersion(t *testing.T) { projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - _, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + _, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) ErrEquals(t, "Malformed version: malformed", err) } @@ -283,11 +284,11 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) { return []ReturnValue{binPath, err} }) - c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) - output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, v, "") + output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, v, "") Assert(t, err == nil, "err: %s: %s", err, output) Equals(t, "\nTerraform v99.99.99\n\n", output) @@ -301,10 +302,10 @@ func TestEnsureVersion_downloaded(t *testing.T) { projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() mockDownloader := mocks.NewMockDownloader() - distibution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) + distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) downloadsAllowed := true - c, err := terraform.NewTestClient(logger, distibution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler) + c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) @@ -318,7 +319,7 @@ func TestEnsureVersion_downloaded(t *testing.T) { return []ReturnValue{binPath, err} }) - err = c.EnsureVersion(logger, v) + err = c.EnsureVersion(logger, distribution, v) Ok(t, err) @@ -337,7 +338,7 @@ func TestEnsureVersion_downloaded_customURL(t *testing.T) { downloadsAllowed := true customURL := "http://releases.example.com" - c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, customURL, downloadsAllowed, true, projectCmdOutputHandler) + c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, customURL, downloadsAllowed, true, projectCmdOutputHandler) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) @@ -351,7 +352,7 @@ func TestEnsureVersion_downloaded_customURL(t *testing.T) { return []ReturnValue{binPath, err} }) - err = c.EnsureVersion(logger, v) + err = c.EnsureVersion(logger, distribution, v) Ok(t, err) @@ -369,7 +370,7 @@ func TestEnsureVersion_downloaded_downloadingDisabled(t *testing.T) { distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) downloadsAllowed := false - c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler) + c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler) Ok(t, err) Equals(t, "0.11.10", c.DefaultVersion().String()) @@ -377,7 +378,7 @@ func TestEnsureVersion_downloaded_downloadingDisabled(t *testing.T) { v, err := version.NewVersion("99.99.99") Ok(t, err) - err = c.EnsureVersion(logger, v) + err = c.EnsureVersion(logger, distribution, v) ErrContains(t, "could not find terraform version", err) ErrContains(t, "downloads are disabled", err) mockDownloader.VerifyWasCalled(Never()) @@ -501,7 +502,7 @@ terraform { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewTestClient( + c, err := tfclient.NewTestClient( logger, distribution, binDir, @@ -548,7 +549,7 @@ func TestExtractExactRegex(t *testing.T) { mockDownloader := mocks.NewMockDownloader() distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader) - c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) + c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler) Ok(t, err) tests := []struct { diff --git a/server/events/command/project_context.go b/server/events/command/project_context.go index 5490b4a41e..6627a5a627 100644 --- a/server/events/command/project_context.go +++ b/server/events/command/project_context.go @@ -93,6 +93,10 @@ type ProjectContext struct { // Steps are the sequence of commands we need to run for this project and this // stage. Steps []valid.Step + // TerraformDistribution is the distribution of terraform we should use when + // executing commands for this project. This can be set to nil in which case + // we will use the default Atlantis terraform distribution. + TerraformDistribution *string // TerraformVersion is the version of terraform we should use when executing // commands for this project. This can be set to nil in which case we will // use the default Atlantis terraform version. diff --git a/server/events/command/scope_tags.go b/server/events/command/scope_tags.go index 2f51d86c83..8416927eab 100644 --- a/server/events/command/scope_tags.go +++ b/server/events/command/scope_tags.go @@ -7,12 +7,13 @@ import ( ) type ProjectScopeTags struct { - BaseRepo string - PrNumber string - Project string - ProjectPath string - TerraformVersion string - Workspace string + BaseRepo string + PrNumber string + Project string + ProjectPath string + TerraformDistribution string + TerraformVersion string + Workspace string } func (s ProjectScopeTags) Loadtags() map[string]string { diff --git a/server/events/mock_workingdir_test.go b/server/events/mock_workingdir_test.go index c11b9e28bf..65d5fc00a7 100644 --- a/server/events/mock_workingdir_test.go +++ b/server/events/mock_workingdir_test.go @@ -4,12 +4,11 @@ package events import ( - "reflect" - "time" - pegomock "github.com/petergtz/pegomock/v4" models "github.com/runatlantis/atlantis/server/events/models" logging "github.com/runatlantis/atlantis/server/logging" + "reflect" + "time" ) type MockWorkingDir struct { diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 5870bf22e0..de0e47c986 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -11,7 +11,7 @@ import ( tally "github.com/uber-go/tally/v4" "github.com/runatlantis/atlantis/server/core/config/valid" - "github.com/runatlantis/atlantis/server/core/terraform" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/logging" "github.com/runatlantis/atlantis/server/metrics" @@ -59,7 +59,7 @@ func NewInstrumentedProjectCommandBuilder( IncludeGitUntrackedFiles bool, AutoDiscoverMode string, scope tally.Scope, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) *InstrumentedProjectCommandBuilder { scope = scope.SubScope("builder") @@ -119,7 +119,7 @@ func NewProjectCommandBuilder( IncludeGitUntrackedFiles bool, AutoDiscoverMode string, scope tally.Scope, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) *DefaultProjectCommandBuilder { return &DefaultProjectCommandBuilder{ ParserValidator: parserValidator, @@ -249,7 +249,7 @@ type DefaultProjectCommandBuilder struct { // User config option: Controls auto-discovery of projects in a repository. AutoDiscoverMode string // Handles the actual running of Terraform commands. - TerraformExecutor terraform.Client + TerraformExecutor tfclient.Client } // See ProjectCommandBuilder.BuildAutoplanCommands. diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index d020871b31..115657e38e 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -9,7 +9,7 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config" "github.com/runatlantis/atlantis/server/core/config/valid" - "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks" @@ -648,7 +648,7 @@ projects: Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600)) } - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( false, @@ -865,7 +865,7 @@ projects: statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( false, @@ -1112,7 +1112,7 @@ workflows: } statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( true, @@ -1264,7 +1264,7 @@ projects: } statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( false, @@ -1406,7 +1406,7 @@ projects: } statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") - terraformClient := mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := NewProjectCommandBuilder( false, diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index 7560b5d6de..30dec015a5 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-version" . "github.com/petergtz/pegomock/v4" - terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/core/config" "github.com/runatlantis/atlantis/server/core/config/valid" @@ -233,7 +233,7 @@ terraform { scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") userConfig := defaultUserConfig - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() for _, c := range cases { t.Run(c.Description, func(t *testing.T) { @@ -616,7 +616,7 @@ projects: AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -804,7 +804,7 @@ projects: AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1133,7 +1133,7 @@ projects: AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1231,7 +1231,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { globalCfgArgs := valid.GlobalCfgArgs{} scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1317,7 +1317,7 @@ projects: scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") userConfig := defaultUserConfig - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1405,7 +1405,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1558,7 +1558,7 @@ projects: AllowAllRepoSettings: true, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() When(terraformClient.DetectVersion(Any[logging.SimpleLogging](), Any[string]())).Then(func(params []Param) ReturnValues { projectName := filepath.Base(params[1].(string)) testVersion := testCase.Exp[projectName] @@ -1677,7 +1677,7 @@ projects: AllowAllRepoSettings: true, } scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1746,7 +1746,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman } globalCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs) - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( true, @@ -1834,7 +1834,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { globalCfgArgs := valid.GlobalCfgArgs{ AllowAllRepoSettings: false, } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, @@ -1964,7 +1964,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_Single_With_RestrictFile Ok(t, err) } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, // policyChecksSupported @@ -2075,7 +2075,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_with_IncludeGitUntracked Ok(t, err) } - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() builder := events.NewProjectCommandBuilder( false, // policyChecksSupported diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go index 19c1c8ff34..f606fdba5d 100644 --- a/server/events/project_command_context_builder.go +++ b/server/events/project_command_context_builder.go @@ -5,7 +5,7 @@ import ( "github.com/google/uuid" "github.com/runatlantis/atlantis/server/core/config/valid" - "github.com/runatlantis/atlantis/server/core/terraform" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" tally "github.com/uber-go/tally/v4" @@ -38,7 +38,7 @@ type ProjectCommandContextBuilder interface { prjCfg valid.MergedProjectCfg, commentFlags []string, repoDir string, - automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool, terraformClient terraform.Client, + automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool, terraformClient tfclient.Client, ) []command.ProjectContext } @@ -59,7 +59,7 @@ func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext( commentFlags []string, repoDir string, automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) (projectCmds []command.ProjectContext) { cb.ProjectCounter.Inc(1) @@ -93,7 +93,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext( commentFlags []string, repoDir string, automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) (projectCmds []command.ProjectContext) { ctx.Log.Debug("Building project command context for %s", cmdName) @@ -166,7 +166,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext( commentFlags []string, repoDir string, automerge, parallelApply, parallelPlan, verbose, abortOnExcecutionOrderFail bool, - terraformClient terraform.Client, + terraformClient tfclient.Client, ) (projectCmds []command.ProjectContext) { if prjCfg.PolicyCheck { ctx.Log.Debug("PolicyChecks are enabled") @@ -297,6 +297,7 @@ func newProjectCommandContext(ctx *command.Context, RePlanCmd: planCmd, RepoRelDir: projCfg.RepoRelDir, RepoConfigVersion: projCfg.RepoCfgVersion, + TerraformDistribution: projCfg.TerraformDistribution, TerraformVersion: projCfg.TerraformVersion, User: ctx.User, Verbose: verbose, diff --git a/server/events/project_command_context_builder_test.go b/server/events/project_command_context_builder_test.go index 84ce0ff630..c12f5bc8f5 100644 --- a/server/events/project_command_context_builder_test.go +++ b/server/events/project_command_context_builder_test.go @@ -5,7 +5,7 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config/valid" - terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/mocks" @@ -47,7 +47,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) { expectedApplyCmt := "Apply Comment" expectedPlanCmt := "Plan Comment" - terraformClient := terraform_mocks.NewMockClient() + terraformClient := tfclientmocks.NewMockClient() t.Run("with project name defined", func(t *testing.T) { When(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, projName, []string{})).ThenReturn(expectedPlanCmt) diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go index 68548efdd0..d064a712d2 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -23,7 +23,9 @@ import ( . "github.com/petergtz/pegomock/v4" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/runtime" + "github.com/runatlantis/atlantis/server/core/terraform" tmocks "github.com/runatlantis/atlantis/server/core/terraform/mocks" + tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks" "github.com/runatlantis/atlantis/server/events" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/mocks" @@ -542,12 +544,14 @@ func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) { // not running any Terraform. func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) { RegisterMockTestingT(t) - tfClient := tmocks.NewMockClient() + tfClient := tfclientmocks.NewMockClient() + tfDistribution := terraform.NewDistributionTerraformWithDownloader(tmocks.NewMockDownloader()) tfVersion, err := version.NewVersion("0.12.0") Ok(t, err) projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler() run := runtime.RunStepRunner{ TerraformExecutor: tfClient, + DefaultTFDistribution: tfDistribution, DefaultTFVersion: tfVersion, ProjectCmdOutputHandler: projectCmdOutputHandler, } diff --git a/server/server.go b/server/server.go index 22f6db5498..a77eeddaf8 100644 --- a/server/server.go +++ b/server/server.go @@ -42,6 +42,7 @@ import ( "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/core/db" "github.com/runatlantis/atlantis/server/core/redis" + "github.com/runatlantis/atlantis/server/core/terraform/tfclient" "github.com/runatlantis/atlantis/server/jobs" "github.com/runatlantis/atlantis/server/metrics" "github.com/runatlantis/atlantis/server/scheduled" @@ -127,12 +128,13 @@ type Server struct { // Config holds config for server that isn't passed in by the user. type Config struct { - AllowForkPRsFlag string - AtlantisURLFlag string - AtlantisVersion string - DefaultTFVersionFlag string - RepoConfigJSONFlag string - SilenceForkPRErrorsFlag string + AllowForkPRsFlag string + AtlantisURLFlag string + AtlantisVersion string + DefaultTFDistributionFlag string + DefaultTFVersionFlag string + RepoConfigJSONFlag string + SilenceForkPRErrorsFlag string } // WebhookConfig is nested within UserConfig. It's used to configure webhooks. @@ -427,12 +429,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { ) } - distribution := terraform.NewDistributionTerraform() - if userConfig.TFDistribution == "opentofu" { - distribution = terraform.NewDistributionOpenTofu() - } + distribution := terraform.NewDistribution(userConfig.DefaultTFDistribution) - terraformClient, err := terraform.NewClient( + terraformClient, err := tfclient.NewClient( logger, distribution, binDir, @@ -449,7 +448,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { // are, then we don't error out because we don't have/want terraform // installed on our CI system where the unit tests run. if err != nil && flag.Lookup("test.v") == nil { - return nil, errors.Wrap(err, fmt.Sprintf("initializing %s", userConfig.TFDistribution)) + return nil, errors.Wrap(err, fmt.Sprintf("initializing %s", userConfig.DefaultTFDistribution)) } markdownRenderer := events.NewMarkdownRenderer( gitlabClient.SupportsCommonMark(), @@ -586,10 +585,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { userConfig.ExecutableName, allowCommands, ) + defaultTfDistribution := terraformClient.DefaultDistribution() defaultTfVersion := terraformClient.DefaultVersion() pendingPlanFinder := &events.DefaultPendingPlanFinder{} runStepRunner := &runtime.RunStepRunner{ TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTfDistribution, DefaultTFVersion: defaultTfVersion, TerraformBinDir: terraformClient.TerraformBinDir(), ProjectCmdOutputHandler: projectCmdOutputHandler, @@ -648,13 +649,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { terraformClient, ) - showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfVersion) + showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion) if err != nil { return nil, errors.Wrap(err, "initializing show step runner") } policyCheckStepRunner, err := runtime.NewPolicyCheckStepRunner( + defaultTfDistribution, defaultTfVersion, policy.NewConfTestExecutorWorkflow(logger, binDir, &policy.ConfTestGoGetterVersionDownloader{}), ) @@ -672,17 +674,19 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { Locker: projectLocker, LockURLGenerator: router, InitStepRunner: &runtime.InitStepRunner{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, + TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTfDistribution, + DefaultTFVersion: defaultTfVersion, }, - PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfVersion, commitStatusUpdater, terraformClient), + PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion, commitStatusUpdater, terraformClient), ShowStepRunner: showStepRunner, PolicyCheckStepRunner: policyCheckStepRunner, ApplyStepRunner: &runtime.ApplyStepRunner{ - TerraformExecutor: terraformClient, - DefaultTFVersion: defaultTfVersion, - CommitStatusUpdater: commitStatusUpdater, - AsyncTFExec: terraformClient, + TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTfDistribution, + DefaultTFVersion: defaultTfVersion, + CommitStatusUpdater: commitStatusUpdater, + AsyncTFExec: terraformClient, }, RunStepRunner: runStepRunner, EnvStepRunner: &runtime.EnvStepRunner{ @@ -695,8 +699,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { TerraformExecutor: terraformClient, DefaultTFVersion: defaultTfVersion, }, - ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfVersion), - StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTfVersion), + ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion), + StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion), WorkingDir: workingDir, Webhooks: webhooksManager, WorkingDirLocker: workingDirLocker, diff --git a/server/user_config.go b/server/user_config.go index 10e6e6b9fc..27941c52b6 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -102,29 +102,31 @@ type UserConfig struct { SilenceVCSStatusNoPlans bool `mapstructure:"silence-vcs-status-no-plans"` // SilenceVCSStatusNoProjects is whether autoplan should set commit status if no projects // are found. - SilenceVCSStatusNoProjects bool `mapstructure:"silence-vcs-status-no-projects"` - SilenceAllowlistErrors bool `mapstructure:"silence-allowlist-errors"` - SkipCloneNoChanges bool `mapstructure:"skip-clone-no-changes"` - SlackToken string `mapstructure:"slack-token"` - SSLCertFile string `mapstructure:"ssl-cert-file"` - SSLKeyFile string `mapstructure:"ssl-key-file"` - RestrictFileList bool `mapstructure:"restrict-file-list"` - TFDistribution string `mapstructure:"tf-distribution"` - TFDownload bool `mapstructure:"tf-download"` - TFDownloadURL string `mapstructure:"tf-download-url"` - TFEHostname string `mapstructure:"tfe-hostname"` - TFELocalExecutionMode bool `mapstructure:"tfe-local-execution-mode"` - TFEToken string `mapstructure:"tfe-token"` - VarFileAllowlist string `mapstructure:"var-file-allowlist"` - VCSStatusName string `mapstructure:"vcs-status-name"` - DefaultTFVersion string `mapstructure:"default-tf-version"` - Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"` - WebBasicAuth bool `mapstructure:"web-basic-auth"` - WebUsername string `mapstructure:"web-username"` - WebPassword string `mapstructure:"web-password"` - WriteGitCreds bool `mapstructure:"write-git-creds"` - WebsocketCheckOrigin bool `mapstructure:"websocket-check-origin"` - UseTFPluginCache bool `mapstructure:"use-tf-plugin-cache"` + SilenceVCSStatusNoProjects bool `mapstructure:"silence-vcs-status-no-projects"` + SilenceAllowlistErrors bool `mapstructure:"silence-allowlist-errors"` + SkipCloneNoChanges bool `mapstructure:"skip-clone-no-changes"` + SlackToken string `mapstructure:"slack-token"` + SSLCertFile string `mapstructure:"ssl-cert-file"` + SSLKeyFile string `mapstructure:"ssl-key-file"` + RestrictFileList bool `mapstructure:"restrict-file-list"` + // TFDistribution is deprecated in favor of DefaultTFDistribution + TFDistribution string `mapstructure:"tf-distribution"` + TFDownload bool `mapstructure:"tf-download"` + TFDownloadURL string `mapstructure:"tf-download-url"` + TFEHostname string `mapstructure:"tfe-hostname"` + TFELocalExecutionMode bool `mapstructure:"tfe-local-execution-mode"` + TFEToken string `mapstructure:"tfe-token"` + VarFileAllowlist string `mapstructure:"var-file-allowlist"` + VCSStatusName string `mapstructure:"vcs-status-name"` + DefaultTFDistribution string `mapstructure:"default-tf-distribution"` + DefaultTFVersion string `mapstructure:"default-tf-version"` + Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"` + WebBasicAuth bool `mapstructure:"web-basic-auth"` + WebUsername string `mapstructure:"web-username"` + WebPassword string `mapstructure:"web-password"` + WriteGitCreds bool `mapstructure:"write-git-creds"` + WebsocketCheckOrigin bool `mapstructure:"websocket-check-origin"` + UseTFPluginCache bool `mapstructure:"use-tf-plugin-cache"` } // ToAllowCommandNames parse AllowCommands into a slice of CommandName