Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pattern detector works as a regex #5

Merged
merged 4 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Run the tool using the following command:
| `-e, --output-type` | Atlantis YAML output type [file stdout] | `OUTPUT_TYPE` | `file` |
| `--parallel-apply` | Atlantis parallel apply config value. | `PARALLEL_APPLY` | `true` |
| `--parallel-plan` | Atlantis parallel plan config value. | `PARALLEL_PLAN` | `true` |
| `-q, --pattern-detector`| Discover projects based on files or directories names. | `PATTERN_DETECTOR` | `main.tf` |
| `-q, --pattern-detector`| Discover projects based on files, directories names or regex. | `PATTERN_DETECTOR` | `main.tf` |
| `-u, --pr-filter` | Filter projects based on the PR changes (Only for github SCM).| `PR_FILTER` | `false` |
| `-p, --pull-num` | Github Pull Request Number to check diffs. | `PULL_NUM` | |
| `--terraform-base-dir` | Basedir for terraform resources. | `TERRAFORM_BASE_DIR`| `./` |
Expand Down
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/totmicro/atlantis-yaml-generator

go 1.21

require github.com/google/go-github v17.0.0+incompatible
require (
github.com/google/go-github v17.0.0+incompatible
github.com/spf13/afero v1.11.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
Expand All @@ -13,6 +16,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand Down Expand Up @@ -58,6 +60,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
Expand Down
23 changes: 20 additions & 3 deletions pkg/atlantis/atlantis.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ type ProjectFolder struct {
WorkspaceList []string
}

func (pf ProjectFolder) Hash() string {
// Implement a unique string generation based on the content of atlantis.ProjectFolder
return fmt.Sprintf("%s", pf.Path)
}

type OSFileSystem struct{}

func (OSFileSystem) Walk(root string, walkFn filepath.WalkFunc) error {
return filepath.Walk(root, walkFn)
}

// GenerateAtlantisYAML generates the atlantis.yaml file
func GenerateAtlantisYAML() error {

Expand All @@ -59,6 +70,7 @@ func GenerateAtlantisYAML() error {

// Scan folders to detect projects
projectFoldersList, err := scanProjectFolders(
OSFileSystem{},
config.GlobalConfig.Parameters["terraform-base-dir"],
config.GlobalConfig.Parameters["discovery-mode"],
config.GlobalConfig.Parameters["pattern-detector"],
Expand Down Expand Up @@ -120,18 +132,23 @@ func GenerateAtlantisYAML() error {
return nil
}

func scanProjectFolders(basePath, discoveryMode, patternDetector string) (projectFolders []ProjectFolder, err error) {
err = filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
// Scans a folder for projects and returns a list of unique projects.
func scanProjectFolders(filesystem helpers.Walkable, basePath, discoveryMode, patternDetector string) (projectFolders []ProjectFolder, err error) {
uniques := helpers.NewSet()
err = filesystem.Walk(basePath, func(path string, info os.FileInfo, err error) error {
if err != nil || info == nil {
return err
}
if discoveryFilter(info, path, discoveryMode, patternDetector) {
relPath, _ := filepath.Rel(basePath, filepath.Dir(path))
projectFolders = append(projectFolders, ProjectFolder{Path: relPath})
uniques.Add(ProjectFolder{Path: relPath})
}
return nil
})

for _, projectFolder := range uniques.Elements {
projectFolders = append(projectFolders, projectFolder.(ProjectFolder))
}
return projectFolders, err
}

Expand Down
40 changes: 34 additions & 6 deletions pkg/atlantis/atlantis_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package atlantis

import (
"os"
"path/filepath"
"reflect"
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/totmicro/atlantis-yaml-generator/pkg/config"
"github.com/totmicro/atlantis-yaml-generator/pkg/helpers"
"gopkg.in/yaml.v3"
"os"
"path/filepath"
"reflect"
"testing"
)

func TestApplyProjectFilter(t *testing.T) {
Expand Down Expand Up @@ -387,7 +387,11 @@ func TestScanProjectFolders(t *testing.T) {

for _, test := range tests {
t.Run(test.discoveryMode, func(t *testing.T) {
projectFolders, err := scanProjectFolders(test.basePath, test.discoveryMode, test.patternDetector)
projectFolders, err := scanProjectFolders(
OSFileSystem{},
test.basePath,
test.discoveryMode,
test.patternDetector)
if test.expectedError {
assert.Error(t, err)
} else {
Expand Down Expand Up @@ -720,3 +724,27 @@ func TestGenerateAtlantisYAML(t *testing.T) {
assert.NoError(t, err)

}

// Tests multiple project hits returns unique projects only.
// e.g. if we scan for *.tf the same project isn't hit twice.
func TestScanProjectFoldersUniques(t *testing.T) {
memFS := afero.NewMemMapFs()
fs := afero.Afero{Fs: memFS}
// Create directories and files
// project3 has multiple hits
afero.WriteFile(fs, "projects_root/project1/main.tf", []byte("content"), 0644)
afero.WriteFile(fs, "projects_root/project2/main.tf", []byte("content"), 0644)
afero.WriteFile(fs, "projects_root/project3/main.tf", []byte("content"), 0644)
afero.WriteFile(fs, "projects_root/project3/outputs.tf", []byte("content"), 0644)

// Use the fs (implementing Walkable) to call scanProjectFolders
projectFolders, err := scanProjectFolders(fs, "projects_root", "single-workspace", `.*\.tf`)
if err != nil {
t.Errorf("scanProjectFolders returned an error: %v", err)
}

// Verify that 3 project folders were returned
if len(projectFolders) != 3 {
t.Errorf("Expected 3 project folders, got %d. Projects %v", len(projectFolders), projectFolders)
}
}
4 changes: 2 additions & 2 deletions pkg/atlantis/multiple-workspace-discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
func multiWorkspaceGetProjectScope(relPath, patternDetector string, changedFiles []string) string {
for _, file := range changedFiles {
if strings.HasPrefix(file, fmt.Sprintf("%s/", relPath)) &&
!strings.Contains(file, patternDetector) {
!helpers.MatchesPattern(patternDetector, file) {
return "crossWorkspace"
}
}
Expand Down Expand Up @@ -58,6 +58,6 @@ func multiWorkspaceDetectProjectWorkspaces(changedFiles []string, enablePRFilter

func multiWorkspaceDiscoveryFilter(info os.FileInfo, path, patternDetector string) bool {
return info.IsDir() &&
info.Name() == patternDetector &&
helpers.MatchesPattern(patternDetector, info.Name()) &&
!strings.Contains(path, ".terraform")
}
3 changes: 2 additions & 1 deletion pkg/atlantis/single-workspace-discovery.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package atlantis

import (
"github.com/totmicro/atlantis-yaml-generator/pkg/helpers"
"os"
"strings"
)

func singleWorkspaceDiscoveryFilter(info os.FileInfo, path, patternDetector string) bool {
return !info.IsDir() &&
info.Name() == patternDetector &&
helpers.MatchesPattern(patternDetector, info.Name()) &&
!strings.Contains(path, ".terraform")
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ var ParameterList = []Parameter{
},
{
Name: "pattern-detector",
Description: "discover projects based on files or directories names.",
Description: "discover projects based on files, directories names or regex.",
Required: false,
DefaultValue: "main.tf",
Shorthand: "q",
Expand Down
66 changes: 66 additions & 0 deletions pkg/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package helpers
import (
"os"
"path/filepath"
"regexp"
"strings"
)

Expand Down Expand Up @@ -51,3 +52,68 @@ func ReadFile(filename string) (string, error) {
_, err = file.Read(content)
return string(content), err
}

// MatchesPattern checks if the given string matches the specified regex pattern.
// It returns true if the pattern matches the string, and false otherwise.
func MatchesPattern(pattern string, str string) bool {
matched, err := regexp.MatchString(pattern, str)
if err != nil {
return false
}
return matched
}

// Set data structure
// Keys are strings and elements must implement Hashable to calculate keys.
type Set struct {
Elements map[string]Hashable
}

// NewSet creates a new Set
func NewSet() *Set {
return &Set{
Elements: make(map[string]Hashable),
}
}

// Add adds an element to the set
func (s *Set) Add(element Hashable) {
key := element.Hash()
s.Elements[key] = element
}

// Remove removes an element from the set
func (s *Set) Remove(element Hashable) {
key := element.Hash()
delete(s.Elements, key)
}

// Contains checks if an element is in the set
func (s *Set) Contains(element Hashable) bool {
key := element.Hash()
_, exists := s.Elements[key]
return exists
}

// Size returns the number of Elements in the set
func (s *Set) Size() int {
return len(s.Elements)
}

// List returns all the Elements in the set
func (s *Set) List() []Hashable {
list := make([]Hashable, 0, len(s.Elements))
for _, element := range s.Elements {
list = append(list, element)
}
return list
}

// Enables use of Set by requiring its elements to be hashable.
type Hashable interface {
Hash() string
}

type Walkable interface {
Walk(root string, walkFn filepath.WalkFunc) error
}
77 changes: 77 additions & 0 deletions pkg/helpers/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package helpers
import (
"fmt"
"os"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -175,3 +176,79 @@ func TestReadFile_ReadError(t *testing.T) {
_, err = ReadFile(tempFile.Name())
assert.Error(t, err) // Check that an error is returned
}

func TestMatchesPattern(t *testing.T) {
tests := []struct {
pattern string
str string
want bool
}{
{"workspace_vars", "workspace_vars", true},
{"main.tf", "main.tf", true},
{".*\\.tf", "main.tf", true},
{".*\\.tf", "vpc.tf", true},
{"^a.*z$", "alphabet", false}, // Negative test case
}

for _, tt := range tests {
t.Run(tt.pattern, func(t *testing.T) {
if got := MatchesPattern(tt.pattern, tt.str); got != tt.want {
t.Errorf("MatchesPattern(%q, %q) = %v, want %v", tt.pattern, tt.str, got, tt.want)
}
})
}
}

// HashableString is a simple Hashable type for testing.
type HashableString string

func (h HashableString) Hash() string {
return string(h)
}

func TestNewSet(t *testing.T) {
s := NewSet()
if s == nil || len(s.Elements) != 0 {
t.Errorf("NewSet() = %v, want a new Set instance with empty Elements", s)
}
}

func TestAddAndContains(t *testing.T) {
s := NewSet()
element := HashableString("test")
s.Add(element)
if !s.Contains(element) {
t.Errorf("Set does not contain element %v after Add", element)
}
}

func TestRemove(t *testing.T) {
s := NewSet()
element := HashableString("test")
s.Add(element)
s.Remove(element)
if s.Contains(element) {
t.Errorf("Set contains element %v after Remove", element)
}
}

func TestSize(t *testing.T) {
s := NewSet()
s.Add(HashableString("one"))
s.Add(HashableString("two"))
if s.Size() != 2 {
t.Errorf("Size() = %d, want 2", s.Size())
}
}

func TestList(t *testing.T) {
s := NewSet()
elements := []Hashable{HashableString("one"), HashableString("two")}
for _, e := range elements {
s.Add(e)
}
list := s.List()
if !reflect.DeepEqual(list, elements) && len(list) == len(elements) {
t.Errorf("List() = %v, want %v", list, elements)
}
}