From e786aa025710bca16ba251eeaa9d3d39b69e90f4 Mon Sep 17 00:00:00 2001 From: Dinesh B Date: Tue, 4 Jul 2023 22:02:30 +0530 Subject: [PATCH 1/3] Initial commit for summarzing the current terraform state Signed-off-by: Dinesh B --- example/.terraform.lock.hcl | 2 ++ main.go | 6 ++++++ summarizeState.go | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 summarizeState.go diff --git a/example/.terraform.lock.hcl b/example/.terraform.lock.hcl index b28c736..485328c 100644 --- a/example/.terraform.lock.hcl +++ b/example/.terraform.lock.hcl @@ -4,6 +4,7 @@ provider "registry.terraform.io/hashicorp/github" { version = "4.23.0" hashes = [ + "h1:C8mQRZrHenfru/LKN+qqwAI6aEIGbu9iWaWVmGFwcy4=", "h1:eylJFdBYJIxdyyElE1mV7cMcv2HjUYzv+pFZjg/aRLU=", "zh:003f67dcb506ea50b34acce92f575cd04560a21c57bb63de1c9b3874dda10337", "zh:1b9e77fb728e3d2c8d25d04ac613e7587714c63c54532ac96787b4d351b164de", @@ -25,6 +26,7 @@ provider "registry.terraform.io/integrations/github" { version = "4.23.0" constraints = "4.23.0" hashes = [ + "h1:C8mQRZrHenfru/LKN+qqwAI6aEIGbu9iWaWVmGFwcy4=", "h1:eylJFdBYJIxdyyElE1mV7cMcv2HjUYzv+pFZjg/aRLU=", "zh:003f67dcb506ea50b34acce92f575cd04560a21c57bb63de1c9b3874dda10337", "zh:1b9e77fb728e3d2c8d25d04ac613e7587714c63c54532ac96787b4d351b164de", diff --git a/main.go b/main.go index 5fc1815..0a06087 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ func main() { separateTree := flag.Bool("separate-tree", false, "[Optional] print changes in tree format for add/delete/change/recreate changes") drawable := flag.Bool("draw", false, "[Optional, used only with -tree or -separate-tree] draw trees instead of plain tree") md := flag.Bool("md", false, "[Optional, used only with table view] output table as markdown") + state := flag.Bool("state", false, "[Optional] represent input is of type terraform state. If not provided, input is considered as terraform plan") outputFileName := flag.String("out", "", "[Optional] write output to file") flag.Usage = func() { @@ -33,6 +34,11 @@ func main() { os.Exit(0) } + if *state { + summarizeState(*tree, *separateTree, *drawable, *md, *json, *outputFileName) + return + } + args := flag.Args() err := validateFlags(*tree, *separateTree, *drawable, *md, args) logIfErrorAndExit("invalid input flags: %s\n", err, flag.Usage) diff --git a/summarizeState.go b/summarizeState.go new file mode 100644 index 0000000..58a5821 --- /dev/null +++ b/summarizeState.go @@ -0,0 +1,4 @@ +package main + +func summarizeState(tree, separateTree, drawable, md, json bool, outputFileName string) { +} From 1f63ac83c3d833414d6e9c2b20131ceb361af4dd Mon Sep 17 00:00:00 2001 From: Dinesh B Date: Sun, 16 Jul 2023 19:21:57 +0530 Subject: [PATCH 2/3] Show terraform state in tree,md,drawable-tree,table format Signed-off-by: Dinesh B --- main.go | 17 ++++--- state/models.go | 72 ++++++++++++++++++++++++++ state/parser.go | 27 ++++++++++ state/summarize.go | 78 +++++++++++++++++++++++++++++ state/tree.go | 122 +++++++++++++++++++++++++++++++++++++++++++++ summarizeState.go | 4 -- 6 files changed, 310 insertions(+), 10 deletions(-) create mode 100644 state/models.go create mode 100644 state/parser.go create mode 100644 state/summarize.go create mode 100644 state/tree.go delete mode 100644 summarizeState.go diff --git a/main.go b/main.go index 0a06087..b35c986 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "github.com/dineshba/tf-summarize/parser" "github.com/dineshba/tf-summarize/reader" + "github.com/dineshba/tf-summarize/state" "github.com/dineshba/tf-summarize/writer" ) @@ -20,7 +21,7 @@ func main() { separateTree := flag.Bool("separate-tree", false, "[Optional] print changes in tree format for add/delete/change/recreate changes") drawable := flag.Bool("draw", false, "[Optional, used only with -tree or -separate-tree] draw trees instead of plain tree") md := flag.Bool("md", false, "[Optional, used only with table view] output table as markdown") - state := flag.Bool("state", false, "[Optional] represent input is of type terraform state. If not provided, input is considered as terraform plan") + isState := flag.Bool("state", false, "[Optional] represent input is of type terraform state. If not provided, input is considered as terraform plan") outputFileName := flag.String("out", "", "[Optional] write output to file") flag.Usage = func() { @@ -34,11 +35,6 @@ func main() { os.Exit(0) } - if *state { - summarizeState(*tree, *separateTree, *drawable, *md, *json, *outputFileName) - return - } - args := flag.Args() err := validateFlags(*tree, *separateTree, *drawable, *md, args) logIfErrorAndExit("invalid input flags: %s\n", err, flag.Usage) @@ -49,6 +45,15 @@ func main() { input, err := newReader.Read() logIfErrorAndExit("error reading from input: %s", err, func() {}) + if *isState { + parser, err := state.CreateParser(input, newReader.Name()) + logIfErrorAndExit("error creating parser: %s", err, func() {}) + terraformState, err := parser.Parse() + logIfErrorAndExit("%s", err, func() {}) + state.Summarize(*tree, *drawable, *md, *outputFileName, terraformState) + return + } + newParser, err := parser.CreateParser(input, newReader.Name()) logIfErrorAndExit("error creating parser: %s", err, func() {}) diff --git a/state/models.go b/state/models.go new file mode 100644 index 0000000..1fb2854 --- /dev/null +++ b/state/models.go @@ -0,0 +1,72 @@ +package state + +import "encoding/json" + +type stateV4 struct { + Version stateVersionV4 `json:"version"` + TerraformVersion string `json:"terraform_version"` + Serial uint64 `json:"serial"` + Lineage string `json:"lineage"` + RootOutputs map[string]outputStateV4 `json:"outputs"` + Resources []resourceStateV4 `json:"resources"` + CheckResults []checkResultsV4 `json:"check_results"` +} + +type outputStateV4 struct { + ValueRaw json.RawMessage `json:"value"` + ValueTypeRaw json.RawMessage `json:"type"` + Sensitive bool `json:"sensitive,omitempty"` +} + +type resourceStateV4 struct { + Module string `json:"module,omitempty"` + Mode string `json:"mode"` + Type string `json:"type"` + Name string `json:"name"` + EachMode string `json:"each,omitempty"` + ProviderConfig string `json:"provider"` + Instances []instanceObjectStateV4 `json:"instances"` +} + +type instanceObjectStateV4 struct { + IndexKey interface{} `json:"index_key,omitempty"` + Status string `json:"status,omitempty"` + Deposed string `json:"deposed,omitempty"` + + SchemaVersion uint64 `json:"schema_version"` + AttributesRaw json.RawMessage `json:"attributes,omitempty"` + AttributesFlat map[string]string `json:"attributes_flat,omitempty"` + AttributeSensitivePaths json.RawMessage `json:"sensitive_attributes,omitempty"` + + PrivateRaw []byte `json:"private,omitempty"` + + Dependencies []string `json:"dependencies,omitempty"` + + CreateBeforeDestroy bool `json:"create_before_destroy,omitempty"` +} + +type checkResultsV4 struct { + ObjectKind string `json:"object_kind"` + ConfigAddr string `json:"config_addr"` + Status string `json:"status"` + Objects []checkResultsObjectV4 `json:"objects"` +} + +type checkResultsObjectV4 struct { + ObjectAddr string `json:"object_addr"` + Status string `json:"status"` + FailureMessages []string `json:"failure_messages,omitempty"` +} + +// stateVersionV4 is a weird special type we use to produce our hard-coded +// "version": 4 in the JSON serialization. +type stateVersionV4 struct{} + +func (sv stateVersionV4) MarshalJSON() ([]byte, error) { + return []byte{'4'}, nil +} + +func (sv stateVersionV4) UnmarshalJSON([]byte) error { + // Nothing to do: we already know we're version 4 + return nil +} diff --git a/state/parser.go b/state/parser.go new file mode 100644 index 0000000..9752804 --- /dev/null +++ b/state/parser.go @@ -0,0 +1,27 @@ +package state + +import ( + "encoding/json" + "fmt" +) + +type StateParser interface { + Parse() (stateV4, error) +} + +func CreateParser(data []byte, fileName string) (StateParser, error) { + return DefaultStateParser{data: data}, nil +} + +type DefaultStateParser struct { + data []byte +} + +func (d DefaultStateParser) Parse() (stateV4, error) { + state := stateV4{} + err := json.Unmarshal(d.data, &state) + if err != nil { + return stateV4{}, fmt.Errorf("error when parsing input: %s", err.Error()) + } + return state, nil +} diff --git a/state/summarize.go b/state/summarize.go new file mode 100644 index 0000000..e2b5945 --- /dev/null +++ b/state/summarize.go @@ -0,0 +1,78 @@ +package state + +import ( + "fmt" + "io" + "os" + + "github.com/olekukonko/tablewriter" +) + +func Summarize(tree, drawable, md bool, outputFileName string, stateValue stateV4) error { + resources := make([]string, 0, len(stateValue.Resources)) + + for _, resource := range stateValue.Resources { + resources = append(resources, fmt.Sprintf("%s.%s.%s", resource.Module, resource.Type, resource.Name)) + } + + if tree { + tree := CreateTree(resources) + + if drawable { + fmt.Printf("%v\n", tree.DrawableTree()) + return nil + } + for _, t := range tree { + err := printTree(os.Stdout, t, "") + if err != nil { + return fmt.Errorf("error writing data to %s: %s", "stdout", err.Error()) + } + } + return nil + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Resources"}) + format := "%s" + if md { + format = "`%s`" + } + for _, resource := range resources { + table.Append([]string{fmt.Sprintf(format, resource)}) + } + + if md { + // Adding a println to break up the tables in md mode + fmt.Println() + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + } else { + table.SetRowLine(true) + } + + table.Render() + return nil +} + +func printTree(writer io.Writer, tree *Tree, prefixSpace string) error { + var err error + prefixSymbol := fmt.Sprintf("%s|---", prefixSpace) + // if tree.Value != nil { + // colorPrefix, suffix := tree.Value.ColorPrefixAndSuffixText() + // _, err = fmt.Fprintf(writer, "%s%s%s%s%s\n", prefixSymbol, colorPrefix, tree.Name, suffix, terraformstate.ColorReset) + // } else { + _, err = fmt.Fprintf(writer, "%s%s\n", prefixSymbol, tree.Name) + // } + if err != nil { + return fmt.Errorf("error writing data to %s: %s", writer, err.Error()) + } + + for _, c := range tree.Children { + separator := "|" + err = printTree(writer, c, fmt.Sprintf("%s%s\t", prefixSpace, separator)) + if err != nil { + return fmt.Errorf("error writing data to %s: %s", writer, err.Error()) + } + } + return nil +} diff --git a/state/tree.go b/state/tree.go new file mode 100644 index 0000000..4af3870 --- /dev/null +++ b/state/tree.go @@ -0,0 +1,122 @@ +package state + +import ( + "fmt" + "strings" + + "github.com/m1gwings/treedrawer/tree" +) + +type Tree struct { + Name string + level int + Children Trees +} + +func (t Tree) String() string { + return fmt.Sprintf("{name: %s, children: %+v}", t.Name, t.Children) +} + +type Trees []*Tree + +func (t Trees) DrawableTree() *tree.Tree { + newTree := tree.NewTree(tree.NodeString(".")) + for _, t1 := range t { + t1.AddChild(newTree) + } + return newTree +} + +func (t *Tree) AddChild(parent *tree.Tree) { + isLeafNode := len(t.Children) == 0 + + var childNode tree.NodeString + if isLeafNode { + // _, suffix := t.Value.ColorPrefixAndSuffixText() + childNode = tree.NodeString(fmt.Sprintf("%s%s", t.Name, "")) + } else { + childNode = tree.NodeString(t.Name) + } + + currentChildIndex := len(parent.Children()) + parent.AddChild(childNode) + currentTree, err := parent.Child(currentChildIndex) + for _, c := range t.Children { + if err != nil { + panic(err) + } + c.AddChild(currentTree) + } +} + +func (t Trees) String() string { + result := "" + for _, tree := range t { + result = fmt.Sprintf("%s,{name: %s, children: %+v}", result, tree.Name, tree.Children) + } + return strings.TrimPrefix(result, ",") +} + +func CreateTree(resources []string) Trees { + result := &Tree{Name: ".", Children: Trees{}, level: 0} + for _, r := range resources { + levels := splitResources(r) + createTreeMultiLevel(r, levels, result) + } + return result.Children +} + +func splitResources(address string) []string { + acc := make([]string, 0) + var resource strings.Builder + for i := 0; i < len(address); i++ { + currentIndex := string(address[i]) + + if currentIndex == "[" { + lastIndex := strings.Index(address[i:], "]") + resource.WriteString(address[i : i+lastIndex+1]) + i = i + lastIndex + continue + } + + if currentIndex == "." { + acc = append(acc, resource.String()) + resource = strings.Builder{} + continue + } + resource.Write([]byte{address[i]}) + } + acc = append(acc, resource.String()) + return acc +} + +func createTreeMultiLevel(r string, levels []string, currentTree *Tree) { + parentTree := currentTree + for i, name := range levels { + matchedTree := getTree(name, parentTree.Children) + if matchedTree == nil { + // var resourceChange *terraformstate.ResourceChange + if i+1 == len(levels) { + // resourceChange = &r + } + newTree := &Tree{ + Name: name, + // Value: r, + } + parentTree.Children = append(parentTree.Children, + newTree) + parentTree = newTree + } else { + parentTree = matchedTree + } + } +} + +func getTree(name string, siblings Trees) *Tree { + for _, s := range siblings { + if s.Name == name { + return s + } + } + return nil +} diff --git a/summarizeState.go b/summarizeState.go deleted file mode 100644 index 58a5821..0000000 --- a/summarizeState.go +++ /dev/null @@ -1,4 +0,0 @@ -package main - -func summarizeState(tree, separateTree, drawable, md, json bool, outputFileName string) { -} From 9ed23425c82605492e1cc192028521d3e6038e6b Mon Sep 17 00:00:00 2001 From: Dinesh B Date: Sun, 16 Jul 2023 19:24:19 +0530 Subject: [PATCH 3/3] Remove unused fields in the state Signed-off-by: Dinesh B --- state/models.go | 50 ++++++------------------------------------------- 1 file changed, 6 insertions(+), 44 deletions(-) diff --git a/state/models.go b/state/models.go index 1fb2854..950f212 100644 --- a/state/models.go +++ b/state/models.go @@ -3,13 +3,9 @@ package state import "encoding/json" type stateV4 struct { - Version stateVersionV4 `json:"version"` - TerraformVersion string `json:"terraform_version"` - Serial uint64 `json:"serial"` - Lineage string `json:"lineage"` - RootOutputs map[string]outputStateV4 `json:"outputs"` - Resources []resourceStateV4 `json:"resources"` - CheckResults []checkResultsV4 `json:"check_results"` + Version stateVersionV4 `json:"version"` + RootOutputs map[string]outputStateV4 `json:"outputs"` + Resources []resourceStateV4 `json:"resources"` } type outputStateV4 struct { @@ -19,43 +15,9 @@ type outputStateV4 struct { } type resourceStateV4 struct { - Module string `json:"module,omitempty"` - Mode string `json:"mode"` - Type string `json:"type"` - Name string `json:"name"` - EachMode string `json:"each,omitempty"` - ProviderConfig string `json:"provider"` - Instances []instanceObjectStateV4 `json:"instances"` -} - -type instanceObjectStateV4 struct { - IndexKey interface{} `json:"index_key,omitempty"` - Status string `json:"status,omitempty"` - Deposed string `json:"deposed,omitempty"` - - SchemaVersion uint64 `json:"schema_version"` - AttributesRaw json.RawMessage `json:"attributes,omitempty"` - AttributesFlat map[string]string `json:"attributes_flat,omitempty"` - AttributeSensitivePaths json.RawMessage `json:"sensitive_attributes,omitempty"` - - PrivateRaw []byte `json:"private,omitempty"` - - Dependencies []string `json:"dependencies,omitempty"` - - CreateBeforeDestroy bool `json:"create_before_destroy,omitempty"` -} - -type checkResultsV4 struct { - ObjectKind string `json:"object_kind"` - ConfigAddr string `json:"config_addr"` - Status string `json:"status"` - Objects []checkResultsObjectV4 `json:"objects"` -} - -type checkResultsObjectV4 struct { - ObjectAddr string `json:"object_addr"` - Status string `json:"status"` - FailureMessages []string `json:"failure_messages,omitempty"` + Module string `json:"module,omitempty"` + Type string `json:"type"` + Name string `json:"name"` } // stateVersionV4 is a weird special type we use to produce our hard-coded