From cd7ae114ebed9bd2e0ff4bbeabbb182e9ff57c25 Mon Sep 17 00:00:00 2001 From: Simon Heather <32168619+X-Guardian@users.noreply.github.com> Date: Mon, 11 Sep 2023 15:57:50 +0100 Subject: [PATCH] feat: add ability to track git untracked files (#3724) --- cmd/server.go | 5 + runatlantis.io/docs/custom-workflows.md | 50 +- runatlantis.io/docs/server-configuration.md | 10 + .../events/events_controller_e2e_test.go | 1 + server/events/mock_workingdir_test.go | 54 ++ server/events/mocks/mock_working_dir.go | 54 ++ server/events/project_command_builder.go | 124 +++-- .../project_command_builder_internal_test.go | 4 + server/events/project_command_builder_test.go | 472 ++++++++++++++---- server/events/project_finder.go | 2 +- server/events/working_dir.go | 27 +- server/server.go | 1 + server/user_config.go | 1 + 13 files changed, 653 insertions(+), 152 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index b9949a390..e79012408 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -97,6 +97,7 @@ const ( GitlabTokenFlag = "gitlab-token" GitlabUserFlag = "gitlab-user" GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec + IncludeGitUntrackedFiles = "include-git-untracked-files" APISecretFlag = "api-secret" HidePrevPlanComments = "hide-prev-plan-comments" QuietPolicyChecks = "quiet-policy-checks" @@ -475,6 +476,10 @@ var boolFlags = map[string]boolFlag{ "VCS support is limited to: GitHub.", defaultValue: false, }, + IncludeGitUntrackedFiles: { + description: "Include git untracked files in the Atlantis modified file scope.", + defaultValue: false, + }, ParallelPlanFlag: { description: "Run plan operations in parallel.", defaultValue: false, diff --git a/runatlantis.io/docs/custom-workflows.md b/runatlantis.io/docs/custom-workflows.md index 3ba12e132..a6ee35ab6 100644 --- a/runatlantis.io/docs/custom-workflows.md +++ b/runatlantis.io/docs/custom-workflows.md @@ -161,16 +161,17 @@ workflows: - run: terraform apply $PLANFILE ``` -### cdktf -Here are the requirements to enable [cdktf](https://developer.hashicorp.com/terraform/cdktf) +### CDKTF +Here are the requirements to enable [CDKTF](https://developer.hashicorp.com/terraform/cdktf) -- A custom image with `cdktf` installed -- The autoplan file updated to trigger off of `**/cdk.tf.json` -- The output of `cdktf synth` has to be committed to the pull request -- Optional: Use `pre_workflow_hooks` to run `cdktf synth` as a double check +- A custom image with `CDKTF` installed +- Add `**/cdk.tf.json` to the list of Atlantis autoplan files. +- Set the `atlantis-include-git-untracked-files` flag so that the Terraform files dynamically generated +by CDKTF will be add to the Atlantis modified file list. +- Use `pre_workflow_hooks` to run `cdktf synth` - Optional: There isn't a requirement to use a repo `atlantis.yaml` but one can be leveraged if needed. -#### custom image +#### Custom Image ```dockerfile # Dockerfile @@ -179,11 +180,12 @@ FROM ghcr.io/runatlantis/atlantis:v0.19.7 RUN apk add npm && npm i -g cdktf-cli ``` -#### server config +#### Server Config ```bash # env variables ATLANTIS_AUTOPLAN_FILE_LIST="**/*.tf,**/*.tfvars,**/*.tfvars.json,**/cdk.tf.json" +ATLANTIS_INCLUDE_GIT_UNTRACKED_FILES=true ``` OR @@ -192,9 +194,10 @@ OR ```yaml # config.yaml autoplan-file-list: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/cdk.tf.json" +include-git-untracked-files: true ``` -#### server repo config +#### Server Repo Config Use `pre_workflow_hooks` @@ -204,32 +207,39 @@ Use `pre_workflow_hooks` repos: - id: /.*cdktf.*/ pre_workflow_hooks: - - run: npm i && cdktf get && cdktf synth + - run: npm i && cdktf get && cdktf synth --output ci-cdktf.out ``` -#### repo structure +**Note:** don't use the default `cdktf.out` directory that CDKTF uses, as this should be in the `.gitignore` list of the +repo, so that locally generated files are not checked in. -This is the git repo structure after running `cdktf synth`. The `cdk.tf.json` files contain the HCL that atlantis can run. +#### Repo Structure + +This is the git repo structure after running `cdktf synth`. The `cdk.tf.json` files contain the Terraform configuration +that atlantis can run. ```bash $ tree --gitignore . ├── cdktf.json -├── cdktf.out +├── ci-cdktf.out │ ├── manifest.json │ └── stacks │ └── eks │ └── cdk.tf.json ``` -#### workflow +#### Workflow -1. Container orchestrator (k8s/fargate/ecs/etc) uses the custom docker image of atlantis with `cdktf` installed with the `--autoplan-file-list` to trigger on json files -1. PR branch is pushed up containing `cdktf` changes and generated hcl json -1. Atlantis checks out the branch in the repo -1. Atlantis runs the `npm i && cdktf get && cdktf synth` command in the repo root as a step in `pre_workflow_hooks` (as a double check described above) -1. Atlantis detects the change to the generated hcl json files in a number of `dir`s -1. Atlantis then runs `terraform` workflows in the respective `dir`s as usual +1. Container orchestrator (k8s/fargate/ecs/etc) uses the custom docker image of atlantis with `cdktf` installed with +the `--autoplan-file-list` to trigger on `cdk.tf.json` files and `--include-git-untracked-files` set to include the +CDKTF dynamically generated Terraform files in the Atlantis plan. +1. PR branch is pushed up containing `cdktf` code changes. +1. Atlantis checks out the branch in the repo. +1. Atlantis runs the `npm i && cdktf get && cdktf synth` command in the repo root as a step in `pre_workflow_hooks`, +generating the `cdk.tf.json` Terraform files. +1. Atlantis detects the `cdk.tf.json` untracked files in a number of directories. +1. Atlantis then runs `terraform` workflows in the respective directories as usual. ### Terragrunt Atlantis supports running custom commands in place of the default Atlantis diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 98a83f4e7..ccdd32833 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -626,6 +626,16 @@ This is useful when you have many projects and want to keep the pull request cle Hide previous plan comments to declutter PRs. This is only supported in GitHub and GitLab currently. This is not enabled by default. +### `--include-git-untracked-files` + ```bash + atlantis server --include-git-untracked-files + # or + ATLANTIS_INCLUDE_GIT_UNTRACKED_FILES=true + ``` + Include git untracked files in the Atlantis modified file list. + Used for example with CDKTF pre-workflow hooks that dynamically generate + Terraform files. + ### `--locking-db-type` ```bash atlantis server --locking-db-type="" diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 551851ab3..14b1f02c0 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1323,6 +1323,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", false, false, + false, statsScope, logger, terraformClient, diff --git a/server/events/mock_workingdir_test.go b/server/events/mock_workingdir_test.go index 046b9a203..27a0695ea 100644 --- a/server/events/mock_workingdir_test.go +++ b/server/events/mock_workingdir_test.go @@ -93,6 +93,25 @@ func (mock *MockWorkingDir) DeletePlan(r models.Repo, p models.PullRequest, work return ret0 } +func (mock *MockWorkingDir) GetGitUntrackedFiles(r models.Repo, p models.PullRequest, workspace string) ([]string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockWorkingDir().") + } + params := []pegomock.Param{r, p, workspace} + result := pegomock.GetGenericMockFrom(mock).Invoke("GetGitUntrackedFiles", params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 []string + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].([]string) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + func (mock *MockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") @@ -335,6 +354,41 @@ func (c *MockWorkingDir_DeletePlan_OngoingVerification) GetAllCapturedArguments( return } +func (verifier *VerifierMockWorkingDir) GetGitUntrackedFiles(r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification { + params := []pegomock.Param{r, p, workspace} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetGitUntrackedFiles", params, verifier.timeout) + return &MockWorkingDir_GetGitUntrackedFiles_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockWorkingDir_GetGitUntrackedFiles_OngoingVerification struct { + mock *MockWorkingDir + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, string) { + r, p, workspace := c.GetAllCapturedArguments() + return r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1] +} + +func (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(models.Repo) + } + _param1 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(models.PullRequest) + } + _param2 = make([]string, len(c.methodInvocations)) + for u, param := range params[2] { + _param2[u] = param.(string) + } + } + return +} + func (verifier *VerifierMockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) *MockWorkingDir_GetPullDir_OngoingVerification { params := []pegomock.Param{r, p} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullDir", params, verifier.timeout) diff --git a/server/events/mocks/mock_working_dir.go b/server/events/mocks/mock_working_dir.go index e22c2ee92..8f051f193 100644 --- a/server/events/mocks/mock_working_dir.go +++ b/server/events/mocks/mock_working_dir.go @@ -93,6 +93,25 @@ func (mock *MockWorkingDir) DeletePlan(r models.Repo, p models.PullRequest, work return ret0 } +func (mock *MockWorkingDir) GetGitUntrackedFiles(r models.Repo, p models.PullRequest, workspace string) ([]string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockWorkingDir().") + } + params := []pegomock.Param{r, p, workspace} + result := pegomock.GetGenericMockFrom(mock).Invoke("GetGitUntrackedFiles", params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var ret0 []string + var ret1 error + if len(result) != 0 { + if result[0] != nil { + ret0 = result[0].([]string) + } + if result[1] != nil { + ret1 = result[1].(error) + } + } + return ret0, ret1 +} + func (mock *MockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) (string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockWorkingDir().") @@ -335,6 +354,41 @@ func (c *MockWorkingDir_DeletePlan_OngoingVerification) GetAllCapturedArguments( return } +func (verifier *VerifierMockWorkingDir) GetGitUntrackedFiles(r models.Repo, p models.PullRequest, workspace string) *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification { + params := []pegomock.Param{r, p, workspace} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetGitUntrackedFiles", params, verifier.timeout) + return &MockWorkingDir_GetGitUntrackedFiles_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockWorkingDir_GetGitUntrackedFiles_OngoingVerification struct { + mock *MockWorkingDir + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, string) { + r, p, workspace := c.GetAllCapturedArguments() + return r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1] +} + +func (c *MockWorkingDir_GetGitUntrackedFiles_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []string) { + params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(params) > 0 { + _param0 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range params[0] { + _param0[u] = param.(models.Repo) + } + _param1 = make([]models.PullRequest, len(c.methodInvocations)) + for u, param := range params[1] { + _param1[u] = param.(models.PullRequest) + } + _param2 = make([]string, len(c.methodInvocations)) + for u, param := range params[2] { + _param2[u] = param.(string) + } + } + return +} + func (verifier *VerifierMockWorkingDir) GetPullDir(r models.Repo, p models.PullRequest) *MockWorkingDir_GetPullDir_OngoingVerification { params := []pegomock.Param{r, p} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullDir", params, verifier.timeout) diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 2b56d60a6..2347c9072 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -53,6 +53,7 @@ func NewInstrumentedProjectCommandBuilder( AutoplanFileList string, RestrictFileList bool, SilenceNoProjects bool, + IncludeGitUntrackedFiles bool, scope tally.Scope, logger logging.SimpleLogging, terraformClient terraform.Client, @@ -83,6 +84,7 @@ func NewInstrumentedProjectCommandBuilder( AutoplanFileList, RestrictFileList, SilenceNoProjects, + IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -111,27 +113,29 @@ func NewProjectCommandBuilder( AutoplanFileList string, RestrictFileList bool, SilenceNoProjects bool, + IncludeGitUntrackedFiles bool, scope tally.Scope, logger logging.SimpleLogging, terraformClient terraform.Client, ) *DefaultProjectCommandBuilder { return &DefaultProjectCommandBuilder{ - ParserValidator: parserValidator, - ProjectFinder: projectFinder, - VCSClient: vcsClient, - WorkingDir: workingDir, - WorkingDirLocker: workingDirLocker, - GlobalCfg: globalCfg, - PendingPlanFinder: pendingPlanFinder, - SkipCloneNoChanges: skipCloneNoChanges, - EnableRegExpCmd: EnableRegExpCmd, - EnableAutoMerge: EnableAutoMerge, - EnableParallelPlan: EnableParallelPlan, - EnableParallelApply: EnableParallelApply, - AutoDetectModuleFiles: AutoDetectModuleFiles, - AutoplanFileList: AutoplanFileList, - RestrictFileList: RestrictFileList, - SilenceNoProjects: SilenceNoProjects, + ParserValidator: parserValidator, + ProjectFinder: projectFinder, + VCSClient: vcsClient, + WorkingDir: workingDir, + WorkingDirLocker: workingDirLocker, + GlobalCfg: globalCfg, + PendingPlanFinder: pendingPlanFinder, + SkipCloneNoChanges: skipCloneNoChanges, + EnableRegExpCmd: EnableRegExpCmd, + EnableAutoMerge: EnableAutoMerge, + EnableParallelPlan: EnableParallelPlan, + EnableParallelApply: EnableParallelApply, + AutoDetectModuleFiles: AutoDetectModuleFiles, + AutoplanFileList: AutoplanFileList, + RestrictFileList: RestrictFileList, + SilenceNoProjects: SilenceNoProjects, + IncludeGitUntrackedFiles: IncludeGitUntrackedFiles, ProjectCommandContextBuilder: NewProjectCommandContextBuilder( policyChecksSupported, commentBuilder, @@ -184,7 +188,7 @@ type ProjectStateCommandBuilder interface { BuildStateRmCommands(ctx *command.Context, comment *CommentCommand) ([]command.ProjectContext, error) } -//go:generate pegomock generate --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder +//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_project_command_builder.go ProjectCommandBuilder // ProjectCommandBuilder builds commands that run on individual projects. type ProjectCommandBuilder interface { @@ -200,25 +204,46 @@ type ProjectCommandBuilder interface { // This class combines the data from the comment and any atlantis.yaml file or // Atlantis server config and then generates a set of contexts. type DefaultProjectCommandBuilder struct { - ParserValidator *config.ParserValidator - ProjectFinder ProjectFinder - VCSClient vcs.Client - WorkingDir WorkingDir - WorkingDirLocker WorkingDirLocker - GlobalCfg valid.GlobalCfg - PendingPlanFinder *DefaultPendingPlanFinder + // Parses and validates server-side repo config files and repo-level atlantis.yaml files. + ParserValidator *config.ParserValidator + // Determines which projects were modified in a given pull request. + ProjectFinder ProjectFinder + // Used to make API calls to a VCS host like GitHub or GitLab. + VCSClient vcs.Client + // Handles the workspace on disk for running commands. + WorkingDir WorkingDir + // Used to prevent multiple commands from executing at the same time for a single repo, pull, and workspace. + WorkingDirLocker WorkingDirLocker + // The final parsed version of the server-side repo config. + GlobalCfg valid.GlobalCfg + // Finds unapplied plans. + PendingPlanFinder *DefaultPendingPlanFinder + // Builds project command contexts for Atlantis commands. ProjectCommandContextBuilder ProjectCommandContextBuilder - SkipCloneNoChanges bool - EnableRegExpCmd bool - EnableAutoMerge bool - EnableParallelPlan bool - EnableParallelApply bool - AutoDetectModuleFiles string - AutoplanFileList string - EnableDiffMarkdownFormat bool - RestrictFileList bool - SilenceNoProjects bool - TerraformExecutor terraform.Client + // User config option: Skip cloning the repo during autoplan if there are no changes to Terraform projects. + SkipCloneNoChanges bool + // User config option: Enable the use of regular expressions to run plan/apply commands against defined project names. + EnableRegExpCmd bool + // User config option: Automatically merge pull requests after all plans have been successfully applied. + EnableAutoMerge bool + // User config option: Whether to run plan operations in parallel. + EnableParallelPlan bool + // User config option: Whether to run apply operations in parallel. + EnableParallelApply bool + // User config option: Enables auto-planning of projects when a module dependency in the same repository has changed. + AutoDetectModuleFiles string + // User config option: List of file patterns to to to check if a directory contains modified files. + AutoplanFileList string + // User config option: Format Terraform plan output into a markdown-diff friendy format for color-coding purposes. + EnableDiffMarkdownFormat bool + // User config option: Block plan requests from projects outside the files modified in the pull request. + RestrictFileList bool + // User config option: Ignore PR if none of the modified files are part of a project. + SilenceNoProjects bool + // User config option: Include git untracked files in the modified file list. + IncludeGitUntrackedFiles bool + // Handles the actual running of Terraform commands. + TerraformExecutor terraform.Client } // See ProjectCommandBuilder.BuildAutoplanCommands. @@ -241,8 +266,11 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Contex // See ProjectCommandBuilder.BuildPlanCommands. func (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { if !cmd.IsForSpecificProject() { + ctx.Log.Debug("Building plan command for all affected projects") return p.buildAllCommandsByCfg(ctx, cmd.CommandName(), cmd.SubName, cmd.Flags, cmd.Verbose) } + ctx.Log.Debug("Building plan command for specific project with directory: '%v', workspace: '%v', project: '%v'", + cmd.RepoRelDir, cmd.Workspace, cmd.ProjectName) pcc, err := p.buildProjectPlanCommand(ctx, cmd) return pcc, err } @@ -296,7 +324,17 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex if err != nil { return nil, err } - ctx.Log.Debug("%d files were modified in this pull request", len(modifiedFiles)) + + if p.IncludeGitUntrackedFiles { + ctx.Log.Debug(("'include-git-untracked-files' option is set, getting untracked files")) + untrackedFiles, err := p.WorkingDir.GetGitUntrackedFiles(ctx.HeadRepo, ctx.Pull, DefaultWorkspace) + if err != nil { + return nil, err + } + modifiedFiles = append(modifiedFiles, untrackedFiles...) + } + + ctx.Log.Debug("%d files were modified in this pull request. Modified files: %v", len(modifiedFiles), modifiedFiles) if p.SkipCloneNoChanges && p.VCSClient.SupportsSingleFileDownload(ctx.Pull.BaseRepo) { repoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID()) @@ -479,12 +517,25 @@ func (p *DefaultProjectCommandBuilder) buildProjectPlanCommand(ctx *command.Cont } if p.RestrictFileList { + ctx.Log.Debug("'restrict-file-list' option is set, checking modified files") modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Pull.BaseRepo, ctx.Pull) if err != nil { return nil, err } + if p.IncludeGitUntrackedFiles { + ctx.Log.Debug(("'include-git-untracked-files' option is set, getting untracked files")) + untrackedFiles, err := p.WorkingDir.GetGitUntrackedFiles(ctx.HeadRepo, ctx.Pull, workspace) + if err != nil { + return nil, err + } + modifiedFiles = append(modifiedFiles, untrackedFiles...) + } + + ctx.Log.Debug("%d files were modified in this pull request. Modified files: %v", len(modifiedFiles), modifiedFiles) + if cmd.RepoRelDir != "" { + ctx.Log.Debug("Command directory specified: %s", cmd.RepoRelDir) foundDir := false for _, f := range modifiedFiles { @@ -499,6 +550,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectPlanCommand(ctx *command.Cont } if cmd.ProjectName != "" { + ctx.Log.Debug("Command project name specified: %s", cmd.ProjectName) var notFoundFiles = []string{} var repoConfig valid.RepoCfg diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index b0a4c8760..e8804c514 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -672,6 +672,7 @@ projects: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", false, false, + false, statsScope, logger, terraformClient, @@ -886,6 +887,7 @@ projects: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", false, false, + false, statsScope, logger, terraformClient, @@ -1134,6 +1136,7 @@ workflows: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", false, false, + false, statsScope, logger, terraformClient, @@ -1289,6 +1292,7 @@ projects: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", false, true, + false, statsScope, logger, terraformClient, diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index 36ed6426a..6a3e73798 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -22,6 +22,30 @@ import ( . "github.com/runatlantis/atlantis/testing" ) +var defaultUserConfig = struct { + SkipCloneNoChanges bool + EnableRegExpCmd bool + EnableAutoMerge bool + EnableParallelPlan bool + EnableParallelApply bool + AutoDetectModuleFiles string + AutoplanFileList string + RestrictFileList bool + SilenceNoProjects bool + IncludeGitUntrackedFiles bool +}{ + SkipCloneNoChanges: false, + EnableRegExpCmd: false, + EnableAutoMerge: false, + EnableParallelPlan: false, + EnableParallelApply: false, + AutoDetectModuleFiles: "", + AutoplanFileList: "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", + RestrictFileList: false, + SilenceNoProjects: false, + IncludeGitUntrackedFiles: true, +} + func TestDefaultProjectCommandBuilder_BuildAutoplanCommands(t *testing.T) { // expCtxFields define the ctx fields we're going to assert on. // Since we're focused on autoplanning here, we don't validate all the @@ -122,6 +146,7 @@ projects: logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig terraformClient := terraform_mocks.NewMockClient() When(terraformClient.ListAvailableVersions(Any[logging.SimpleLogging]())).ThenReturn([]string{}, nil) @@ -159,15 +184,16 @@ projects: valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - false, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -473,6 +499,7 @@ projects: logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig for _, c := range cases { // NOTE: we're testing both plan and apply here. @@ -513,15 +540,16 @@ projects: valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - true, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, c.EnableAutoMergeUserCfg, c.EnableParallelPlanUserCfg, c.EnableParallelApplyUserCfg, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, c.Silenced, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -663,6 +691,8 @@ projects: logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig + userConfig.RestrictFileList = true for _, c := range cases { t.Run(c.Description+"_"+command.Plan.String(), func(t *testing.T) { @@ -699,15 +729,16 @@ projects: valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - true, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - true, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -929,6 +960,8 @@ projects: logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig + for name, c := range cases { t.Run(name, func(t *testing.T) { RegisterMockTestingT(t) @@ -964,15 +997,16 @@ projects: valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, c.ParallelPlanEnabledUserCfg, c.ParallelApplyEnabledUserCfg, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -1044,6 +1078,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { ThenReturn(tmpDir, nil) logger := logging.NewNoopLogger(t) + userConfig := defaultUserConfig globalCfgArgs := valid.GlobalCfgArgs{ AllowRepoCfg: false, @@ -1066,15 +1101,16 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - false, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -1145,6 +1181,8 @@ projects: } logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig + terraformClient := terraform_mocks.NewMockClient() When(terraformClient.ListAvailableVersions(Any[logging.SimpleLogging]())).ThenReturn([]string{}, nil) @@ -1158,15 +1196,16 @@ projects: valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - false, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -1212,6 +1251,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig for _, c := range cases { t.Run(strings.Join(c.ExtraArgs, " "), func(t *testing.T) { @@ -1246,15 +1286,16 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - false, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -1366,6 +1407,7 @@ projects: logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig for name, testCase := range testCases { t.Run(name, func(t *testing.T) { @@ -1415,15 +1457,16 @@ projects: valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - false, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -1482,6 +1525,9 @@ parallel_plan: true`, }, } + userConfig := defaultUserConfig + userConfig.SkipCloneNoChanges = true + for _, c := range cases { RegisterMockTestingT(t) vcsClient := vcsmocks.NewMockClient() @@ -1512,15 +1558,16 @@ parallel_plan: true`, valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - true, - false, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -1552,6 +1599,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig workingDir := mocks.NewMockWorkingDir() When(workingDir.Clone(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, false, nil) @@ -1580,15 +1628,16 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman globalCfg, &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - false, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -1650,6 +1699,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { logger := logging.NewNoopLogger(t) scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig globalCfgArgs := valid.GlobalCfgArgs{ AllowRepoCfg: false, @@ -1670,15 +1720,16 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { valid.NewGlobalCfgFromArgs(globalCfgArgs), &events.DefaultPendingPlanFinder{}, &events.CommentParser{ExecutableName: "atlantis"}, - false, - false, - false, - false, - false, - "", - "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", - false, - false, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, scope, logger, terraformClient, @@ -1708,3 +1759,238 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { Equals(t, "project2", ctxs[3].RepoRelDir) Equals(t, "workspace2", ctxs[3].Workspace) } + +// Test +func TestDefaultProjectCommandBuilder_BuildPlanCommands_Single_With_RestrictFileList_And_IncludeGitUntrackedFiles(t *testing.T) { + testDir1 := "directory-1" + testDir2 := "directory-2" + + cases := []struct { + Description string + AtlantisYAML string + DirectoryStructure map[string]interface{} + ModifiedFiles []string + UntrackedFiles []string + Cmd events.CommentCommand + ExpRepoRelDir string + ExpErr string + }{ + { + Description: "planning a git untracked file project in a modified directory", + Cmd: events.CommentCommand{ + Name: command.Plan, + RepoRelDir: testDir1 + "/ci-cdktf.out/stacks/test", + Workspace: "default", + }, + DirectoryStructure: map[string]interface{}{ + testDir1: map[string]interface{}{ + "main.ts": nil, + }, + }, + ModifiedFiles: []string{testDir1 + "/main.ts"}, + UntrackedFiles: []string{testDir1 + "/ci-cdktf.out/stacks/test/cdk.tf.json"}, + ExpRepoRelDir: testDir1 + "/ci-cdktf.out/stacks/test", + }, + { + Description: "planning a git untracked file project outside a modified directory", + Cmd: events.CommentCommand{ + Name: command.Plan, + RepoRelDir: testDir2 + "/ci-cdktf.out/stacks/test", + Workspace: "default", + }, + DirectoryStructure: map[string]interface{}{ + testDir1: map[string]interface{}{ + "main.ts": nil, + }, + }, + ModifiedFiles: []string{testDir1 + "/main.ts"}, + UntrackedFiles: []string{testDir1 + "/ci-cdktf.out/stacks/test/cdk.tf.json"}, + ExpErr: "the dir \"" + testDir2 + "/ci-cdktf.out/stacks/test\" is not in the plan list of this pull request", + }, + } + + globalCfgArgs := valid.GlobalCfgArgs{ + AllowRepoCfg: true, + MergeableReq: false, + ApprovedReq: false, + UnDivergedReq: false, + } + + logger := logging.NewNoopLogger(t) + scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig + userConfig.RestrictFileList = true + userConfig.IncludeGitUntrackedFiles = true + + for _, c := range cases { + t.Run(c.Description+"_"+command.Plan.String(), func(t *testing.T) { + RegisterMockTestingT(t) + tmpDir := DirStructure(t, c.DirectoryStructure) + + workingDir := mocks.NewMockWorkingDir() + When(workingDir.Clone(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, false, nil) + When(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil) + When(workingDir.GetGitUntrackedFiles(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(c.UntrackedFiles, nil) + vcsClient := vcsmocks.NewMockClient() + When(vcsClient.GetModifiedFiles(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil) + if c.AtlantisYAML != "" { + err := os.WriteFile(filepath.Join(tmpDir, valid.DefaultAtlantisFile), []byte(c.AtlantisYAML), 0600) + Ok(t, err) + } + + terraformClient := terraform_mocks.NewMockClient() + When(terraformClient.ListAvailableVersions(Any[logging.SimpleLogging]())).ThenReturn([]string{}, nil) + + builder := events.NewProjectCommandBuilder( + false, // policyChecksSupported + &config.ParserValidator{}, + &events.DefaultProjectFinder{}, + vcsClient, + workingDir, + events.NewDefaultWorkingDirLocker(), + valid.NewGlobalCfgFromArgs(globalCfgArgs), + &events.DefaultPendingPlanFinder{}, + &events.CommentParser{ExecutableName: "atlantis"}, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, + scope, + logger, + terraformClient, + ) + + var actCtxs []command.ProjectContext + var err error + actCtxs, err = builder.BuildPlanCommands(&command.Context{ + Log: logger, + Scope: scope, + }, &c.Cmd) + if c.ExpErr != "" { + ErrEquals(t, c.ExpErr, err) + return + } + Ok(t, err) + Equals(t, 1, len(actCtxs)) + actCtx := actCtxs[0] + Equals(t, c.ExpRepoRelDir, actCtx.RepoRelDir) + }) + } +} + +func TestDefaultProjectCommandBuilder_BuildPlanCommands_with_IncludeGitUntrackedFiles(t *testing.T) { + testDir1 := "directory-1" + + cases := []struct { + Description string + AtlantisYAML string + DirectoryStructure map[string]interface{} + ModifiedFiles []string + UntrackedFiles []string + Cmd events.CommentCommand + ExpRepoRelDir string + ExpErr string + }{ + { + Description: "planning with a git untracked file", + Cmd: events.CommentCommand{ + Name: command.Plan, + }, + DirectoryStructure: map[string]interface{}{ + testDir1: map[string]interface{}{ + "main.ts": nil, + "ci-cdktf.out": map[string]interface{}{ + "stacks": map[string]interface{}{ + "test": map[string]interface{}{ + "cdk.tf.json": nil, + }, + }, + }, + }, + }, + ModifiedFiles: []string{testDir1 + "/main.ts"}, + UntrackedFiles: []string{testDir1 + "/ci-cdktf.out/stacks/test/cdk.tf.json"}, + ExpRepoRelDir: testDir1 + "/ci-cdktf.out/stacks/test", + }, + } + + globalCfgArgs := valid.GlobalCfgArgs{ + AllowRepoCfg: true, + MergeableReq: false, + ApprovedReq: false, + UnDivergedReq: false, + } + + logger := logging.NewNoopLogger(t) + scope, _, _ := metrics.NewLoggingScope(logger, "atlantis") + userConfig := defaultUserConfig + userConfig.IncludeGitUntrackedFiles = true + userConfig.AutoplanFileList = "**/cdk.tf.json" + + for _, c := range cases { + t.Run(c.Description+"_"+command.Plan.String(), func(t *testing.T) { + RegisterMockTestingT(t) + tmpDir := DirStructure(t, c.DirectoryStructure) + + workingDir := mocks.NewMockWorkingDir() + When(workingDir.Clone(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, false, nil) + When(workingDir.GetWorkingDir(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmpDir, nil) + When(workingDir.GetGitUntrackedFiles(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(c.UntrackedFiles, nil) + vcsClient := vcsmocks.NewMockClient() + When(vcsClient.GetModifiedFiles(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.ModifiedFiles, nil) + if c.AtlantisYAML != "" { + err := os.WriteFile(filepath.Join(tmpDir, valid.DefaultAtlantisFile), []byte(c.AtlantisYAML), 0600) + Ok(t, err) + } + + terraformClient := terraform_mocks.NewMockClient() + When(terraformClient.ListAvailableVersions(Any[logging.SimpleLogging]())).ThenReturn([]string{}, nil) + + builder := events.NewProjectCommandBuilder( + false, // policyChecksSupported + &config.ParserValidator{}, + &events.DefaultProjectFinder{}, + vcsClient, + workingDir, + events.NewDefaultWorkingDirLocker(), + valid.NewGlobalCfgFromArgs(globalCfgArgs), + &events.DefaultPendingPlanFinder{}, + &events.CommentParser{ExecutableName: "atlantis"}, + userConfig.SkipCloneNoChanges, + userConfig.EnableRegExpCmd, + userConfig.EnableAutoMerge, + userConfig.EnableParallelPlan, + userConfig.EnableParallelApply, + userConfig.AutoDetectModuleFiles, + userConfig.AutoplanFileList, + userConfig.RestrictFileList, + userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, + scope, + logger, + terraformClient, + ) + + var actCtxs []command.ProjectContext + var err error + actCtxs, err = builder.BuildPlanCommands(&command.Context{ + Log: logger, + Scope: scope, + }, &c.Cmd) + if c.ExpErr != "" { + ErrEquals(t, c.ExpErr, err) + return + } + Ok(t, err) + Equals(t, 1, len(actCtxs)) + actCtx := actCtxs[0] + Equals(t, c.ExpRepoRelDir, actCtx.RepoRelDir) + }) + } +} diff --git a/server/events/project_finder.go b/server/events/project_finder.go index 661495faf..0321701b4 100644 --- a/server/events/project_finder.go +++ b/server/events/project_finder.go @@ -144,7 +144,7 @@ func (p *DefaultProjectFinder) DetermineProjects(log logging.SimpleLogging, modi if len(modifiedTerraformFiles) == 0 { return projects } - log.Info("filtered modified files to %d .tf or terragrunt.hcl files: %v", + log.Info("filtered modified files to %d file(s) in the autoplan file list: %v", len(modifiedTerraformFiles), modifiedTerraformFiles) var dirs []string diff --git a/server/events/working_dir.go b/server/events/working_dir.go index db016dce1..65fd28a30 100644 --- a/server/events/working_dir.go +++ b/server/events/working_dir.go @@ -32,8 +32,8 @@ const workingDirPrefix = "repos" var cloneLocks sync.Map -//go:generate pegomock generate --package mocks -o mocks/mock_working_dir.go WorkingDir -//go:generate pegomock generate --package events WorkingDir +//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package mocks -o mocks/mock_working_dir.go WorkingDir +//go:generate pegomock generate github.com/runatlantis/atlantis/server/events --package events WorkingDir // WorkingDir handles the workspace on disk for running commands. type WorkingDir interface { @@ -56,6 +56,8 @@ type WorkingDir interface { SetSafeToReClone() // DeletePlan deletes the plan for this repo, pull, workspace path and project name DeletePlan(r models.Repo, p models.PullRequest, workspace string, path string, projectName string) error + // GetGitUntrackedFiles returns a list of Git untracked files in the working dir. + GetGitUntrackedFiles(r models.Repo, p models.PullRequest, workspace string) ([]string, error) } // FileWorkspace implements WorkingDir with the file system. @@ -382,3 +384,24 @@ func (w *FileWorkspace) DeletePlan(r models.Repo, p models.PullRequest, workspac w.Logger.Info("Deleting plan: " + planPath) return os.Remove(planPath) } + +// getGitUntrackedFiles returns a list of Git untracked files in the working dir. +func (w *FileWorkspace) GetGitUntrackedFiles(r models.Repo, p models.PullRequest, workspace string) ([]string, error) { + workingDir, err := w.GetWorkingDir(r, p, workspace) + if err != nil { + return nil, err + } + + w.Logger.Debug("Checking for Git untracked files in directory: '%s'", workingDir) + cmd := exec.Command("git", "ls-files", "--others", "--exclude-standard") + cmd.Dir = workingDir + + output, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + + untrackedFiles := strings.Split(string(output), "\n")[:] + w.Logger.Debug("Untracked files: '%s'", strings.Join(untrackedFiles, ",")) + return untrackedFiles, nil +} diff --git a/server/server.go b/server/server.go index 380af5baa..c93ed170d 100644 --- a/server/server.go +++ b/server/server.go @@ -596,6 +596,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { userConfig.AutoplanFileList, userConfig.RestrictFileList, userConfig.SilenceNoProjects, + userConfig.IncludeGitUntrackedFiles, statsScope, logger, terraformClient, diff --git a/server/user_config.go b/server/user_config.go index 5552df6a2..7104b2df5 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -60,6 +60,7 @@ type UserConfig struct { GitlabToken string `mapstructure:"gitlab-token"` GitlabUser string `mapstructure:"gitlab-user"` GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` + IncludeGitUntrackedFiles bool `mapstructure:"include-git-untracked-files"` APISecret string `mapstructure:"api-secret"` HidePrevPlanComments bool `mapstructure:"hide-prev-plan-comments"` LockingDBType string `mapstructure:"locking-db-type"`