Skip to content
Merged
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
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{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Q] moved blockにはっこのimportantとされているattributeのみが差分として表示されるという理解であっているでしょうか。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

はい、その理解であっています。

"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