github.com/diggerhq/digger/libs@v0.0.0-20240604170430-9d61cdf01cc5/terraform_utils/plan_summary.go (about)

     1  package terraform_utils
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"sort"
     8  
     9  	"github.com/dineshba/tf-summarize/terraformstate"
    10  	"github.com/dineshba/tf-summarize/writer"
    11  	tfjson "github.com/hashicorp/terraform-json"
    12  
    13  	"github.com/samber/lo"
    14  )
    15  
    16  type PlanSummary struct {
    17  	ResourcesCreated uint `json:"resources_created"`
    18  	ResourcesUpdated uint `json:"resources_updated"`
    19  	ResourcesDeleted uint `json:"resources_deleted"`
    20  }
    21  
    22  type TerraformPlan struct {
    23  	ResourceChanges []ResourceChange `json:"resource_changes"`
    24  }
    25  
    26  type ResourceChange struct {
    27  	Name       string `json:"name"`
    28  	Address    string `json:"address"`
    29  	Type       string `json:"type"`
    30  	Change     Change `json:"change"`
    31  	ChangeType string `json:"change_type"`
    32  }
    33  
    34  type Change struct {
    35  	Actions []string `json:"actions"`
    36  }
    37  
    38  // TerraformPlanFootprint represents a derivation of a terraform plan json that has
    39  // any sensitive data stripped out. Used for performing operations such
    40  // as plan similarity check
    41  type TerraformPlanFootprint struct {
    42  	Addresses []string `json:"addresses"`
    43  }
    44  
    45  func (f *TerraformPlanFootprint) ToJson() map[string]interface{} {
    46  	if f == nil {
    47  		return map[string]interface{}{}
    48  	}
    49  	return map[string]interface{}{
    50  		"addresses": f.Addresses,
    51  	}
    52  }
    53  
    54  func (footprint TerraformPlanFootprint) hash() string {
    55  	addresses := make([]string, len(footprint.Addresses))
    56  	copy(addresses, footprint.Addresses)
    57  	sort.Strings(addresses)
    58  	// concatenate all the addreses after sorting to form the hash
    59  	return lo.Reduce(addresses, func(a string, b string, i int) string {
    60  		return a + b
    61  	}, "")
    62  }
    63  
    64  func (p *PlanSummary) ToJson() map[string]interface{} {
    65  	if p == nil {
    66  		return map[string]interface{}{}
    67  	}
    68  	return map[string]interface{}{
    69  		"resources_created": p.ResourcesCreated,
    70  		"resources_updated": p.ResourcesUpdated,
    71  		"resources_deleted": p.ResourcesDeleted,
    72  	}
    73  }
    74  func parseTerraformPlanOutput(terraformJson string) (*TerraformPlan, error) {
    75  	var plan TerraformPlan
    76  	if err := json.Unmarshal([]byte(terraformJson), &plan); err != nil {
    77  		return nil, fmt.Errorf("Unable to parse the plan file: %v", err)
    78  	}
    79  
    80  	return &plan, nil
    81  }
    82  
    83  func GetPlanSummary(planJson string) (bool, *PlanSummary, error) {
    84  	tfplan, err := parseTerraformPlanOutput(planJson)
    85  	if err != nil {
    86  		return false, nil, fmt.Errorf("Error while parsing json file: %v", err)
    87  	}
    88  	isPlanEmpty := true
    89  	for _, change := range tfplan.ResourceChanges {
    90  		if len(change.Change.Actions) != 1 || change.Change.Actions[0] != "no-op" {
    91  			isPlanEmpty = false
    92  			break
    93  		}
    94  	}
    95  
    96  	planSummary := PlanSummary{}
    97  	for _, resourceChange := range tfplan.ResourceChanges {
    98  		switch resourceChange.Change.Actions[0] {
    99  		case "create":
   100  			planSummary.ResourcesCreated++
   101  		case "delete":
   102  			planSummary.ResourcesDeleted++
   103  		case "update":
   104  			planSummary.ResourcesUpdated++
   105  		}
   106  	}
   107  	return isPlanEmpty, &planSummary, nil
   108  }
   109  
   110  func GetPlanFootprint(planJson string) (*TerraformPlanFootprint, error) {
   111  	tfplan, err := parseTerraformPlanOutput(planJson)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  	planAddresses := lo.Map[ResourceChange, string](tfplan.ResourceChanges, func(change ResourceChange, idx int) string {
   116  		return change.Address
   117  	})
   118  	footprint := TerraformPlanFootprint{
   119  		Addresses: planAddresses,
   120  	}
   121  	return &footprint, nil
   122  }
   123  
   124  func PerformPlanSimilarityCheck(footprint1 TerraformPlanFootprint, footprint2 TerraformPlanFootprint) (bool, error) {
   125  	return footprint1.hash() == footprint2.hash(), nil
   126  }
   127  
   128  func SimilarityCheck(footprints []TerraformPlanFootprint) (bool, error) {
   129  	if len(footprints) < 2 {
   130  		return true, nil
   131  	}
   132  	footprintHashes := lo.Map(footprints, func(footprint TerraformPlanFootprint, i int) string {
   133  		return footprint.hash()
   134  	})
   135  	allSimilar := lo.EveryBy(footprintHashes, func(footprint string) bool {
   136  		return footprint == footprintHashes[0]
   137  	})
   138  	return allSimilar, nil
   139  
   140  }
   141  
   142  func GetTfSummarizePlan(planJson string) (string, error) {
   143  	plan := tfjson.Plan{}
   144  	err := json.Unmarshal([]byte(planJson), &plan)
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  
   149  	buf := new(bytes.Buffer)
   150  	w := writer.NewTableWriter(terraformstate.GetAllResourceChanges(plan), terraformstate.GetAllOutputChanges(plan), true)
   151  	w.Write(buf)
   152  
   153  	return buf.String(), nil
   154  }