diff --git a/cmd/terraform-j2md/main.go b/cmd/terraform-j2md/main.go index 772f9f3..3f8deae 100644 --- a/cmd/terraform-j2md/main.go +++ b/cmd/terraform-j2md/main.go @@ -22,12 +22,12 @@ func main() { } func run() int { - planData, err := terraform.NewPlanData(os.Stdin) + planData, err := terraform.NewPlanData(os.Stdin, escapeHTML) if err != nil { fmt.Fprintf(os.Stderr, "cannot parse input as Terraform plan JSON: %v", err) return 1 } - if err = planData.Render(os.Stdout, escapeHTML); err != nil { + if err = planData.Render(os.Stdout); err != nil { fmt.Fprintf(os.Stderr, "cannot render: %v", err) return 1 } diff --git a/go.mod b/go.mod index 1e71984..1539407 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,15 @@ module github.com/reproio/terraform-j2md go 1.18 require ( - github.com/hashicorp/terraform-json v0.14.0 + github.com/hashicorp/terraform-json v0.17.2-0.20230912071934-9901d28699bc github.com/pmezard/go-difflib v1.0.0 ) require ( - github.com/hashicorp/go-version v1.5.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/zclconf/go-cty v1.10.0 // indirect - golang.org/x/text v0.3.5 // indirect + github.com/zclconf/go-cty v1.14.0 // indirect + golang.org/x/text v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index f3fb121..87da965 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,11 @@ -github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E= -github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s= -github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +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.17.2-0.20230912071934-9901d28699bc h1:ZtMfoibHiPAYJykA5nuHryaNoNDvfuREGWnIvukMb2Y= +github.com/hashicorp/terraform-json v0.17.2-0.20230912071934-9901d28699bc/go.mod h1:0a5tk65jPDbGo2lEMmvmwwvM0qCbOhW33hXtGrJQBgc= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= @@ -24,28 +13,8 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sebdah/goldie v1.0.0 h1:9GNhIat69MSlz/ndaBg48vl9dF5fI+NBB6kfOxgfkMc= -github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= -github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0= -github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc= +github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/internal/terraform/moved_block_renderer.go b/internal/terraform/moved_block_renderer.go new file mode 100644 index 0000000..8c9b57f --- /dev/null +++ b/internal/terraform/moved_block_renderer.go @@ -0,0 +1,64 @@ +package terraform + +import ( + "bytes" + "fmt" + tfjson "github.com/hashicorp/terraform-json" + "text/template" +) + +const movedBlockTemplateBody = `resource "{{.ResourceChange.Type}}" "{{.ResourceChange.Name}}" { +{{.Attributes -}} +} +` + +// These attributes are important (https://github.com/hashicorp/terraform/blob/v1.5.6/internal/command/jsonformat/computed/renderers/block.go#L19-L23) +var importantAttributes = []string{ + "id", + "name", + "tags", +} + +type MovedBlockRenderer struct { + ResourceChange *tfjson.ResourceChange +} + +func NewMovedBlockRenderer(resourceChange *tfjson.ResourceChange) *MovedBlockRenderer { + return &MovedBlockRenderer{ResourceChange: resourceChange} +} + +func (r *MovedBlockRenderer) Render() (string, error) { + var buff bytes.Buffer + t, err := template.New("plan").Parse(movedBlockTemplateBody) + if err != nil { + return "", fmt.Errorf("invalid template text: %w", err) + } + + if err := t.Execute(&buff, r); err != nil { + return "", fmt.Errorf("failed to render template: %w", err) + } + return buff.String(), nil +} + +func (r *MovedBlockRenderer) Header() string { + return fmt.Sprintf("%s has moved to %s", r.ResourceChange.PreviousAddress, r.ResourceChange.Address) +} + +func (r *MovedBlockRenderer) Attributes() string { + var buff bytes.Buffer + for _, attr := range importantAttributes { + if v, ok := r.ResourceChange.Change.After.(map[string]interface{})[attr]; ok { + buff.WriteString(fmt.Sprintf(" %-*s = %s\n", 2, attr, r.value(v))) + } + } + return buff.String() +} + +func (r *MovedBlockRenderer) value(v any) string { + switch v.(type) { + case string: + return fmt.Sprintf("%q", v) + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/internal/terraform/plan.go b/internal/terraform/plan.go index 0050539..2a500f6 100644 --- a/internal/terraform/plan.go +++ b/internal/terraform/plan.go @@ -1,17 +1,14 @@ package terraform import ( - "bytes" "encoding/json" "fmt" "github.com/hashicorp/terraform-json/sanitize" "github.com/reproio/terraform-j2md/internal/format" "io" - "strings" "text/template" tfjson "github.com/hashicorp/terraform-json" - "github.com/pmezard/go-difflib/difflib" ) const planTemplateBody = `### {{len .CreatedAddresses}} to add, {{len .UpdatedAddresses}} to change, {{len .DeletedAddresses}} to destroy, {{len .ReplacedAddresses}} to replace. @@ -31,12 +28,16 @@ const planTemplateBody = `### {{len .CreatedAddresses}} to add, {{len .UpdatedAd - replace{{ range .ReplacedAddresses }} - {{. -}} {{end}}{{end}} +{{- if .MovedAddresses}} +- moved{{ range .MovedAddresses }} + - {{. -}} +{{end}}{{end}} {{if .ResourceChanges -}}
Change details {{ range .ResourceChanges }} {{codeFence}}diff # {{.Header}} -{{.GetUnifiedDiffString}}{{codeFence}} +{{.Render}}{{codeFence}} {{end}}
{{end}}` @@ -46,87 +47,29 @@ type PlanData struct { UpdatedAddresses []string DeletedAddresses []string ReplacedAddresses []string + MovedAddresses []string ResourceChanges []ResourceChangeData } -type ResourceChangeData struct { - ResourceChange *tfjson.ResourceChange -} - -type Config struct { - EscapeHTML bool -} - -var config Config - -func (r ResourceChangeData) GetUnifiedDiffString() (string, error) { - before, err := r.marshalChangeBefore() - if err != nil { - return "", fmt.Errorf("invalid resource changes (before): %w", err) - } - after, err := r.marshalChangeAfter() - if err != nil { - return "", fmt.Errorf("invalid resource changes (after) : %w", err) - } - // Try to parse JSON string in values - replacer := strings.NewReplacer(`\n`, "\n ", `\"`, "\"") - diff := difflib.UnifiedDiff{ - A: difflib.SplitLines(replacer.Replace(string(before))), - B: difflib.SplitLines(replacer.Replace(string(after))), - Context: 3, - } - diffText, err := difflib.GetUnifiedDiffString(diff) - if err != nil { - return "", fmt.Errorf("failed to create diff: %w", err) - } - return diffText, nil -} -func (r ResourceChangeData) Header() string { - header := fmt.Sprintf("%s.%s %s", r.ResourceChange.Type, r.ResourceChange.Name, r.HeaderSuffix()) - - if r.ResourceChange.ModuleAddress == "" { - return header - } else { - return fmt.Sprintf("%s.%s", r.ResourceChange.ModuleAddress, header) - } +type ResourceChangeDataRenderer interface { + Render() (string, error) + Header() string } -func (r ResourceChangeData) marshalChangeBefore() ([]byte, error) { - return r.marshalChange(r.ResourceChange.Change.Before) +type ResourceChangeData struct { + ResourceChange *tfjson.ResourceChange + Renderer ResourceChangeDataRenderer } -func (r ResourceChangeData) marshalChangeAfter() ([]byte, error) { - return r.marshalChange(r.ResourceChange.Change.After) +func (r ResourceChangeData) Render() (string, error) { + return r.Renderer.Render() } -func (r ResourceChangeData) marshalChange(v any) ([]byte, error) { - var buffer bytes.Buffer - enc := json.NewEncoder(&buffer) - enc.SetIndent("", " ") - enc.SetEscapeHTML(config.EscapeHTML) - err := enc.Encode(v) - if err != nil { - return nil, err - } - return buffer.Bytes(), nil -} - -func (r ResourceChangeData) HeaderSuffix() string { - switch { - case r.ResourceChange.Change.Actions.Create(): - return "will be created" - case r.ResourceChange.Change.Actions.Update(): - return "will be updated in-place" - case r.ResourceChange.Change.Actions.Delete(): - return "will be destroyed" - case r.ResourceChange.Change.Actions.Replace(): - return "will be replaced" - } - return "" +func (r ResourceChangeData) Header() string { + return r.Renderer.Header() } -func (plan *PlanData) Render(w io.Writer, escapeHTML bool) error { - config.EscapeHTML = escapeHTML +func (plan *PlanData) Render(w io.Writer) error { funcMap := template.FuncMap{ "codeFence": func() string { return "````````" @@ -166,7 +109,7 @@ func processPlan(plan *tfjson.Plan) (*tfjson.Plan, error) { return plan, nil } -func NewPlanData(input io.Reader) (*PlanData, error) { +func NewPlanData(input io.Reader, escapeHTML bool) (*PlanData, error) { var err error var plan tfjson.Plan if err := json.NewDecoder(input).Decode(&plan); err != nil { @@ -180,6 +123,15 @@ func NewPlanData(input io.Reader) (*PlanData, error) { planData := PlanData{} for _, c := range processedPlan.ResourceChanges { + if isMovedBlock(c) { + planData.MovedAddresses = append(planData.MovedAddresses, fmt.Sprintf("%s (from %s)", c.Address, c.PreviousAddress)) + planData.ResourceChanges = append(planData.ResourceChanges, ResourceChangeData{ + ResourceChange: c, + Renderer: NewMovedBlockRenderer(c), + }) + continue + } + if c.Change.Actions.NoOp() || c.Change.Actions.Read() { continue } @@ -196,7 +148,12 @@ func NewPlanData(input io.Reader) (*PlanData, error) { } planData.ResourceChanges = append(planData.ResourceChanges, ResourceChangeData{ ResourceChange: c, + Renderer: NewUnifiedDiffRenderer(c, escapeHTML), }) } return &planData, nil } + +func isMovedBlock(rc *tfjson.ResourceChange) bool { + return rc.Change.Actions.NoOp() && rc.PreviousAddress != "" +} diff --git a/internal/terraform/unified_diff_renderer.go b/internal/terraform/unified_diff_renderer.go new file mode 100644 index 0000000..293511c --- /dev/null +++ b/internal/terraform/unified_diff_renderer.go @@ -0,0 +1,87 @@ +package terraform + +import ( + "bytes" + "encoding/json" + "fmt" + tfjson "github.com/hashicorp/terraform-json" + "github.com/pmezard/go-difflib/difflib" + "strings" +) + +type UnifiedDiffRenderer struct { + ResourceChange *tfjson.ResourceChange + EnableEscapeHTML bool +} + +func NewUnifiedDiffRenderer(resourceChange *tfjson.ResourceChange, enableEscapeHTML bool) *UnifiedDiffRenderer { + return &UnifiedDiffRenderer{ResourceChange: resourceChange, EnableEscapeHTML: enableEscapeHTML} +} + +func (r *UnifiedDiffRenderer) Render() (string, error) { + before, err := r.marshalChangeBefore() + if err != nil { + return "", fmt.Errorf("invalid resource changes (before): %w", err) + } + after, err := r.marshalChangeAfter() + if err != nil { + return "", fmt.Errorf("invalid resource changes (after) : %w", err) + } + // Try to parse JSON string in values + replacer := strings.NewReplacer(`\n`, "\n ", `\"`, "\"") + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(replacer.Replace(string(before))), + B: difflib.SplitLines(replacer.Replace(string(after))), + Context: 3, + } + diffText, err := difflib.GetUnifiedDiffString(diff) + if err != nil { + return "", fmt.Errorf("failed to create diff: %w", err) + } + + return diffText, nil +} + +func (r *UnifiedDiffRenderer) Header() string { + header := fmt.Sprintf("%s.%s %s", r.ResourceChange.Type, r.ResourceChange.Name, r.headerSuffix()) + + if r.ResourceChange.ModuleAddress == "" { + return header + } else { + return fmt.Sprintf("%s.%s", r.ResourceChange.ModuleAddress, header) + } +} + +func (r *UnifiedDiffRenderer) headerSuffix() string { + switch { + case r.ResourceChange.Change.Actions.Create(): + return "will be created" + case r.ResourceChange.Change.Actions.Update(): + return "will be updated in-place" + case r.ResourceChange.Change.Actions.Delete(): + return "will be destroyed" + case r.ResourceChange.Change.Actions.Replace(): + return "will be replaced" + } + return "" +} + +func (r *UnifiedDiffRenderer) marshalChangeBefore() ([]byte, error) { + return r.marshalChange(r.ResourceChange.Change.Before) +} + +func (r *UnifiedDiffRenderer) marshalChangeAfter() ([]byte, error) { + return r.marshalChange(r.ResourceChange.Change.After) +} + +func (r *UnifiedDiffRenderer) marshalChange(v any) ([]byte, error) { + var buffer bytes.Buffer + enc := json.NewEncoder(&buffer) + enc.SetIndent("", " ") + enc.SetEscapeHTML(r.EnableEscapeHTML) + err := enc.Encode(v) + if err != nil { + return nil, err + } + return buffer.Bytes(), nil +} diff --git a/test/plan_test/plan_test.go b/test/plan_test/plan_test.go index aa7fc11..e937b66 100644 --- a/test/plan_test/plan_test.go +++ b/test/plan_test/plan_test.go @@ -38,7 +38,7 @@ func Test_newPlanData(t *testing.T) { } defer file.Close() - _, err = terraform.NewPlanData(file) + _, err = terraform.NewPlanData(file, false) if (err != nil) != tt.wantErr { t.Errorf("NewPlanData() error = %v, wantErr %v", err, tt.wantErr) return @@ -64,6 +64,7 @@ func Test_render(t *testing.T) { {name: "include_code_fence", wantErr: false}, {name: "include_module", wantErr: false}, {name: "known_after_apply", wantErr: false}, + {name: "moved_block", wantErr: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -75,14 +76,14 @@ func Test_render(t *testing.T) { } defer file.Close() - plan, err := terraform.NewPlanData(file) + plan, err := terraform.NewPlanData(file, true) if err != nil { t.Errorf("cannot parse JSON as plan: %v", err) return } got := bytes.Buffer{} - err = plan.Render(&got, true) + err = plan.Render(&got) if (err != nil) != tt.wantErr { t.Errorf("render() error = %v, wantErr %v", err, tt.wantErr) return @@ -119,14 +120,14 @@ func Test_render(t *testing.T) { } defer file.Close() - plan, err := terraform.NewPlanData(file) + plan, err := terraform.NewPlanData(file, false) if err != nil { t.Errorf("cannot parse JSON as plan: %v", err) return } got := bytes.Buffer{} - err = plan.Render(&got, false) + err = plan.Render(&got) if (err != nil) != tt.wantErr { t.Errorf("render() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/test/testdata/moved_block/expected.md b/test/testdata/moved_block/expected.md new file mode 100644 index 0000000..8e8ae76 --- /dev/null +++ b/test/testdata/moved_block/expected.md @@ -0,0 +1,13 @@ +### 0 to add, 0 to change, 0 to destroy, 0 to replace. +- moved + - random_id.test2 (from random_id.test) +
Change details + +````````diff +# random_id.test has moved to random_id.test2 +resource "random_id" "test2" { + id = "qD4MEwtJeTOwqg" +} +```````` + +
diff --git a/test/testdata/moved_block/main.tf b/test/testdata/moved_block/main.tf new file mode 100644 index 0000000..d9271e5 --- /dev/null +++ b/test/testdata/moved_block/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + env = { + source = "tchupp/env" + version = "0.0.2" + } + } +} + +provider "env" { + # Configuration options +} + +resource "env_variable" "test" { + name = random_id.test2.hex +} + +moved { + from = random_id.test + to = random_id.test2 +} + +resource "random_id" "test2" { + byte_length = 10 +} diff --git a/test/testdata/moved_block/plan.tfplan b/test/testdata/moved_block/plan.tfplan new file mode 100644 index 0000000..8271099 Binary files /dev/null and b/test/testdata/moved_block/plan.tfplan differ diff --git a/test/testdata/moved_block/show.json b/test/testdata/moved_block/show.json new file mode 100644 index 0000000..4293a12 --- /dev/null +++ b/test/testdata/moved_block/show.json @@ -0,0 +1,205 @@ +{ + "format_version": "1.2", + "terraform_version": "1.5.3", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "env_variable.test", + "mode": "managed", + "type": "env_variable", + "name": "test", + "provider_name": "registry.terraform.io/tchupp/env", + "schema_version": 0, + "values": { + "id": "a83e0c130b497933b0aa", + "name": "a83e0c130b497933b0aa", + "value": "" + }, + "sensitive_values": {} + }, + { + "address": "random_id.test2", + "mode": "managed", + "type": "random_id", + "name": "test2", + "provider_name": "registry.terraform.io/hashicorp/random", + "schema_version": 0, + "values": { + "b64_std": "qD4MEwtJeTOwqg==", + "b64_url": "qD4MEwtJeTOwqg", + "byte_length": 10, + "dec": "794502137306233594687658", + "hex": "a83e0c130b497933b0aa", + "id": "qD4MEwtJeTOwqg", + "keepers": null, + "prefix": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "env_variable.test", + "mode": "managed", + "type": "env_variable", + "name": "test", + "provider_name": "registry.terraform.io/tchupp/env", + "change": { + "actions": [ + "no-op" + ], + "before": { + "id": "a83e0c130b497933b0aa", + "name": "a83e0c130b497933b0aa", + "value": "" + }, + "after": { + "id": "a83e0c130b497933b0aa", + "name": "a83e0c130b497933b0aa", + "value": "" + }, + "after_unknown": {}, + "before_sensitive": { + "value": true + }, + "after_sensitive": { + "value": true + } + } + }, + { + "address": "random_id.test2", + "previous_address": "random_id.test", + "mode": "managed", + "type": "random_id", + "name": "test2", + "provider_name": "registry.terraform.io/hashicorp/random", + "change": { + "actions": [ + "no-op" + ], + "before": { + "b64_std": "qD4MEwtJeTOwqg==", + "b64_url": "qD4MEwtJeTOwqg", + "byte_length": 10, + "dec": "794502137306233594687658", + "hex": "a83e0c130b497933b0aa", + "id": "qD4MEwtJeTOwqg", + "keepers": null, + "prefix": null + }, + "after": { + "b64_std": "qD4MEwtJeTOwqg==", + "b64_url": "qD4MEwtJeTOwqg", + "byte_length": 10, + "dec": "794502137306233594687658", + "hex": "a83e0c130b497933b0aa", + "id": "qD4MEwtJeTOwqg", + "keepers": null, + "prefix": null + }, + "after_unknown": {}, + "before_sensitive": {}, + "after_sensitive": {} + } + } + ], + "prior_state": { + "format_version": "1.0", + "terraform_version": "1.5.3", + "values": { + "root_module": { + "resources": [ + { + "address": "env_variable.test", + "mode": "managed", + "type": "env_variable", + "name": "test", + "provider_name": "registry.terraform.io/tchupp/env", + "schema_version": 0, + "values": { + "id": "a83e0c130b497933b0aa", + "name": "a83e0c130b497933b0aa", + "value": "" + }, + "sensitive_values": { + "value": true + }, + "depends_on": [ + "random_id.test2" + ] + }, + { + "address": "random_id.test2", + "mode": "managed", + "type": "random_id", + "name": "test2", + "provider_name": "registry.terraform.io/hashicorp/random", + "schema_version": 0, + "values": { + "b64_std": "qD4MEwtJeTOwqg==", + "b64_url": "qD4MEwtJeTOwqg", + "byte_length": 10, + "dec": "794502137306233594687658", + "hex": "a83e0c130b497933b0aa", + "id": "qD4MEwtJeTOwqg", + "keepers": null, + "prefix": null + }, + "sensitive_values": {} + } + ] + } + } + }, + "configuration": { + "provider_config": { + "env": { + "name": "env", + "full_name": "registry.terraform.io/tchupp/env", + "version_constraint": "0.0.2" + }, + "random": { + "name": "random", + "full_name": "registry.terraform.io/hashicorp/random" + } + }, + "root_module": { + "resources": [ + { + "address": "env_variable.test", + "mode": "managed", + "type": "env_variable", + "name": "test", + "provider_config_key": "env", + "expressions": { + "name": { + "references": [ + "random_id.test2.hex", + "random_id.test2" + ] + } + }, + "schema_version": 0 + }, + { + "address": "random_id.test2", + "mode": "managed", + "type": "random_id", + "name": "test2", + "provider_config_key": "random", + "expressions": { + "byte_length": { + "constant_value": 10 + } + }, + "schema_version": 0 + } + ] + } + }, + "timestamp": "2023-08-29T08:27:04Z" +} diff --git a/test/testdata/moved_block/terraform.tfstate b/test/testdata/moved_block/terraform.tfstate new file mode 100644 index 0000000..d6a893b --- /dev/null +++ b/test/testdata/moved_block/terraform.tfstate @@ -0,0 +1,53 @@ +{ + "version": 4, + "terraform_version": "1.5.3", + "serial": 3, + "lineage": "dc23aa52-bb4e-e4f3-e6e1-fdd58d30b617", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "env_variable", + "name": "test", + "provider": "provider[\"registry.terraform.io/tchupp/env\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "a83e0c130b497933b0aa", + "name": "a83e0c130b497933b0aa", + "value": "" + }, + "sensitive_attributes": [], + "private": "bnVsbA==", + "dependencies": [ + "random_id.test" + ] + } + ] + }, + { + "mode": "managed", + "type": "random_id", + "name": "test", + "provider": "provider[\"registry.terraform.io/hashicorp/random\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "b64_std": "qD4MEwtJeTOwqg==", + "b64_url": "qD4MEwtJeTOwqg", + "byte_length": 10, + "dec": "794502137306233594687658", + "hex": "a83e0c130b497933b0aa", + "id": "qD4MEwtJeTOwqg", + "keepers": null, + "prefix": null + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +}