pulumi/pkg/resource/stack/deployment_test.go
Luke Hoban 8587f5410e
Error instead of assert on invalid resource in state file (#7065)
* Error instead of assert on invalid resource in state file

Fixes #6955

* Add CHANGELOG
2021-05-17 09:47:28 +01:00

438 lines
15 KiB
Go

// Copyright 2016-2018, 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 stack
import (
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)
// TestDeploymentSerialization creates a basic snapshot of a given resource state.
func TestDeploymentSerialization(t *testing.T) {
res := resource.NewState(
tokens.Type("Test"),
resource.NewURN(
tokens.QName("test"),
tokens.PackageName("resource/test"),
tokens.Type(""),
tokens.Type("Test"),
tokens.QName("resource-x"),
),
true,
false,
resource.ID("test-resource-x"),
resource.NewPropertyMapFromMap(map[string]interface{}{
"in-nil": nil,
"in-bool": true,
"in-float64": float64(1.5),
"in-string": "lumilumilo",
"in-array": []interface{}{"a", true, float64(32)},
"in-empty-array": []interface{}{},
"in-map": map[string]interface{}{
"a": true,
"b": float64(88),
"c": "c-see-saw",
"d": "d-dee-daw",
},
"in-empty-map": map[string]interface{}{},
"in-component-resource-reference": resource.MakeComponentResourceReference("urn", "1.2.3").V,
"in-custom-resource-reference": resource.MakeCustomResourceReference("urn2", "id", "2.3.4").V,
"in-custom-resource-reference-unknown-id": resource.MakeCustomResourceReference("urn3", "", "3.4.5").V,
}),
resource.NewPropertyMapFromMap(map[string]interface{}{
"out-nil": nil,
"out-bool": false,
"out-float64": float64(76),
"out-string": "loyolumiloom",
"out-array": []interface{}{false, "zzxx"},
"out-empty-array": []interface{}{},
"out-map": map[string]interface{}{
"x": false,
"y": "z-zee-zaw",
"z": float64(999.9),
},
"out-empty-map": map[string]interface{}{},
}),
"",
false,
false,
[]resource.URN{
resource.URN("foo:bar:baz"),
resource.URN("foo:bar:boo"),
},
[]string{},
"",
nil,
false,
nil,
nil,
nil,
"",
)
dep, err := SerializeResource(res, config.NopEncrypter, false /* showSecrets */)
assert.NoError(t, err)
// assert some things about the deployment record:
assert.NotNil(t, dep)
assert.NotNil(t, dep.ID)
assert.Equal(t, resource.ID("test-resource-x"), dep.ID)
assert.Equal(t, tokens.Type("Test"), dep.Type)
assert.Equal(t, 2, len(dep.Dependencies))
assert.Equal(t, resource.URN("foo:bar:baz"), dep.Dependencies[0])
assert.Equal(t, resource.URN("foo:bar:boo"), dep.Dependencies[1])
// assert some things about the inputs:
assert.NotNil(t, dep.Inputs)
assert.Nil(t, dep.Inputs["in-nil"])
assert.NotNil(t, dep.Inputs["in-bool"])
assert.True(t, dep.Inputs["in-bool"].(bool))
assert.NotNil(t, dep.Inputs["in-float64"])
assert.Equal(t, float64(1.5), dep.Inputs["in-float64"].(float64))
assert.NotNil(t, dep.Inputs["in-string"])
assert.Equal(t, "lumilumilo", dep.Inputs["in-string"].(string))
assert.NotNil(t, dep.Inputs["in-array"])
assert.Equal(t, 3, len(dep.Inputs["in-array"].([]interface{})))
assert.Equal(t, "a", dep.Inputs["in-array"].([]interface{})[0])
assert.Equal(t, true, dep.Inputs["in-array"].([]interface{})[1])
assert.Equal(t, float64(32), dep.Inputs["in-array"].([]interface{})[2])
assert.NotNil(t, dep.Inputs["in-empty-array"])
assert.Equal(t, 0, len(dep.Inputs["in-empty-array"].([]interface{})))
assert.NotNil(t, dep.Inputs["in-map"])
inmap := dep.Inputs["in-map"].(map[string]interface{})
assert.Equal(t, 4, len(inmap))
assert.NotNil(t, inmap["a"])
assert.Equal(t, true, inmap["a"].(bool))
assert.NotNil(t, inmap["b"])
assert.Equal(t, float64(88), inmap["b"].(float64))
assert.NotNil(t, inmap["c"])
assert.Equal(t, "c-see-saw", inmap["c"].(string))
assert.NotNil(t, inmap["d"])
assert.Equal(t, "d-dee-daw", inmap["d"].(string))
assert.NotNil(t, dep.Inputs["in-empty-map"])
assert.Equal(t, 0, len(dep.Inputs["in-empty-map"].(map[string]interface{})))
assert.Equal(t, map[string]interface{}{
resource.SigKey: resource.ResourceReferenceSig,
"urn": "urn",
"packageVersion": "1.2.3",
}, dep.Inputs["in-component-resource-reference"])
assert.Equal(t, map[string]interface{}{
resource.SigKey: resource.ResourceReferenceSig,
"urn": "urn2",
"id": "id",
"packageVersion": "2.3.4",
}, dep.Inputs["in-custom-resource-reference"])
assert.Equal(t, map[string]interface{}{
resource.SigKey: resource.ResourceReferenceSig,
"urn": "urn3",
"id": "",
"packageVersion": "3.4.5",
}, dep.Inputs["in-custom-resource-reference-unknown-id"])
// assert some things about the outputs:
assert.NotNil(t, dep.Outputs)
assert.Nil(t, dep.Outputs["out-nil"])
assert.NotNil(t, dep.Outputs["out-bool"])
assert.False(t, dep.Outputs["out-bool"].(bool))
assert.NotNil(t, dep.Outputs["out-float64"])
assert.Equal(t, float64(76), dep.Outputs["out-float64"].(float64))
assert.NotNil(t, dep.Outputs["out-string"])
assert.Equal(t, "loyolumiloom", dep.Outputs["out-string"].(string))
assert.NotNil(t, dep.Outputs["out-array"])
assert.Equal(t, 2, len(dep.Outputs["out-array"].([]interface{})))
assert.Equal(t, false, dep.Outputs["out-array"].([]interface{})[0])
assert.Equal(t, "zzxx", dep.Outputs["out-array"].([]interface{})[1])
assert.NotNil(t, dep.Outputs["out-empty-array"])
assert.Equal(t, 0, len(dep.Outputs["out-empty-array"].([]interface{})))
assert.NotNil(t, dep.Outputs["out-map"])
outmap := dep.Outputs["out-map"].(map[string]interface{})
assert.Equal(t, 3, len(outmap))
assert.NotNil(t, outmap["x"])
assert.Equal(t, false, outmap["x"].(bool))
assert.NotNil(t, outmap["y"])
assert.Equal(t, "z-zee-zaw", outmap["y"].(string))
assert.NotNil(t, outmap["z"])
assert.Equal(t, float64(999.9), outmap["z"].(float64))
assert.NotNil(t, dep.Outputs["out-empty-map"])
assert.Equal(t, 0, len(dep.Outputs["out-empty-map"].(map[string]interface{})))
}
func TestLoadTooNewDeployment(t *testing.T) {
untypedDeployment := &apitype.UntypedDeployment{
Version: apitype.DeploymentSchemaVersionCurrent + 1,
}
deployment, err := DeserializeUntypedDeployment(untypedDeployment, DefaultSecretsProvider)
assert.Nil(t, deployment)
assert.Error(t, err)
assert.Equal(t, ErrDeploymentSchemaVersionTooNew, err)
}
func TestLoadTooOldDeployment(t *testing.T) {
untypedDeployment := &apitype.UntypedDeployment{
Version: DeploymentSchemaVersionOldestSupported - 1,
}
deployment, err := DeserializeUntypedDeployment(untypedDeployment, DefaultSecretsProvider)
assert.Nil(t, deployment)
assert.Error(t, err)
assert.Equal(t, ErrDeploymentSchemaVersionTooOld, err)
}
func TestUnsupportedSecret(t *testing.T) {
rawProp := map[string]interface{}{
resource.SigKey: resource.SecretSig,
}
_, err := DeserializePropertyValue(rawProp, config.NewPanicCrypter(), config.NewPanicCrypter())
assert.Error(t, err)
}
func TestUnknownSig(t *testing.T) {
rawProp := map[string]interface{}{
resource.SigKey: "foobar",
}
_, err := DeserializePropertyValue(rawProp, config.NewPanicCrypter(), config.NewPanicCrypter())
assert.Error(t, err)
}
// TestDeserializeResourceReferencePropertyValueID tests the ability of the deserializer to handle resource references
// that were serialized without unwrapping their ID PropertyValue due to a bug in the serializer. Such resource
// references were produced by Pulumi v2.18.0.
func TestDeserializeResourceReferencePropertyValueID(t *testing.T) {
// Serialize replicates Pulumi 2.18.0's buggy resource reference serializer. We round-trip the value through JSON
// in order to convert the ID property value into a plain map[string]interface{}.
serialize := func(v resource.PropertyValue) interface{} {
ref := v.ResourceReferenceValue()
bytes, err := json.Marshal(map[string]interface{}{
resource.SigKey: resource.ResourceReferenceSig,
"urn": ref.URN,
"id": ref.ID,
"packageVersion": ref.PackageVersion,
})
contract.IgnoreError(err)
var sv interface{}
err = json.Unmarshal(bytes, &sv)
contract.IgnoreError(err)
return sv
}
serialized := map[string]interface{}{
"component-resource": serialize(resource.MakeComponentResourceReference("urn", "1.2.3")),
"custom-resource": serialize(resource.MakeCustomResourceReference("urn2", "id", "2.3.4")),
"custom-resource-unknown-id": serialize(resource.MakeCustomResourceReference("urn3", "", "3.4.5")),
}
deserialized, err := DeserializePropertyValue(serialized, config.NewPanicCrypter(), config.NewPanicCrypter())
assert.NoError(t, err)
assert.Equal(t, resource.NewPropertyValue(map[string]interface{}{
"component-resource": resource.MakeComponentResourceReference("urn", "1.2.3").V,
"custom-resource": resource.MakeCustomResourceReference("urn2", "id", "2.3.4").V,
"custom-resource-unknown-id": resource.MakeCustomResourceReference("urn3", "", "3.4.5").V,
}), deserialized)
}
func TestCustomSerialization(t *testing.T) {
textAsset, err := resource.NewTextAsset("alpha beta gamma")
assert.NoError(t, err)
strProp := resource.NewStringProperty("strProp")
computed := resource.Computed{Element: strProp}
output := resource.Output{Element: strProp}
secret := &resource.Secret{Element: strProp}
propMap := resource.NewPropertyMapFromMap(map[string]interface{}{
// Primitive types
"nil": nil,
"bool": true,
"int32": int64(41),
"int64": int64(42),
"float32": float32(2.5),
"float64": float64(1.5),
"string": "string literal",
// Data structures
"array": []interface{}{"a", true, float64(32)},
"array-empty": []interface{}{},
"map": map[string]interface{}{
"a": true,
"b": float64(88),
"c": "c-see-saw",
"d": "d-dee-daw",
},
"map-empty": map[string]interface{}{},
// Specialized resource types
"asset-text": textAsset,
"computed": computed,
"output": output,
"secret": secret,
})
assert.True(t, propMap.ContainsSecrets())
assert.True(t, propMap.ContainsUnknowns())
// Confirm the expected shape of serializing a ResourceProperty and PropertyMap using the
// reflection-based default JSON encoder. This should NOT be used when serializing resources,
// but we confirm the expected shape here while we migrate older code that relied on the
// specific format.
t.Run("SerializeToJSON", func(t *testing.T) {
b, err := json.Marshal(propMap)
if err != nil {
t.Fatalf("Marshalling PropertyMap: %v", err)
}
json := string(b)
// Look for the specific JSON serialization of the properties.
tests := []string{
// Primitives
`"nil":{"V":null}`,
`"bool":{"V":true}`,
`"string":{"V":"string literal"}}`,
`"float32":{"V":2.5}`,
`"float64":{"V":1.5}`,
`"int32":{"V":41}`,
`"int64":{"V":42}`,
// Data structures
`array":{"V":[{"V":"a"},{"V":true},{"V":32}]}`,
`"array-empty":{"V":[]}`,
`"map":{"V":{"a":{"V":true},"b":{"V":88},"c":{"V":"c-see-saw"},"d":{"V":"d-dee-daw"}}}`,
`"map-empty":{"V":{}}`,
// Specialized resource types
// nolint: lll
`"asset-text":{"V":{"4dabf18193072939515e22adb298388d":"c44067f5952c0a294b673a41bacd8c17","hash":"64989ccbf3efa9c84e2afe7cee9bc5828bf0fcb91e44f8c1e591638a2c2e90e3","text":"alpha beta gamma"}}`,
`"computed":{"V":{"Element":{"V":"strProp"}}}`,
`"output":{"V":{"Element":{"V":"strProp"}}}`,
`"secret":{"V":{"Element":{"V":"strProp"}}}`,
}
for _, want := range tests {
if !strings.Contains(json, want) {
t.Errorf("Did not find expected snippet: %v", want)
}
}
if t.Failed() {
t.Logf("Full JSON encoding:\n%v", json)
}
})
// Using stack.SerializeProperties will get the correct behavior and should be used
// whenever persisting resources into some durable form.
t.Run("SerializeProperties", func(t *testing.T) {
serializedPropMap, err := SerializeProperties(propMap, config.BlindingCrypter, false /* showSecrets */)
assert.NoError(t, err)
// Now JSON encode the results?
b, err := json.Marshal(serializedPropMap)
if err != nil {
t.Fatalf("Marshalling PropertyMap: %v", err)
}
json := string(b)
// Look for the specific JSON serialization of the properties.
tests := []string{
// Primitives
`"bool":true`,
`"string":"string literal"`,
`"float32":2.5`,
`"float64":1.5`,
`"int32":41`,
`"int64":42`,
`"nil":null`,
// Data structures
`"array":["a",true,32]`,
`"array-empty":[]`,
`"map":{"a":true,"b":88,"c":"c-see-saw","d":"d-dee-daw"}`,
`"map-empty":{}`,
// Specialized resource types
// nolint: lll
`"asset-text":{"4dabf18193072939515e22adb298388d":"c44067f5952c0a294b673a41bacd8c17","hash":"64989ccbf3efa9c84e2afe7cee9bc5828bf0fcb91e44f8c1e591638a2c2e90e3","text":"alpha beta gamma"}`,
// Computed values are replaced with a magic constant.
`"computed":"04da6b54-80e4-46f7-96ec-b56ff0331ba9"`,
`"output":"04da6b54-80e4-46f7-96ec-b56ff0331ba9"`,
// Secrets are serialized with the special sig key, and their underlying cipher text.
// Since we passed in a config.BlindingCrypter the cipher text isn't super-useful.
`"secret":{"4dabf18193072939515e22adb298388d":"1b47061264138c4ac30d75fd1eb44270","ciphertext":"[secret]"}`,
}
for _, want := range tests {
if !strings.Contains(json, want) {
t.Errorf("Did not find expected snippet: %v", want)
}
}
if t.Failed() {
t.Logf("Full JSON encoding:\n%v", json)
}
})
}
func TestDeserializeInvalidResourceErrors(t *testing.T) {
deployment, err := DeserializeDeploymentV3(apitype.DeploymentV3{
Resources: []apitype.ResourceV3{
{},
},
}, DefaultSecretsProvider)
assert.Nil(t, deployment)
assert.Error(t, err)
assert.Equal(t, "resource missing required 'urn' field", err.Error())
urn := "urn:pulumi:prod::acme::acme:erp:Backend$aws:ebs/volume:Volume::PlatformBackendDb"
deployment, err = DeserializeDeploymentV3(apitype.DeploymentV3{
Resources: []apitype.ResourceV3{
{
URN: resource.URN(urn),
},
},
}, DefaultSecretsProvider)
assert.Nil(t, deployment)
assert.Error(t, err)
assert.Equal(t, fmt.Sprintf("resource '%s' missing required 'type' field", urn), err.Error())
deployment, err = DeserializeDeploymentV3(apitype.DeploymentV3{
Resources: []apitype.ResourceV3{
{
URN: resource.URN(urn),
Type: "aws:ebs/volume:Volume",
Custom: false,
ID: "vol-044ba5ad2bd959bc1",
},
},
}, DefaultSecretsProvider)
assert.Nil(t, deployment)
assert.Error(t, err)
assert.Equal(t, fmt.Sprintf("resource '%s' has 'custom' false but non-empty ID", urn), err.Error())
}