Skip to content

Commit

Permalink
fix: atlantis import on workspaces (runatlantis#2937)
Browse files Browse the repository at this point in the history
* atlantis import not works on workspace

* extract workspace_step_runner_delegate from plan_step_runner

* import step runner requires workspace delegate
  • Loading branch information
krrrr38 authored Jan 11, 2023
1 parent 9461766 commit c771493
Show file tree
Hide file tree
Showing 26 changed files with 769 additions and 547 deletions.
31 changes: 23 additions & 8 deletions server/controllers/events/events_controller_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,21 @@ func TestGitHubWorkflow(t *testing.T) {
{"exp-output-merge.txt"},
},
},
{
Description: "import workspace",
RepoDir: "import-workspace",
Comments: []string{
"atlantis import -d dir1 -w ops 'random_id.dummy1[0]' AA",
"atlantis import -p dir1-ops 'random_id.dummy2[0]' BB",
"atlantis plan -p dir1-ops",
},
ExpReplies: [][]string{
{"exp-output-import-dir1-ops-dummy1.txt"},
{"exp-output-import-dir1-ops-dummy2.txt"},
{"exp-output-plan.txt"},
{"exp-output-merge.txt"},
},
},
{
Description: "import single project with -var",
RepoDir: "import-single-project-var",
Expand Down Expand Up @@ -1054,6 +1069,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
silenceNoProjects := false

commitStatusUpdater := mocks.NewMockCommitStatusUpdater()
asyncTfExec := runtimemocks.NewMockAsyncTFExec()

mockPreWorkflowHookRunner = runtimemocks.NewMockPreWorkflowHookRunner()
preWorkflowHookURLGenerator := mocks.NewMockPreWorkflowHookURLGenerator()
Expand Down Expand Up @@ -1124,19 +1140,18 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTFVersion,
},
PlanStepRunner: &runtime.PlanStepRunner{
TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTFVersion,
},
PlanStepRunner: runtime.NewPlanStepRunner(
terraformClient,
defaultTFVersion,
commitStatusUpdater,
asyncTfExec,
),
ShowStepRunner: showStepRunner,
PolicyCheckStepRunner: policyCheckRunner,
ApplyStepRunner: &runtime.ApplyStepRunner{
TerraformExecutor: terraformClient,
},
ImportStepRunner: &runtime.ImportStepRunner{
TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTFVersion,
},
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFVersion),
RunStepRunner: &runtime.RunStepRunner{
TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTFVersion,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: 3
projects:
- name: dir1-ops
dir: dir1
workspace: ops
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
resource "random_id" "dummy1" {
count = terraform.workspace == "ops" ? 1 : 0

keepers = {}
byte_length = 1
}

resource "random_id" "dummy2" {
count = terraform.workspace == "ops" ? 1 : 0

keepers = {}
byte_length = 1
}

output "workspace" {
value = terraform.workspace
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Ran Import for project: `dir1-ops` dir: `dir1` workspace: `ops`

```diff
random_id.dummy1[0]: Importing from ID "AA"...
random_id.dummy1[0]: Import prepared!
Prepared random_id for import
random_id.dummy1[0]: Refreshing state... [id=AA]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.


```

* :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops`


Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Ran Import for project: `dir1-ops` dir: `dir1` workspace: `ops`

```diff
random_id.dummy2[0]: Importing from ID "BB"...
random_id.dummy2[0]: Import prepared!
Prepared random_id for import
random_id.dummy2[0]: Refreshing state... [id=BB]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.


```

* :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops`


Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Locks and plans deleted for the projects and workspaces modified in this pull request:

- dir: `dir1` workspace: `ops`
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Ran Plan for project: `dir1-ops` dir: `dir1` workspace: `ops`

```diff

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.

```

* :arrow_forward: To **apply** this plan, comment:
* `atlantis apply -p dir1-ops`
* :put_litter_in_its_place: To **delete** this plan click [here](lock-url)
* :repeat: To **plan** this project again, comment:
* `atlantis plan -p dir1-ops`

---
* :fast_forward: To **apply** all unapplied plans from this pull request, comment:
* `atlantis apply`
* :put_litter_in_its_place: To delete all plans and locks for the PR, comment:
* `atlantis unlock`
20 changes: 14 additions & 6 deletions server/core/runtime/import_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,29 @@ import (
"github.com/runatlantis/atlantis/server/events/command"
)

type ImportStepRunner struct {
TerraformExecutor TerraformExec
DefaultTFVersion *version.Version
type importStepRunner struct {
terraformExecutor TerraformExec
defaultTFVersion *version.Version
}

func (p *ImportStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
tfVersion := p.DefaultTFVersion
func NewImportStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version) Runner {
runner := &importStepRunner{
terraformExecutor: terraformExecutor,
defaultTFVersion: defaultTfVersion,
}
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner)
}

func (p *importStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
tfVersion := p.defaultTFVersion
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}

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, 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))
Expand Down
53 changes: 38 additions & 15 deletions server/core/runtime/import_step_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
matchers2 "github.com/runatlantis/atlantis/server/core/terraform/mocks/matchers"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
)
Expand All @@ -29,33 +28,57 @@ func TestImportStepRunner_Run_Success(t *testing.T) {
Log: logger,
EscapedCommentArgs: []string{"-var", "foo=bar", "addr", "id"},
Workspace: workspace,
RepoRelDir: ".",
User: models.User{Username: "username"},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
Owner: "owner",
Name: "repo",
},
}

RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
s := &ImportStepRunner{
TerraformExecutor: terraform,
DefaultTFVersion: tfVersion,
s := NewImportStepRunner(terraform, tfVersion)

When(terraform.RunCommandWithVersion(matchers.AnyCommandProjectContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
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)
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}

func TestImportStepRunner_Run_Workspace(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)

context := command.ProjectContext{
Log: logger,
EscapedCommentArgs: []string{"-var", "foo=bar", "addr", "id"},
Workspace: workspace,
}

RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
s := NewImportStepRunner(terraform, tfVersion)

When(terraform.RunCommandWithVersion(matchers.AnyCommandProjectContext(), AnyString(), AnyStringSlice(), matchers2.AnyMapOfStringToString(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
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)

// exec import
commands := []string{"import", "-var", "foo=bar", "addr", "id"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, "default")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace)

_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
10 changes: 5 additions & 5 deletions server/core/runtime/minimum_version_step_runner_delegate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"github.com/runatlantis/atlantis/server/events/command"
)

// MinimumVersionStepRunnerDelegate ensures that a given step runner can't run unless the command version being used
// minimumVersionStepRunnerDelegate ensures that a given step runner can't run unless the command version being used
// is greater than a provided minimum
type MinimumVersionStepRunnerDelegate struct {
type minimumVersionStepRunnerDelegate struct {
minimumVersion *version.Version
defaultTfVersion *version.Version
delegate Runner
Expand All @@ -20,17 +20,17 @@ func NewMinimumVersionStepRunnerDelegate(minimumVersionStr string, defaultVersio
minimumVersion, err := version.NewVersion(minimumVersionStr)

if err != nil {
return &MinimumVersionStepRunnerDelegate{}, errors.Wrap(err, "initializing minimum version")
return &minimumVersionStepRunnerDelegate{}, errors.Wrap(err, "initializing minimum version")
}

return &MinimumVersionStepRunnerDelegate{
return &minimumVersionStepRunnerDelegate{
minimumVersion: minimumVersion,
defaultTfVersion: defaultVersion,
delegate: delegate,
}, nil
}

func (r *MinimumVersionStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
func (r *minimumVersionStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
tfVersion := r.defaultTfVersion
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestRunMinimumVersionDelegate(t *testing.T) {
expectedOut := "some valid output from delegate"

t.Run("default version success", func(t *testing.T) {
subject := &MinimumVersionStepRunnerDelegate{
subject := &minimumVersionStepRunnerDelegate{
defaultTfVersion: tfVersion12,
minimumVersion: tfVersion12,
delegate: mockDelegate,
Expand All @@ -48,7 +48,7 @@ func TestRunMinimumVersionDelegate(t *testing.T) {
})

t.Run("ctx version success", func(t *testing.T) {
subject := &MinimumVersionStepRunnerDelegate{
subject := &minimumVersionStepRunnerDelegate{
defaultTfVersion: tfVersion11,
minimumVersion: tfVersion12,
delegate: mockDelegate,
Expand All @@ -72,7 +72,7 @@ func TestRunMinimumVersionDelegate(t *testing.T) {
})

t.Run("default version failure", func(t *testing.T) {
subject := &MinimumVersionStepRunnerDelegate{
subject := &minimumVersionStepRunnerDelegate{
defaultTfVersion: tfVersion11,
minimumVersion: tfVersion12,
delegate: mockDelegate,
Expand All @@ -94,7 +94,7 @@ func TestRunMinimumVersionDelegate(t *testing.T) {
})

t.Run("ctx version failure", func(t *testing.T) {
subject := &MinimumVersionStepRunnerDelegate{
subject := &minimumVersionStepRunnerDelegate{
defaultTfVersion: tfVersion12,
minimumVersion: tfVersion12,
delegate: mockDelegate,
Expand Down
33 changes: 33 additions & 0 deletions server/core/runtime/mocks/matchers/recv_chan_of_models_line.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c771493

Please sign in to comment.