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 }