mirror of https://github.com/pulumi/pulumi.git
534 lines
13 KiB
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
|
|
}
|