Skip to content

Commit

Permalink
Add ability to ignore path to autodiscover
Browse files Browse the repository at this point in the history
Signed-off-by: Luke Massa <[email protected]>
  • Loading branch information
lukemassa committed Jan 20, 2025
1 parent 7b4576a commit e8c7bd1
Show file tree
Hide file tree
Showing 10 changed files with 245 additions and 10 deletions.
9 changes: 9 additions & 0 deletions runatlantis.io/docs/repo-level-atlantis-yaml.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ version: 3
automerge: true
autodiscover:
mode: auto
ignore: /some\/path/
delete_source_branch_on_merge: true
parallel_plan: true
parallel_apply: true
Expand Down Expand Up @@ -405,6 +406,14 @@ 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.

```yaml
autodiscover:
mode: "enabled"
ignore: /dir.*/
```

Autodiscover can be configured to skip over directories that match a regex.

### Custom Backend Config

See [Custom Workflow Use Cases: Custom Backend Config](custom-workflows.md#custom-backend-config)
Expand Down
2 changes: 2 additions & 0 deletions runatlantis.io/docs/server-side-repo-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ repos:
# autodiscover defines how atlantis should automatically discover projects in this repository.
autodiscover:
mode: auto
# Optionally ignore some paths for autodiscovery by regex
ignore: /foo.*/

# id can also be an exact match.
- id: github.com/myorg/specific-repo
Expand Down
33 changes: 31 additions & 2 deletions server/core/config/raw/autodiscover.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package raw

import (
"regexp"
"strings"

validation "github.com/go-ozzo/ozzo-validation"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config/valid"
)

var DefaultAutoDiscoverMode = valid.AutoDiscoverAutoMode

type AutoDiscover struct {
Mode *valid.AutoDiscoverMode `yaml:"mode,omitempty"`
Mode *valid.AutoDiscoverMode `yaml:"mode,omitempty"`
Ignore *string `yaml:"ignore,omitempty"`
}

func (a AutoDiscover) ToValid() *valid.AutoDiscover {
Expand All @@ -20,19 +25,43 @@ func (a AutoDiscover) ToValid() *valid.AutoDiscover {
v.Mode = DefaultAutoDiscoverMode
}

if a.Ignore != nil {
ignore := *a.Ignore
withoutSlashes := ignore[1 : len(ignore)-1]
// Safe to use MustCompile because we test it in Validate().
v.Ignore = regexp.MustCompile(withoutSlashes)
}

return &v
}

func (a AutoDiscover) Validate() error {

ignoreValid := func(value interface{}) error {
strPtr := value.(*string)
if strPtr == nil {
return nil
}
ignore := *strPtr
if !strings.HasPrefix(ignore, "/") || !strings.HasSuffix(ignore, "/") {
return errors.New("regex must begin and end with a slash '/'")
}
withoutSlashes := ignore[1 : len(ignore)-1]
_, err := regexp.Compile(withoutSlashes)
return errors.Wrapf(err, "parsing: %s", ignore)
}

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)),
validation.Field(&a.Ignore, validation.By(ignoreValid)),
)
return res
}

func DefaultAutoDiscover() *valid.AutoDiscover {
return &valid.AutoDiscover{
Mode: DefaultAutoDiscoverMode,
Mode: DefaultAutoDiscoverMode,
Ignore: nil,
}
}
56 changes: 51 additions & 5 deletions server/core/config/raw/autodiscover_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package raw_test

import (
"regexp"
"testing"

"github.com/runatlantis/atlantis/server/core/config/raw"
Expand All @@ -10,6 +11,7 @@ import (

func TestAutoDiscover_UnmarshalYAML(t *testing.T) {
autoDiscoverEnabled := valid.AutoDiscoverEnabledMode
ignoreString := "foobar"
cases := []struct {
description string
input string
Expand All @@ -19,16 +21,19 @@ func TestAutoDiscover_UnmarshalYAML(t *testing.T) {
description: "omit unset fields",
input: "",
exp: raw.AutoDiscover{
Mode: nil,
Mode: nil,
Ignore: nil,
},
},
{
description: "all fields set",
input: `
mode: enabled
ignore: foobar
`,
exp: raw.AutoDiscover{
Mode: &autoDiscoverEnabled,
Mode: &autoDiscoverEnabled,
Ignore: &ignoreString,
},
},
}
Expand All @@ -48,6 +53,10 @@ func TestAutoDiscover_Validate(t *testing.T) {
autoDiscoverEnabled := valid.AutoDiscoverEnabledMode
autoDiscoverDisabled := valid.AutoDiscoverDisabledMode
randomString := valid.AutoDiscoverMode("random_string")
regexWithoutSlahes := ".*"
invalidRegexString := "/***/"
validRegexString := "/.*/"
validRegexPath := `/some\/path\//`
cases := []struct {
description string
input raw.AutoDiscover
Expand Down Expand Up @@ -86,6 +95,38 @@ func TestAutoDiscover_Validate(t *testing.T) {
},
errContains: String("valid value"),
},
{
description: "ignore set to regex without slashes",
input: raw.AutoDiscover{
Mode: &autoDiscoverAuto,
Ignore: &regexWithoutSlahes,
},
errContains: String("regex must begin and end with a slash '/'"),
},
{
description: "ignore set to broken regex",
input: raw.AutoDiscover{
Mode: &autoDiscoverAuto,
Ignore: &invalidRegexString,
},
errContains: String("error parsing regexp: missing argument to repetition operator: `*`"),
},
{
description: "ignore set to valid regex",
input: raw.AutoDiscover{
Mode: &autoDiscoverAuto,
Ignore: &validRegexString,
},
errContains: nil,
},
{
description: "ignore set to valid regex path",
input: raw.AutoDiscover{
Mode: &autoDiscoverAuto,
Ignore: &validRegexPath,
},
errContains: nil,
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
Expand All @@ -100,6 +141,8 @@ func TestAutoDiscover_Validate(t *testing.T) {

func TestAutoDiscover_ToValid(t *testing.T) {
autoDiscoverEnabled := valid.AutoDiscoverEnabledMode
ignoreString := ".*"
ignoreRegex := regexp.MustCompile(".*")
cases := []struct {
description string
input raw.AutoDiscover
Expand All @@ -109,16 +152,19 @@ func TestAutoDiscover_ToValid(t *testing.T) {
description: "nothing set",
input: raw.AutoDiscover{},
exp: &valid.AutoDiscover{
Mode: valid.AutoDiscoverAutoMode,
Mode: valid.AutoDiscoverAutoMode,
Ignore: nil,
},
},
{
description: "value set",
input: raw.AutoDiscover{
Mode: &autoDiscoverEnabled,
Mode: &autoDiscoverEnabled,
Ignore: &ignoreString,
},
exp: &valid.AutoDiscover{
Mode: valid.AutoDiscoverEnabledMode,
Mode: valid.AutoDiscoverEnabledMode,
Ignore: ignoreRegex,
},
},
}
Expand Down
1 change: 1 addition & 0 deletions server/core/config/raw/repo_cfg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ version: 3
automerge: true
autodiscover:
mode: enabled
ignore: /foo.*/
parallel_apply: true
parallel_plan: false
repo_locks:
Expand Down
5 changes: 4 additions & 1 deletion server/core/config/valid/autodiscover.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package valid

import "regexp"

// AutoDiscoverMode enum
type AutoDiscoverMode string

Expand All @@ -10,5 +12,6 @@ const (
)

type AutoDiscover struct {
Mode AutoDiscoverMode
Mode AutoDiscoverMode
Ignore *regexp.Regexp
}
7 changes: 7 additions & 0 deletions server/core/config/valid/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ func (r RepoCfg) AutoDiscoverEnabled(defaultAutoDiscoverMode AutoDiscoverMode) b
return autoDiscoverMode == AutoDiscoverEnabledMode
}

func (r RepoCfg) IsPathIgnoredForAutoDiscover(path string) bool {
if r.AutoDiscover == nil || r.AutoDiscover.Ignore == nil {
return false
}
return r.AutoDiscover.Ignore.MatchString(path)
}

// 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
Expand Down
66 changes: 65 additions & 1 deletion server/core/config/valid/repo_cfg_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package valid_test

import (
"regexp"
"testing"

validation "github.com/go-ozzo/ozzo-validation"
Expand Down Expand Up @@ -316,10 +317,73 @@ func TestConfig_AutoDiscoverEnabled(t *testing.T) {
AutoDiscover: nil,
}
if c.repoAutoDiscover != "" {
r.AutoDiscover = &valid.AutoDiscover{c.repoAutoDiscover}
r.AutoDiscover = &valid.AutoDiscover{
Mode: c.repoAutoDiscover,
}
}
enabled := r.AutoDiscoverEnabled(c.defaultAutoDiscover)
Equals(t, c.expEnabled, enabled)
})
}
}

func TestConfig_IsPathIgnoredForAutoDiscover(t *testing.T) {
cases := []struct {
description string
repoCfg valid.RepoCfg
path string
expIgnored bool
}{
{
description: "auto discover unconfigured",
repoCfg: valid.RepoCfg{},
path: "foo",
expIgnored: false,
},
{
description: "auto discover configured, but not path",
repoCfg: valid.RepoCfg{
AutoDiscover: &valid.AutoDiscover{},
},
path: "foo",
expIgnored: false,
},
{
description: "path does not match regex",
repoCfg: valid.RepoCfg{
AutoDiscover: &valid.AutoDiscover{
Ignore: regexp.MustCompile("bar"),
},
},
path: "foo",
expIgnored: false,
},
{
description: "path does match regex",
repoCfg: valid.RepoCfg{
AutoDiscover: &valid.AutoDiscover{
Ignore: regexp.MustCompile("fo.*"),
},
},
path: "foo",
expIgnored: true,
},
{
description: "long path does match regex",
repoCfg: valid.RepoCfg{
AutoDiscover: &valid.AutoDiscover{
Ignore: regexp.MustCompile(`foo\/.*\/baz`),
},
},
path: "foo/bar/baz",
expIgnored: true,
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {

enabled := c.repoCfg.IsPathIgnoredForAutoDiscover(c.path)
Equals(t, c.expIgnored, enabled)
})
}
}
6 changes: 5 additions & 1 deletion server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,11 @@ func (p *DefaultProjectCommandBuilder) getMergedProjectCfgs(ctx *command.Context
configuredProjDirs[filepath.Clean(configProj.Dir)] = true
}
for _, mp := range allModifiedProjects {
_, dirExists := configuredProjDirs[filepath.Clean(mp.Path)]
path := filepath.Clean(mp.Path)
if repoCfg.IsPathIgnoredForAutoDiscover(path) {
continue
}
_, dirExists := configuredProjDirs[path]
if !dirExists {
modifiedProjects = append(modifiedProjects, mp)
}
Expand Down
Loading

0 comments on commit e8c7bd1

Please sign in to comment.