diff --git a/go.mod b/go.mod index 854dc02e5..e2a7b6591 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/hil v0.0.0-20190212132231-97b3a9cdfa93 - github.com/hashicorp/terraform-plugin-framework v1.15.0 + github.com/hashicorp/terraform-plugin-framework v1.16.1 github.com/hashicorp/terraform-plugin-framework-validators v0.17.0 github.com/hashicorp/terraform-plugin-mux v0.20.0 github.com/hashicorp/terraform-plugin-sdk v1.7.0 diff --git a/go.sum b/go.sum index 12e1602f7..d18f54029 100644 --- a/go.sum +++ b/go.sum @@ -2049,8 +2049,8 @@ github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo github.com/hashicorp/terraform-json v0.4.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU= github.com/hashicorp/terraform-json v0.27.1 h1:zWhEracxJW6lcjt/JvximOYyc12pS/gaKSy/wzzE7nY= github.com/hashicorp/terraform-json v0.27.1/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= -github.com/hashicorp/terraform-plugin-framework v1.15.0 h1:LQ2rsOfmDLxcn5EeIwdXFtr03FVsNktbbBci8cOKdb4= -github.com/hashicorp/terraform-plugin-framework v1.15.0/go.mod h1:hxrNI/GY32KPISpWqlCoTLM9JZsGH3CyYlir09bD/fI= +github.com/hashicorp/terraform-plugin-framework v1.16.1 h1:1+zwFm3MEqd/0K3YBB2v9u9DtyYHyEuhVOfeIXbteWA= +github.com/hashicorp/terraform-plugin-framework v1.16.1/go.mod h1:0xFOxLy5lRzDTayc4dzK/FakIgBhNf/lC4499R9cV4Y= github.com/hashicorp/terraform-plugin-framework-validators v0.17.0 h1:0uYQcqqgW3BMyyve07WJgpKorXST3zkpzvrOnf3mpbg= github.com/hashicorp/terraform-plugin-framework-validators v0.17.0/go.mod h1:VwdfgE/5Zxm43flraNa0VjcvKQOGVrcO4X8peIri0T0= github.com/hashicorp/terraform-plugin-go v0.29.0 h1:1nXKl/nSpaYIUBU1IG/EsDOX0vv+9JxAltQyDMpq5mU= diff --git a/pkg/convert/convert.go b/pkg/convert/convert.go index b43b5bc9b..8104005db 100644 --- a/pkg/convert/convert.go +++ b/pkg/convert/convert.go @@ -41,6 +41,8 @@ type Encoding interface { NewResourceEncoder(resource string, resourceType tftypes.Object) (Encoder, error) NewDataSourceDecoder(dataSource string, dataSourceType tftypes.Object) (Decoder, error) NewDataSourceEncoder(dataSource string, dataSourceType tftypes.Object) (Encoder, error) + NewListResourceEncoder(listResource string, listResourceType tftypes.Object) (Encoder, error) + NewListResourceDecoder(listResource string, listResourceType tftypes.Object) (Decoder, error) } // Like PropertyNames but specialized to either a type by token or config property. diff --git a/pkg/convert/encoding.go b/pkg/convert/encoding.go index 5da1f7b8a..2f9111a52 100644 --- a/pkg/convert/encoding.go +++ b/pkg/convert/encoding.go @@ -89,6 +89,28 @@ func (e *encoding) NewDataSourceDecoder( return dec, nil } +func (e *encoding) NewListResourceEncoder( + listResource string, objectType tftypes.Object, +) (Encoder, error) { + mctx := newListResourceSchemaMapContext(listResource, e.SchemaOnlyProvider, e.ProviderInfo) + enc, err := NewObjectEncoder(ObjectSchema{mctx.schemaMap, mctx.schemaInfos, &objectType}) + if err != nil { + return nil, fmt.Errorf("cannot derive an encoder for list resource %q: %w", listResource, err) + } + return enc, nil +} + +func (e *encoding) NewListResourceDecoder( + listResource string, objectType tftypes.Object, +) (Decoder, error) { + mctx := newListResourceSchemaMapContext(listResource, e.SchemaOnlyProvider, e.ProviderInfo) + dec, err := NewObjectDecoder(ObjectSchema{mctx.schemaMap, mctx.schemaInfos, &objectType}) + if err != nil { + return nil, fmt.Errorf("cannot derive an decoder for list resource %q: %w", listResource, err) + } + return dec, nil +} + func buildPropertyEncoders( mctx *schemaMapContext, objectType tftypes.Object, ) (map[terraformPropertyName]Encoder, error) { diff --git a/pkg/convert/schema_context.go b/pkg/convert/schema_context.go index ed5c093e4..76e53343c 100644 --- a/pkg/convert/schema_context.go +++ b/pkg/convert/schema_context.go @@ -23,7 +23,7 @@ import ( "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" - "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim/walk" ) @@ -73,6 +73,21 @@ func newDataSourceSchemaMapContext( return newSchemaMapContext(sm, fields) } +func newListResourceSchemaMapContext( + listResource string, + schemaOnlyProvider shim.Provider, + providerInfo *tfbridge.ProviderInfo, +) *schemaMapContext { + r := schemaOnlyProvider.ListResourcesMap().Get(listResource) + contract.Assertf(r != nil, "no list resource %q found in ListResourcesMap", listResource) + sm := r.Schema() + var fields map[string]*tfbridge.SchemaInfo + if providerInfo != nil { + fields = providerInfo.ListResources[listResource].GetFields() + } + return newSchemaMapContext(sm, fields) +} + func (sc *schemaMapContext) PropertyKey(tfname terraformPropertyName, _ tftypes.Type) resource.PropertyKey { return sc.ToPropertyKey(tfname) } diff --git a/pkg/pf/internal/pfutils/attr.go b/pkg/pf/internal/pfutils/attr.go index ef405d09e..07fc1bb5d 100644 --- a/pkg/pf/internal/pfutils/attr.go +++ b/pkg/pf/internal/pfutils/attr.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + lrschema "github.com/hashicorp/terraform-plugin-framework/list/schema" prschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -65,6 +66,10 @@ func FromResourceAttribute(x rschema.Attribute) Attr { return FromAttrLike(x) } +func FromListResourceAttribute(x lrschema.Attribute) Attr { + return FromAttrLike(x) +} + func FromAttrLike(attrLike AttrLike) Attr { nested, nestingMode := extractNestedAttributes(attrLike) hasDefault := hasDefault(attrLike) diff --git a/pkg/pf/internal/pfutils/block.go b/pkg/pf/internal/pfutils/block.go index 3ccd74840..a22835505 100644 --- a/pkg/pf/internal/pfutils/block.go +++ b/pkg/pf/internal/pfutils/block.go @@ -21,6 +21,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + lrschema "github.com/hashicorp/terraform-plugin-framework/list/schema" prschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -58,6 +59,10 @@ func FromResourceBlock(x rschema.Block) Block { return FromBlockLike(x) } +func FromListResourceBlock(x lrschema.Block) Block { + return FromBlockLike(x) +} + func FromBlockLike(x BlockLike) Block { minItems, maxItems, _ := detectSizeConstraints(x) attrs, blocks, mode := extractBlockNesting(x) diff --git a/pkg/pf/internal/pfutils/listresources.go b/pkg/pf/internal/pfutils/listresources.go new file mode 100644 index 000000000..4205d68cc --- /dev/null +++ b/pkg/pf/internal/pfutils/listresources.go @@ -0,0 +1,74 @@ +// Copyright 2016-2022, 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 pfutils + +import ( + "context" + "fmt" + + listres "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/resource" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/runtypes" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" +) + +func GatherListResources[F func(Schema) shim.SchemaMap]( + ctx context.Context, prov provider.Provider, f F, +) (runtypes.ListResources, error) { + provMetadata := queryProviderMetadata(ctx, prov) + lr := make(collection[func() listres.ListResource]) + lp, ok := prov.(provider.ProviderWithListResources) + if !ok { + return &listResources{collection: lr, convert: f}, nil + } + + for _, makeListResource := range lp.ListResources(ctx) { + listResource := makeListResource() + + meta := resource.MetadataResponse{} + listResource.Metadata(ctx, resource.MetadataRequest{ + ProviderTypeName: provMetadata.TypeName, + }, &meta) + + schemaResponse := &listres.ListResourceSchemaResponse{} + listResource.ListResourceConfigSchema(ctx, listres.ListResourceSchemaRequest{}, schemaResponse) + + if err := checkDiagsForErrors(schemaResponse.Diagnostics); err != nil { + return nil, fmt.Errorf("ListResource %s GetSchema() error: %w", meta.TypeName, err) + } + + lr[runtypes.TypeOrRenamedEntityName(meta.TypeName)] = entry[func() listres.ListResource]{ + t: makeListResource, + schema: FromListResourceSchema(schemaResponse.Schema), + tfName: runtypes.TypeName(meta.TypeName), + } + } + + return &listResources{collection: lr, convert: f}, nil +} + +type listResources struct { + collection[func() listres.ListResource] + convert func(Schema) shim.SchemaMap +} + +func (r listResources) Schema(t runtypes.TypeOrRenamedEntityName) runtypes.Schema { + entry := r.collection[t] + return runtypesSchemaAdapter{entry.schema, r.convert, entry.tfName} +} + +func (listResources) IsListResources() {} diff --git a/pkg/pf/internal/pfutils/schema.go b/pkg/pf/internal/pfutils/schema.go index b5b618797..79feb3082 100644 --- a/pkg/pf/internal/pfutils/schema.go +++ b/pkg/pf/internal/pfutils/schema.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + lrschema "github.com/hashicorp/terraform-plugin-framework/list/schema" prschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" rschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -66,6 +67,12 @@ func FromResourceSchema(x rschema.Schema) Schema { return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, &x) } +func FromListResourceSchema(x lrschema.Schema) Schema { + attrs := convertMap(FromListResourceAttribute, x.Attributes) + blocks := convertMap(FromListResourceBlock, x.Blocks) + return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, nil) +} + type schemaAdapter struct { tftypes.AttributePathStepper attrType attr.Type diff --git a/pkg/pf/internal/runtypes/types.go b/pkg/pf/internal/runtypes/types.go index bc55caf3c..ee45441dd 100644 --- a/pkg/pf/internal/runtypes/types.go +++ b/pkg/pf/internal/runtypes/types.go @@ -66,3 +66,8 @@ type DataSources interface { collection IsDataSources() } + +type ListResources interface { + collection + IsListResources() +} diff --git a/pkg/pf/internal/schemashim/list_resource_map.go b/pkg/pf/internal/schemashim/list_resource_map.go new file mode 100644 index 000000000..88375b541 --- /dev/null +++ b/pkg/pf/internal/schemashim/list_resource_map.go @@ -0,0 +1,90 @@ +// Copyright 2016-2022, 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 schemashim + +import ( + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/runtypes" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" +) + +// Resource map needs to support Set (mutability) for RenameResourceWithAlias. +func newSchemaOnlyListResourceMap(resources runtypes.ListResources) schemaOnlyListResourceMap { + m := schemaOnlyListResourceMap{Map: make(map[string]*schemaOnlyListResource)} + for _, name := range resources.All() { + key := string(name) + v := resources.Schema(name) + m.Map[key] = &schemaOnlyListResource{v, internalinter.Internal{}} + } + return m +} + +type schemaOnlyListResourceMap struct { + internalinter.Internal + Map map[string]*schemaOnlyListResource +} + +var ( + _ shim.ResourceMap = schemaOnlyListResourceMap{} + _ runtypes.ListResources = schemaOnlyListResourceMap{} +) + +func (m schemaOnlyListResourceMap) Len() int { + return len(m.Map) +} + +func (m schemaOnlyListResourceMap) Get(key string) shim.Resource { + return m.Map[key] +} + +func (m schemaOnlyListResourceMap) GetOk(key string) (shim.Resource, bool) { + v, ok := m.Map[key] + return v, ok +} + +func (m schemaOnlyListResourceMap) Range(each func(key string, value shim.Resource) bool) { + for k, v := range m.Map { + if !each(k, v) { + return + } + } +} + +func (m schemaOnlyListResourceMap) Set(key string, value shim.Resource) { + v, ok := value.(*schemaOnlyListResource) + contract.Assertf(ok, "Set must be a %T, found a %T", v, value) + m.Map[key] = v +} + +func (m schemaOnlyListResourceMap) All() []runtypes.TypeOrRenamedEntityName { + arr := make([]runtypes.TypeOrRenamedEntityName, 0, len(m.Map)) + for k := range m.Map { + arr = append(arr, runtypes.TypeOrRenamedEntityName(k)) + } + return arr +} + +func (m schemaOnlyListResourceMap) Has(key runtypes.TypeOrRenamedEntityName) bool { + _, ok := m.Map[string(key)] + return ok +} + +func (m schemaOnlyListResourceMap) Schema(key runtypes.TypeOrRenamedEntityName) runtypes.Schema { + return m.Map[string(key)].tf +} + +func (m schemaOnlyListResourceMap) IsListResources() {} diff --git a/pkg/pf/internal/schemashim/listresource.go b/pkg/pf/internal/schemashim/listresource.go new file mode 100644 index 000000000..b80d8ff28 --- /dev/null +++ b/pkg/pf/internal/schemashim/listresource.go @@ -0,0 +1,71 @@ +// Copyright 2016-2022, 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 schemashim + +import ( + "context" + + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/runtypes" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/valueshim" +) + +type schemaOnlyListResource struct { + tf runtypes.Schema + internalinter.Internal +} + +var _ shim.Resource = (*schemaOnlyListResource)(nil) + +func (r *schemaOnlyListResource) SchemaType() valueshim.Type { + protoSchema, err := r.tf.ResourceProtoSchema(context.Background()) + contract.AssertNoErrorf(err, "ResourceProtoSchema failed") + return valueshim.FromTType(protoSchema.ValueType()) +} + +func (r *schemaOnlyListResource) Schema() shim.SchemaMap { + return r.tf.Shim() +} + +func (r *schemaOnlyListResource) SchemaVersion() int { + panic("list resources do not have schema versions") +} + +func (r *schemaOnlyListResource) DeprecationMessage() string { + return r.tf.DeprecationMessage() +} + +func (*schemaOnlyListResource) Importer() shim.ImportFunc { + panic("schemaOnlyListResource does not implement runtime operation ImporterFunc") +} + +func (*schemaOnlyListResource) Timeouts() *shim.ResourceTimeout { + panic("schemaOnlyListResource does not implement runtime operation Timeouts") +} + +func (*schemaOnlyListResource) InstanceState(id string, object, + meta map[string]interface{}, +) (shim.InstanceState, error) { + panic("schemaOnlyListResource does not implement runtime operation InstanceState") +} + +func (*schemaOnlyListResource) DecodeTimeouts( + config shim.ResourceConfig, +) (*shim.ResourceTimeout, error) { + panic("schemaOnlyListResource does not implement runtime operation DecodeTimeouts") +} diff --git a/pkg/pf/internal/schemashim/provider.go b/pkg/pf/internal/schemashim/provider.go index f12ec9b83..27142cd57 100644 --- a/pkg/pf/internal/schemashim/provider.go +++ b/pkg/pf/internal/schemashim/provider.go @@ -33,10 +33,11 @@ import ( var _ = pf.ShimProvider(&SchemaOnlyProvider{}) type SchemaOnlyProvider struct { - ctx context.Context - tf pfprovider.Provider - resourceMap schemaOnlyResourceMap - dataSourceMap schemaOnlyDataSourceMap + ctx context.Context + tf pfprovider.Provider + resourceMap schemaOnlyResourceMap + dataSourceMap schemaOnlyDataSourceMap + listResourceMap schemaOnlyListResourceMap internalinter.Internal } @@ -58,6 +59,10 @@ func (p *SchemaOnlyProvider) Resources(ctx context.Context) (runtypes.Resources, return p.resourceMap, nil } +func (p *SchemaOnlyProvider) ListResources(ctx context.Context) (runtypes.ListResources, error) { + return p.listResourceMap, nil +} + func (p *SchemaOnlyProvider) DataSources(ctx context.Context) (runtypes.DataSources, error) { return p.dataSourceMap, nil } @@ -89,6 +94,10 @@ func (p *SchemaOnlyProvider) ResourcesMap() shim.ResourceMap { return p.resourceMap } +func (p *SchemaOnlyProvider) ListResourcesMap() shim.ResourceMap { + return p.listResourceMap +} + func (p *SchemaOnlyProvider) DataSourcesMap() shim.ResourceMap { return p.dataSourceMap } diff --git a/pkg/pf/internal/schemashim/schemashim.go b/pkg/pf/internal/schemashim/schemashim.go index fe413c038..6dfaf9fff 100644 --- a/pkg/pf/internal/schemashim/schemashim.go +++ b/pkg/pf/internal/schemashim/schemashim.go @@ -32,12 +32,20 @@ func ShimSchemaOnlyProvider(ctx context.Context, provider pfprovider.Provider) s if err != nil { panic(err) } + + listResources, err := pfutils.GatherListResources(ctx, provider, NewSchemaMap) + if err != nil { + panic(err) + } + resourceMap := newSchemaOnlyResourceMap(resources) dataSourceMap := newSchemaOnlyDataSourceMap(dataSources) + listResourceMap := newSchemaOnlyListResourceMap(listResources) return &SchemaOnlyProvider{ - ctx: ctx, - tf: provider, - resourceMap: resourceMap, - dataSourceMap: dataSourceMap, + ctx: ctx, + tf: provider, + resourceMap: resourceMap, + dataSourceMap: dataSourceMap, + listResourceMap: listResourceMap, } } diff --git a/pkg/pf/proto/protov6.go b/pkg/pf/proto/protov6.go index dd882b005..6fd4758a8 100644 --- a/pkg/pf/proto/protov6.go +++ b/pkg/pf/proto/protov6.go @@ -125,3 +125,12 @@ func (p Provider) DataSourcesMap() shim.ResourceMap { } return resourceMap(v.DataSourceSchemas) } + +func (p Provider) ListResourcesMap() shim.ResourceMap { + v, err := p.getSchema() + if err != nil { + tfbridge.GetLogger(p.ctx).Error(err.Error()) + return nil + } + return resourceMap(v.ListResourceSchemas) +} diff --git a/pkg/pf/proto/runtypes.go b/pkg/pf/proto/runtypes.go index 18b271c90..df5278937 100644 --- a/pkg/pf/proto/runtypes.go +++ b/pkg/pf/proto/runtypes.go @@ -41,6 +41,14 @@ func (p Provider) DataSources(context.Context) (runtypes.DataSources, error) { return datasources{collection(v.DataSourceSchemas)}, nil } +func (p Provider) ListResources(ctx context.Context) (runtypes.ListResources, error) { + v, err := p.getSchema() + if err != nil { + return nil, err + } + return listresources{collection(v.ListResourceSchemas)}, nil +} + type schema struct { s *tfprotov6.Schema tfName runtypes.TypeName @@ -83,6 +91,12 @@ type datasources struct{ collection } var _ runtypes.DataSources = datasources{} +type listresources struct{ collection } + +func (listresources) IsListResources() {} + +var _ runtypes.ListResources = listresources{} + func (datasources) IsDataSources() {} type collection map[string]*tfprotov6.Schema diff --git a/pkg/pf/provider.go b/pkg/pf/provider.go index 8356ca411..8d709aa33 100644 --- a/pkg/pf/provider.go +++ b/pkg/pf/provider.go @@ -33,6 +33,7 @@ type ShimProvider interface { Server(context.Context) (tfprotov6.ProviderServer, error) Resources(context.Context) (runtypes.Resources, error) + ListResources(context.Context) (runtypes.ListResources, error) DataSources(context.Context) (runtypes.DataSources, error) Config(context.Context) (tftypes.Object, error) } diff --git a/pkg/pf/tests/internal/testprovider/testbridge.go b/pkg/pf/tests/internal/testprovider/testbridge.go index 037725d1d..afe23a812 100644 --- a/pkg/pf/tests/internal/testprovider/testbridge.go +++ b/pkg/pf/tests/internal/testprovider/testbridge.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/list" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" prschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -130,6 +131,10 @@ func SyntheticTestBridgeProvider() tfbridge.ProviderInfo { }, MetadataInfo: tfbridge.NewProviderMetadata(testBridgeMetadata), + + ListResources: map[string]*tfbridge.ListResourceInfo{ + "testbridge_testres": {Tok: "testbridge:index:listTestres"}, + }, } info.SetAutonaming(255, "-") @@ -145,6 +150,7 @@ type resourceData struct { } var _ provider.Provider = (*syntheticProvider)(nil) +var _ provider.ProviderWithListResources = (*syntheticProvider)(nil) func (p *syntheticProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { resp.TypeName = "testbridge" @@ -227,6 +233,12 @@ func (p *syntheticProvider) Schema(_ context.Context, _ provider.SchemaRequest, } } +func (p *syntheticProvider) ListResources(context.Context) []func() list.ListResource { + return []func() list.ListResource{ + newListTestres, + } +} + func validateNested( ctx context.Context, req provider.ConfigureRequest, diff --git a/pkg/pf/tests/internal/testprovider/tfbridge_list_resource_testres.go b/pkg/pf/tests/internal/testprovider/tfbridge_list_resource_testres.go new file mode 100644 index 000000000..15087be2a --- /dev/null +++ b/pkg/pf/tests/internal/testprovider/tfbridge_list_resource_testres.go @@ -0,0 +1,83 @@ +// Copyright 2016-2023, 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 testprovider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/list" + "github.com/hashicorp/terraform-plugin-framework/list/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +type listTestres struct{} + +var _ list.ListResource = &listTestres{} + +func newListTestres() list.ListResource { + return &listTestres{} +} + +func (e *listTestres) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_testres" +} + +func (e *listTestres) ListResourceConfigSchema(_ context.Context, + req list.ListResourceSchemaRequest, + res *list.ListResourceSchemaResponse) { + res.Schema = schema.Schema{ + Description: "A test list resource", + Attributes: map[string]schema.Attribute{ + "count": schema.Int32Attribute{ + Description: "An integer count", + Optional: true, + }, + }, + } +} + +func (e *listTestres) List(ctx context.Context, req list.ListRequest, resp *list.ListResultsStream) { + var model listTestresModel + req.Config.Get(ctx, &model) + resp.Results = func(yield func(list.ListResult) bool) { + if !model.Count.IsNull() { + count, _ := model.Count.ToInt32Value(ctx) + for i := 0; i < int(count.ValueInt32()); i++ { + result := list.ListResult{ + DisplayName: fmt.Sprintf("Example %d", i), + Resource: &tfsdk.Resource{ + Raw: tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, fmt.Sprintf("example-%d", i)), + "current": tftypes.NewValue(tftypes.Number, float64(i)), + }), + }, + } + + if !yield(result) { + return + } + } + } + } +} + +type listTestresModel struct { + Count types.Int32 `tfsdk:"count"` +} diff --git a/pkg/pf/tests/schema_test.go b/pkg/pf/tests/schema_test.go index b991ff548..21049280a 100644 --- a/pkg/pf/tests/schema_test.go +++ b/pkg/pf/tests/schema_test.go @@ -65,6 +65,15 @@ func TestSchemaGen(t *testing.T) { actionParameterPhases := spec.Types["testbridge:index/TestnestRuleActionParametersPhases:TestnestRuleActionParametersPhases"] assert.Equal(t, "object", actionParameterPhases.Type) assert.Equal(t, "boolean", actionParameterPhases.Properties["p2"].Type) + + listTestResourceFunc := spec.Functions["testbridge:index:listTestres"] + assert.Equal(t, "integer", listTestResourceFunc.Inputs.Properties["limit"].Type) + assert.Equal(t, "boolean", listTestResourceFunc.Inputs.Properties["includeResource"].Type) + config, ok := listTestResourceFunc.Inputs.Properties["config"] + require.True(t, ok) + assert.Equal(t, + "#/types/testbridge:index/ListTestresConfigArgs:ListTestresConfigArgs", + config.Ref) }) } diff --git a/pkg/pf/tfbridge/provider.go b/pkg/pf/tfbridge/provider.go index 6ae562ff6..a1ac0a49c 100644 --- a/pkg/pf/tfbridge/provider.go +++ b/pkg/pf/tfbridge/provider.go @@ -83,6 +83,7 @@ type provider struct { info tfbridge.ProviderInfo resources runtypes.Resources datasources runtypes.DataSources + listResources runtypes.ListResources pulumiSchema func(context.Context, plugin.GetSchemaRequest) ([]byte, error) encoding convert.Encoding diagSink diag.Sink @@ -150,6 +151,10 @@ func newProviderWithContext(ctx context.Context, info tfbridge.ProviderInfo, if err != nil { return nil, err } + listResources, err := pfServer.ListResources(ctx) + if err != nil { + return nil, err + } if info.MetadataInfo == nil { return nil, fmt.Errorf("[pf/tfbridge] ProviderInfo.BridgeMetadata is required but is nil") @@ -191,6 +196,7 @@ func newProviderWithContext(ctx context.Context, info tfbridge.ProviderInfo, info: info, resources: resources, datasources: datasources, + listResources: listResources, pulumiSchema: schema, encoding: enc, configEncoder: configEncoder, diff --git a/pkg/pf/tfbridge/provider_invoke.go b/pkg/pf/tfbridge/provider_invoke.go index 01257be7c..832a45b32 100644 --- a/pkg/pf/tfbridge/provider_invoke.go +++ b/pkg/pf/tfbridge/provider_invoke.go @@ -31,6 +31,72 @@ import ( "github.com/pulumi/pulumi-terraform-bridge/v3/unstable/propertyvalue" ) +func (p *provider) handleListResourceInvoke( + ctx context.Context, + tok tokens.ModuleMember, + args resource.PropertyMap, + terraformListResourceType string, +) (resource.PropertyMap, []plugin.CheckFailure, error) { + server, ok := p.tfServer.(tfprotov6.ProviderServerWithListResource) + if !ok { + return nil, nil, fmt.Errorf("provider does not support ListResources") + } + + handle, err := p.listResourceHandle(ctx, tok) + typ := handle.schema.Type(ctx).(tftypes.Object) + if err != nil { + return nil, nil, err + } + + request := &tfprotov6.ListResourceRequest{ + TypeName: terraformListResourceType, + } + + if limit, ok := args["limit"]; ok && limit.IsNumber() { + request.Limit = int64(limit.NumberValue()) + } + + if includeResource, ok := args["includeResource"]; ok && includeResource.IsBool() { + request.IncludeResource = includeResource.BoolValue() + } + + if config, ok := args["config"]; ok && config.IsObject() { + configObject := config.ObjectValue() + config, err := convert.EncodePropertyMapToDynamic(handle.encoder, typ, configObject) + if err != nil { + return nil, nil, fmt.Errorf("cannot encode config to call ListResource for %q: %w", + handle.terraformListResourceName, err) + } + request.Config = config + } + + response, err := server.ListResource(ctx, request) + + if err != nil { + return nil, nil, fmt.Errorf("error calling ListResource for %q: %w", + handle.terraformListResourceName, err) + } + + results := []resource.PropertyValue{} + for item := range response.Results { + decodedResource, err := convert.DecodePropertyMapFromDynamic(ctx, handle.decoder, typ, item.Resource) + if err != nil { + return nil, nil, fmt.Errorf("cannot decode resource from ListResource response for %q: %w", + handle.terraformListResourceName, err) + } + + result := resource.PropertyMap{ + "resource": resource.NewObjectProperty(decodedResource), + "displayName": resource.NewStringProperty(item.DisplayName), + } + + results = append(results, resource.NewObjectProperty(result)) + } + + decoded := resource.PropertyMap{"results": resource.NewArrayProperty(results)} + return decoded, nil, nil +} + // Invoke dynamically executes a built-in function in the provider. func (p *provider) InvokeWithContext( ctx context.Context, @@ -39,6 +105,10 @@ func (p *provider) InvokeWithContext( ) (resource.PropertyMap, []plugin.CheckFailure, error) { ctx = p.initLogging(ctx, p.logSink, "") + if tfResourceType, _, ok := p.isListResource(tok); ok { + return p.handleListResourceInvoke(ctx, tok, args, tfResourceType) + } + handle, err := p.datasourceHandle(ctx, tok) if err != nil { return nil, nil, err diff --git a/pkg/pf/tfbridge/provider_listresources.go b/pkg/pf/tfbridge/provider_listresources.go new file mode 100644 index 000000000..c7e3baa69 --- /dev/null +++ b/pkg/pf/tfbridge/provider_listresources.go @@ -0,0 +1,82 @@ +// Copyright 2016-2025, 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 tfbridge + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/convert" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/runtypes" + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/info" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" +) + +type listResourceHandle struct { + token tokens.ModuleMember + terraformListResourceName string + schema runtypes.Schema + encoder convert.Encoder + decoder convert.Decoder + schemaOnlyShim shim.Resource // optional + pulumiListResourceInfo *info.ListResource // optional +} + +func (p *provider) isListResource(pulumiType tokens.ModuleMember) (string, *info.ListResource, bool) { + for tfResourceType, res := range p.info.ListResources { + if pulumiType == tokens.ModuleMember(res.Tok) { + return tfResourceType, res, true + } + } + + return "", nil, false +} + +func (p *provider) listResourceHandle(ctx context.Context, token tokens.ModuleMember) (listResourceHandle, error) { + listResourceName, info, ok := p.isListResource(token) + if !ok { + return listResourceHandle{}, fmt.Errorf("unknown list resource %q", token) + } + + schema := p.listResources.Schema(runtypes.TypeOrRenamedEntityName(listResourceName)) + typ := schema.Type(ctx).(tftypes.Object) + + encoder, err := p.encoding.NewDataSourceEncoder(listResourceName, typ) + if err != nil { + return listResourceHandle{}, err + } + + decoder, err := p.encoding.NewDataSourceDecoder(listResourceName, typ) + if err != nil { + return listResourceHandle{}, err + } + + shim, _ := p.schemaOnlyProvider.ListResourcesMap().GetOk(listResourceName) + + result := listResourceHandle{ + token: token, + terraformListResourceName: listResourceName, + schema: schema, + encoder: encoder, + decoder: decoder, + schemaOnlyShim: shim, + pulumiListResourceInfo: info, + } + + return result, nil +} diff --git a/pkg/tfbridge/info.go b/pkg/tfbridge/info.go index 3db4fad5c..94d796e96 100644 --- a/pkg/tfbridge/info.go +++ b/pkg/tfbridge/info.go @@ -135,6 +135,9 @@ type PreCheckCallback = info.PreCheckCallback // DataSourceInfo can be used to override a data source's standard name mangling and argument/return information. type DataSourceInfo = info.DataSource +// ListResource can be used override the name mangling of a list resource. +type ListResourceInfo = info.ListResource + // SchemaInfo contains optional name transformations to apply. type SchemaInfo = info.Schema diff --git a/pkg/tfbridge/info/info.go b/pkg/tfbridge/info/info.go index 1d6b4c1de..c01a9b39a 100644 --- a/pkg/tfbridge/info/info.go +++ b/pkg/tfbridge/info/info.go @@ -72,6 +72,7 @@ type Provider struct { Config map[string]*Schema // a map of TF name to config schema overrides. ExtraConfig map[string]*Config // a list of Pulumi-only configuration variables. Resources map[string]*Resource // a map of TF type or renamed entity name to Pulumi resource info. + ListResources map[string]*ListResource // a map of TF type or renamed entity name to Pulumi list resource info. DataSources map[string]*DataSource // a map of TF type or renamed entity name to Pulumi resource info. ExtraTypes map[string]pschema.ComplexTypeSpec // a map of Pulumi token to schema type for extra types. ExtraResources map[string]pschema.ResourceSpec // a map of Pulumi token to schema type for extra resources. @@ -475,6 +476,21 @@ func (info *DataSource) ReplaceExamplesSection() bool { return info.Docs != nil && info.Docs.ReplaceExamplesSection } +// ListResource can be used override the name mangling of a list resource. +type ListResource struct { + Tok tokens.Type + Fields map[string]*Schema +} + +func (info *ListResource) GetTok() tokens.Token { return tokens.Token(info.Tok) } + +func (info *ListResource) GetFields() map[string]*Schema { + if info == nil { + return nil + } + return info.Fields +} + // Schema contains optional name transformations to apply. type Schema struct { // a name to override the default; "" uses the default. @@ -1285,15 +1301,46 @@ func (m *MarshallableDataSource) Unmarshal() *DataSource { } } +// MarshallableListResource is the JSON-marshallable form of a Pulumi ListResource value. +type MarshallableListResource struct { + Tok tokens.Type `json:"tok"` + Fields map[string]*MarshallableSchema `json:"fields"` +} + +// MarshalListResource converts a Pulumi ListResource value into a MarshallableListResource value. +func MarshalListResource(l *ListResource) *MarshallableListResource { + fields := make(map[string]*MarshallableSchema) + for k, v := range l.Fields { + fields[k] = MarshalSchema(v) + } + return &MarshallableListResource{ + Tok: l.Tok, + Fields: fields, + } +} + +// Unmarshal creates a mostly-initialized Pulumi ListResource value from the given MarshallableListResource. +func (m *MarshallableListResource) Unmarshal() *ListResource { + fields := make(map[string]*Schema) + for k, v := range m.Fields { + fields[k] = v.Unmarshal() + } + return &ListResource{ + Tok: m.Tok, + Fields: fields, + } +} + // MarshallableProvider is the JSON-marshallable form of a Pulumi ProviderInfo value. type MarshallableProvider struct { - Provider *MarshallableProviderShim `json:"provider"` - Name string `json:"name"` - Version string `json:"version"` - Config map[string]*MarshallableSchema `json:"config,omitempty"` - Resources map[string]*MarshallableResource `json:"resources,omitempty"` - DataSources map[string]*MarshallableDataSource `json:"dataSources,omitempty"` - TFProviderVersion string `json:"tfProviderVersion,omitempty"` + Provider *MarshallableProviderShim `json:"provider"` + Name string `json:"name"` + Version string `json:"version"` + Config map[string]*MarshallableSchema `json:"config,omitempty"` + Resources map[string]*MarshallableResource `json:"resources,omitempty"` + DataSources map[string]*MarshallableDataSource `json:"dataSources,omitempty"` + ListResources map[string]*MarshallableListResource `json:"listResources,omitempty"` + TFProviderVersion string `json:"tfProviderVersion,omitempty"` } // MarshalProvider converts a Pulumi ProviderInfo value into a MarshallableProviderInfo value. @@ -1310,6 +1357,10 @@ func MarshalProvider(p *Provider) *MarshallableProvider { for k, v := range p.DataSources { dataSources[k] = MarshalDataSource(v) } + listResources := make(map[string]*MarshallableListResource) + for k, v := range p.ListResources { + listResources[k] = MarshalListResource(v) + } info := MarshallableProvider{ Provider: MarshalProviderShim(p.P), @@ -1318,6 +1369,7 @@ func MarshalProvider(p *Provider) *MarshallableProvider { Config: config, Resources: resources, DataSources: dataSources, + ListResources: listResources, TFProviderVersion: p.TFProviderVersion, } @@ -1338,6 +1390,10 @@ func (m *MarshallableProvider) Unmarshal() *Provider { for k, v := range m.DataSources { dataSources[k] = v.Unmarshal() } + listResources := make(map[string]*ListResource) + for k, v := range m.ListResources { + listResources[k] = v.Unmarshal() + } info := Provider{ P: m.Provider.Unmarshal(), @@ -1346,6 +1402,7 @@ func (m *MarshallableProvider) Unmarshal() *Provider { Config: config, Resources: resources, DataSources: dataSources, + ListResources: listResources, TFProviderVersion: m.TFProviderVersion, } diff --git a/pkg/tfgen/generate.go b/pkg/tfgen/generate.go index 234fc30a7..cd4168d3d 100644 --- a/pkg/tfgen/generate.go +++ b/pkg/tfgen/generate.go @@ -871,6 +871,29 @@ func (rf *resourceFunc) ModuleMemberToken() tokens.ModuleMember { return tokens.NewModuleMemberToken(rf.mod, tokens.ModuleMemberName(rf.name)) } +// listResourceFunc is a generated list resource which is exposed as a function/invoke +type listResourceFunc struct { + mod tokens.Module + name string + doc string + args []*variable + rets []*variable + reqargs map[string]bool + argst *propertyType + retst *propertyType + schema shim.Resource + info *tfbridge.ListResourceInfo + entityDocs entityDocs + listResourcePath *paths.ListResourcePath +} + +func (rf *listResourceFunc) Name() string { return rf.name } +func (rf *listResourceFunc) Doc() string { return rf.doc } + +func (rf *listResourceFunc) ModuleMemberToken() tokens.ModuleMember { + return tokens.NewModuleMemberToken(rf.mod, tokens.ModuleMemberName(rf.name)) +} + // overlayFile is a file that should be added to a module "as-is" and then exported from its index. type overlayFile struct { name string @@ -1271,6 +1294,14 @@ func (g *Generator) gatherPackage() (*pkg, error) { pack.addModuleMap(dsmods) } + // Gather up all resource modules and merge them into the current set. + listResourceModules, err := g.gatherListResources() + if err != nil { + return nil, pkgerrors.Wrapf(err, "problem gathering list resources") + } else if listResourceModules != nil { + pack.addModuleMap(listResourceModules) + } + // Now go ahead and merge in any overlays into the modules if there are any. olaymods, err := g.gatherOverlays() if err != nil { @@ -1571,6 +1602,91 @@ func (g *Generator) gatherResource(rawname string, return res, nil } +func skipFailBuildOnExtraMapErrorEnv() bool { + return cmdutil.IsTruthy(os.Getenv("PULUMI_SKIP_MISSING_MAPPING_ERROR")) || + cmdutil.IsTruthy(os.Getenv("PULUMI_SKIP_PROVIDER_MAP_ERROR")) +} + +func skipFailBuildOnMissingMapErrorEnv() bool { + return cmdutil.IsTruthy(os.Getenv("PULUMI_SKIP_EXTRA_MAPPING_ERROR")) +} + +func (g *Generator) gatherListResources() (moduleMap, error) { + listResources := g.provider().ListResourcesMap() + if listResources.Len() == 0 { + return nil, nil + } + modules := make(moduleMap) + + skipFailBuildOnMissingMapError := skipFailBuildOnMissingMapErrorEnv() + skipFailBuildOnExtraMapError := skipFailBuildOnExtraMapErrorEnv() + + // let's keep a list of TF mapping errors that we can present to the user + var listResourceMappingErrors error + + // For each list resource, create its own dedicated function and module export. + var resourceError error + seen := make(map[string]bool) + for _, resource := range stableResources(listResources) { + resourceInfo := g.info.ListResources[resource] + if resourceInfo == nil { + if sliceContains(g.info.IgnoreMappings, resource) { + g.debug("TF list resource %q not found in provider map but ignored", resource) + continue + } + + if !skipFailBuildOnMissingMapError { + listResourceMappingErrors = multierror.Append(listResourceMappingErrors, + fmt.Errorf("TF list resource %q not mapped to the Pulumi provider", resource)) + } else { + g.warn("TF list resource %q not found in provider map", resource) + } + continue + } + seen[resource] = true + + fun, err := g.gatherListResource(resource, listResources.Get(resource), resourceInfo) + if err != nil { + // Keep track of the error, but keep going, so we can expose more at once. + resourceError = multierror.Append(resourceError, err) + } else { + // Add any members returned to the specified module. + modules.ensureModule(fun.mod).addMember(fun) + } + } + if resourceError != nil { + return nil, resourceError + } + + // Emit a warning if there is a map but some names didn't match. + var names []string + for name := range g.info.ListResources { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + if !seen[name] { + if !skipFailBuildOnExtraMapError { + listResourceMappingErrors = multierror.Append(listResourceMappingErrors, + fmt.Errorf("pulumi token %q is mapped to TF provider list resource %q, but no such "+ + "list resource found. Remove the mapping and try again", + g.info.ListResources[name].Tok, name)) + } else { + g.warn("pulumi token %q is mapped to TF provider list resource %q, but no such "+ + "list resource found. The mapping will be ignored in the generated provider", + g.info.ListResources[name].Tok, name) + } + } + } + + // let's check the unmapped DataSource Errors + if listResourceMappingErrors != nil { + return nil, listResourceMappingErrors + } + + return modules, nil +} + func (g *Generator) gatherDataSources() (moduleMap, error) { // If there aren't any data sources, skip this altogether. sources := g.provider().DataSourcesMap() @@ -1579,9 +1695,8 @@ func (g *Generator) gatherDataSources() (moduleMap, error) { } modules := make(moduleMap) - skipFailBuildOnMissingMapError := cmdutil.IsTruthy(os.Getenv("PULUMI_SKIP_MISSING_MAPPING_ERROR")) || - cmdutil.IsTruthy(os.Getenv("PULUMI_SKIP_PROVIDER_MAP_ERROR")) - skipFailBuildOnExtraMapError := cmdutil.IsTruthy(os.Getenv("PULUMI_SKIP_EXTRA_MAPPING_ERROR")) + skipFailBuildOnMissingMapError := skipFailBuildOnMissingMapErrorEnv() + skipFailBuildOnExtraMapError := skipFailBuildOnExtraMapErrorEnv() // let's keep a list of TF mapping errors that we can present to the user var dataSourceMappingErrors error @@ -1649,6 +1764,91 @@ func (g *Generator) gatherDataSources() (moduleMap, error) { return modules, nil } +func (g *Generator) gatherListResource( + rawname string, + resource shim.Resource, + info *tfbridge.ListResourceInfo, +) (*listResourceFunc, error) { + name, moduleName := listResourceName(g.info.Name, rawname, info) + mod := tokens.NewModuleToken(g.pkg, moduleName) + path := paths.NewListResourcePath(rawname, tokens.NewModuleMemberToken(mod, name)) + + entityDocs := entityDocs{Description: ""} + fun := &listResourceFunc{ + mod: mod, + name: name.String(), + doc: "", + reqargs: make(map[string]bool), + schema: resource, + info: info, + entityDocs: entityDocs, + listResourcePath: path, + } + + // See if arguments for this function are optional, and generate detailed metadata. + for _, arg := range stableSchemas(resource.Schema()) { + sch := resource.Schema().Get(arg) + if sch.Removed() != "" { + continue + } + cust := info.Fields[arg] + + // Remember detailed information for every input arg (we will use it below). + if input(sch, cust) { + doc, foundInAttributes := getDescriptionFromParsedDocs(entityDocs, arg) + if foundInAttributes { + argumentDescriptionsFromAttributes++ + msg := fmt.Sprintf("Argument desc taken from attributes: data source, rawname = '%s', property = '%s'", + rawname, arg) + g.debug(msg) + } + + argvar, err := g.propertyVariable(path.Args(), + arg, resource.Schema(), info.Fields, doc, "", false /*out*/, entityDocs) + if err != nil { + return nil, err + } + if argvar != nil { + fun.args = append(fun.args, argvar) + if !argvar.optional() { + fun.reqargs[argvar.name] = true + } + } + } + + // Also remember properties for the resulting return data structure. + // Emit documentation for the property if available + p, err := g.propertyVariable(path.Results(), arg, resource.Schema(), info.Fields, + "" /*attributeDoc*/, "", true /*out*/, entityDocs) + if err != nil { + return nil, err + } + if p != nil { + fun.rets = append(fun.rets, p) + } + } + + // Produce the args/return types, if needed. + if len(fun.args) > 0 { + fun.argst = &propertyType{ + kind: kindObject, + name: fmt.Sprintf("%sArgs", upperFirst(name.String())), + doc: fmt.Sprintf("A collection of arguments for invoking %s.", name), + properties: fun.args, + } + } + if len(fun.rets) > 0 { + fun.retst = &propertyType{ + kind: kindObject, + name: fmt.Sprintf("%sResult", upperFirst(name.String())), + doc: fmt.Sprintf("A collection of values returned by %s.", name), + properties: fun.rets, + } + } + + return fun, nil +} + // gatherDataSource returns the module name and members for the given data source function. func (g *Generator) gatherDataSource(rawname string, ds shim.Resource, info *tfbridge.DataSourceInfo, @@ -2010,6 +2210,22 @@ func resourceName(provider string, rawname string, return info.Tok.Name(), info.Tok.Module().Name() } +func listResourceName(provider string, rawname string, + info *tfbridge.ListResourceInfo, +) (tokens.ModuleMemberName, tokens.ModuleName) { + if info == nil || info.Tok == "" { + // default transformations. + nameAndModule := withoutPackageName(provider, rawname) // strip off the pkg prefix. + pulumiName := tfbridge.TerraformToPulumiNameV2(nameAndModule, nil, nil) + moduleName := tokens.ModuleName(nameAndModule) + memberName := "list" + upperFirst(pulumiName) + return tokens.ModuleMemberName(memberName), moduleName + } + + // otherwise, a custom transformation exists; use it. + return tokens.ModuleMemberName(info.Tok.Name()), info.Tok.Module().Name() +} + // withoutPackageName strips off the package prefix from a raw name. func withoutPackageName(pkg string, rawname string) string { // Providers almost always have function and resource names prefixed with the package name, diff --git a/pkg/tfgen/generate_schema.go b/pkg/tfgen/generate_schema.go index f3d28dc70..3254ec3dc 100644 --- a/pkg/tfgen/generate_schema.go +++ b/pkg/tfgen/generate_schema.go @@ -101,6 +101,10 @@ func (nt *schemaNestedTypes) gatherFromMember(member moduleMember) { if !member.IsProvider() { nt.gatherFromProperties(p.State(), member, member.name, member.statet.properties, true) } + case *listResourceFunc: + p := member.listResourcePath + nt.gatherFromProperties(p.Args(), member, member.name, member.args, true) + nt.gatherFromProperties(p.Results(), member, member.name, member.rets, false) case *resourceFunc: p := member.dataSourcePath nt.gatherFromProperties(p.Args(), member, member.name, member.args, true) @@ -267,6 +271,148 @@ func genPulumiSchema( return pulumiPackageSpec, nil } +func (g *schemaGenerator) genListResourceFunc( + _ tokens.Module, + fun *listResourceFunc, +) (pschema.FunctionSpec, map[string]pschema.ObjectTypeSpec) { + var spec pschema.FunctionSpec + supportingTypes := make(map[string]pschema.ObjectTypeSpec) + + description := "" + if fun.doc != "" { + description = g.genDocComment(fun.doc) + } + + spec.Description = description + + inputs := map[string]pschema.PropertySpec{} + inputs["limit"] = pschema.PropertySpec{ + Description: "Maximum number of results to return.", + TypeSpec: pschema.TypeSpec{ + Type: "integer", + }, + } + + inputs["includeResource"] = pschema.PropertySpec{ + Description: "Whether to populate the field resource in the results.", + TypeSpec: pschema.TypeSpec{ + Type: "boolean", + }, + } + + if fun.args != nil { + // generate the ConfigArgs object type. + configObjectTypeName := upperFirst(fun.name) + "ConfigArgs" + configObjectType := &schemaNestedType{ + typePaths: paths.SingletonTypePathSet(fun.listResourcePath.Args()), + typ: &propertyType{ + doc: "Configuration for listing the resource", + kind: kindObject, + properties: fun.args, + name: configObjectTypeName, + }, + } + + configArgsType := g.genObjectType(configObjectType, false) + argTypeToken := g.genObjectTypeToken(configObjectType) + supportingTypes[argTypeToken] = configArgsType + + inputs["config"] = pschema.PropertySpec{ + Description: "Configuration for listing the resource", + TypeSpec: pschema.TypeSpec{ + Ref: "#/types/" + argTypeToken, + }, + } + } + + spec.Inputs = &pschema.ObjectTypeSpec{ + Type: "object", + Properties: inputs, + } + + // ListResource responses are in the shape of + // { results: [ { resource: , displayName: }, ... ] } + // generate the output type of the invoke accordingly. + if fun.rets != nil { + resourceOutputTypeName := upperFirst(fun.name) + "Resource" + resourceOutputType := &schemaNestedType{ + typePaths: paths.SingletonTypePathSet(fun.listResourcePath.Results()), + typ: &propertyType{ + doc: "A resource returned by the list resource function", + kind: kindObject, + properties: fun.rets, + name: resourceOutputTypeName, + }, + } + + resourceType := g.genObjectType(resourceOutputType, false) + resourceTypeToken := g.genObjectTypeToken(resourceOutputType) + + resultItemTypeName := upperFirst(fun.name) + "ResultItem" + resultItemTypePaths := paths.SingletonTypePathSet(fun.listResourcePath.Results()) + mod := modulePlacementForTypeSet(g.pkg, resultItemTypePaths) + resultItemTypeToken := fmt.Sprintf("%s/%s:%s", mod.String(), resultItemTypeName, resultItemTypeName) + + resultItemType := pschema.ObjectTypeSpec{ + Type: "object", + Properties: map[string]pschema.PropertySpec{ + "resource": { + Description: "The resource returned by the list resource function", + TypeSpec: pschema.TypeSpec{ + Ref: "#/types/" + resourceTypeToken, + }, + }, + "displayName": { + Description: "The display name of the resource", + TypeSpec: pschema.TypeSpec{ + Type: "string", + }, + }, + }, + } + + spec.Outputs = &pschema.ObjectTypeSpec{ + Type: "object", + Properties: map[string]pschema.PropertySpec{ + "results": { + Description: "The list of results returned by the function", + TypeSpec: pschema.TypeSpec{ + Type: "array", + Items: &pschema.TypeSpec{ + Ref: "#/types/" + resultItemTypeToken, + }, + }, + }, + }, + } + + supportingTypes[resourceTypeToken] = resourceType + supportingTypes[resultItemTypeToken] = resultItemType + } else { + // if outputs aren't schematized, generate a the following shape: + // { results: [ { : } ] } + spec.Outputs = &pschema.ObjectTypeSpec{ + Type: "object", + Properties: map[string]pschema.PropertySpec{ + "results": { + Description: "The list of results returned by the function", + TypeSpec: pschema.TypeSpec{ + Type: "array", + Items: &pschema.TypeSpec{ + Type: "object", + AdditionalProperties: &pschema.TypeSpec{ + Ref: "pulumi.json#/Any", + }, + }, + }, + }, + }, + } + } + + return spec, supportingTypes +} + func (g *schemaGenerator) genPackageSpec(pack *pkg, sink diag.Sink) (pschema.PackageSpec, error) { spec := pschema.PackageSpec{ Name: g.pkg.String(), @@ -313,6 +459,14 @@ func (g *schemaGenerator) genPackageSpec(pack *pkg, sink diag.Sink) (pschema.Pac spec.Resources[string(t.info.Tok)] = g.genResourceType(mod.name, t) case *resourceFunc: spec.Functions[string(t.info.Tok)] = g.genDatasourceFunc(mod.name, t) + case *listResourceFunc: + funcSpec, supportingTypes := g.genListResourceFunc(mod.name, t) + spec.Functions[string(t.info.Tok)] = funcSpec + for k, v := range supportingTypes { + spec.Types[k] = pschema.ComplexTypeSpec{ + ObjectTypeSpec: v, + } + } case *variable: contract.Assertf(mod.config(), `mod.config()`) config = append(config, t) @@ -1304,6 +1458,9 @@ func modulePlacementForType(pkg tokens.Package, path paths.TypePath) tokens.Modu // may also be defined in the same module. m := pp.DataSourcePath.Token().Module() return parentModuleOrSelf(m) + case *paths.ListResourceMemberPath: + m := pp.ListResourcePath.Token().Module() + return parentModuleOrSelf(m) case *paths.ConfigPath: return tokens.NewModuleToken(pkg, configMod) default: diff --git a/pkg/tfgen/internal/paths/paths.go b/pkg/tfgen/internal/paths/paths.go index 4b2c0ae0c..e9db2dd33 100644 --- a/pkg/tfgen/internal/paths/paths.go +++ b/pkg/tfgen/internal/paths/paths.go @@ -226,6 +226,87 @@ func (p *DataSourceMemberPath) UniqueKey() string { return p.String() } +// Identifies a list resource uniquely. +type ListResourcePath struct { + key string + token tokens.ModuleMember +} + +func NewListResourcePath(key string, token tokens.ModuleMember) *ListResourcePath { + return &ListResourcePath{ + key: key, + token: token, + } +} + +// Pulumi token uniquely identifying the ListResource. +func (p *ListResourcePath) Token() tokens.ModuleMember { + return p.token +} + +// Unique identifier for the ListResource preserved from the shim layer, typically the Terraform name. +func (p *ListResourcePath) Key() string { + return p.key +} + +func (p *ListResourcePath) Args() *ListResourceMemberPath { + return &ListResourceMemberPath{ + ListResourcePath: p, + ListResourceMemberKind: ListResourceArgs, + } +} + +func (p *ListResourcePath) Results() *ListResourceMemberPath { + return &ListResourceMemberPath{ + ListResourcePath: p, + ListResourceMemberKind: ListResourceResults, + } +} + +func (p *ListResourcePath) String() string { + return fmt.Sprintf("listresource[key=%q,token=%q]", + p.key, + p.token.String()) +} + +type ListResourceMemberKind int + +const ( + ListResourceArgs ListResourceMemberKind = iota + ListResourceResults +) + +func (s ListResourceMemberKind) String() string { + switch s { + case ListResourceArgs: + return "args" + case ListResourceResults: + return "results" + } + return "unknown" +} + +type ListResourceMemberPath struct { + ListResourcePath *ListResourcePath + ListResourceMemberKind ListResourceMemberKind +} + +var _ TypePath = (*ListResourceMemberPath)(nil) + +func (p *ListResourceMemberPath) Parent() TypePath { + return nil +} + +func (p *ListResourceMemberPath) String() string { + return fmt.Sprintf("%s.%s", + p.ListResourcePath.String(), + p.ListResourceMemberKind.String()) +} + +func (p *ListResourceMemberPath) UniqueKey() string { + return p.String() +} + type ConfigPath struct{} var _ TypePath = (*ConfigPath)(nil) diff --git a/pkg/tfshim/schema/provider.go b/pkg/tfshim/schema/provider.go index 331583070..9c5e16b45 100644 --- a/pkg/tfshim/schema/provider.go +++ b/pkg/tfshim/schema/provider.go @@ -8,9 +8,10 @@ import ( ) type Provider struct { - Schema shim.SchemaMap - ResourcesMap shim.ResourceMap - DataSourcesMap shim.ResourceMap + Schema shim.SchemaMap + ResourcesMap shim.ResourceMap + DataSourcesMap shim.ResourceMap + ListResourcesMap shim.ResourceMap internalinter.Internal } @@ -25,6 +26,9 @@ func (p *Provider) Shim() shim.Provider { if c.DataSourcesMap == nil { c.DataSourcesMap = ResourceMap{} } + if c.ListResourcesMap == nil { + c.ListResourcesMap = ResourceMap{} + } return newProviderShim(c) } @@ -49,6 +53,10 @@ func (s ProviderShim) DataSourcesMap() shim.ResourceMap { return s.V.DataSourcesMap } +func (s ProviderShim) ListResourcesMap() shim.ResourceMap { + return s.V.ListResourcesMap +} + func (s ProviderShim) InternalValidate() error { return nil } diff --git a/pkg/tfshim/sdk-v1/provider.go b/pkg/tfshim/sdk-v1/provider.go index 7a18461a0..55d7c7247 100644 --- a/pkg/tfshim/sdk-v1/provider.go +++ b/pkg/tfshim/sdk-v1/provider.go @@ -73,6 +73,10 @@ func (p v1Provider) DataSourcesMap() shim.ResourceMap { return v1ResourceMap(p.tf.DataSourcesMap) } +func (p v1Provider) ListResourcesMap() shim.ResourceMap { + return v1ResourceMap{} +} + func (p v1Provider) InternalValidate() error { return p.tf.InternalValidate() } diff --git a/pkg/tfshim/sdk-v2/provider.go b/pkg/tfshim/sdk-v2/provider.go index 915169e79..ad9a94d36 100644 --- a/pkg/tfshim/sdk-v2/provider.go +++ b/pkg/tfshim/sdk-v2/provider.go @@ -81,6 +81,10 @@ func (p v2Provider) DataSourcesMap() shim.ResourceMap { return v2ResourceMap(p.tf.DataSourcesMap) } +func (p v2Provider) ListResourcesMap() shim.ResourceMap { + return v2ResourceMap{} +} + func (p v2Provider) InternalValidate() error { return p.tf.InternalValidate() } diff --git a/pkg/tfshim/shim.go b/pkg/tfshim/shim.go index ab6b56a80..7ab0b21ae 100644 --- a/pkg/tfshim/shim.go +++ b/pkg/tfshim/shim.go @@ -300,6 +300,7 @@ type Provider interface { Schema() SchemaMap ResourcesMap() ResourceMap DataSourcesMap() ResourceMap + ListResourcesMap() ResourceMap InternalValidate() error Validate(ctx context.Context, c ResourceConfig) ([]string, []error) diff --git a/pkg/tfshim/util/filter.go b/pkg/tfshim/util/filter.go index 9c49fb460..44090dbee 100644 --- a/pkg/tfshim/util/filter.go +++ b/pkg/tfshim/util/filter.go @@ -41,9 +41,10 @@ import ( // PULUMI_SKIP_EXTRA_MAPPING_ERROR=1 make provider type FilteringProvider struct { - Provider shim.Provider - ResourceFilter func(token string) bool - DataSourceFilter func(token string) bool + Provider shim.Provider + ResourceFilter func(token string) bool + DataSourceFilter func(token string) bool + ListResourcesFilter func(token string) bool internalinter.Internal } @@ -61,6 +62,10 @@ func (p *FilteringProvider) DataSourcesMap() shim.ResourceMap { return &filteringMap{p.Provider.DataSourcesMap(), p.DataSourceFilter} } +func (p *FilteringProvider) ListResourcesMap() shim.ResourceMap { + return &filteringMap{p.Provider.ListResourcesMap(), p.ListResourcesFilter} +} + func (p *FilteringProvider) InternalValidate() error { return p.Provider.InternalValidate() } diff --git a/pkg/tfshim/util/util.go b/pkg/tfshim/util/util.go index d526d9b37..27312574c 100644 --- a/pkg/tfshim/util/util.go +++ b/pkg/tfshim/util/util.go @@ -28,11 +28,11 @@ type UnimplementedProvider struct { internalinter.Internal } -func (UnimplementedProvider) Schema() shim.SchemaMap { panic("unimplemented") } -func (UnimplementedProvider) ResourcesMap() shim.ResourceMap { panic("unimplemented") } -func (UnimplementedProvider) DataSourcesMap() shim.ResourceMap { panic("unimplemented") } - -func (UnimplementedProvider) InternalValidate() error { panic("unimplemented") } +func (UnimplementedProvider) Schema() shim.SchemaMap { panic("unimplemented") } +func (UnimplementedProvider) ResourcesMap() shim.ResourceMap { panic("unimplemented") } +func (UnimplementedProvider) DataSourcesMap() shim.ResourceMap { panic("unimplemented") } +func (UnimplementedProvider) ListResourcesMap() shim.ResourceMap { panic("unimplemented") } +func (UnimplementedProvider) InternalValidate() error { panic("unimplemented") } func (UnimplementedProvider) Validate( ctx context.Context, c shim.ResourceConfig,