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..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,6 +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") + 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() { @@ -43,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..950f212 --- /dev/null +++ b/state/models.go @@ -0,0 +1,34 @@ +package state + +import "encoding/json" + +type stateV4 struct { + Version stateVersionV4 `json:"version"` + RootOutputs map[string]outputStateV4 `json:"outputs"` + Resources []resourceStateV4 `json:"resources"` +} + +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"` + Type string `json:"type"` + Name string `json:"name"` +} + +// 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 +}