-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #63 from padok-team/feat/TG_DRY_001
feat(tg_dry_001): init
- Loading branch information
Showing
11 changed files
with
1,544 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
package checks | ||
|
||
import ( | ||
"fmt" | ||
"maps" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/gruntwork-io/terragrunt/config" | ||
"github.com/gruntwork-io/terragrunt/options" | ||
"github.com/hashicorp/hcl/v2/gohcl" | ||
"github.com/hashicorp/hcl/v2/hclparse" | ||
"github.com/padok-team/guacamole/data" | ||
"github.com/padok-team/guacamole/helpers" | ||
"github.com/spf13/viper" | ||
"github.com/zclconf/go-cty/cty" | ||
) | ||
|
||
func Dry() (data.Check, error) { | ||
dataCheck := data.Check{ | ||
ID: "TG_DRY_001", | ||
Name: "No duplicate inputs within a layer", | ||
RelatedGuidelines: "https://padok-team.github.io/docs-terraform-guidelines/terragrunt/context_pattern.html#%EF%B8%8F-context", | ||
Status: "✅", | ||
} | ||
codebasePath := viper.GetString("codebase-path") | ||
// Default options | ||
options := options.NewTerragruntOptions() | ||
// Get all layers | ||
layers, err := config.FindConfigFilesInPath(codebasePath, options) | ||
if err != nil { | ||
fmt.Println("Couldn't find files on codebase-path, is it a terragrunt repo root ?") | ||
fmt.Println(err) | ||
return dataCheck, err | ||
} | ||
|
||
duplicates := []string{} | ||
for _, layer := range layers { | ||
// Get all files that are included with Terragrunt include block | ||
files, _ := findFilesInLayers(layer) | ||
// If there is more that 1 file, aka if we at least include 1 othe file | ||
if len(files) > 1 { | ||
// Get every 2 combination of file possible | ||
for combination := range helpers.CombinationsStr(files, 2) { | ||
findings, err := findDuplicationInInputs(combination[0], combination[1]) | ||
if err != nil { | ||
return dataCheck, err | ||
} | ||
for key, f := range findings { | ||
errmsg := "Duplicate in file " + combination[0] + " and " + combination[1] + " --> " + key + ":" + f | ||
duplicates = append(duplicates, errmsg) | ||
} | ||
} | ||
} | ||
} | ||
dataCheck.Errors = duplicates | ||
|
||
if len(duplicates) > 0 { | ||
dataCheck.Status = "❌" | ||
} | ||
return dataCheck, nil | ||
} | ||
|
||
func findFilesInLayers(path string) ([]string, error) { | ||
files := []string{} | ||
// Create new terragrunt option scoped to the file we are scanning | ||
options, _ := options.NewTerragruntOptionsWithConfigPath(path) | ||
options.OriginalTerragruntConfigPath = path | ||
// Parse the file with PartialParseConfigFile which parse all essential block, in our case local and include | ||
// https://github.com/gruntwork-io/terragrunt/blob/master/config/config_partial.go#L147 | ||
terragruntConfig, err := config.PartialParseConfigFile(path, options, nil, []config.PartialDecodeSectionType{ | ||
config.DependenciesBlock, | ||
config.DependencyBlock, | ||
}) | ||
if err != nil { | ||
fmt.Println("Error parsing file", err.Error()) | ||
return files, err | ||
} | ||
// Add initial terragrunt.hcl file | ||
files = append(files, path) | ||
// Add all includes files | ||
for _, i := range terragruntConfig.ProcessedIncludes { | ||
if strings.HasPrefix(i.Path, ".") || !strings.HasPrefix(i.Path, "/") { | ||
// Convert relative path to absolute | ||
files = append(files, filepath.Clean(filepath.Dir(path)+"/"+i.Path)) | ||
} else { | ||
files = append(files, i.Path) | ||
} | ||
} | ||
return files, nil | ||
} | ||
|
||
// We use a custom struct and not the one from Terragrunt because it's simplier | ||
type Input struct { | ||
Inputs *cty.Value `hcl:"inputs,attr"` | ||
} | ||
|
||
func findDuplicationInInputs(file1 string, file2 string) (map[string]string, error) { | ||
hcl := hclparse.NewParser() | ||
var input Input | ||
hclFile, err := hcl.ParseHCLFile(file1) | ||
if err != nil { | ||
return nil, err | ||
} | ||
gohcl.DecodeBody(hclFile.Body, nil, &input) | ||
var input2 Input | ||
hclfile2, err := hcl.ParseHCLFile(file2) | ||
if err != nil { | ||
return nil, err | ||
} | ||
gohcl.DecodeBody(hclfile2.Body, nil, &input2) | ||
// Recursive function to compare two hcl file and check if their are duplication of inputs | ||
if input.Inputs != nil && input2.Inputs != nil { | ||
findings := recursiveLookupHclFileToFindDuplication(*input.Inputs, *input2.Inputs, "") | ||
if len(findings) > 0 { | ||
return findings, nil | ||
} | ||
} | ||
return nil, nil | ||
} | ||
|
||
// Recursive function that checks two cty.Value from a parsed hcl file, ctx evolve by going down a map | ||
func recursiveLookupHclFileToFindDuplication(file1 cty.Value, file2 cty.Value, ctx string) map[string]string { | ||
findings := make(map[string]string) | ||
for keyf1, i := range file1.AsValueMap() { | ||
for keyf2, j := range file2.AsValueMap() { | ||
if i.Type() == cty.String && j.Type() == cty.String { | ||
if i == j && keyf1 == keyf2 { // If 2 strings or number are the same? | ||
findings[strings.Trim(ctx+"."+keyf1, ".")] = i.AsString() | ||
} | ||
} else if i.Type() == cty.Number && j.Type() == cty.Number { | ||
// Numbers are arbitrary-precision decimal numbers | ||
// https://pkg.go.dev/github.com/zclconf/go-cty/cty#Type | ||
// We convert them with AsBigFloat().String() | ||
if i.AsBigFloat().String() == j.AsBigFloat().String() && keyf1 == keyf2 { | ||
findings[strings.Trim(ctx+"."+keyf1, ".")] = i.AsBigFloat().String() | ||
} | ||
} else if i.Type().IsObjectType() && j.Type().IsObjectType() { | ||
maps.Copy(findings, recursiveLookupHclFileToFindDuplication(i, j, ctx+"."+keyf1)) | ||
} | ||
} | ||
} | ||
return findings | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package checks | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/padok-team/guacamole/data" | ||
|
||
"golang.org/x/exp/slices" | ||
) | ||
|
||
func LayerStaticChecks() []data.Check { | ||
// Add static checks here | ||
checks := map[string]func() (data.Check, error){ | ||
"Dry": Dry, | ||
} | ||
|
||
var checkResults []data.Check | ||
|
||
wg := new(sync.WaitGroup) | ||
wg.Add(len(checks)) | ||
|
||
c := make(chan data.Check, len(checks)) | ||
defer close(c) | ||
|
||
for _, checkFunction := range checks { | ||
go func(checkFunction func() (data.Check, error)) { | ||
defer wg.Done() | ||
|
||
check, err := checkFunction() | ||
if err != nil { | ||
panic(err) | ||
} | ||
c <- check | ||
}(checkFunction) | ||
} | ||
|
||
wg.Wait() | ||
|
||
for i := 0; i < len(checks); i++ { | ||
check := <-c | ||
checkResults = append(checkResults, check) | ||
} | ||
|
||
// Sort the checks by their ID | ||
slices.SortFunc(checkResults, func(i, j data.Check) int { | ||
if i.ID < j.ID { | ||
return -1 | ||
} | ||
if i.ID > j.ID { | ||
return 1 | ||
} | ||
return 0 | ||
}) | ||
|
||
return checkResults | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
Copyright © 2024 NAME HERE <EMAIL ADDRESS> | ||
*/ | ||
package cmd | ||
|
||
import ( | ||
"log" | ||
"os" | ||
|
||
"github.com/padok-team/guacamole/checks" | ||
"github.com/padok-team/guacamole/helpers" | ||
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
) | ||
|
||
// layerCmd represents the layer command | ||
var layerCmd = &cobra.Command{ | ||
Use: "layer", | ||
Short: "Run static code checks on layers, it can be ran on a unique layer or at the root of your repo", | ||
Run: func(cmd *cobra.Command, args []string) { | ||
l := log.New(os.Stderr, "", 0) | ||
l.Println("Running static checks on layers...") | ||
checkResults := checks.LayerStaticChecks() | ||
// helpers.RenderTable(checkResults) | ||
verbose := viper.GetBool("verbose") | ||
helpers.RenderChecks(checkResults, verbose) | ||
// If there is at least one error, exit with code 1 | ||
if helpers.HasError(checkResults) { | ||
os.Exit(1) | ||
} | ||
}, | ||
} | ||
|
||
func init() { | ||
static.AddCommand(layerCmd) | ||
|
||
// Here you will define your flags and configuration settings. | ||
|
||
// Cobra supports Persistent Flags which will work for this command | ||
// and all subcommands, e.g.: | ||
// layerCmd.PersistentFlags().String("foo", "", "A help for foo") | ||
|
||
// Cobra supports local flags which will only run when this command | ||
// is called directly, e.g.: | ||
// layerCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/* | ||
Copyright © 2024 NAME HERE <EMAIL ADDRESS> | ||
*/ | ||
package cmd | ||
|
||
import ( | ||
"log" | ||
"os" | ||
|
||
"github.com/padok-team/guacamole/checks" | ||
"github.com/padok-team/guacamole/helpers" | ||
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
) | ||
|
||
// moduleCmd represents the module command | ||
var moduleCmd = &cobra.Command{ | ||
Use: "module", | ||
Short: "Run static code checks on modules", | ||
Run: func(cmd *cobra.Command, args []string) { | ||
l := log.New(os.Stderr, "", 0) | ||
l.Println("Running static checks on modules...") | ||
checkResults := checks.ModuleStaticChecks() | ||
// helpers.RenderTable(checkResults) | ||
verbose := viper.GetBool("verbose") | ||
helpers.RenderChecks(checkResults, verbose) | ||
// If there is at least one error, exit with code 1 | ||
if helpers.HasError(checkResults) { | ||
os.Exit(1) | ||
} | ||
}, | ||
} | ||
|
||
func init() { | ||
static.AddCommand(moduleCmd) | ||
|
||
// Here you will define your flags and configuration settings. | ||
|
||
// Cobra supports Persistent Flags which will work for this command | ||
// and all subcommands, e.g.: | ||
// moduleCmd.PersistentFlags().String("foo", "", "A help for foo") | ||
|
||
// Cobra supports local flags which will only run when this command | ||
// is called directly, e.g.: | ||
// moduleCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.