Merge pull request #28608 from hashicorp/alisdair/json-plan-replace-paths
jsonplan: Add replace_paths
This commit is contained in:
commit
91cdde1d67
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,8 @@
|
|||
"id": true
|
||||
},
|
||||
"after_sensitive": {},
|
||||
"before_sensitive": {}
|
||||
"before_sensitive": {},
|
||||
"replace_paths": [["ami"]]
|
||||
},
|
||||
"action_reason": "replace_because_cannot_update"
|
||||
}
|
||||
|
|
|
@ -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"]]
|
||||
}
|
||||
```
|
||||
|
|
Loading…
Reference in New Issue