Skip to content

Commit

Permalink
Merge pull request #63 from padok-team/feat/TG_DRY_001
Browse files Browse the repository at this point in the history
feat(tg_dry_001): init
  • Loading branch information
Benjamin Sanvoisin authored Jan 29, 2024
2 parents 802e4ed + d3c40f7 commit ef946c7
Show file tree
Hide file tree
Showing 11 changed files with 1,544 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build the guacamole binary
FROM docker.io/library/golang:1.20.10@sha256:8a3e8d1d3a513c0155451c522d381e901837610296f5a077b19f3d350b3a1585 as builder
FROM docker.io/library/golang:1.21.5@sha256:672a2286da3ee7a854c3e0a56e0838918d0dbb1c18652992930293312de898a6 as builder
ARG TARGETOS
ARG TARGETARCH
ARG PACKAGE=github.com/padok-team/guacamole
Expand Down
2 changes: 1 addition & 1 deletion checks/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func All(layers []*data.Layer) []data.Check {

go func() {
defer wg.Done()
c <- StaticChecks()
c <- ModuleStaticChecks()
}()

wg.Wait()
Expand Down
144 changes: 144 additions & 0 deletions checks/dry.go
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
}
56 changes: 56 additions & 0 deletions checks/layer_static_checks.go
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
}
2 changes: 1 addition & 1 deletion checks/static_checks.go → checks/module_static_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"golang.org/x/exp/slices"
)

func StaticChecks() []data.Check {
func ModuleStaticChecks() []data.Check {
// Add static checks here
checks := map[string]func() (data.Check, error){
"ProviderInModule": ProviderInModule,
Expand Down
46 changes: 46 additions & 0 deletions cmd/static_check_layer.go
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")
}
46 changes: 46 additions & 0 deletions cmd/static_check_module.go
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")
}
18 changes: 2 additions & 16 deletions cmd/static_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,17 @@ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
package cmd

import (
"log"
"os"

"github.com/padok-team/guacamole/checks"
"github.com/padok-team/guacamole/helpers"
"fmt"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

// plan represents the run command
var static = &cobra.Command{
Use: "static",
Short: "Run static code checks",
Run: func(cmd *cobra.Command, args []string) {
l := log.New(os.Stderr, "", 0)
l.Println("Running static checks...")
checkResults := checks.StaticChecks()
// 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)
}
fmt.Println("You have to specify what you want to check : layer or module")
},
}

Expand Down
Loading

0 comments on commit ef946c7

Please sign in to comment.