diff --git a/pkg/convert/convert.go b/pkg/convert/convert.go index b43b5bc9b..c392041c2 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) + NewEphemeralResourceDecoder(ephemeralResource string, resourceType tftypes.Object) (Decoder, error) + NewEphemeralResourceEncoder(ephemeralResource string, resourceType tftypes.Object) (Encoder, 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..e593eb405 100644 --- a/pkg/convert/encoding.go +++ b/pkg/convert/encoding.go @@ -89,6 +89,24 @@ func (e *encoding) NewDataSourceDecoder( return dec, nil } +func (e *encoding) NewEphemeralResourceEncoder(resource string, objectType tftypes.Object) (Encoder, error) { + mctx := newEphemeralResourceSchemaMapContext(resource, 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 resource %q: %w", resource, err) + } + return enc, nil +} + +func (e *encoding) NewEphemeralResourceDecoder(resource string, objectType tftypes.Object) (Decoder, error) { + mctx := newEphemeralResourceSchemaMapContext(resource, e.SchemaOnlyProvider, e.ProviderInfo) + dec, err := NewObjectDecoder(ObjectSchema{mctx.schemaMap, mctx.schemaInfos, &objectType}) + if err != nil { + return nil, fmt.Errorf("cannot derive a decoder for resource %q: %w", resource, 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..694985aa6 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 newEphemeralResourceSchemaMapContext( + resource string, + schemaOnlyProvider shim.Provider, + providerInfo *tfbridge.ProviderInfo, +) *schemaMapContext { + r := schemaOnlyProvider.EphemeralResourcesMap().Get(resource) + contract.Assertf(r != nil, "no ephemeral resource %q found in ResourceMap", resource) + sm := r.Schema() + var fields map[string]*tfbridge.SchemaInfo + if providerInfo != nil { + fields = providerInfo.EphemeralResources[resource].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..0b85894c4 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" + eschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/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 FromEphemeralResourceAttribute(x eschema.Attribute) Attr { + return FromAttrLike(x) +} + func FromAttrLike(attrLike AttrLike) Attr { nested, nestingMode := extractNestedAttributes(attrLike) hasDefault := hasDefault(attrLike) diff --git a/pkg/pf/internal/pfutils/ephemeral_resources.go b/pkg/pf/internal/pfutils/ephemeral_resources.go new file mode 100644 index 000000000..bf73f5e1d --- /dev/null +++ b/pkg/pf/internal/pfutils/ephemeral_resources.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" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/provider" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/internal/runtypes" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" +) + +func GatherEphemeralResources[F func(Schema) shim.SchemaMap]( + ctx context.Context, prov provider.Provider, f F, +) (runtypes.EphemeralResources, error) { + provMetadata := queryProviderMetadata(ctx, prov) + es := make(collection[func() ephemeral.EphemeralResource]) + + eprov, ok := prov.(provider.ProviderWithEphemeralResources) + if ok { + for _, makeEphemeralResource := range eprov.EphemeralResources(ctx) { + ephemeralResource := makeEphemeralResource() + + meta := ephemeral.MetadataResponse{} + ephemeralResource.Metadata(ctx, ephemeral.MetadataRequest{ + ProviderTypeName: provMetadata.TypeName, + }, &meta) + + schemaResponse := &ephemeral.SchemaResponse{} + ephemeralResource.Schema(ctx, ephemeral.SchemaRequest{}, schemaResponse) + + ephemeralResourceSchema := schemaResponse.Schema + diag := schemaResponse.Diagnostics + if err := checkDiagsForErrors(diag); err != nil { + return nil, fmt.Errorf("Resource %s GetSchema() error: %w", meta.TypeName, err) + } + + es[runtypes.TypeOrRenamedEntityName(meta.TypeName)] = entry[func() ephemeral.EphemeralResource]{ + t: makeEphemeralResource, + schema: FromEphemeralResourceSchema(ephemeralResourceSchema), + tfName: runtypes.TypeName(meta.TypeName), + } + } + } + + return &ephemeralResources{collection: es, convert: f}, nil +} + +type ephemeralResources struct { + collection[func() ephemeral.EphemeralResource] + convert func(Schema) shim.SchemaMap +} + +func (r ephemeralResources) Schema(t runtypes.TypeOrRenamedEntityName) runtypes.Schema { + entry := r.collection[t] + return runtypesSchemaAdapter{entry.schema, r.convert, entry.tfName} +} + +func (ephemeralResources) IsEphemeralResources() {} diff --git a/pkg/pf/internal/pfutils/schema.go b/pkg/pf/internal/pfutils/schema.go index b5b618797..bb132bc45 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" + eschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/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,11 @@ func FromResourceSchema(x rschema.Schema) Schema { return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, &x) } +func FromEphemeralResourceSchema(x eschema.Schema) Schema { + attrs := convertMap(FromEphemeralResourceAttribute, x.Attributes) + return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, nil, 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..c18a9ef66 100644 --- a/pkg/pf/internal/runtypes/types.go +++ b/pkg/pf/internal/runtypes/types.go @@ -66,3 +66,9 @@ type DataSources interface { collection IsDataSources() } + +// Represents all provider's ephemeral resources pre-indexed by TypeOrRenamedEntityName. +type EphemeralResources interface { + collection + IsEphemeralResources() +} diff --git a/pkg/pf/internal/schemashim/ephemeral_resource_map.go b/pkg/pf/internal/schemashim/ephemeral_resource_map.go new file mode 100644 index 000000000..d4326133a --- /dev/null +++ b/pkg/pf/internal/schemashim/ephemeral_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 newSchemaOnlyEphemeralResourceMap(resources runtypes.EphemeralResources) schemaOnlyEphemeralResourceMap { + m := schemaOnlyEphemeralResourceMap{Map: make(map[string]*schemaOnlyResource)} + for _, name := range resources.All() { + key := string(name) + v := resources.Schema(name) + m.Map[key] = newSchemaOnlyResource(v) + } + return m +} + +type schemaOnlyEphemeralResourceMap struct { + internalinter.Internal + Map map[string]*schemaOnlyResource +} + +var ( + _ shim.ResourceMap = schemaOnlyEphemeralResourceMap{} + _ runtypes.EphemeralResources = schemaOnlyEphemeralResourceMap{} +) + +func (m schemaOnlyEphemeralResourceMap) Len() int { + return len(m.Map) +} + +func (m schemaOnlyEphemeralResourceMap) Get(key string) shim.Resource { + return m.Map[key] +} + +func (m schemaOnlyEphemeralResourceMap) GetOk(key string) (shim.Resource, bool) { + v, ok := m.Map[key] + return v, ok +} + +func (m schemaOnlyEphemeralResourceMap) Range(each func(key string, value shim.Resource) bool) { + for k, v := range m.Map { + if !each(k, v) { + return + } + } +} + +func (m schemaOnlyEphemeralResourceMap) Set(key string, value shim.Resource) { + v, ok := value.(*schemaOnlyResource) + contract.Assertf(ok, "Set must be a %T, found a %T", v, value) + m.Map[key] = v +} + +func (m schemaOnlyEphemeralResourceMap) 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 schemaOnlyEphemeralResourceMap) Has(key runtypes.TypeOrRenamedEntityName) bool { + _, ok := m.Map[string(key)] + return ok +} + +func (m schemaOnlyEphemeralResourceMap) Schema(key runtypes.TypeOrRenamedEntityName) runtypes.Schema { + return m.Map[string(key)].tf +} + +func (m schemaOnlyEphemeralResourceMap) IsEphemeralResources() {} diff --git a/pkg/pf/internal/schemashim/provider.go b/pkg/pf/internal/schemashim/provider.go index f12ec9b83..7c3ede9f0 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 + ephemeralResourceMap schemaOnlyEphemeralResourceMap internalinter.Internal } @@ -62,6 +63,10 @@ func (p *SchemaOnlyProvider) DataSources(ctx context.Context) (runtypes.DataSour return p.dataSourceMap, nil } +func (p *SchemaOnlyProvider) EphemeralResources(ctx context.Context) (runtypes.EphemeralResources, error) { + return p.ephemeralResourceMap, nil +} + func (p *SchemaOnlyProvider) Config(ctx context.Context) (tftypes.Object, error) { schemaResponse := &pfprovider.SchemaResponse{} p.tf.Schema(ctx, pfprovider.SchemaRequest{}, schemaResponse) @@ -93,6 +98,10 @@ func (p *SchemaOnlyProvider) DataSourcesMap() shim.ResourceMap { return p.dataSourceMap } +func (p *SchemaOnlyProvider) EphemeralResourcesMap() shim.ResourceMap { + return p.ephemeralResourceMap +} + func (p *SchemaOnlyProvider) InternalValidate() error { return nil } diff --git a/pkg/pf/internal/schemashim/schemashim.go b/pkg/pf/internal/schemashim/schemashim.go index fe413c038..5d6b6e330 100644 --- a/pkg/pf/internal/schemashim/schemashim.go +++ b/pkg/pf/internal/schemashim/schemashim.go @@ -32,12 +32,18 @@ func ShimSchemaOnlyProvider(ctx context.Context, provider pfprovider.Provider) s if err != nil { panic(err) } + ephemeralResources, err := pfutils.GatherEphemeralResources(ctx, provider, NewSchemaMap) + if err != nil { + panic(err) + } resourceMap := newSchemaOnlyResourceMap(resources) dataSourceMap := newSchemaOnlyDataSourceMap(dataSources) + ephemeralResourceMap := newSchemaOnlyEphemeralResourceMap(ephemeralResources) return &SchemaOnlyProvider{ - ctx: ctx, - tf: provider, - resourceMap: resourceMap, - dataSourceMap: dataSourceMap, + ctx: ctx, + tf: provider, + resourceMap: resourceMap, + dataSourceMap: dataSourceMap, + ephemeralResourceMap: ephemeralResourceMap, } } diff --git a/pkg/pf/proto/protov6.go b/pkg/pf/proto/protov6.go index dd882b005..197f07e6c 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) EphemeralResourcesMap() shim.ResourceMap { + v, err := p.getSchema() + if err != nil { + tfbridge.GetLogger(p.ctx).Error(err.Error()) + return nil + } + return resourceMap(v.EphemeralResourceSchemas) +} diff --git a/pkg/pf/proto/runtypes.go b/pkg/pf/proto/runtypes.go index 18b271c90..36d21561f 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) EphemeralResources(context.Context) (runtypes.EphemeralResources, error) { + v, err := p.getSchema() + if err != nil { + return nil, err + } + return ephemeralResources{collection(v.EphemeralResourceSchemas)}, nil +} + type schema struct { s *tfprotov6.Schema tfName runtypes.TypeName @@ -85,6 +93,12 @@ var _ runtypes.DataSources = datasources{} func (datasources) IsDataSources() {} +var _ runtypes.EphemeralResources = ephemeralResources{} + +type ephemeralResources struct{ collection } + +func (ephemeralResources) IsEphemeralResources() {} + type collection map[string]*tfprotov6.Schema func (c collection) All() []runtypes.TypeOrRenamedEntityName { diff --git a/pkg/pf/provider.go b/pkg/pf/provider.go index 8356ca411..231ad36ca 100644 --- a/pkg/pf/provider.go +++ b/pkg/pf/provider.go @@ -34,5 +34,6 @@ type ShimProvider interface { Server(context.Context) (tfprotov6.ProviderServer, error) Resources(context.Context) (runtypes.Resources, error) DataSources(context.Context) (runtypes.DataSources, error) + EphemeralResources(context.Context) (runtypes.EphemeralResources, 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..93155b50c 100644 --- a/pkg/pf/tests/internal/testprovider/testbridge.go +++ b/pkg/pf/tests/internal/testprovider/testbridge.go @@ -1,4 +1,4 @@ -// Copyright 2016-2023, Pulumi Corporation. +// 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. @@ -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/ephemeral" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" prschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -129,6 +130,10 @@ func SyntheticTestBridgeProvider() tfbridge.ProviderInfo { "testbridge_smac_ds": {Tok: "testbridge:index/smac:SMAC"}, }, + EphemeralResources: map[string]*tfbridge.EphemeralResourceInfo{ + "testbridge_testeph": {Tok: "testbridge:index/ephemeral:Testeph"}, + }, + MetadataInfo: tfbridge.NewProviderMetadata(testBridgeMetadata), } @@ -145,6 +150,7 @@ type resourceData struct { } var _ provider.Provider = (*syntheticProvider)(nil) +var _ provider.ProviderWithEphemeralResources = (*syntheticProvider)(nil) func (p *syntheticProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { resp.TypeName = "testbridge" @@ -337,3 +343,9 @@ func (p *syntheticProvider) Resources(context.Context) []func() resource.Resourc newVlanNamesRes, } } + +func (p *syntheticProvider) EphemeralResources(context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + newTesteph, + } +} diff --git a/pkg/pf/tests/internal/testprovider/testbridge_resource_testeph.go b/pkg/pf/tests/internal/testprovider/testbridge_resource_testeph.go new file mode 100644 index 000000000..1f8aa34a4 --- /dev/null +++ b/pkg/pf/tests/internal/testprovider/testbridge_resource_testeph.go @@ -0,0 +1,225 @@ +// Copyright 2025-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 testprovider + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + eschema "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil" +) + +type testeph struct{} + +var _ ephemeral.EphemeralResource = &testeph{} + +func newTesteph() ephemeral.EphemeralResource { + return &testeph{} +} + +func (*testeph) schema() eschema.Schema { + return eschema.Schema{ + Description: ` +testbridge_testeph resource is built to facilitate testing ephemeral resources in the Pulumi bridge. + +It emulates cloud state by storing the state in a binary file identified, with location configured by the statedir +attribute. +`, + Attributes: map[string]eschema.Attribute{ + "id": eschema.StringAttribute{ + Computed: true, + }, + "statedir": eschema.StringAttribute{ + Required: true, + Description: "Dir to store pseudo-cloud state in.", + }, + }, + } +} + +func (e *testeph) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_testeph" +} + +func (e *testeph) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = e.schema() +} + +func (e *testeph) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var statedir string + diags0 := req.Config.GetAttribute(ctx, path.Root("statedir"), &statedir) + resp.Diagnostics.Append(diags0...) + if resp.Diagnostics.HasError() { + return + } + resourceID, err := e.freshID(statedir) + if err != nil { + resp.Diagnostics.AddError("testres.freshID", err.Error()) + return + } + + cloudStateFile := e.cloudStateFile(statedir, resourceID) + if _, gotState, err := e.readCloudState(ctx, cloudStateFile); gotState && err == nil { + resp.Diagnostics.AddError("testbridge_testres.Create found unexpected pseudo-cloud state", + cloudStateFile) + } + + // Copy plan to state. + resp.Result.Raw = req.Config.Raw.Copy() + + // Set id computed by the provider. + diags2 := resp.Result.SetAttribute(ctx, path.Root("id"), resourceID) + resp.Diagnostics.Append(diags2...) + if resp.Diagnostics.HasError() { + return + } + + if err := e.writeCloudState(ctx, cloudStateFile, resp.Result); err != nil { + resp.Diagnostics.AddError("testbridge_testres.Create cannot write pseudo-cloud state", + err.Error()) + } + + setJSON := func(key string, value string) { + data, err := json.Marshal(value) + if err != nil { + resp.Diagnostics.AddError("testbridge_testres.Open cannot marshal private state", + err.Error()) + return + } + diags := resp.Private.SetKey(ctx, key, data) + resp.Diagnostics.Append(diags...) + } + + setJSON("statedir", statedir) + setJSON("id", resourceID) +} + +func (e *testeph) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + getJSON := func(key string) string { + var value string + data, diags := req.Private.GetKey(ctx, key) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return "" + } + if err := json.Unmarshal(data, &value); err != nil { + resp.Diagnostics.AddError("testbridge_testres.Close cannot unmarshal private state", + err.Error()) + return "" + } + return value + } + + statedir := getJSON("statedir") + if statedir == "" { + return + } + resourceID := getJSON("id") + if resourceID == "" { + return + } + + cloudStateFile := e.cloudStateFile(statedir, resourceID) + if err := e.deleteCloudState(cloudStateFile); err != nil { + resp.Diagnostics.AddError("testbridge_testres.Close cannot delete pseudo-cloud state", + err.Error()) + } +} + +func (e *testeph) freshID(statedir string) (string, error) { + mu := fsutil.NewFileMutex(filepath.Join(statedir, "testres.lock")) + if err := mu.Lock(); err != nil { + return "", err + } + defer func() { + if err := mu.Unlock(); err != nil { + panic(err) + } + }() + + cF := filepath.Join(statedir, "testres.counter") + + i := 0 + f, err := os.ReadFile(cF) + if err != nil && !os.IsNotExist(err) { + return "", err + } + if err == nil { + i, err = strconv.Atoi(string(f)) + if err != nil { + return "", err + } + } + + if err := os.WriteFile(cF, []byte(fmt.Sprintf("%d", i+1)), 0o600); err != nil { + return "", err + } + + return fmt.Sprintf("%d", i), nil +} + +func (e *testeph) cloudStateFile(statedir, resourceID string) string { + return filepath.Join(statedir, fmt.Sprintf("%s.bin", resourceID)) +} + +func (e *testeph) deleteCloudState(file string) error { + return os.Remove(file) +} + +func (e *testeph) readCloudState(ctx context.Context, file string) (tfsdk.State, bool, error) { + bytes, err := os.ReadFile(file) + + if err != nil && os.IsNotExist(err) { + return tfsdk.State{}, false, nil + } + + if err != nil { + return tfsdk.State{}, false, err + } + + state, err := e.bytesToState(ctx, bytes) + return state, err == nil, err +} + +func (e *testeph) writeCloudState(ctx context.Context, file string, state tfsdk.EphemeralResultData) error { + stateBytes, err := e.stateToBytes(ctx, state) + if err != nil { + return err + } + return os.WriteFile(file, stateBytes, 0o600) +} + +func (*testeph) stateToBytes(ctx context.Context, state tfsdk.EphemeralResultData) ([]byte, error) { + typ := state.Schema.Type().TerraformType(ctx) + dv, err := tfprotov6.NewDynamicValue(typ, state.Raw) + return dv.MsgPack, err +} + +func (e *testeph) bytesToState(ctx context.Context, raw []byte) (tfsdk.State, error) { + schema := e.schema() + dv := tfprotov6.DynamicValue{MsgPack: raw} + typ := schema.Type().TerraformType(ctx) + v, err := dv.Unmarshal(typ) + return tfsdk.State{Raw: v, Schema: schema}, err +} diff --git a/pkg/pf/tests/provider_epemeral_test.go b/pkg/pf/tests/provider_epemeral_test.go new file mode 100644 index 000000000..79fae83b6 --- /dev/null +++ b/pkg/pf/tests/provider_epemeral_test.go @@ -0,0 +1,56 @@ +// Copyright 2025-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 tfbridgetests + +import ( + "os" + "path/filepath" + "testing" + + pulumirpc "github.com/pulumi/pulumi/sdk/v3/proto/go" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/tests/internal/testprovider" +) + +func TestEphemeralInvokeClose(t *testing.T) { + t.Parallel() + server, err := newProviderServer(t, testprovider.SyntheticTestBridgeProvider()) + require.NoError(t, err) + + properties, err := structpb.NewStruct(map[string]any{ + "statedir": "/tmp", + }) + require.NoError(t, err) + resp, err := server.Invoke(t.Context(), &pulumirpc.InvokeRequest{ + Tok: "testbridge:index/ephemeral:Testeph", + Args: properties, + }) + require.NoError(t, err) + + id := resp.Return.Fields["id"].GetStringValue() + + path := filepath.Join("/tmp", id+".bin") + data, err := os.ReadFile(path) + require.NoError(t, err) + require.Contains(t, string(data), "\xa8statedir\xa4/tmp") + + _, err = server.Cancel(t.Context(), &emptypb.Empty{}) + require.NoError(t, err) + _, err = os.Stat(path) + require.True(t, os.IsNotExist(err)) +} diff --git a/pkg/pf/tfbridge/naming.go b/pkg/pf/tfbridge/naming.go index 3b0005439..37ed3ebd0 100644 --- a/pkg/pf/tfbridge/naming.go +++ b/pkg/pf/tfbridge/naming.go @@ -37,3 +37,20 @@ func functionPropertyKey(ds datasourceHandle, path *tftypes.AttributePath) (reso return "", false } } + +func ephemeralFunctionPropertyKey(eh ephemeralResourceHandle, path *tftypes.AttributePath) (resource.PropertyKey, bool) { + if path == nil { + return "", false + } + if len(path.Steps()) != 1 { + return "", false + } + switch attrName := path.LastStep().(type) { + case tftypes.AttributeName: + pulumiName := tfbridge.TerraformToPulumiNameV2(string(attrName), + eh.schemaOnlyShim.Schema(), eh.pulumiEphemeralResourceInfo.GetFields()) + return resource.PropertyKey(pulumiName), true + default: + return "", false + } +} diff --git a/pkg/pf/tfbridge/provider.go b/pkg/pf/tfbridge/provider.go index 6ae562ff6..6a4aff0bb 100644 --- a/pkg/pf/tfbridge/provider.go +++ b/pkg/pf/tfbridge/provider.go @@ -21,6 +21,8 @@ import ( "fmt" "os" "strings" + "sync" + "time" "github.com/blang/semver" pfprovider "github.com/hashicorp/terraform-plugin-framework/provider" @@ -74,22 +76,29 @@ func getProviderOptions(opts []providerOption) (providerOptions, error) { return res, nil } +type ephemeral struct { + typeName string + renewAt time.Time + private []byte +} + // Provider implements the Pulumi resource provider operations for any // Terraform plugin built with Terraform Plugin Framework. // // https://www.terraform.io/plugin/framework type provider struct { - tfServer tfprotov6.ProviderServer - info tfbridge.ProviderInfo - resources runtypes.Resources - datasources runtypes.DataSources - pulumiSchema func(context.Context, plugin.GetSchemaRequest) ([]byte, error) - encoding convert.Encoding - diagSink diag.Sink - configEncoder convert.Encoder - configType tftypes.Object - version semver.Version - logSink logging.Sink + tfServer tfprotov6.ProviderServer + info tfbridge.ProviderInfo + resources runtypes.Resources + datasources runtypes.DataSources + ephemeralResources runtypes.EphemeralResources + pulumiSchema func(context.Context, plugin.GetSchemaRequest) ([]byte, error) + encoding convert.Encoding + diagSink diag.Sink + configEncoder convert.Encoder + configType tftypes.Object + version semver.Version + logSink logging.Sink parameterize func(context.Context, plugin.ParameterizeRequest) (plugin.ParameterizeResponse, error) @@ -99,6 +108,10 @@ type provider struct { schemaOnlyProvider shim.Provider providerOpts []providerOption + + // ephemeralState holds any provider-specific state that needs propagating across operations. + ephemeralState []ephemeral + ephemeralStateLock sync.Mutex } var _ pl.ProviderWithContext = &provider{} @@ -150,6 +163,10 @@ func newProviderWithContext(ctx context.Context, info tfbridge.ProviderInfo, if err != nil { return nil, err } + ephemeralResources, err := pfServer.EphemeralResources(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 +208,7 @@ func newProviderWithContext(ctx context.Context, info tfbridge.ProviderInfo, info: info, resources: resources, datasources: datasources, + ephemeralResources: ephemeralResources, pulumiSchema: schema, encoding: enc, configEncoder: configEncoder, @@ -298,6 +316,21 @@ func (p *provider) GetPluginInfoWithContext(_ context.Context) (workspace.Plugin // initialization error. SignalCancellation is advisory and non-blocking; it is up to the host to decide how long to // wait after SignalCancellation is called before (e.g.) hard-closing any gRPC connection. func (p *provider) SignalCancellationWithContext(_ context.Context) error { + // Close any ephemeral resources. + p.ephemeralStateLock.Lock() + defer p.ephemeralStateLock.Unlock() + + for _, e := range p.ephemeralState { + req := &tfprotov6.CloseEphemeralResourceRequest{ + TypeName: e.typeName, + Private: e.private, + } + _, err := p.tfServer.CloseEphemeralResource(context.Background(), req) + if err != nil { + p.logSink.Log(context.Background(), diag.Warning, "", fmt.Sprintf("error closing ephemeral resource: %v", err)) + } + } + // Some improvements are possible here to gracefully shut down. return nil } @@ -320,6 +353,15 @@ func (p *provider) terraformDatasourceNameOrRenamedEntity(functionToken tokens.M return "", fmt.Errorf("[pf/tfbridge] unknown datasource token: %v", functionToken) } +func (p *provider) terraformEphemeralResourceNameOrRenamedEntity(resourceToken tokens.ModuleMember) (string, bool) { + for tfname, v := range p.info.EphemeralResources { + if v.Tok == resourceToken { + return tfname, true + } + } + return "", false +} + func (p *provider) returnTerraformConfig() (resource.PropertyMap, error) { // Get the current configuration config, err := convert.EncodePropertyMapToDynamic(p.configEncoder, p.configType, p.lastKnownProviderConfig) diff --git a/pkg/pf/tfbridge/provider_ephemeral_resources.go b/pkg/pf/tfbridge/provider_ephemeral_resources.go new file mode 100644 index 000000000..ad6b14403 --- /dev/null +++ b/pkg/pf/tfbridge/provider_ephemeral_resources.go @@ -0,0 +1,82 @@ +// 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 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" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" +) + +type ephemeralResourceHandle struct { + token tokens.ModuleMember + terraformEphemeralResourceName string + schema runtypes.Schema + pulumiEphemeralResourceInfo *tfbridge.EphemeralResourceInfo // optional + encoder convert.Encoder + decoder convert.Decoder + schemaOnlyShim shim.Resource +} + +func (p *provider) ephemeralResourceHandle( + ctx context.Context, tok tokens.ModuleMember, +) (ephemeralResourceHandle, bool, error) { + typeOrRenamedEntityName, has := p.terraformEphemeralResourceNameOrRenamedEntity(tok) + if !has { + return ephemeralResourceHandle{}, false, nil + } + + schema := p.ephemeralResources.Schema(runtypes.TypeOrRenamedEntityName(typeOrRenamedEntityName)) + + result := ephemeralResourceHandle{ + terraformEphemeralResourceName: string(schema.TFName()), + schema: schema, + } + + if info, ok := p.info.EphemeralResources[typeOrRenamedEntityName]; ok { + result.pulumiEphemeralResourceInfo = info + } + + token := result.pulumiEphemeralResourceInfo.Tok + if token == "" { + return ephemeralResourceHandle{}, true, fmt.Errorf("Tok cannot be empty: %s", token) + } + + objectType := result.schema.Type(ctx).(tftypes.Object) + + encoder, err := p.encoding.NewEphemeralResourceEncoder(typeOrRenamedEntityName, objectType) + if err != nil { + return ephemeralResourceHandle{}, true, fmt.Errorf("Failed to prepare an ephemeral resource encoder: %s", err) + } + + outputsDecoder, err := p.encoding.NewEphemeralResourceDecoder(typeOrRenamedEntityName, objectType) + if err != nil { + return ephemeralResourceHandle{}, true, fmt.Errorf("Failed to prepare an ephemeral resource decoder: %s", err) + } + + result.encoder = encoder + result.decoder = outputsDecoder + result.token = token + + result.schemaOnlyShim, _ = p.schemaOnlyProvider.EphemeralResourcesMap().GetOk(typeOrRenamedEntityName) + return result, true, nil +} diff --git a/pkg/pf/tfbridge/provider_invoke.go b/pkg/pf/tfbridge/provider_invoke.go index 01257be7c..6dd510cfb 100644 --- a/pkg/pf/tfbridge/provider_invoke.go +++ b/pkg/pf/tfbridge/provider_invoke.go @@ -39,6 +39,69 @@ func (p *provider) InvokeWithContext( ) (resource.PropertyMap, []plugin.CheckFailure, error) { ctx = p.initLogging(ctx, p.logSink, "") + eh, has, err := p.ephemeralResourceHandle(ctx, tok) + if err != nil { + return nil, nil, err + } + if has { + typ := eh.schema.Type(ctx).(tftypes.Object) + + // Transform checkedInputs to apply Pulumi-level defaults. + news := defaults.ApplyDefaultInfoValues(ctx, defaults.ApplyDefaultInfoValuesArgs{ + SchemaMap: eh.schemaOnlyShim.Schema(), + SchemaInfos: eh.pulumiEphemeralResourceInfo.Fields, + PropertyMap: args, + ProviderConfig: p.lastKnownProviderConfig, + }) + + config, err := convert.EncodePropertyMapToDynamic(eh.encoder, typ, news) + if err != nil { + return nil, nil, fmt.Errorf("cannot encode config to call ReadDataSource for %q: %w", + eh.terraformEphemeralResourceName, err) + } + + if failures, err := p.validateEphemeralResourceConfig(ctx, eh, config); err != nil || len(failures) > 0 { + return nil, failures, err + } + + req := &tfprotov6.OpenEphemeralResourceRequest{ + Config: config, + TypeName: eh.terraformEphemeralResourceName, + } + + resp, err := p.tfServer.OpenEphemeralResource(ctx, req) + if err != nil { + return nil, nil, fmt.Errorf("error calling ReadDataSource: %w", err) + } + + failures, err := p.processEphemeralInvokeDiagnostics(eh, resp.Diagnostics) + if err != nil || len(failures) > 0 { + return nil, failures, err + } + + propertyMap, err := convert.DecodePropertyMapFromDynamic(ctx, eh.decoder, typ, resp.Result) + if err != nil { + return nil, nil, fmt.Errorf("cannot decode state from a call to ReadDataSource for %q: %w", + eh.terraformEphemeralResourceName, err) + } + + // All ephemeral resource properties are treated as secrets by pulumi. + for k, v := range propertyMap { + propertyMap[k] = resource.MakeSecret(v) + } + + // Keep track of this ephemeral resource so that it can be closed later. + p.ephemeralStateLock.Lock() + p.ephemeralState = append(p.ephemeralState, ephemeral{ + typeName: eh.terraformEphemeralResourceName, + renewAt: resp.RenewAt, + private: resp.Private, + }) + p.ephemeralStateLock.Unlock() + + return propertyMap, nil, nil + } + handle, err := p.datasourceHandle(ctx, tok) if err != nil { return nil, nil, err @@ -67,18 +130,49 @@ func (p *provider) InvokeWithContext( return p.readDataSource(ctx, handle, config) } -func (p *provider) validateDataResourceConfig(ctx context.Context, handle datasourceHandle, +func (p *provider) validateEphemeralResourceConfig(ctx context.Context, handle ephemeralResourceHandle, config *tfprotov6.DynamicValue, ) ([]plugin.CheckFailure, error) { - req := &tfprotov6.ValidateDataResourceConfigRequest{ - TypeName: handle.terraformDataSourceName, + req := &tfprotov6.ValidateEphemeralResourceConfigRequest{ + TypeName: handle.terraformEphemeralResourceName, Config: config, } - resp, err := p.tfServer.ValidateDataResourceConfig(ctx, req) + resp, err := p.tfServer.ValidateEphemeralResourceConfig(ctx, req) if err != nil { - return nil, fmt.Errorf("error calling ValidateDataResourceConfig: %w", err) + return nil, fmt.Errorf("error calling ValidateEphemeralResourceConfig: %w", err) } - return p.processInvokeDiagnostics(handle, resp.Diagnostics) + return p.processEphemeralInvokeDiagnostics(handle, resp.Diagnostics) +} + +func (p *provider) processEphemeralInvokeDiagnostics(eh ephemeralResourceHandle, + diags []*tfprotov6.Diagnostic, +) ([]plugin.CheckFailure, error) { + failures, rest := p.parseEphemeralInvokePropertyCheckFailures(eh, diags) + return failures, p.processDiagnostics(rest) +} + +// Some of the diagnostics pertain to an individual property and should be returned as plugin.CheckFailure for an +// optimal rendering by Pulumi CLI. +func (p *provider) parseEphemeralInvokePropertyCheckFailures(eh ephemeralResourceHandle, diags []*tfprotov6.Diagnostic) ( + []plugin.CheckFailure, []*tfprotov6.Diagnostic, +) { + rest := []*tfprotov6.Diagnostic{} + failures := []plugin.CheckFailure{} + + for _, d := range diags { + if pk, ok := ephemeralFunctionPropertyKey(eh, d.Attribute); ok { + reason := strings.Join([]string{d.Summary, d.Detail}, ": ") + failure := plugin.CheckFailure{ + Property: pk, + Reason: reason, + } + failures = append(failures, failure) + continue + } + rest = append(rest, d) + } + + return failures, rest } func (p *provider) readDataSource(ctx context.Context, handle datasourceHandle, @@ -126,6 +220,20 @@ func (p *provider) readDataSource(ctx context.Context, handle datasourceHandle, return propertyMap, nil, nil } +func (p *provider) validateDataResourceConfig(ctx context.Context, handle datasourceHandle, + config *tfprotov6.DynamicValue, +) ([]plugin.CheckFailure, error) { + req := &tfprotov6.ValidateDataResourceConfigRequest{ + TypeName: handle.terraformDataSourceName, + Config: config, + } + resp, err := p.tfServer.ValidateDataResourceConfig(ctx, req) + if err != nil { + return nil, fmt.Errorf("error calling ValidateDataResourceConfig: %w", err) + } + return p.processInvokeDiagnostics(handle, resp.Diagnostics) +} + func (p *provider) processInvokeDiagnostics(ds datasourceHandle, diags []*tfprotov6.Diagnostic, ) ([]plugin.CheckFailure, error) { diff --git a/pkg/tfbridge/info.go b/pkg/tfbridge/info.go index 3db4fad5c..8d4952f6c 100644 --- a/pkg/tfbridge/info.go +++ b/pkg/tfbridge/info.go @@ -121,6 +121,8 @@ type AliasInfo = info.Alias // ResourceOrDataSourceInfo is a shared interface to ResourceInfo and DataSourceInfo mappings type ResourceOrDataSourceInfo = info.ResourceOrDataSource +type EphemeralResourceInfo = info.EphemeralResource + // ResourceInfo is a top-level type exported by a provider. This structure can override the type to generate. It can // also give custom metadata for fields, using the SchemaInfo structure below. Finally, a set of composite keys can be // given; this is used when Terraform needs more than just the ID to uniquely identify and query for a resource. diff --git a/pkg/tfbridge/info/info.go b/pkg/tfbridge/info/info.go index 1d6b4c1de..a3df1765f 100644 --- a/pkg/tfbridge/info/info.go +++ b/pkg/tfbridge/info/info.go @@ -58,24 +58,25 @@ type Provider struct { // // require github.com/my-company/terraform-repo-example v1.0.0 // replace github.com/my-company/terraform-repo-example => github.com/some-fork/terraform-repo-example v1.0.0 - GitHubOrg string // the GitHub org of the provider. Defaults to `terraform-providers`. - GitHubHost string // the GitHub host for the provider. Defaults to `github.com`. - Description string // an optional descriptive overview of the package (a default supplied). - Keywords []string // an optional list of keywords to help discovery of this package. e.g. "category/cloud, category/infrastructure" - License string // the license, if any, the resulting package has (default is none). - LogoURL string // an optional URL to the logo of the package - DisplayName string // the human friendly name of the package used in the Pulumi registry - Publisher string // the name of the person or organization that authored and published the package. - Homepage string // the URL to the project homepage. - Repository string // the URL to the project source code repository. - Version string // the version of the provider package. - 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. - 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. - ExtraFunctions map[string]pschema.FunctionSpec // a map of Pulumi token to schema type for extra functions. + GitHubOrg string // the GitHub org of the provider. Defaults to `terraform-providers`. + GitHubHost string // the GitHub host for the provider. Defaults to `github.com`. + Description string // an optional descriptive overview of the package (a default supplied). + Keywords []string // an optional list of keywords to help discovery of this package. e.g. "category/cloud, category/infrastructure" + License string // the license, if any, the resulting package has (default is none). + LogoURL string // an optional URL to the logo of the package + DisplayName string // the human friendly name of the package used in the Pulumi registry + Publisher string // the name of the person or organization that authored and published the package. + Homepage string // the URL to the project homepage. + Repository string // the URL to the project source code repository. + Version string // the version of the provider package. + 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. + DataSources map[string]*DataSource // a map of TF type or renamed entity name to Pulumi resource info. + EphemeralResources map[string]*EphemeralResource // a map of TF type or renamed entity name to Pulumi ephemeral 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. + ExtraFunctions map[string]pschema.FunctionSpec // a map of Pulumi token to schema type for extra functions. // ExtraResourceHclExamples is a slice of additional HCL examples attached to resources which are converted to the // relevant target language(s) @@ -367,6 +368,32 @@ type ResourceOrDataSource interface { ReplaceExamplesSection() bool // whether we are replacing the upstream TF examples generation } +// EphemeralResource is a top-level type exported by a provider. +type EphemeralResource struct { + Tok tokens.ModuleMember // a type token to override the default; "" uses the default. + Fields map[string]*Schema // a map of custom field names; if a type is missing, uses the default. + Docs *Doc // overrides for finding and mapping TF docs. +} + +// GetTok returns a resource type token +func (info *EphemeralResource) GetTok() tokens.Token { return tokens.Token(info.Tok) } + +// GetFields returns information about the resource's custom fields +func (info *EphemeralResource) GetFields() map[string]*Schema { + if info == nil { + return nil + } + return info.Fields +} + +// GetDocs returns a resource docs override from the Pulumi provider +func (info *EphemeralResource) GetDocs() *Doc { return info.Docs } + +// ReplaceExamplesSection returns whether to replace the upstream examples with our own source +func (info *EphemeralResource) ReplaceExamplesSection() bool { + return info.Docs != nil && info.Docs.ReplaceExamplesSection +} + // Resource is a top-level type exported by a provider. This structure can override the type to generate. It can // also give custom metadata for fields, using the SchemaInfo structure below. Finally, a set of composite keys can be // given; this is used when Terraform needs more than just the ID to uniquely identify and query for a resource. diff --git a/pkg/tfshim/schema/provider.go b/pkg/tfshim/schema/provider.go index 331583070..35f8657fe 100644 --- a/pkg/tfshim/schema/provider.go +++ b/pkg/tfshim/schema/provider.go @@ -49,6 +49,10 @@ func (s ProviderShim) DataSourcesMap() shim.ResourceMap { return s.V.DataSourcesMap } +func (s ProviderShim) EphemeralResourcesMap() shim.ResourceMap { + return ResourceMap{} +} + 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..2667a0d11 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) EphemeralResourcesMap() 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..add29370b 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) EphemeralResourcesMap() 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..f7c2738d4 100644 --- a/pkg/tfshim/shim.go +++ b/pkg/tfshim/shim.go @@ -300,6 +300,7 @@ type Provider interface { Schema() SchemaMap ResourcesMap() ResourceMap DataSourcesMap() ResourceMap + EphemeralResourcesMap() 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..52263a3ce 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 + EphemeralResourceFilter 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) EphemeralResourcesMap() shim.ResourceMap { + return &filteringMap{p.Provider.EphemeralResourcesMap(), p.EphemeralResourceFilter} +} + 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..914724fa6 100644 --- a/pkg/tfshim/util/util.go +++ b/pkg/tfshim/util/util.go @@ -31,6 +31,9 @@ type UnimplementedProvider struct { func (UnimplementedProvider) Schema() shim.SchemaMap { panic("unimplemented") } func (UnimplementedProvider) ResourcesMap() shim.ResourceMap { panic("unimplemented") } func (UnimplementedProvider) DataSourcesMap() shim.ResourceMap { panic("unimplemented") } +func (UnimplementedProvider) EphemeralResourcesMap() shim.ResourceMap { + panic("unimplemented") +} func (UnimplementedProvider) InternalValidate() error { panic("unimplemented") }