From 4aed6c9fafca173128763ddd1b22098dea09d4c2 Mon Sep 17 00:00:00 2001 From: Andrew Borg Date: Fri, 20 Dec 2024 08:35:28 -0500 Subject: [PATCH] Add tests for TF Distribution and fix styles Signed-off-by: Andrew Borg --- cmd/server.go | 31 ++-- cmd/server_test.go | 40 +++++ .../events/events_controller_e2e_test.go | 1 + server/core/config/raw/project.go | 2 +- server/core/runtime/apply_step_runner_test.go | 38 ++++- .../core/runtime/import_step_runner_test.go | 41 +++++ server/core/runtime/init_step_runner_test.go | 67 ++++++++ server/core/runtime/plan_step_runner_test.go | 76 +++++++++ .../plan_type_step_runner_delegate_test.go | 145 ++++++++++++++++++ server/core/runtime/run_step_runner_test.go | 45 ++++-- server/core/runtime/show_step_runner_test.go | 30 ++++ .../core/runtime/state_rm_step_runner_test.go | 42 +++++ .../core/runtime/version_step_runner_test.go | 41 +++++ .../workspace_step_runner_delegate_test.go | 61 ++++++++ .../terraform/tfclient/terraform_client.go | 5 +- server/user_config.go | 49 +++--- 16 files changed, 655 insertions(+), 59 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index 895e3b84b9..aa8581e705 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -142,22 +142,21 @@ const ( SSLCertFileFlag = "ssl-cert-file" SSLKeyFileFlag = "ssl-key-file" RestrictFileList = "restrict-file-list" - // 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" + TFDistributionFlag = "tf-distribution" // deprecated for DefaultTFDistributionFlag + 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 = "" diff --git a/cmd/server_test.go b/cmd/server_test.go index af6c5eb136..7d7c1b52d5 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -978,6 +978,46 @@ func TestExecute_AutoplanFileList(t *testing.T) { } } +func TestExecute_ValidateDefaultTFDistribution(t *testing.T) { + cases := []struct { + description string + flags map[string]interface{} + expectErr string + }{ + { + "terraform", + map[string]interface{}{ + DefaultTFDistributionFlag: "terraform", + }, + "", + }, + { + "opentofu", + map[string]interface{}{ + DefaultTFDistributionFlag: "opentofu", + }, + "", + }, + { + "errs on invalid distribution", + map[string]interface{}{ + DefaultTFDistributionFlag: "invalid_distribution", + }, + "invalid tf distribution: expected one of terraform or opentofu", + }, + } + for _, testCase := range cases { + t.Log("Should validate default tf distribution when " + testCase.description) + c := setupWithDefaults(testCase.flags, t) + err := c.Execute() + if testCase.expectErr != "" { + ErrEquals(t, testCase.expectErr, err) + } else { + Ok(t, err) + } + } +} + func setup(flags map[string]interface{}, t *testing.T) *cobra.Command { vipr := viper.New() for k, v := range flags { diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 0f5e87b443..4a8c8e5ca6 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1474,6 +1474,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion), RunStepRunner: &runtime.RunStepRunner{ TerraformExecutor: terraformClient, + DefaultTFDistribution: defaultTFDistribution, DefaultTFVersion: defaultTFVersion, ProjectCmdOutputHandler: projectCmdOutputHandler, }, diff --git a/server/core/config/raw/project.go b/server/core/config/raw/project.go index 4fbb2811cc..5b389c8605 100644 --- a/server/core/config/raw/project.go +++ b/server/core/config/raw/project.go @@ -211,7 +211,7 @@ func validImportReq(value interface{}) error { 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 fmt.Errorf("'%s' is not a valid terraform_distribution, only '%s' and '%s' are supported", *distribution, "terraform", "opentofu") } return nil } diff --git a/server/core/runtime/apply_step_runner_test.go b/server/core/runtime/apply_step_runner_test.go index b3bb99545c..d9be33e1d6 100644 --- a/server/core/runtime/apply_step_runner_test.go +++ b/server/core/runtime/apply_step_runner_test.go @@ -113,7 +113,7 @@ func TestRun_AppliesCorrectProjectPlan(t *testing.T) { Assert(t, os.IsNotExist(err), "planfile should be deleted") } -func TestRun_UsesConfiguredTFVersion(t *testing.T) { +func TestApplyStepRunner_TestRun_UsesConfiguredTFVersion(t *testing.T) { tmpDir := t.TempDir() planPath := filepath.Join(tmpDir, "workspace.tfplan") err := os.WriteFile(planPath, nil, 0600) @@ -147,6 +147,42 @@ func TestRun_UsesConfiguredTFVersion(t *testing.T) { Assert(t, os.IsNotExist(err), "planfile should be deleted") } +func TestApplyStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, nil, 0600) + Ok(t, err) + + logger := logging.NewNoopLogger(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + tfVersion, _ := version.NewVersion("0.11.0") + projTFDistribution := "opentofu" + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &projTFDistribution, + Log: logger, + } + + RegisterMockTestingT(t) + terraform := tfclientmocks.NewMockClient() + o := runtime.ApplyStepRunner{ + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, + } + When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), NotEq[tf.Distribution](tfDistribution), 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(Eq(ctx), Eq(tmpDir), Eq([]string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}), Eq(map[string]string(nil)), NotEq[tf.Distribution](tfDistribution), Eq(tfVersion), Eq("workspace")) + _, err = os.Stat(planPath) + Assert(t, os.IsNotExist(err), "planfile should be deleted") +} + // Apply ignores the -target flag when used with a planfile so we should give // an error if it's being used with -target. func TestRun_UsingTarget(t *testing.T) { diff --git a/server/core/runtime/import_step_runner_test.go b/server/core/runtime/import_step_runner_test.go index 21c8d054d1..d7cacf9a5f 100644 --- a/server/core/runtime/import_step_runner_test.go +++ b/server/core/runtime/import_step_runner_test.go @@ -86,3 +86,44 @@ func TestImportStepRunner_Run_Workspace(t *testing.T) { _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } + +func TestImportStepRunner_Run_UsesConfiguredDistribution(t *testing.T) { + logger := logging.NewNoopLogger(t) + workspace := "something" + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, fmt.Sprintf("%s.tfplan", workspace)) + err := os.WriteFile(planPath, nil, 0600) + Ok(t, err) + + projTFDistribution := "opentofu" + context := command.ProjectContext{ + Log: logger, + EscapedCommentArgs: []string{"-var", "foo=bar", "addr", "id"}, + Workspace: workspace, + TerraformDistribution: &projTFDistribution, + } + + RegisterMockTestingT(t) + terraform := tfclientmocks.NewMockClient() + tfVersion, _ := version.NewVersion("0.15.0") + 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[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(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "show"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "select", workspace}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + + // exec import + commands := []string{"import", "-var", "foo=bar", "addr", "id"} + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq(commands), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + + _, err = os.Stat(planPath) + Assert(t, os.IsNotExist(err), "planfile should be deleted") +} diff --git a/server/core/runtime/init_step_runner_test.go b/server/core/runtime/init_step_runner_test.go index 989ca6a169..86d029c2d8 100644 --- a/server/core/runtime/init_step_runner_test.go +++ b/server/core/runtime/init_step_runner_test.go @@ -81,6 +81,73 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) { } } +func TestInitStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) { + RegisterMockTestingT(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + cases := []struct { + version string + distribution string + expCmd string + }{ + { + "0.8.9", + "opentofu", + "get", + }, + { + "0.8.9", + "terraform", + "get", + }, + { + "0.9.0", + "opentofu", + "init", + }, + { + "0.9.1", + "terraform", + "init", + }, + } + + for _, c := range cases { + t.Run(c.version, func(t *testing.T) { + terraform := tfclientmocks.NewMockClient() + + logger := logging.NewNoopLogger(t) + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + Log: logger, + TerraformDistribution: &c.distribution, + } + + tfVersion, _ := version.NewVersion(c.version) + iso := runtime.InitStepRunner{ + TerraformExecutor: terraform, + DefaultTFDistribution: tfDistribution, + DefaultTFVersion: tfVersion, + } + 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)) + Ok(t, err) + // When there is no error, should not return init output to PR. + Equals(t, "", output) + + // If using init then we specify -input=false but not for get. + expArgs := []string{c.expCmd, "-input=false", "-upgrade", "extra", "args"} + if c.expCmd == "get" { + expArgs = []string{c.expCmd, "-upgrade", "extra", "args"} + } + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq("/path"), Eq(expArgs), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("workspace")) + }) + } +} + func TestRun_ShowInitOutputOnError(t *testing.T) { // If there was an error during init then we want the output to be returned. RegisterMockTestingT(t) diff --git a/server/core/runtime/plan_step_runner_test.go b/server/core/runtime/plan_step_runner_test.go index c6e0b0e497..6a16b03e3f 100644 --- a/server/core/runtime/plan_step_runner_test.go +++ b/server/core/runtime/plan_step_runner_test.go @@ -554,6 +554,82 @@ Plan: 0 to add, 0 to change, 1 to destroy.`, output) } } +func TestPlanStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) { + RegisterMockTestingT(t) + + expPlanArgs := []string{ + "plan", + "-input=false", + "-refresh", + "-out", + fmt.Sprintf("%q", "/path/default.tfplan"), + "extra", + "args", + "comment", + "args", + } + + cases := []struct { + name string + tfVersion string + tfDistribution string + }{ + { + "stable version", + "0.12.0", + "terraform", + }, + { + "with prerelease", + "0.14.0-rc1", + "opentofu", + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + terraform := tfclientmocks.NewMockClient() + commitStatusUpdater := runtimemocks.NewMockStatusUpdater() + asyncTfExec := runtimemocks.NewMockAsyncTFExec() + 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) + + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + tfVersion, _ := version.NewVersion(c.tfVersion) + s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec) + ctx := command.ProjectContext{ + Workspace: "default", + RepoRelDir: ".", + User: models.User{Username: "username"}, + EscapedCommentArgs: []string{"comment", "args"}, + Pull: models.PullRequest{ + Num: 2, + }, + BaseRepo: models.Repo{ + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + }, + TerraformDistribution: &c.tfDistribution, + } + + output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil)) + Ok(t, err) + Equals(t, "output", output) + + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq("/path"), Eq(expPlanArgs), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("default")) + }) + } + +} + type remotePlanMock struct { // LinesToSend will be sent on the channel. LinesToSend string diff --git a/server/core/runtime/plan_type_step_runner_delegate_test.go b/server/core/runtime/plan_type_step_runner_delegate_test.go index 286ae9ad40..db4be0ff03 100644 --- a/server/core/runtime/plan_type_step_runner_delegate_test.go +++ b/server/core/runtime/plan_type_step_runner_delegate_test.go @@ -153,3 +153,148 @@ func TestRunDelegate(t *testing.T) { }) } + +var openTofuPlanFileContents = ` +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + - destroy + +OpenTofu will perform the following actions: + + - null_resource.hi[1] + + +Plan: 0 to add, 0 to change, 1 to destroy.` + +func TestRunDelegate_UsesConfiguredDistribution(t *testing.T) { + + RegisterMockTestingT(t) + + mockDefaultRunner := mocks.NewMockRunner() + mockRemoteRunner := mocks.NewMockRunner() + + subject := &planTypeStepRunnerDelegate{ + defaultRunner: mockDefaultRunner, + remotePlanRunner: mockRemoteRunner, + } + + tfDistribution := "opentofu" + tfVersion, _ := version.NewVersion("1.7.0") + + t.Run("Remote Runner Success", func(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, []byte("Atlantis: this plan was created by remote ops\n"+openTofuPlanFileContents), 0600) + Ok(t, err) + + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &tfDistribution, + TerraformVersion: tfVersion, + } + extraArgs := []string{"extra", "args"} + envs := map[string]string{} + + expectedOut := "some random output" + + When(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil) + + output, err := subject.Run(ctx, extraArgs, tmpDir, envs) + + mockDefaultRunner.VerifyWasCalled(Never()) + + Equals(t, expectedOut, output) + Ok(t, err) + + }) + + t.Run("Remote Runner Failure", func(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, []byte("Atlantis: this plan was created by remote ops\n"+openTofuPlanFileContents), 0600) + Ok(t, err) + + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &tfDistribution, + TerraformVersion: tfVersion, + } + extraArgs := []string{"extra", "args"} + envs := map[string]string{} + + expectedOut := "some random output" + + When(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New("err")) + + output, err := subject.Run(ctx, extraArgs, tmpDir, envs) + + mockDefaultRunner.VerifyWasCalled(Never()) + + Equals(t, expectedOut, output) + Assert(t, err != nil, "err should not be nil") + + }) + + t.Run("Local Runner Success", func(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, []byte(openTofuPlanFileContents), 0600) + Ok(t, err) + + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &tfDistribution, + TerraformVersion: tfVersion, + } + extraArgs := []string{"extra", "args"} + envs := map[string]string{} + + expectedOut := "some random output" + + When(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil) + + output, err := subject.Run(ctx, extraArgs, tmpDir, envs) + + mockRemoteRunner.VerifyWasCalled(Never()) + + Equals(t, expectedOut, output) + Ok(t, err) + + }) + + t.Run("Local Runner Failure", func(t *testing.T) { + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, "workspace.tfplan") + err := os.WriteFile(planPath, []byte(openTofuPlanFileContents), 0600) + Ok(t, err) + + ctx := command.ProjectContext{ + Workspace: "workspace", + RepoRelDir: ".", + EscapedCommentArgs: []string{"comment", "args"}, + TerraformDistribution: &tfDistribution, + TerraformVersion: tfVersion, + } + extraArgs := []string{"extra", "args"} + envs := map[string]string{} + + expectedOut := "some random output" + + When(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New("err")) + + output, err := subject.Run(ctx, extraArgs, tmpDir, envs) + + mockRemoteRunner.VerifyWasCalled(Never()) + + Equals(t, expectedOut, output) + Assert(t, err != nil, "err should not be nil") + + }) + +} diff --git a/server/core/runtime/run_step_runner_test.go b/server/core/runtime/run_step_runner_test.go index a2a6fb8a28..2429d88fe8 100644 --- a/server/core/runtime/run_step_runner_test.go +++ b/server/core/runtime/run_step_runner_test.go @@ -22,11 +22,12 @@ import ( func TestRunStepRunner_Run(t *testing.T) { cases := []struct { - Command string - ProjectName string - ExpOut string - ExpErr string - Version string + Command string + ProjectName string + ExpOut string + ExpErr string + Version string + Distribution string }{ { Command: "", @@ -71,6 +72,18 @@ func TestRunStepRunner_Run(t *testing.T) { ProjectName: "my/project/name", ExpOut: "workspace=myworkspace version=0.11.0 dir=$DIR planfile=$DIR/my::project::name-myworkspace.tfplan showfile=$DIR/my::project::name-myworkspace.json project=my/project/name\n", }, + { + Command: "echo distribution=$ATLANTIS_TERRAFORM_DISTRIBUTION", + ProjectName: "my/project/name", + ExpOut: "distribution=terraform\n", + Distribution: "terraform", + }, + { + Command: "echo distribution=$ATLANTIS_TERRAFORM_DISTRIBUTION", + ProjectName: "my/project/name", + ExpOut: "distribution=tofu\n", + Distribution: "opentofu", + }, { Command: "echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_url=$PULL_URL pull_author=$PULL_AUTHOR repo_rel_dir=$REPO_REL_DIR", ExpOut: "base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=main pull_num=2 pull_url=https://github.com/runatlantis/atlantis/pull/2 pull_author=acme repo_rel_dir=mydir\n", @@ -102,6 +115,11 @@ func TestRunStepRunner_Run(t *testing.T) { Ok(t, err) + projTFDistribution := "terraform" + if c.Distribution != "" { + projTFDistribution = c.Distribution + } + defaultVersion, _ := version.NewVersion("0.8") RegisterMockTestingT(t) @@ -142,12 +160,13 @@ func TestRunStepRunner_Run(t *testing.T) { User: models.User{ Username: "acme-user", }, - Log: logger, - Workspace: "myworkspace", - RepoRelDir: "mydir", - TerraformVersion: projVersion, - ProjectName: c.ProjectName, - EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"}, + Log: logger, + Workspace: "myworkspace", + RepoRelDir: "mydir", + TerraformDistribution: &projTFDistribution, + TerraformVersion: projVersion, + ProjectName: c.ProjectName, + EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"}, } out, err := r.Run(ctx, nil, c.Command, tmpDir, map[string]string{"test": "var"}, true, valid.PostProcessRunOutputShow) if c.ExpErr != "" { @@ -161,8 +180,8 @@ func TestRunStepRunner_Run(t *testing.T) { expOut := strings.Replace(c.ExpOut, "$DIR", tmpDir, -1) Equals(t, expOut, out) - terraform.VerifyWasCalledOnce().EnsureVersion(logger, defaultDistribution, projVersion) - terraform.VerifyWasCalled(Never()).EnsureVersion(logger, defaultDistribution, defaultVersion) + terraform.VerifyWasCalledOnce().EnsureVersion(Eq(logger), NotEq(defaultDistribution), Eq(projVersion)) + terraform.VerifyWasCalled(Never()).EnsureVersion(Eq(logger), Eq(defaultDistribution), Eq(defaultVersion)) }) } diff --git a/server/core/runtime/show_step_runner_test.go b/server/core/runtime/show_step_runner_test.go index 87c717d8d6..8c390014ad 100644 --- a/server/core/runtime/show_step_runner_test.go +++ b/server/core/runtime/show_step_runner_test.go @@ -88,6 +88,36 @@ func TestShowStepRunnner(t *testing.T) { }) + t.Run("success w/ distribution override", func(t *testing.T) { + + v, _ := version.NewVersion("0.13.0") + mockDownloader := mocks.NewMockDownloader() + d := tf.NewDistributionTerraformWithDownloader(mockDownloader) + projTFDistribution := "opentofu" + + contextWithDistributionOverride := command.ProjectContext{ + Workspace: "default", + ProjectName: "test", + Log: logger, + TerraformDistribution: &projTFDistribution, + } + + When(mockExecutor.RunCommandWithVersion( + Eq(contextWithDistributionOverride), Eq(path), Eq([]string{"show", "-json", filepath.Join(path, "test-default.tfplan")}), Eq(envs), NotEq(d), NotEq(v), Eq(context.Workspace), + )).ThenReturn("success", nil) + + r, err := subject.Run(contextWithDistributionOverride, []string{}, path, envs) + + Ok(t, err) + + actual, _ := os.ReadFile(resultPath) + + actualStr := string(actual) + Assert(t, actualStr == "success", "got expected result") + Assert(t, r == "success", "returned expected result") + + }) + t.Run("failure running command", func(t *testing.T) { When(mockExecutor.RunCommandWithVersion( context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfDistribution, tfVersion, context.Workspace, diff --git a/server/core/runtime/state_rm_step_runner_test.go b/server/core/runtime/state_rm_step_runner_test.go index 5de1f63c5f..194879f2bd 100644 --- a/server/core/runtime/state_rm_step_runner_test.go +++ b/server/core/runtime/state_rm_step_runner_test.go @@ -86,3 +86,45 @@ func TestStateRmStepRunner_Run_Workspace(t *testing.T) { _, err = os.Stat(planPath) Assert(t, os.IsNotExist(err), "planfile should be deleted") } + +func TestStateRmStepRunner_Run_UsesConfiguredDistribution(t *testing.T) { + logger := logging.NewNoopLogger(t) + workspace := "something" + tmpDir := t.TempDir() + planPath := filepath.Join(tmpDir, fmt.Sprintf("%s.tfplan", workspace)) + err := os.WriteFile(planPath, nil, 0600) + Ok(t, err) + + projTFDistribution := "opentofu" + + context := command.ProjectContext{ + Log: logger, + EscapedCommentArgs: []string{"-lock=false", "addr1", "addr2", "addr3"}, + Workspace: workspace, + TerraformDistribution: &projTFDistribution, + } + + RegisterMockTestingT(t) + terraform := tfclientmocks.NewMockClient() + tfVersion, _ := version.NewVersion("0.15.0") + 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[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(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "show"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "select", workspace}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + + // exec state rm + commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"} + terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq(commands), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace)) + + _, err = os.Stat(planPath) + Assert(t, os.IsNotExist(err), "planfile should be deleted") +} diff --git a/server/core/runtime/version_step_runner_test.go b/server/core/runtime/version_step_runner_test.go index 242059c586..45bf890fab 100644 --- a/server/core/runtime/version_step_runner_test.go +++ b/server/core/runtime/version_step_runner_test.go @@ -53,3 +53,44 @@ func TestRunVersionStep(t *testing.T) { Ok(t, err) }) } + +func TestVersionStepRunner_Run_UsesConfiguredDistribution(t *testing.T) { + RegisterMockTestingT(t) + logger := logging.NewNoopLogger(t) + workspace := "default" + projTFDistribution := "opentofu" + context := command.ProjectContext{ + Log: logger, + EscapedCommentArgs: []string{"comment", "args"}, + Workspace: workspace, + RepoRelDir: ".", + User: models.User{Username: "username"}, + Pull: models.PullRequest{ + Num: 2, + }, + BaseRepo: models.Repo{ + FullName: "owner/repo", + Owner: "owner", + Name: "repo", + }, + TerraformDistribution: &projTFDistribution, + } + + terraform := tfclientmocks.NewMockClient() + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + tfVersion, _ := version.NewVersion("0.15.0") + tmpDir := t.TempDir() + + s := &VersionStepRunner{ + 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(Eq(context), Eq(tmpDir), Eq([]string{"version"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("default")) + Ok(t, err) + }) +} diff --git a/server/core/runtime/workspace_step_runner_delegate_test.go b/server/core/runtime/workspace_step_runner_delegate_test.go index ae1d85a772..e705e93b00 100644 --- a/server/core/runtime/workspace_step_runner_delegate_test.go +++ b/server/core/runtime/workspace_step_runner_delegate_test.go @@ -128,6 +128,67 @@ func TestRun_SwitchesWorkspace(t *testing.T) { } } +func TestRun_SwitchesWorkspaceDistribution(t *testing.T) { + RegisterMockTestingT(t) + mockDownloader := mocks.NewMockDownloader() + tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader) + + cases := []struct { + tfVersion string + tfDistribution string + expWorkspaceCmd string + }{ + { + "0.9.0", + "opentofu", + "env", + }, + { + "0.9.11", + "terraform", + "env", + }, + { + "0.10.0", + "terraform", + "workspace", + }, + { + "0.11.0", + "opentofu", + "workspace", + }, + } + + for _, c := range cases { + t.Run(c.tfVersion, func(t *testing.T) { + terraform := tfclientmocks.NewMockClient() + tfVersion, _ := version.NewVersion(c.tfVersion) + logger := logging.NewNoopLogger(t) + ctx := command.ProjectContext{ + Log: logger, + Workspace: "workspace", + TerraformDistribution: &c.tfDistribution, + } + s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{}) + + _, 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(Eq(ctx), + Eq("/path"), + Eq([]string{c.expWorkspaceCmd, + "select", + "workspace"}), + Eq(map[string]string(nil)), + NotEq(tfDistribution), + Eq(tfVersion), + Eq("workspace")) + }) + } +} + func TestRun_CreatesWorkspace(t *testing.T) { // Test that if `workspace select` fails, we call `workspace new`. RegisterMockTestingT(t) diff --git a/server/core/terraform/tfclient/terraform_client.go b/server/core/terraform/tfclient/terraform_client.go index 603153c249..5ef864db79 100644 --- a/server/core/terraform/tfclient/terraform_client.go +++ b/server/core/terraform/tfclient/terraform_client.go @@ -13,7 +13,7 @@ // limitations under the License. // Modified hereafter by contributors to runatlantis/atlantis. // -// Package terraform handles the actual running of terraform commands. +// Package tfclient handles the actual running of terraform commands. package tfclient import ( @@ -32,9 +32,8 @@ 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" "github.com/runatlantis/atlantis/server/core/terraform/ansi" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/jobs" diff --git a/server/user_config.go b/server/user_config.go index 27941c52b6..9cd4f54675 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -102,31 +102,30 @@ 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 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"` + 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"` // deprecated in favor of DefaultTFDistribution + 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