Skip to content

Commit

Permalink
feat(GH-33): Support rule loading from config file
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanilves committed Dec 11, 2022
1 parent 5329fd2 commit d251115
Show file tree
Hide file tree
Showing 18 changed files with 171 additions and 64 deletions.
6 changes: 6 additions & 0 deletions .travelgrunt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
rules:
- prefix: vendor/
exclude: true
- prefix: fixtures/
exclude: true
- name: '^.*\.go$'
3 changes: 2 additions & 1 deletion fixtures/config/travelgrunt.yml.dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mode: dockerfile
rules:
- mode: dockerfile
3 changes: 2 additions & 1 deletion fixtures/config/travelgrunt.yml.illegal
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mode: bogus
rules:
- mode: bogus
3 changes: 2 additions & 1 deletion fixtures/config/travelgrunt.yml.jenkins
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mode: jenkins
rules:
- mode: jenkins
3 changes: 2 additions & 1 deletion fixtures/config/travelgrunt.yml.terraform
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mode: terraform
rules:
- mode: terraform
4 changes: 3 additions & 1 deletion fixtures/config/travelgrunt.yml.terraform_or_terragrunt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
mode: terraform_or_terragrunt
rules:
- mode: terraform
- mode: terragrunt
3 changes: 2 additions & 1 deletion fixtures/config/travelgrunt.yml.terragrunt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mode: terragrunt
rules:
- mode: terragrunt
55 changes: 24 additions & 31 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package config

import (
"fmt"
"os"

"gopkg.in/yaml.v3"
Expand All @@ -13,12 +12,19 @@ var configFile = ".travelgrunt.yml"

// Config contains travelgrunt repo-level configuration
type Config struct {
Mode string `yaml:"mode"`
IncludeFn func(os.DirEntry) bool
Rules []Rule `yaml:"rules"`

IsDefault bool
}

// DefaultConfig returns default travelgrunt repo-level configuration
func DefaultConfig() Config {
return Config{
Rules: []Rule{{IncludeFn: include.IsTerragrunt}},
IsDefault: true,
}
}

// NewConfig creates new travelgrunt repo-level configuration
func NewConfig(path string) (cfg Config, err error) {
var data []byte
Expand All @@ -34,39 +40,26 @@ func NewConfig(path string) (cfg Config, err error) {
return cfg, err
}

cfg.IncludeFn, err = GetIncludeFn(cfg.Mode)
for idx := range cfg.Rules {
cfg.Rules[idx].IncludeFn, err = getIncludeFn(cfg.Rules[idx].Mode)

return cfg, err
}
if err != nil {
cfg.Rules = nil

// DefaultConfig returns default travelgrunt repo-level configuration
func DefaultConfig() Config {
return Config{
Mode: "terragrunt",
IncludeFn: include.IsTerragrunt,
IsDefault: true,
return cfg, err
}
}
}

// GetIncludeFn gets an "include" func for the given mode (if mode is unknown, it returns a nil func and a non-nil error)
func GetIncludeFn(mode string) (fn func(os.DirEntry) bool, err error) {
err = nil
return cfg, nil
}

switch mode {
case "terragrunt":
fn = include.IsTerragrunt
case "terraform":
fn = include.IsTerraform
case "terraform_or_terragrunt":
fn = include.IsTerraformOrTerragrunt
case "dockerfile":
fn = include.IsDockerfile
case "jenkins":
fn = include.IsJenkins
default:
fn = nil
err = fmt.Errorf("illegal mode: %s", mode)
// Include is a "decider" function that includes/excludes the path given
func (cfg Config) Include(d os.DirEntry, rel string) bool {
for idx := range cfg.Rules {
if cfg.Rules[idx].Include(d, rel) {
return !cfg.Rules[idx].Exclude
}
}

return fn, err
return false
}
34 changes: 16 additions & 18 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,64 +15,62 @@ const (
fixturePath = "../../fixtures/config"
)

func TestCornerCases(t *testing.T) {
func TestNewConfigCornerCases(t *testing.T) {
assert := assert.New(t)

testCases := map[string]struct {
cfg Config
success bool
}{
"travelgrunt.yml.invalid": {cfg: Config{Mode: "", IncludeFn: nil, IsDefault: false}, success: false},
"travelgrunt.yml.illegal": {cfg: Config{Mode: "bogus", IncludeFn: nil, IsDefault: false}, success: false},
"travelgrunt.yml.nonexistent": {cfg: Config{Mode: "terragrunt", IncludeFn: include.IsTerragrunt, IsDefault: true}, success: true},
"travelgrunt.yml.invalid": {cfg: Config{Rules: nil, IsDefault: false}, success: false},
"travelgrunt.yml.illegal": {cfg: Config{Rules: nil, IsDefault: false}, success: false},
"travelgrunt.yml.nonexistent": {cfg: Config{Rules: []Rule{{Mode: "terragrunt", IncludeFn: include.IsTerragrunt}}, IsDefault: true}, success: true},
}

for cfgFile, expected := range testCases {
configFile = cfgFile

cfg, err := NewConfig(fixturePath)

assert.Equal(expected.cfg.Mode, cfg.Mode)
assert.Equal(expected.cfg.IsDefault, cfg.IsDefault)

if expected.success {
assert.NotNil(cfg.IncludeFn)
assert.NotNil(cfg.Rules)
assert.Nil(err)
} else {
assert.Nil(cfg.IncludeFn)
assert.Nil(cfg.Rules)
assert.NotNil(err)
}
}
}

func getNormalConfig(mode string) Config {
includeFn, _ := GetIncludeFn(mode)
fn, _ := getIncludeFn(mode)

return Config{Mode: mode, IncludeFn: includeFn, IsDefault: false}
return Config{Rules: []Rule{{Mode: mode, IncludeFn: fn}}, IsDefault: false}
}

func TestNormalFlow(t *testing.T) {
func TestNewConfigNormalFlow(t *testing.T) {
assert := assert.New(t)

testCases := map[string]Config{
"travelgrunt.yml.terragrunt": getNormalConfig("terragrunt"),
"travelgrunt.yml.terraform": getNormalConfig("terraform"),
"travelgrunt.yml.terraform_or_terragrunt": getNormalConfig("terraform_or_terragrunt"),
"travelgrunt.yml.dockerfile": getNormalConfig("dockerfile"),
"travelgrunt.yml.jenkins": getNormalConfig("jenkins"),
"travelgrunt.yml.terragrunt": getNormalConfig("terragrunt"),
"travelgrunt.yml.terraform": getNormalConfig("terraform"),
"travelgrunt.yml.dockerfile": getNormalConfig("dockerfile"),
"travelgrunt.yml.jenkins": getNormalConfig("jenkins"),
}

for cfgFile, expected := range testCases {
configFile = cfgFile

cfg, err := NewConfig(fixturePath)

assert.Equal(expected.Mode, cfg.Mode)
assert.NotNil(expected.Rules, cfg.Rules)
assert.Equal(expected.IsDefault, false)

assert.Equalf(
runtime.FuncForPC(reflect.ValueOf(expected.IncludeFn).Pointer()).Name(),
runtime.FuncForPC(reflect.ValueOf(cfg.IncludeFn).Pointer()).Name(),
runtime.FuncForPC(reflect.ValueOf(expected.Rules[0].IncludeFn).Pointer()).Name(),
runtime.FuncForPC(reflect.ValueOf(cfg.Rules[0].IncludeFn).Pointer()).Name(),
"got unexpected include function while loading config file: %s", configFile,
)

Expand Down
3 changes: 2 additions & 1 deletion pkg/config/include/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
)

func fileOrSymlink(d os.DirEntry) bool {
// FileOrSymlink tells us if dir entry in question is a regular file or a symlink
func FileOrSymlink(d os.DirEntry) bool {
return d.Type().IsRegular() || d.Type() == os.ModeSymlink
}
2 changes: 1 addition & 1 deletion pkg/config/include/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import (

// IsDockerfile tells us if we operate on Dockerfile(s) or Dockerfile template(s)
func IsDockerfile(d os.DirEntry) bool {
return fileOrSymlink(d) && strings.Contains(strings.ToLower(d.Name()), "dockerfile")
return FileOrSymlink(d) && strings.Contains(strings.ToLower(d.Name()), "dockerfile")
}
2 changes: 1 addition & 1 deletion pkg/config/include/groovy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import (

// IsGroovy tells us we operate on Groovy file(s)
func IsGroovy(d os.DirEntry) bool {
return fileOrSymlink(d) && strings.HasSuffix(d.Name(), ".groovy")
return FileOrSymlink(d) && strings.HasSuffix(d.Name(), ".groovy")
}
2 changes: 1 addition & 1 deletion pkg/config/include/jenkinsfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import (

// IsJenkinsfile tells us we operate on Jenkinsfile(s)
func IsJenkinsfile(d os.DirEntry) bool {
return fileOrSymlink(d) && strings.Contains(strings.ToLower(d.Name()), "jenkinsfile")
return FileOrSymlink(d) && strings.Contains(strings.ToLower(d.Name()), "jenkinsfile")
}
2 changes: 1 addition & 1 deletion pkg/config/include/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import (

// IsTerraform tells us if we operate on Terraform file(s)
func IsTerraform(d os.DirEntry) bool {
return fileOrSymlink(d) && strings.HasSuffix(d.Name(), ".tf")
return FileOrSymlink(d) && strings.HasSuffix(d.Name(), ".tf")
}
2 changes: 1 addition & 1 deletion pkg/config/include/terragrunt.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ import (

// IsTerragrunt tells us if we operate on Terragrunt config file
func IsTerragrunt(d os.DirEntry) bool {
return fileOrSymlink(d) && d.Name() == "terragrunt.hcl"
return FileOrSymlink(d) && d.Name() == "terragrunt.hcl"
}
30 changes: 30 additions & 0 deletions pkg/config/include_fn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package config

import (
"fmt"
"os"

"github.com/ivanilves/travelgrunt/pkg/config/include"
)

func getIncludeFn(mode string) (fn func(os.DirEntry) bool, err error) {
err = nil

switch mode {
case "":
fn = nil
case "terragrunt":
fn = include.IsTerragrunt
case "terraform":
fn = include.IsTerraform
case "dockerfile":
fn = include.IsDockerfile
case "jenkins":
fn = include.IsJenkins
default:
fn = nil
err = fmt.Errorf("illegal mode: %s", mode)
}

return fn, err
}
72 changes: 72 additions & 0 deletions pkg/config/rule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package config

import (
"os"
"regexp"
"strings"

"github.com/ivanilves/travelgrunt/pkg/config/include"
)

// Rule represents a single configuration rule entity
type Rule struct {
// Mode is a behaviour backed by a function from the `include` package
Mode string `yaml:"mode"`
// Prefix is a literal path prefix to be matched against the passed file path
Prefix string `yaml:"prefix"`
// PathEx is a regex to be matched against the passed file full path
PathEx string `yaml:"path"`
// NameEx is a regex to be matched against the passed file base name
NameEx string `yaml:"name"`
// Exclude reverses the rule match effect
Exclude bool `yaml:"exclude"`

// IncludeFn is an `include` package function to invoke on passed file / dir entry
IncludeFn func(os.DirEntry) bool
}

// Matched matches passed file / dir entry name against supplied expressions (if any)
func (r Rule) Matched(d os.DirEntry, rel string) bool {
return r.pathPrefixed(rel) && r.pathMatched(rel) && r.nameMatched(d)
}

func (r Rule) pathPrefixed(path string) bool {
if r.Prefix != "" {
return strings.HasPrefix(path, r.Prefix)
}

return true
}

func (r Rule) pathMatched(path string) bool {
if r.PathEx != "" {
rx := regexp.MustCompile(r.PathEx)

return rx.MatchString(path)
}

return true
}

func (r Rule) nameMatched(d os.DirEntry) bool {
if r.NameEx != "" {
if !include.FileOrSymlink(d) {
return false
}

rx := regexp.MustCompile(r.NameEx)

return rx.MatchString(d.Name())
}

return true
}

// Include is a "decider" function that includes/excludes the path given [on the single rule level]
func (r Rule) Include(d os.DirEntry, rel string) bool {
if r.IncludeFn == nil {
return r.Matched(d, rel)
}

return r.IncludeFn(d) && r.Matched(d, rel)
}
6 changes: 3 additions & 3 deletions pkg/directory/directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ func Collect(rootPath string, cfg config.Config) (entries map[string]string, pat
return filepath.SkipDir
}

if cfg.IncludeFn(d) {
abs := filepath.Dir(path)
rel := strings.TrimPrefix(abs, rootPath+"/")
abs := filepath.Dir(path)
rel := strings.TrimPrefix(abs, rootPath+"/")

if cfg.Include(d, rel) {
entries[rel] = abs
paths = append(paths, rel)
}
Expand Down

0 comments on commit d251115

Please sign in to comment.