Merge pull request #28608 from hashicorp/alisdair/json-plan-replace-paths

jsonplan: Add replace_paths
This commit is contained in:
Alisdair McDiarmid 2021-05-06 08:23:09 -04:00 committed by GitHub
commit 91cdde1d67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 139 additions and 3 deletions

View File

@ -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)
}

View File

@ -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))
}
})
}
}

View File

@ -40,7 +40,8 @@
"id": true
},
"after_sensitive": {},
"before_sensitive": {}
"before_sensitive": {},
"replace_paths": [["ami"]]
},
"action_reason": "replace_because_cannot_update"
}

View File

@ -490,7 +490,7 @@ A `<change-representation>` 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 `<change-representation>` 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": <value-representation>,
"after": <value-representation>
"after": <value-representation>,
// "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"]]
}
```