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.
|
// display of sensitive values in user interfaces.
|
||||||
BeforeSensitive json.RawMessage `json:"before_sensitive,omitempty"`
|
BeforeSensitive json.RawMessage `json:"before_sensitive,omitempty"`
|
||||||
AfterSensitive json.RawMessage `json:"after_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 {
|
type output struct {
|
||||||
|
@ -257,6 +265,10 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
replacePaths, err := encodePaths(rc.RequiredReplace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
r.Change = change{
|
r.Change = change{
|
||||||
Actions: actionString(rc.Action.String()),
|
Actions: actionString(rc.Action.String()),
|
||||||
|
@ -265,6 +277,7 @@ func (p *plan) marshalResourceChanges(changes *plans.Changes, schemas *terraform
|
||||||
AfterUnknown: a,
|
AfterUnknown: a,
|
||||||
BeforeSensitive: json.RawMessage(beforeSensitive),
|
BeforeSensitive: json.RawMessage(beforeSensitive),
|
||||||
AfterSensitive: json.RawMessage(afterSensitive),
|
AfterSensitive: json.RawMessage(afterSensitive),
|
||||||
|
ReplacePaths: replacePaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
if rc.DeposedKey != states.NotDeposed {
|
if rc.DeposedKey != states.NotDeposed {
|
||||||
|
@ -625,3 +638,54 @@ func actionString(action string) []string {
|
||||||
return []string{action}
|
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
|
package jsonplan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"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
|
"id": true
|
||||||
},
|
},
|
||||||
"after_sensitive": {},
|
"after_sensitive": {},
|
||||||
"before_sensitive": {}
|
"before_sensitive": {},
|
||||||
|
"replace_paths": [["ami"]]
|
||||||
},
|
},
|
||||||
"action_reason": "replace_because_cannot_update"
|
"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
|
// e.g. just scan the list for "delete" to recognize all three situations
|
||||||
// where the object will be deleted, allowing for any new deletion
|
// where the object will be deleted, allowing for any new deletion
|
||||||
// combinations that might be added in future.
|
// combinations that might be added in future.
|
||||||
"actions": ["update"]
|
"actions": ["update"],
|
||||||
|
|
||||||
// "before" and "after" are representations of the object value both before
|
// "before" and "after" are representations of the object value both before
|
||||||
// and after the action. For ["create"] and ["delete"] actions, either
|
// 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
|
// after values are identical. The "after" value will be incomplete if there
|
||||||
// are values within it that won't be known until after apply.
|
// are values within it that won't be known until after apply.
|
||||||
"before": <value-representation>,
|
"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