mirror of https://github.com/pulumi/pulumi.git
605 lines
18 KiB
Go
605 lines
18 KiB
Go
// Copyright 2019-2024, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package resource
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/logging"
|
|
)
|
|
|
|
// 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]
|
|
// - root.array[*].field
|
|
// - root.array["*"].field
|
|
func parsePropertyPath(path string, strict bool) (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 := { '.' } [a-zA-Z_$][a-zA-Z0-9_$]
|
|
// pathIndex := '[' ( [0-9]+ | '"' ('\' '"' | [^"] )+ '"' ']'
|
|
// path := { pathElement | pathIndex }
|
|
var elements []interface{}
|
|
if len(path) > 0 && path[0] == '.' {
|
|
return nil, errors.New("expected property path to start with a name or index")
|
|
}
|
|
for len(path) > 0 {
|
|
switch path[0] {
|
|
case '.':
|
|
path = path[1:]
|
|
if len(path) == 0 {
|
|
return nil, errors.New("expected property path to end with a name or index")
|
|
}
|
|
if path[0] == '[' && strict {
|
|
return nil, errors.New("expected property name after '.'")
|
|
} else if path[0] == '[' {
|
|
// We tolerate a '.' followed by a '[', which is not strictly legal, but is common from old providers.
|
|
logging.V(10).Infof("property path '%s' contains a '.' followed by a '['; this is not strictly legal", path)
|
|
}
|
|
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")
|
|
}
|
|
|
|
segment := path[1:rbracket]
|
|
if segment == "*" {
|
|
pathElement, path = "*", path[rbracket:]
|
|
} else {
|
|
index, err := strconv.ParseInt(segment, 10, 0)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid array index: %w", err)
|
|
}
|
|
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
|
|
}
|
|
|
|
func ParsePropertyPath(path string) (PropertyPath, error) {
|
|
return parsePropertyPath(path, false)
|
|
}
|
|
|
|
func ParsePropertyPathStrict(path string) (PropertyPath, error) {
|
|
return parsePropertyPath(path, true)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Add sets the location inside a PropertyValue indicated by the PropertyPath to the given value. Any components
|
|
// referred to by the path that do not exist will be created. If there is a mismatch between the type of an existing
|
|
// component and a key that traverses that component, this function will return false. If the destination is a null
|
|
// property value, this function will create and return a new property value.
|
|
func (p PropertyPath) Add(dest, v PropertyValue) (PropertyValue, bool) {
|
|
if len(p) == 0 {
|
|
return PropertyValue{}, false
|
|
}
|
|
|
|
// set sets the destination referred to by the last element of the path to the given value.
|
|
rv := dest
|
|
set := func(v PropertyValue) {
|
|
dest, rv = v, v
|
|
}
|
|
for _, key := range p {
|
|
switch key := key.(type) {
|
|
case int:
|
|
// This key is an int, so we expect an array.
|
|
switch {
|
|
case dest.IsNull():
|
|
// If the destination array does not exist, create a new array with enough room to store the value at
|
|
// the requested index.
|
|
dest = NewArrayProperty(make([]PropertyValue, key+1))
|
|
set(dest)
|
|
case dest.IsArray():
|
|
// If the destination array does exist, ensure that it is large enough to accommodate the requested
|
|
// index.
|
|
if arr := dest.ArrayValue(); key >= len(arr) {
|
|
dest = NewArrayProperty(append(make([]PropertyValue, key+1-len(arr)), arr...))
|
|
set(dest)
|
|
}
|
|
default:
|
|
return PropertyValue{}, false
|
|
}
|
|
destV := dest.ArrayValue()
|
|
set = func(v PropertyValue) {
|
|
destV[key] = v
|
|
}
|
|
dest = destV[key]
|
|
case string:
|
|
// This key is a string, so we expect an object.
|
|
switch {
|
|
case dest.IsNull():
|
|
// If the destination does not exist, create a new object.
|
|
dest = NewObjectProperty(PropertyMap{})
|
|
set(dest)
|
|
case dest.IsObject():
|
|
// OK
|
|
default:
|
|
return PropertyValue{}, false
|
|
}
|
|
destV := dest.ObjectValue()
|
|
set = func(v PropertyValue) {
|
|
destV[PropertyKey(key)] = v
|
|
}
|
|
dest = destV[PropertyKey(key)]
|
|
default:
|
|
return PropertyValue{}, false
|
|
}
|
|
}
|
|
|
|
set(v)
|
|
return rv, 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
|
|
}
|
|
|
|
// Contains returns true if the receiver property path contains the other property path.
|
|
// For example, the path `foo["bar"][1]` contains the path `foo.bar[1].baz`. The key `"*"`
|
|
// is a wildcard which matches any string or int index at that same nesting level. So for example,
|
|
// the path `foo.*.baz` contains `foo.bar.baz.bam`, and the path `*` contains any path.
|
|
func (p PropertyPath) Contains(other PropertyPath) bool {
|
|
if len(other) < len(p) {
|
|
return false
|
|
}
|
|
|
|
for i := range p {
|
|
pp := p[i]
|
|
otherp := other[i]
|
|
|
|
switch pp := pp.(type) {
|
|
case int:
|
|
if otherpi, ok := otherp.(int); !ok || otherpi != pp {
|
|
return false
|
|
}
|
|
case string:
|
|
if pp == "*" {
|
|
continue
|
|
}
|
|
if otherps, ok := otherp.(string); !ok || otherps != pp {
|
|
return false
|
|
}
|
|
default:
|
|
// Invalid path, return false
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// unwrapSecrets recursively unwraps any secrets from the given PropertyValue returning true if any secrets were
|
|
// unwrapped.
|
|
func unwrapSecrets(v PropertyValue) (PropertyValue, bool) {
|
|
if v.IsSecret() {
|
|
inner, _ := unwrapSecrets(v.SecretValue().Element)
|
|
return inner, true
|
|
}
|
|
return v, false
|
|
}
|
|
|
|
func (p PropertyPath) reset(old, new PropertyValue, oldIsSecret, newIsSecret bool) bool {
|
|
if len(p) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Unwrap any secrets from old & new, we can just go through them for this traversal.
|
|
old, isSecret := unwrapSecrets(old)
|
|
oldIsSecret = oldIsSecret || isSecret
|
|
new, isSecret = unwrapSecrets(new)
|
|
newIsSecret = newIsSecret || isSecret
|
|
|
|
// If this is the last component we want to do the reset, else we want to search for the next component.
|
|
key := p[0]
|
|
switch key := key.(type) {
|
|
case int:
|
|
// An index < 0 is always a path error, even for empty arrays or objects
|
|
if key < 0 {
|
|
return false
|
|
}
|
|
|
|
// This is a leaf path element, so we want to reset the value at this index in new to the value at this index from old
|
|
if len(p) == 1 {
|
|
if !old.IsArray() && !new.IsArray() {
|
|
// Neither old nor new are arrays, so we can't reset this index
|
|
return true
|
|
} else if !old.IsArray() || !new.IsArray() {
|
|
// One of old or new is an array but the other isn't, so this is a path error
|
|
return false
|
|
}
|
|
|
|
// If neither array contains this index then this is a _same_ and so ok, e.g. given old:[1, 2] and
|
|
// new:[1] and a path of [3] we can return true because new at [3] is the same as old at [3], it
|
|
// doesn't exist.
|
|
if key >= len(old.ArrayValue()) && key >= len(new.ArrayValue()) {
|
|
return true
|
|
}
|
|
// If one array has this index but the other doesn't this is a path failure because we can't
|
|
// remove a location from an array.
|
|
if key >= len(old.ArrayValue()) || key >= len(new.ArrayValue()) {
|
|
return false
|
|
}
|
|
// Otherwise both arrays contain this index and we can reset the value of it in new to what is in
|
|
// old.
|
|
v := old.ArrayValue()[key]
|
|
// If this was a secret value in old, but new isn't currently a secret context then we need to mark this
|
|
// reset value as secret.
|
|
if oldIsSecret && !newIsSecret {
|
|
v = MakeSecret(v)
|
|
}
|
|
new.ArrayValue()[key] = v
|
|
return true
|
|
}
|
|
|
|
if !old.IsArray() || !new.IsArray() {
|
|
// At least one of old or new is not an array, so we can't keep searching along this path but
|
|
// we only return an error if both are not arrays.
|
|
return !old.IsArray() && !new.IsArray()
|
|
}
|
|
|
|
// If this index is out of bounds in either array then this is a path failure because we can't
|
|
// continue the search of this path down each PropertyValue.
|
|
if key >= len(old.ArrayValue()) || key >= len(new.ArrayValue()) {
|
|
return false
|
|
}
|
|
old = old.ArrayValue()[key]
|
|
new = new.ArrayValue()[key]
|
|
return p[1:].reset(old, new, oldIsSecret, newIsSecret)
|
|
|
|
case string:
|
|
if key == "*" {
|
|
if len(p) == 1 {
|
|
if new.IsObject() {
|
|
if old.IsObject() {
|
|
for k := range old.ObjectValue() {
|
|
v := old.ObjectValue()[k]
|
|
// If this was a secret value in old, but new isn't currently a secret context then we need
|
|
// to mark this reset value as secret.
|
|
if oldIsSecret && !newIsSecret {
|
|
v = MakeSecret(v)
|
|
}
|
|
new.ObjectValue()[k] = v
|
|
}
|
|
for k := range new.ObjectValue() {
|
|
if _, has := old.ObjectValue()[k]; !has {
|
|
delete(new.ObjectValue(), k)
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
} else if new.IsArray() {
|
|
if old.IsArray() {
|
|
oldArray := old.ArrayValue()
|
|
newArray := new.ArrayValue()
|
|
// If arrays are of different length then this is a path failure because we can't
|
|
// synchronise the two values.
|
|
if len(oldArray) != len(newArray) {
|
|
return false
|
|
}
|
|
|
|
for i := range oldArray {
|
|
v := oldArray[i]
|
|
// If this was a secret value in old, but new isn't currently a secret context then we need
|
|
// to mark this reset value as secret.
|
|
if oldIsSecret && !newIsSecret {
|
|
v = MakeSecret(v)
|
|
}
|
|
newArray[i] = v
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
if old.IsObject() && new.IsObject() {
|
|
oldObject := old.ObjectValue()
|
|
newObject := new.ObjectValue()
|
|
|
|
for k := range oldObject {
|
|
var hasOld, hasNew bool
|
|
oldValue, hasOld := oldObject[k]
|
|
newValue, hasNew := newObject[k]
|
|
if !hasOld || !hasNew {
|
|
return false
|
|
}
|
|
|
|
if !p[1:].reset(oldValue, newValue, oldIsSecret, newIsSecret) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
} else if old.IsArray() && new.IsArray() {
|
|
oldArray := old.ArrayValue()
|
|
newArray := new.ArrayValue()
|
|
// If arrays are of different length then this is a path failure because we can't
|
|
// continue the search of this path down each PropertyValue.
|
|
if len(oldArray) != len(newArray) {
|
|
return false
|
|
}
|
|
|
|
for i := range oldArray {
|
|
if !p[1:].reset(oldArray[i], newArray[i], oldIsSecret, newIsSecret) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
} else {
|
|
pkey := PropertyKey(key)
|
|
|
|
if len(p) == 1 {
|
|
// This is the leaf path entry, so we want to reset this property in new to it's value in old.
|
|
|
|
// Firstly if old doesn't have this key (either because it isn't an object or because it
|
|
// doesn't have the property) then we want to delete this from new.
|
|
var v PropertyValue
|
|
var has bool
|
|
if old.IsObject() {
|
|
v, has = old.ObjectValue()[pkey]
|
|
}
|
|
|
|
if has {
|
|
// If this path exists in old but new isn't an object than return a path error
|
|
if !new.IsObject() {
|
|
return false
|
|
}
|
|
// Else simply overwrite the value in new with the value from old, if this was a secret value in
|
|
// old, but new isn't currently a secret context then we need to mark this reset value as secret.
|
|
if oldIsSecret && !newIsSecret {
|
|
v = MakeSecret(v)
|
|
}
|
|
new.ObjectValue()[pkey] = v
|
|
} else {
|
|
// If the path doesn't exist in old then we want to delete it from new, but if new isn't
|
|
// an object then we can just do nothing we don't consider this a path error. e.g. given
|
|
// old:{} and new:1 and a path of "a" we can return true because ["a"] in both is the
|
|
// same (it doesn't exist).
|
|
if new.IsObject() {
|
|
delete(new.ObjectValue(), pkey)
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
if !old.IsObject() || !new.IsObject() {
|
|
// At least one of old or new is not an object, so we can't keep searching along this path but
|
|
// we only return an error if both are not objects.
|
|
return !old.IsObject() && !new.IsObject()
|
|
}
|
|
|
|
new, hasNew := new.ObjectValue()[pkey]
|
|
old, hasOld := old.ObjectValue()[pkey]
|
|
|
|
if hasOld && !hasNew {
|
|
// Old has this key but new doesn't, but we still searching for the leaf item to set so this
|
|
// is a path error.
|
|
return false
|
|
}
|
|
if !hasOld && !hasNew {
|
|
// Neither value contain this path, so we're done.
|
|
return true
|
|
}
|
|
|
|
return p[1:].reset(old, new, oldIsSecret, newIsSecret)
|
|
}
|
|
}
|
|
|
|
contract.Failf("Invalid property path component type: %T", key)
|
|
return true
|
|
}
|
|
|
|
// Reset attempts to reset the values located by the PropertyPath inside the given new PropertyMap to the
|
|
// values from the same location in the old PropertyMap. Reset behaves likes Set in that it will not create
|
|
// intermediate locations, it also won't create or delete array locations (because that would change the size
|
|
// of the array).
|
|
func (p PropertyPath) Reset(old, new PropertyMap) bool {
|
|
return p.reset(NewObjectProperty(old), NewObjectProperty(new), false, false)
|
|
}
|
|
|
|
func requiresQuote(c rune) bool {
|
|
return !(c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '_')
|
|
}
|
|
|
|
func (p PropertyPath) String() string {
|
|
var buf bytes.Buffer
|
|
for i, k := range p {
|
|
switch k := k.(type) {
|
|
case string:
|
|
var keyBuf bytes.Buffer
|
|
quoted := false
|
|
for _, c := range k {
|
|
if requiresQuote(c) {
|
|
quoted = true
|
|
if c == '"' {
|
|
keyBuf.WriteByte('\\')
|
|
}
|
|
}
|
|
keyBuf.WriteRune(c)
|
|
}
|
|
if !quoted {
|
|
if i == 0 {
|
|
fmt.Fprintf(&buf, "%s", keyBuf.String())
|
|
} else {
|
|
fmt.Fprintf(&buf, ".%s", keyBuf.String())
|
|
}
|
|
} else {
|
|
fmt.Fprintf(&buf, `["%s"]`, keyBuf.String())
|
|
}
|
|
case int:
|
|
fmt.Fprintf(&buf, "[%d]", k)
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|