Skip to content

Commit

Permalink
fix(count): switch to using state
Browse files Browse the repository at this point in the history
  • Loading branch information
cterence committed Oct 17, 2023
1 parent 366c9e7 commit c671c18
Show file tree
Hide file tree
Showing 14 changed files with 108 additions and 96 deletions.
2 changes: 1 addition & 1 deletion checks/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func All(layers []*data.Layer) []data.Check {

go func() {
defer wg.Done()
c <- PlanChecks(layers)
c <- StateChecks(layers)
}()

go func() {
Expand Down
72 changes: 36 additions & 36 deletions checks/iterate_use_for_each.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package checks

import (
"fmt"
"encoding/json"
"guacamole/data"
"regexp"
"strconv"
"sync"
)
Expand All @@ -28,68 +27,69 @@ type ResourceChanges struct {

func IterateUseForEach(layers []*data.Layer) (data.Check, error) {
dataCheck := data.Check{
ID: "TF_MOD_003",
ID: "TF_MOD_004",
Name: "Use for_each to create multiple resources of the same type",
RelatedGuidelines: "https://padok-team.github.io/docs-terraform-guidelines/terraform/iterate_on_your_resources.html",
Status: "✅",
}

c := make(chan []string, len(layers))
var checkableLayers []*data.Layer

var allCheckErrors []string

for _, layer := range layers {
if layer.RootModule != nil {
checkableLayers = append(checkableLayers, layer)
}
}

c := make(chan []string, len(checkableLayers))

wg := new(sync.WaitGroup)
wg.Add(len(layers))
wg.Add(len(checkableLayers))

for i := range layers {
for i := range checkableLayers {
go func(layer *data.Layer) {
defer wg.Done()
c <- checkLayer(layer)
}(layers[i])
c <- checkModules(layer.Name, layer.RootModule)
}(checkableLayers[i])
}

wg.Wait()

// Wait for all goroutines to finish
for i := 0; i < len(layers); i++ {
for i := 0; i < len(checkableLayers); i++ {
checkErrors := <-c
if len(checkErrors) > 0 {
dataCheck.Status = "❌"
dataCheck.Errors = append(dataCheck.Errors, checkErrors...)
allCheckErrors = append(allCheckErrors, checkErrors...)
}
}

dataCheck.Errors = allCheckErrors

return dataCheck, nil
}

func checkLayer(layer *data.Layer) []string {
var index int
var indexString string
var checkedResources, checkErrors []string

// Analyze plan for resources with count > 1
for _, rc := range layer.Plan.ResourceChanges {
// Parse the module address to find numbers inside of []
regexpIndexMatch := regexp.MustCompile(`\[(.*?)\]`).FindStringSubmatch(rc.Address)
if len(regexpIndexMatch) > 0 {
indexString = regexpIndexMatch[1]
}

// Ignore error in case of a string into the brackets, meaning that the resource was not created with count
index, _ = strconv.Atoi(indexString)
func checkModules(layerAddress string, m *data.Module) []string {
var checkErrors []string

if index > 0 {
// Check if the resource was already checked
alreadyChecked := false
for _, checkedResource := range checkedResources {
if checkedResource == rc.ModuleAddress {
alreadyChecked = true
break
if len(m.ObjectTypes) > 0 {
for _, o := range m.ObjectTypes {
// Check if type of o.Index is int or string
switch o.Index.(type) {
// If it's an int, we can assume that the resource was created with count
case json.Number:
if o.Count > 1 {
checkErrors = append(checkErrors, "[layer] "+layerAddress+" --> [module] "+m.Address+" --> [resource] "+o.Type+" ("+strconv.Itoa(o.Count)+")")
}
}
if !alreadyChecked {
checkedResources = append(checkedResources, rc.ModuleAddress)
checkErrors = append(checkErrors, fmt.Sprintf("%s --> %s", layer.Name, rc.Address))
}
}
}

for _, c := range m.Children {
checkErrors = append(checkErrors, checkModules(layerAddress, c)...)
}

return checkErrors
}
5 changes: 2 additions & 3 deletions checks/plan_checks.go → checks/state_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import (
"sync"
)

func PlanChecks(layers []*data.Layer) []data.Check {
// Add plan checks here
func StateChecks(layers []*data.Layer) []data.Check {
// Add state checks here
checks := map[string]func([]*data.Layer) (data.Check, error){
"IterateUseForEach": IterateUseForEach,
"RefreshTime": RefreshTime,
}

var checkResults []data.Check
Expand Down
43 changes: 0 additions & 43 deletions cmd/plan_checks.go

This file was deleted.

3 changes: 0 additions & 3 deletions cmd/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ func init() {
rootCmd.AddCommand(profile)

// Add verbose flag
profile.PersistentFlags().BoolP("verbose", "v", false, "Display verbose output")

viper.BindPFlag("verbose", profile.PersistentFlags().Lookup("verbose"))

// Here you will define your flags and configuration settings.

Expand Down
3 changes: 3 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&codebasePath, "codebase-path", "p", ".", "path to your IaC codebase")

viper.BindPFlag("codebase-path", rootCmd.PersistentFlags().Lookup("codebase-path"))

rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Display verbose output")
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
}

// initConfig reads in config file and ENV variables if set.
Expand Down
56 changes: 56 additions & 0 deletions cmd/state_checks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright © 2023 NAME HERE <EMAIL ADDRESS>
*/
package cmd

import (
"fmt"
"guacamole/checks"
"guacamole/helpers"
"log"
"os"

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

// state represents the run command
var state = &cobra.Command{
Use: "state",
Short: "Run state-related checks",
Run: func(cmd *cobra.Command, args []string) {
verbose := viper.GetBool("verbose")
l := log.New(os.Stderr, "", 0)
var prompt string
fmt.Println("⚠ WARNING: This command may fail in unexpected way if all the layers you want to check are not initialized properly.")
for prompt != "y" && prompt != "n" {
fmt.Print("Please confirm that you want to run this command (y/n) : ")
fmt.Scanln(&prompt)
}
if prompt == "y" {
layers, err := helpers.ComputeLayers(false)
l.Println("Running state checks...")
if err != nil {
panic(err)
}
checkResults := checks.StateChecks(layers)
helpers.RenderChecks(checkResults, verbose)
} else {
l.Println("Aborting...")
}
},
}

func init() {
rootCmd.AddCommand(state)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// runCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// runCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
3 changes: 0 additions & 3 deletions cmd/static_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ var static = &cobra.Command{
func init() {
rootCmd.AddCommand(static)

static.PersistentFlags().BoolP("verbose", "v", false, "Display verbose output")

viper.BindPFlag("verbose", static.PersistentFlags().Lookup("verbose"))
// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
Expand Down
6 changes: 4 additions & 2 deletions data/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Module struct {
type ObjectType struct {
Type string
Kind string // resource or datasource
Index any // Can be a string or an int
Instances []Object
Count int
}
Expand Down Expand Up @@ -92,8 +93,9 @@ func (m *Module) buildResourcesAndDatasources(state *tfjson.StateModule) {

if typeIndex == -1 {
m.ObjectTypes = append(m.ObjectTypes, &ObjectType{
Type: r.Type,
Kind: kind,
Type: r.Type,
Kind: kind,
Index: r.Index,
Instances: []Object{
{
Name: r.Name,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module guacamole
go 1.20

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/fatih/color v1.15.0
github.com/gertd/go-pluralize v0.2.1
github.com/hashicorp/terraform-config-inspect v0.0.0-20230925220900-5a6f8d18746d
Expand Down
2 changes: 1 addition & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
Expand Down Expand Up @@ -387,7 +388,6 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
Expand Down
2 changes: 1 addition & 1 deletion helpers/compute_layer_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func ComputeLayers(withPlan bool) ([]*data.Layer, error) {
// fmt.Println("Processing layer: ", layers[i].Name)
go func(layer *data.Layer) {
defer wg.Done()
layer.ComputeState()
layer.BuildRootModule()
if withPlan {
layer.ComputePlan()
}
Expand Down
4 changes: 2 additions & 2 deletions helpers/getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func GetModules() ([]data.TerraformModule, error) {
// Check if the path is a file and its name matches "*.tf"
if !info.IsDir() && strings.HasSuffix(info.Name(), ".tf") {
// exclude the files which are in the .terragrunt-cache or .terraform directory
if !regexp.MustCompile(`.terragrunt-cache|.terraform`).MatchString(path) {
if !regexp.MustCompile(`\.terragrunt-cache|\.terraform`).MatchString(path) {
module := data.TerraformModule{FullPath: filepath.Dir(path), Name: filepath.Base(filepath.Dir(path))}
// Check if the module is already in the list
alreadyInList := false
Expand Down Expand Up @@ -58,7 +58,7 @@ func GetLayers() ([]*data.Layer, error) {
// Check if the current path is a file and its name matches "terragrunt.hcl"
if !info.IsDir() && info.Name() == "terragrunt.hcl" {
// exclude the files which are in the .terragrunt-cache or .terraform directory
if !regexp.MustCompile(`.terragrunt-cache|.terraform`).MatchString(path) {
if !regexp.MustCompile(`\.terragrunt-cache|\.terraform`).MatchString(path) {
// TODO: start from the codebase path instead of the relative path
absPath, err := filepath.Abs(path)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion helpers/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func RenderChecks(checkResults []data.Check, verbose bool) {
fmt.Printf("%s %s - %s\n", c.Status, c.ID, termlink.Link(c.Name, c.RelatedGuidelines))
if len(c.Errors) > 0 && verbose {
for _, err := range c.Errors {
fmt.Println(" - ", err)
fmt.Println(" -", err)
}
}
}
Expand Down

0 comments on commit c671c18

Please sign in to comment.