mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 01:18:27 +00:00
439 lines
13 KiB
Go
439 lines
13 KiB
Go
// Copyright 2017 HootSuite Media Inc.
|
||
//
|
||
// Licensed under the Apache License, Version 2.0 (the License);
|
||
// you may not use this file except in compliance with the License.
|
||
// You may obtain a copy of the License at
|
||
// http://www.apache.org/licenses/LICENSE-2.0
|
||
// Unless required by applicable law or agreed to in writing, software
|
||
// distributed under the License is distributed on an AS IS BASIS,
|
||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
// See the License for the specific language governing permissions and
|
||
// limitations under the License.
|
||
// Modified hereafter by contributors to runatlantis/atlantis.
|
||
|
||
package events_test
|
||
|
||
import (
|
||
"os"
|
||
"testing"
|
||
|
||
"github.com/hashicorp/go-version"
|
||
. "github.com/petergtz/pegomock"
|
||
"github.com/runatlantis/atlantis/server/events"
|
||
"github.com/runatlantis/atlantis/server/events/mocks"
|
||
"github.com/runatlantis/atlantis/server/events/mocks/matchers"
|
||
"github.com/runatlantis/atlantis/server/events/models"
|
||
"github.com/runatlantis/atlantis/server/events/runtime"
|
||
mocks2 "github.com/runatlantis/atlantis/server/events/runtime/mocks"
|
||
tmocks "github.com/runatlantis/atlantis/server/events/terraform/mocks"
|
||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||
"github.com/runatlantis/atlantis/server/logging"
|
||
. "github.com/runatlantis/atlantis/testing"
|
||
)
|
||
|
||
// Test that it runs the expected plan steps.
|
||
func TestDefaultProjectCommandRunner_Plan(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockInit := mocks.NewMockStepRunner()
|
||
mockPlan := mocks.NewMockStepRunner()
|
||
mockApply := mocks.NewMockStepRunner()
|
||
mockRun := mocks.NewMockCustomStepRunner()
|
||
realEnv := runtime.EnvStepRunner{}
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
InitStepRunner: mockInit,
|
||
PlanStepRunner: mockPlan,
|
||
ApplyStepRunner: mockApply,
|
||
RunStepRunner: mockRun,
|
||
EnvStepRunner: &realEnv,
|
||
PullApprovedChecker: nil,
|
||
WorkingDir: mockWorkingDir,
|
||
Webhooks: nil,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
}
|
||
|
||
repoDir, cleanup := TempDir(t)
|
||
defer cleanup()
|
||
When(mockWorkingDir.Clone(
|
||
matchers.AnyPtrToLoggingSimpleLogger(),
|
||
matchers.AnyModelsRepo(),
|
||
matchers.AnyModelsPullRequest(),
|
||
AnyString(),
|
||
)).ThenReturn(repoDir, false, nil)
|
||
When(mockLocker.TryLock(
|
||
matchers.AnyPtrToLoggingSimpleLogger(),
|
||
matchers.AnyModelsPullRequest(),
|
||
matchers.AnyModelsUser(),
|
||
AnyString(),
|
||
matchers.AnyModelsProject(),
|
||
)).ThenReturn(&events.TryLockResponse{
|
||
LockAcquired: true,
|
||
LockKey: "lock-key",
|
||
}, nil)
|
||
|
||
expEnvs := map[string]string{
|
||
"name": "value",
|
||
}
|
||
ctx := models.ProjectCommandContext{
|
||
Log: logging.NewNoopLogger(),
|
||
Steps: []valid.Step{
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "name",
|
||
EnvVarValue: "value",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
},
|
||
{
|
||
StepName: "apply",
|
||
},
|
||
{
|
||
StepName: "plan",
|
||
},
|
||
{
|
||
StepName: "init",
|
||
},
|
||
},
|
||
Workspace: "default",
|
||
RepoRelDir: ".",
|
||
}
|
||
// Each step will output its step name.
|
||
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
|
||
When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil)
|
||
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil)
|
||
When(mockRun.Run(ctx, "", repoDir, expEnvs)).ThenReturn("run", nil)
|
||
res := runner.Plan(ctx)
|
||
|
||
Assert(t, res.PlanSuccess != nil, "exp plan success")
|
||
Equals(t, "https://lock-key", res.PlanSuccess.LockURL)
|
||
Equals(t, "run\napply\nplan\ninit", res.PlanSuccess.TerraformOutput)
|
||
|
||
expSteps := []string{"run", "apply", "plan", "init", "env"}
|
||
for _, step := range expSteps {
|
||
switch step {
|
||
case "init":
|
||
mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "plan":
|
||
mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "apply":
|
||
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "run":
|
||
mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Test what happens if there's no working dir. This signals that the project
|
||
// was never planned.
|
||
func TestDefaultProjectCommandRunner_ApplyNotCloned(t *testing.T) {
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
runner := &events.DefaultProjectCommandRunner{
|
||
WorkingDir: mockWorkingDir,
|
||
}
|
||
ctx := models.ProjectCommandContext{}
|
||
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn("", os.ErrNotExist)
|
||
|
||
res := runner.Apply(ctx)
|
||
ErrEquals(t, "project has not been cloned–did you run plan?", res.Error)
|
||
}
|
||
|
||
// Test that if approval is required and the PR isn't approved we give an error.
|
||
func TestDefaultProjectCommandRunner_ApplyNotApproved(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockApproved := mocks2.NewMockPullApprovedChecker()
|
||
runner := &events.DefaultProjectCommandRunner{
|
||
WorkingDir: mockWorkingDir,
|
||
PullApprovedChecker: mockApproved,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
}
|
||
ctx := models.ProjectCommandContext{
|
||
ApplyRequirements: []string{"approved"},
|
||
}
|
||
tmp, cleanup := TempDir(t)
|
||
defer cleanup()
|
||
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)
|
||
When(mockApproved.PullIsApproved(ctx.BaseRepo, ctx.Pull)).ThenReturn(false, nil)
|
||
|
||
res := runner.Apply(ctx)
|
||
Equals(t, "Pull request must be approved by at least one person other than the author before running apply.", res.Failure)
|
||
}
|
||
|
||
// Test that if mergeable is required and the PR isn't mergeable we give an error.
|
||
func TestDefaultProjectCommandRunner_ApplyNotMergeable(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
runner := &events.DefaultProjectCommandRunner{
|
||
WorkingDir: mockWorkingDir,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
}
|
||
ctx := models.ProjectCommandContext{
|
||
PullMergeable: false,
|
||
ApplyRequirements: []string{"mergeable"},
|
||
}
|
||
tmp, cleanup := TempDir(t)
|
||
defer cleanup()
|
||
When(mockWorkingDir.GetWorkingDir(ctx.BaseRepo, ctx.Pull, ctx.Workspace)).ThenReturn(tmp, nil)
|
||
|
||
res := runner.Apply(ctx)
|
||
Equals(t, "Pull request must be mergeable before running apply.", res.Failure)
|
||
}
|
||
|
||
// Test that it runs the expected apply steps.
|
||
func TestDefaultProjectCommandRunner_Apply(t *testing.T) {
|
||
cases := []struct {
|
||
description string
|
||
steps []valid.Step
|
||
applyReqs []string
|
||
|
||
expSteps []string
|
||
expOut string
|
||
expFailure string
|
||
pullMergeable bool
|
||
}{
|
||
{
|
||
description: "normal workflow",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
expSteps: []string{"apply"},
|
||
expOut: "apply",
|
||
},
|
||
{
|
||
description: "approval required",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
applyReqs: []string{"approved"},
|
||
expSteps: []string{"approve", "apply"},
|
||
expOut: "apply",
|
||
},
|
||
{
|
||
description: "mergeable required",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
pullMergeable: true,
|
||
applyReqs: []string{"mergeable"},
|
||
expSteps: []string{"apply"},
|
||
expOut: "apply",
|
||
},
|
||
{
|
||
description: "mergeable required, pull not mergeable",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
pullMergeable: false,
|
||
applyReqs: []string{"mergeable"},
|
||
expSteps: []string{""},
|
||
expOut: "",
|
||
expFailure: "Pull request must be mergeable before running apply.",
|
||
},
|
||
{
|
||
description: "mergeable and approved required",
|
||
steps: valid.DefaultApplyStage.Steps,
|
||
pullMergeable: true,
|
||
applyReqs: []string{"mergeable", "approved"},
|
||
expSteps: []string{"approved", "apply"},
|
||
expOut: "apply",
|
||
},
|
||
{
|
||
description: "workflow with custom apply stage",
|
||
steps: []valid.Step{
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "key",
|
||
EnvVarValue: "value",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
},
|
||
{
|
||
StepName: "apply",
|
||
},
|
||
{
|
||
StepName: "plan",
|
||
},
|
||
{
|
||
StepName: "init",
|
||
},
|
||
},
|
||
expSteps: []string{"env", "run", "apply", "plan", "init"},
|
||
expOut: "run\napply\nplan\ninit",
|
||
},
|
||
}
|
||
|
||
for _, c := range cases {
|
||
if c.description != "workflow with custom apply stage" {
|
||
continue
|
||
}
|
||
t.Run(c.description, func(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
mockInit := mocks.NewMockStepRunner()
|
||
mockPlan := mocks.NewMockStepRunner()
|
||
mockApply := mocks.NewMockStepRunner()
|
||
mockRun := mocks.NewMockCustomStepRunner()
|
||
mockEnv := mocks.NewMockEnvStepRunner()
|
||
mockApproved := mocks2.NewMockPullApprovedChecker()
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
mockSender := mocks.NewMockWebhooksSender()
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
InitStepRunner: mockInit,
|
||
PlanStepRunner: mockPlan,
|
||
ApplyStepRunner: mockApply,
|
||
RunStepRunner: mockRun,
|
||
EnvStepRunner: mockEnv,
|
||
PullApprovedChecker: mockApproved,
|
||
WorkingDir: mockWorkingDir,
|
||
Webhooks: mockSender,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
}
|
||
repoDir, cleanup := TempDir(t)
|
||
defer cleanup()
|
||
When(mockWorkingDir.GetWorkingDir(
|
||
matchers.AnyModelsRepo(),
|
||
matchers.AnyModelsPullRequest(),
|
||
AnyString(),
|
||
)).ThenReturn(repoDir, nil)
|
||
|
||
ctx := models.ProjectCommandContext{
|
||
Log: logging.NewNoopLogger(),
|
||
Steps: c.steps,
|
||
Workspace: "default",
|
||
ApplyRequirements: c.applyReqs,
|
||
RepoRelDir: ".",
|
||
PullMergeable: c.pullMergeable,
|
||
}
|
||
expEnvs := map[string]string{
|
||
"key": "value",
|
||
}
|
||
When(mockInit.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("init", nil)
|
||
When(mockPlan.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("plan", nil)
|
||
When(mockApply.Run(ctx, nil, repoDir, expEnvs)).ThenReturn("apply", nil)
|
||
When(mockRun.Run(ctx, "", repoDir, expEnvs)).ThenReturn("run", nil)
|
||
When(mockEnv.Run(ctx, "", "value", repoDir, make(map[string]string))).ThenReturn("value", nil)
|
||
When(mockApproved.PullIsApproved(ctx.BaseRepo, ctx.Pull)).ThenReturn(true, nil)
|
||
|
||
res := runner.Apply(ctx)
|
||
Equals(t, c.expOut, res.ApplySuccess)
|
||
Equals(t, c.expFailure, res.Failure)
|
||
|
||
for _, step := range c.expSteps {
|
||
switch step {
|
||
case "approved":
|
||
mockApproved.VerifyWasCalledOnce().PullIsApproved(ctx.BaseRepo, ctx.Pull)
|
||
case "init":
|
||
mockInit.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "plan":
|
||
mockPlan.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "apply":
|
||
mockApply.VerifyWasCalledOnce().Run(ctx, nil, repoDir, expEnvs)
|
||
case "run":
|
||
mockRun.VerifyWasCalledOnce().Run(ctx, "", repoDir, expEnvs)
|
||
case "env":
|
||
mockEnv.VerifyWasCalledOnce().Run(ctx, "", "value", repoDir, expEnvs)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// Test run and env steps. We don't use mocks for this test since we're
|
||
// not running any Terraform.
|
||
func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) {
|
||
RegisterMockTestingT(t)
|
||
tfClient := tmocks.NewMockClient()
|
||
tfVersion, err := version.NewVersion("0.12.0")
|
||
Ok(t, err)
|
||
run := runtime.RunStepRunner{
|
||
TerraformExecutor: tfClient,
|
||
DefaultTFVersion: tfVersion,
|
||
}
|
||
env := runtime.EnvStepRunner{
|
||
RunStepRunner: &run,
|
||
}
|
||
mockWorkingDir := mocks.NewMockWorkingDir()
|
||
mockLocker := mocks.NewMockProjectLocker()
|
||
|
||
runner := events.DefaultProjectCommandRunner{
|
||
Locker: mockLocker,
|
||
LockURLGenerator: mockURLGenerator{},
|
||
RunStepRunner: &run,
|
||
EnvStepRunner: &env,
|
||
PullApprovedChecker: nil,
|
||
WorkingDir: mockWorkingDir,
|
||
Webhooks: nil,
|
||
WorkingDirLocker: events.NewDefaultWorkingDirLocker(),
|
||
}
|
||
|
||
repoDir, cleanup := TempDir(t)
|
||
defer cleanup()
|
||
When(mockWorkingDir.Clone(
|
||
matchers.AnyPtrToLoggingSimpleLogger(),
|
||
matchers.AnyModelsRepo(),
|
||
matchers.AnyModelsPullRequest(),
|
||
AnyString(),
|
||
)).ThenReturn(repoDir, false, nil)
|
||
When(mockLocker.TryLock(
|
||
matchers.AnyPtrToLoggingSimpleLogger(),
|
||
matchers.AnyModelsPullRequest(),
|
||
matchers.AnyModelsUser(),
|
||
AnyString(),
|
||
matchers.AnyModelsProject(),
|
||
)).ThenReturn(&events.TryLockResponse{
|
||
LockAcquired: true,
|
||
LockKey: "lock-key",
|
||
}, nil)
|
||
|
||
ctx := models.ProjectCommandContext{
|
||
Log: logging.NewNoopLogger(),
|
||
Steps: []valid.Step{
|
||
{
|
||
StepName: "run",
|
||
RunCommand: "echo var=$var",
|
||
},
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "var",
|
||
EnvVarValue: "value",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
RunCommand: "echo var=$var",
|
||
},
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "dynamic_var",
|
||
RunCommand: "echo dynamic_value",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
RunCommand: "echo dynamic_var=$dynamic_var",
|
||
},
|
||
// Test overriding the variable
|
||
{
|
||
StepName: "env",
|
||
EnvVarName: "dynamic_var",
|
||
EnvVarValue: "overridden",
|
||
},
|
||
{
|
||
StepName: "run",
|
||
RunCommand: "echo dynamic_var=$dynamic_var",
|
||
},
|
||
},
|
||
Workspace: "default",
|
||
RepoRelDir: ".",
|
||
}
|
||
res := runner.Plan(ctx)
|
||
Assert(t, res.PlanSuccess != nil, "exp plan success")
|
||
Equals(t, "https://lock-key", res.PlanSuccess.LockURL)
|
||
Equals(t, "var=\n\nvar=value\n\ndynamic_var=dynamic_value\n\ndynamic_var=overridden\n", res.PlanSuccess.TerraformOutput)
|
||
}
|
||
|
||
type mockURLGenerator struct{}
|
||
|
||
func (m mockURLGenerator) GenerateLockURL(lockID string) string {
|
||
return "https://" + lockID
|
||
}
|