github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/resource/stack/deployment.go (about) 1 // Copyright 2016-2022, 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 stack 16 17 import ( 18 "context" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "reflect" 25 "strings" 26 27 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 28 "github.com/pulumi/pulumi/pkg/v3/secrets" 29 "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" 30 "github.com/pulumi/pulumi/sdk/v3/go/common/apitype/migrate" 31 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 32 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" 33 "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" 34 "github.com/santhosh-tekuri/jsonschema/v5" 35 ) 36 37 const ( 38 // DeploymentSchemaVersionOldestSupported is the oldest deployment schema that we 39 // still support, i.e. we can produce a `deploy.Snapshot` from. This will generally 40 // need to be at least one less than the current schema version so that old deployments can 41 // be migrated to the current schema. 42 DeploymentSchemaVersionOldestSupported = 1 43 44 // computedValue is a magic number we emit for a value of a resource.Property value 45 // whenever we need to serialize a resource.Computed. (Since the real/actual value 46 // is not known.) This allows us to persist engine events and resource states that 47 // indicate a value will changed... but is unknown what it will change to. 48 computedValuePlaceholder = "04da6b54-80e4-46f7-96ec-b56ff0331ba9" 49 ) 50 51 var ( 52 // ErrDeploymentSchemaVersionTooOld is returned from `DeserializeDeployment` if the 53 // untyped deployment being deserialized is too old to understand. 54 ErrDeploymentSchemaVersionTooOld = fmt.Errorf("this stack's deployment is too old") 55 56 // ErrDeploymentSchemaVersionTooNew is returned from `DeserializeDeployment` if the 57 // untyped deployment being deserialized is too new to understand. 58 ErrDeploymentSchemaVersionTooNew = fmt.Errorf("this stack's deployment version is too new") 59 ) 60 61 var deploymentSchema *jsonschema.Schema 62 var resourceSchema *jsonschema.Schema 63 var propertyValueSchema *jsonschema.Schema 64 65 func init() { 66 compiler := jsonschema.NewCompiler() 67 compiler.LoadURL = func(s string) (io.ReadCloser, error) { 68 var schema string 69 switch s { 70 case apitype.DeploymentSchemaID: 71 schema = apitype.DeploymentSchema() 72 case apitype.ResourceSchemaID: 73 schema = apitype.ResourceSchema() 74 case apitype.PropertyValueSchemaID: 75 schema = apitype.PropertyValueSchema() 76 default: 77 return jsonschema.LoadURL(s) 78 } 79 return ioutil.NopCloser(strings.NewReader(schema)), nil 80 } 81 deploymentSchema = compiler.MustCompile(apitype.DeploymentSchemaID) 82 resourceSchema = compiler.MustCompile(apitype.ResourceSchemaID) 83 propertyValueSchema = compiler.MustCompile(apitype.PropertyValueSchemaID) 84 } 85 86 // ValidateUntypedDeployment validates a deployment against the Deployment JSON schema. 87 func ValidateUntypedDeployment(deployment *apitype.UntypedDeployment) error { 88 bytes, err := json.Marshal(deployment) 89 if err != nil { 90 return err 91 } 92 93 var raw interface{} 94 if err := json.Unmarshal(bytes, &raw); err != nil { 95 return err 96 } 97 98 return deploymentSchema.Validate(raw) 99 } 100 101 // SerializeDeployment serializes an entire snapshot as a deploy record. 102 func SerializeDeployment(snap *deploy.Snapshot, sm secrets.Manager, showSecrets bool) (*apitype.DeploymentV3, error) { 103 contract.Require(snap != nil, "snap") 104 105 // Capture the version information into a manifest. 106 manifest := snap.Manifest.Serialize() 107 108 // If a specific secrets manager was not provided, use the one in the snapshot, if present. 109 if sm == nil { 110 sm = snap.SecretsManager 111 } 112 113 var enc config.Encrypter 114 if sm != nil { 115 e, err := sm.Encrypter() 116 if err != nil { 117 return nil, fmt.Errorf("getting encrypter for deployment: %w", err) 118 } 119 enc = e 120 } else { 121 enc = config.NewPanicCrypter() 122 } 123 124 // Serialize all vertices and only include a vertex section if non-empty. 125 var resources []apitype.ResourceV3 126 for _, res := range snap.Resources { 127 sres, err := SerializeResource(res, enc, showSecrets) 128 if err != nil { 129 return nil, fmt.Errorf("serializing resources: %w", err) 130 } 131 resources = append(resources, sres) 132 } 133 134 var operations []apitype.OperationV2 135 for _, op := range snap.PendingOperations { 136 sop, err := SerializeOperation(op, enc, showSecrets) 137 if err != nil { 138 return nil, err 139 } 140 operations = append(operations, sop) 141 } 142 143 var secretsProvider *apitype.SecretsProvidersV1 144 if sm != nil { 145 secretsProvider = &apitype.SecretsProvidersV1{ 146 Type: sm.Type(), 147 } 148 if state := sm.State(); state != nil { 149 rm, err := json.Marshal(state) 150 if err != nil { 151 return nil, err 152 } 153 secretsProvider.State = rm 154 } 155 } 156 157 return &apitype.DeploymentV3{ 158 Manifest: manifest, 159 Resources: resources, 160 SecretsProviders: secretsProvider, 161 PendingOperations: operations, 162 }, nil 163 } 164 165 // DeserializeUntypedDeployment deserializes an untyped deployment and produces a `deploy.Snapshot` 166 // from it. DeserializeDeployment will return an error if the untyped deployment's version is 167 // not within the range `DeploymentSchemaVersionCurrent` and `DeploymentSchemaVersionOldestSupported`. 168 func DeserializeUntypedDeployment( 169 ctx context.Context, 170 deployment *apitype.UntypedDeployment, 171 secretsProv SecretsProvider) (*deploy.Snapshot, error) { 172 173 contract.Require(deployment != nil, "deployment") 174 switch { 175 case deployment.Version > apitype.DeploymentSchemaVersionCurrent: 176 return nil, ErrDeploymentSchemaVersionTooNew 177 case deployment.Version < DeploymentSchemaVersionOldestSupported: 178 return nil, ErrDeploymentSchemaVersionTooOld 179 } 180 181 var v3deployment apitype.DeploymentV3 182 switch deployment.Version { 183 case 1: 184 var v1deployment apitype.DeploymentV1 185 if err := json.Unmarshal([]byte(deployment.Deployment), &v1deployment); err != nil { 186 return nil, err 187 } 188 v2deployment := migrate.UpToDeploymentV2(v1deployment) 189 v3deployment = migrate.UpToDeploymentV3(v2deployment) 190 case 2: 191 var v2deployment apitype.DeploymentV2 192 if err := json.Unmarshal([]byte(deployment.Deployment), &v2deployment); err != nil { 193 return nil, err 194 } 195 v3deployment = migrate.UpToDeploymentV3(v2deployment) 196 case 3: 197 if err := json.Unmarshal([]byte(deployment.Deployment), &v3deployment); err != nil { 198 return nil, err 199 } 200 default: 201 contract.Failf("unrecognized version: %d", deployment.Version) 202 } 203 204 return DeserializeDeploymentV3(ctx, v3deployment, secretsProv) 205 } 206 207 // DeserializeDeploymentV3 deserializes a typed DeploymentV3 into a `deploy.Snapshot`. 208 func DeserializeDeploymentV3( 209 ctx context.Context, 210 deployment apitype.DeploymentV3, 211 secretsProv SecretsProvider) (*deploy.Snapshot, error) { 212 213 // Unpack the versions. 214 manifest, err := deploy.DeserializeManifest(deployment.Manifest) 215 if err != nil { 216 return nil, err 217 } 218 219 var secretsManager secrets.Manager 220 if deployment.SecretsProviders != nil && deployment.SecretsProviders.Type != "" { 221 if secretsProv == nil { 222 return nil, errors.New("deployment uses a SecretsProvider but no SecretsProvider was provided") 223 } 224 225 sm, err := secretsProv.OfType(deployment.SecretsProviders.Type, deployment.SecretsProviders.State) 226 if err != nil { 227 return nil, err 228 } 229 secretsManager = sm 230 } 231 232 var dec config.Decrypter 233 var enc config.Encrypter 234 if secretsManager == nil { 235 dec = config.NewPanicCrypter() 236 enc = config.NewPanicCrypter() 237 } else { 238 d, err := secretsManager.Decrypter() 239 if err != nil { 240 return nil, err 241 } 242 243 // Do a first pass through state and collect all of the secrets that need decrypting. 244 // We will collect all secrets and decrypt them all at once, rather than just-in-time. 245 // We do this to avoid serial calls to the decryption endpoint which can result in long 246 // wait times in stacks with a large number of secrets. 247 var ciphertexts []string 248 for _, res := range deployment.Resources { 249 collectCiphertexts(&ciphertexts, res.Inputs) 250 collectCiphertexts(&ciphertexts, res.Outputs) 251 } 252 253 // Decrypt the collected secrets and create a decrypter that will use the result as a cache. 254 cache, err := d.BulkDecrypt(ctx, ciphertexts) 255 if err != nil { 256 return nil, err 257 } 258 dec = newMapDecrypter(d, cache) 259 260 e, err := secretsManager.Encrypter() 261 if err != nil { 262 return nil, err 263 } 264 enc = e 265 } 266 267 // For every serialized resource vertex, create a ResourceDeployment out of it. 268 var resources []*resource.State 269 for _, res := range deployment.Resources { 270 desres, err := DeserializeResource(res, dec, enc) 271 if err != nil { 272 return nil, err 273 } 274 resources = append(resources, desres) 275 } 276 277 var ops []resource.Operation 278 for _, op := range deployment.PendingOperations { 279 desop, err := DeserializeOperation(op, dec, enc) 280 if err != nil { 281 return nil, err 282 } 283 ops = append(ops, desop) 284 } 285 286 return deploy.NewSnapshot(*manifest, secretsManager, resources, ops), nil 287 } 288 289 // SerializeResource turns a resource into a structure suitable for serialization. 290 func SerializeResource(res *resource.State, enc config.Encrypter, showSecrets bool) (apitype.ResourceV3, error) { 291 contract.Assert(res != nil) 292 contract.Assertf(string(res.URN) != "", "Unexpected empty resource resource.URN") 293 294 // Serialize all input and output properties recursively, and add them if non-empty. 295 var inputs map[string]interface{} 296 if inp := res.Inputs; inp != nil { 297 sinp, err := SerializeProperties(inp, enc, showSecrets) 298 if err != nil { 299 return apitype.ResourceV3{}, err 300 } 301 inputs = sinp 302 } 303 var outputs map[string]interface{} 304 if outp := res.Outputs; outp != nil { 305 soutp, err := SerializeProperties(outp, enc, showSecrets) 306 if err != nil { 307 return apitype.ResourceV3{}, err 308 } 309 outputs = soutp 310 } 311 312 v3Resource := apitype.ResourceV3{ 313 URN: res.URN, 314 Custom: res.Custom, 315 Delete: res.Delete, 316 ID: res.ID, 317 Type: res.Type, 318 Parent: res.Parent, 319 Inputs: inputs, 320 Outputs: outputs, 321 Protect: res.Protect, 322 External: res.External, 323 Dependencies: res.Dependencies, 324 InitErrors: res.InitErrors, 325 Provider: res.Provider, 326 PropertyDependencies: res.PropertyDependencies, 327 PendingReplacement: res.PendingReplacement, 328 AdditionalSecretOutputs: res.AdditionalSecretOutputs, 329 Aliases: res.Aliases, 330 ImportID: res.ImportID, 331 RetainOnDelete: res.RetainOnDelete, 332 DeletedWith: res.DeletedWith, 333 } 334 335 if res.CustomTimeouts.IsNotEmpty() { 336 v3Resource.CustomTimeouts = &res.CustomTimeouts 337 } 338 339 return v3Resource, nil 340 } 341 342 func SerializeOperation(op resource.Operation, enc config.Encrypter, showSecrets bool) (apitype.OperationV2, error) { 343 res, err := SerializeResource(op.Resource, enc, showSecrets) 344 if err != nil { 345 return apitype.OperationV2{}, fmt.Errorf("serializing resource: %w", err) 346 } 347 return apitype.OperationV2{ 348 Resource: res, 349 Type: apitype.OperationType(op.Type), 350 }, nil 351 } 352 353 // SerializeProperties serializes a resource property bag so that it's suitable for serialization. 354 func SerializeProperties(props resource.PropertyMap, enc config.Encrypter, 355 showSecrets bool) (map[string]interface{}, error) { 356 dst := make(map[string]interface{}) 357 for _, k := range props.StableKeys() { 358 v, err := SerializePropertyValue(props[k], enc, showSecrets) 359 if err != nil { 360 return nil, err 361 } 362 dst[string(k)] = v 363 } 364 return dst, nil 365 } 366 367 // SerializePropertyValue serializes a resource property value so that it's suitable for serialization. 368 func SerializePropertyValue(prop resource.PropertyValue, enc config.Encrypter, 369 showSecrets bool) (interface{}, error) { 370 ctx := context.TODO() 371 372 // Serialize nulls as nil. 373 if prop.IsNull() { 374 return nil, nil 375 } 376 377 // A computed value marks something that will be determined at a later time. (e.g. the result of 378 // a computation that we don't perform during a preview operation.) We serialize a magic constant 379 // to record its existence. 380 if prop.IsComputed() || prop.IsOutput() { 381 return computedValuePlaceholder, nil 382 } 383 384 // For arrays, make sure to recurse. 385 if prop.IsArray() { 386 srcarr := prop.ArrayValue() 387 dstarr := make([]interface{}, len(srcarr)) 388 for i, elem := range prop.ArrayValue() { 389 selem, err := SerializePropertyValue(elem, enc, showSecrets) 390 if err != nil { 391 return nil, err 392 } 393 dstarr[i] = selem 394 } 395 return dstarr, nil 396 } 397 398 // Also for objects, recurse and use naked properties. 399 if prop.IsObject() { 400 return SerializeProperties(prop.ObjectValue(), enc, showSecrets) 401 } 402 403 // For assets, we need to serialize them a little carefully, so we can recover them afterwards. 404 if prop.IsAsset() { 405 return prop.AssetValue().Serialize(), nil 406 } else if prop.IsArchive() { 407 return prop.ArchiveValue().Serialize(), nil 408 } 409 410 // We serialize resource references using a map-based representation similar to assets, archives, and secrets. 411 if prop.IsResourceReference() { 412 ref := prop.ResourceReferenceValue() 413 serialized := map[string]interface{}{ 414 resource.SigKey: resource.ResourceReferenceSig, 415 "urn": string(ref.URN), 416 "packageVersion": ref.PackageVersion, 417 } 418 if id, hasID := ref.IDString(); hasID { 419 serialized["id"] = id 420 } 421 return serialized, nil 422 } 423 424 if prop.IsSecret() { 425 // Since we are going to encrypt property value, we can elide encrypting sub-elements. We'll mark them as 426 // "secret" so we retain that information when deserializing the overall structure, but there is no 427 // need to double encrypt everything. 428 value, err := SerializePropertyValue(prop.SecretValue().Element, config.NopEncrypter, showSecrets) 429 if err != nil { 430 return nil, err 431 } 432 bytes, err := json.Marshal(value) 433 if err != nil { 434 return nil, fmt.Errorf("encoding serialized property value: %w", err) 435 } 436 plaintext := string(bytes) 437 438 // If the encrypter is a cachingCrypter, call through its encryptSecret method, which will look for a matching 439 // *resource.Secret + plaintext in its cache in order to avoid re-encrypting the value. 440 var ciphertext string 441 if cachingCrypter, ok := enc.(*cachingCrypter); ok { 442 ciphertext, err = cachingCrypter.encryptSecret(prop.SecretValue(), plaintext) 443 } else { 444 ciphertext, err = enc.EncryptValue(ctx, plaintext) 445 } 446 if err != nil { 447 return nil, fmt.Errorf("failed to encrypt secret value: %w", err) 448 } 449 contract.AssertNoErrorf(err, "marshalling underlying secret value to JSON") 450 451 secret := apitype.SecretV1{ 452 Sig: resource.SecretSig, 453 } 454 455 if showSecrets { 456 secret.Plaintext = plaintext 457 } else { 458 secret.Ciphertext = ciphertext 459 } 460 461 return secret, nil 462 } 463 464 // All others are returned as-is. 465 return prop.V, nil 466 } 467 468 // collectCiphertexts collects encrypted secrets from resource properties. 469 func collectCiphertexts(ciphertexts *[]string, prop interface{}) { 470 switch prop := prop.(type) { 471 case []interface{}: 472 for _, v := range prop { 473 collectCiphertexts(ciphertexts, v) 474 } 475 case map[string]interface{}: 476 if prop[resource.SigKey] == resource.SecretSig { 477 if ciphertext, cipherOk := prop["ciphertext"].(string); cipherOk { 478 *ciphertexts = append(*ciphertexts, ciphertext) 479 } 480 } else { 481 for _, v := range prop { 482 collectCiphertexts(ciphertexts, v) 483 } 484 } 485 } 486 } 487 488 // DeserializeResource turns a serialized resource back into its usual form. 489 func DeserializeResource(res apitype.ResourceV3, dec config.Decrypter, enc config.Encrypter) (*resource.State, error) { 490 // Deserialize the resource properties, if they exist. 491 inputs, err := DeserializeProperties(res.Inputs, dec, enc) 492 if err != nil { 493 return nil, err 494 } 495 outputs, err := DeserializeProperties(res.Outputs, dec, enc) 496 if err != nil { 497 return nil, err 498 } 499 500 if res.URN == "" { 501 return nil, fmt.Errorf("resource missing required 'urn' field") 502 } 503 504 if res.Type == "" { 505 return nil, fmt.Errorf("resource '%s' missing required 'type' field", res.URN) 506 } 507 508 if !res.Custom && res.ID != "" { 509 return nil, fmt.Errorf("resource '%s' has 'custom' false but non-empty ID", res.URN) 510 } 511 512 return resource.NewState( 513 res.Type, res.URN, res.Custom, res.Delete, res.ID, 514 inputs, outputs, res.Parent, res.Protect, res.External, res.Dependencies, res.InitErrors, res.Provider, 515 res.PropertyDependencies, res.PendingReplacement, res.AdditionalSecretOutputs, res.Aliases, res.CustomTimeouts, 516 res.ImportID, res.RetainOnDelete, res.DeletedWith), nil 517 } 518 519 func DeserializeOperation(op apitype.OperationV2, dec config.Decrypter, 520 enc config.Encrypter) (resource.Operation, error) { 521 res, err := DeserializeResource(op.Resource, dec, enc) 522 if err != nil { 523 return resource.Operation{}, err 524 } 525 return resource.NewOperation(res, resource.OperationType(op.Type)), nil 526 } 527 528 // DeserializeProperties deserializes an entire map of deploy properties into a resource property map. 529 func DeserializeProperties(props map[string]interface{}, dec config.Decrypter, 530 enc config.Encrypter) (resource.PropertyMap, error) { 531 result := make(resource.PropertyMap) 532 for k, prop := range props { 533 desprop, err := DeserializePropertyValue(prop, dec, enc) 534 if err != nil { 535 return nil, err 536 } 537 result[resource.PropertyKey(k)] = desprop 538 } 539 return result, nil 540 } 541 542 // DeserializePropertyValue deserializes a single deploy property into a resource property value. 543 func DeserializePropertyValue(v interface{}, dec config.Decrypter, 544 enc config.Encrypter) (resource.PropertyValue, error) { 545 ctx := context.TODO() 546 if v != nil { 547 switch w := v.(type) { 548 case bool: 549 return resource.NewBoolProperty(w), nil 550 case float64: 551 return resource.NewNumberProperty(w), nil 552 case string: 553 if w == computedValuePlaceholder { 554 return resource.MakeComputed(resource.NewStringProperty("")), nil 555 } 556 return resource.NewStringProperty(w), nil 557 case []interface{}: 558 var arr []resource.PropertyValue 559 for _, elem := range w { 560 ev, err := DeserializePropertyValue(elem, dec, enc) 561 if err != nil { 562 return resource.PropertyValue{}, err 563 } 564 arr = append(arr, ev) 565 } 566 return resource.NewArrayProperty(arr), nil 567 case map[string]interface{}: 568 obj, err := DeserializeProperties(w, dec, enc) 569 if err != nil { 570 return resource.PropertyValue{}, err 571 } 572 573 // This could be an asset or archive; if so, recover its type. 574 objmap := obj.Mappable() 575 if sig, hasSig := objmap[resource.SigKey]; hasSig { 576 switch sig { 577 case resource.AssetSig: 578 asset, isasset, err := resource.DeserializeAsset(objmap) 579 if err != nil { 580 return resource.PropertyValue{}, err 581 } 582 contract.Assert(isasset) 583 return resource.NewAssetProperty(asset), nil 584 case resource.ArchiveSig: 585 archive, isarchive, err := resource.DeserializeArchive(objmap) 586 if err != nil { 587 return resource.PropertyValue{}, err 588 } 589 contract.Assert(isarchive) 590 return resource.NewArchiveProperty(archive), nil 591 case resource.SecretSig: 592 ciphertext, cipherOk := objmap["ciphertext"].(string) 593 plaintext, plainOk := objmap["plaintext"].(string) 594 if (!cipherOk && !plainOk) || (plainOk && cipherOk) { 595 return resource.PropertyValue{}, errors.New( 596 "malformed secret value: one of `ciphertext` or `plaintext` must be supplied") 597 } 598 599 if plainOk { 600 encryptedText, err := enc.EncryptValue(ctx, plaintext) 601 if err != nil { 602 return resource.PropertyValue{}, fmt.Errorf("encrypting secret value: %w", err) 603 } 604 ciphertext = encryptedText 605 606 } else { 607 unencryptedText, err := dec.DecryptValue(ctx, ciphertext) 608 if err != nil { 609 return resource.PropertyValue{}, fmt.Errorf("error decrypting secret value: %s", err.Error()) 610 } 611 plaintext = unencryptedText 612 } 613 614 var elem interface{} 615 616 if err := json.Unmarshal([]byte(plaintext), &elem); err != nil { 617 return resource.PropertyValue{}, err 618 } 619 ev, err := DeserializePropertyValue(elem, config.NopDecrypter, enc) 620 if err != nil { 621 return resource.PropertyValue{}, err 622 } 623 prop := resource.MakeSecret(ev) 624 // If the decrypter is a cachingCrypter, insert the plain- and ciphertext into the cache with the 625 // new *resource.Secret as the key. 626 if cachingCrypter, ok := dec.(*cachingCrypter); ok { 627 cachingCrypter.insert(prop.SecretValue(), plaintext, ciphertext) 628 } 629 return prop, nil 630 case resource.ResourceReferenceSig: 631 var packageVersion string 632 if packageVersionV, ok := objmap["packageVersion"]; ok { 633 packageVersion, ok = packageVersionV.(string) 634 if !ok { 635 return resource.PropertyValue{}, 636 errors.New("malformed resource reference: packageVersion must be a string") 637 } 638 } 639 640 urnStr, ok := objmap["urn"].(string) 641 if !ok { 642 return resource.PropertyValue{}, errors.New("malformed resource reference: missing urn") 643 } 644 urn := resource.URN(urnStr) 645 646 // deserializeID handles two cases, one of which arose from a bug in a refactoring of resource.ResourceReference. 647 // This bug caused the raw ID PropertyValue to be serialized as a map[string]interface{}. In the normal case, the 648 // ID is serialized as a string. 649 deserializeID := func() (string, bool, error) { 650 idV, ok := objmap["id"] 651 if !ok { 652 return "", false, nil 653 } 654 655 switch idV := idV.(type) { 656 case string: 657 return idV, true, nil 658 case map[string]interface{}: 659 switch v := idV["V"].(type) { 660 case nil: 661 // This happens for component resource references, which do not have an associated ID. 662 return "", false, nil 663 case string: 664 // This happens for custom resource references, which do have an associated ID. 665 return v, true, nil 666 case map[string]interface{}: 667 // This happens for custom resource references with an unknown ID. In this case, the ID should be 668 // deserialized as the empty string. 669 return "", true, nil 670 } 671 } 672 return "", false, errors.New("malformed resource reference: id must be a string") 673 } 674 675 id, hasID, err := deserializeID() 676 if err != nil { 677 return resource.PropertyValue{}, err 678 } 679 if hasID { 680 return resource.MakeCustomResourceReference(urn, resource.ID(id), packageVersion), nil 681 } 682 return resource.MakeComponentResourceReference(urn, packageVersion), nil 683 default: 684 return resource.PropertyValue{}, fmt.Errorf("unrecognized signature '%v' in property map", sig) 685 } 686 } 687 688 // Otherwise, it's just a weakly typed object map. 689 return resource.NewObjectProperty(obj), nil 690 default: 691 contract.Failf("Unrecognized property type %T: %v", v, reflect.ValueOf(v)) 692 } 693 } 694 695 return resource.NewNullProperty(), nil 696 }