Skip to content

Commit

Permalink
[WIP] Feat: use terraform-json package (#60)
Browse files Browse the repository at this point in the history
* Feature: differentiate between (+/-) and (-/+) in output

* Apply linter suggestions

* First version that builds and works

* Remove old functions and cover another nil reference

* go mod tify; get tests running

* Remove old types

* Make names consistent

* Update go to v1.21

* Update gh actions to go v1.21

* Fix build

* Whoops

* Fix linter issues
  • Loading branch information
QuintenBruynseraede authored Jan 24, 2024
1 parent 4287c0d commit b74b13d
Show file tree
Hide file tree
Showing 14 changed files with 137 additions and 139 deletions.
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@ go 1.21

require (
github.com/fatih/color v1.15.0
github.com/hashicorp/terraform-json v0.20.0
github.com/m1gwings/treedrawer v0.3.3-beta
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249
github.com/olekukonko/tablewriter v0.0.5
github.com/stretchr/testify v1.7.0
)

require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/zclconf/go-cty v1.14.1 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.11.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
)
14 changes: 13 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/terraform-json v0.20.0 h1:cJcvn4gIOTi0SD7pIy+xiofV1zFA3hza+6K+fo52IX8=
github.com/hashicorp/terraform-json v0.20.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
github.com/m1gwings/treedrawer v0.3.3-beta h1:VeeQ4I90+NL0G2Tga3H4EY4hbOyVP3ID4T93r21oLbQ=
github.com/m1gwings/treedrawer v0.3.3-beta/go.mod h1:Sebh5tCtjQWAG/B9xWct163vB9pCbBcA1ykaUErDUTY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
Expand All @@ -20,9 +28,13 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA=
github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
3 changes: 2 additions & 1 deletion 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/terraformstate"
"github.com/dineshba/tf-summarize/writer"
)

Expand Down Expand Up @@ -49,7 +50,7 @@ func main() {
terraformState, err := newParser.Parse()
logIfErrorAndExit("%s", err, func() {})

terraformState.FilterNoOpResources()
terraformstate.FilterNoOpResources(&terraformState)

newWriter := writer.CreateWriter(*tree, *separateTree, *drawable, *md, *json, terraformState)

Expand Down
14 changes: 7 additions & 7 deletions parser/binary-parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,27 @@ import (
"fmt"
"os/exec"

"github.com/dineshba/tf-summarize/terraformstate"
tfjson "github.com/hashicorp/terraform-json"
)

type BinaryParser struct {
fileName string
}

func (j BinaryParser) Parse() (terraformstate.TerraformState, error) {
func (j BinaryParser) Parse() (tfjson.Plan, error) {
cmd := exec.Command("terraform", "show", "-json", j.fileName)
output, err := cmd.CombinedOutput()
if err != nil {
return terraformstate.TerraformState{}, fmt.Errorf(
return tfjson.Plan{}, fmt.Errorf(
"error when running 'terraform show -json %s': \n%s\n\n%s",
j.fileName, output, "Make sure you are running in terraform directory and terraform init is done")
}
ts := terraformstate.TerraformState{}
err = json.Unmarshal(output, &ts)
plan := tfjson.Plan{}
err = json.Unmarshal(output, &plan)
if err != nil {
return terraformstate.TerraformState{}, fmt.Errorf("error when parsing input: %s", err.Error())
return tfjson.Plan{}, fmt.Errorf("error when parsing input: %s", err.Error())
}
return ts, nil
return plan, nil
}

func NewBinaryParser(fileName string) Parser {
Expand Down
12 changes: 6 additions & 6 deletions parser/json-parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import (
"encoding/json"
"fmt"

"github.com/dineshba/tf-summarize/terraformstate"
tfjson "github.com/hashicorp/terraform-json"
)

type JSONParser struct {
data []byte
}

func (j JSONParser) Parse() (terraformstate.TerraformState, error) {
ts := terraformstate.TerraformState{}
err := json.Unmarshal(j.data, &ts)
func (j JSONParser) Parse() (tfjson.Plan, error) {
plan := tfjson.Plan{}
err := json.Unmarshal(j.data, &plan)
if err != nil {
return terraformstate.TerraformState{}, fmt.Errorf("error when parsing input: %s", err.Error())
return tfjson.Plan{}, fmt.Errorf("error when parsing input: %s", err.Error())
}
return ts, nil
return plan, nil
}

func NewJSONParser(data []byte) Parser {
Expand Down
4 changes: 2 additions & 2 deletions parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"strings"

"github.com/dineshba/tf-summarize/reader"
"github.com/dineshba/tf-summarize/terraformstate"
tfjson "github.com/hashicorp/terraform-json"
)

type Parser interface {
Parse() (terraformstate.TerraformState, error)
Parse() (tfjson.Plan, error)
}

func CreateParser(data []byte, fileName string) (Parser, error) {
Expand Down
96 changes: 34 additions & 62 deletions terraformstate/terraform_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package terraformstate
import (
"encoding/json"
"fmt"
"slices"

tfjson "github.com/hashicorp/terraform-json"
)

const ColorReset = "\033[0m"
Expand All @@ -13,75 +14,42 @@ const ColorMagenta = "\033[35m"
const ColorYellow = "\033[33m"
const ColorCyan = "\033[36m"

type ResourceChange struct {
Address string `json:"address"`
ModuleAddress string `json:"module_address"`
Mode string `json:"mode"`
Type string `json:"type"`
Name string `json:"name"`
ProviderName string `json:"provider_name"`
Change Change `json:"change"`
ActionReason string `json:"action_reason,omitempty"`
}

type Change struct {
Actions []string `json:"actions"`
Before json.RawMessage `json:"before,omitempty"`
After json.RawMessage `json:"after,omitempty"`
Importing Importing `json:"importing"`
}

type Importing struct {
ID string `json:"id"`
}

type OutputValues struct {
Actions []string `json:"actions"`
Before json.RawMessage `json:"before"`
After json.RawMessage `json:"after"`
}
type ResourceChanges = []*tfjson.ResourceChange //Type alias for brevity

func (rc ResourceChange) ColorPrefixAndSuffixText() (string, string) {
func GetColorPrefixAndSuffixText(rc *tfjson.ResourceChange) (string, string) {
var colorPrefix, suffix string
actions := rc.Change.Actions
if len(actions) == 1 && actions[0] != "no-op" {
if actions[0] == "create" {
actions := (*rc).Change.Actions
if len(actions) == 1 && !actions.NoOp() {
if actions.Create() {
colorPrefix = ColorGreen
suffix = "(+)"
} else if actions[0] == "delete" {
} else if actions.Delete() {
colorPrefix = ColorRed
suffix = "(-)"
} else {
colorPrefix = ColorYellow
suffix = "(~)"
}
} else if rc.Change.Importing.ID != "" {
} else if rc.Change.Importing != nil && rc.Change.Importing.ID != "" {
colorPrefix = ColorCyan
suffix = "(i)"
} else if slices.Equal(actions, []string{"delete", "create"}) {
} else if actions.DestroyBeforeCreate() {
colorPrefix = ColorMagenta
suffix = "(-/+)"
} else if slices.Equal(actions, []string{"create", "delete"}) {
} else if actions.CreateBeforeDestroy() {
colorPrefix = ColorMagenta
suffix = "(+/-)"
}
return colorPrefix, suffix
}

type ResourceChanges []ResourceChange

type TerraformState struct {
ResourceChanges ResourceChanges `json:"resource_changes"`
OutputChanges map[string]OutputValues `json:"output_changes"`
}

func Parse(input []byte) (TerraformState, error) {
ts := TerraformState{}
err := json.Unmarshal(input, &ts)
func Parse(input []byte) (tfjson.Plan, error) {
plan := tfjson.Plan{}
err := json.Unmarshal(input, &plan)
if err != nil {
return TerraformState{}, fmt.Errorf("error when parsing input: %s", err.Error())
return tfjson.Plan{}, fmt.Errorf("error when parsing input: %s", err.Error())
}
return ts, nil
return plan, nil
}

func addedResources(resources ResourceChanges) ResourceChanges {
Expand Down Expand Up @@ -109,6 +77,10 @@ func deletedResources(resources ResourceChanges) ResourceChanges {
func importedResources(resources ResourceChanges) ResourceChanges {
acc := make(ResourceChanges, 0)
for _, r := range resources {
if r.Change.Importing == nil {
continue
}

id := r.Change.Importing.ID
if id != "" {
acc = append(acc, r)
Expand All @@ -117,23 +89,23 @@ func importedResources(resources ResourceChanges) ResourceChanges {
return acc
}

func (ts *TerraformState) FilterNoOpResources() {
func FilterNoOpResources(ts *tfjson.Plan) {
acc := make(ResourceChanges, 0)
for _, r := range ts.ResourceChanges {
if len(r.Change.Actions) == 1 && r.Change.Actions[0] == "no-op" && r.Change.Importing.ID == "" {
if len(r.Change.Actions) == 1 && r.Change.Actions[0] == "no-op" && r.Change.Importing != nil && r.Change.Importing.ID == "" {
continue
}
acc = append(acc, r)
}
ts.ResourceChanges = acc
}

func (ts *TerraformState) AllResourceChanges() map[string]ResourceChanges {
addedResources := addedResources(ts.ResourceChanges)
deletedResources := deletedResources(ts.ResourceChanges)
updatedResources := updatedResources(ts.ResourceChanges)
recreatedResources := recreatedResources(ts.ResourceChanges)
importedResources := importedResources(ts.ResourceChanges)
func GetAllResourceChanges(plan tfjson.Plan) map[string]ResourceChanges {
addedResources := addedResources(plan.ResourceChanges)
deletedResources := deletedResources(plan.ResourceChanges)
updatedResources := updatedResources(plan.ResourceChanges)
recreatedResources := recreatedResources(plan.ResourceChanges)
importedResources := importedResources(plan.ResourceChanges)

return map[string]ResourceChanges{
"import": importedResources,
Expand All @@ -144,12 +116,12 @@ func (ts *TerraformState) AllResourceChanges() map[string]ResourceChanges {
}
}

func (ts *TerraformState) AllOutputChanges() map[string][]string {
func GetAllOutputChanges(plan tfjson.Plan) map[string][]string {
// create, update, and delete are the only available actions for outputChanges
// https://developer.hashicorp.com/terraform/internals/json-format
addedResources := filterOutputs(ts.OutputChanges, "create")
deletedResources := filterOutputs(ts.OutputChanges, "delete")
updatedResources := filterOutputs(ts.OutputChanges, "update")
addedResources := filterOutputs(plan.OutputChanges, "create")
deletedResources := filterOutputs(plan.OutputChanges, "delete")
updatedResources := filterOutputs(plan.OutputChanges, "update")

return map[string][]string{
"add": addedResources,
Expand All @@ -158,7 +130,7 @@ func (ts *TerraformState) AllOutputChanges() map[string][]string {
}
}

func filterResources(resources ResourceChanges, action string) ResourceChanges {
func filterResources(resources ResourceChanges, action tfjson.Action) ResourceChanges {
acc := make(ResourceChanges, 0)
for _, r := range resources {
if len(r.Change.Actions) == 1 && r.Change.Actions[0] == action {
Expand All @@ -168,7 +140,7 @@ func filterResources(resources ResourceChanges, action string) ResourceChanges {
return acc
}

func filterOutputs(outputChanges map[string]OutputValues, action string) []string {
func filterOutputs(outputChanges map[string]*tfjson.Change, action tfjson.Action) []string {
acc := make([]string, 0)
for k, v := range outputChanges {
if len(v.Actions) == 1 && v.Actions[0] == action {
Expand Down
Loading

0 comments on commit b74b13d

Please sign in to comment.