mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 05:48:21 +00:00
Implement plan step.
This commit is contained in:
@@ -31,7 +31,7 @@ func TestRun_NoDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRun_NoPlanFile(t *testing.T) {
|
||||
tmpDir, cleanup := applyTestTmpDir(t)
|
||||
tmpDir, cleanup := tmpDir_stepTests(t)
|
||||
defer cleanup()
|
||||
|
||||
s := atlantisyaml.ApplyStep{
|
||||
@@ -49,7 +49,7 @@ func TestRun_NoPlanFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRun_Success(t *testing.T) {
|
||||
tmpDir, cleanup := applyTestTmpDir(t)
|
||||
tmpDir, cleanup := tmpDir_stepTests(t)
|
||||
defer cleanup()
|
||||
planPath := filepath.Join(tmpDir, "workspace.tfplan")
|
||||
err := ioutil.WriteFile(planPath, nil, 0644)
|
||||
@@ -80,9 +80,9 @@ func TestRun_Success(t *testing.T) {
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(nil, tmpDir, []string{"apply", "-no-color", "extra", "args", "comment", "args", planPath}, tfVersion, "workspace")
|
||||
}
|
||||
|
||||
// applyTestTmpDir creates a temporary directory and returns its path along
|
||||
// tmpDir_stepTests creates a temporary directory and returns its path along
|
||||
// with a cleanup function to be called via defer.
|
||||
func applyTestTmpDir(t *testing.T) (string, func()) {
|
||||
func tmpDir_stepTests(t *testing.T) (string, func()) {
|
||||
tmpDir, err := ioutil.TempDir("", "")
|
||||
Ok(t, err)
|
||||
return tmpDir, func() { os.RemoveAll(tmpDir) }
|
||||
|
||||
94
server/events/atlantisyaml/plan_step.go
Normal file
94
server/events/atlantisyaml/plan_step.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package atlantisyaml
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// atlantisUserTFVar is the name of the variable we execute terraform
|
||||
// with, containing the vcs username of who is running the command
|
||||
const atlantisUserTFVar = "atlantis_user"
|
||||
const defaultWorkspace = "default"
|
||||
|
||||
// PlanStep runs `terraform plan`.
|
||||
type PlanStep struct {
|
||||
ExtraArgs []string
|
||||
TerraformExecutor TerraformExec
|
||||
Meta StepMeta
|
||||
}
|
||||
|
||||
func (p *PlanStep) Run() (string, error) {
|
||||
// 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.
|
||||
// In newer versions, the TF_WORKSPACE env var is respected and will handle
|
||||
// using the right workspace and even creating it if it doesn't exist.
|
||||
// This variable is set inside the Terraform executor.
|
||||
if err := p.switchWorkspace(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
planFile := filepath.Join(p.Meta.AbsolutePath, fmt.Sprintf("%s.tfplan", p.Meta.Workspace))
|
||||
userVar := fmt.Sprintf("%s=%s", atlantisUserTFVar, p.Meta.Username)
|
||||
tfPlanCmd := append(append([]string{"plan", "-refresh", "-no-color", "-out", planFile, "-var", userVar}, p.ExtraArgs...), p.Meta.ExtraCommentArgs...)
|
||||
|
||||
// Check if env/{workspace}.tfvars exist and include it. This is a use-case
|
||||
// from Hootsuite where Atlantis was first created so we're keeping this as
|
||||
// an homage and a favor so they don't need to refactor all their repos.
|
||||
// It's also a nice way to structure your repos to reduce duplication.
|
||||
optionalEnvFile := filepath.Join(p.Meta.AbsolutePath, "env", p.Meta.Workspace+".tfvars")
|
||||
if _, err := os.Stat(optionalEnvFile); err == nil {
|
||||
tfPlanCmd = append(tfPlanCmd, "-var-file", optionalEnvFile)
|
||||
}
|
||||
|
||||
return p.TerraformExecutor.RunCommandWithVersion(p.Meta.Log, filepath.Join(p.Meta.AbsolutePath), tfPlanCmd, p.Meta.TerraformVersion, p.Meta.Workspace)
|
||||
}
|
||||
|
||||
// switchWorkspace changes the terraform workspace if necessary and will create
|
||||
// it if it doesn't exist. It handles differences between versions.
|
||||
func (p *PlanStep) switchWorkspace() error {
|
||||
// In versions less than 0.9 there is no support for workspaces.
|
||||
noWorkspaceSupport := MustConstraint("<0.9").Check(p.Meta.TerraformVersion)
|
||||
if noWorkspaceSupport && p.Meta.Workspace != defaultWorkspace {
|
||||
return fmt.Errorf("terraform version %s does not support workspaces", p.Meta.TerraformVersion)
|
||||
}
|
||||
if noWorkspaceSupport {
|
||||
return nil
|
||||
}
|
||||
|
||||
// In version 0.9.* the workspace command was called env.
|
||||
workspaceCmd := "workspace"
|
||||
runningZeroPointNine := MustConstraint(">=0.9,<0.10").Check(p.Meta.TerraformVersion)
|
||||
if runningZeroPointNine {
|
||||
workspaceCmd = "env"
|
||||
}
|
||||
|
||||
// Use `workspace show` to find out what workspace we're in now. If we're
|
||||
// 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 := p.TerraformExecutor.RunCommandWithVersion(p.Meta.Log, p.Meta.AbsolutePath, []string{workspaceCmd, "show"}, p.Meta.TerraformVersion, p.Meta.Workspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If `show` says we're already on this workspace then we're done.
|
||||
if strings.TrimSpace(workspaceShowOutput) == p.Meta.Workspace {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Finally we'll have to select the workspace. Although we end up running
|
||||
// with TF_WORKSPACE set, we need to figure out if this workspace exists so
|
||||
// we can create it if it doesn't. 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 := p.TerraformExecutor.RunCommandWithVersion(p.Meta.Log, p.Meta.AbsolutePath, []string{workspaceCmd, "select", "-no-color", p.Meta.Workspace}, p.Meta.TerraformVersion, p.Meta.Workspace)
|
||||
if err != nil {
|
||||
// If terraform workspace select fails we run terraform workspace
|
||||
// new to create a new workspace automatically.
|
||||
_, err = p.TerraformExecutor.RunCommandWithVersion(p.Meta.Log, p.Meta.AbsolutePath, []string{workspaceCmd, "new", "-no-color", p.Meta.Workspace}, p.Meta.TerraformVersion, p.Meta.Workspace)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
285
server/events/atlantisyaml/plan_step_test.go
Normal file
285
server/events/atlantisyaml/plan_step_test.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package atlantisyaml_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
. "github.com/petergtz/pegomock"
|
||||
"github.com/runatlantis/atlantis/server/events/atlantisyaml"
|
||||
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
|
||||
matchers2 "github.com/runatlantis/atlantis/server/events/run/mocks/matchers"
|
||||
"github.com/runatlantis/atlantis/server/events/terraform/mocks"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
func TestRun_NoWorkspaceIn08(t *testing.T) {
|
||||
// We don't want any workspace commands to be run in 0.8.
|
||||
RegisterMockTestingT(t)
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.8")
|
||||
logger := logging.NewNoopLogger()
|
||||
workspace := "default"
|
||||
s := atlantisyaml.PlanStep{
|
||||
Meta: atlantisyaml.StepMeta{
|
||||
Log: logger,
|
||||
Workspace: workspace,
|
||||
AbsolutePath: "/path",
|
||||
DirRelativeToRepoRoot: ".",
|
||||
TerraformVersion: tfVersion,
|
||||
ExtraCommentArgs: []string{"comment", "args"},
|
||||
Username: "username",
|
||||
},
|
||||
ExtraArgs: []string{"extra", "args"},
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := s.Run()
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", []string{"plan", "-refresh", "-no-color", "-out", "/path/default.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}, tfVersion, workspace)
|
||||
|
||||
// Verify that no env or workspace commands were run
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, "/path", []string{"env", "select", "-no-color", "workspace"}, tfVersion, workspace)
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, "/path", []string{"workspace", "select", "-no-color", "workspace"}, tfVersion, workspace)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
tfVersion, _ := version.NewVersion("0.8")
|
||||
logger := logging.NewNoopLogger()
|
||||
workspace := "notdefault"
|
||||
s := atlantisyaml.PlanStep{
|
||||
Meta: atlantisyaml.StepMeta{
|
||||
Log: logger,
|
||||
Workspace: workspace,
|
||||
AbsolutePath: "/path",
|
||||
DirRelativeToRepoRoot: ".",
|
||||
TerraformVersion: tfVersion,
|
||||
ExtraCommentArgs: []string{"comment", "args"},
|
||||
Username: "username",
|
||||
},
|
||||
ExtraArgs: []string{"extra", "args"},
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
_, err := s.Run()
|
||||
ErrEquals(t, "terraform version 0.8.0 does not support workspaces", err)
|
||||
}
|
||||
|
||||
func TestRun_SwitchesWorkspace(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
cases := []struct {
|
||||
tfVersion string
|
||||
expWorkspaceCmd string
|
||||
}{
|
||||
{
|
||||
"0.9.0",
|
||||
"env",
|
||||
},
|
||||
{
|
||||
"0.9.11",
|
||||
"env",
|
||||
},
|
||||
{
|
||||
"0.10.0",
|
||||
"workspace",
|
||||
},
|
||||
{
|
||||
"0.11.0",
|
||||
"workspace",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.tfVersion, func(t *testing.T) {
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
tfVersion, _ := version.NewVersion(c.tfVersion)
|
||||
logger := logging.NewNoopLogger()
|
||||
s := atlantisyaml.PlanStep{
|
||||
Meta: atlantisyaml.StepMeta{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
AbsolutePath: "/path",
|
||||
DirRelativeToRepoRoot: ".",
|
||||
TerraformVersion: tfVersion,
|
||||
ExtraCommentArgs: []string{"comment", "args"},
|
||||
Username: "username",
|
||||
},
|
||||
ExtraArgs: []string{"extra", "args"},
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := s.Run()
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
// Verify that env select was called as well as plan.
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", []string{c.expWorkspaceCmd, "select", "-no-color", "workspace"}, tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", []string{"plan", "-refresh", "-no-color", "-out", "/path/workspace.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}, tfVersion, "workspace")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_CreatesWorkspace(t *testing.T) {
|
||||
// Test that if `workspace select` fails, we call `workspace new`.
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
cases := []struct {
|
||||
tfVersion string
|
||||
expWorkspaceCommand string
|
||||
}{
|
||||
{
|
||||
"0.9.0",
|
||||
"env",
|
||||
},
|
||||
{
|
||||
"0.9.11",
|
||||
"env",
|
||||
},
|
||||
{
|
||||
"0.10.0",
|
||||
"workspace",
|
||||
},
|
||||
{
|
||||
"0.11.0",
|
||||
"workspace",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.tfVersion, func(t *testing.T) {
|
||||
terraform := mocks.NewMockClient()
|
||||
tfVersion, _ := version.NewVersion(c.tfVersion)
|
||||
logger := logging.NewNoopLogger()
|
||||
s := atlantisyaml.PlanStep{
|
||||
Meta: atlantisyaml.StepMeta{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
AbsolutePath: "/path",
|
||||
DirRelativeToRepoRoot: ".",
|
||||
TerraformVersion: tfVersion,
|
||||
ExtraCommentArgs: []string{"comment", "args"},
|
||||
Username: "username",
|
||||
},
|
||||
ExtraArgs: []string{"extra", "args"},
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
// Ensure that we actually try to switch workspaces by making the
|
||||
// output of `workspace show` to be a different name.
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil)
|
||||
|
||||
expWorkspaceArgs := []string{c.expWorkspaceCommand, "select", "-no-color", "workspace"}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", expWorkspaceArgs, tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist"))
|
||||
|
||||
expPlanArgs := []string{"plan", "-refresh", "-no-color", "-out", "/path/workspace.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
|
||||
output, err := s.Run()
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
// Verify that env select was called as well as plan.
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expWorkspaceArgs, tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
tfVersion, _ := version.NewVersion("0.10.0")
|
||||
logger := logging.NewNoopLogger()
|
||||
s := atlantisyaml.PlanStep{
|
||||
Meta: atlantisyaml.StepMeta{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
AbsolutePath: "/path",
|
||||
DirRelativeToRepoRoot: ".",
|
||||
TerraformVersion: tfVersion,
|
||||
ExtraCommentArgs: []string{"comment", "args"},
|
||||
Username: "username",
|
||||
},
|
||||
ExtraArgs: []string{"extra", "args"},
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", []string{"workspace", "show"}, tfVersion, "workspace")).ThenReturn("workspace\n", nil)
|
||||
|
||||
expPlanArgs := []string{"plan", "-refresh", "-no-color", "-out", "/path/workspace.tfplan", "-var", "atlantis_user=username", "extra", "args", "comment", "args"}
|
||||
When(terraform.RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
|
||||
output, err := s.Run()
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, "/path", expPlanArgs, tfVersion, "workspace")
|
||||
|
||||
// Verify that workspace select was never called.
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, "/path", []string{"workspace", "select", "-no-color", "workspace"}, tfVersion, "workspace")
|
||||
}
|
||||
|
||||
func TestRun_AddsEnvVarFile(t *testing.T) {
|
||||
// Test that if env/workspace.tfvars file exists we use -var-file option.
|
||||
RegisterMockTestingT(t)
|
||||
terraform := mocks.NewMockClient()
|
||||
|
||||
// Create the env/workspace.tfvars file.
|
||||
tmpDir, cleanup := tmpDir_stepTests(t)
|
||||
defer cleanup()
|
||||
err := os.MkdirAll(filepath.Join(tmpDir, "env"), 0700)
|
||||
Ok(t, err)
|
||||
envVarsFile := filepath.Join(tmpDir, "env/workspace.tfvars")
|
||||
err = ioutil.WriteFile(envVarsFile, nil, 0644)
|
||||
Ok(t, err)
|
||||
|
||||
// Using version >= 0.10 here so we don't expect any env commands.
|
||||
tfVersion, _ := version.NewVersion("0.10.0")
|
||||
logger := logging.NewNoopLogger()
|
||||
s := atlantisyaml.PlanStep{
|
||||
Meta: atlantisyaml.StepMeta{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
AbsolutePath: tmpDir,
|
||||
DirRelativeToRepoRoot: ".",
|
||||
TerraformVersion: tfVersion,
|
||||
ExtraCommentArgs: []string{"comment", "args"},
|
||||
Username: "username",
|
||||
},
|
||||
ExtraArgs: []string{"extra", "args"},
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
expPlanArgs := []string{"plan", "-refresh", "-no-color", "-out", filepath.Join(tmpDir, "workspace.tfplan"), "-var", "atlantis_user=username", "extra", "args", "comment", "args", "-var-file", envVarsFile}
|
||||
When(terraform.RunCommandWithVersion(logger, tmpDir, expPlanArgs, tfVersion, "workspace")).ThenReturn("output", nil)
|
||||
|
||||
output, err := s.Run()
|
||||
Ok(t, err)
|
||||
|
||||
Equals(t, "output", output)
|
||||
// Verify that env select was never called since we're in version >= 0.10
|
||||
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(logger, tmpDir, []string{"env", "select", "-no-color", "workspace"}, tfVersion, "workspace")
|
||||
terraform.VerifyWasCalledOnce().RunCommandWithVersion(logger, tmpDir, expPlanArgs, tfVersion, "workspace")
|
||||
}
|
||||
Reference in New Issue
Block a user