diff --git a/command/jsonplan/plan.go b/command/jsonplan/plan.go index eeb634255..1db2067d4 100644 --- a/command/jsonplan/plan.go +++ b/command/jsonplan/plan.go @@ -84,6 +84,14 @@ type change struct { // display of sensitive values in user interfaces. BeforeSensitive json.RawMessage `json:"before_sensitive,omitempty"` AfterSensitive json.RawMessage `json:"after_sensitive,omitempty"` + + // ReplacePaths is an array of arrays representing a set of paths into the + // object value which resulted in the action being "replace". This will be + // omitted if the action is not replace, or if no paths caused the + // replacement (for example, if the resource was tainted). Each path + // consists of one or more steps, each of which will be a number or a + // string. + ReplacePaths json.RawMessage `json:"replace_paths,omitempty"` } type output struct { @@ -257,6 +265,10 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform if err != nil { return err } + replacePaths, err := encodePaths(rc.RequiredReplace) + if err != nil { + return err + } r.Change = change{ Actions: actionString(rc.Action.String()), @@ -265,6 +277,7 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform AfterUnknown: a, BeforeSensitive: json.RawMessage(beforeSensitive), AfterSensitive: json.RawMessage(afterSensitive), + ReplacePaths: replacePaths, } if rc.DeposedKey != states.NotDeposed { @@ -625,3 +638,54 @@ func actionString(action string) []string { return []string{action} } } + +// encodePaths lossily encodes a cty.PathSet into an array of arrays of step +// values, such as: +// +// [["length"],["triggers",0,"value"]] +// +// The lossiness is that we cannot distinguish between an IndexStep with string +// key and a GetAttr step. This is fine with JSON output, because JSON's type +// system means that those two steps are equivalent anyway: both are object +// indexes. +// +// JavaScript (or similar dynamic language) consumers of these values can +// recursively apply the steps to a given object using an index operation for +// each step. +func encodePaths(pathSet cty.PathSet) (json.RawMessage, error) { + if pathSet.Empty() { + return nil, nil + } + + pathList := pathSet.List() + jsonPaths := make([]json.RawMessage, 0, len(pathList)) + + for _, path := range pathList { + steps := make([]json.RawMessage, 0, len(path)) + for _, step := range path { + switch s := step.(type) { + case cty.IndexStep: + key, err := ctyjson.Marshal(s.Key, s.Key.Type()) + if err != nil { + return nil, fmt.Errorf("Failed to marshal index step key %#v: %s", s.Key, err) + } + steps = append(steps, key) + case cty.GetAttrStep: + name, err := json.Marshal(s.Name) + if err != nil { + return nil, fmt.Errorf("Failed to marshal get attr step name %#v: %s", s.Name, err) + } + steps = append(steps, name) + default: + return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) + } + } + jsonPath, err := json.Marshal(steps) + if err != nil { + return nil, err + } + jsonPaths = append(jsonPaths, jsonPath) + } + + return json.Marshal(jsonPaths) +} diff --git a/command/jsonplan/plan_test.go b/command/jsonplan/plan_test.go index 9d43d1dc5..5656a6231 100644 --- a/command/jsonplan/plan_test.go +++ b/command/jsonplan/plan_test.go @@ -1,9 +1,11 @@ package jsonplan import ( + "encoding/json" "reflect" "testing" + "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" ) @@ -475,3 +477,43 @@ func TestSensitiveAsBool(t *testing.T) { } } } + +func TestEncodePaths(t *testing.T) { + tests := map[string]struct { + Input cty.PathSet + Want json.RawMessage + }{ + "empty set": { + cty.NewPathSet(), + json.RawMessage(nil), + }, + "index path with string and int steps": { + cty.NewPathSet(cty.IndexStringPath("boop").IndexInt(0)), + json.RawMessage(`[["boop",0]]`), + }, + "get attr path with one step": { + cty.NewPathSet(cty.GetAttrPath("triggers")), + json.RawMessage(`[["triggers"]]`), + }, + "multiple paths of different types": { + cty.NewPathSet( + cty.GetAttrPath("alpha").GetAttr("beta").GetAttr("gamma"), + cty.GetAttrPath("triggers").IndexString("name"), + cty.IndexIntPath(0).IndexInt(1).IndexInt(2).IndexInt(3), + ), + json.RawMessage(`[["alpha","beta","gamma"],["triggers","name"],[0,1,2,3]]`), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, err := encodePaths(test.Input) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if !cmp.Equal(got, test.Want) { + t.Errorf("wrong result:\n %v\n", cmp.Diff(got, test.Want)) + } + }) + } +} diff --git a/command/testdata/show-json/requires-replace/output.json b/command/testdata/show-json/requires-replace/output.json index b650e15a5..34dddcef2 100644 --- a/command/testdata/show-json/requires-replace/output.json +++ b/command/testdata/show-json/requires-replace/output.json @@ -40,7 +40,8 @@ "id": true }, "after_sensitive": {}, - "before_sensitive": {} + "before_sensitive": {}, + "replace_paths": [["ami"]] }, "action_reason": "replace_because_cannot_update" } diff --git a/website/docs/internals/json-format.html.md b/website/docs/internals/json-format.html.md index 52b080ef1..7741cd08a 100644 --- a/website/docs/internals/json-format.html.md +++ b/website/docs/internals/json-format.html.md @@ -490,7 +490,7 @@ A `` describes the change that will be made to the indica // e.g. just scan the list for "delete" to recognize all three situations // where the object will be deleted, allowing for any new deletion // combinations that might be added in future. - "actions": ["update"] + "actions": ["update"], // "before" and "after" are representations of the object value both before // and after the action. For ["create"] and ["delete"] actions, either @@ -498,6 +498,35 @@ A `` describes the change that will be made to the indica // after values are identical. The "after" value will be incomplete if there // are values within it that won't be known until after apply. "before": , - "after": + "after": , + + // "after_unknown" is an object value with similar structure to "after", but + // with all unknown leaf values replaced with "true", and all known leaf + // values omitted. This can be combined with "after" to reconstruct a full + // value after the action, including values which will only be known after + // apply. + "after_unknown": { + "id": true + }, + + // "before_sensitive" and "after_sensitive" are object values with similar + // structure to "before" and "after", but with all sensitive leaf values + // replaced with true, and all non-sensitive leaf values omitted. These + // objects should be combined with "before" and "after" to prevent accidental + // display of sensitive values in user interfaces. + "before_sensitive": {}, + "after_sensitive": { + "triggers": { + "boop": true + } + }, + + // "replace_paths" is an array of arrays representing a set of paths into the + // object value which resulted in the action being "replace". This will be + // omitted if the action is not replace, or if no paths caused the + // replacement (for example, if the resource was tainted). Each path + // consists of one or more steps, each of which will be a number or a + // string. + "replace_paths": [["triggers"]] } ```