These changes add support for passing `ignoreChanges` paths to resource providers. This is intended to accommodate providers that perform diffs between resource inputs and resource state (e.g. all Terraform-based providers, the k8s provider when using API server dry-runs). These paths are specified using the same syntax as the paths used in detailed diffs. In addition to passing these paths to providers, the existing support for `ignoreChanges` in inputs has been extended to accept paths rather than top-level keys. It is an error to specify a path that is missing one or more component in the old or new inputs. Fixes #2936, #2663.
194 lines
5.3 KiB
Go
194 lines
5.3 KiB
Go
package resource
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// PropertyPath represents a path to a nested property. The path may be composed of strings (which access properties
|
|
// in ObjectProperty values) and integers (which access elements of ArrayProperty values).
|
|
type PropertyPath []interface{}
|
|
|
|
// ParsePropertyPath parses a property path into a PropertyPath value.
|
|
//
|
|
// A property path string is essentially a Javascript property access expression in which all elements are literals.
|
|
// Valid property 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 }
|
|
//
|
|
// Examples of valid paths:
|
|
// - root
|
|
// - root.nested
|
|
// - root["nested"]
|
|
// - root.double.nest
|
|
// - root["double"].nest
|
|
// - root["double"]["nest"]
|
|
// - root.array[0]
|
|
// - root.array[100]
|
|
// - root.array[0].nested
|
|
// - root.array[0][1].nested
|
|
// - root.nested.array[0].double[1]
|
|
// - root["key with \"escaped\" quotes"]
|
|
// - root["key with a ."]
|
|
// - ["root key with \"escaped\" quotes"].nested
|
|
// - ["root key with a ."][100]
|
|
func ParsePropertyPath(path string) (PropertyPath, error) {
|
|
// We interpret the grammar above 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 len(path) > 1 && 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 PropertyPath(elements), nil
|
|
}
|
|
|
|
// Get attempts to get the value located by the PropertyPath inside the given PropertyValue. If any component of the
|
|
// path does not exist, this function will return (NullPropertyValue, false).
|
|
func (p PropertyPath) Get(v PropertyValue) (PropertyValue, bool) {
|
|
for _, key := range p {
|
|
switch {
|
|
case v.IsArray():
|
|
index, ok := key.(int)
|
|
if !ok || index < 0 || index >= len(v.ArrayValue()) {
|
|
return PropertyValue{}, false
|
|
}
|
|
v = v.ArrayValue()[index]
|
|
case v.IsObject():
|
|
k, ok := key.(string)
|
|
if !ok {
|
|
return PropertyValue{}, false
|
|
}
|
|
v, ok = v.ObjectValue()[PropertyKey(k)]
|
|
if !ok {
|
|
return PropertyValue{}, false
|
|
}
|
|
default:
|
|
return PropertyValue{}, false
|
|
}
|
|
}
|
|
return v, true
|
|
}
|
|
|
|
// Set attempts to set the location inside a PropertyValue indicated by the PropertyPath to the given value If any
|
|
// component of the path besides the last component does not exist, this function will return false.
|
|
func (p PropertyPath) Set(dest, v PropertyValue) bool {
|
|
if len(p) == 0 {
|
|
return false
|
|
}
|
|
|
|
dest, ok := p[:len(p)-1].Get(dest)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
key := p[len(p)-1]
|
|
switch {
|
|
case dest.IsArray():
|
|
index, ok := key.(int)
|
|
if !ok || index < 0 || index >= len(dest.ArrayValue()) {
|
|
return false
|
|
}
|
|
dest.ArrayValue()[index] = v
|
|
case dest.IsObject():
|
|
k, ok := key.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
dest.ObjectValue()[PropertyKey(k)] = v
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Delete attempts to delete the value located by the PropertyPath inside the given PropertyValue. If any component
|
|
// of the path does not exist, this function will return false.
|
|
func (p PropertyPath) Delete(dest PropertyValue) bool {
|
|
if len(p) == 0 {
|
|
return false
|
|
}
|
|
|
|
dest, ok := p[:len(p)-1].Get(dest)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
key := p[len(p)-1]
|
|
switch {
|
|
case dest.IsArray():
|
|
index, ok := key.(int)
|
|
if !ok || index < 0 || index >= len(dest.ArrayValue()) {
|
|
return false
|
|
}
|
|
dest.ArrayValue()[index] = PropertyValue{}
|
|
case dest.IsObject():
|
|
k, ok := key.(string)
|
|
if !ok {
|
|
return false
|
|
}
|
|
delete(dest.ObjectValue(), PropertyKey(k))
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
|
|
}
|