pulumi/sdk/go/common/resource/config/object.go

534 lines
13 KiB
Go

// Copyright 2016-2022, 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 config
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
var errSecureReprReserved = errors.New(`maps with the single key "secure" are reserved`)
// object is the internal object representation of a single config value. All operations on Value first decode the
// Value's string representation into its object representation. Secure strings are stored in objects as ciphertext.
type object struct {
value any
secure bool
}
// objectType describes the types of values that may be stored in the value field of an object
type objectType interface {
bool | int64 | float64 | string | []object | map[string]object
}
// newObject creates a new object with the given representation.
func newObject[T objectType](v T) object {
return object{value: v}
}
// newSecureObject creates a new secure object with the given ciphertext.
func newSecureObject(ciphertext string) object {
return object{value: ciphertext, secure: true}
}
// Secure returns true if the receiver is a secure string or a composite value that contains a secure string.
func (c object) Secure() bool {
switch v := c.value.(type) {
case []object:
for _, v := range v {
if v.Secure() {
return true
}
}
return false
case map[string]object:
for _, v := range v {
if v.Secure() {
return true
}
}
return false
case string:
return c.secure
default:
return false
}
}
// Decrypt decrypts any ciphertexts within the object and returns appropriately-shaped Plaintext values.
func (c object) Decrypt(ctx context.Context, decrypter Decrypter) (Plaintext, error) {
return c.decrypt(ctx, nil, decrypter)
}
func (c object) decrypt(ctx context.Context, path resource.PropertyPath, decrypter Decrypter) (Plaintext, error) {
switch v := c.value.(type) {
case bool:
return NewPlaintext(v), nil
case int64:
return NewPlaintext(v), nil
case float64:
return NewPlaintext(v), nil
case string:
if !c.secure {
return NewPlaintext(v), nil
}
plaintext, err := decrypter.DecryptValue(ctx, v)
if err != nil {
return Plaintext{}, fmt.Errorf("%v: %w", path, err)
}
return NewSecurePlaintext(plaintext), nil
case []object:
vs := make([]Plaintext, len(v))
for i, v := range v {
pv, err := v.decrypt(ctx, append(path, i), decrypter)
if err != nil {
return Plaintext{}, err
}
vs[i] = pv
}
return NewPlaintext(vs), nil
case map[string]object:
vs := make(map[string]Plaintext, len(v))
for k, v := range v {
pv, err := v.decrypt(ctx, append(path, k), decrypter)
if err != nil {
return Plaintext{}, err
}
vs[k] = pv
}
return NewPlaintext(vs), nil
case nil:
return Plaintext{}, nil
default:
contract.Failf("unexpected value of type %T", v)
return Plaintext{}, nil
}
}
// Merge merges the receiver onto the given base using JSON merge patch semantics. Merge does not modify the receiver or
// the base.
func (c object) Merge(base object) object {
if co, ok := c.value.(map[string]object); ok {
if bo, ok := base.value.(map[string]object); ok {
mo := make(map[string]object, len(co))
for k, v := range bo {
mo[k] = v
}
for k, v := range co {
mo[k] = v.Merge(mo[k])
}
return newObject(mo)
}
}
return c
}
// Get gets the member value at path. The path to the receiver is prefix.
func (c object) Get(path resource.PropertyPath) (_ object, ok bool, err error) {
if len(path) == 0 {
return c, true, nil
}
switch v := c.value.(type) {
case []object:
index, ok := path[0].(int)
if !ok || index < 0 || index >= len(v) {
return object{}, false, nil
}
elem := v[index]
return elem.Get(path[1:])
case map[string]object:
key, ok := path[0].(string)
if !ok {
return object{}, false, nil
}
elem, ok := v[key]
if !ok {
return object{}, false, nil
}
return elem.Get(path[1:])
default:
return object{}, false, nil
}
}
// Delete deletes the member value at path. The path to the receiver is prefix.
func (c *object) Delete(prefix, path resource.PropertyPath) error {
if len(path) == 0 {
return nil
}
prefix = append(prefix, path[0])
switch v := c.value.(type) {
case []object:
index, ok := path[0].(int)
if !ok || index < 0 || index >= len(v) {
return nil
}
if len(path) == 1 {
c.value = append(v[:index], v[index+1:]...)
return nil
}
elem := &v[index]
return elem.Delete(prefix, path[1:])
case map[string]object:
key, ok := path[0].(string)
if !ok {
return nil
}
// If we're deleting a property from this object, make sure that the result won't be mistaken for a secure
// value when it is encoded. Secure values are encoded as `{"secure": "ciphertext"}`.
if len(path) == 1 {
if len(v) == 2 {
keys := make([]string, 0, 2)
for k := range v {
if k != key {
keys = append(keys, k)
}
}
if len(keys) == 1 && keys[0] == "secure" {
if _, ok := v["secure"].value.(string); ok {
return fmt.Errorf("%v: %w", prefix, errSecureReprReserved)
}
}
}
delete(v, key)
return nil
}
elem, ok := v[key]
if !ok {
return nil
}
err := elem.Delete(prefix, path[1:])
v[key] = elem
return err
default:
return nil
}
}
func newContainer(accessor any) any {
switch accessor := accessor.(type) {
case int:
return make([]object, accessor+1)
case string:
return make(map[string]object)
default:
contract.Failf("unexpected accessor kind %T", accessor)
return nil
}
}
// Set sets the member value at path to new. The path to the receiver is prefix.
func (c *object) Set(prefix, path resource.PropertyPath, new object) error {
if len(path) == 0 {
*c = new
return nil
}
// Check the type of the receiver and create a new container if allowed.
switch c.value.(type) {
case []object, map[string]object:
// OK
case nil:
// This value is nil. Create a new container ny inferring the container type (i.e. array or object) from the
// accessor at the head of the path.
c.value = newContainer(path[0])
default:
// COMPAT: If this is the first level, we create a new container and overwrite the old value rather than issuing
// a type error.
if len(prefix) == 1 {
c.value, c.secure = newContainer(path[0]), false
} else {
switch path[0].(type) {
case int:
return fmt.Errorf("%v: expected an array", prefix)
case string:
return fmt.Errorf("%v: expected a map", prefix)
default:
contract.Failf("unreachable")
return nil
}
}
}
prefix = append(prefix, path[0])
switch v := c.value.(type) {
case []object:
index, ok := path[0].(int)
if !ok {
return fmt.Errorf("%v: key for an array must be an int", prefix)
}
if index < 0 || index > len(v) {
return fmt.Errorf("%v: array index out of range", prefix)
}
if index == len(v) {
v = append(v, object{})
c.value = v
}
elem := &v[index]
return elem.Set(prefix, path[1:], new)
case map[string]object:
key, ok := path[0].(string)
if !ok {
return fmt.Errorf("%v: key for a map must be a string", prefix)
}
// If we're adding a property tothis object, make sure that the result won't be mistaken for a secure
// value when it is encoded. Secure values are encoded as `{"secure": "ciphertext"}`.
if len(path) == 1 && len(v) == 0 && key == "secure" {
if _, ok := new.value.(string); ok {
return errSecureReprReserved
}
}
elem := v[key]
err := elem.Set(prefix, path[1:], new)
v[key] = elem
return err
default:
contract.Failf("unreachable")
return nil
}
}
// SecureValues returns the plaintext values for any secure strings contained in the receiver.
func (c object) SecureValues(dec Decrypter) ([]string, error) {
switch v := c.value.(type) {
case []object:
var values []string
for _, v := range v {
vs, err := v.SecureValues(dec)
if err != nil {
return nil, err
}
values = append(values, vs...)
}
return values, nil
case map[string]object:
var values []string
for _, v := range v {
vs, err := v.SecureValues(dec)
if err != nil {
return nil, err
}
values = append(values, vs...)
}
return values, nil
case string:
if c.secure {
plaintext, err := dec.DecryptValue(context.TODO(), v)
if err != nil {
return nil, err
}
return []string{plaintext}, nil
}
return nil, nil
default:
return nil, nil
}
}
// marshalValue converts the receiver into a Value.
func (c object) marshalValue() (v Value, err error) {
v.value, v.secure, v.object, err = c.MarshalString()
return
}
// marshalObjectValue converts the receiver into a shape that is compatible with Value.ToObject().
func (c object) marshalObjectValue(root bool) any {
switch v := c.value.(type) {
case []object:
vs := make([]any, len(v))
for i, v := range v {
vs[i] = v.marshalObjectValue(false)
}
return vs
case map[string]object:
vs := make(map[string]any, len(v))
for k, v := range v {
vs[k] = v.marshalObjectValue(false)
}
return vs
case string:
if !root && c.secure {
return map[string]any{"secure": c.value}
}
return c.value
default:
return c.value
}
}
// MarshalString returns the receiver's string representation. The string representation is accompanied by bools that
// indicate whether the receiver is secure and whether it is an object.
func (c object) MarshalString() (text string, secure, object bool, err error) {
switch v := c.value.(type) {
case bool, int64, float64:
bytes, err := c.MarshalJSON()
return string(bytes), false, false, err
case string:
return v, c.secure, false, nil
default:
bytes, err := c.MarshalJSON()
if err != nil {
return "", false, false, err
}
return string(bytes), c.Secure(), true, nil
}
}
// UnmarshalString unmarshals the string representation accompanied by secure and object metadata into the receiver.
func (c *object) UnmarshalString(text string, secure, object bool) error {
if !object {
c.value, c.secure = text, secure
return nil
}
return c.UnmarshalJSON([]byte(text))
}
func (c object) MarshalJSON() ([]byte, error) {
return json.Marshal(c.marshalObject())
}
func (c *object) UnmarshalJSON(b []byte) error {
dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber()
var v any
err := dec.Decode(&v)
if err != nil {
return err
}
*c, err = unmarshalObject(v)
return err
}
func (c object) MarshalYAML() (any, error) {
return c.marshalObject(), nil
}
func (c *object) UnmarshalYAML(unmarshal func(any) error) error {
var v any
err := unmarshal(&v)
if err != nil {
return err
}
*c, err = unmarshalObject(v)
return err
}
// unmarshalObject unmarshals a raw JSON or YAML value into an object. json.Number values are converted to int64 if
// possible and float64 otherwise.
func unmarshalObject(v any) (object, error) {
switch v := v.(type) {
case bool:
return newObject(v), nil
case json.Number:
if i, err := v.Int64(); err == nil {
return newObject(i), nil
}
f, err := v.Float64()
if err == nil {
return newObject(f), nil
}
return object{}, fmt.Errorf("unrepresentable number %v: %w", v, err)
case int:
return newObject(int64(v)), nil
case int64:
return newObject(v), nil
case float64:
return newObject(v), nil
case string:
return newObject(v), nil
case time.Time:
return newObject(v.String()), nil
case map[string]any:
if ok, ciphertext := isSecureValue(v); ok {
return newSecureObject(ciphertext), nil
}
m := make(map[string]object, len(v))
for k, v := range v {
sv, err := unmarshalObject(v)
if err != nil {
return object{}, err
}
m[k] = sv
}
return newObject(m), nil
case map[any]any:
m := make(map[string]any, len(v))
for k, v := range v {
m[fmt.Sprintf("%v", k)] = v
}
return unmarshalObject(m)
case []any:
a := make([]object, len(v))
for i, v := range v {
sv, err := unmarshalObject(v)
if err != nil {
return object{}, err
}
a[i] = sv
}
return newObject(a), nil
case nil:
return object{}, nil
default:
contract.Failf("unexpected wire type %T", v)
return object{}, nil
}
}
// marshalObject returns the value that should be passed to the JSON or YAML packages when marshaling the receiver.
func (c object) marshalObject() any {
if str, ok := c.value.(string); ok && c.secure {
type secureValue struct {
Secure string `json:"secure" yaml:"secure"`
}
return secureValue{Secure: str}
}
return c.value
}
// isSecureValue returns true if the object is a `map[string]any` of length one with a "secure" property of type string.
func isSecureValue(v any) (bool, string) {
if m, isMap := v.(map[string]any); isMap && len(m) == 1 {
if val, hasSecureKey := m["secure"]; hasSecureKey {
if valString, isString := val.(string); isString {
return true, valString
}
}
}
return false, ""
}
func (c object) toDecryptedPropertyValue(ctx context.Context, decrypter Decrypter) (resource.PropertyValue, error) {
plaintext, err := c.Decrypt(ctx, decrypter)
if err != nil {
return resource.PropertyValue{}, err
}
return plaintext.PropertyValue(), nil
}