mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 18:48:24 +00:00
This is two changes: Replacing all usages of SimpleLogger with it's interface SimpleLogging which makes it easier to swap out loggers going forward Nukes SimpleLogger in favor of StructuredLogger which allows us to better analyze logs in our log management system we choose.
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(t),
|
||
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(t),
|
||
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(t),
|
||
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
|
||
}
|