feat: add autodiscover enabled feature (#3895)

* add flag to allow the user disable the autodiscover

* add global config and doc

* feat: Implement autodiscover.mode

* fix: Minor doc fixes

* fix: Small fixes to docs/indent/tests

* fix: Line length, quoting, function comments

* fix: Add a few more tests and remove newlines

* fix: Always camel case never snake

---------

Co-authored-by: Marcelo Medeiros <m.medeiros@carepay.com>
Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com>
Co-authored-by: PePe Amengual <jose.amengual@gmail.com>
This commit is contained in:
jskrill
2023-11-29 18:43:53 -05:00
committed by GitHub
parent 6ff0e2f90e
commit 00aae565b2
22 changed files with 822 additions and 35 deletions

View File

@@ -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"
@@ -148,6 +149,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
@@ -207,6 +209,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." +
@@ -871,6 +879,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 {

View File

@@ -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",

View File

@@ -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)

View File

@@ -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="<auto|enabled|disabled>"
# or
ATLANTIS_AUTODISCOVER_MODE="<auto|enabled|disabled>"
```
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

View File

@@ -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

View File

@@ -1352,6 +1352,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
false,
false,
false,
"auto",
statsScope,
logger,
terraformClient,

View File

@@ -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{

View File

@@ -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,
}
}

View File

@@ -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) {
autoDiscoverEnabled := 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: &autoDiscoverEnabled,
},
},
}
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) {
autoDiscoverAuto := valid.AutoDiscoverAutoMode
autoDiscoverEnabled := valid.AutoDiscoverEnabledMode
autoDiscoverDisabled := valid.AutoDiscoverDisabledMode
randomString := 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: &autoDiscoverAuto,
},
errContains: nil,
},
{
description: "mode set to disabled",
input: raw.AutoDiscover{
Mode: &autoDiscoverDisabled,
},
errContains: nil,
},
{
description: "mode set to enabled",
input: raw.AutoDiscover{
Mode: &autoDiscoverEnabled,
},
errContains: nil,
},
{
description: "mode set to random string",
input: raw.AutoDiscover{
Mode: &randomString,
},
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) {
autoDiscoverEnabled := 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: &autoDiscoverEnabled,
},
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())
})
}
}

View File

@@ -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 {
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
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,
}
}

View File

@@ -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
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,

View File

@@ -11,6 +11,7 @@ import (
)
func TestConfig_UnmarshalYAML(t *testing.T) {
autoDiscoverEnabled := 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: &autoDiscoverEnabled},
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) {
autoDiscoverEnabled := 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: &autoDiscoverEnabled},
},
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: &autoDiscoverEnabled,
},
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",

View File

@@ -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
}

View File

@@ -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,17 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo
}
}
// RepoAutoDiscoverCfg returns the AutoDiscover config from the global config
// for the repo with id repoID. If no matching repo is found or there is no
// AutoDiscover config then this function returns nil.
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 +543,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 +565,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 +622,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
}
}
}
}

View File

@@ -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{

View File

@@ -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,24 @@ func isRegexAllowed(name string, allowedRegexpPrefixes []string) bool {
return false
}
// This function returns a final true/false decision for whether AutoDiscover is enabled
// for a repo. It takes into account the defaultAutoDiscoverMode when there is no explicit
// repo config. The defaultAutoDiscoverMode param should be understood as the default
// AutoDiscover mode as may be set via CLI params or server side repo config.
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

View File

@@ -216,3 +216,110 @@ func TestConfig_FindProjectsByDir(t *testing.T) {
})
}
}
func TestConfig_AutoDiscoverEnabled(t *testing.T) {
cases := []struct {
description string
repoAutoDiscover valid.AutoDiscoverMode
defaultAutoDiscover valid.AutoDiscoverMode
projects []valid.Project
expEnabled bool
}{
{
description: "repo disabled autodiscover default enabled",
repoAutoDiscover: valid.AutoDiscoverDisabledMode,
defaultAutoDiscover: valid.AutoDiscoverEnabledMode,
expEnabled: false,
},
{
description: "repo disabled autodiscover default disabled",
repoAutoDiscover: valid.AutoDiscoverDisabledMode,
defaultAutoDiscover: valid.AutoDiscoverDisabledMode,
expEnabled: false,
},
{
description: "repo enabled autodiscover default enabled",
repoAutoDiscover: valid.AutoDiscoverEnabledMode,
defaultAutoDiscover: valid.AutoDiscoverEnabledMode,
expEnabled: true,
},
{
description: "repo enabled autodiscover default disabled",
repoAutoDiscover: valid.AutoDiscoverEnabledMode,
defaultAutoDiscover: valid.AutoDiscoverDisabledMode,
expEnabled: true,
},
{
description: "repo set auto autodiscover with no projects default enabled",
repoAutoDiscover: valid.AutoDiscoverAutoMode,
defaultAutoDiscover: valid.AutoDiscoverEnabledMode,
expEnabled: true,
},
{
description: "repo set auto autodiscover with no projects default disabled",
repoAutoDiscover: valid.AutoDiscoverAutoMode,
defaultAutoDiscover: valid.AutoDiscoverDisabledMode,
expEnabled: true,
},
{
description: "repo set auto autodiscover with a project default enabled",
repoAutoDiscover: valid.AutoDiscoverAutoMode,
defaultAutoDiscover: valid.AutoDiscoverEnabledMode,
projects: []valid.Project{{}},
expEnabled: false,
},
{
description: "repo set auto autodiscover with a project default disabled",
repoAutoDiscover: valid.AutoDiscoverAutoMode,
defaultAutoDiscover: valid.AutoDiscoverDisabledMode,
projects: []valid.Project{{}},
expEnabled: false,
},
{
description: "repo unset autodiscover with no projects default enabled",
defaultAutoDiscover: valid.AutoDiscoverEnabledMode,
expEnabled: true,
},
{
description: "repo unset autodiscover with no projects default disabled",
defaultAutoDiscover: valid.AutoDiscoverDisabledMode,
expEnabled: false,
},
{
description: "repo unset autodiscover with no projects default auto",
defaultAutoDiscover: valid.AutoDiscoverAutoMode,
expEnabled: true,
},
{
description: "repo unset autodiscover with a project default enabled",
projects: []valid.Project{{}},
defaultAutoDiscover: valid.AutoDiscoverEnabledMode,
expEnabled: true,
},
{
description: "repo unset autodiscover with a project default disabled",
projects: []valid.Project{{}},
defaultAutoDiscover: valid.AutoDiscoverDisabledMode,
expEnabled: false,
},
{
description: "repo unset autodiscover with a project default auto",
projects: []valid.Project{{}},
defaultAutoDiscover: valid.AutoDiscoverAutoMode,
expEnabled: false,
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
r := valid.RepoCfg{
Projects: c.projects,
AutoDiscover: nil,
}
if c.repoAutoDiscover != "" {
r.AutoDiscover = &valid.AutoDiscover{c.repoAutoDiscover}
}
enabled := r.AutoDiscoverEnabled(c.defaultAutoDiscover)
Equals(t, c.expEnabled, enabled)
})
}
}

View File

@@ -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"
)
@@ -54,6 +55,7 @@ func NewInstrumentedProjectCommandBuilder(
RestrictFileList bool,
SilenceNoProjects bool,
IncludeGitUntrackedFiles bool,
AutoDiscoverMode string,
scope tally.Scope,
logger logging.SimpleLogging,
terraformClient terraform.Client,
@@ -85,6 +87,7 @@ func NewInstrumentedProjectCommandBuilder(
RestrictFileList,
SilenceNoProjects,
IncludeGitUntrackedFiles,
AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -114,6 +117,7 @@ func NewProjectCommandBuilder(
RestrictFileList bool,
SilenceNoProjects bool,
IncludeGitUntrackedFiles bool,
AutoDiscoverMode string,
scope tally.Scope,
logger logging.SimpleLogging,
terraformClient terraform.Client,
@@ -136,6 +140,7 @@ func NewProjectCommandBuilder(
RestrictFileList: RestrictFileList,
SilenceNoProjects: SilenceNoProjects,
IncludeGitUntrackedFiles: IncludeGitUntrackedFiles,
AutoDiscoverMode: AutoDiscoverMode,
ProjectCommandContextBuilder: NewProjectCommandContextBuilder(
policyChecksSupported,
commentBuilder,
@@ -242,6 +247,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 +343,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 +363,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 discover 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 +425,15 @@ func (p *DefaultProjectCommandBuilder) buildAllCommandsByCfg(ctx *command.Contex
return nil, errors.Wrapf(err, "parsing %s", repoCfgFile)
}
ctx.Log.Info("successfully parsed %s file", repoCfgFile)
// It's possible we've already set defaultAutoDiscoverMode
// from the config file while checking whether we can skip
// cloning. We still need to set it here in the case that
// we were not able to check whether we can skip cloning
// and thus were not able to previously fetch the repo
// config.
if repoCfg.AutoDiscover != nil {
defaultAutoDiscoverMode = repoCfg.AutoDiscover.Mode
}
}
moduleInfo, err := FindModuleProjects(repoDir, p.AutoDetectModuleFiles)
@@ -450,21 +482,46 @@ 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)

View File

@@ -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 {

View File

@@ -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,67 @@ 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"},
// project2 is autodiscovered, whereas project1 is not
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 +1075,7 @@ projects:
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1021,7 +1090,7 @@ projects:
RepoRelDir: "",
Flags: nil,
Name: command.Plan,
Verbose: false,
Verbose: true,
Workspace: "",
ProjectName: "",
})
@@ -1111,6 +1180,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1206,6 +1276,7 @@ projects:
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1296,6 +1367,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1467,6 +1539,7 @@ projects:
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1523,6 +1596,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 +1652,7 @@ parallel_plan: true`,
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1638,6 +1723,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1730,6 +1816,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) {
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1861,6 +1948,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_Single_With_RestrictFile
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,
@@ -1972,6 +2060,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_with_IncludeGitUntracked
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverMode,
scope,
logger,
terraformClient,

View File

@@ -597,6 +597,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
userConfig.RestrictFileList,
userConfig.SilenceNoProjects,
userConfig.IncludeGitUntrackedFiles,
userConfig.AutoDiscoverModeFlag,
statsScope,
logger,
terraformClient,

View File

@@ -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"`