diff --git a/pf/tfgen/testdata/TestTypeOverride/attr-list-element.golden b/pf/tfgen/testdata/TestTypeOverride/attr-list-element.golden new file mode 100644 index 0000000000..302841192d --- /dev/null +++ b/pf/tfgen/testdata/TestTypeOverride/attr-list-element.golden @@ -0,0 +1,41 @@ +{ + "name": "testprovider", + "attribution": "This Pulumi package is based on the [`testprovider` Terraform Provider](https://github.com/terraform-providers/terraform-provider-testprovider).", + "meta": { + "moduleFormat": "(.*)(?:/[^/]*)" + }, + "config": {}, + "provider": {}, + "resources": { + "testprovider:index:Res": { + "properties": { + "a1s": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "inputProperties": { + "a1s": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "a1s": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/pf/tfgen/testdata/TestTypeOverride/attr-list-max-items-one.golden b/pf/tfgen/testdata/TestTypeOverride/attr-list-max-items-one.golden new file mode 100644 index 0000000000..96b4ce3175 --- /dev/null +++ b/pf/tfgen/testdata/TestTypeOverride/attr-list-max-items-one.golden @@ -0,0 +1,32 @@ +{ + "name": "testprovider", + "attribution": "This Pulumi package is based on the [`testprovider` Terraform Provider](https://github.com/terraform-providers/terraform-provider-testprovider).", + "meta": { + "moduleFormat": "(.*)(?:/[^/]*)" + }, + "config": {}, + "provider": {}, + "resources": { + "testprovider:index:Res": { + "properties": { + "a1": { + "type": "string" + } + }, + "inputProperties": { + "a1": { + "type": "string" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "a1": { + "type": "string" + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/pf/tfgen/testdata/TestTypeOverride/attr-map-element.golden b/pf/tfgen/testdata/TestTypeOverride/attr-map-element.golden new file mode 100644 index 0000000000..fc77035f4f --- /dev/null +++ b/pf/tfgen/testdata/TestTypeOverride/attr-map-element.golden @@ -0,0 +1,41 @@ +{ + "name": "testprovider", + "attribution": "This Pulumi package is based on the [`testprovider` Terraform Provider](https://github.com/terraform-providers/terraform-provider-testprovider).", + "meta": { + "moduleFormat": "(.*)(?:/[^/]*)" + }, + "config": {}, + "provider": {}, + "resources": { + "testprovider:index:Res": { + "properties": { + "a1": { + "type": "object", + "additionalProperties": { + "type": "number" + } + } + }, + "inputProperties": { + "a1": { + "type": "object", + "additionalProperties": { + "type": "number" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "a1": { + "type": "object", + "additionalProperties": { + "type": "number" + } + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/pf/tfgen/testdata/TestTypeOverride/attr-map-object-element.golden b/pf/tfgen/testdata/TestTypeOverride/attr-map-object-element.golden new file mode 100644 index 0000000000..9e0073c550 --- /dev/null +++ b/pf/tfgen/testdata/TestTypeOverride/attr-map-object-element.golden @@ -0,0 +1,51 @@ +{ + "name": "testprovider", + "attribution": "This Pulumi package is based on the [`testprovider` Terraform Provider](https://github.com/terraform-providers/terraform-provider-testprovider).", + "meta": { + "moduleFormat": "(.*)(?:/[^/]*)" + }, + "config": {}, + "types": { + "testprovider:index/ResA1:ResA1": { + "properties": { + "n1": { + "type": "number" + } + }, + "type": "object" + } + }, + "provider": {}, + "resources": { + "testprovider:index:Res": { + "properties": { + "a1": { + "type": "object", + "additionalProperties": { + "$ref": "#/types/testprovider:index/ResA1:ResA1" + } + } + }, + "inputProperties": { + "a1": { + "type": "object", + "additionalProperties": { + "$ref": "#/types/testprovider:index/ResA1:ResA1" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "a1": { + "type": "object", + "additionalProperties": { + "$ref": "#/types/testprovider:index/ResA1:ResA1" + } + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/pf/tfgen/testdata/TestTypeOverride/attr-set-element.golden b/pf/tfgen/testdata/TestTypeOverride/attr-set-element.golden new file mode 100644 index 0000000000..302841192d --- /dev/null +++ b/pf/tfgen/testdata/TestTypeOverride/attr-set-element.golden @@ -0,0 +1,41 @@ +{ + "name": "testprovider", + "attribution": "This Pulumi package is based on the [`testprovider` Terraform Provider](https://github.com/terraform-providers/terraform-provider-testprovider).", + "meta": { + "moduleFormat": "(.*)(?:/[^/]*)" + }, + "config": {}, + "provider": {}, + "resources": { + "testprovider:index:Res": { + "properties": { + "a1s": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "inputProperties": { + "a1s": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "a1s": { + "type": "array", + "items": { + "type": "number" + } + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/pf/tfgen/testdata/TestTypeOverride/attr-single-nested-object-element.golden b/pf/tfgen/testdata/TestTypeOverride/attr-single-nested-object-element.golden new file mode 100644 index 0000000000..e7001ac2fa --- /dev/null +++ b/pf/tfgen/testdata/TestTypeOverride/attr-single-nested-object-element.golden @@ -0,0 +1,42 @@ +{ + "name": "testprovider", + "attribution": "This Pulumi package is based on the [`testprovider` Terraform Provider](https://github.com/terraform-providers/terraform-provider-testprovider).", + "meta": { + "moduleFormat": "(.*)(?:/[^/]*)" + }, + "config": {}, + "types": { + "testprovider:index/ResA1:ResA1": { + "properties": { + "n1": { + "type": "number" + } + }, + "type": "object" + } + }, + "provider": {}, + "resources": { + "testprovider:index:Res": { + "properties": { + "a1": { + "$ref": "#/types/testprovider:index/ResA1:ResA1" + } + }, + "inputProperties": { + "a1": { + "$ref": "#/types/testprovider:index/ResA1:ResA1" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "a1": { + "$ref": "#/types/testprovider:index/ResA1:ResA1" + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/pf/tfgen/testdata/TestTypeOverride/attr-single-nested-object.golden b/pf/tfgen/testdata/TestTypeOverride/attr-single-nested-object.golden new file mode 100644 index 0000000000..d08fabbddb --- /dev/null +++ b/pf/tfgen/testdata/TestTypeOverride/attr-single-nested-object.golden @@ -0,0 +1,42 @@ +{ + "name": "testprovider", + "attribution": "This Pulumi package is based on the [`testprovider` Terraform Provider](https://github.com/terraform-providers/terraform-provider-testprovider).", + "meta": { + "moduleFormat": "(.*)(?:/[^/]*)" + }, + "config": {}, + "types": { + "testprovider:index/ResA1:ResA1": { + "properties": { + "n1": { + "type": "string" + } + }, + "type": "object" + } + }, + "provider": {}, + "resources": { + "testprovider:index:Res": { + "properties": { + "a1": { + "$ref": "#/types/testprovider:index:SomeOtherType" + } + }, + "inputProperties": { + "a1": { + "$ref": "#/types/testprovider:index:SomeOtherType" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "a1": { + "$ref": "#/types/testprovider:index:SomeOtherType" + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/pf/tfgen/testdata/TestTypeOverride/no-override.golden b/pf/tfgen/testdata/TestTypeOverride/no-override.golden new file mode 100644 index 0000000000..e88dd90b84 --- /dev/null +++ b/pf/tfgen/testdata/TestTypeOverride/no-override.golden @@ -0,0 +1,51 @@ +{ + "name": "testprovider", + "attribution": "This Pulumi package is based on the [`testprovider` Terraform Provider](https://github.com/terraform-providers/terraform-provider-testprovider).", + "meta": { + "moduleFormat": "(.*)(?:/[^/]*)" + }, + "config": {}, + "types": { + "testprovider:index/ResB1:ResB1": { + "properties": { + "a1": { + "type": "string" + } + }, + "type": "object" + } + }, + "provider": {}, + "resources": { + "testprovider:index:Res": { + "properties": { + "a1": { + "type": "string" + }, + "b1": { + "$ref": "#/types/testprovider:index/ResB1:ResB1" + } + }, + "inputProperties": { + "a1": { + "type": "string" + }, + "b1": { + "$ref": "#/types/testprovider:index/ResB1:ResB1" + } + }, + "stateInputs": { + "description": "Input properties used for looking up and filtering Res resources.\n", + "properties": { + "a1": { + "type": "string" + }, + "b1": { + "$ref": "#/types/testprovider:index/ResB1:ResB1" + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/pf/tfgen/tfgen_test.go b/pf/tfgen/tfgen_test.go index 898706d69b..a33bd50ef5 100644 --- a/pf/tfgen/tfgen_test.go +++ b/pf/tfgen/tfgen_test.go @@ -15,6 +15,7 @@ package tfgen import ( + "bytes" "context" "encoding/json" "testing" @@ -22,10 +23,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/provider" - "github.com/hashicorp/terraform-plugin-framework/provider/schema" + pschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" + rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hexops/autogold/v2" pulumiSchema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" + pulumischema "github.com/pulumi/pulumi/pkg/v3/codegen/schema" "github.com/stretchr/testify/require" pftfbridge "github.com/pulumi/pulumi-terraform-bridge/pf/tfbridge" @@ -36,15 +41,15 @@ import ( // listvalidator.SizeAtMost(1). func TestMaxItemsOne(t *testing.T) { ctx := context.Background() - s := schema.Schema{ - Blocks: map[string]schema.Block{ - "assume_role": schema.ListNestedBlock{ + s := pschema.Schema{ + Blocks: map[string]pschema.Block{ + "assume_role": pschema.ListNestedBlock{ Validators: []validator.List{ listvalidator.SizeAtMost(1), }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "external_id": schema.StringAttribute{ + NestedObject: pschema.NestedBlockObject{ + Attributes: map[string]pschema.Attribute{ + "external_id": pschema.StringAttribute{ Optional: true, Description: "A unique identifier that might be required when you assume a role in another account.", }, @@ -56,7 +61,7 @@ func TestMaxItemsOne(t *testing.T) { res, err := GenerateSchema(ctx, GenerateSchemaOptions{ ProviderInfo: tfbridge.ProviderInfo{ Name: "testprovider", - P: pftfbridge.ShimProvider(&schemaTestProvider{s}), + P: pftfbridge.ShimProvider(&schemaTestProvider{schema: s}), }, }) require.NoError(t, err) @@ -71,7 +76,8 @@ func TestMaxItemsOne(t *testing.T) { } type schemaTestProvider struct { - schema schema.Schema + schema pschema.Schema + resources map[string]rschema.Schema } func (*schemaTestProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { @@ -90,6 +96,327 @@ func (*schemaTestProvider) DataSources(ctx context.Context) []func() datasource. return nil } -func (*schemaTestProvider) Resources(context.Context) []func() resource.Resource { - return nil +func (p *schemaTestProvider) Resources(context.Context) []func() resource.Resource { + r := make([]func() resource.Resource, 0, len(p.resources)) + for k, v := range p.resources { + r = append(r, makeTestResource(k, v)) + } + return r +} + +func makeTestResource(name string, schema rschema.Schema) func() resource.Resource { + return func() resource.Resource { return schemaTestResource{name, schema} } +} + +type schemaTestResource struct { + name string + schema rschema.Schema +} + +func (r schemaTestResource) Metadata( + _ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + r.name +} + +func (r schemaTestResource) Schema( + _ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse, +) { + resp.Schema = r.schema +} + +func (r schemaTestResource) Create(context.Context, resource.CreateRequest, *resource.CreateResponse) { + panic(r.name) +} + +func (r schemaTestResource) Read(context.Context, resource.ReadRequest, *resource.ReadResponse) { + panic(r.name) +} + +func (r schemaTestResource) Update(context.Context, resource.UpdateRequest, *resource.UpdateResponse) { + panic(r.name) +} + +func (r schemaTestResource) Delete(context.Context, resource.DeleteRequest, *resource.DeleteResponse) { + panic(r.name) +} + +func TestTypeOverride(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + schema rschema.Schema + info *tfbridge.ResourceInfo + expectedError autogold.Value + }{ + { + name: "no-override", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.StringAttribute{Optional: true}, + }, + Blocks: map[string]rschema.Block{ + "b1": rschema.SingleNestedBlock{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.StringAttribute{Optional: true}, + }, + }, + }, + }, + }, + { + name: "attr-single-nested-object-element", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]rschema.Attribute{ + "n1": rschema.StringAttribute{Optional: true}, + }, + }, + }, + }, + info: &tfbridge.ResourceInfo{Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Elem: &tfbridge.SchemaInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "n1": {Type: "number"}, + }, + }}, + }}, + }, + { + // This test case reproduces https://github.com/pulumi/pulumi-terraform-bridge/issues/2185 + name: "attr-single-nested-object", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]rschema.Attribute{ + "n1": rschema.StringAttribute{Optional: true}, + }, + }, + }, + }, + info: &tfbridge.ResourceInfo{Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Elem: &tfbridge.SchemaInfo{ + Type: "testprovider:index:SomeOtherType", + }}, + }}, + }, + { + name: "attr-map-element", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.MapAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Elem: &tfbridge.SchemaInfo{ + Type: "number", + }}, + }, + }, + }, + { + name: "attr-map-object-element", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.MapNestedAttribute{ + Optional: true, + NestedObject: rschema.NestedAttributeObject{ + Attributes: map[string]rschema.Attribute{ + "n1": rschema.StringAttribute{Optional: true}, + }, + }, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Elem: &tfbridge.SchemaInfo{ + Elem: &tfbridge.SchemaInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "n1": {Type: "number"}, + }, + }, + }}, + }, + }, + }, + { + name: "invalid-attr-map-fields", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.MapAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Fields: map[string]*tfbridge.SchemaInfo{ + "invalid": {}, + }}, + }, + }, + expectedError: autogold.Expect("test_res: [{a1}]: cannot specify .Fields on a List[T] or Set[T] type"), + }, + { + name: "invalid-attr-map-max-items-one", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.MapAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {MaxItemsOne: tfbridge.True()}, + }, + }, + expectedError: autogold.Expect("test_res: [{a1}]: can only specify .MaxItemsOne on List[T] or Set[T] type"), + }, + { + name: "attr-set-element", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Elem: &tfbridge.SchemaInfo{ + Type: "number", + }}, + }, + }, + }, + { + name: "invalid-attr-map-fields", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.SetAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Fields: map[string]*tfbridge.SchemaInfo{ + "invalid": {}, + }}, + }, + }, + expectedError: autogold.Expect("test_res: [{a1}]: cannot specify .Fields on a List[T] or Set[T] type"), + }, + { + name: "attr-list-element", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Elem: &tfbridge.SchemaInfo{ + Type: "number", + }}, + }, + }, + }, + { + name: "attr-list-max-items-one", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {MaxItemsOne: tfbridge.True()}, + }, + }, + }, + { + name: "attr-override-map-fields", + schema: rschema.Schema{ + Attributes: map[string]rschema.Attribute{ + "a1": rschema.ListAttribute{ + Optional: true, + ElementType: types.StringType, + }, + }, + }, + info: &tfbridge.ResourceInfo{ + Fields: map[string]*tfbridge.SchemaInfo{ + "a1": {Fields: map[string]*tfbridge.SchemaInfo{ + "invalid": {}, + }}, + }, + }, + expectedError: autogold.Expect("test_res: [{a1}]: cannot specify .Fields on a List[T] or Set[T] type"), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := context.Background() + if tt.info == nil { + tt.info = &tfbridge.ResourceInfo{} + } + tt.info.Tok = "testprovider:index:Res" + tt.info.Docs = &tfbridge.DocInfo{Markdown: []byte{' '}} + if _, ok := tt.schema.Attributes["id"]; !ok { + tt.schema.Attributes["id"] = rschema.StringAttribute{Optional: true} + } + res, err := GenerateSchema(ctx, GenerateSchemaOptions{ + ProviderInfo: tfbridge.ProviderInfo{ + Name: "testprovider", + UpstreamRepoPath: ".", // no invalid mappings warnings + P: pftfbridge.ShimProvider(&schemaTestProvider{ + resources: map[string]rschema.Schema{ + "res": tt.schema, + }, + }), + Resources: map[string]*tfbridge.ResourceInfo{ + "test_res": tt.info, + }, + // Trim the schema for easier comparison + SchemaPostProcessor: func(p *pulumischema.PackageSpec) { + p.Language = nil + p.Provider.Description = "" + }, + }, + }) + if tt.expectedError != nil { + require.Error(t, err) + tt.expectedError.Equal(t, err.Error()) + return + } + require.NoError(t, err) + var b bytes.Buffer + require.NoError(t, json.Indent(&b, res.ProviderMetadata.PackageSchema, "", " ")) + autogold.ExpectFile(t, autogold.Raw(b.String())) + }) + } } diff --git a/pkg/tf2pulumi/convert/testdata/mappings/renames.json b/pkg/tf2pulumi/convert/testdata/mappings/renames.json index 96665d4480..6eb346ca87 100644 --- a/pkg/tf2pulumi/convert/testdata/mappings/renames.json +++ b/pkg/tf2pulumi/convert/testdata/mappings/renames.json @@ -82,11 +82,9 @@ }, "a_resource": { "name": "theResource", - "elem": { - "fields": { - "inner_string": { - "name": "theInnerString" - } + "fields": { + "inner_string": { + "name": "theInnerString" } } }, @@ -96,4 +94,4 @@ } } } -} \ No newline at end of file +} diff --git a/pkg/tfbridge/info/validate.go b/pkg/tfbridge/info/validate.go index df99ae409c..53d38ac55b 100644 --- a/pkg/tfbridge/info/validate.go +++ b/pkg/tfbridge/info/validate.go @@ -118,6 +118,14 @@ func (c *infoCheck) checkProperty(path walk.SchemaPath, tfs shim.Schema, ps *Sch c.checkElem(path.Element(), elem, ps.Elem) + switch tfs.Type() { + case shim.TypeSet, shim.TypeList: + default: + if ps.MaxItemsOne != nil { + c.error(path, errCannotSetMaxItemsOne) + } + } + // Either `path` represents an object nested under a list or set, or `path` is // itself an object, depending on the .Type() property. case shim.Resource: