diff --git a/.node-version b/.node-version index 6569dfa4f3..f3f52b42d3 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.8.1 +20.9.0 diff --git a/cmd/server.go b/cmd/server.go index d9e37a7c62..a8ebab5174 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -54,6 +54,7 @@ const ( AllowForkPRsFlag = "allow-fork-prs" AllowRepoConfigFlag = "allow-repo-config" AtlantisURLFlag = "atlantis-url" + AutoDiscoverModeFlag = "autodiscover-mode" AutomergeFlag = "automerge" ParallelPlanFlag = "parallel-plan" ParallelApplyFlag = "parallel-apply" @@ -152,6 +153,7 @@ const ( DefaultADBasicUser = "" DefaultADBasicPassword = "" DefaultADHostname = "dev.azure.com" + DefaultAutoDiscoverMode = "auto" DefaultAutoplanFileList = "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl" DefaultAllowCommands = "version,plan,apply,unlock,approve_policies" DefaultCheckoutStrategy = CheckoutStrategyBranch @@ -211,6 +213,12 @@ var stringFlags = map[string]stringFlag{ AtlantisURLFlag: { description: "URL that Atlantis can be reached at. Defaults to http://$(hostname):$port where $port is from --" + PortFlag + ". Supports a base path ex. https://example.com/basepath.", }, + AutoDiscoverModeFlag: { + description: "Auto discover mode controls whether projects in a repo are discovered by Atlantis. Defaults to \"auto\" which " + + "means projects will be discovered when no explicit projects are defined in repo config. Also supports \"enabled\" (always " + + "discover projects) and \"disabled\" (never discover projects).", + defaultValue: DefaultAutoDiscoverMode, + }, AutoplanModulesFromProjects: { description: "Comma separated list of file patterns to select projects Atlantis will index for module dependencies." + " Indexed projects will automatically be planned if a module they depend on is modified." + @@ -884,6 +892,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) { if c.WebPassword == "" { c.WebPassword = DefaultWebPassword } + if c.AutoDiscoverModeFlag == "" { + c.AutoDiscoverModeFlag = DefaultAutoDiscoverMode + } } func (s *ServerCmd) validate(userConfig server.UserConfig) error { diff --git a/cmd/server_test.go b/cmd/server_test.go index 3ed524df77..c5312396b4 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -60,6 +60,7 @@ var testFlags = map[string]interface{}{ AllowCommandsFlag: "version,plan,unlock,import,approve_policies", // apply is disabled by DisableApply AllowForkPRsFlag: true, AllowRepoConfigFlag: true, + AutoDiscoverModeFlag: "auto", AutomergeFlag: true, AutoplanFileListFlag: "**/*.tf,**/*.yml", BitbucketBaseURLFlag: "https://bitbucket-base-url.com", diff --git a/go.mod b/go.mod index 0a7c2546f9..fce7e13ef8 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.0.0 github.com/google/go-github/v54 v54.0.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/google/uuid v1.3.1 + github.com/google/uuid v1.4.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/go-getter/v2 v2.2.1 @@ -43,7 +43,7 @@ require ( github.com/urfave/negroni/v3 v3.0.0 github.com/warrensbox/terraform-switcher v0.1.1-0.20221027055942-201c8e92e997 github.com/xanzy/go-gitlab v0.93.2 - go.etcd.io/bbolt v1.3.7 + go.etcd.io/bbolt v1.3.8 go.uber.org/zap v1.26.0 golang.org/x/term v0.13.0 golang.org/x/text v0.13.0 diff --git a/go.sum b/go.sum index beffc5bfcf..23de6a0fb4 100644 --- a/go.sum +++ b/go.sum @@ -228,8 +228,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -467,8 +467,8 @@ github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= -go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= -go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/runatlantis.io/docs/repo-level-atlantis-yaml.md b/runatlantis.io/docs/repo-level-atlantis-yaml.md index 5484f26e34..777552c634 100644 --- a/runatlantis.io/docs/repo-level-atlantis-yaml.md +++ b/runatlantis.io/docs/repo-level-atlantis-yaml.md @@ -34,12 +34,18 @@ By default, this is not allowed. ::: ::: warning -Once an `atlantis.yaml` file exists in a repo, Atlantis won't try to determine -where to run plan automatically. Instead it will just follow the project configuration. -This means that you'll need to define each project in your repo. +Once an `atlantis.yaml` file exists in a repo and one or more `projects` are configured, +Atlantis won't try to determine where to run plan automatically. Instead it will just +follow the project configuration. This means that you'll need to define each project +in your repo. If you have many directories with Terraform configuration, each directory will need to be defined. + +This behavior can be overriden by setting `autodiscover.mode` to +`enabled` in which case Atlantis will still try to discover projects which were not +explicitly configured. If the directory of any discovered project conflicts with a +manually configured project, the manually configured project will take precedence. ::: ## Example Using All Keys @@ -47,6 +53,8 @@ need to be defined. ```yaml version: 3 automerge: true +autodiscover: + mode: auto delete_source_branch_on_merge: true parallel_plan: true parallel_apply: true @@ -281,6 +289,34 @@ in each group one by one. If any plan/apply fails and `abort_on_execution_order_fail` is set to true on a repo level, all the following groups will be aborted. For this example, if project2 fails then project1 will not run. +### Autodiscovery Config +```yaml +autodiscover: + mode: "auto" +``` +The above is the default configuration for `autodiscover.mode`. When `autodiscover.mode` is auto, +projects will be discovered only if the repo has no `projects` configured. + +```yaml +autodiscover: + mode: "disabled" +``` +With the config above, Atlantis will never try to discover projects, even when there are no +`projects` configured. This is useful if dynamically generating Atlantis config in pre_workflow hooks. +See [Dynamic Repo Config Generation](pre-workflow-hooks.html#dynamic-repo-config-generation). + +```yaml +autodiscover: + mode: "enabled" +``` +With the config above, Atlantis will unconditionally try to discover projects based on modified_files, +even when the directory of the project is missing from the configured `projects` in the repo configuration. +If a discovered project has the same directory as a project which was manually configured in `projects`, +the manual configuration will take precedence. + +Use this feature when some projects require specific configuration in a repo with many projects yet +it's still desirable for Atlantis to plan/apply for projects not enumerated in the config. + ### Custom Backend Config See [Custom Workflow Use Cases: Custom Backend Config](custom-workflows.html#custom-backend-config) diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index f87ec83144..e0f890615f 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -106,6 +106,22 @@ Values are chosen in this order: * If a load balancer with a non http/https port (not the one defined in the `--port` flag) is used, update the URL to include the port like in the example above. * This URL is used as the `details` link next to each atlantis job to view the job's logs. +### `--autodiscover-mode` + ```bash + atlantis server --autodiscover-mode="" + # or + ATLANTIS_AUTODISCOVER_MODE="" + ``` + Sets auto discover mode, default is "auto". When set to "auto", projects in a repo will be discovered by + Atlantis when there are no projects configured in the repo config. If one or more projects are defined + in the repo config then auto discovery will be completely disabled. + + When set to "enabled" projects will be discovered unconditionally. If an auto discovered project is already + defined in the projects section of the repo config, the project from the repo config will take precedence over + the auto discovered project. + + When set to "disabled" projects will never be discovered, even if there are no projects configured in the repo config. + ### `--automerge` ```bash atlantis server --automerge diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 01d8a175aa..178d2afe56 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -88,6 +88,10 @@ repos: # policy_check defines if policy checking should be enable on this repository. policy_check: false + # autodiscover defines how atlantis should automatically discover projects in this repository. + autodiscover: + mode: auto + # id can also be an exact match. - id: github.com/myorg/specific-repo @@ -496,6 +500,7 @@ If you set a workflow with the key `default`, it will override this. | repo_locking | bool | false | no | Whether or not to get a lock. | | policy_check | bool | false | no | Whether or not to run policy checks on this repository. | | custom_policy_check | bool | false | no | Whether or not to enable custom policy check tools outside of Conftest on this repository. | +| autodiscover | AutoDiscover | none | no | Auto discover settings for this repo :::tip Notes diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 93c63df3bd..32348a1f0a 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1326,6 +1326,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers false, false, false, + "auto", statsScope, logger, terraformClient, diff --git a/server/core/config/parser_validator_test.go b/server/core/config/parser_validator_test.go index 53caf9e539..3b269695da 100644 --- a/server/core/config/parser_validator_test.go +++ b/server/core/config/parser_validator_test.go @@ -1329,6 +1329,22 @@ func TestParseGlobalCfg(t *testing.T) { import_requirements: [invalid]`, expErr: "repos: (0: (import_requirements: \"invalid\" is not a valid import_requirement, only \"approved\", \"mergeable\" and \"undiverged\" are supported.).).", }, + "disable autodiscover": { + input: `repos: +- id: /.*/ + autodiscover: + mode: disabled`, + exp: valid.GlobalCfg{ + Repos: []valid.Repo{ + defaultCfg.Repos[0], + { + IDRegex: regexp.MustCompile(".*"), + AutoDiscover: &valid.AutoDiscover{Mode: valid.AutoDiscoverDisabledMode}, + }, + }, + Workflows: defaultCfg.Workflows, + }, + }, "no workflows key": { input: `repos: []`, exp: defaultCfg, @@ -1404,6 +1420,8 @@ repos: allowed_overrides: [plan_requirements, apply_requirements, import_requirements, workflow, delete_source_branch_on_merge] allow_custom_workflows: true policy_check: true + autodiscover: + mode: enabled - id: /.*/ branch: /(master|main)/ pre_workflow_hooks: @@ -1411,6 +1429,8 @@ repos: post_workflow_hooks: - run: custom workflow command policy_check: false + autodiscover: + mode: disabled workflows: custom1: plan: @@ -1457,6 +1477,7 @@ policies: AllowedOverrides: []string{"plan_requirements", "apply_requirements", "import_requirements", "workflow", "delete_source_branch_on_merge"}, AllowCustomWorkflows: Bool(true), PolicyCheck: Bool(true), + AutoDiscover: &valid.AutoDiscover{Mode: valid.AutoDiscoverEnabledMode}, }, { IDRegex: regexp.MustCompile(".*"), @@ -1464,6 +1485,7 @@ policies: PreWorkflowHooks: preWorkflowHooks, PostWorkflowHooks: postWorkflowHooks, PolicyCheck: Bool(false), + AutoDiscover: &valid.AutoDiscover{Mode: valid.AutoDiscoverDisabledMode}, }, }, Workflows: map[string]valid.Workflow{ @@ -1574,6 +1596,7 @@ workflows: RepoLocking: Bool(true), PolicyCheck: Bool(false), CustomPolicyCheck: Bool(false), + AutoDiscover: raw.DefaultAutoDiscover(), }, }, Workflows: map[string]valid.Workflow{ @@ -1727,7 +1750,10 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { "allowed_workflows": ["custom"], "apply_requirements": ["mergeable", "approved"], "allowed_overrides": ["workflow", "apply_requirements"], - "allow_custom_workflows": true + "allow_custom_workflows": true, + "autodiscover": { + "mode": "enabled" + } }, { "id": "github.com/owner/repo" @@ -1792,6 +1818,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { AllowedWorkflows: []string{"custom"}, AllowedOverrides: []string{"workflow", "apply_requirements"}, AllowCustomWorkflows: Bool(true), + AutoDiscover: &valid.AutoDiscover{Mode: valid.AutoDiscoverEnabledMode}, }, { ID: "github.com/owner/repo", @@ -1799,6 +1826,7 @@ func TestParserValidator_ParseGlobalCfgJSON(t *testing.T) { ApplyRequirements: nil, AllowedOverrides: nil, AllowCustomWorkflows: nil, + AutoDiscover: nil, }, }, Workflows: map[string]valid.Workflow{ diff --git a/server/core/config/raw/autodiscover.go b/server/core/config/raw/autodiscover.go new file mode 100644 index 0000000000..156128d271 --- /dev/null +++ b/server/core/config/raw/autodiscover.go @@ -0,0 +1,38 @@ +package raw + +import ( + validation "github.com/go-ozzo/ozzo-validation" + "github.com/runatlantis/atlantis/server/core/config/valid" +) + +var DefaultAutoDiscoverMode = valid.AutoDiscoverAutoMode + +type AutoDiscover struct { + Mode *valid.AutoDiscoverMode `yaml:"mode,omitempty"` +} + +func (a AutoDiscover) ToValid() *valid.AutoDiscover { + var v valid.AutoDiscover + + if a.Mode != nil { + v.Mode = *a.Mode + } else { + v.Mode = DefaultAutoDiscoverMode + } + + return &v +} + +func (a AutoDiscover) Validate() error { + res := validation.ValidateStruct(&a, + // If a.Mode is nil, this should still pass validation. + validation.Field(&a.Mode, validation.In(valid.AutoDiscoverAutoMode, valid.AutoDiscoverDisabledMode, valid.AutoDiscoverEnabledMode)), + ) + return res +} + +func DefaultAutoDiscover() *valid.AutoDiscover { + return &valid.AutoDiscover{ + Mode: DefaultAutoDiscoverMode, + } +} diff --git a/server/core/config/raw/autodiscover_test.go b/server/core/config/raw/autodiscover_test.go new file mode 100644 index 0000000000..417257ad64 --- /dev/null +++ b/server/core/config/raw/autodiscover_test.go @@ -0,0 +1,131 @@ +package raw_test + +import ( + "testing" + + "github.com/runatlantis/atlantis/server/core/config/raw" + "github.com/runatlantis/atlantis/server/core/config/valid" + . "github.com/runatlantis/atlantis/testing" + yaml "gopkg.in/yaml.v2" +) + +func TestAutoDiscover_UnmarshalYAML(t *testing.T) { + auto_discover_enabled := valid.AutoDiscoverEnabledMode + cases := []struct { + description string + input string + exp raw.AutoDiscover + }{ + { + description: "omit unset fields", + input: "", + exp: raw.AutoDiscover{ + Mode: nil, + }, + }, + { + description: "all fields set", + input: ` +mode: enabled +`, + exp: raw.AutoDiscover{ + Mode: &auto_discover_enabled, + }, + }, + } + + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + var a raw.AutoDiscover + err := yaml.UnmarshalStrict([]byte(c.input), &a) + Ok(t, err) + Equals(t, c.exp, a) + }) + } +} + +func TestAutoDiscover_Validate(t *testing.T) { + auto_discover_auto := valid.AutoDiscoverAutoMode + auto_discover_enabled := valid.AutoDiscoverEnabledMode + auto_discover_disabled := valid.AutoDiscoverDisabledMode + random_string := valid.AutoDiscoverMode("random_string") + cases := []struct { + description string + input raw.AutoDiscover + errContains *string + }{ + { + description: "nothing set", + input: raw.AutoDiscover{}, + errContains: nil, + }, + { + description: "mode set to auto", + input: raw.AutoDiscover{ + Mode: &auto_discover_auto, + }, + errContains: nil, + }, + { + description: "mode set to disabled", + input: raw.AutoDiscover{ + Mode: &auto_discover_disabled, + }, + errContains: nil, + }, + { + description: "mode set to enabled", + input: raw.AutoDiscover{ + Mode: &auto_discover_enabled, + }, + errContains: nil, + }, + { + description: "mode set to random string", + input: raw.AutoDiscover{ + Mode: &random_string, + }, + errContains: String("valid value"), + }, + } + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + if c.errContains == nil { + Ok(t, c.input.Validate()) + } else { + ErrContains(t, *c.errContains, c.input.Validate()) + } + }) + } +} + +func TestAutoDiscover_ToValid(t *testing.T) { + auto_discover_enabled := valid.AutoDiscoverEnabledMode + cases := []struct { + description string + input raw.AutoDiscover + exp *valid.AutoDiscover + }{ + { + description: "nothing set", + input: raw.AutoDiscover{}, + exp: &valid.AutoDiscover{ + Mode: valid.AutoDiscoverAutoMode, + }, + }, + { + description: "value set", + input: raw.AutoDiscover{ + Mode: &auto_discover_enabled, + }, + exp: &valid.AutoDiscover{ + Mode: valid.AutoDiscoverEnabledMode, + }, + }, + } + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + Equals(t, c.exp, c.input.ToValid()) + }) + } +} diff --git a/server/core/config/raw/global_cfg.go b/server/core/config/raw/global_cfg.go index 92a9ec29b6..c14b590afb 100644 --- a/server/core/config/raw/global_cfg.go +++ b/server/core/config/raw/global_cfg.go @@ -36,6 +36,7 @@ type Repo struct { RepoLocking *bool `yaml:"repo_locking,omitempty" json:"repo_locking,omitempty"` PolicyCheck *bool `yaml:"policy_check,omitempty" json:"policy_check,omitempty"` CustomPolicyCheck *bool `yaml:"custom_policy_check,omitempty" json:"custom_policy_check,omitempty"` + AutoDiscover *AutoDiscover `yaml:"autodiscover,omitempty" json:"autodiscover,omitempty"` } func (g GlobalCfg) Validate() error { @@ -211,6 +212,14 @@ func (r Repo) Validate() error { return nil } + autoDiscoverValid := func(value interface{}) error { + var autoDiscover *AutoDiscover = value.(*AutoDiscover) + if autoDiscover != nil { + return autoDiscover.Validate() + } + return nil + } + return validation.ValidateStruct(&r, validation.Field(&r.ID, validation.Required, validation.By(idValid)), validation.Field(&r.Branch, validation.By(branchValid)), @@ -221,6 +230,7 @@ func (r Repo) Validate() error { validation.Field(&r.ImportRequirements, validation.By(validImportReq)), validation.Field(&r.Workflow, validation.By(workflowExists)), validation.Field(&r.DeleteSourceBranchOnMerge, validation.By(deleteSourceBranchOnMergeValid)), + validation.Field(&r.AutoDiscover, validation.By(autoDiscoverValid)), ) } @@ -281,7 +291,7 @@ OuterGlobalPlanReqs: } // dont add policy_check step if repo have it explicitly disabled - if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && *r.PolicyCheck == false { + if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck { continue } mergedPlanReqs = append(mergedPlanReqs, globalReq) @@ -295,7 +305,7 @@ OuterGlobalApplyReqs: } // dont add policy_check step if repo have it explicitly disabled - if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && *r.PolicyCheck == false { + if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck { continue } mergedApplyReqs = append(mergedApplyReqs, globalReq) @@ -309,12 +319,17 @@ OuterGlobalImportReqs: } // dont add policy_check step if repo have it explicitly disabled - if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && *r.PolicyCheck == false { + if globalReq == valid.PoliciesPassedCommandReq && r.PolicyCheck != nil && !*r.PolicyCheck { continue } mergedImportReqs = append(mergedImportReqs, globalReq) } + var autoDiscover *valid.AutoDiscover = nil + if r.AutoDiscover != nil { + autoDiscover = r.AutoDiscover.ToValid() + } + return valid.Repo{ ID: id, IDRegex: idRegex, @@ -333,5 +348,6 @@ OuterGlobalImportReqs: RepoLocking: r.RepoLocking, PolicyCheck: r.PolicyCheck, CustomPolicyCheck: r.CustomPolicyCheck, + AutoDiscover: autoDiscover, } } diff --git a/server/core/config/raw/repo_cfg.go b/server/core/config/raw/repo_cfg.go index eb511b04aa..d11f32d996 100644 --- a/server/core/config/raw/repo_cfg.go +++ b/server/core/config/raw/repo_cfg.go @@ -19,6 +19,7 @@ type RepoCfg struct { Projects []Project `yaml:"projects,omitempty"` Workflows map[string]Workflow `yaml:"workflows,omitempty"` PolicySets PolicySets `yaml:"policies,omitempty"` + AutoDiscover *AutoDiscover `yaml:"autodiscover,omitempty"` Automerge *bool `yaml:"automerge,omitempty"` ParallelApply *bool `yaml:"parallel_apply,omitempty"` ParallelPlan *bool `yaml:"parallel_plan,omitempty"` @@ -71,10 +72,16 @@ func (r RepoCfg) ToValid() valid.RepoCfg { abortOnExcecutionOrderFail = *r.AbortOnExcecutionOrderFail } + var autoDiscover *valid.AutoDiscover = nil + if r.AutoDiscover != nil { + autoDiscover = r.AutoDiscover.ToValid() + } + return valid.RepoCfg{ Version: *r.Version, Projects: validProjects, Workflows: validWorkflows, + AutoDiscover: autoDiscover, Automerge: automerge, ParallelApply: parallelApply, ParallelPlan: parallelPlan, diff --git a/server/core/config/raw/repo_cfg_test.go b/server/core/config/raw/repo_cfg_test.go index 7b11655c13..ac3fe53b0c 100644 --- a/server/core/config/raw/repo_cfg_test.go +++ b/server/core/config/raw/repo_cfg_test.go @@ -11,6 +11,7 @@ import ( ) func TestConfig_UnmarshalYAML(t *testing.T) { + auto_discover_enabled := valid.AutoDiscoverEnabledMode cases := []struct { description string input string @@ -126,6 +127,8 @@ func TestConfig_UnmarshalYAML(t *testing.T) { input: ` version: 3 automerge: true +autodiscover: + mode: enabled parallel_apply: true parallel_plan: false projects: @@ -150,6 +153,7 @@ allowed_regexp_prefixes: - staging/`, exp: raw.RepoCfg{ Version: Int(3), + AutoDiscover: &raw.AutoDiscover{Mode: &auto_discover_enabled}, Automerge: Bool(true), ParallelApply: Bool(true), ParallelPlan: Bool(false), @@ -232,6 +236,7 @@ func TestConfig_Validate(t *testing.T) { } func TestConfig_ToValid(t *testing.T) { + auto_discover_enabled := valid.AutoDiscoverEnabledMode cases := []struct { description string input raw.RepoCfg @@ -248,18 +253,20 @@ func TestConfig_ToValid(t *testing.T) { { description: "set to empty", input: raw.RepoCfg{ - Version: Int(2), - Workflows: map[string]raw.Workflow{}, - Projects: []raw.Project{}, + Version: Int(2), + AutoDiscover: &raw.AutoDiscover{}, + Workflows: map[string]raw.Workflow{}, + Projects: []raw.Project{}, }, exp: valid.RepoCfg{ - Version: 2, - Workflows: map[string]valid.Workflow{}, - Projects: nil, + Version: 2, + AutoDiscover: raw.DefaultAutoDiscover(), + Workflows: map[string]valid.Workflow{}, + Projects: nil, }, }, { - description: "automerge, parallel_apply and abort_on_execution_order_fail omitted", + description: "automerge, parallel_apply, abort_on_execution_order_fail omitted", input: raw.RepoCfg{ Version: Int(2), }, @@ -272,7 +279,7 @@ func TestConfig_ToValid(t *testing.T) { }, }, { - description: "automerge, parallel_apply and abort_on_execution_order_fail true", + description: "automerge, parallel_apply, abort_on_execution_order_fail true", input: raw.RepoCfg{ Version: Int(2), Automerge: Bool(true), @@ -288,7 +295,7 @@ func TestConfig_ToValid(t *testing.T) { }, }, { - description: "automerge, parallel_apply and abort_on_execution_order_fail false", + description: "automerge, parallel_apply, abort_on_execution_order_fail false", input: raw.RepoCfg{ Version: Int(2), Automerge: Bool(false), @@ -303,6 +310,30 @@ func TestConfig_ToValid(t *testing.T) { Workflows: map[string]valid.Workflow{}, }, }, + { + description: "autodiscover omitted", + input: raw.RepoCfg{ + Version: Int(2), + }, + exp: valid.RepoCfg{ + Version: 2, + Workflows: map[string]valid.Workflow{}, + }, + }, + { + description: "autodiscover included", + input: raw.RepoCfg{ + Version: Int(2), + AutoDiscover: &raw.AutoDiscover{Mode: &auto_discover_enabled}, + }, + exp: valid.RepoCfg{ + Version: 2, + AutoDiscover: &valid.AutoDiscover{ + Mode: valid.AutoDiscoverEnabledMode, + }, + Workflows: map[string]valid.Workflow{}, + }, + }, { description: "only plan stage set", input: raw.RepoCfg{ @@ -339,6 +370,9 @@ func TestConfig_ToValid(t *testing.T) { Version: Int(2), Automerge: Bool(true), ParallelApply: Bool(true), + AutoDiscover: &raw.AutoDiscover{ + Mode: &auto_discover_enabled, + }, Workflows: map[string]raw.Workflow{ "myworkflow": { Apply: &raw.Stage{ @@ -388,6 +422,9 @@ func TestConfig_ToValid(t *testing.T) { Version: 2, Automerge: Bool(true), ParallelApply: Bool(true), + AutoDiscover: &valid.AutoDiscover{ + Mode: valid.AutoDiscoverEnabledMode, + }, Workflows: map[string]valid.Workflow{ "myworkflow": { Name: "myworkflow", diff --git a/server/core/config/valid/autodiscover.go b/server/core/config/valid/autodiscover.go new file mode 100644 index 0000000000..c131c3bffe --- /dev/null +++ b/server/core/config/valid/autodiscover.go @@ -0,0 +1,14 @@ +package valid + +// AutoDiscoverMode enum +type AutoDiscoverMode string + +const ( + AutoDiscoverEnabledMode AutoDiscoverMode = "enabled" + AutoDiscoverDisabledMode AutoDiscoverMode = "disabled" + AutoDiscoverAutoMode AutoDiscoverMode = "auto" +) + +type AutoDiscover struct { + Mode AutoDiscoverMode +} diff --git a/server/core/config/valid/global_cfg.go b/server/core/config/valid/global_cfg.go index 8aab42f67b..96a3464c1d 100644 --- a/server/core/config/valid/global_cfg.go +++ b/server/core/config/valid/global_cfg.go @@ -28,6 +28,7 @@ const DeleteSourceBranchOnMergeKey = "delete_source_branch_on_merge" const RepoLockingKey = "repo_locking" const PolicyCheckKey = "policy_check" const CustomPolicyCheckKey = "custom_policy_check" +const AutoDiscoverKey = "autodiscover" // DefaultAtlantisFile is the default name of the config file for each repo. const DefaultAtlantisFile = "atlantis.yaml" @@ -84,6 +85,7 @@ type Repo struct { RepoLocking *bool PolicyCheck *bool CustomPolicyCheck *bool + AutoDiscover *AutoDiscover } type MergedProjectCfg struct { @@ -245,6 +247,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { deleteSourceBranchOnMerge := false repoLockingKey := true customPolicyCheck := false + autoDiscover := AutoDiscover{Mode: AutoDiscoverAutoMode} if args.AllowRepoCfg { allowedOverrides = []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, PolicyCheckKey} allowCustomWorkflows = true @@ -269,6 +272,7 @@ func NewGlobalCfgFromArgs(args GlobalCfgArgs) GlobalCfg { RepoLocking: &repoLockingKey, PolicyCheck: &policyCheck, CustomPolicyCheck: &customPolicyCheck, + AutoDiscover: &autoDiscover, }, }, Workflows: map[string]Workflow{ @@ -305,7 +309,7 @@ func (r Repo) IDString() string { // final config. It assumes that all configs have been validated. func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, proj Project, rCfg RepoCfg) MergedProjectCfg { log.Debug("MergeProjectCfg started") - planReqs, applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocking, policyCheck, customPolicyCheck := g.getMatchingCfg(log, repoID) + planReqs, applyReqs, importReqs, workflow, allowedOverrides, allowCustomWorkflows, deleteSourceBranchOnMerge, repoLocking, policyCheck, customPolicyCheck, _ := g.getMatchingCfg(log, repoID) // If repos are allowed to override certain keys then override them. for _, key := range allowedOverrides { @@ -407,7 +411,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro // repo with id repoID. It is used when there is no repo config. func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repoRelDir string, workspace string) MergedProjectCfg { log.Debug("building config based on server-side config") - planReqs, applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocking, policyCheck, customPolicyCheck := g.getMatchingCfg(log, repoID) + planReqs, applyReqs, importReqs, workflow, _, _, deleteSourceBranchOnMerge, repoLocking, policyCheck, customPolicyCheck, _ := g.getMatchingCfg(log, repoID) return MergedProjectCfg{ PlanRequirements: planReqs, ApplyRequirements: applyReqs, @@ -426,6 +430,14 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo } } +func (g GlobalCfg) RepoAutoDiscoverCfg(repoID string) *AutoDiscover { + repo := g.MatchingRepo(repoID) + if repo != nil { + return repo.AutoDiscover + } + return nil +} + // ValidateRepoCfg validates that rCfg for repo with id repoID is valid based // on our global config. func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { @@ -528,7 +540,7 @@ func (g GlobalCfg) ValidateRepoCfg(rCfg RepoCfg, repoID string) error { } // getMatchingCfg returns the key settings for repoID. -func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (planReqs []string, applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocking bool, policyCheck bool, customPolicyCheck bool) { +func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (planReqs []string, applyReqs []string, importReqs []string, workflow Workflow, allowedOverrides []string, allowCustomWorkflows bool, deleteSourceBranchOnMerge bool, repoLocking bool, policyCheck bool, customPolicyCheck bool, autoDiscover AutoDiscover) { toLog := make(map[string]string) traceF := func(repoIdx int, repoID string, key string, val interface{}) string { from := "default server config" @@ -550,6 +562,9 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla return fmt.Sprintf("setting %s: %s from %s", key, valStr, from) } + // Can't use raw.DefaultAutoDiscoverMode() because of an import cycle. Should refactor to avoid that. + autoDiscover = AutoDiscover{Mode: AutoDiscoverAutoMode} + for _, key := range []string{PlanRequirementsKey, ApplyRequirementsKey, ImportRequirementsKey, WorkflowKey, AllowedOverridesKey, AllowCustomWorkflowsKey, DeleteSourceBranchOnMergeKey, RepoLockingKey, PolicyCheckKey, CustomPolicyCheckKey} { for i, repo := range g.Repos { if repo.IDMatches(repoID) { @@ -604,6 +619,11 @@ func (g GlobalCfg) getMatchingCfg(log logging.SimpleLogging, repoID string) (pla toLog[CustomPolicyCheckKey] = traceF(i, repo.IDString(), CustomPolicyCheckKey, *repo.CustomPolicyCheck) customPolicyCheck = *repo.CustomPolicyCheck } + case AutoDiscoverKey: + if repo.AutoDiscover != nil { + toLog[AutoDiscoverKey] = traceF(i, repo.IDString(), AutoDiscoverKey, repo.AutoDiscover.Mode) + autoDiscover = *repo.AutoDiscover + } } } } diff --git a/server/core/config/valid/global_cfg_test.go b/server/core/config/valid/global_cfg_test.go index d778a9f12d..f60c07caec 100644 --- a/server/core/config/valid/global_cfg_test.go +++ b/server/core/config/valid/global_cfg_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/go-version" "github.com/mohae/deepcopy" "github.com/runatlantis/atlantis/server/core/config" + "github.com/runatlantis/atlantis/server/core/config/raw" "github.com/runatlantis/atlantis/server/core/config/valid" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" @@ -82,6 +83,7 @@ func TestNewGlobalCfg(t *testing.T) { RepoLocking: Bool(true), PolicyCheck: Bool(false), CustomPolicyCheck: Bool(false), + AutoDiscover: raw.DefaultAutoDiscover(), }, }, Workflows: map[string]valid.Workflow{ @@ -677,6 +679,7 @@ policies: - name: good-policy source: local path: rel/path/to/source + `, repoID: "github.com/owner/repo", proj: valid.Project{ diff --git a/server/core/config/valid/repo_cfg.go b/server/core/config/valid/repo_cfg.go index fe441f4d05..0102f30d00 100644 --- a/server/core/config/valid/repo_cfg.go +++ b/server/core/config/valid/repo_cfg.go @@ -19,6 +19,7 @@ type RepoCfg struct { Workflows map[string]Workflow PolicySets PolicySets Automerge *bool + AutoDiscover *AutoDiscover ParallelApply *bool ParallelPlan *bool ParallelPolicyCheck *bool @@ -91,6 +92,20 @@ func isRegexAllowed(name string, allowedRegexpPrefixes []string) bool { return false } +func (r RepoCfg) AutoDiscoverEnabled(defaultAutoDiscoverMode AutoDiscoverMode) bool { + autoDiscoverMode := defaultAutoDiscoverMode + if r.AutoDiscover != nil { + autoDiscoverMode = r.AutoDiscover.Mode + } + + if autoDiscoverMode == AutoDiscoverAutoMode { + // Autodiscover is enabled by default when no projects are defined + return len(r.Projects) == 0 + } + + return autoDiscoverMode == AutoDiscoverEnabledMode +} + // validateWorkspaceAllowed returns an error if repoCfg defines projects in // repoRelDir but none of them use workspace. We want this to be an error // because if users have gone to the trouble of defining projects in repoRelDir diff --git a/server/core/config/valid/valid.go b/server/core/config/valid/valid.go index 7fa35827a0..c638451f19 100644 --- a/server/core/config/valid/valid.go +++ b/server/core/config/valid/valid.go @@ -3,3 +3,4 @@ package valid const DefaultAutoPlanEnabled = true +const DefaultAutoDiscoverEnabled = true diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 2347c9072a..4a1427ee5e 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -18,6 +18,7 @@ import ( "github.com/runatlantis/atlantis/server/core/config" "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" ) @@ -28,6 +29,14 @@ const ( // DefaultWorkspace is the default Terraform workspace we run commands in. // This is also Terraform's default workspace. DefaultWorkspace = "default" + // DefaultAutomergeEnabled is the default for the automerge setting. + DefaultAutomergeEnabled = false + // DefaultAutoDiscoverEnabled is the default for the auto discover setting. + DefaultAutoDiscoverEnabled = true + // DefaultParallelApplyEnabled is the default for the parallel apply setting. + DefaultParallelApplyEnabled = false + // DefaultParallelPlanEnabled is the default for the parallel plan setting. + DefaultParallelPlanEnabled = false // DefaultDeleteSourceBranchOnMerge being false is the default setting whether or not to remove a source branch on merge DefaultDeleteSourceBranchOnMerge = false // DefaultAbortOnExcecutionOrderFail being false is the default setting for abort on execution group failiures @@ -54,6 +63,7 @@ func NewInstrumentedProjectCommandBuilder( RestrictFileList bool, SilenceNoProjects bool, IncludeGitUntrackedFiles bool, + AutoDiscoverMode string, scope tally.Scope, logger logging.SimpleLogging, terraformClient terraform.Client, @@ -85,6 +95,7 @@ func NewInstrumentedProjectCommandBuilder( RestrictFileList, SilenceNoProjects, IncludeGitUntrackedFiles, + AutoDiscoverMode, scope, logger, terraformClient, @@ -114,6 +125,7 @@ func NewProjectCommandBuilder( RestrictFileList bool, SilenceNoProjects bool, IncludeGitUntrackedFiles bool, + AutoDiscoverMode string, scope tally.Scope, logger logging.SimpleLogging, terraformClient terraform.Client, @@ -136,6 +148,7 @@ func NewProjectCommandBuilder( RestrictFileList: RestrictFileList, SilenceNoProjects: SilenceNoProjects, IncludeGitUntrackedFiles: IncludeGitUntrackedFiles, + AutoDiscoverMode: AutoDiscoverMode, ProjectCommandContextBuilder: NewProjectCommandContextBuilder( policyChecksSupported, commentBuilder, @@ -242,6 +255,8 @@ type DefaultProjectCommandBuilder struct { SilenceNoProjects bool // User config option: Include git untracked files in the modified file list. IncludeGitUntrackedFiles bool + // User config option: Controls auto-discovery of projects in a repository. + AutoDiscoverMode string // Handles the actual running of Terraform commands. TerraformExecutor terraform.Client } @@ -336,6 +351,13 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex ctx.Log.Debug("%d files were modified in this pull request. Modified files: %v", len(modifiedFiles), modifiedFiles) + // Get default AutoDiscoverMode from userConfig/globalConfig + defaultAutoDiscoverMode := valid.AutoDiscoverMode(p.AutoDiscoverMode) + globalAutoDiscover := p.GlobalCfg.RepoAutoDiscoverCfg(ctx.Pull.BaseRepo.ID()) + if globalAutoDiscover != nil { + defaultAutoDiscoverMode = globalAutoDiscover.Mode + } + if p.SkipCloneNoChanges && p.VCSClient.SupportsSingleFileDownload(ctx.Pull.BaseRepo) { repoCfgFile := p.GlobalCfg.RepoConfigFile(ctx.Pull.BaseRepo.ID()) hasRepoCfg, repoCfgData, err := p.VCSClient.GetFileContent(ctx.Pull, repoCfgFile) @@ -349,18 +371,27 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex return nil, errors.Wrapf(err, "parsing %s", repoCfgFile) } ctx.Log.Info("successfully parsed remote %s file", repoCfgFile) - if len(repoCfg.Projects) > 0 { - matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, "", nil) - if err != nil { - return nil, err - } - ctx.Log.Info("%d projects are changed on MR %q based on their when_modified config", len(matchingProjects), ctx.Pull.Num) - if len(matchingProjects) == 0 { - ctx.Log.Info("skipping repo clone since no project was modified") - return []command.ProjectContext{}, nil + + if repoCfg.AutoDiscover != nil { + defaultAutoDiscoverMode = repoCfg.AutoDiscover.Mode + } + // If auto_discovery is enabled, we never want to skip cloning + if !repoCfg.AutoDiscoverEnabled(defaultAutoDiscoverMode) { + if len(repoCfg.Projects) > 0 { + matchingProjects, err := p.ProjectFinder.DetermineProjectsViaConfig(ctx.Log, modifiedFiles, repoCfg, "", nil) + if err != nil { + return nil, err + } + ctx.Log.Info("%d projects are changed on MR %q based on their when_modified config", len(matchingProjects), ctx.Pull.Num) + if len(matchingProjects) == 0 { + ctx.Log.Info("skipping repo clone since no project was modified") + return []command.ProjectContext{}, nil + } + } else { + ctx.Log.Info("no projects are defined in %s. Will resume automatic detection", repoCfgFile) } } else { - ctx.Log.Info("No projects are defined in %s. Will resume automatic detection", repoCfgFile) + ctx.Log.Info("automatic project discovery enabled. Will resume automatic detection") } // NOTE: We discard this work here and end up doing it again after // cloning to ensure all the return values are set properly with @@ -402,6 +433,10 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex return nil, errors.Wrapf(err, "parsing %s", repoCfgFile) } ctx.Log.Info("successfully parsed %s file", repoCfgFile) + // This above condition to set this may not have been reached + if repoCfg.AutoDiscover != nil { + defaultAutoDiscoverMode = repoCfg.AutoDiscover.Mode + } } moduleInfo, err := FindModuleProjects(repoDir, p.AutoDetectModuleFiles) @@ -450,21 +485,43 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex parallelApply, parallelPlan, verbose, - repoCfg.AbortOnExcecutionOrderFail, + abortOnExcecutionOrderFail, p.TerraformExecutor, )...) } - } else { + } + + if repoCfg.AutoDiscoverEnabled(defaultAutoDiscoverMode) { // If there is no config file or it specified no projects, then we'll plan each project that // our algorithm determines was modified. if hasRepoCfg { - ctx.Log.Info("No projects are defined in %s. Will resume automatic detection", repoCfgFile) + if len(repoCfg.Projects) == 0 { + ctx.Log.Info("no projects are defined in %s. Will resume automatic detection", repoCfgFile) + } else { + ctx.Log.Info("automatic project discovery enabled. Will resume automatic detection") + } } else { ctx.Log.Info("found no %s file", repoCfgFile) } // build a module index for projects that are explicitly included - modifiedProjects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.Pull.BaseRepo.FullName, repoDir, p.AutoplanFileList, moduleInfo) - ctx.Log.Info("automatically determined that there were %d projects modified in this pull request: %s", len(modifiedProjects), modifiedProjects) + allModifiedProjects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.Pull.BaseRepo.FullName, repoDir, p.AutoplanFileList, moduleInfo) + // If a project is already manually configured with the same dir as a discovered project, the manually configured project should take precedence + modifiedProjects := make([]models.Project, 0) + configuredProjDirs := make(map[string]bool) + // We compare against all configured projects instead of projects which match the modified files in case a + // project is being specifically excluded (ex: when_modified doesn't match). We don't want to accidentally + // "discover" it again. + for _, configProj := range repoCfg.Projects { + // Clean the path to make sure ./rel_path is equivalent to rel_path, etc + configuredProjDirs[filepath.Clean(configProj.Dir)] = true + } + for _, mp := range allModifiedProjects { + _, dirExists := configuredProjDirs[filepath.Clean(mp.Path)] + if !dirExists { + modifiedProjects = append(modifiedProjects, mp) + } + } + ctx.Log.Info("automatically determined that there were %d additional projects modified in this pull request: %s", len(modifiedProjects), modifiedProjects) for _, mp := range modifiedProjects { ctx.Log.Debug("determining config for project at dir: %q", mp.Path) pWorkspace, err := p.ProjectFinder.DetermineWorkspaceFromHCL(ctx.Log, repoDir) @@ -772,6 +829,7 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *command.Conte } var projCtxs []command.ProjectContext var projCfg valid.MergedProjectCfg + automerge := p.EnableAutoMerge parallelApply := p.EnableParallelApply parallelPlan := p.EnableParallelPlan diff --git a/server/events/project_command_builder_internal_test.go b/server/events/project_command_builder_internal_test.go index e8804c5142..bd524dea90 100644 --- a/server/events/project_command_builder_internal_test.go +++ b/server/events/project_command_builder_internal_test.go @@ -673,6 +673,7 @@ projects: false, false, false, + "auto", statsScope, logger, terraformClient, @@ -888,6 +889,7 @@ projects: false, false, false, + "auto", statsScope, logger, terraformClient, @@ -1137,6 +1139,7 @@ workflows: false, false, false, + "auto", statsScope, logger, terraformClient, @@ -1293,6 +1296,7 @@ projects: false, true, false, + "auto", statsScope, logger, terraformClient, @@ -1318,6 +1322,149 @@ projects: } } +func TestBuildProjectCmdCtx_AutoDiscoverRespectsRepoConfig(t *testing.T) { + logger := logging.NewNoopLogger(t) + cases := map[string]struct { + globalCfg string + repoCfg string + modifiedFiles []string + expLen int + }{ + "autodiscover disabled": { + globalCfg: ` +repos: +- id: /.*/ + autodiscover: + mode: disabled +`, + repoCfg: ` +version: 3 +automerge: true +`, + modifiedFiles: []string{"project1/main.tf", "project2/main.tf", "project3/main.tf"}, + expLen: 0, + }, + "autodiscover auto": { + globalCfg: ` +repos: +- id: /.*/ + autodiscover: + mode: auto +`, + repoCfg: ` +version: 3 +automerge: true +projects: +- dir: project1 + workspace: myworkspace +`, + modifiedFiles: []string{"project1/main.tf", "project2/main.tf", "project3/main.tf"}, + expLen: 1, + }, + "autodiscover enabled": { + globalCfg: ` +repos: +- id: /.*/ + autodiscover: + mode: enabled +`, + repoCfg: ` +version: 3 +automerge: true +projects: +- dir: project1 + workspace: myworkspace +`, + modifiedFiles: []string{"project1/main.tf", "project2/main.tf", "project3/main.tf"}, + expLen: 3, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + tmp := DirStructure(t, map[string]interface{}{ + "project1": map[string]interface{}{ + "main.tf": nil, + }, + "project2": map[string]interface{}{ + "main.tf": nil, + }, + "project3": map[string]interface{}{ + "main.tf": nil, + }, + }) + + workingDir := NewMockWorkingDir() + When(workingDir.Clone(Any[models.Repo](), Any[models.PullRequest](), Any[string]())).ThenReturn(tmp, false, nil) + vcsClient := vcsmocks.NewMockClient() + When(vcsClient.GetModifiedFiles(Any[models.Repo](), Any[models.PullRequest]())).ThenReturn(c.modifiedFiles, nil) + + // Write and parse the global config file. + globalCfgPath := filepath.Join(tmp, "global.yaml") + Ok(t, os.WriteFile(globalCfgPath, []byte(c.globalCfg), 0600)) + parser := &config.ParserValidator{} + globalCfgArgs := valid.GlobalCfgArgs{ + AllowRepoCfg: false, + MergeableReq: false, + ApprovedReq: false, + UnDivergedReq: false, + } + + globalCfg, err := parser.ParseGlobalCfg(globalCfgPath, valid.NewGlobalCfgFromArgs(globalCfgArgs)) + Ok(t, err) + + if c.repoCfg != "" { + Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600)) + } + statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis") + + terraformClient := mocks.NewMockClient() + + builder := NewProjectCommandBuilder( + false, + parser, + &DefaultProjectFinder{}, + vcsClient, + workingDir, + NewDefaultWorkingDirLocker(), + globalCfg, + &DefaultPendingPlanFinder{}, + &CommentParser{ExecutableName: "atlantis"}, + false, + false, + false, + false, + false, + "", + "**/*.tf,**/*.tfvars,**/*.tfvars.json,**/terragrunt.hcl,**/.terraform.lock.hcl", + false, + true, + false, + "auto", + statsScope, + logger, + terraformClient, + ) + + ctxs, err := builder.BuildPlanCommands( + &command.Context{ + Log: logger, + Scope: statsScope, + }, + &CommentCommand{ + RepoRelDir: "", + Flags: nil, + Name: command.Plan, + Verbose: false, + }, + ) + Equals(t, c.expLen, len(ctxs)) + Ok(t, err) + + }) + } +} + func mustVersion(v string) *version.Version { vers, err := version.NewVersion(v) if err != nil { diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index 6a3e73798d..a702dd3d00 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -33,6 +33,7 @@ var defaultUserConfig = struct { RestrictFileList bool SilenceNoProjects bool IncludeGitUntrackedFiles bool + AutoDiscoverMode string }{ SkipCloneNoChanges: false, EnableRegExpCmd: false, @@ -44,6 +45,7 @@ var defaultUserConfig = struct { RestrictFileList: false, SilenceNoProjects: false, IncludeGitUntrackedFiles: true, + AutoDiscoverMode: "auto", } func TestDefaultProjectCommandBuilder_BuildAutoplanCommands(t *testing.T) { @@ -194,6 +196,7 @@ projects: userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -232,6 +235,7 @@ func TestDefaultProjectCommandBuilder_BuildSinglePlanApplyCommand(t *testing.T) ExpErr string ExpApplyReqs []string EnableAutoMergeUserCfg bool + AutoDiscoverModeUserCfg string EnableParallelPlanUserCfg bool EnableParallelApplyUserCfg bool ExpAutoMerge bool @@ -550,6 +554,7 @@ projects: userConfig.RestrictFileList, c.Silenced, userConfig.IncludeGitUntrackedFiles, + c.AutoDiscoverModeUserCfg, scope, logger, terraformClient, @@ -739,6 +744,7 @@ projects: userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -770,6 +776,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands(t *testing.T) { RepoRelDir string Workspace string Automerge bool + AutoDiscover valid.AutoDiscover ExpParallelPlan bool ExpParallelApply bool } @@ -956,6 +963,66 @@ projects: }, }, }, + "follow autodiscover enabled config": { + DirStructure: map[string]interface{}{ + "project1": map[string]interface{}{ + "main.tf": nil, + }, + "project2": map[string]interface{}{ + "main.tf": nil, + }, + "project3": map[string]interface{}{ + "main.tf": nil, + }, + }, + AtlantisYAML: `version: 3 +autodiscover: + mode: enabled +projects: +- name: project1-custom-name + dir: project1`, + ModifiedFiles: []string{"project1/main.tf", "project2/main.tf"}, + Exp: []expCtxFields{ + { + ProjectName: "project1-custom-name", + RepoRelDir: "project1", + Workspace: "default", + }, + { + ProjectName: "", + RepoRelDir: "project2", + Workspace: "default", + }, + }, + }, + "autodiscover enabled but project excluded by empty when_modified": { + DirStructure: map[string]interface{}{ + "project1": map[string]interface{}{ + "main.tf": nil, + }, + "project2": map[string]interface{}{ + "main.tf": nil, + }, + "project3": map[string]interface{}{ + "main.tf": nil, + }, + }, + AtlantisYAML: `version: 3 +autodiscover: + mode: enabled +projects: +- dir: project1 + autoplan: + when_modified: []`, + ModifiedFiles: []string{"project1/main.tf", "project2/main.tf"}, + Exp: []expCtxFields{ + { + ProjectName: "", + RepoRelDir: "project2", + Workspace: "default", + }, + }, + }, } logger := logging.NewNoopLogger(t) @@ -1007,6 +1074,7 @@ projects: userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1021,7 +1089,7 @@ projects: RepoRelDir: "", Flags: nil, Name: command.Plan, - Verbose: false, + Verbose: true, Workspace: "", ProjectName: "", }) @@ -1111,6 +1179,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1206,6 +1275,7 @@ projects: userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1296,6 +1366,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) { userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1467,6 +1538,7 @@ projects: userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1523,6 +1595,17 @@ parallel_plan: true`, ExpectedClones: Once(), ModifiedFiles: []string{"README.md"}, }, + { + AtlantisYAML: ` +version: 3 +autodiscover: + mode: enabled +projects: +- dir: dir1`, + ExpectedCtxs: 0, + ExpectedClones: Once(), + ModifiedFiles: []string{"dir2/main.tf"}, + }, } userConfig := defaultUserConfig @@ -1568,6 +1651,7 @@ parallel_plan: true`, userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1638,6 +1722,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1730,6 +1815,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) { userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1861,6 +1947,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_Single_With_RestrictFile userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, @@ -1972,6 +2059,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_with_IncludeGitUntracked userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverMode, scope, logger, terraformClient, diff --git a/server/server.go b/server/server.go index 6f80cb420f..4245fa1f36 100644 --- a/server/server.go +++ b/server/server.go @@ -597,6 +597,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { userConfig.RestrictFileList, userConfig.SilenceNoProjects, userConfig.IncludeGitUntrackedFiles, + userConfig.AutoDiscoverModeFlag, statsScope, logger, terraformClient, diff --git a/server/user_config.go b/server/user_config.go index 49e3ed6fba..d0e007edf7 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -15,6 +15,7 @@ type UserConfig struct { AllowRepoConfig bool `mapstructure:"allow-repo-config"` AllowCommands string `mapstructure:"allow-commands"` AtlantisURL string `mapstructure:"atlantis-url"` + AutoDiscoverModeFlag string `mapstructure:"autodiscover-mode"` Automerge bool `mapstructure:"automerge"` AutoplanFileList string `mapstructure:"autoplan-file-list"` AutoplanModules bool `mapstructure:"autoplan-modules"`