github.com/pulumi/pulumi/sdk/v3@v3.108.1/go/common/resource/config/repr_test.go (about)

     1  // Copyright 2016-2018, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package config
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/base64"
    20  	"encoding/gob"
    21  	"encoding/json"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
    28  	"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  	yaml "gopkg.in/yaml.v2"
    32  )
    33  
    34  // mapPaths returns the set of all possible paths in c.
    35  func mapPaths(t *testing.T, c Map) []Key {
    36  	//nolint:prealloc
    37  	var paths []Key
    38  	for k, v := range c {
    39  		obj, err := v.ToObject()
    40  		require.NoError(t, err)
    41  
    42  		paths = append(paths, k)
    43  		for _, p := range valuePaths(obj) {
    44  			p = append(resource.PropertyPath{k.Name()}, p...)
    45  			paths = append(paths, MustMakeKey(k.Namespace(), p.String()))
    46  		}
    47  	}
    48  	return paths
    49  }
    50  
    51  // valuePaths returns the set of all possible paths in o.
    52  func valuePaths(o any) []resource.PropertyPath {
    53  	switch o := o.(type) {
    54  	case []any:
    55  		var paths []resource.PropertyPath
    56  		for i, v := range o {
    57  			paths = append(paths, resource.PropertyPath{i})
    58  			for _, p := range valuePaths(v) {
    59  				paths = append(paths, append(resource.PropertyPath{i}, p...))
    60  			}
    61  		}
    62  		return paths
    63  	case map[string]any:
    64  		isSecure, _ := isSecureValue(o)
    65  		if isSecure {
    66  			return nil
    67  		}
    68  
    69  		var paths []resource.PropertyPath
    70  		for k, v := range o {
    71  			paths = append(paths, resource.PropertyPath{k})
    72  			for _, p := range valuePaths(v) {
    73  				paths = append(paths, append(resource.PropertyPath{k}, p...))
    74  			}
    75  		}
    76  		return paths
    77  	default:
    78  		return nil
    79  	}
    80  }
    81  
    82  // A gobObject is a gob-encoded Go object that marshals into YAML as a base64-encoded string.
    83  //
    84  // It is intended to encode the result of Value.ToObject while preserving type information (hence the use of gobs
    85  // rather than e.g. JSON).
    86  type gobObject struct {
    87  	value any
    88  }
    89  
    90  func init() {
    91  	gob.Register([]any(nil))
    92  	gob.Register(map[string]any(nil))
    93  }
    94  
    95  func (o gobObject) MarshalYAML() (any, error) {
    96  	var data bytes.Buffer
    97  	if err := gob.NewEncoder(&data).Encode(&o.value); err != nil {
    98  		return nil, err
    99  	}
   100  	b64 := base64.StdEncoding.EncodeToString(data.Bytes())
   101  	return b64, nil
   102  }
   103  
   104  func (o *gobObject) UnmarshalYAML(unmarshal func(v any) error) error {
   105  	var b64 string
   106  	if err := unmarshal(&b64); err != nil {
   107  		return err
   108  	}
   109  	data, err := base64.StdEncoding.DecodeString(b64)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	return gob.NewDecoder(bytes.NewReader(data)).Decode(&o.value)
   114  }
   115  
   116  func TestRepr(t *testing.T) {
   117  	t.Parallel()
   118  
   119  	type expectedValue struct {
   120  		Value        Value     `yaml:"value"`                  // The raw Value
   121  		String       string    `yaml:"string"`                 // The result of Value.Value(NopDecrypter)
   122  		Redacted     string    `yaml:"redacted"`               // The result of Value.Value(NewBlindingDecrypter)
   123  		Object       gobObject `yaml:"object"`                 // The result of Value.ToObject()
   124  		Secure       bool      `yaml:"secure"`                 // The result of Value.Secure()
   125  		IsObject     bool      `yaml:"isObject"`               // The result of Value.Object()
   126  		SecureValues []string  `yaml:"secureValues,omitempty"` // The result of Value.SecureValues()
   127  	}
   128  
   129  	type expectedRepr struct {
   130  		Decrypt map[string]string        `yaml:"decrypt"` // The result of Map.Decrypt
   131  		Paths   map[string]expectedValue `yaml:"paths"`   // Each path in the map and information about its value
   132  	}
   133  
   134  	isAccept := cmdutil.IsTruthy(os.Getenv("PULUMI_ACCEPT"))
   135  
   136  	root := filepath.Join("testdata", "repr")
   137  	entries, err := os.ReadDir(root)
   138  	require.NoError(t, err)
   139  
   140  	// For each entry in testdata/repr, load the YAML representation and generate the JSON representation and the
   141  	// actual results of calling most of the Value APIs. Then, either write the results out as the new ground
   142  	// truth (if PULUMI_ACCEPT is truthy) or load the expected results and validate against the actual results.
   143  	for _, entry := range entries {
   144  		id, ok := strings.CutSuffix(entry.Name(), ".yaml")
   145  		if !ok || strings.HasSuffix(id, ".expected") {
   146  			continue
   147  		}
   148  		basePath := filepath.Join(root, id)
   149  
   150  		t.Run(id, func(t *testing.T) {
   151  			t.Parallel()
   152  
   153  			expectedYAMLBytes, err := os.ReadFile(basePath + ".yaml")
   154  			require.NoError(t, err)
   155  
   156  			var c Map
   157  			err = yaml.Unmarshal(expectedYAMLBytes, &c)
   158  			require.NoError(t, err)
   159  
   160  			yamlBytes, err := yaml.Marshal(c)
   161  			require.NoError(t, err)
   162  
   163  			jsonBytes, err := json.Marshal(c)
   164  			require.NoError(t, err)
   165  
   166  			decrypted, err := c.Decrypt(NopDecrypter)
   167  			require.NoError(t, err)
   168  
   169  			decryptedMap := make(map[string]string)
   170  			for k, v := range decrypted {
   171  				decryptedMap[k.String()] = v
   172  			}
   173  
   174  			paths := make(map[string]expectedValue)
   175  			for _, p := range mapPaths(t, c) {
   176  				v, _, _ := c.Get(p, true)
   177  
   178  				value, err := v.Value(NopDecrypter)
   179  				require.NoError(t, err)
   180  
   181  				redacted, err := v.Value(NewBlindingDecrypter())
   182  				require.NoError(t, err)
   183  
   184  				vo, err := v.ToObject()
   185  				require.NoError(t, err)
   186  
   187  				secureValues, err := v.SecureValues(NopDecrypter)
   188  				require.NoError(t, err)
   189  
   190  				paths[p.String()] = expectedValue{
   191  					Value:        v,
   192  					String:       value,
   193  					Redacted:     redacted,
   194  					Object:       gobObject{value: vo},
   195  					Secure:       v.Secure(),
   196  					IsObject:     v.Object(),
   197  					SecureValues: secureValues,
   198  				}
   199  			}
   200  
   201  			actual := expectedRepr{
   202  				Decrypt: decryptedMap,
   203  				Paths:   paths,
   204  			}
   205  
   206  			if isAccept {
   207  				expectedBytes, err := yaml.Marshal(actual)
   208  				require.NoError(t, err)
   209  
   210  				err = os.WriteFile(basePath+".json", jsonBytes, 0o600)
   211  				require.NoError(t, err)
   212  
   213  				err = os.WriteFile(basePath+".expected.yaml", expectedBytes, 0o600)
   214  				require.NoError(t, err)
   215  			} else {
   216  				expectedJSONBytes, err := os.ReadFile(basePath + ".json")
   217  				require.NoError(t, err)
   218  
   219  				var expected expectedRepr
   220  				expectedBytes, err := os.ReadFile(basePath + ".expected.yaml")
   221  				require.NoError(t, err)
   222  				err = yaml.Unmarshal(expectedBytes, &expected)
   223  				require.NoError(t, err)
   224  
   225  				assert.Equal(t, expectedYAMLBytes, yamlBytes)
   226  				assert.Equal(t, expectedJSONBytes, jsonBytes)
   227  				assert.Equal(t, expected, actual)
   228  			}
   229  		})
   230  	}
   231  }