diff --git a/pkg/tfbridge/schema_provider_test.go b/pkg/tfbridge/schema_provider_test.go index 75808bb0a..692f5aa16 100644 --- a/pkg/tfbridge/schema_provider_test.go +++ b/pkg/tfbridge/schema_provider_test.go @@ -247,7 +247,7 @@ func (f shimv2Factory) NewResource(r *schema.Resource) shim.Resource { } func (f shimv2Factory) NewInstanceState(id string) shim.InstanceState { - return shimv2.NewInstanceState(&terraformv2.InstanceState{ + return shimv2.NewTestOnlyInstanceState(&terraformv2.InstanceState{ ID: id, Attributes: map[string]string{}, Meta: map[string]interface{}{}, }) } diff --git a/pkg/tfshim/sdk-v2/instance_state.go b/pkg/tfshim/sdk-v2/instance_state.go index b4def4cf1..67af80a5c 100644 --- a/pkg/tfshim/sdk-v2/instance_state.go +++ b/pkg/tfshim/sdk-v2/instance_state.go @@ -17,56 +17,11 @@ package sdkv2 import ( "bytes" "encoding/json" - "strings" "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" - diff_reader "github.com/pulumi/terraform-diff-reader/sdk-v2" - - shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" ) -var _ = shim.InstanceState(v2InstanceState{}) - -type v2InstanceState struct { - resource *schema.Resource - tf *terraform.InstanceState - diff *terraform.InstanceDiff -} - -func NewInstanceState(s *terraform.InstanceState) shim.InstanceState { - return v2InstanceState{tf: s} -} - -func NewInstanceStateForResource(s *terraform.InstanceState, resource *schema.Resource) shim.InstanceState { - return v2InstanceState{ - resource: resource, - tf: s, - diff: nil, - } -} - -func IsInstanceState(s shim.InstanceState) (*terraform.InstanceState, bool) { - if is, ok := s.(v2InstanceState); ok { - return is.tf, true - } - return nil, false -} - -func (s v2InstanceState) Type() string { - return s.tf.Ephemeral.Type -} - -func (s v2InstanceState) ID() string { - return s.tf.ID -} - -func (s v2InstanceState) Object(sch shim.SchemaMap) (map[string]interface{}, error) { - return s.objectV1(sch) -} - // This is needed because json.Unmarshal uses float64 for numbers by default which truncates int64 numbers. func unmarshalJSON(data []byte, v interface{}) error { dec := json.NewDecoder(bytes.NewReader(data)) @@ -94,76 +49,3 @@ func objectFromCtyValue(v cty.Value) map[string]interface{} { return m } - -// The legacy version of Object used custom Pulumi code forked from TF sources. -func (s v2InstanceState) objectV1(sch shim.SchemaMap) (map[string]interface{}, error) { - obj := make(map[string]interface{}) - - schemaMap := map[string]*schema.Schema(sch.(v2SchemaMap)) - - attrs := s.tf.Attributes - - var reader schema.FieldReader = &schema.MapFieldReader{ - Schema: schemaMap, - Map: schema.BasicMapReader(attrs), - } - - // If this is a state + a diff, use a diff reader rather than a map reader. - if s.diff != nil { - reader = &diff_reader.DiffFieldReader{ - Diff: s.diff, - Schema: schemaMap, - Source: reader, - } - } - - // Read each top-level field out of the attributes. - keys := make(map[string]bool) - readAttributeField := func(key string) error { - // Pull the top-level field out of this attribute key. If we've already read the top-level field, skip - // this key. - dot := strings.Index(key, ".") - if dot != -1 { - key = key[:dot] - } - if _, ok := keys[key]; ok { - return nil - } - keys[key] = true - - // Read the top-level attribute for this key. - res, err := reader.ReadField([]string{key}) - if err != nil { - return err - } - if res.Value != nil && !res.Computed { - obj[key] = res.Value - } - return nil - } - - for key := range attrs { - if err := readAttributeField(key); err != nil { - return nil, err - } - } - if s.diff != nil { - for key := range s.diff.Attributes { - if err := readAttributeField(key); err != nil { - return nil, err - } - } - } - - // Populate the "id" property if it is not set. Most schemas do not include this property, and leaving it out - // can cause unnecessary diffs when refreshing/updating resources after a provider upgrade. - if _, ok := obj["id"]; !ok { - obj["id"] = attrs["id"] - } - - return obj, nil -} - -func (s v2InstanceState) Meta() map[string]interface{} { - return s.tf.Meta -} diff --git a/pkg/tfshim/sdk-v2/instance_state_test.go b/pkg/tfshim/sdk-v2/instance_state_test.go deleted file mode 100644 index 6551ffa32..000000000 --- a/pkg/tfshim/sdk-v2/instance_state_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package sdkv2 - -import ( - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "github.com/stretchr/testify/assert" -) - -func TestToInstanceState(t *testing.T) { - t.Parallel() - res := newElemResource(&schema.Resource{ - Schema: map[string]*schema.Schema{ - "nil_property_value": {Type: schema.TypeMap}, - "bool_property_value": {Type: schema.TypeBool}, - "number_property_value": {Type: schema.TypeInt}, - "float_property_value": {Type: schema.TypeFloat}, - "string_property_value": {Type: schema.TypeString}, - "array_property_value": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "object_property_value": {Type: schema.TypeMap}, - "map_property_value": {Type: schema.TypeMap}, - "nested_resources": { - Type: schema.TypeList, - MaxItems: 1, - // Embed a `*schema.Resource` to validate that type directed - // walk of the schema successfully walks inside Resources as well - // as Schemas. - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "configuration": {Type: schema.TypeMap}, - }, - }, - }, - "set_property_value": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "string_with_bad_interpolation": {Type: schema.TypeString}, - }, - }) - - state, err := res.InstanceState("id", map[string]interface{}{ - "nil_property_value": nil, - "bool_property_value": false, - "number_property_value": 42, - "float_property_value": 99.6767932, - "string_property_value": "ognirts", - "array_property_value": []interface{}{"an array"}, - "object_property_value": map[string]interface{}{ - "property_a": "a", - "property_b": true, - }, - "map_property_value": map[string]interface{}{ - "propertyA": "a", - "propertyB": true, - "propertyC": map[string]interface{}{ - "nestedPropertyA": true, - }, - }, - "nested_resources": []interface{}{ - map[string]interface{}{ - "configuration": map[string]interface{}{ - "configurationValue": true, - }, - }, - }, - "set_property_value": []interface{}{"set member 1", "set member 2"}, - "string_with_bad_interpolation": "some ${interpolated:value} with syntax errors", - }, nil) - - assert.NoError(t, err) - assert.Equal(t, state.(v2InstanceState).tf.Attributes, map[string]string{ - "array_property_value.#": "1", - "array_property_value.0": "an array", - "bool_property_value": "false", - "float_property_value": "99.6767932", - "map_property_value.%": "3", - "map_property_value.propertyA": "a", - "map_property_value.propertyB": "true", - "map_property_value.propertyC.%": "1", - "map_property_value.propertyC.nestedPropertyA": "true", - "nested_resources.#": "1", - "nested_resources.0.%": "1", - "nested_resources.0.configuration.%": "1", - "nested_resources.0.configuration.configurationValue": "true", - "number_property_value": "42", - "object_property_value.%": "2", - "object_property_value.property_a": "a", - "object_property_value.property_b": "true", - "set_property_value.#": "2", - "set_property_value.3618983862": "set member 2", - "set_property_value.4237827189": "set member 1", - "string_property_value": "ognirts", - "string_with_bad_interpolation": "some ${interpolated:value} with syntax errors", - }) - - // MapFieldWriter has issues with values of TypeMap. Build a schema without such values s.t. we can test - // MakeTerraformState against the output of MapFieldWriter. - sharedSchema := map[string]*schema.Schema{ - "bool_property_value": {Type: schema.TypeBool}, - "number_property_value": {Type: schema.TypeInt}, - "float_property_value": {Type: schema.TypeFloat}, - "string_property_value": {Type: schema.TypeString}, - "array_property_value": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "nested_resource_value": { - Type: schema.TypeList, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "nested_set_property": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "nested_string_property": {Type: schema.TypeString}, - }, - }, - }, - "set_property_value": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "string_with_bad_interpolation": {Type: schema.TypeString}, - } - sharedInputs := map[string]interface{}{ - "bool_property_value": false, - "number_property_value": 42, - "float_property_value": 99.6767932, - "string_property_value": "ognirts", - "array_property_value": []interface{}{"an array"}, - "nested_resource_value": map[string]interface{}{ - "nested_set_property": []interface{}{"nested set member"}, - "nested_string_property": "value", - }, - "set_property_value": []interface{}{"set member 1", "set member 2"}, - "string_with_bad_interpolation": "some ${interpolated:value} with syntax errors", - } - - // Build a TF attribute map using schema.MapFieldWriter. - cfg := terraform.NewResourceConfigRaw(sharedInputs) - reader := &schema.ConfigFieldReader{Config: cfg, Schema: sharedSchema} - writer := &schema.MapFieldWriter{Schema: sharedSchema} - for k := range sharedInputs { - f, ferr := reader.ReadField([]string{k}) - assert.NoError(t, ferr) - - err = writer.WriteField([]string{k}, f.Value) - assert.NoError(t, err) - } - expected := writer.Map() - - // Build the same using makeTerraformAttributesFromInputs. - res = newElemResource(&schema.Resource{Schema: sharedSchema}) - state, err = res.InstanceState("id", sharedInputs, nil) - assert.NoError(t, err) - assert.Equal(t, expected, state.(v2InstanceState).tf.Attributes) -} - -// Test that an unset list still generates a length attribute. -func TestEmptyListAttribute(t *testing.T) { - t.Parallel() - res := newElemResource(&schema.Resource{ - Schema: map[string]*schema.Schema{ - "list_property": {Type: schema.TypeList, Optional: true}, - }, - }) - - state, err := res.InstanceState("id", map[string]interface{}{}, nil) - assert.NoError(t, err) - assert.Equal(t, state.(v2InstanceState).tf.Attributes, map[string]string{ - "list_property.#": "0", - }) -} - -func TestObjectFromInstanceDiff(t *testing.T) { - t.Parallel() - res := newElemResource(&schema.Resource{ - Schema: map[string]*schema.Schema{ - "nil_property_value": {Type: schema.TypeMap}, - "bool_property_value": {Type: schema.TypeBool}, - "number_property_value": {Type: schema.TypeInt}, - "float_property_value": {Type: schema.TypeFloat}, - "string_property_value": {Type: schema.TypeString}, - "array_property_value": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "object_property_value": {Type: schema.TypeMap}, - "map_property_value": {Type: schema.TypeMap}, - "nested_resources": { - Type: schema.TypeList, - MaxItems: 1, - // Embed a `*schema.Resource` to validate that type directed - // walk of the schema successfully walks inside Resources as well - // as Schemas. - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "configuration": {Type: schema.TypeMap}, - }, - }, - }, - "set_property_value": { - Type: schema.TypeSet, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "resource_set_property_value": { - Type: schema.TypeSet, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "name": {Type: schema.TypeString}, - "labels": {Type: schema.TypeList, Elem: &schema.Schema{Type: schema.TypeInt}}, - }, - }, - }, - "string_with_bad_interpolation": {Type: schema.TypeString}, - "removed_property_value": { - Type: schema.TypeString, - }, - }, - }) - - state, err := res.InstanceState("id", map[string]interface{}{ - "nil_property_value": nil, - "bool_property_value": false, - "number_property_value": 42, - "float_property_value": 99.6767932, - "string_property_value": "ognirts", - "array_property_value": []interface{}{"an array"}, - "object_property_value": map[string]interface{}{ - "property_a": "a", - "property_b": true, - }, - "map_property_value": map[string]interface{}{ - "propertyA": "a", - "propertyB": true, - "propertyC": map[string]interface{}{ - "nestedPropertyA": true, - }, - }, - "nested_resources": []interface{}{ - map[string]interface{}{ - "configuration": map[string]interface{}{ - "configurationValue": true, - }, - }, - }, - "set_property_value": []interface{}{"set member 1", "set member 2"}, - "resource_set_property_value": []interface{}{ - map[string]interface{}{ - "name": "someName", - "labels": []interface{}{42}, - }, - }, - "string_with_bad_interpolation": "some ${interpolated:value} with syntax errors", - "removed_property_value": "a removed property", - }, nil) - assert.NoError(t, err) - - s := state.(v2InstanceState) - s.diff = &terraform.InstanceDiff{ - Attributes: map[string]*terraform.ResourceAttrDiff{ - "number_property_value": { - Old: "42", - New: UnknownVariableValue, - NewComputed: true, - }, - "object_property_value.property_a": { - Old: "a", - New: UnknownVariableValue, - NewComputed: true, - }, - "map_property_value.%": { - Old: "3", - New: UnknownVariableValue, - NewComputed: true, - }, - "nested_resources.0.configuration.configurationValue": { - Old: "true", - New: UnknownVariableValue, - NewComputed: true, - }, - "set_property_value.1234": { - New: UnknownVariableValue, - NewComputed: true, - }, - "resource_set_property_value.2450673662.labels.0": { - New: UnknownVariableValue, - NewComputed: true, - }, - }, - } - - obj, err := s.Object(res.Schema()) - assert.NoError(t, err) - - assert.Equal(t, map[string]interface{}{ - "id": "", - "bool_property_value": false, - "float_property_value": 99.6767932, - "string_property_value": "ognirts", - "array_property_value": []interface{}{"an array"}, - "object_property_value": map[string]interface{}{ - "property_a": UnknownVariableValue, - "property_b": "true", - }, - "nested_resources": []interface{}{ - map[string]interface{}{ - "configuration": map[string]interface{}{ - "configurationValue": UnknownVariableValue, - }, - }, - }, - "string_with_bad_interpolation": "some ${interpolated:value} with syntax errors", - "removed_property_value": "a removed property", - }, obj) -} diff --git a/pkg/tfshim/sdk-v2/provider.go b/pkg/tfshim/sdk-v2/provider.go index 36a52ca00..98bbdb0ef 100644 --- a/pkg/tfshim/sdk-v2/provider.go +++ b/pkg/tfshim/sdk-v2/provider.go @@ -18,7 +18,11 @@ func stateToShim(r *schema.Resource, s *terraform.InstanceState) shim.InstanceSt if s == nil { return nil } - return NewInstanceStateForResource(s, r) + return &v2InstanceState2{ + resourceType: s.Ephemeral.Type, + stateValue: s.RawState, + meta: s.Meta, + } } func diffFromShim(d shim.InstanceDiff) *terraform.InstanceDiff { diff --git a/pkg/tfshim/sdk-v2/provider2.go b/pkg/tfshim/sdk-v2/provider2.go index 93104413d..6118fabc4 100644 --- a/pkg/tfshim/sdk-v2/provider2.go +++ b/pkg/tfshim/sdk-v2/provider2.go @@ -119,6 +119,16 @@ func (r *v2Resource2) DecodeTimeouts(config shim.ResourceConfig) (*shim.Resource }, nil } +// NewTestOnlyInstanceState is a test-only constructor for v2InstanceState2. +// New tests should avoid using this and instead construct a v2 Provider with a TF schema. +func NewTestOnlyInstanceState(s *terraform.InstanceState) shim.InstanceState { + return &v2InstanceState2{ + resourceType: s.Ephemeral.Type, + stateValue: s.RawState, + meta: s.Meta, + } +} + type v2InstanceState2 struct { resourceType string stateValue cty.Value