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 }