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

feat(tg_dry_001): init #63

Merged
merged 3 commits into from
Jan 29, 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 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
Loading