// Copyright 2016-2021, 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 ( "fmt" "reflect" "sort" "strings" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/archive" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/asset" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/sig" "github.com/pulumi/pulumi/sdk/v3/go/common/slice" "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/pulumi/pulumi/sdk/v3/go/common/util/mapper" ) // PropertyKey is the name of a property. type PropertyKey tokens.Name // PropertyMap is a simple map keyed by property name with "JSON-like" values. type PropertyMap map[PropertyKey]PropertyValue // NewPropertyMap turns a struct into a property map, using any JSON tags inside to determine naming. func NewPropertyMap(s interface{}) PropertyMap { return NewPropertyMapRepl(s, nil, nil) } // NewPropertyMapRepl turns a struct into a property map, using any JSON tags inside to determine naming. If non-nil // replk or replv function(s) are provided, key and/or value transformations are performed during the mapping. func NewPropertyMapRepl(s interface{}, replk func(string) (PropertyKey, bool), replv func(interface{}) (PropertyValue, bool), ) PropertyMap { m, err := mapper.Unmap(s) contract.Assertf(err == nil, "Struct of properties failed to map correctly: %v", err) return NewPropertyMapFromMapRepl(m, replk, replv) } // NewPropertyMapFromMap creates a resource map from a regular weakly typed JSON-like map. func NewPropertyMapFromMap(m map[string]interface{}) PropertyMap { return NewPropertyMapFromMapRepl(m, nil, nil) } // NewPropertyMapFromMapRepl optionally replaces keys/values in an existing map while creating a new resource map. func NewPropertyMapFromMapRepl(m map[string]interface{}, replk func(string) (PropertyKey, bool), replv func(interface{}) (PropertyValue, bool), ) PropertyMap { result := make(PropertyMap) for k, v := range m { key := PropertyKey(k) if replk != nil { if rk, repl := replk(k); repl { key = rk } } result[key] = NewPropertyValueRepl(v, replk, replv) } return result } // PropertyValue is the value of a property, limited to a select few types (see below). type PropertyValue struct { V interface{} } // Computed represents the absence of a property value, because it will be computed at some point in the future. It // contains a property value which represents the underlying expected type of the eventual property value. type Computed struct { Element PropertyValue // the eventual value (type) of the computed property. } // Output is a property value that will eventually be computed by the resource provider. If an output property is // encountered, it means the resource has not yet been created, and so the output value is unavailable. Note that an // output property is a special case of computed, but carries additional semantic meaning. type Output struct { Element PropertyValue // the value of this output if it is resolved. Known bool `json:"-"` // true if this output's value is known. Secret bool `json:"-"` // true if this output's value is secret. Dependencies []URN `json:"-"` // the dependencies associated with this output. } // Secret indicates that the underlying value should be persisted securely. // // In order to facilitate the ability to distinguish secrets with identical plaintext in downstream code that may // want to cache a secret's ciphertext, secret PropertyValues hold the address of the Secret. If a secret must be // copied, its value--not its address--should be copied. type Secret struct { Element PropertyValue } // ResourceReference is a property value that represents a reference to a Resource. The reference captures the // resource's URN, ID, and the version of its containing package. Note that there are several cases to consider with // respect to the ID: // // - The reference may not contain an ID if the referenced resource is a component resource. In this case, the ID will // be null. // - The ID may be unknown (in which case it will be the unknown property value) // - Otherwise, the ID must be a string. // //nolint:revive type ResourceReference struct { URN URN ID PropertyValue PackageVersion string } func (ref ResourceReference) IDString() (value string, hasID bool) { switch { case ref.ID.IsComputed(): return "", true case ref.ID.IsString(): return ref.ID.StringValue(), true default: return "", false } } func (ref ResourceReference) Equal(other ResourceReference) bool { if ref.URN != other.URN { return false } vid, oid := ref.ID, other.ID if vid.IsComputed() && oid.IsComputed() { return true } return vid.DeepEquals(oid) } type ReqError struct { K PropertyKey } func IsReqError(err error) bool { _, isreq := err.(*ReqError) return isreq } func (err *ReqError) Error() string { return fmt.Sprintf("required property '%v' is missing", err.K) } // HasValue returns true if the slot associated with the given property key contains a real value. It returns false // if a value is null or an output property that is awaiting a value to be assigned. That is to say, HasValue indicates // a semantically meaningful value is present (even if it's a computed one whose concrete value isn't yet evaluated). func (props PropertyMap) HasValue(k PropertyKey) bool { v, has := props[k] return has && v.HasValue() } // ContainsUnknowns returns true if the property map contains at least one unknown value. func (props PropertyMap) ContainsUnknowns() bool { for _, v := range props { if v.ContainsUnknowns() { return true } } return false } // ContainsSecrets returns true if the property map contains at least one secret value. func (props PropertyMap) ContainsSecrets() bool { for _, v := range props { if v.ContainsSecrets() { return true } } return false } // Mappable returns a mapper-compatible object map, suitable for deserialization into structures. func (props PropertyMap) Mappable() map[string]interface{} { return props.MapRepl(nil, nil) } // MapRepl returns a mapper-compatible object map, suitable for deserialization into structures. A key and/or value // replace function, replk/replv, may be passed that will replace elements using custom logic if appropriate. func (props PropertyMap) MapRepl(replk func(string) (string, bool), replv func(PropertyValue) (interface{}, bool), ) map[string]interface{} { obj := make(map[string]interface{}) for _, k := range props.StableKeys() { key := string(k) if replk != nil { if rk, repk := replk(key); repk { key = rk } } obj[key] = props[k].MapRepl(replk, replv) } return obj } // Copy makes a shallow copy of the map. func (props PropertyMap) Copy() PropertyMap { new := make(PropertyMap) for k, v := range props { new[k] = v } return new } // StableKeys returns all of the map's keys in a stable order. func (props PropertyMap) StableKeys() []PropertyKey { sorted := slice.Prealloc[PropertyKey](len(props)) for k := range props { sorted = append(sorted, k) } sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) return sorted } // PropertyValueType enumerates the actual types that may be stored in a PropertyValue. // //nolint:lll type PropertyValueType interface { bool | float64 | string | *asset.Asset | *archive.Archive | Computed | Output | *Secret | ResourceReference | []PropertyValue | PropertyMap } // NewProperty creates a new PropertyValue. func NewProperty[T PropertyValueType](v T) PropertyValue { return PropertyValue{v} } func NewNullProperty() PropertyValue { return PropertyValue{nil} } func NewBoolProperty(v bool) PropertyValue { return PropertyValue{v} } func NewNumberProperty(v float64) PropertyValue { return PropertyValue{v} } func NewStringProperty(v string) PropertyValue { return PropertyValue{v} } func NewArrayProperty(v []PropertyValue) PropertyValue { return PropertyValue{v} } func NewAssetProperty(v *asset.Asset) PropertyValue { return PropertyValue{v} } func NewArchiveProperty(v *archive.Archive) PropertyValue { return PropertyValue{v} } func NewObjectProperty(v PropertyMap) PropertyValue { return PropertyValue{v} } func NewComputedProperty(v Computed) PropertyValue { return PropertyValue{v} } func NewOutputProperty(v Output) PropertyValue { return PropertyValue{v} } func NewSecretProperty(v *Secret) PropertyValue { return PropertyValue{v} } func NewResourceReferenceProperty(v ResourceReference) PropertyValue { return PropertyValue{v} } func MakeComputed(v PropertyValue) PropertyValue { return NewProperty(Computed{Element: v}) } func MakeOutput(v PropertyValue) PropertyValue { return NewProperty(Output{Element: v}) } func MakeSecret(v PropertyValue) PropertyValue { return NewProperty(&Secret{Element: v}) } // MakeComponentResourceReference creates a reference to a component resource. func MakeComponentResourceReference(urn URN, packageVersion string) PropertyValue { return NewProperty(ResourceReference{ URN: urn, PackageVersion: packageVersion, }) } // MakeCustomResourceReference creates a reference to a custom resource. If the resource's ID is the empty string, it // will be treated as unknown. func MakeCustomResourceReference(urn URN, id ID, packageVersion string) PropertyValue { idProp := NewProperty(string(id)) if id == "" { idProp = MakeComputed(NewProperty("")) } return NewProperty(ResourceReference{ ID: idProp, URN: urn, PackageVersion: packageVersion, }) } // NewPropertyValue turns a value into a property value, provided it is of a legal "JSON-like" kind. func NewPropertyValue(v interface{}) PropertyValue { return NewPropertyValueRepl(v, nil, nil) } // NewPropertyValueRepl turns a value into a property value, provided it is of a legal "JSON-like" kind. The // replacement functions, replk and replv, may be supplied to transform keys and/or values as the mapping takes place. func NewPropertyValueRepl(v interface{}, replk func(string) (PropertyKey, bool), replv func(interface{}) (PropertyValue, bool), ) PropertyValue { // If a replacement routine is supplied, use that. if replv != nil { if rv, repl := replv(v); repl { return rv } } // If nil, easy peasy, just return a null. if v == nil { return NewNullProperty() } // Else, check for some known primitive types. switch t := v.(type) { case bool: return NewProperty(t) case int: return NewProperty(float64(t)) case uint: return NewProperty(float64(t)) case int32: return NewProperty(float64(t)) case uint32: return NewProperty(float64(t)) case int64: return NewProperty(float64(t)) case uint64: return NewProperty(float64(t)) case float32: return NewProperty(float64(t)) case float64: return NewProperty(t) case string: return NewProperty(t) case *asset.Asset: return NewProperty(t) case *archive.Archive: return NewProperty(t) case Computed: return NewProperty(t) case Output: return NewProperty(t) case *Secret: return NewProperty(t) case ResourceReference: return NewProperty(t) case PropertyValue: return t } // Next, see if it's an array, slice, pointer or struct, and handle each accordingly. rv := reflect.ValueOf(v) //nolint:exhaustive // We intentionally only handle some types here. switch rk := rv.Type().Kind(); rk { case reflect.Array, reflect.Slice: // If an array or slice, just create an array out of it. arr := []PropertyValue{} for i := 0; i < rv.Len(); i++ { elem := rv.Index(i) arr = append(arr, NewPropertyValueRepl(elem.Interface(), replk, replv)) } return NewProperty(arr) case reflect.Ptr: // If a pointer, recurse and return the underlying value. if rv.IsNil() { return NewNullProperty() } return NewPropertyValueRepl(rv.Elem().Interface(), replk, replv) case reflect.Map: // If a map, create a new property map, provided the keys and values are okay. obj := PropertyMap{} for iter := rv.MapRange(); iter.Next(); { key := iter.Key() if key.Kind() != reflect.String { contract.Failf("Unrecognized PropertyMap key type %v", key.Type()) } pk := PropertyKey(key.String()) if replk != nil { if rk, repl := replk(string(pk)); repl { pk = rk } } val := iter.Value().Interface() pv := NewPropertyValueRepl(val, replk, replv) obj[pk] = pv } return NewProperty(obj) case reflect.String: return NewProperty(rv.String()) case reflect.Struct: obj := NewPropertyMapRepl(v, replk, replv) return NewProperty(obj) default: contract.Failf("Unrecognized value type: type=%v kind=%v", rv.Type(), rk) return NewNullProperty() } } // HasValue returns true if a value is semantically meaningful. func (v PropertyValue) HasValue() bool { if v.IsOutput() { return v.OutputValue().Known } return !v.IsNull() } // ContainsUnknowns returns true if the property value contains at least one unknown (deeply). func (v PropertyValue) ContainsUnknowns() bool { if v.IsComputed() || (v.IsOutput() && !v.OutputValue().Known) { return true } else if v.IsArray() { for _, e := range v.ArrayValue() { if e.ContainsUnknowns() { return true } } } else if v.IsObject() { return v.ObjectValue().ContainsUnknowns() } else if v.IsSecret() { return v.SecretValue().Element.ContainsUnknowns() } return false } // ContainsSecrets returns true if the property value contains at least one secret (deeply). func (v PropertyValue) ContainsSecrets() bool { if v.IsSecret() { return true } else if v.IsComputed() { return v.Input().Element.ContainsSecrets() } else if v.IsOutput() { return v.OutputValue().Secret || v.OutputValue().Element.ContainsSecrets() } else if v.IsArray() { for _, e := range v.ArrayValue() { if e.ContainsSecrets() { return true } } } else if v.IsObject() { return v.ObjectValue().ContainsSecrets() } return false } // BoolValue fetches the underlying bool value (panicking if it isn't a bool). func (v PropertyValue) BoolValue() bool { return v.V.(bool) } // NumberValue fetches the underlying number value (panicking if it isn't a number). func (v PropertyValue) NumberValue() float64 { return v.V.(float64) } // StringValue fetches the underlying string value (panicking if it isn't a string). func (v PropertyValue) StringValue() string { return v.V.(string) } // ArrayValue fetches the underlying array value (panicking if it isn't a array). func (v PropertyValue) ArrayValue() []PropertyValue { return v.V.([]PropertyValue) } // AssetValue fetches the underlying asset value (panicking if it isn't an asset). func (v PropertyValue) AssetValue() *asset.Asset { return v.V.(*asset.Asset) } // ArchiveValue fetches the underlying archive value (panicking if it isn't an archive). func (v PropertyValue) ArchiveValue() *archive.Archive { return v.V.(*archive.Archive) } // ObjectValue fetches the underlying object value (panicking if it isn't a object). func (v PropertyValue) ObjectValue() PropertyMap { return v.V.(PropertyMap) } // Input fetches the underlying computed value (panicking if it isn't a computed). func (v PropertyValue) Input() Computed { return v.V.(Computed) } // OutputValue fetches the underlying output value (panicking if it isn't a output). func (v PropertyValue) OutputValue() Output { return v.V.(Output) } // SecretValue fetches the underlying secret value (panicking if it isn't a secret). func (v PropertyValue) SecretValue() *Secret { return v.V.(*Secret) } // ResourceReferenceValue fetches the underlying resource reference value (panicking if it isn't a resource reference). func (v PropertyValue) ResourceReferenceValue() ResourceReference { return v.V.(ResourceReference) } // IsNull returns true if the underlying value is a null. func (v PropertyValue) IsNull() bool { return v.V == nil } // IsBool returns true if the underlying value is a bool. func (v PropertyValue) IsBool() bool { _, is := v.V.(bool) return is } // IsNumber returns true if the underlying value is a number. func (v PropertyValue) IsNumber() bool { _, is := v.V.(float64) return is } // IsString returns true if the underlying value is a string. func (v PropertyValue) IsString() bool { _, is := v.V.(string) return is } // IsArray returns true if the underlying value is an array. func (v PropertyValue) IsArray() bool { _, is := v.V.([]PropertyValue) return is } // IsAsset returns true if the underlying value is an object. func (v PropertyValue) IsAsset() bool { _, is := v.V.(*asset.Asset) return is } // IsArchive returns true if the underlying value is an object. func (v PropertyValue) IsArchive() bool { _, is := v.V.(*archive.Archive) return is } // IsObject returns true if the underlying value is an object. func (v PropertyValue) IsObject() bool { _, is := v.V.(PropertyMap) return is } // IsComputed returns true if the underlying value is a computed value. func (v PropertyValue) IsComputed() bool { _, is := v.V.(Computed) return is } // IsOutput returns true if the underlying value is an output value. func (v PropertyValue) IsOutput() bool { _, is := v.V.(Output) return is } // IsSecret returns true if the underlying value is a secret value. func (v PropertyValue) IsSecret() bool { _, is := v.V.(*Secret) return is } // IsResourceReference returns true if the underlying value is a resource reference value. func (v PropertyValue) IsResourceReference() bool { _, is := v.V.(ResourceReference) return is } // TypeString returns a type representation of the property value's holder type. func (v PropertyValue) TypeString() string { if v.IsNull() { return "null" } else if v.IsBool() { return "bool" } else if v.IsNumber() { return "number" } else if v.IsString() { return "string" } else if v.IsArray() { return "[]" } else if v.IsAsset() { return "asset" } else if v.IsArchive() { return "archive" } else if v.IsObject() { return "object" } else if v.IsComputed() { return "output<" + v.Input().Element.TypeString() + ">" } else if v.IsOutput() { if !v.OutputValue().Known { return MakeComputed(v.OutputValue().Element).TypeString() } else if v.OutputValue().Secret { return MakeSecret(v.OutputValue().Element).TypeString() } return v.OutputValue().Element.TypeString() } else if v.IsSecret() { return "secret<" + v.SecretValue().Element.TypeString() + ">" } else if v.IsResourceReference() { ref := v.ResourceReferenceValue() return fmt.Sprintf("resourceReference(%q, %q, %q)", ref.URN, ref.ID, ref.PackageVersion) } contract.Failf("Unrecognized PropertyValue type") return "" } // Mappable returns a mapper-compatible value, suitable for deserialization into structures. func (v PropertyValue) Mappable() interface{} { return v.MapRepl(nil, nil) } // MapRepl returns a mapper-compatible object map, suitable for deserialization into structures. A key and/or value // replace function, replk/replv, may be passed that will replace elements using custom logic if appropriate. func (v PropertyValue) MapRepl(replk func(string) (string, bool), replv func(PropertyValue) (interface{}, bool), ) interface{} { if replv != nil { if rv, repv := replv(v); repv { return rv } } if v.IsNull() { return nil } else if v.IsBool() { return v.BoolValue() } else if v.IsNumber() { return v.NumberValue() } else if v.IsString() { return v.StringValue() } else if v.IsArray() { arr := []interface{}{} for _, e := range v.ArrayValue() { arr = append(arr, e.MapRepl(replk, replv)) } return arr } else if v.IsAsset() { return v.AssetValue() } else if v.IsArchive() { return v.ArchiveValue() } else if v.IsComputed() { return v.Input() } else if v.IsOutput() { return v.OutputValue() } else if v.IsSecret() { return v.SecretValue() } else if v.IsResourceReference() { return v.ResourceReferenceValue() } contract.Assertf(v.IsObject(), "v is not Object '%v' instead", v.TypeString()) return v.ObjectValue().MapRepl(replk, replv) } // String implements the fmt.Stringer interface to add slightly more information to the output. func (v PropertyValue) String() string { if v.IsComputed() { // For computed properties, show the type followed by an empty object string. return fmt.Sprintf("%v{}", v.TypeString()) } else if v.IsOutput() { if !v.OutputValue().Known { return MakeComputed(v.OutputValue().Element).String() } else if v.OutputValue().Secret { return MakeSecret(v.OutputValue().Element).String() } return v.OutputValue().Element.String() } // For all others, just display the underlying property value. return fmt.Sprintf("{%v}", v.V) } // Property is a pair of key and value. type Property struct { Key PropertyKey Value PropertyValue } // SigKey is sometimes used to encode type identity inside of a map. This is required when flattening into ordinary // maps, like we do when performing serialization, to ensure recoverability of type identities later on. const SigKey = sig.Key // HasSig checks to see if the given property map contains the specific signature match. func HasSig(obj PropertyMap, match string) bool { if sig, hassig := obj[SigKey]; hassig { return sig.IsString() && sig.StringValue() == match } return false } // SecretSig is the unique secret signature. const SecretSig = sig.Secret // ResourceReferenceSig is the unique resource reference signature. const ResourceReferenceSig = sig.ResourceReference // OutputValueSig is the unique output value signature. const OutputValueSig = sig.OutputValue // IsInternalPropertyKey returns true if the given property key is an internal key that should not be displayed to // users. func IsInternalPropertyKey(key PropertyKey) bool { return strings.HasPrefix(string(key), "__") }