mirror of https://github.com/pulumi/pulumi.git
232 lines
7.5 KiB
Go
232 lines
7.5 KiB
Go
package display
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/pulumi/pulumi/pkg/engine"
|
|
"github.com/pulumi/pulumi/pkg/resource"
|
|
"github.com/pulumi/pulumi/pkg/resource/plugin"
|
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
|
)
|
|
|
|
func parseDiffPath(path string) ([]interface{}, error) {
|
|
// Complete paths obey the following EBNF-ish grammar:
|
|
//
|
|
// propertyName := [a-zA-Z_$] { [a-zA-Z0-9_$] }
|
|
// quotedPropertyName := '"' ( '\' '"' | [^"] ) { ( '\' '"' | [^"] ) } '"'
|
|
// arrayIndex := { [0-9] }
|
|
//
|
|
// propertyIndex := '[' ( quotedPropertyName | arrayIndex ) ']'
|
|
// rootProperty := ( propertyName | propertyIndex )
|
|
// propertyAccessor := ( ( '.' propertyName ) | propertyIndex )
|
|
// path := rootProperty { propertyAccessor }
|
|
//
|
|
// We interpret this a little loosely in order to keep things simple. Specifically, we will accept something close
|
|
// to the following:
|
|
// pathElement := { '.' } ( '[' ( [0-9]+ | '"' ('\' '"' | [^"] )+ '"' ']' | [a-zA-Z_$][a-zA-Z0-9_$] )
|
|
// path := { pathElement }
|
|
|
|
var elements []interface{}
|
|
for len(path) > 0 {
|
|
switch path[0] {
|
|
case '.':
|
|
path = path[1:]
|
|
case '[':
|
|
// If the character following the '[' is a '"', parse a string key.
|
|
var pathElement interface{}
|
|
if path[1] == '"' {
|
|
var propertyKey []byte
|
|
var i int
|
|
for i = 2; ; {
|
|
if i == len(path) {
|
|
return nil, errors.New("missing closing quote in property name")
|
|
} else if path[i] == '"' {
|
|
i++
|
|
break
|
|
} else if path[i] == '\\' && i+1 < len(path) && path[i+1] == '"' {
|
|
propertyKey = append(propertyKey, '"')
|
|
i += 2
|
|
} else {
|
|
propertyKey = append(propertyKey, path[i])
|
|
i++
|
|
}
|
|
}
|
|
if i == len(path) || path[i] != ']' {
|
|
return nil, errors.New("missing closing bracket in property access")
|
|
}
|
|
pathElement, path = string(propertyKey), path[i:]
|
|
} else {
|
|
// Look for a closing ']'
|
|
rbracket := strings.IndexRune(path, ']')
|
|
if rbracket == -1 {
|
|
return nil, errors.New("missing closing bracket in array index")
|
|
}
|
|
|
|
index, err := strconv.ParseInt(path[1:rbracket], 10, 0)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "invalid array index")
|
|
}
|
|
pathElement, path = int(index), path[rbracket:]
|
|
}
|
|
elements, path = append(elements, pathElement), path[1:]
|
|
default:
|
|
for i := 0; ; i++ {
|
|
if i == len(path) || path[i] == '.' || path[i] == '[' {
|
|
elements, path = append(elements, path[:i]), path[i:]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return elements, nil
|
|
}
|
|
|
|
// getProperty fetches the child property with the indicated key from the given property value. If the key does not
|
|
// exist, it returns an empty `PropertyValue`.
|
|
func getProperty(key interface{}, v resource.PropertyValue) resource.PropertyValue {
|
|
switch {
|
|
case v.IsArray():
|
|
index, ok := key.(int)
|
|
if !ok || index < 0 || index >= len(v.ArrayValue()) {
|
|
return resource.PropertyValue{}
|
|
}
|
|
return v.ArrayValue()[index]
|
|
case v.IsObject():
|
|
k, ok := key.(string)
|
|
if !ok {
|
|
return resource.PropertyValue{}
|
|
}
|
|
return v.ObjectValue()[resource.PropertyKey(k)]
|
|
case v.IsComputed() || v.IsOutput() || v.IsSecret():
|
|
// We consider the contents of these values opaque and return them as-is, as we cannot know whether or not the
|
|
// value will or does contain an element with the given key.
|
|
return v
|
|
default:
|
|
return resource.PropertyValue{}
|
|
}
|
|
}
|
|
|
|
// addDiff inserts a diff of the given kind at the given path into the parent ValueDiff.
|
|
//
|
|
// If the path consists of a single element, a diff of the indicated kind is inserted directly. Otherwise, if the
|
|
// property named by the first element of the path exists in both parents, we snip off the first element of the path
|
|
// and recurse into the property itself. If the property does not exist in one parent or the other, the diff kind is
|
|
// disregarded and the change is treated as either an Add or a Delete.
|
|
func addDiff(path []interface{}, kind plugin.DiffKind, parent *resource.ValueDiff,
|
|
oldParent, newParent resource.PropertyValue) {
|
|
|
|
contract.Require(len(path) > 0, "len(path) > 0")
|
|
|
|
element := path[0]
|
|
|
|
old, new := getProperty(element, oldParent), getProperty(element, newParent)
|
|
|
|
switch element := element.(type) {
|
|
case int:
|
|
if parent.Array == nil {
|
|
parent.Array = &resource.ArrayDiff{
|
|
Adds: make(map[int]resource.PropertyValue),
|
|
Deletes: make(map[int]resource.PropertyValue),
|
|
Sames: make(map[int]resource.PropertyValue),
|
|
Updates: make(map[int]resource.ValueDiff),
|
|
}
|
|
}
|
|
|
|
// For leaf diffs, the provider tells us exactly what to record. For other diffs, we will derive the
|
|
// difference from the old and new property values.
|
|
if len(path) == 1 {
|
|
switch kind {
|
|
case plugin.DiffAdd, plugin.DiffAddReplace:
|
|
parent.Array.Adds[element] = new
|
|
case plugin.DiffDelete, plugin.DiffDeleteReplace:
|
|
parent.Array.Deletes[element] = old
|
|
case plugin.DiffUpdate, plugin.DiffUpdateReplace:
|
|
valueDiff := resource.ValueDiff{Old: old, New: new}
|
|
if d := old.Diff(new); d != nil {
|
|
valueDiff = *d
|
|
}
|
|
parent.Array.Updates[element] = valueDiff
|
|
default:
|
|
contract.Failf("unexpected diff kind %v", kind)
|
|
}
|
|
} else {
|
|
switch {
|
|
case old.IsNull() && !new.IsNull():
|
|
parent.Array.Adds[element] = new
|
|
case !old.IsNull() && new.IsNull():
|
|
parent.Array.Deletes[element] = old
|
|
default:
|
|
ed := parent.Array.Updates[element]
|
|
addDiff(path[1:], kind, &ed, old, new)
|
|
parent.Array.Updates[element] = ed
|
|
}
|
|
}
|
|
case string:
|
|
if parent.Object == nil {
|
|
parent.Object = &resource.ObjectDiff{
|
|
Adds: make(resource.PropertyMap),
|
|
Deletes: make(resource.PropertyMap),
|
|
Sames: make(resource.PropertyMap),
|
|
Updates: make(map[resource.PropertyKey]resource.ValueDiff),
|
|
}
|
|
}
|
|
|
|
e := resource.PropertyKey(element)
|
|
if len(path) == 1 {
|
|
switch kind {
|
|
case plugin.DiffAdd, plugin.DiffAddReplace:
|
|
parent.Object.Adds[e] = new
|
|
case plugin.DiffDelete, plugin.DiffDeleteReplace:
|
|
parent.Object.Deletes[e] = old
|
|
case plugin.DiffUpdate, plugin.DiffUpdateReplace:
|
|
valueDiff := resource.ValueDiff{Old: old, New: new}
|
|
if d := old.Diff(new); d != nil {
|
|
valueDiff = *d
|
|
}
|
|
parent.Object.Updates[e] = valueDiff
|
|
default:
|
|
contract.Failf("unexpected diff kind %v", kind)
|
|
}
|
|
} else {
|
|
switch {
|
|
case old.IsNull() && !new.IsNull():
|
|
parent.Object.Adds[e] = new
|
|
case !old.IsNull() && new.IsNull():
|
|
parent.Object.Deletes[e] = old
|
|
default:
|
|
ed := parent.Object.Updates[e]
|
|
addDiff(path[1:], kind, &ed, old, new)
|
|
parent.Object.Updates[e] = ed
|
|
}
|
|
}
|
|
default:
|
|
contract.Failf("unexpected path element type: %T", element)
|
|
}
|
|
}
|
|
|
|
// translateDetailedDiff converts the detailed diff stored in the step event into an ObjectDiff that is appropriate
|
|
// for display.
|
|
func translateDetailedDiff(step engine.StepEventMetadata) *resource.ObjectDiff {
|
|
contract.Assert(step.DetailedDiff != nil)
|
|
|
|
// The rich diff is presented as a list of simple JS property paths and corresponding diffs. We translate this to
|
|
// an ObjectDiff by iterating the list and inserting ValueDiffs that reflect the changes in the detailed diff. Old
|
|
// values are always taken from a step's Outputs; new values are always taken from its Inputs.
|
|
|
|
var diff resource.ValueDiff
|
|
for path, pdiff := range step.DetailedDiff {
|
|
elements, err := parseDiffPath(path)
|
|
contract.Assert(err == nil)
|
|
|
|
olds := resource.NewObjectProperty(step.Old.Outputs)
|
|
if pdiff.InputDiff {
|
|
olds = resource.NewObjectProperty(step.Old.Inputs)
|
|
}
|
|
addDiff(elements, pdiff.Kind, &diff, olds, resource.NewObjectProperty(step.New.Inputs))
|
|
}
|
|
|
|
return diff.Object
|
|
}
|