Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Summarize current state of terraform #40

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions example/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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() {
Expand All @@ -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() {})

Expand Down
34 changes: 34 additions & 0 deletions state/models.go
Original file line number Diff line number Diff line change
@@ -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
}
27 changes: 27 additions & 0 deletions state/parser.go
Original file line number Diff line number Diff line change
@@ -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
}
78 changes: 78 additions & 0 deletions state/summarize.go
Original file line number Diff line number Diff line change
@@ -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
}
122 changes: 122 additions & 0 deletions state/tree.go
Original file line number Diff line number Diff line change
@@ -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
}