mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 19:38:24 +00:00
Signed-off-by: RB <7775707+nitrocode@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: RB <7775707+nitrocode@users.noreply.github.com>
1927 lines
62 KiB
Go
1927 lines
62 KiB
Go
package events_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/go-github/v71/github"
|
|
"github.com/hashicorp/go-version"
|
|
. "github.com/petergtz/pegomock/v4"
|
|
|
|
"github.com/runatlantis/atlantis/server"
|
|
events_controllers "github.com/runatlantis/atlantis/server/controllers/events"
|
|
"github.com/runatlantis/atlantis/server/core/config"
|
|
"github.com/runatlantis/atlantis/server/core/config/valid"
|
|
"github.com/runatlantis/atlantis/server/core/db"
|
|
"github.com/runatlantis/atlantis/server/core/locking"
|
|
"github.com/runatlantis/atlantis/server/core/runtime"
|
|
runtimemocks "github.com/runatlantis/atlantis/server/core/runtime/mocks"
|
|
"github.com/runatlantis/atlantis/server/core/runtime/policy"
|
|
mock_policy "github.com/runatlantis/atlantis/server/core/runtime/policy/mocks"
|
|
"github.com/runatlantis/atlantis/server/core/terraform"
|
|
terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
|
|
"github.com/runatlantis/atlantis/server/core/terraform/tfclient"
|
|
"github.com/runatlantis/atlantis/server/events"
|
|
"github.com/runatlantis/atlantis/server/events/command"
|
|
"github.com/runatlantis/atlantis/server/events/mocks"
|
|
"github.com/runatlantis/atlantis/server/events/models"
|
|
"github.com/runatlantis/atlantis/server/events/vcs"
|
|
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
|
|
"github.com/runatlantis/atlantis/server/events/webhooks"
|
|
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
|
|
"github.com/runatlantis/atlantis/server/logging"
|
|
"github.com/runatlantis/atlantis/server/metrics"
|
|
. "github.com/runatlantis/atlantis/testing"
|
|
)
|
|
|
|
// In the e2e test, we use `conftest` not `conftest$version`.
|
|
// Because if depends on the version, we need to upgrade test base image before e2e fix it.
|
|
const conftestCommand = "conftest"
|
|
|
|
var applyLocker locking.ApplyLocker
|
|
var userConfig server.UserConfig
|
|
|
|
type NoopTFDownloader struct{}
|
|
|
|
var mockPreWorkflowHookRunner *runtimemocks.MockPreWorkflowHookRunner
|
|
|
|
var mockPostWorkflowHookRunner *runtimemocks.MockPostWorkflowHookRunner
|
|
|
|
func (m *NoopTFDownloader) Install(_ string, _ string, _ *version.Version) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
type LocalConftestCache struct {
|
|
}
|
|
|
|
func (m *LocalConftestCache) Get(_ *version.Version) (string, error) {
|
|
return exec.LookPath(conftestCommand)
|
|
}
|
|
|
|
func TestGitHubWorkflow(t *testing.T) {
|
|
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
// Ensure we have >= TF 0.14 locally.
|
|
ensureRunning014(t)
|
|
|
|
cases := []struct {
|
|
Description string
|
|
// RepoDir is relative to testdata/test-repos.
|
|
RepoDir string
|
|
// RepoConfigFile is path for atlantis.yaml
|
|
RepoConfigFile string
|
|
// ModifiedFiles are the list of files that have been modified in this
|
|
// pull request.
|
|
ModifiedFiles []string
|
|
// Comments are what our mock user writes to the pull request.
|
|
Comments []string
|
|
// ApplyLock creates an apply lock that temporarily disables apply command
|
|
ApplyLock bool
|
|
// AllowCommands flag what kind of atlantis commands are available.
|
|
AllowCommands []command.Name
|
|
// DisableAutoplan flag disable auto plans when any pull request is opened.
|
|
DisableAutoplan bool
|
|
// DisablePreWorkflowHooks if set to true, pre-workflow hooks will be disabled
|
|
DisablePreWorkflowHooks bool
|
|
// ExpAutomerge is true if we expect Atlantis to automerge.
|
|
ExpAutomerge bool
|
|
// ExpAutoplan is true if we expect Atlantis to autoplan.
|
|
ExpAutoplan bool
|
|
// ExpParallel is true if we expect Atlantis to run parallel plans or applies.
|
|
ExpParallel bool
|
|
// ExpMergeable is true if we expect Atlantis to be able to merge.
|
|
// If for instance policy check is failing and there are no approvals
|
|
// ExpMergeable should be false
|
|
ExpMergeable bool
|
|
// ExpReplies is a list of files containing the expected replies that
|
|
// Atlantis writes to the pull request in order. A reply from a parallel operation
|
|
// will be matched using a substring check.
|
|
ExpReplies [][]string
|
|
// ExpAllowResponseCommentBack allow http response content with "Commenting back on pull request"
|
|
ExpAllowResponseCommentBack bool
|
|
// ExpParseFailedCount represents how many times test sends invalid commands
|
|
ExpParseFailedCount int
|
|
// ExpNoLocksToDelete whether we expect that there are no locks at the end to delete
|
|
ExpNoLocksToDelete bool
|
|
}{
|
|
{
|
|
Description: "no comment or change",
|
|
RepoDir: "simple",
|
|
ModifiedFiles: []string{},
|
|
Comments: []string{},
|
|
ExpReplies: [][]string{},
|
|
ExpNoLocksToDelete: true,
|
|
},
|
|
{
|
|
Description: "no comment",
|
|
RepoDir: "simple",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
Comments: []string{},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
ExpAutoplan: true,
|
|
},
|
|
{
|
|
Description: "simple",
|
|
RepoDir: "simple",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
ExpAutoplan: true,
|
|
},
|
|
{
|
|
Description: "simple with plan comment",
|
|
RepoDir: "simple",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis plan",
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "simple with comment -var",
|
|
RepoDir: "simple",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis plan -- -var var=overridden",
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-atlantis-plan-var-overridden.txt"},
|
|
{"exp-output-apply-var.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "simple with workspaces",
|
|
RepoDir: "simple",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis plan -- -var var=default_workspace",
|
|
"atlantis plan -w new_workspace -- -var var=new_workspace",
|
|
"atlantis apply -w default",
|
|
"atlantis apply -w new_workspace",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-atlantis-plan.txt"},
|
|
{"exp-output-atlantis-plan-new-workspace.txt"},
|
|
{"exp-output-apply-var-default-workspace.txt"},
|
|
{"exp-output-apply-var-new-workspace.txt"},
|
|
{"exp-output-merge-workspaces.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "simple with workspaces and apply all",
|
|
RepoDir: "simple",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis plan -- -var var=default_workspace",
|
|
"atlantis plan -w new_workspace -- -var var=new_workspace",
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-atlantis-plan.txt"},
|
|
{"exp-output-atlantis-plan-new-workspace.txt"},
|
|
{"exp-output-apply-var-all.txt"},
|
|
{"exp-output-merge-workspaces.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "simple with allow commands",
|
|
RepoDir: "simple",
|
|
AllowCommands: []command.Name{command.Plan, command.Apply},
|
|
Comments: []string{
|
|
"atlantis import ADDRESS ID",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-allow-command-unknown-import.txt"},
|
|
},
|
|
ExpAllowResponseCommentBack: true,
|
|
ExpParseFailedCount: 1,
|
|
ExpNoLocksToDelete: true,
|
|
},
|
|
{
|
|
Description: "simple with atlantis.yaml",
|
|
RepoDir: "simple-yaml",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis apply -w staging",
|
|
"atlantis apply -w default",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply-staging.txt"},
|
|
{"exp-output-apply-default.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "simple with atlantis.yaml - autoplan disabled",
|
|
RepoDir: "simple-yaml",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
DisableAutoplan: true,
|
|
DisablePreWorkflowHooks: true,
|
|
ExpAutoplan: false,
|
|
Comments: []string{
|
|
"atlantis plan -w staging",
|
|
"atlantis plan -w default",
|
|
"atlantis apply -w staging",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-plan-staging.txt"},
|
|
{"exp-output-plan-default.txt"},
|
|
{"exp-output-apply-staging.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "simple with atlantis.yaml and apply all",
|
|
RepoDir: "simple-yaml",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply-all.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "custom repo config file",
|
|
RepoDir: "repo-config-file",
|
|
RepoConfigFile: "infrastructure/custom-name-atlantis.yaml",
|
|
ModifiedFiles: []string{
|
|
"infrastructure/staging/main.tf",
|
|
"infrastructure/production/main.tf",
|
|
},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "modules staging only",
|
|
RepoDir: "modules",
|
|
ModifiedFiles: []string{"staging/main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis apply -d staging",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan-only-staging.txt"},
|
|
{"exp-output-apply-staging.txt"},
|
|
{"exp-output-merge-only-staging.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "modules staging only - autoplan disabled",
|
|
RepoDir: "modules",
|
|
ModifiedFiles: []string{"staging/main.tf"},
|
|
DisableAutoplan: true,
|
|
DisablePreWorkflowHooks: true,
|
|
ExpAutoplan: false,
|
|
Comments: []string{
|
|
"atlantis plan -d staging",
|
|
"atlantis apply -d staging",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-plan-staging.txt"},
|
|
{"exp-output-apply-staging.txt"},
|
|
{"exp-output-merge-only-staging.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "modules modules only",
|
|
RepoDir: "modules",
|
|
ModifiedFiles: []string{"modules/null/main.tf"},
|
|
ExpAutoplan: false,
|
|
Comments: []string{
|
|
"atlantis plan -d staging",
|
|
"atlantis plan -d production",
|
|
"atlantis apply -d staging",
|
|
"atlantis apply -d production",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-plan-staging.txt"},
|
|
{"exp-output-plan-production.txt"},
|
|
{"exp-output-apply-staging.txt"},
|
|
{"exp-output-apply-production.txt"},
|
|
{"exp-output-merge-all-dirs.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "modules-yaml",
|
|
RepoDir: "modules-yaml",
|
|
ModifiedFiles: []string{"modules/null/main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis apply -d staging",
|
|
"atlantis apply -d production",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply-staging.txt"},
|
|
{"exp-output-apply-production.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "tfvars-yaml",
|
|
RepoDir: "tfvars-yaml",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis apply -p staging",
|
|
"atlantis apply -p default",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply-staging.txt"},
|
|
{"exp-output-apply-default.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "tfvars no autoplan",
|
|
RepoDir: "tfvars-yaml-no-autoplan",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: false,
|
|
Comments: []string{
|
|
"atlantis plan -p staging",
|
|
"atlantis plan -p default",
|
|
"atlantis apply -p staging",
|
|
"atlantis apply -p default",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-plan-staging.txt"},
|
|
{"exp-output-plan-default.txt"},
|
|
{"exp-output-apply-staging.txt"},
|
|
{"exp-output-apply-default.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "automerge",
|
|
RepoDir: "automerge",
|
|
ExpAutomerge: true,
|
|
ExpAutoplan: true,
|
|
ModifiedFiles: []string{"dir1/main.tf", "dir2/main.tf"},
|
|
Comments: []string{
|
|
"atlantis apply -d dir1",
|
|
"atlantis apply -d dir2",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply-dir1.txt"},
|
|
{"exp-output-apply-dir2.txt"},
|
|
{"exp-output-automerge.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "server-side cfg",
|
|
RepoDir: "server-side-cfg",
|
|
ExpAutomerge: false,
|
|
ExpAutoplan: true,
|
|
ModifiedFiles: []string{"main.tf"},
|
|
Comments: []string{
|
|
"atlantis apply -w staging",
|
|
"atlantis apply -w default",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply-staging-workspace.txt"},
|
|
{"exp-output-apply-default-workspace.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "workspaces parallel with atlantis.yaml",
|
|
RepoDir: "workspace-parallel-yaml",
|
|
ModifiedFiles: []string{"production/main.tf", "staging/main.tf"},
|
|
ExpAutoplan: true,
|
|
ExpParallel: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan-staging.txt", "exp-output-autoplan-production.txt"},
|
|
{"exp-output-apply-all-staging.txt", "exp-output-apply-all-production.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "global apply lock disables apply commands",
|
|
RepoDir: "simple-yaml",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ApplyLock: true,
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply-locked.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "omitting apply from allow commands always takes precedence",
|
|
RepoDir: "simple-yaml",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
AllowCommands: []command.Name{command.Plan},
|
|
ApplyLock: false,
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpParseFailedCount: 1,
|
|
ExpAllowResponseCommentBack: true,
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
// Disabling apply is implementing by omitting it from the apply list
|
|
// See: https://github.com/runatlantis/atlantis/pull/2877
|
|
{"exp-output-allow-command-unknown-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "import single project",
|
|
RepoDir: "import-single-project",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis import random_id.dummy1 AA",
|
|
"atlantis apply",
|
|
"atlantis import random_id.dummy2 BB",
|
|
"atlantis plan",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-import-dummy1.txt"},
|
|
{"exp-output-apply-no-projects.txt"},
|
|
{"exp-output-import-dummy2.txt"},
|
|
{"exp-output-plan-again.txt"},
|
|
{"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",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis import 'random_id.for_each[\"overridden\"]' AA -- -var var=overridden",
|
|
"atlantis import random_id.count[0] BB",
|
|
"atlantis plan -- -var var=overridden",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-import-foreach.txt"},
|
|
{"exp-output-import-count.txt"},
|
|
{"exp-output-plan-again.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "import multiple project",
|
|
RepoDir: "import-multiple-project",
|
|
ModifiedFiles: []string{"dir1/main.tf", "dir2/main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis import random_id.dummy1 AA",
|
|
"atlantis import -d dir1 random_id.dummy1 AA",
|
|
"atlantis plan",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-import-multiple-projects.txt"},
|
|
{"exp-output-import-dummy1.txt"},
|
|
{"exp-output-plan-again.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "state rm single project",
|
|
RepoDir: "state-rm-single-project",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis import random_id.simple AA",
|
|
"atlantis import 'random_id.for_each[\"overridden\"]' BB -- -var var=overridden",
|
|
"atlantis import random_id.count[0] BB",
|
|
"atlantis plan -- -var var=overridden",
|
|
"atlantis state rm 'random_id.for_each[\"overridden\"]' -- -lock=false",
|
|
"atlantis state rm random_id.count[0] random_id.simple",
|
|
"atlantis plan -- -var var=overridden",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-import-simple.txt"},
|
|
{"exp-output-import-foreach.txt"},
|
|
{"exp-output-import-count.txt"},
|
|
{"exp-output-plan.txt"},
|
|
{"exp-output-state-rm-foreach.txt"},
|
|
{"exp-output-state-rm-multiple.txt"},
|
|
{"exp-output-plan-again.txt"},
|
|
{"exp-output-merged.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "state rm workspace",
|
|
RepoDir: "state-rm-workspace",
|
|
Comments: []string{
|
|
"atlantis import -p dir1-ops 'random_id.dummy1[0]' AA",
|
|
"atlantis plan -p dir1-ops",
|
|
"atlantis state rm -p dir1-ops 'random_id.dummy1[0]'",
|
|
"atlantis plan -p dir1-ops",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-import-dummy1.txt"},
|
|
{"exp-output-plan.txt"},
|
|
{"exp-output-state-rm-dummy1.txt"},
|
|
{"exp-output-plan-again.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "state rm multiple project",
|
|
RepoDir: "state-rm-multiple-project",
|
|
ModifiedFiles: []string{"dir1/main.tf", "dir2/main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis import -d dir1 random_id.dummy AA",
|
|
"atlantis import -d dir2 random_id.dummy BB",
|
|
"atlantis plan",
|
|
"atlantis state rm random_id.dummy",
|
|
"atlantis plan",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-import-dummy1.txt"},
|
|
{"exp-output-import-dummy2.txt"},
|
|
{"exp-output-plan.txt"},
|
|
{"exp-output-state-rm-multiple-projects.txt"},
|
|
{"exp-output-plan-again.txt"},
|
|
{"exp-output-merged.txt"},
|
|
},
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.Description, func(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
|
|
// reset userConfig
|
|
userConfig = server.UserConfig{}
|
|
|
|
opt := setupOption{
|
|
repoConfigFile: c.RepoConfigFile,
|
|
allowCommands: c.AllowCommands,
|
|
disableAutoplan: c.DisableAutoplan,
|
|
disablePreWorkflowHooks: c.DisablePreWorkflowHooks,
|
|
}
|
|
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, opt)
|
|
// Set the repo to be cloned through the testing backdoor.
|
|
repoDir, headSHA := initializeRepo(t, c.RepoDir)
|
|
atlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf("file://%s", repoDir)
|
|
|
|
// Setup test dependencies.
|
|
w := httptest.NewRecorder()
|
|
When(githubGetter.GetPullRequest(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[int]())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)
|
|
When(vcsClient.GetModifiedFiles(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)
|
|
|
|
// First, send the open pull request event which triggers autoplan.
|
|
pullOpenedReq := GitHubPullRequestOpenedEvent(t, headSHA)
|
|
ctrl.Post(w, pullOpenedReq)
|
|
ResponseContains(t, w, 200, "Processing...")
|
|
|
|
// Create global apply lock if required
|
|
if c.ApplyLock {
|
|
_, _ = applyLocker.LockApply()
|
|
}
|
|
|
|
// Now send any other comments.
|
|
for _, comment := range c.Comments {
|
|
commentReq := GitHubCommentEvent(t, comment)
|
|
w = httptest.NewRecorder()
|
|
ctrl.Post(w, commentReq)
|
|
if c.ExpAllowResponseCommentBack {
|
|
ResponseContains(t, w, 200, "Commenting back on pull request")
|
|
} else {
|
|
ResponseContains(t, w, 200, "Processing...")
|
|
}
|
|
}
|
|
|
|
// Send the "pull closed" event which would be triggered by the
|
|
// automerge or a manual merge.
|
|
pullClosedReq := GitHubPullRequestClosedEvent(t)
|
|
w = httptest.NewRecorder()
|
|
ctrl.Post(w, pullClosedReq)
|
|
ResponseContains(t, w, 200, "Pull request cleaned successfully")
|
|
|
|
expNumHooks := len(c.Comments) - c.ExpParseFailedCount
|
|
// if auto plan is disabled, hooks will not be called on pull request opened event
|
|
if !c.DisableAutoplan {
|
|
expNumHooks++
|
|
}
|
|
// Let's verify the pre-workflow hook was called for each comment including the pull request opened event
|
|
if !c.DisablePreWorkflowHooks {
|
|
mockPreWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](),
|
|
Eq("some dummy command"), Any[string](), Any[string](), Any[string]())
|
|
}
|
|
// Let's verify the post-workflow hook was called for each comment including the pull request opened event
|
|
mockPostWorkflowHookRunner.VerifyWasCalled(Times(expNumHooks)).Run(Any[models.WorkflowHookCommandContext](),
|
|
Eq("some post dummy command"), Any[string](), Any[string](), Any[string]())
|
|
|
|
// Now we're ready to verify Atlantis made all the comments back (or
|
|
// replies) that we expect. We expect each plan to have 1 comment,
|
|
// and apply have 1 for each comment
|
|
expNumReplies := len(c.Comments)
|
|
|
|
// If there are locks to delete at the end, that will take a comment
|
|
if !c.ExpNoLocksToDelete {
|
|
expNumReplies++
|
|
}
|
|
|
|
if c.ExpAutoplan {
|
|
expNumReplies++
|
|
}
|
|
|
|
if c.ExpAutomerge {
|
|
expNumReplies++
|
|
}
|
|
|
|
_, _, _, actReplies, _ := vcsClient.VerifyWasCalled(Times(expNumReplies)).CreateComment(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetAllCapturedArguments()
|
|
Assert(t, len(c.ExpReplies) == len(actReplies), "missing expected replies, got %d but expected %d", len(actReplies), len(c.ExpReplies))
|
|
for i, expReply := range c.ExpReplies {
|
|
assertCommentEquals(t, expReply, actReplies[i], c.RepoDir, c.ExpParallel)
|
|
}
|
|
|
|
if c.ExpAutomerge {
|
|
// Verify that the merge API call was made.
|
|
vcsClient.VerifyWasCalledOnce().MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())
|
|
} else {
|
|
vcsClient.VerifyWasCalled(Never()).MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSimpleWorkflow_terraformLockFile(t *testing.T) {
|
|
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
// Ensure we have >= TF 0.14 locally.
|
|
ensureRunning014(t)
|
|
|
|
cases := []struct {
|
|
Description string
|
|
// RepoDir is relative to testdata/test-repos.
|
|
RepoDir string
|
|
// ModifiedFiles are the list of files that have been modified in this
|
|
// pull request.
|
|
ModifiedFiles []string
|
|
// ExpAutoplan is true if we expect Atlantis to autoplan.
|
|
ExpAutoplan bool
|
|
// Comments are what our mock user writes to the pull request.
|
|
Comments []string
|
|
// ExpReplies is a list of files containing the expected replies that
|
|
// Atlantis writes to the pull request in order. A reply from a parallel operation
|
|
// will be matched using a substring check.
|
|
ExpReplies [][]string
|
|
// LockFileTracked deterims if the `.terraform.lock.hcl` file is tracked in git
|
|
// if this is true we dont expect the lockfile to be modified by terraform init
|
|
// if false we expect the lock file to be updated
|
|
LockFileTracked bool
|
|
}{
|
|
{
|
|
Description: "simple with plan comment lockfile staged",
|
|
RepoDir: "simple-with-lockfile",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis plan",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-plan.txt"},
|
|
},
|
|
LockFileTracked: true,
|
|
},
|
|
{
|
|
Description: "simple with plan comment lockfile not staged",
|
|
RepoDir: "simple-with-lockfile",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
Comments: []string{
|
|
"atlantis plan",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-plan.txt"},
|
|
},
|
|
LockFileTracked: false,
|
|
},
|
|
{
|
|
Description: "Modified .terraform.lock.hcl triggers autoplan ",
|
|
RepoDir: "simple-with-lockfile",
|
|
ModifiedFiles: []string{".terraform.lock.hcl"},
|
|
ExpAutoplan: true,
|
|
Comments: []string{
|
|
"atlantis plan",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-plan.txt"},
|
|
},
|
|
LockFileTracked: true,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.Description, func(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
|
|
// reset userConfig
|
|
userConfig = server.UserConfig{}
|
|
|
|
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, setupOption{})
|
|
// Set the repo to be cloned through the testing backdoor.
|
|
repoDir, headSHA := initializeRepo(t, c.RepoDir)
|
|
|
|
oldLockFilePath, err := filepath.Abs(filepath.Join("testdata", "null_provider_lockfile_old_version"))
|
|
Ok(t, err)
|
|
oldLockFileContent, err := os.ReadFile(oldLockFilePath)
|
|
Ok(t, err)
|
|
|
|
if c.LockFileTracked {
|
|
runCmd(t, "", "cp", oldLockFilePath, fmt.Sprintf("%s/.terraform.lock.hcl", repoDir))
|
|
runCmd(t, repoDir, "git", "add", ".terraform.lock.hcl")
|
|
runCmd(t, repoDir, "git", "commit", "-am", "stage .terraform.lock.hcl")
|
|
}
|
|
|
|
atlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf("file://%s", repoDir)
|
|
|
|
// Setup test dependencies.
|
|
w := httptest.NewRecorder()
|
|
When(githubGetter.GetPullRequest(Any[logging.SimpleLogging](), Any[models.Repo](), Any[int]())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)
|
|
When(vcsClient.GetModifiedFiles(Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)
|
|
|
|
// First, send the open pull request event which triggers autoplan.
|
|
pullOpenedReq := GitHubPullRequestOpenedEvent(t, headSHA)
|
|
ctrl.Post(w, pullOpenedReq)
|
|
ResponseContains(t, w, 200, "Processing...")
|
|
|
|
// check lock file content
|
|
actualLockFileContent, err := os.ReadFile(fmt.Sprintf("%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl", atlantisWorkspace.DataDir))
|
|
Ok(t, err)
|
|
if c.LockFileTracked {
|
|
if string(oldLockFileContent) != string(actualLockFileContent) {
|
|
t.Error("Expected terraform.lock.hcl file not to be different as it has been staged")
|
|
t.FailNow()
|
|
}
|
|
} else {
|
|
if string(oldLockFileContent) == string(actualLockFileContent) {
|
|
t.Error("Expected terraform.lock.hcl file to be different as it should have been updated")
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
if !c.LockFileTracked {
|
|
// replace the lock file generated by the previous init to simulate
|
|
// dependencies needing updating in a latter plan
|
|
runCmd(t, "", "cp", oldLockFilePath, fmt.Sprintf("%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl", atlantisWorkspace.DataDir))
|
|
}
|
|
|
|
// Now send any other comments.
|
|
for _, comment := range c.Comments {
|
|
commentReq := GitHubCommentEvent(t, comment)
|
|
w = httptest.NewRecorder()
|
|
ctrl.Post(w, commentReq)
|
|
ResponseContains(t, w, 200, "Processing...")
|
|
}
|
|
|
|
// check lock file content
|
|
actualLockFileContent, err = os.ReadFile(fmt.Sprintf("%s/repos/runatlantis/atlantis-tests/2/default/.terraform.lock.hcl", atlantisWorkspace.DataDir))
|
|
Ok(t, err)
|
|
if c.LockFileTracked {
|
|
if string(oldLockFileContent) != string(actualLockFileContent) {
|
|
t.Error("Expected terraform.lock.hcl file not to be different as it has been staged")
|
|
t.FailNow()
|
|
}
|
|
} else {
|
|
if string(oldLockFileContent) == string(actualLockFileContent) {
|
|
t.Error("Expected terraform.lock.hcl file to be different as it should have been updated")
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
// Let's verify the pre-workflow hook was called for each comment including the pull request opened event
|
|
mockPreWorkflowHookRunner.VerifyWasCalled(Times(2)).Run(Any[models.WorkflowHookCommandContext](),
|
|
Eq("some dummy command"), Any[string](), Any[string](), Any[string]())
|
|
|
|
// Now we're ready to verify Atlantis made all the comments back (or
|
|
// replies) that we expect. We expect each plan to have 1 comment,
|
|
// and apply have 1 for each comment plus one for the locks deleted at the
|
|
// end.
|
|
|
|
_, _, _, actReplies, _ := vcsClient.VerifyWasCalled(Times(2)).CreateComment(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetAllCapturedArguments()
|
|
Assert(t, len(c.ExpReplies) == len(actReplies), "missing expected replies, got %d but expected %d", len(actReplies), len(c.ExpReplies))
|
|
for i, expReply := range c.ExpReplies {
|
|
assertCommentEquals(t, expReply, actReplies[i], c.RepoDir, false)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitHubWorkflowWithPolicyCheck(t *testing.T) {
|
|
if testing.Short() {
|
|
t.SkipNow()
|
|
}
|
|
// Ensure we have >= TF 0.14 locally.
|
|
ensureRunning014(t)
|
|
// Ensure we have conftest locally.
|
|
ensureRunningConftest(t)
|
|
|
|
cases := []struct {
|
|
Description string
|
|
// RepoDir is relative to testdata/test-repos.
|
|
RepoDir string
|
|
// ModifiedFiles are the list of files that have been modified in this
|
|
// pull request.
|
|
ModifiedFiles []string
|
|
// Comments are what our mock user writes to the pull request.
|
|
Comments []string
|
|
// PolicyCheck is true if we expect Atlantis to run policy checking
|
|
PolicyCheck bool
|
|
// ExpAutomerge is true if we expect Atlantis to automerge.
|
|
ExpAutomerge bool
|
|
// ExpAutoplan is true if we expect Atlantis to autoplan.
|
|
ExpAutoplan bool
|
|
// ExpPolicyChecks is true if we expect Atlantis to execute policy checks
|
|
ExpPolicyChecks bool
|
|
// ExpQuietPolicyChecks is true if we expect Atlantis to exclude policy check output
|
|
// when there's no error
|
|
ExpQuietPolicyChecks bool
|
|
// ExpQuietPolicyCheckFailure is true when we expect Atlantis to post back policy check failures
|
|
// even when QuietPolicyChecks is enabled
|
|
ExpQuietPolicyCheckFailure bool
|
|
// ExpParallel is true if we expect Atlantis to run parallel plans or applies.
|
|
ExpParallel bool
|
|
// ExpReplies is a list of files containing the expected replies that
|
|
// Atlantis writes to the pull request in order. A reply from a parallel operation
|
|
// will be matched using a substring check.
|
|
ExpReplies [][]string
|
|
}{
|
|
{
|
|
Description: "1 failing policy and 1 passing policy ",
|
|
RepoDir: "policy-checks-multi-projects",
|
|
ModifiedFiles: []string{"dir1/main.tf,", "dir2/main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "1 failing policy and 1 passing policy with --quiet-policy-checks",
|
|
RepoDir: "policy-checks-multi-projects",
|
|
ModifiedFiles: []string{"dir1/main.tf,", "dir2/main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
ExpQuietPolicyChecks: true,
|
|
ExpQuietPolicyCheckFailure: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check-quiet.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "failing policy without policies passing using extra args",
|
|
RepoDir: "policy-checks-extra-args",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check.txt"},
|
|
{"exp-output-apply-failed.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "failing policy without policies passing",
|
|
RepoDir: "policy-checks",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check.txt"},
|
|
{"exp-output-apply-failed.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "failing policy without policies passing and custom run steps",
|
|
RepoDir: "policy-checks-custom-run-steps",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check.txt"},
|
|
{"exp-output-apply-failed.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "failing policy additional apply requirements specified",
|
|
RepoDir: "policy-checks-apply-reqs",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check.txt"},
|
|
{"exp-output-apply-failed.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "failing policy approved by non owner",
|
|
RepoDir: "policy-checks-diff-owner",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
Comments: []string{
|
|
"atlantis approve_policies",
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check.txt"},
|
|
{"exp-output-approve-policies.txt"},
|
|
{"exp-output-apply-failed.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "successful policy checks with quiet flag enabled",
|
|
RepoDir: "policy-checks-success-silent",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
ExpQuietPolicyChecks: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "failing policy checks with quiet flag enabled",
|
|
RepoDir: "policy-checks",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
ExpQuietPolicyChecks: true,
|
|
ExpQuietPolicyCheckFailure: true,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check.txt"},
|
|
{"exp-output-apply-failed.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "failing policy with approval and policy approval clear",
|
|
RepoDir: "policy-checks-clear-approval",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: true,
|
|
Comments: []string{
|
|
"atlantis approve_policies",
|
|
"atlantis approve_policies --clear-policy-approval",
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-auto-policy-check.txt"},
|
|
{"exp-output-approve-policies-success.txt"},
|
|
{"exp-output-approve-policies-clear.txt"},
|
|
{"exp-output-apply-failed.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "policy checking disabled on specific repo",
|
|
RepoDir: "policy-checks-disabled-repo",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: false,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "policy checking disabled on specific repo server side",
|
|
RepoDir: "policy-checks-disabled-repo-server-side",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: false,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "policy checking enabled on specific repo but disabled globally",
|
|
RepoDir: "policy-checks-enabled-repo",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: false,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: false,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "policy checking enabled on specific repo server side but disabled globally",
|
|
RepoDir: "policy-checks-enabled-repo-server-side",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: false,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: false,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
{
|
|
Description: "policy checking disabled on previous regex match but not on repo",
|
|
RepoDir: "policy-checks-disabled-previous-match",
|
|
ModifiedFiles: []string{"main.tf"},
|
|
PolicyCheck: true,
|
|
ExpAutoplan: true,
|
|
ExpPolicyChecks: false,
|
|
Comments: []string{
|
|
"atlantis apply",
|
|
},
|
|
ExpReplies: [][]string{
|
|
{"exp-output-autoplan.txt"},
|
|
{"exp-output-apply.txt"},
|
|
{"exp-output-merge.txt"},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.Description, func(t *testing.T) {
|
|
RegisterMockTestingT(t)
|
|
|
|
// reset userConfig
|
|
userConfig = server.UserConfig{}
|
|
userConfig.EnablePolicyChecksFlag = c.PolicyCheck
|
|
userConfig.QuietPolicyChecks = c.ExpQuietPolicyChecks
|
|
|
|
ctrl, vcsClient, githubGetter, atlantisWorkspace := setupE2E(t, c.RepoDir, setupOption{userConfig: userConfig})
|
|
|
|
// Set the repo to be cloned through the testing backdoor.
|
|
repoDir, headSHA := initializeRepo(t, c.RepoDir)
|
|
atlantisWorkspace.TestingOverrideHeadCloneURL = fmt.Sprintf("file://%s", repoDir)
|
|
|
|
// Setup test dependencies.
|
|
w := httptest.NewRecorder()
|
|
When(vcsClient.PullIsMergeable(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest](), Eq("atlantis-test"), Eq([]string{}))).ThenReturn(true, nil)
|
|
When(vcsClient.PullIsApproved(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(models.ApprovalStatus{
|
|
IsApproved: true,
|
|
}, nil)
|
|
When(githubGetter.GetPullRequest(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[int]())).ThenReturn(GitHubPullRequestParsed(headSHA), nil)
|
|
When(vcsClient.GetModifiedFiles(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil)
|
|
|
|
// First, send the open pull request event which triggers autoplan.
|
|
pullOpenedReq := GitHubPullRequestOpenedEvent(t, headSHA)
|
|
ctrl.Post(w, pullOpenedReq)
|
|
ResponseContains(t, w, 200, "Processing...")
|
|
|
|
// Now send any other comments.
|
|
for _, comment := range c.Comments {
|
|
commentReq := GitHubCommentEvent(t, comment)
|
|
w = httptest.NewRecorder()
|
|
ctrl.Post(w, commentReq)
|
|
ResponseContains(t, w, 200, "Processing...")
|
|
}
|
|
|
|
// Send the "pull closed" event which would be triggered by the
|
|
// automerge or a manual merge.
|
|
pullClosedReq := GitHubPullRequestClosedEvent(t)
|
|
w = httptest.NewRecorder()
|
|
ctrl.Post(w, pullClosedReq)
|
|
ResponseContains(t, w, 200, "Pull request cleaned successfully")
|
|
|
|
// Now we're ready to verify Atlantis made all the comments back (or
|
|
// replies) that we expect. We expect each plan to have 2 comments,
|
|
// one for plan one for policy check and apply have 1 for each
|
|
// comment plus one for the locks deleted at the end.
|
|
expNumReplies := len(c.Comments) + 1
|
|
|
|
if c.ExpAutoplan {
|
|
expNumReplies++
|
|
expNumReplies++
|
|
}
|
|
|
|
var planRegex = regexp.MustCompile("plan")
|
|
for _, comment := range c.Comments {
|
|
if planRegex.MatchString(comment) {
|
|
expNumReplies++
|
|
}
|
|
}
|
|
|
|
if c.ExpAutomerge {
|
|
expNumReplies++
|
|
}
|
|
|
|
if c.ExpQuietPolicyChecks && !c.ExpQuietPolicyCheckFailure {
|
|
expNumReplies--
|
|
}
|
|
|
|
if !c.ExpPolicyChecks {
|
|
expNumReplies--
|
|
}
|
|
_, _, _, actReplies, _ := vcsClient.VerifyWasCalled(Times(expNumReplies)).CreateComment(
|
|
Any[logging.SimpleLogging](), Any[models.Repo](), Any[int](), Any[string](), Any[string]()).GetAllCapturedArguments()
|
|
|
|
Assert(t, len(c.ExpReplies) == len(actReplies), "missing expected replies, got %d but expected %d", len(actReplies), len(c.ExpReplies))
|
|
for i, expReply := range c.ExpReplies {
|
|
assertCommentEquals(t, expReply, actReplies[i], c.RepoDir, c.ExpParallel)
|
|
}
|
|
|
|
if c.ExpAutomerge {
|
|
// Verify that the merge API call was made.
|
|
vcsClient.VerifyWasCalledOnce().MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())
|
|
} else {
|
|
vcsClient.VerifyWasCalled(Never()).MergePull(Any[logging.SimpleLogging](), Any[models.PullRequest](), Any[models.PullRequestOptions]())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type setupOption struct {
|
|
repoConfigFile string
|
|
allowCommands []command.Name
|
|
disableAutoplan bool
|
|
disablePreWorkflowHooks bool
|
|
userConfig server.UserConfig
|
|
}
|
|
|
|
func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers.VCSEventsController, *vcsmocks.MockClient, *mocks.MockGithubPullGetter, *events.FileWorkspace) {
|
|
allowForkPRs := false
|
|
discardApprovalOnPlan := true
|
|
dataDir, binDir, cacheDir := mkSubDirs(t)
|
|
// Mocks.
|
|
e2eVCSClient := vcsmocks.NewMockClient()
|
|
e2eStatusUpdater := &events.DefaultCommitStatusUpdater{Client: e2eVCSClient}
|
|
e2eGithubGetter := mocks.NewMockGithubPullGetter()
|
|
e2eGitlabGetter := mocks.NewMockGitlabMergeRequestGetter()
|
|
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
|
|
|
|
// Real dependencies.
|
|
logging.SuppressDefaultLogging()
|
|
logger := logging.NewNoopLogger(t)
|
|
|
|
eventParser := &events.EventParser{
|
|
GithubUser: "github-user",
|
|
GithubToken: "github-token",
|
|
GitlabUser: "gitlab-user",
|
|
GitlabToken: "gitlab-token",
|
|
}
|
|
allowCommands := command.AllCommentCommands
|
|
if opt.allowCommands != nil {
|
|
allowCommands = opt.allowCommands
|
|
}
|
|
disableApply := true
|
|
disableGlobalApplyLock := false
|
|
for _, allowCommand := range allowCommands {
|
|
if allowCommand == command.Apply {
|
|
disableApply = false
|
|
break
|
|
}
|
|
}
|
|
commentParser := &events.CommentParser{
|
|
GithubUser: "github-user",
|
|
GitlabUser: "gitlab-user",
|
|
ExecutableName: "atlantis",
|
|
AllowCommands: allowCommands,
|
|
}
|
|
|
|
mockDownloader := terraform_mocks.NewMockDownloader()
|
|
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
|
|
|
|
terraformClient, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler)
|
|
Ok(t, err)
|
|
boltdb, err := db.New(dataDir)
|
|
Ok(t, err)
|
|
backend := boltdb
|
|
lockingClient := locking.NewClient(boltdb)
|
|
noOpLocker := locking.NewNoOpLocker()
|
|
applyLocker = locking.NewApplyClient(boltdb, disableApply, disableGlobalApplyLock)
|
|
projectLocker := &events.DefaultProjectLocker{
|
|
Locker: lockingClient,
|
|
NoOpLocker: noOpLocker,
|
|
VCSClient: e2eVCSClient,
|
|
}
|
|
workingDir := &events.FileWorkspace{
|
|
DataDir: dataDir,
|
|
TestingOverrideHeadCloneURL: "override-me",
|
|
}
|
|
var preWorkflowHooks []*valid.WorkflowHook
|
|
if !opt.disablePreWorkflowHooks {
|
|
preWorkflowHooks = []*valid.WorkflowHook{
|
|
{
|
|
StepName: "global_hook",
|
|
RunCommand: "some dummy command",
|
|
},
|
|
}
|
|
}
|
|
|
|
defaultTFDistribution := terraformClient.DefaultDistribution()
|
|
defaultTFVersion := terraformClient.DefaultVersion()
|
|
locker := events.NewDefaultWorkingDirLocker()
|
|
parser := &config.ParserValidator{}
|
|
|
|
globalCfgArgs := valid.GlobalCfgArgs{
|
|
RepoConfigFile: opt.repoConfigFile,
|
|
AllowAllRepoSettings: true,
|
|
PreWorkflowHooks: preWorkflowHooks,
|
|
PostWorkflowHooks: []*valid.WorkflowHook{
|
|
{
|
|
StepName: "global_hook",
|
|
RunCommand: "some post dummy command",
|
|
},
|
|
},
|
|
PolicyCheckEnabled: userConfig.EnablePolicyChecksFlag,
|
|
}
|
|
globalCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs)
|
|
expCfgPath := filepath.Join(absRepoPath(t, repoDir), "repos.yaml")
|
|
if _, err := os.Stat(expCfgPath); err == nil {
|
|
globalCfg, err = parser.ParseGlobalCfg(expCfgPath, globalCfg)
|
|
Ok(t, err)
|
|
}
|
|
drainer := &events.Drainer{}
|
|
|
|
parallelPoolSize := 1
|
|
silenceNoProjects := false
|
|
|
|
disableUnlockLabel := "do-not-unlock"
|
|
|
|
statusUpdater := runtimemocks.NewMockStatusUpdater()
|
|
commitStatusUpdater := mocks.NewMockCommitStatusUpdater()
|
|
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
|
|
|
|
mockPreWorkflowHookRunner = runtimemocks.NewMockPreWorkflowHookRunner()
|
|
preWorkflowHookURLGenerator := mocks.NewMockPreWorkflowHookURLGenerator()
|
|
preWorkflowHooksCommandRunner := &events.DefaultPreWorkflowHooksCommandRunner{
|
|
VCSClient: e2eVCSClient,
|
|
GlobalCfg: globalCfg,
|
|
WorkingDirLocker: locker,
|
|
WorkingDir: workingDir,
|
|
PreWorkflowHookRunner: mockPreWorkflowHookRunner,
|
|
CommitStatusUpdater: commitStatusUpdater,
|
|
Router: preWorkflowHookURLGenerator,
|
|
}
|
|
|
|
mockPostWorkflowHookRunner = runtimemocks.NewMockPostWorkflowHookRunner()
|
|
postWorkflowHookURLGenerator := mocks.NewMockPostWorkflowHookURLGenerator()
|
|
postWorkflowHooksCommandRunner := &events.DefaultPostWorkflowHooksCommandRunner{
|
|
VCSClient: e2eVCSClient,
|
|
GlobalCfg: globalCfg,
|
|
WorkingDirLocker: locker,
|
|
WorkingDir: workingDir,
|
|
PostWorkflowHookRunner: mockPostWorkflowHookRunner,
|
|
CommitStatusUpdater: commitStatusUpdater,
|
|
Router: postWorkflowHookURLGenerator,
|
|
}
|
|
statsScope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
|
|
|
|
projectCommandBuilder := events.NewProjectCommandBuilder(
|
|
userConfig.EnablePolicyChecksFlag,
|
|
parser,
|
|
&events.DefaultProjectFinder{},
|
|
e2eVCSClient,
|
|
workingDir,
|
|
locker,
|
|
globalCfg,
|
|
&events.DefaultPendingPlanFinder{},
|
|
commentParser,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
"",
|
|
"**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl",
|
|
false,
|
|
false,
|
|
false,
|
|
"auto",
|
|
statsScope,
|
|
terraformClient,
|
|
)
|
|
|
|
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion)
|
|
|
|
Ok(t, err)
|
|
|
|
conftextExec := policy.NewConfTestExecutorWorkflow(logger, binDir, mock_policy.NewMockDownloader())
|
|
|
|
// swapping out version cache to something that always returns local conftest
|
|
// binary
|
|
conftextExec.VersionCache = &LocalConftestCache{}
|
|
|
|
policyCheckRunner, err := runtime.NewPolicyCheckStepRunner(
|
|
defaultTFDistribution,
|
|
defaultTFVersion,
|
|
conftextExec,
|
|
)
|
|
|
|
Ok(t, err)
|
|
|
|
projectCommandRunner := &events.DefaultProjectCommandRunner{
|
|
VcsClient: e2eVCSClient,
|
|
Locker: projectLocker,
|
|
LockURLGenerator: &mockLockURLGenerator{},
|
|
InitStepRunner: &runtime.InitStepRunner{
|
|
TerraformExecutor: terraformClient,
|
|
DefaultTFDistribution: defaultTFDistribution,
|
|
DefaultTFVersion: defaultTFVersion,
|
|
},
|
|
PlanStepRunner: runtime.NewPlanStepRunner(
|
|
terraformClient,
|
|
defaultTFDistribution,
|
|
defaultTFVersion,
|
|
statusUpdater,
|
|
asyncTfExec,
|
|
),
|
|
ShowStepRunner: showStepRunner,
|
|
PolicyCheckStepRunner: policyCheckRunner,
|
|
ApplyStepRunner: &runtime.ApplyStepRunner{
|
|
TerraformExecutor: terraformClient,
|
|
},
|
|
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion),
|
|
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion),
|
|
RunStepRunner: &runtime.RunStepRunner{
|
|
TerraformExecutor: terraformClient,
|
|
DefaultTFDistribution: defaultTFDistribution,
|
|
DefaultTFVersion: defaultTFVersion,
|
|
ProjectCmdOutputHandler: projectCmdOutputHandler,
|
|
},
|
|
WorkingDir: workingDir,
|
|
Webhooks: &mockWebhookSender{},
|
|
WorkingDirLocker: locker,
|
|
CommandRequirementHandler: &events.DefaultCommandRequirementHandler{
|
|
WorkingDir: workingDir,
|
|
},
|
|
}
|
|
|
|
dbUpdater := &events.DBUpdater{
|
|
Backend: backend,
|
|
}
|
|
|
|
pullUpdater := &events.PullUpdater{
|
|
HidePrevPlanComments: false,
|
|
VCSClient: e2eVCSClient,
|
|
MarkdownRenderer: events.NewMarkdownRenderer(
|
|
false, // gitlabSupportsCommonMark
|
|
false, // disableApplyAll
|
|
false, // disableApply
|
|
false, // disableMarkdownFolding
|
|
false, // disableRepoLocking
|
|
false, // enableDiffMarkdownFormat
|
|
"", // markdownTemplateOverridesDir
|
|
"atlantis", // executableName
|
|
false, // hideUnchangedPlanComments
|
|
opt.userConfig.QuietPolicyChecks, // quietPolicyChecks
|
|
),
|
|
}
|
|
|
|
autoMerger := &events.AutoMerger{
|
|
VCSClient: e2eVCSClient,
|
|
GlobalAutomerge: false,
|
|
}
|
|
|
|
policyCheckCommandRunner := events.NewPolicyCheckCommandRunner(
|
|
dbUpdater,
|
|
pullUpdater,
|
|
e2eStatusUpdater,
|
|
projectCommandRunner,
|
|
parallelPoolSize,
|
|
false,
|
|
userConfig.QuietPolicyChecks,
|
|
)
|
|
|
|
e2ePullReqStatusFetcher := vcs.NewPullReqStatusFetcher(e2eVCSClient, "atlantis-test", []string{})
|
|
|
|
planCommandRunner := events.NewPlanCommandRunner(
|
|
false,
|
|
false,
|
|
e2eVCSClient,
|
|
&events.DefaultPendingPlanFinder{},
|
|
workingDir,
|
|
e2eStatusUpdater,
|
|
projectCommandBuilder,
|
|
projectCommandRunner,
|
|
dbUpdater,
|
|
pullUpdater,
|
|
policyCheckCommandRunner,
|
|
autoMerger,
|
|
parallelPoolSize,
|
|
silenceNoProjects,
|
|
boltdb,
|
|
lockingClient,
|
|
discardApprovalOnPlan,
|
|
e2ePullReqStatusFetcher,
|
|
)
|
|
|
|
applyCommandRunner := events.NewApplyCommandRunner(
|
|
e2eVCSClient,
|
|
false,
|
|
applyLocker,
|
|
e2eStatusUpdater,
|
|
projectCommandBuilder,
|
|
projectCommandRunner,
|
|
autoMerger,
|
|
pullUpdater,
|
|
dbUpdater,
|
|
boltdb,
|
|
parallelPoolSize,
|
|
silenceNoProjects,
|
|
false,
|
|
e2ePullReqStatusFetcher,
|
|
)
|
|
|
|
approvePoliciesCommandRunner := events.NewApprovePoliciesCommandRunner(
|
|
e2eStatusUpdater,
|
|
projectCommandBuilder,
|
|
projectCommandRunner,
|
|
pullUpdater,
|
|
dbUpdater,
|
|
silenceNoProjects,
|
|
false,
|
|
e2eVCSClient,
|
|
)
|
|
|
|
unlockCommandRunner := events.NewUnlockCommandRunner(
|
|
mocks.NewMockDeleteLockCommand(),
|
|
e2eVCSClient,
|
|
silenceNoProjects,
|
|
disableUnlockLabel,
|
|
)
|
|
|
|
versionCommandRunner := events.NewVersionCommandRunner(
|
|
pullUpdater,
|
|
projectCommandBuilder,
|
|
projectCommandRunner,
|
|
parallelPoolSize,
|
|
silenceNoProjects,
|
|
)
|
|
|
|
importCommandRunner := events.NewImportCommandRunner(
|
|
pullUpdater,
|
|
e2ePullReqStatusFetcher,
|
|
projectCommandBuilder,
|
|
projectCommandRunner,
|
|
silenceNoProjects,
|
|
)
|
|
|
|
stateCommandRunner := events.NewStateCommandRunner(
|
|
pullUpdater,
|
|
projectCommandBuilder,
|
|
projectCommandRunner,
|
|
)
|
|
|
|
commentCommandRunnerByCmd := map[command.Name]events.CommentCommandRunner{
|
|
command.Plan: planCommandRunner,
|
|
command.Apply: applyCommandRunner,
|
|
command.ApprovePolicies: approvePoliciesCommandRunner,
|
|
command.Unlock: unlockCommandRunner,
|
|
command.Version: versionCommandRunner,
|
|
command.Import: importCommandRunner,
|
|
command.State: stateCommandRunner,
|
|
}
|
|
|
|
commandRunner := &events.DefaultCommandRunner{
|
|
EventParser: eventParser,
|
|
VCSClient: e2eVCSClient,
|
|
GithubPullGetter: e2eGithubGetter,
|
|
GitlabMergeRequestGetter: e2eGitlabGetter,
|
|
Logger: logger,
|
|
GlobalCfg: globalCfg,
|
|
StatsScope: statsScope,
|
|
AllowForkPRs: allowForkPRs,
|
|
AllowForkPRsFlag: "allow-fork-prs",
|
|
CommentCommandRunnerByCmd: commentCommandRunnerByCmd,
|
|
Drainer: drainer,
|
|
PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner,
|
|
PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner,
|
|
PullStatusFetcher: backend,
|
|
DisableAutoplan: opt.disableAutoplan,
|
|
CommitStatusUpdater: commitStatusUpdater,
|
|
}
|
|
|
|
repoAllowlistChecker, err := events.NewRepoAllowlistChecker("*")
|
|
Ok(t, err)
|
|
|
|
ctrl := events_controllers.VCSEventsController{
|
|
TestingMode: true,
|
|
CommandRunner: commandRunner,
|
|
PullCleaner: &events.PullClosedExecutor{
|
|
Locker: lockingClient,
|
|
VCSClient: e2eVCSClient,
|
|
WorkingDir: workingDir,
|
|
Backend: backend,
|
|
PullClosedTemplate: &events.PullClosedEventTemplate{},
|
|
LogStreamResourceCleaner: projectCmdOutputHandler,
|
|
},
|
|
Logger: logger,
|
|
Scope: statsScope,
|
|
Parser: eventParser,
|
|
CommentParser: commentParser,
|
|
GithubWebhookSecret: nil,
|
|
GithubRequestValidator: &events_controllers.DefaultGithubRequestValidator{},
|
|
GitlabRequestParserValidator: &events_controllers.DefaultGitlabRequestParserValidator{},
|
|
GitlabWebhookSecret: nil,
|
|
RepoAllowlistChecker: repoAllowlistChecker,
|
|
SupportedVCSHosts: []models.VCSHostType{models.Gitlab, models.Github, models.BitbucketCloud},
|
|
VCSClient: e2eVCSClient,
|
|
}
|
|
return ctrl, e2eVCSClient, e2eGithubGetter, workingDir
|
|
}
|
|
|
|
type mockLockURLGenerator struct{}
|
|
|
|
func (m *mockLockURLGenerator) GenerateLockURL(_ string) string {
|
|
return "lock-url"
|
|
}
|
|
|
|
type mockWebhookSender struct{}
|
|
|
|
func (w *mockWebhookSender) Send(_ logging.SimpleLogging, _ webhooks.ApplyResult) error {
|
|
return nil
|
|
}
|
|
|
|
func GitHubCommentEvent(t *testing.T, comment string) *http.Request {
|
|
requestJSON, err := os.ReadFile(filepath.Join("testdata", "githubIssueCommentEvent.json"))
|
|
Ok(t, err)
|
|
escapedComment, err := json.Marshal(comment)
|
|
Ok(t, err)
|
|
requestJSON = []byte(strings.Replace(string(requestJSON), "\"###comment body###\"", string(escapedComment), 1))
|
|
req, err := http.NewRequest("POST", "/events", bytes.NewBuffer(requestJSON))
|
|
Ok(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set(githubHeader, "issue_comment")
|
|
return req
|
|
}
|
|
|
|
func GitHubPullRequestOpenedEvent(t *testing.T, headSHA string) *http.Request {
|
|
requestJSON, err := os.ReadFile(filepath.Join("testdata", "githubPullRequestOpenedEvent.json"))
|
|
Ok(t, err)
|
|
// Replace sha with expected sha.
|
|
requestJSONStr := strings.Replace(string(requestJSON), "c31fd9ea6f557ad2ea659944c3844a059b83bc5d", headSHA, -1)
|
|
req, err := http.NewRequest("POST", "/events", bytes.NewBuffer([]byte(requestJSONStr)))
|
|
Ok(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set(githubHeader, "pull_request")
|
|
return req
|
|
}
|
|
|
|
func GitHubPullRequestClosedEvent(t *testing.T) *http.Request {
|
|
requestJSON, err := os.ReadFile(filepath.Join("testdata", "githubPullRequestClosedEvent.json"))
|
|
Ok(t, err)
|
|
req, err := http.NewRequest("POST", "/events", bytes.NewBuffer(requestJSON))
|
|
Ok(t, err)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set(githubHeader, "pull_request")
|
|
return req
|
|
}
|
|
|
|
func GitHubPullRequestParsed(headSHA string) *github.PullRequest {
|
|
// headSHA can't be empty so default if not set.
|
|
if headSHA == "" {
|
|
headSHA = "13940d121be73f656e2132c6d7b4c8e87878ac8d"
|
|
}
|
|
return &github.PullRequest{
|
|
Number: github.Ptr(2),
|
|
State: github.Ptr("open"),
|
|
HTMLURL: github.Ptr("htmlurl"),
|
|
Head: &github.PullRequestBranch{
|
|
Repo: &github.Repository{
|
|
FullName: github.Ptr("runatlantis/atlantis-tests"),
|
|
CloneURL: github.Ptr("https://github.com/runatlantis/atlantis-tests.git"),
|
|
},
|
|
SHA: github.Ptr(headSHA),
|
|
Ref: github.Ptr("branch"),
|
|
},
|
|
Base: &github.PullRequestBranch{
|
|
Repo: &github.Repository{
|
|
FullName: github.Ptr("runatlantis/atlantis-tests"),
|
|
CloneURL: github.Ptr("https://github.com/runatlantis/atlantis-tests.git"),
|
|
},
|
|
Ref: github.Ptr("main"),
|
|
},
|
|
User: &github.User{
|
|
Login: github.Ptr("atlantisbot"),
|
|
},
|
|
}
|
|
}
|
|
|
|
// absRepoPath returns the absolute path to the test repo under dir repoDir.
|
|
func absRepoPath(t *testing.T, repoDir string) string {
|
|
path, err := filepath.Abs(filepath.Join("testdata", "test-repos", repoDir))
|
|
Ok(t, err)
|
|
return path
|
|
}
|
|
|
|
// initializeRepo copies the repo data from testdata and initializes a new
|
|
// git repo in a temp directory. It returns that directory and a function
|
|
// to run in a defer that will delete the dir.
|
|
// The purpose of this function is to create a real git repository with a branch
|
|
// called 'branch' from the files under repoDir. This is so we can check in
|
|
// those files normally to this repo without needing a .git directory.
|
|
func initializeRepo(t *testing.T, repoDir string) (string, string) {
|
|
originRepo := absRepoPath(t, repoDir)
|
|
|
|
// Copy the files to the temp dir.
|
|
destDir := t.TempDir()
|
|
runCmd(t, "", "cp", "-r", fmt.Sprintf("%s/.", originRepo), destDir)
|
|
|
|
// Initialize the git repo.
|
|
runCmd(t, destDir, "git", "init")
|
|
runCmd(t, destDir, "touch", ".gitkeep")
|
|
runCmd(t, destDir, "git", "add", ".gitkeep")
|
|
runCmd(t, destDir, "git", "config", "--local", "user.email", "atlantisbot@runatlantis.io")
|
|
runCmd(t, destDir, "git", "config", "--local", "user.name", "atlantisbot")
|
|
runCmd(t, destDir, "git", "commit", "-m", "initial commit")
|
|
runCmd(t, destDir, "git", "checkout", "-b", "branch")
|
|
runCmd(t, destDir, "git", "add", ".")
|
|
runCmd(t, destDir, "git", "commit", "-am", "branch commit")
|
|
headSHA := runCmd(t, destDir, "git", "rev-parse", "HEAD")
|
|
headSHA = strings.Trim(headSHA, "\n")
|
|
|
|
return destDir, headSHA
|
|
}
|
|
|
|
func runCmd(t *testing.T, dir string, name string, args ...string) string {
|
|
cpCmd := exec.Command(name, args...)
|
|
cpCmd.Dir = dir
|
|
cpOut, err := cpCmd.CombinedOutput()
|
|
Assert(t, err == nil, "err running %q: %s", strings.Join(append([]string{name}, args...), " "), cpOut)
|
|
return string(cpOut)
|
|
}
|
|
|
|
func assertCommentEquals(t *testing.T, expReplies []string, act string, repoDir string, parallel bool) {
|
|
t.Helper()
|
|
|
|
// Replace all 'Creation complete after 0s [id=2135833172528078362]' strings with
|
|
// 'Creation complete after *s [id=*******************]' so we can do a comparison.
|
|
idRegex := regexp.MustCompile(`Creation complete after [0-9]+s \[id=[0-9]+]`)
|
|
act = idRegex.ReplaceAllString(act, "Creation complete after *s [id=*******************]")
|
|
|
|
// Replace all null_resource.simple{n}: .* with null_resource.simple: because
|
|
// with multiple resources being created the logs are all out of order which
|
|
// makes comparison impossible.
|
|
resourceRegex := regexp.MustCompile(`null_resource\.simple(\[\d])?\d?:.*`)
|
|
act = resourceRegex.ReplaceAllString(act, "null_resource.simple:")
|
|
|
|
// For parallel plans and applies, do a substring match since output may be out of order
|
|
var replyMatchesExpected func(string, string) bool
|
|
if parallel {
|
|
replyMatchesExpected = func(act string, expStr string) bool {
|
|
return strings.Contains(act, expStr)
|
|
}
|
|
} else {
|
|
replyMatchesExpected = func(act string, expStr string) bool {
|
|
return expStr == act
|
|
}
|
|
}
|
|
|
|
for _, expFile := range expReplies {
|
|
exp, err := os.ReadFile(filepath.Join(absRepoPath(t, repoDir), expFile))
|
|
Ok(t, err)
|
|
expStr := string(exp)
|
|
// My editor adds a newline to all the files, so if the actual comment
|
|
// doesn't end with a newline then strip the last newline from the file's
|
|
// contents.
|
|
if !strings.HasSuffix(act, "\n") {
|
|
expStr = strings.TrimSuffix(expStr, "\n")
|
|
}
|
|
|
|
if !replyMatchesExpected(act, expStr) {
|
|
// If in CI, we write the diff to the console. Otherwise we write the diff
|
|
// to file so we can use our local diff viewer.
|
|
if os.Getenv("CI") == "true" {
|
|
t.Logf("exp: %s, got: %s", expStr, act)
|
|
t.FailNow()
|
|
} else {
|
|
actFile := filepath.Join(absRepoPath(t, repoDir), expFile+".act")
|
|
err := os.WriteFile(actFile, []byte(act), 0600)
|
|
Ok(t, err)
|
|
cwd, err := os.Getwd()
|
|
Ok(t, err)
|
|
rel, err := filepath.Rel(cwd, actFile)
|
|
Ok(t, err)
|
|
t.Errorf("%q was different, wrote actual comment to %q", expFile, rel)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// returns parent, bindir, cachedir, cleanup func
|
|
func mkSubDirs(t *testing.T) (string, string, string) {
|
|
tmp := t.TempDir()
|
|
binDir := filepath.Join(tmp, "bin")
|
|
err := os.MkdirAll(binDir, 0700)
|
|
Ok(t, err)
|
|
|
|
cachedir := filepath.Join(tmp, "plugin-cache")
|
|
err = os.MkdirAll(cachedir, 0700)
|
|
Ok(t, err)
|
|
|
|
return tmp, binDir, cachedir
|
|
}
|
|
|
|
// Will fail test if conftest isn't in path
|
|
func ensureRunningConftest(t *testing.T) {
|
|
// use `conftest` command instead `conftest$version`, so tests may fail on the environment cause the output logs may become change by version.
|
|
t.Logf("conftest check may fail depends on conftest version. please use latest stable conftest.")
|
|
_, err := exec.LookPath(conftestCommand)
|
|
if err != nil {
|
|
t.Logf(`%s must be installed to run this test
|
|
- on local, please install conftest command or run 'make docker/test-all'
|
|
- on CI, please check testing-env docker image contains conftest command. see testing/Dockerfile
|
|
`, conftestCommand)
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
// Will fail test if terraform isn't in path and isn't version >= 0.14
|
|
func ensureRunning014(t *testing.T) {
|
|
localPath, err := exec.LookPath("terraform")
|
|
if err != nil {
|
|
t.Log("terraform >= 0.14 must be installed to run this test")
|
|
t.FailNow()
|
|
}
|
|
versionOutBytes, err := exec.Command(localPath, "version").Output() // #nosec
|
|
if err != nil {
|
|
t.Logf("error running terraform version: %s", err)
|
|
t.FailNow()
|
|
}
|
|
versionOutput := string(versionOutBytes)
|
|
match := versionRegex.FindStringSubmatch(versionOutput)
|
|
if len(match) <= 1 {
|
|
t.Logf("could not parse terraform version from %s", versionOutput)
|
|
t.FailNow()
|
|
}
|
|
localVersion, err := version.NewVersion(match[1])
|
|
Ok(t, err)
|
|
minVersion, err := version.NewVersion("0.14.0")
|
|
Ok(t, err)
|
|
if localVersion.LessThan(minVersion) {
|
|
t.Logf("must have terraform version >= %s, you have %s", minVersion, localVersion)
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
// versionRegex extracts the version from `terraform version` output.
|
|
//
|
|
// Terraform v0.12.0-alpha4 (2c36829d3265661d8edbd5014de8090ea7e2a076)
|
|
// => 0.12.0-alpha4
|
|
//
|
|
// Terraform v0.11.10
|
|
// => 0.11.10
|
|
//
|
|
// OpenTofu v1.0.0
|
|
// => 1.0.0
|
|
var versionRegex = regexp.MustCompile("(?:Terraform|OpenTofu) v(.*?)(\\s.*)?\n")
|