diff --git a/pkg/tfbridge/schema_provider_test.go b/pkg/tfbridge/schema_provider_test.go index 1db4c39ff..75808bb0a 100644 --- a/pkg/tfbridge/schema_provider_test.go +++ b/pkg/tfbridge/schema_provider_test.go @@ -243,7 +243,7 @@ func (f shimv2Factory) newResource(r shim.Resource) *schemav2.Resource { } func (f shimv2Factory) NewResource(r *schema.Resource) shim.Resource { - return shimv2.NewResource(f.newResource(r.Shim())) + return shimv2.NewTestOnlyResource(f.newResource(r.Shim())) } func (f shimv2Factory) NewInstanceState(id string) shim.InstanceState { diff --git a/pkg/tfbridge/schema_test.go b/pkg/tfbridge/schema_test.go index f7931343e..c855357cd 100644 --- a/pkg/tfbridge/schema_test.go +++ b/pkg/tfbridge/schema_test.go @@ -3057,7 +3057,6 @@ func TestMakeTerraformInputsOnMapNestedObjects(t *testing.T) { }, } - shimmedR := shimv2.NewResource(r) type testCase struct { name string ps map[string]*SchemaInfo @@ -3132,7 +3131,7 @@ func TestMakeTerraformInputsOnMapNestedObjects(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { - i, _, err := makeTerraformInputsForConfig(tc.olds, tc.news, shimmedR.Schema(), tc.ps) + i, _, err := makeTerraformInputsForConfig(tc.olds, tc.news, shimv2.NewSchemaMap(r.Schema), tc.ps) require.NoError(t, err) require.Equal(t, tc.expect, i) }) @@ -3161,7 +3160,6 @@ func TestRegress940(t *testing.T) { }, }, } - shimmedR := shimv2.NewResource(r) var olds, news resource.PropertyMap @@ -3175,7 +3173,7 @@ func TestRegress940(t *testing.T) { }), } - result, _, err := makeTerraformInputsForConfig(olds, news, shimmedR.Schema(), map[string]*SchemaInfo{}) + result, _, err := makeTerraformInputsForConfig(olds, news, shimv2.NewSchemaMap(r.Schema), map[string]*SchemaInfo{}) t.Run("no error with empty keys", func(t *testing.T) { assert.NoError(t, err) diff --git a/pkg/tfshim/sdk-v2/flatten.go b/pkg/tfshim/sdk-v2/flatten.go deleted file mode 100644 index 11865a4e8..000000000 --- a/pkg/tfshim/sdk-v2/flatten.go +++ /dev/null @@ -1,71 +0,0 @@ -package sdkv2 - -import ( - "strconv" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" -) - -// flattenValue takes a single value and recursively flattens its properties into the given string -> string map under -// the provided prefix. It expects that the value has been "schema-fied" by being read out of a schema.FieldReader (in -// particular, all sets *must* be represented as schema.Set values). The flattened value may then be used as the value -// of a terraform.InstanceState.Attributes field. -// -// Note that this duplicates much of the logic in TF's schema.MapFieldWriter. Ideally, we would just use that type, -// but there are various API/implementation challenges that preclude that option. The most worrying (and potentially -// fragile) piece of duplication is the code that calculates a set member's hash code; see the code under -// `case *schema.Set`. -func flattenValue(result map[string]string, prefix string, value interface{}) { - if value == nil { - return - } - - switch t := value.(type) { - case bool: - if t { - result[prefix] = "true" - } else { - result[prefix] = "false" - } - case int: - result[prefix] = strconv.FormatInt(int64(t), 10) - case float64: - result[prefix] = strconv.FormatFloat(t, 'G', -1, 64) - case string: - result[prefix] = t - case []interface{}: - // flatten each element. - for i, elem := range t { - flattenValue(result, prefix+"."+strconv.FormatInt(int64(i), 10), elem) - } - - // Set the count. - result[prefix+".#"] = strconv.FormatInt(int64(len(t)), 10) - case *schema.Set: - // flatten each element. - setList := t.List() - for _, elem := range setList { - // Note that the logic below is duplicated from `scheme.Set.hash`. If that logic ever changes, this will - // need to change in kind. - code := t.F(elem) - if code < 0 { - code = -code - } - - flattenValue(result, prefix+"."+strconv.Itoa(code), elem) - } - - // Set the count. - result[prefix+".#"] = strconv.FormatInt(int64(len(setList)), 10) - case map[string]interface{}: - for k, v := range t { - flattenValue(result, prefix+"."+k, v) - } - - // Set the count. - result[prefix+".%"] = strconv.Itoa(len(t)) - default: - contract.Failf("Unexpected TF input value: %v", t) - } -} diff --git a/pkg/tfshim/sdk-v2/instance_state_test.go b/pkg/tfshim/sdk-v2/instance_state_test.go index 3db456af5..6551ffa32 100644 --- a/pkg/tfshim/sdk-v2/instance_state_test.go +++ b/pkg/tfshim/sdk-v2/instance_state_test.go @@ -10,7 +10,7 @@ import ( func TestToInstanceState(t *testing.T) { t.Parallel() - res := NewResource(&schema.Resource{ + res := newElemResource(&schema.Resource{ Schema: map[string]*schema.Schema{ "nil_property_value": {Type: schema.TypeMap}, "bool_property_value": {Type: schema.TypeBool}, @@ -156,7 +156,7 @@ func TestToInstanceState(t *testing.T) { expected := writer.Map() // Build the same using makeTerraformAttributesFromInputs. - res = NewResource(&schema.Resource{Schema: sharedSchema}) + 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) @@ -165,7 +165,7 @@ func TestToInstanceState(t *testing.T) { // Test that an unset list still generates a length attribute. func TestEmptyListAttribute(t *testing.T) { t.Parallel() - res := NewResource(&schema.Resource{ + res := newElemResource(&schema.Resource{ Schema: map[string]*schema.Schema{ "list_property": {Type: schema.TypeList, Optional: true}, }, @@ -180,7 +180,7 @@ func TestEmptyListAttribute(t *testing.T) { func TestObjectFromInstanceDiff(t *testing.T) { t.Parallel() - res := NewResource(&schema.Resource{ + res := newElemResource(&schema.Resource{ Schema: map[string]*schema.Schema{ "nil_property_value": {Type: schema.TypeMap}, "bool_property_value": {Type: schema.TypeBool}, diff --git a/pkg/tfshim/sdk-v2/provider2.go b/pkg/tfshim/sdk-v2/provider2.go index a56d64693..93104413d 100644 --- a/pkg/tfshim/sdk-v2/provider2.go +++ b/pkg/tfshim/sdk-v2/provider2.go @@ -32,6 +32,16 @@ type v2Resource2 struct { resourceType string } +// NewTestOnlyResource is a test-only constructor for v2Resource2. +// New tests should avoid using this and instead construct a v2 Provider with a TF schema. +func NewTestOnlyResource(r *schema.Resource) shim.Resource { + return &v2Resource2{r, nil, ""} +} + +func newElemResource(r *schema.Resource) shim.Resource { + return &v2Resource2{r, nil, ""} +} + var _ shim.Resource = (*v2Resource2)(nil) func (r *v2Resource2) Schema() shim.SchemaMap { @@ -82,11 +92,31 @@ func (r *v2Resource2) DeprecationMessage() string { } func (r *v2Resource2) Timeouts() *shim.ResourceTimeout { - return v2Resource{r.tf}.Timeouts() + if r.tf.Timeouts == nil { + return nil + } + return &shim.ResourceTimeout{ + Create: r.tf.Timeouts.Create, + Read: r.tf.Timeouts.Read, + Update: r.tf.Timeouts.Update, + Delete: r.tf.Timeouts.Delete, + Default: r.tf.Timeouts.Default, + } } func (r *v2Resource2) DecodeTimeouts(config shim.ResourceConfig) (*shim.ResourceTimeout, error) { - return v2Resource{r.tf}.DecodeTimeouts(config) + v2Timeouts := &schema.ResourceTimeout{} + if err := v2Timeouts.ConfigDecode(r.tf, configFromShim(config)); err != nil { + return nil, err + } + + return &shim.ResourceTimeout{ + Create: v2Timeouts.Create, + Read: v2Timeouts.Read, + Update: v2Timeouts.Update, + Delete: v2Timeouts.Delete, + Default: v2Timeouts.Default, + }, nil } type v2InstanceState2 struct { @@ -260,7 +290,12 @@ func (p *planResourceChangeImpl) ResourcesMap() shim.ResourceMap { } func (p *planResourceChangeImpl) DataSourcesMap() shim.ResourceMap { - return v2ResourceMap(p.tf.DataSourcesMap) + return &v2ResourceCustomMap{ + resources: p.tf.DataSourcesMap, + pack: func(token string, res *schema.Resource) shim.Resource { + return &v2Resource2{res, nil, token} + }, + } } func (p *planResourceChangeImpl) InternalValidate() error { @@ -1038,12 +1073,7 @@ func (m *v2ResourceCustomMap) Range(each func(key string, value shim.Resource) b } func (m *v2ResourceCustomMap) Set(key string, value shim.Resource) { - switch r := value.(type) { - case v2Resource: - m.resources[key] = r.tf - case *v2Resource2: - m.resources[key] = r.tf - } + m.resources[key] = value.(*v2Resource2).tf } func NewProvider(p *schema.Provider, opts ...providerOption) shim.Provider { diff --git a/pkg/tfshim/sdk-v2/resource.go b/pkg/tfshim/sdk-v2/resource.go deleted file mode 100644 index 70245fc3f..000000000 --- a/pkg/tfshim/sdk-v2/resource.go +++ /dev/null @@ -1,161 +0,0 @@ -package sdkv2 - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - - shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" -) - -var ( - _ = shim.Resource(v2Resource{}) - _ = shim.ResourceMap(v2ResourceMap{}) -) - -type v2Resource struct { - tf *schema.Resource -} - -func NewResource(r *schema.Resource) shim.Resource { - return v2Resource{r} -} - -func (r v2Resource) Schema() shim.SchemaMap { - return v2SchemaMap(r.tf.SchemaMap()) -} - -func (r v2Resource) SchemaVersion() int { - return r.tf.SchemaVersion -} - -//nolint:staticcheck -func (r v2Resource) Importer() shim.ImportFunc { - if r.tf.Importer == nil { - return nil - } - return func(t, id string, meta interface{}) ([]shim.InstanceState, error) { - data := r.tf.Data(nil) - data.SetId(id) - data.SetType(t) - - var v2Results []*schema.ResourceData - var err error - switch { - case r.tf.Importer.State != nil: - v2Results, err = r.tf.Importer.State(data, meta) - case r.tf.Importer.StateContext != nil: - v2Results, err = r.tf.Importer.StateContext(context.TODO(), data, meta) - } - if err != nil { - return nil, err - } - results := make([]shim.InstanceState, len(v2Results)) - for i, v := range v2Results { - s := v.State() - if s == nil { - return nil, fmt.Errorf("importer for %s returned a empty resource state. This is always "+ - "the result of a bug in the resource provider - please report this "+ - "as a bug in the Pulumi provider repository.", id) - } - if s.Attributes != nil { - results[i] = v2InstanceState{r.tf, s, nil} - } - } - return results, nil - } -} - -func (r v2Resource) DeprecationMessage() string { - return r.tf.DeprecationMessage -} - -func (r v2Resource) Timeouts() *shim.ResourceTimeout { - if r.tf.Timeouts == nil { - return nil - } - return &shim.ResourceTimeout{ - Create: r.tf.Timeouts.Create, - Read: r.tf.Timeouts.Read, - Update: r.tf.Timeouts.Update, - Delete: r.tf.Timeouts.Delete, - Default: r.tf.Timeouts.Default, - } -} - -func (r v2Resource) InstanceState(id string, object, meta map[string]interface{}) (shim.InstanceState, error) { - // Read each top-level value out of the object using a ConfigFieldReader and recursively flatten - // them into their TF attribute form. The result is our set of TF attributes. - config := &terraform.ResourceConfig{Raw: object, Config: object} - attributes := map[string]string{} - reader := &schema.ConfigFieldReader{Config: config, Schema: r.tf.SchemaMap()} - for k := range r.tf.SchemaMap() { - // Elide nil values. - if v, ok := object[k]; ok && v == nil { - continue - } - - f, err := reader.ReadField([]string{k}) - if err != nil { - return nil, fmt.Errorf("could not read field %v: %w", k, err) - } - - flattenValue(attributes, k, f.Value) - } - - return v2InstanceState{ - r.tf, - &terraform.InstanceState{ - ID: id, - Attributes: attributes, - Meta: meta, - }, nil, - }, nil -} - -func (r v2Resource) DecodeTimeouts(config shim.ResourceConfig) (*shim.ResourceTimeout, error) { - v2Timeouts := &schema.ResourceTimeout{} - if err := v2Timeouts.ConfigDecode(r.tf, configFromShim(config)); err != nil { - return nil, err - } - - return &shim.ResourceTimeout{ - Create: v2Timeouts.Create, - Read: v2Timeouts.Read, - Update: v2Timeouts.Update, - Delete: v2Timeouts.Delete, - Default: v2Timeouts.Default, - }, nil -} - -type v2ResourceMap map[string]*schema.Resource - -func (m v2ResourceMap) Len() int { - return len(m) -} - -func (m v2ResourceMap) Get(key string) shim.Resource { - r, _ := m.GetOk(key) - return r -} - -func (m v2ResourceMap) GetOk(key string) (shim.Resource, bool) { - if r, ok := m[key]; ok { - return v2Resource{r}, true - } - return nil, false -} - -func (m v2ResourceMap) Range(each func(key string, value shim.Resource) bool) { - for key, value := range m { - if !each(key, v2Resource{value}) { - return - } - } -} - -func (m v2ResourceMap) Set(key string, value shim.Resource) { - m[key] = value.(v2Resource).tf -} diff --git a/pkg/tfshim/sdk-v2/schema.go b/pkg/tfshim/sdk-v2/schema.go index 0c1bac8de..313972dda 100644 --- a/pkg/tfshim/sdk-v2/schema.go +++ b/pkg/tfshim/sdk-v2/schema.go @@ -86,7 +86,7 @@ func (s v2Schema) StateFunc() shim.SchemaStateFunc { func (s v2Schema) Elem() interface{} { switch e := s.tf.Elem.(type) { case *schema.Resource: - return v2Resource{e} + return newElemResource(e) case *schema.Schema: return v2Schema{e} default: