Skip to content

Commit

Permalink
Merge pull request #22 from reproio/feat/support-moved-block
Browse files Browse the repository at this point in the history
support moved block
  • Loading branch information
takaishi authored Sep 25, 2023
2 parents b0a8400 + d7167fd commit 1b2381e
Show file tree
Hide file tree
Showing 12 changed files with 503 additions and 128 deletions.
4 changes: 2 additions & 2 deletions cmd/terraform-j2md/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
53 changes: 11 additions & 42 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,51 +1,20 @@
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=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
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=
64 changes: 64 additions & 0 deletions internal/terraform/moved_block_renderer.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
107 changes: 32 additions & 75 deletions internal/terraform/plan.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 -}}
<details><summary>Change details</summary>
{{ range .ResourceChanges }}
{{codeFence}}diff
# {{.Header}}
{{.GetUnifiedDiffString}}{{codeFence}}
{{.Render}}{{codeFence}}
{{end}}
</details>
{{end}}`
Expand All @@ -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 "````````"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand All @@ -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 != ""
}
Loading

0 comments on commit 1b2381e

Please sign in to comment.