From 66142e9e6b9dfd0d0855f248a2e2be9ab0e0c104 Mon Sep 17 00:00:00 2001 From: Fraser Waters Date: Tue, 28 Oct 2025 10:31:41 +0100 Subject: [PATCH 1/2] Update to terrafrom-plugin-framework 1.16.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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= From 89307154a9d7d14174a6caeb56eaed0ae4f70d7e Mon Sep 17 00:00:00 2001 From: Fraser Waters Date: Tue, 28 Oct 2025 11:24:18 +0100 Subject: [PATCH 2/2] Add actions to the tfbridge --- pkg/convert/convert.go | 1 + pkg/convert/encoding.go | 11 + pkg/convert/schema_context.go | 17 +- pkg/pf/internal/pfutils/actions.go | 74 ++++++ pkg/pf/internal/pfutils/attr.go | 5 + pkg/pf/internal/pfutils/block.go | 5 + pkg/pf/internal/pfutils/schema.go | 7 + pkg/pf/internal/runtypes/types.go | 6 + pkg/pf/internal/schemashim/action.go | 52 ++++ pkg/pf/internal/schemashim/actions_map.go | 87 +++++++ pkg/pf/internal/schemashim/provider.go | 9 + pkg/pf/internal/schemashim/schemashim.go | 6 + pkg/pf/proto/action.go | 86 +++++++ pkg/pf/proto/protov6.go | 9 + pkg/pf/proto/runtypes.go | 36 +++ pkg/pf/provider.go | 1 + .../tests/internal/testprovider/testbridge.go | 14 +- .../testprovider/testbridge_action_print.go | 81 +++++++ pkg/pf/tests/provider_action_test.go | 66 ++++++ pkg/pf/tests/schema_test.go | 3 + pkg/pf/tfbridge/naming.go | 17 ++ pkg/pf/tfbridge/provider.go | 21 +- pkg/pf/tfbridge/provider_actions.go | 68 ++++++ pkg/pf/tfbridge/provider_datasources.go | 14 +- pkg/pf/tfbridge/provider_invoke.go | 184 +++++++++++++-- pkg/tfbridge/info.go | 2 + pkg/tfbridge/info/info.go | 28 +++ pkg/tfgen/docs.go | 6 + pkg/tfgen/docs_test.go | 4 + pkg/tfgen/generate.go | 222 ++++++++++++++++++ pkg/tfgen/generate_schema.go | 29 +++ pkg/tfgen/internal/paths/paths.go | 78 +++++- pkg/tfgen/source.go | 9 +- pkg/tfshim/schema/action.go | 65 +++++ pkg/tfshim/schema/provider.go | 8 + pkg/tfshim/sdk-v1/action.go | 58 +++++ pkg/tfshim/sdk-v1/provider.go | 4 + pkg/tfshim/sdk-v2/action.go | 58 +++++ pkg/tfshim/sdk-v2/provider.go | 4 + pkg/tfshim/shim.go | 19 ++ pkg/tfshim/util/filter.go | 42 ++++ pkg/tfshim/util/util.go | 1 + 42 files changed, 1487 insertions(+), 30 deletions(-) create mode 100644 pkg/pf/internal/pfutils/actions.go create mode 100644 pkg/pf/internal/schemashim/action.go create mode 100644 pkg/pf/internal/schemashim/actions_map.go create mode 100644 pkg/pf/proto/action.go create mode 100644 pkg/pf/tests/internal/testprovider/testbridge_action_print.go create mode 100644 pkg/pf/tests/provider_action_test.go create mode 100644 pkg/pf/tfbridge/provider_actions.go create mode 100644 pkg/tfshim/schema/action.go create mode 100644 pkg/tfshim/sdk-v1/action.go create mode 100644 pkg/tfshim/sdk-v2/action.go diff --git a/pkg/convert/convert.go b/pkg/convert/convert.go index b43b5bc9b..f325825b1 100644 --- a/pkg/convert/convert.go +++ b/pkg/convert/convert.go @@ -41,6 +41,7 @@ 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) + NewActionEncoder(action string, actionType 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..71dbb8835 100644 --- a/pkg/convert/encoding.go +++ b/pkg/convert/encoding.go @@ -89,6 +89,17 @@ func (e *encoding) NewDataSourceDecoder( return dec, nil } +func (e *encoding) NewActionEncoder( + dataSource string, objectType tftypes.Object, +) (Encoder, error) { + mctx := newActionSchemaMapContext(dataSource, 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 action %q: %w", dataSource, err) + } + return enc, 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..c4250c488 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 newActionSchemaMapContext( + action string, + schemaOnlyProvider shim.Provider, + providerInfo *tfbridge.ProviderInfo, +) *schemaMapContext { + r := schemaOnlyProvider.ActionsMap().Get(action) + contract.Assertf(r != nil, "no action %q found in ActionsMap", action) + sm := r.Schema() + var fields map[string]*tfbridge.SchemaInfo + if providerInfo != nil { + fields = providerInfo.Actions[action].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/actions.go b/pkg/pf/internal/pfutils/actions.go new file mode 100644 index 000000000..b362f301d --- /dev/null +++ b/pkg/pf/internal/pfutils/actions.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/action" + "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 GatherActions[F func(Schema) shim.SchemaMap]( + ctx context.Context, prov provider.Provider, f F, +) (runtypes.Actions, error) { + provMetadata := queryProviderMetadata(ctx, prov) + ds := make(collection[func() action.Action]) + + aprov, is := prov.(provider.ProviderWithActions) + if is { + for _, makeAction := range aprov.Actions(ctx) { + act := makeAction() + + meta := action.MetadataResponse{} + act.Metadata(ctx, action.MetadataRequest{ + ProviderTypeName: provMetadata.TypeName, + }, &meta) + + schemaResponse := &action.SchemaResponse{} + act.Schema(ctx, action.SchemaRequest{}, schemaResponse) + + actionSchema := schemaResponse.Schema + diag := schemaResponse.Diagnostics + if err := checkDiagsForErrors(diag); err != nil { + return nil, fmt.Errorf("Action %s GetSchema() error: %w", meta.TypeName, err) + } + + ds[runtypes.TypeOrRenamedEntityName(meta.TypeName)] = entry[func() action.Action]{ + t: makeAction, + schema: FromActionSchema(actionSchema), + tfName: runtypes.TypeName(meta.TypeName), + } + } + } + + return &actions{collection: ds, convert: f}, nil +} + +type actions struct { + collection[func() action.Action] + convert func(Schema) shim.SchemaMap +} + +func (r actions) Schema(t runtypes.TypeOrRenamedEntityName) runtypes.Schema { + entry := r.collection[t] + return runtypesSchemaAdapter{entry.schema, r.convert, entry.tfName} +} + +func (actions) IsActions() {} diff --git a/pkg/pf/internal/pfutils/attr.go b/pkg/pf/internal/pfutils/attr.go index ef405d09e..4b5b4c2a3 100644 --- a/pkg/pf/internal/pfutils/attr.go +++ b/pkg/pf/internal/pfutils/attr.go @@ -17,6 +17,7 @@ package pfutils import ( "fmt" + aschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/attr" dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" prschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -65,6 +66,10 @@ func FromResourceAttribute(x rschema.Attribute) Attr { return FromAttrLike(x) } +func FromActionAttribute(x aschema.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..e32e3c121 100644 --- a/pkg/pf/internal/pfutils/block.go +++ b/pkg/pf/internal/pfutils/block.go @@ -19,6 +19,7 @@ import ( "regexp" "strconv" + aschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/attr" dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" prschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -58,6 +59,10 @@ func FromResourceBlock(x rschema.Block) Block { return FromBlockLike(x) } +func FromActionBlock(x aschema.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/schema.go b/pkg/pf/internal/pfutils/schema.go index b5b618797..9cb0bfa44 100644 --- a/pkg/pf/internal/pfutils/schema.go +++ b/pkg/pf/internal/pfutils/schema.go @@ -17,6 +17,7 @@ package pfutils import ( "context" + aschema "github.com/hashicorp/terraform-plugin-framework/action/schema" "github.com/hashicorp/terraform-plugin-framework/attr" dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" prschema "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -66,6 +67,12 @@ func FromResourceSchema(x rschema.Schema) Schema { return newSchemaAdapter(x, x.Type(), x.DeprecationMessage, attrs, blocks, &x) } +func FromActionSchema(x aschema.Schema) Schema { + attrs := convertMap(FromActionAttribute, x.Attributes) + blocks := convertMap(FromActionBlock, 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..26c7a273a 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 actions pre-indexed by TypeOrRenamedEntityName. +type Actions interface { + collection + IsActions() +} diff --git a/pkg/pf/internal/schemashim/action.go b/pkg/pf/internal/schemashim/action.go new file mode 100644 index 000000000..75d2c097f --- /dev/null +++ b/pkg/pf/internal/schemashim/action.go @@ -0,0 +1,52 @@ +// 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/resource" + "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 schemaOnlyAction struct { + tf runtypes.Schema + internalinter.Internal +} + +var _ shim.Action = (*schemaOnlyAction)(nil) + +func (r *schemaOnlyAction) SchemaType() valueshim.Type { + protoSchema, err := r.tf.ResourceProtoSchema(context.Background()) + contract.AssertNoErrorf(err, "ResourceProtoSchema failed") + return valueshim.FromTType(protoSchema.ValueType()) +} + +func (r *schemaOnlyAction) Schema() shim.SchemaMap { + return r.tf.Shim() +} + +func (r *schemaOnlyAction) Metadata() string { + panic("schemaOnlyAction does not implement runtime operation Metadata") +} + +func (r *schemaOnlyAction) Invoke(context.Context, resource.PropertyMap) (resource.PropertyMap, error) { + panic("schemaOnlyAction does not implement runtime operation Invoke") +} diff --git a/pkg/pf/internal/schemashim/actions_map.go b/pkg/pf/internal/schemashim/actions_map.go new file mode 100644 index 000000000..e0a455a35 --- /dev/null +++ b/pkg/pf/internal/schemashim/actions_map.go @@ -0,0 +1,87 @@ +// 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" +) + +// Data Source map needs to support Set (mutability) for RenameAction. +func newSchemaOnlyActionMap(actions runtypes.Actions) schemaOnlyActionMap { + m := schemaOnlyActionMap{} + for _, name := range actions.All() { + key := string(name) + v := actions.Schema(name) + m[key] = &schemaOnlyAction{v, internalinter.Internal{}} + } + return m +} + +type schemaOnlyActionMap map[string]*schemaOnlyAction + +var ( + _ shim.ActionMap = schemaOnlyActionMap{} + _ runtypes.Actions = schemaOnlyActionMap{} +) + +func (m schemaOnlyActionMap) Len() int { + return len(m) +} + +func (m schemaOnlyActionMap) Get(key string) shim.Action { + return m[key] +} + +func (m schemaOnlyActionMap) GetOk(key string) (shim.Action, bool) { + v, ok := m[key] + return v, ok +} + +func (m schemaOnlyActionMap) Range(each func(key string, value shim.Action) bool) { + for k, v := range m { + if !each(k, v) { + return + } + } +} + +func (m schemaOnlyActionMap) Set(key string, value shim.Action) { + v, ok := value.(*schemaOnlyAction) + contract.Assertf(ok, "Set must be a %T, found a %T", v, value) + m[key] = v +} + +func (m schemaOnlyActionMap) All() []runtypes.TypeOrRenamedEntityName { + arr := make([]runtypes.TypeOrRenamedEntityName, 0, len(m)) + for k := range m { + arr = append(arr, runtypes.TypeOrRenamedEntityName(k)) + } + return arr +} + +func (m schemaOnlyActionMap) Has(key runtypes.TypeOrRenamedEntityName) bool { + _, ok := m[string(key)] + return ok +} + +func (m schemaOnlyActionMap) Schema(key runtypes.TypeOrRenamedEntityName) runtypes.Schema { + return m[string(key)].tf +} + +func (m schemaOnlyActionMap) IsActions() {} diff --git a/pkg/pf/internal/schemashim/provider.go b/pkg/pf/internal/schemashim/provider.go index f12ec9b83..833965274 100644 --- a/pkg/pf/internal/schemashim/provider.go +++ b/pkg/pf/internal/schemashim/provider.go @@ -37,6 +37,7 @@ type SchemaOnlyProvider struct { tf pfprovider.Provider resourceMap schemaOnlyResourceMap dataSourceMap schemaOnlyDataSourceMap + actionsMap schemaOnlyActionMap internalinter.Internal } @@ -62,6 +63,10 @@ func (p *SchemaOnlyProvider) DataSources(ctx context.Context) (runtypes.DataSour return p.dataSourceMap, nil } +func (p *SchemaOnlyProvider) Actions(ctx context.Context) (runtypes.Actions, error) { + return p.actionsMap, 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) ActionsMap() shim.ActionMap { + return p.actionsMap +} + 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..f021f0e4f 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) } + actions, err := pfutils.GatherActions(ctx, provider, NewSchemaMap) + if err != nil { + panic(err) + } resourceMap := newSchemaOnlyResourceMap(resources) dataSourceMap := newSchemaOnlyDataSourceMap(dataSources) + actionsMap := newSchemaOnlyActionMap(actions) return &SchemaOnlyProvider{ ctx: ctx, tf: provider, resourceMap: resourceMap, dataSourceMap: dataSourceMap, + actionsMap: actionsMap, } } diff --git a/pkg/pf/proto/action.go b/pkg/pf/proto/action.go new file mode 100644 index 000000000..655199021 --- /dev/null +++ b/pkg/pf/proto/action.go @@ -0,0 +1,86 @@ +// Copyright 2016-2024, 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 proto + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + cres "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" +) + +var ( + _ = shim.ActionMap(actionMap{}) + _ = shim.Action(action{}) +) + +type actionMap map[string]*tfprotov6.ActionSchema + +func (m actionMap) Len() int { + return len(m) +} + +func (m actionMap) Get(key string) shim.Action { + v, ok := m.GetOk(key) + contract.Assertf(ok, "unknown key %q", key) + return v +} + +func (m actionMap) GetOk(key string) (shim.Action, bool) { + v, ok := m[key] + if !ok { + return nil, false + } + return newAction(v), true +} + +func (m actionMap) Range(each func(key string, value shim.Action) bool) { + for k, v := range m { + if !each(k, newAction(v)) { + return + } + } +} + +func (m actionMap) Set(key string, value shim.Action) { + v, ok := value.(action) + contract.Assertf(ok, "Set must be a %T, found a %T", v, value) + m[key] = v.r +} + +type action struct { + r *tfprotov6.ActionSchema + internalinter.Internal +} + +func newAction(r *tfprotov6.ActionSchema) *action { + return &action{r, internalinter.Internal{}} +} + +func (r action) Schema() shim.SchemaMap { + return blockMap{r.r.Schema.Block} +} + +func (r action) Metadata() string { + return "" +} + +func (r action) Invoke(ctx context.Context, inputs cres.PropertyMap) (cres.PropertyMap, error) { + return nil, nil +} diff --git a/pkg/pf/proto/protov6.go b/pkg/pf/proto/protov6.go index dd882b005..86e454983 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) ActionsMap() shim.ActionMap { + v, err := p.getSchema() + if err != nil { + tfbridge.GetLogger(p.ctx).Error(err.Error()) + return nil + } + return actionMap(v.ActionSchemas) +} diff --git a/pkg/pf/proto/runtypes.go b/pkg/pf/proto/runtypes.go index 18b271c90..b0c828b3c 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) Actions(context.Context) (runtypes.Actions, error) { + v, err := p.getSchema() + if err != nil { + return nil, err + } + return actions{actionCollection(v.ActionSchemas)}, nil +} + type schema struct { s *tfprotov6.Schema tfName runtypes.TypeName @@ -106,3 +114,31 @@ func (c collection) Schema(key runtypes.TypeOrRenamedEntityName) runtypes.Schema return schema{s, runtypes.TypeName(key)} } + +type actions struct{ actionCollection } + +var _ runtypes.Actions = actions{} + +func (actions) IsActions() {} + +type actionCollection map[string]*tfprotov6.ActionSchema + +func (c actionCollection) All() []runtypes.TypeOrRenamedEntityName { + arr := make([]runtypes.TypeOrRenamedEntityName, 0, len(c)) + for k := range c { + arr = append(arr, runtypes.TypeOrRenamedEntityName(k)) + } + return arr +} + +func (c actionCollection) Has(key runtypes.TypeOrRenamedEntityName) bool { + _, ok := c[string(key)] + return ok +} + +func (c actionCollection) Schema(key runtypes.TypeOrRenamedEntityName) runtypes.Schema { + s, ok := c[string(key)] + contract.Assertf(ok, "called Schema on an action that does not exist") + + return schema{s.Schema, runtypes.TypeName(key)} +} diff --git a/pkg/pf/provider.go b/pkg/pf/provider.go index 8356ca411..ccb1ebbec 100644 --- a/pkg/pf/provider.go +++ b/pkg/pf/provider.go @@ -35,4 +35,5 @@ type ShimProvider interface { Resources(context.Context) (runtypes.Resources, error) DataSources(context.Context) (runtypes.DataSources, error) Config(context.Context) (tftypes.Object, error) + Actions(context.Context) (runtypes.Actions, error) } diff --git a/pkg/pf/tests/internal/testprovider/testbridge.go b/pkg/pf/tests/internal/testprovider/testbridge.go index 037725d1d..31ede7da3 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. @@ -20,6 +20,7 @@ import ( "fmt" "reflect" + "github.com/hashicorp/terraform-plugin-framework/action" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/path" @@ -129,6 +130,10 @@ func SyntheticTestBridgeProvider() tfbridge.ProviderInfo { "testbridge_smac_ds": {Tok: "testbridge:index/smac:SMAC"}, }, + Actions: map[string]*tfbridge.ActionInfo{ + "testbridge_print": {Tok: "testbridge:index/print:Print"}, + }, + MetadataInfo: tfbridge.NewProviderMetadata(testBridgeMetadata), } @@ -145,6 +150,7 @@ type resourceData struct { } var _ provider.Provider = (*syntheticProvider)(nil) +var _ provider.ProviderWithActions = (*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) Actions(context.Context) []func() action.Action { + return []func() action.Action{ + newPrintAction, + } +} diff --git a/pkg/pf/tests/internal/testprovider/testbridge_action_print.go b/pkg/pf/tests/internal/testprovider/testbridge_action_print.go new file mode 100644 index 000000000..269b3f9b6 --- /dev/null +++ b/pkg/pf/tests/internal/testprovider/testbridge_action_print.go @@ -0,0 +1,81 @@ +// 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 testprovider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/action" + "github.com/hashicorp/terraform-plugin-framework/action/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func newPrintAction() action.Action { + return &printAction{} +} + +type printAction struct{} + +func (*printAction) Metadata(ctx context.Context, req action.MetadataRequest, + resp *action.MetadataResponse, +) { + resp.TypeName = req.ProviderTypeName + "_print" +} + +func (*printAction) Schema(_ context.Context, _ action.SchemaRequest, resp *action.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Prints string N times", + Attributes: map[string]schema.Attribute{ + "text": schema.StringAttribute{ + Description: "String to print", + Required: true, + }, + "count": schema.Float64Attribute{ + Description: "Number of times to print the string", + Optional: true, + }, + }, + } +} + +func (a *printAction) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) { + var config printModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + if resp.Diagnostics.HasError() { + return + } + + text := config.Text.ValueString() + + count := 1.0 + if !config.Count.IsNull() { + count = config.Count.ValueFloat64() + } + + iCount := int(count) + + for i := 0; i < iCount; i++ { + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("%s", text), + }) + } +} + +type printModel struct { + Text types.String `tfsdk:"text"` + Count types.Float64 `tfsdk:"count"` +} diff --git a/pkg/pf/tests/provider_action_test.go b/pkg/pf/tests/provider_action_test.go new file mode 100644 index 000000000..f04f46ad5 --- /dev/null +++ b/pkg/pf/tests/provider_action_test.go @@ -0,0 +1,66 @@ +// 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 tfbridgetests + +import ( + "testing" + + testutils "github.com/pulumi/providertest/replay" + "github.com/stretchr/testify/require" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/pf/tests/internal/testprovider" +) + +func TestActionSchema(t *testing.T) { + t.Parallel() + server, err := newProviderServer(t, testprovider.SyntheticTestBridgeProvider()) + require.NoError(t, err) + testCase := ` + { + "method": "/pulumirpc.ResourceProvider/Invoke", + "request": { + "tok": "testbridge:index/print:Print", + "args": { + "urn": "urn:pulumi:st::pg::testprovider:index/res:Res::r", + "preview": true, + "text": "hello world", + "count": 3 + } + }, + "response": { + "return": {} + } + } + ` + testutils.Replay(t, server, testCase) + + testCase = ` + { + "method": "/pulumirpc.ResourceProvider/Invoke", + "request": { + "tok": "testbridge:index/print:Print", + "args": { + "urn": "urn:pulumi:st::pg::testprovider:index/res:Res::r", + "preview": false, + "text": "hello world" + } + }, + "response": { + "return": {} + } + } + ` + testutils.Replay(t, server, testCase) +} diff --git a/pkg/pf/tests/schema_test.go b/pkg/pf/tests/schema_test.go index b991ff548..bc85f5526 100644 --- a/pkg/pf/tests/schema_test.go +++ b/pkg/pf/tests/schema_test.go @@ -65,6 +65,9 @@ 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) + + printAction := spec.Functions["testbridge:index/print:Print"] + assert.Equal(t, "string", printAction.Inputs.Properties["text"].Type) }) } diff --git a/pkg/pf/tfbridge/naming.go b/pkg/pf/tfbridge/naming.go index 3b0005439..bd1967619 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 actionPropertyKey(action actionHandle, 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), + action.schemaOnlyShim.Schema(), action.pulumiActionInfo.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..9d5b2e398 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 + actions runtypes.Actions 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 } + actions, err := pfServer.Actions(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, + actions: actions, pulumiSchema: schema, encoding: enc, configEncoder: configEncoder, @@ -311,13 +317,22 @@ func (p *provider) terraformResourceNameOrRenamedEntity(resourceToken tokens.Typ return "", fmt.Errorf("[pf/tfbridge] unknown resource token: %v", resourceToken) } -func (p *provider) terraformDatasourceNameOrRenamedEntity(functionToken tokens.ModuleMember) (string, error) { +func (p *provider) terraformActionNameOrRenamedEntity(functionToken tokens.ModuleMember) (string, bool) { + for tfname, v := range p.info.Actions { + if v.Tok == functionToken { + return tfname, true + } + } + return "", false +} + +func (p *provider) terraformDatasourceNameOrRenamedEntity(functionToken tokens.ModuleMember) (string, bool) { for tfname, v := range p.info.DataSources { if v.Tok == functionToken { - return tfname, nil + return tfname, true } } - return "", fmt.Errorf("[pf/tfbridge] unknown datasource token: %v", functionToken) + return "", false } func (p *provider) returnTerraformConfig() (resource.PropertyMap, error) { diff --git a/pkg/pf/tfbridge/provider_actions.go b/pkg/pf/tfbridge/provider_actions.go new file mode 100644 index 000000000..dc92b48d9 --- /dev/null +++ b/pkg/pf/tfbridge/provider_actions.go @@ -0,0 +1,68 @@ +// 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 tfbridge + +import ( + "context" + + "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 actionHandle struct { + token tokens.ModuleMember + terraformActionName string + schema runtypes.Schema + encoder convert.Encoder + schemaOnlyShim shim.Action + pulumiActionInfo *tfbridge.ActionInfo // optional +} + +func (p *provider) actionHandle(ctx context.Context, token tokens.ModuleMember) (actionHandle, bool, error) { + actName, has := p.terraformActionNameOrRenamedEntity(token) + if !has { + return actionHandle{}, false, nil + } + + schema := p.actions.Schema(runtypes.TypeOrRenamedEntityName(actName)) + + typ := schema.Type(ctx).(tftypes.Object) + + encoder, err := p.encoding.NewActionEncoder(actName, typ) + if err != nil { + return actionHandle{}, true, err + } + + shim, _ := p.schemaOnlyProvider.ActionsMap().GetOk(actName) + + result := actionHandle{ + token: token, + terraformActionName: actName, + schema: schema, + encoder: encoder, + schemaOnlyShim: shim, + } + + if info, ok := p.info.Actions[actName]; ok { + result.pulumiActionInfo = info + } + + return result, true, nil +} diff --git a/pkg/pf/tfbridge/provider_datasources.go b/pkg/pf/tfbridge/provider_datasources.go index 5dae74dcb..db2d475d1 100644 --- a/pkg/pf/tfbridge/provider_datasources.go +++ b/pkg/pf/tfbridge/provider_datasources.go @@ -36,10 +36,10 @@ type datasourceHandle struct { pulumiDataSourceInfo *tfbridge.DataSourceInfo // optional } -func (p *provider) datasourceHandle(ctx context.Context, token tokens.ModuleMember) (datasourceHandle, error) { - dsName, err := p.terraformDatasourceNameOrRenamedEntity(token) - if err != nil { - return datasourceHandle{}, err +func (p *provider) datasourceHandle(ctx context.Context, token tokens.ModuleMember) (datasourceHandle, bool, error) { + dsName, has := p.terraformDatasourceNameOrRenamedEntity(token) + if !has { + return datasourceHandle{}, false, nil } schema := p.datasources.Schema(runtypes.TypeOrRenamedEntityName(dsName)) @@ -48,12 +48,12 @@ func (p *provider) datasourceHandle(ctx context.Context, token tokens.ModuleMemb encoder, err := p.encoding.NewDataSourceEncoder(dsName, typ) if err != nil { - return datasourceHandle{}, err + return datasourceHandle{}, true, err } decoder, err := p.encoding.NewDataSourceDecoder(dsName, typ) if err != nil { - return datasourceHandle{}, err + return datasourceHandle{}, true, err } shim, _ := p.schemaOnlyProvider.DataSourcesMap().GetOk(dsName) @@ -71,5 +71,5 @@ func (p *provider) datasourceHandle(ctx context.Context, token tokens.ModuleMemb result.pulumiDataSourceInfo = info } - return result, nil + return result, true, nil } diff --git a/pkg/pf/tfbridge/provider_invoke.go b/pkg/pf/tfbridge/provider_invoke.go index 01257be7c..71b8ac884 100644 --- a/pkg/pf/tfbridge/provider_invoke.go +++ b/pkg/pf/tfbridge/provider_invoke.go @@ -22,6 +22,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/pulumi/pulumi/sdk/v3/go/common/diag" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin" "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" @@ -39,32 +40,83 @@ func (p *provider) InvokeWithContext( ) (resource.PropertyMap, []plugin.CheckFailure, error) { ctx = p.initLogging(ctx, p.logSink, "") - handle, err := p.datasourceHandle(ctx, tok) + // First check if this is an action + actionHandle, isAction, err := p.actionHandle(ctx, tok) if err != nil { return nil, nil, err } + if isAction { + typ := actionHandle.schema.Type(ctx).(tftypes.Object) - typ := handle.schema.Type(ctx).(tftypes.Object) + urn, has := args["urn"] + if !has { + return nil, nil, fmt.Errorf("missing required argument 'urn' to Invoke for action %q", actionHandle.token) + } + if !urn.IsString() { + return nil, nil, fmt.Errorf("expected argument 'urn' to Invoke for action %q to be a string", actionHandle.token) + } + urnStr := resource.URN(urn.StringValue()) + delete(args, "urn") + + var previewFlag bool + if preview, has := args["preview"]; has { + if !preview.IsBool() { + return nil, nil, fmt.Errorf("expected argument 'preview' to Invoke for action %q to be a boolean", actionHandle.token) + } + previewFlag = preview.BoolValue() + delete(args, "preview") + } + + // Transform args to apply Pulumi-level defaults. + argsWithDefaults := defaults.ApplyDefaultInfoValues(ctx, defaults.ApplyDefaultInfoValuesArgs{ + SchemaMap: actionHandle.schemaOnlyShim.Schema(), + SchemaInfos: actionHandle.pulumiActionInfo.GetFields(), + PropertyMap: args, + ProviderConfig: p.lastKnownProviderConfig, + }) - // Transform args to apply Pulumi-level defaults. - argsWithDefaults := defaults.ApplyDefaultInfoValues(ctx, defaults.ApplyDefaultInfoValuesArgs{ - SchemaMap: handle.schemaOnlyShim.Schema(), - SchemaInfos: handle.pulumiDataSourceInfo.GetFields(), - PropertyMap: args, - ProviderConfig: p.lastKnownProviderConfig, - }) + config, err := convert.EncodePropertyMapToDynamic(actionHandle.encoder, typ, argsWithDefaults) + if err != nil { + return nil, nil, fmt.Errorf("cannot encode config to call ReadDataSource for %q: %w", + actionHandle.terraformActionName, err) + } + + if failures, err := p.validateActionConfig(ctx, actionHandle, config); err != nil || len(failures) > 0 { + return nil, failures, err + } - config, err := convert.EncodePropertyMapToDynamic(handle.encoder, typ, argsWithDefaults) + return p.invokeAction(ctx, actionHandle, urnStr, previewFlag, config) + } + + datasourceHandle, isDatasource, err := p.datasourceHandle(ctx, tok) if err != nil { - return nil, nil, fmt.Errorf("cannot encode config to call ReadDataSource for %q: %w", - handle.terraformDataSourceName, err) + return nil, nil, err } + if isDatasource { + typ := datasourceHandle.schema.Type(ctx).(tftypes.Object) - if failures, err := p.validateDataResourceConfig(ctx, handle, config); err != nil || len(failures) > 0 { - return nil, failures, err + // Transform args to apply Pulumi-level defaults. + argsWithDefaults := defaults.ApplyDefaultInfoValues(ctx, defaults.ApplyDefaultInfoValuesArgs{ + SchemaMap: datasourceHandle.schemaOnlyShim.Schema(), + SchemaInfos: datasourceHandle.pulumiDataSourceInfo.GetFields(), + PropertyMap: args, + ProviderConfig: p.lastKnownProviderConfig, + }) + + config, err := convert.EncodePropertyMapToDynamic(datasourceHandle.encoder, typ, argsWithDefaults) + if err != nil { + return nil, nil, fmt.Errorf("cannot encode config to call ReadDataSource for %q: %w", + datasourceHandle.terraformDataSourceName, err) + } + + if failures, err := p.validateDataResourceConfig(ctx, datasourceHandle, config); err != nil || len(failures) > 0 { + return nil, failures, err + } + + return p.readDataSource(ctx, datasourceHandle, config) } - return p.readDataSource(ctx, handle, config) + return nil, nil, fmt.Errorf("[pf/tfbridge] unknown datasource or action token: %v", tok) } func (p *provider) validateDataResourceConfig(ctx context.Context, handle datasourceHandle, @@ -156,3 +208,105 @@ func (p *provider) parseInvokePropertyCheckFailures(ds datasourceHandle, diags [ return failures, rest } + +func (p *provider) validateActionConfig(ctx context.Context, handle actionHandle, + config *tfprotov6.DynamicValue, +) ([]plugin.CheckFailure, error) { + req := &tfprotov6.ValidateActionConfigRequest{ + ActionType: handle.terraformActionName, + Config: config, + } + + aserver := p.tfServer.(tfprotov6.ActionServer) + resp, err := aserver.ValidateActionConfig(ctx, req) + if err != nil { + return nil, fmt.Errorf("error calling ValidateActionConfig: %w", err) + } + return p.processActionInvokeDiagnostics(handle, resp.Diagnostics) +} + +func (p *provider) invokeAction(ctx context.Context, handle actionHandle, + urn resource.URN, preview bool, config *tfprotov6.DynamicValue, +) (resource.PropertyMap, []plugin.CheckFailure, error) { + + aserver := p.tfServer.(tfprotov6.ActionServer) + + if preview { + req := &tfprotov6.PlanActionRequest{ + Config: config, + ActionType: handle.terraformActionName, + // TODO: Set Capabilities + } + resp, err := aserver.PlanAction(ctx, req) + if err != nil { + return nil, nil, fmt.Errorf("error calling ReadDataSource: %w", err) + } + + // TODO: Do we need to look at resp.Deferred? + + failures, err := p.processActionInvokeDiagnostics(handle, resp.Diagnostics) + if err != nil || len(failures) > 0 { + return nil, failures, err + } + + return resource.PropertyMap{}, failures, nil + } else { + req := &tfprotov6.InvokeActionRequest{ + Config: config, + ActionType: handle.terraformActionName, + // TODO: Set Capabilities + } + resp, err := aserver.InvokeAction(ctx, req) + if err != nil { + return nil, nil, fmt.Errorf("error calling ReadDataSource: %w", err) + } + + var diagnostics []*tfprotov6.Diagnostic + for event := range resp.Events { + switch e := event.Type.(type) { + case tfprotov6.ProgressInvokeActionEventType: + p.logSink.Log(ctx, diag.Info, urn, e.Message) + case tfprotov6.CompletedInvokeActionEventType: + diagnostics = e.Diagnostics + } + } + + failures, err := p.processActionInvokeDiagnostics(handle, diagnostics) + if err != nil || len(failures) > 0 { + return nil, failures, err + } + + return resource.PropertyMap{}, failures, nil + } +} + +func (p *provider) processActionInvokeDiagnostics(act actionHandle, + diags []*tfprotov6.Diagnostic, +) ([]plugin.CheckFailure, error) { + failures, rest := p.parseActionInvokePropertyCheckFailures(act, 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) parseActionInvokePropertyCheckFailures(act actionHandle, diags []*tfprotov6.Diagnostic) ( + []plugin.CheckFailure, []*tfprotov6.Diagnostic, +) { + rest := []*tfprotov6.Diagnostic{} + failures := []plugin.CheckFailure{} + + for _, d := range diags { + if pk, ok := actionPropertyKey(act, 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 +} diff --git a/pkg/tfbridge/info.go b/pkg/tfbridge/info.go index 3db4fad5c..d597d36f0 100644 --- a/pkg/tfbridge/info.go +++ b/pkg/tfbridge/info.go @@ -135,6 +135,8 @@ 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 +type ActionInfo = info.Action + // 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..cf45cd864 100644 --- a/pkg/tfbridge/info/info.go +++ b/pkg/tfbridge/info/info.go @@ -73,6 +73,7 @@ type Provider struct { 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. + Actions map[string]*Action // a map of TF type or renamed entity name to Pulumi action 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. @@ -448,6 +449,33 @@ func (info *Resource) ReplaceExamplesSection() bool { return info.Docs != nil && info.Docs.ReplaceExamplesSection } +// Action can be used to override an action's standard name mangling and argument information. +type Action struct { + Tok tokens.ModuleMember + Fields map[string]*Schema + Docs *Doc // overrides for finding and mapping TF docs. + DeprecationMessage string // message to use in deprecation warning +} + +// GetTok returns a action type token +func (info *Action) GetTok() tokens.Token { return tokens.Token(info.Tok) } + +// GetFields returns information about the action's custom fields +func (info *Action) GetFields() map[string]*Schema { + if info == nil { + return nil + } + return info.Fields +} + +// GetDocs returns a action docs override from the Pulumi provider +func (info *Action) GetDocs() *Doc { return info.Docs } + +// ReplaceExamplesSection returns whether to replace the upstream examples with our own source +func (info *Action) ReplaceExamplesSection() bool { + return info.Docs != nil && info.Docs.ReplaceExamplesSection +} + // DataSource can be used to override a data source's standard name mangling and argument/return information. type DataSource struct { Tok tokens.ModuleMember diff --git a/pkg/tfgen/docs.go b/pkg/tfgen/docs.go index 2c85f6b28..8ab81ea1c 100644 --- a/pkg/tfgen/docs.go +++ b/pkg/tfgen/docs.go @@ -153,6 +153,8 @@ const ( ResourceDocs DocKind = "resources" // DataSourceDocs indicates documentation pertaining to data source entities. DataSourceDocs DocKind = "data-sources" + // ActionDocs indicates documentation pertaining to action entities. + ActionDocs DocKind = "actions" // InstallationDocs indicates documentation pertaining to provider configuration and installation. InstallationDocs DocKind = "installation" ) @@ -161,6 +163,8 @@ func (k DocKind) String() string { switch k { case DataSourceDocs: return "data source" + case ActionDocs: + return "action" case ResourceDocs: return "resource" default: @@ -199,6 +203,8 @@ func getDocsForResource(g *Generator, source DocsSource, kind DocKind, docFile, err = source.getResource(rawname, docInfo) case DataSourceDocs: docFile, err = source.getDatasource(rawname, docInfo) + case ActionDocs: + docFile, err = source.getAction(rawname, docInfo) default: panic("unknown docs kind") } diff --git a/pkg/tfgen/docs_test.go b/pkg/tfgen/docs_test.go index b4f4ecfa8..fec3c5da5 100644 --- a/pkg/tfgen/docs_test.go +++ b/pkg/tfgen/docs_test.go @@ -2556,6 +2556,10 @@ func (m mockSource) getDatasource(rawname string, info *tfbridge.DocInfo) (*DocF return nil, nil } +func (m mockSource) getAction(rawname string, info *tfbridge.DocInfo) (*DocFile, error) { + return nil, nil +} + func (m mockSource) getInstallation(info *tfbridge.DocInfo) (*DocFile, error) { f, ok := m["index.md"] if !ok { diff --git a/pkg/tfgen/generate.go b/pkg/tfgen/generate.go index 234fc30a7..13e420da4 100644 --- a/pkg/tfgen/generate.go +++ b/pkg/tfgen/generate.go @@ -862,6 +862,7 @@ type resourceFunc struct { info *tfbridge.DataSourceInfo entityDocs entityDocs dataSourcePath *paths.DataSourcePath + actionPath *paths.ActionPath } func (rf *resourceFunc) Name() string { return rf.name } @@ -871,6 +872,27 @@ func (rf *resourceFunc) ModuleMemberToken() tokens.ModuleMember { return tokens.NewModuleMemberToken(rf.mod, tokens.ModuleMemberName(rf.name)) } +// actionFunc is a generated resource function that is exposed to interact with Pulumi objects. +type actionFunc struct { + mod tokens.Module + name string + doc string + args []*variable + reqargs map[string]bool + argst *propertyType + schema shim.Action + info *tfbridge.ActionInfo + entityDocs entityDocs + actionPath *paths.ActionPath +} + +func (rf *actionFunc) Name() string { return rf.name } +func (rf *actionFunc) Doc() string { return rf.doc } + +func (rf *actionFunc) 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 +1293,13 @@ func (g *Generator) gatherPackage() (*pkg, error) { pack.addModuleMap(dsmods) } + actmods, err := g.gatherActions() + if err != nil { + return nil, pkgerrors.Wrapf(err, "problem gathering actions") + } else if actmods != nil { + pack.addModuleMap(actmods) + } + // Now go ahead and merge in any overlays into the modules if there are any. olaymods, err := g.gatherOverlays() if err != nil { @@ -1759,6 +1788,175 @@ func (g *Generator) gatherDataSource(rawname string, return fun, nil } +func (g *Generator) gatherActions() (moduleMap, error) { + // If there aren't any data sources, skip this altogether. + sources := g.provider().ActionsMap() + if sources.Len() == 0 { + return nil, nil + } + 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")) + + // let's keep a list of TF mapping errors that we can present to the user + var dataSourceMappingErrors error + + // For each data source, create its own dedicated function and module export. + var dserr error + seen := make(map[string]bool) + for _, ds := range stableActions(sources) { + dsinfo := g.info.Actions[ds] + if dsinfo == nil { + if sliceContains(g.info.IgnoreMappings, ds) { + g.debug("TF data source %q not found in provider map but ignored", ds) + continue + } + + if !skipFailBuildOnMissingMapError { + dataSourceMappingErrors = multierror.Append(dataSourceMappingErrors, + fmt.Errorf("TF data source %q not mapped to the Pulumi provider", ds)) + } else { + g.warn("TF data source %q not found in provider map", ds) + } + continue + } + seen[ds] = true + + fun, err := g.gatherAction(ds, sources.Get(ds), dsinfo) + if err != nil { + // Keep track of the error, but keep going, so we can expose more at once. + dserr = multierror.Append(dserr, err) + } else { + // Add any members returned to the specified module. + modules.ensureModule(fun.mod).addMember(fun) + } + } + if dserr != nil { + return nil, dserr + } + + // Emit a warning if there is a map but some names didn't match. + var names []string + for name := range g.info.Actions { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + if !seen[name] { + if !skipFailBuildOnExtraMapError { + dataSourceMappingErrors = multierror.Append(dataSourceMappingErrors, + fmt.Errorf("Pulumi token %q is mapped to TF provider data source %q, but no such "+ + "data source found. Remove the mapping and try again", + g.info.Actions[name].Tok, name)) + } else { + g.warn("Pulumi token %q is mapped to TF provider data source %q, but no such "+ + "data source found. The mapping will be ignored in the generated provider", + g.info.Actions[name].Tok, name) + } + } + } + + // let's check the unmapped Action Errors + if dataSourceMappingErrors != nil { + return nil, dataSourceMappingErrors + } + + return modules, nil +} + +// gatherAction returns the module name and members for the given data source function. +func (g *Generator) gatherAction(rawname string, + act shim.Action, info *tfbridge.ActionInfo, +) (*actionFunc, error) { + // Generate the name and module for this data source. + name, moduleName := actionName(g.info.Name, rawname, info) + mod := tokens.NewModuleToken(g.pkg, moduleName) + actionPath := paths.NewActionPath(rawname, tokens.NewModuleMemberToken(mod, name)) + + // Collect documentation information for this data source. + source := NewGitRepoDocsSource(g) + entityDocs, err := getDocsForResource(g, source, ActionDocs, rawname, info) + if err != nil && !g.checkNoDocsError(err) { + return nil, err + } + + // Build up the function information. + fun := &actionFunc{ + mod: mod, + name: name.String(), + doc: entityDocs.Description, + reqargs: make(map[string]bool), + schema: act, + info: info, + entityDocs: entityDocs, + actionPath: actionPath, + } + + // See if arguments for this function are optional, and generate detailed metadata. + for _, arg := range stableSchemas(act.Schema()) { + sch := act.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(actionPath.Args(), + arg, act.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 + } + } + } + } + + // Make up a urn property + if urn, has := act.Schema().GetOk("urn"); has && urn.Removed() == "" { + return nil, fmt.Errorf("action %q must not have a 'urn' property in its schema", rawname) + } + cust := map[string]*tfbridge.SchemaInfo{"urn": {}} + rawdoc := "The URN to run this action against." + urnSchema := schema.SchemaMap(map[string]shim.Schema{ + "urn": (&schema.Schema{Type: shim.TypeString, Required: true}).Shim(), + }) + p, err := g.propertyVariable(actionPath.Args(), "urn", urnSchema, cust, "", + rawdoc, true /*out*/, entityDocs) + if err != nil { + return nil, err + } + if p != nil { + fun.args = append(fun.args, 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, + } + } + + return fun, nil +} + // gatherOverlays returns any overlay modules and their contents. func (g *Generator) gatherOverlays() (moduleMap, error) { modules := make(moduleMap) @@ -1973,6 +2171,20 @@ func (g *Generator) propertyVariable(parentPath paths.TypePath, key string, return nil, nil } +// actionName translates a Terraform name into its Pulumi name equivalent. +func actionName(provider string, rawname string, + info *tfbridge.ActionInfo, +) (tokens.ModuleMemberName, tokens.ModuleName) { + if info == nil || info.Tok == "" { + // default transformations. + name := withoutPackageName(provider, rawname) // strip off the pkg prefix. + name = tfbridge.TerraformToPulumiNameV2(name, nil, nil) + return tokens.ModuleMemberName(name), tokens.ModuleName(name) + } + // otherwise, a custom transformation exists; use it. + return info.Tok.Name(), info.Tok.Module().Name() +} + // dataSourceName translates a Terraform name into its Pulumi name equivalent. func dataSourceName(provider string, rawname string, info *tfbridge.DataSourceInfo, @@ -2023,6 +2235,16 @@ func withoutPackageName(pkg string, rawname string) string { return strings.TrimPrefix(rawname, pkg+"_") } +func stableActions(actions shim.ActionMap) []string { + var acts []string + actions.Range(func(a string, _ shim.Action) bool { + acts = append(acts, a) + return true + }) + sort.Strings(acts) + return acts +} + func stableResources(resources shim.ResourceMap) []string { var rs []string resources.Range(func(r string, _ shim.Resource) bool { diff --git a/pkg/tfgen/generate_schema.go b/pkg/tfgen/generate_schema.go index f3d28dc70..e2b62b970 100644 --- a/pkg/tfgen/generate_schema.go +++ b/pkg/tfgen/generate_schema.go @@ -105,6 +105,9 @@ func (nt *schemaNestedTypes) gatherFromMember(member moduleMember) { p := member.dataSourcePath nt.gatherFromProperties(p.Args(), member, member.name, member.args, true) nt.gatherFromProperties(p.Results(), member, member.name, member.rets, false) + case *actionFunc: + p := member.actionPath + nt.gatherFromProperties(p.Args(), member, member.name, member.args, true) case *variable: contract.Assertf(member.config, `member.config`) if member.typ == nil { @@ -313,6 +316,8 @@ 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 *actionFunc: + spec.Functions[string(t.info.Tok)] = g.genActionFunc(mod.name, t) case *variable: contract.Assertf(mod.config(), `mod.config()`) config = append(config, t) @@ -918,6 +923,30 @@ func (g *schemaGenerator) genDatasourceFunc(mod tokens.Module, fun *resourceFunc return spec } +func (g *schemaGenerator) genActionFunc(mod tokens.Module, fun *actionFunc) pschema.FunctionSpec { + var spec pschema.FunctionSpec + + description := "" + if fun.doc != "" { + description = g.genDocComment(fun.doc) + } + if fun.info.DeprecationMessage != "" { + spec.DeprecationMessage = fun.info.DeprecationMessage + } + spec.Description = description + + // If there are argument and/or return types, emit them. + if fun.argst != nil { + t := g.genObjectType(&schemaNestedType{ + typ: fun.argst, + typePaths: paths.SingletonTypePathSet(fun.actionPath.Args()), + }, false) + spec.Inputs = &t + } + + return spec +} + func setEquals(a, b codegen.StringSet) bool { if len(a) != len(b) { return false diff --git a/pkg/tfgen/internal/paths/paths.go b/pkg/tfgen/internal/paths/paths.go index 4b2c0ae0c..96d770a93 100644 --- a/pkg/tfgen/internal/paths/paths.go +++ b/pkg/tfgen/internal/paths/paths.go @@ -23,8 +23,8 @@ import ( // TypePath values uniquely identify locations within a Pulumi Package Schema that require generating types in a target // programming language when a provider SDK for that language is being built. Examples of such types include resources -// (see ResourcePath), data sources (DataSourcePath), provider configuration (ConfigPath), and nested object types that -// are used to describe the type of resource properties. +// (see ResourcePath), data sources (DataSourcePath), actions (ActionPath), provider configuration (ConfigPath), and +// nested object types that are used to describe the type of resource properties. type TypePath interface { // Parent path, can be nil for root paths. Parent() TypePath @@ -226,6 +226,80 @@ func (p *DataSourceMemberPath) UniqueKey() string { return p.String() } +// Identifies a data source uniquely. +type ActionPath struct { + key string + token tokens.ModuleMember +} + +func NewActionPath(key string, token tokens.ModuleMember) *ActionPath { + return &ActionPath{ + key: key, + token: token, + } +} + +// Pulumi token uniquely identifiying the Action. +func (p *ActionPath) Token() tokens.ModuleMember { + return p.token +} + +// Unique identifier for the Action preserved from the shim layer, typically the Terraform name. +func (p *ActionPath) Key() string { + return p.key +} + +func (p *ActionPath) Args() *ActionMemberPath { + return &ActionMemberPath{ + ActionPath: p, + ActionMemberKind: ActionArgs, + } +} + +func (p *ActionPath) String() string { + return fmt.Sprintf("datasource[key=%q,token=%q]", + p.key, + p.token.String()) +} + +type ActionMemberKind int + +const ( + ActionArgs ActionMemberKind = iota + ActionResults +) + +func (s ActionMemberKind) String() string { + switch s { + case ActionArgs: + return "args" + case ActionResults: + return "results" + } + return "unknown" +} + +type ActionMemberPath struct { + ActionPath *ActionPath + ActionMemberKind ActionMemberKind +} + +var _ TypePath = (*ActionMemberPath)(nil) + +func (p *ActionMemberPath) Parent() TypePath { + return nil +} + +func (p *ActionMemberPath) String() string { + return fmt.Sprintf("%s.%s", + p.ActionPath.String(), + p.ActionMemberKind.String()) +} + +func (p *ActionMemberPath) UniqueKey() string { + return p.String() +} + type ConfigPath struct{} var _ TypePath = (*ConfigPath)(nil) diff --git a/pkg/tfgen/source.go b/pkg/tfgen/source.go index 37fb941cf..58a20371f 100644 --- a/pkg/tfgen/source.go +++ b/pkg/tfgen/source.go @@ -35,6 +35,9 @@ type DocsSource interface { // Get the bytes for a datasource with TF token rawname. getDatasource(rawname string, info *tfbridge.DocInfo) (*DocFile, error) + // Get the bytes for an action with TF token rawname. + getAction(rawname string, info *tfbridge.DocInfo) (*DocFile, error) + // Get the bytes for the provider installation doc. getInstallation(info *tfbridge.DocInfo) (*DocFile, error) } @@ -74,6 +77,10 @@ func (gh *gitRepoSource) getDatasource(rawname string, info *tfbridge.DocInfo) ( return gh.getFile(rawname, info, DataSourceDocs) } +func (gh *gitRepoSource) getAction(rawname string, info *tfbridge.DocInfo) (*DocFile, error) { + return gh.getFile(rawname, info, ActionDocs) +} + func (gh *gitRepoSource) getInstallation(info *tfbridge.DocInfo) (*DocFile, error) { // The installation docs do not have a rawname. return gh.getFile("", info, InstallationDocs) @@ -99,7 +106,7 @@ func (gh *gitRepoSource) getFile( switch kind { case InstallationDocs: possibleMarkdownNames = append(possibleMarkdownNames, "index.md", "index.html.markdown") - case ResourceDocs, DataSourceDocs: + case ResourceDocs, DataSourceDocs, ActionDocs: possibleMarkdownNames = getMarkdownNames(gh.resourcePrefix, rawname, gh.docRules) if info != nil && info.Source != "" { possibleMarkdownNames = append(possibleMarkdownNames, info.Source) diff --git a/pkg/tfshim/schema/action.go b/pkg/tfshim/schema/action.go new file mode 100644 index 000000000..1ba35db33 --- /dev/null +++ b/pkg/tfshim/schema/action.go @@ -0,0 +1,65 @@ +package schema + +import ( + "context" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" +) + +var _ = shim.ActionMap(ActionMap{}) + +type Action struct { + Schema shim.SchemaMap + Metadata string + Invoke func(ctx context.Context, ps resource.PropertyMap) (resource.PropertyMap, error) +} + +func (r *Action) Shim() shim.Action { + return ActionShim{V: r} +} + +type ActionShim struct { + V *Action + internalinter.Internal +} + +func (r ActionShim) Schema() shim.SchemaMap { + return r.V.Schema +} + +func (r ActionShim) Metadata() string { + return r.V.Metadata +} + +func (r ActionShim) Invoke(ctx context.Context, ps resource.PropertyMap) (resource.PropertyMap, error) { + return r.V.Invoke(ctx, ps) +} + +type ActionMap map[string]shim.Action + +func (m ActionMap) Len() int { + return len(m) +} + +func (m ActionMap) Get(key string) shim.Action { + return m[key] +} + +func (m ActionMap) GetOk(key string) (shim.Action, bool) { + r, ok := m[key] + return r, ok +} + +func (m ActionMap) Range(each func(key string, value shim.Action) bool) { + for key, value := range m { + if !each(key, value) { + return + } + } +} + +func (m ActionMap) Set(key string, value shim.Action) { + m[key] = value +} diff --git a/pkg/tfshim/schema/provider.go b/pkg/tfshim/schema/provider.go index 331583070..a48df19dc 100644 --- a/pkg/tfshim/schema/provider.go +++ b/pkg/tfshim/schema/provider.go @@ -11,6 +11,7 @@ type Provider struct { Schema shim.SchemaMap ResourcesMap shim.ResourceMap DataSourcesMap shim.ResourceMap + ActionsMap shim.ActionMap internalinter.Internal } @@ -25,6 +26,9 @@ func (p *Provider) Shim() shim.Provider { if c.DataSourcesMap == nil { c.DataSourcesMap = ResourceMap{} } + if c.ActionsMap == nil { + c.ActionsMap = ActionMap{} + } return newProviderShim(c) } @@ -49,6 +53,10 @@ func (s ProviderShim) DataSourcesMap() shim.ResourceMap { return s.V.DataSourcesMap } +func (s ProviderShim) ActionsMap() shim.ActionMap { + return s.V.ActionsMap +} + func (s ProviderShim) InternalValidate() error { return nil } diff --git a/pkg/tfshim/sdk-v1/action.go b/pkg/tfshim/sdk-v1/action.go new file mode 100644 index 000000000..ac65ad4e1 --- /dev/null +++ b/pkg/tfshim/sdk-v1/action.go @@ -0,0 +1,58 @@ +package sdkv1 + +import ( + "context" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" +) + +var ( + _ = shim.Action(v1Action{}) + _ = shim.ActionMap(v1ActionMap{}) +) + +type v1Action struct { + internalinter.Internal +} + +func (a v1Action) Schema() shim.SchemaMap { + contract.Failf("v1Action does not support Schema") + return nil +} + +func (a v1Action) Metadata() string { + contract.Failf("v1Action does not support Metadata") + return "" +} + +func (a v1Action) Invoke( + ctx context.Context, + inputs resource.PropertyMap, +) (resource.PropertyMap, error) { + contract.Failf("v1Action does not support Invoke") + return nil, nil +} + +type v1ActionMap map[string]any + +func (m v1ActionMap) Len() int { + return 0 +} + +func (m v1ActionMap) Get(key string) shim.Action { + return nil +} + +func (m v1ActionMap) GetOk(key string) (shim.Action, bool) { + return nil, false +} + +func (m v1ActionMap) Range(each func(key string, value shim.Action) bool) { +} + +func (m v1ActionMap) Set(key string, value shim.Action) { + contract.Failf("cannot set on v1ActionMap") +} diff --git a/pkg/tfshim/sdk-v1/provider.go b/pkg/tfshim/sdk-v1/provider.go index 7a18461a0..3fff45c44 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) ActionsMap() shim.ActionMap { + return v1ActionMap{} +} + func (p v1Provider) InternalValidate() error { return p.tf.InternalValidate() } diff --git a/pkg/tfshim/sdk-v2/action.go b/pkg/tfshim/sdk-v2/action.go new file mode 100644 index 000000000..3c950db59 --- /dev/null +++ b/pkg/tfshim/sdk-v2/action.go @@ -0,0 +1,58 @@ +package sdkv2 + +import ( + "context" + + "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/internal/internalinter" + shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" +) + +var ( + _ = shim.Action(v2Action{}) + _ = shim.ActionMap(v2ActionMap{}) +) + +type v2Action struct { + internalinter.Internal +} + +func (a v2Action) Schema() shim.SchemaMap { + contract.Failf("v2Action does not support Schema") + return nil +} + +func (a v2Action) Metadata() string { + contract.Failf("v2Action does not support Metadata") + return "" +} + +func (a v2Action) Invoke( + ctx context.Context, + inputs resource.PropertyMap, +) (resource.PropertyMap, error) { + contract.Failf("v2Action does not support Invoke") + return nil, nil +} + +type v2ActionMap map[string]any + +func (m v2ActionMap) Len() int { + return 0 +} + +func (m v2ActionMap) Get(key string) shim.Action { + return nil +} + +func (m v2ActionMap) GetOk(key string) (shim.Action, bool) { + return nil, false +} + +func (m v2ActionMap) Range(each func(key string, value shim.Action) bool) { +} + +func (m v2ActionMap) Set(key string, value shim.Action) { + contract.Failf("cannot set on v2ActionMap") +} diff --git a/pkg/tfshim/sdk-v2/provider.go b/pkg/tfshim/sdk-v2/provider.go index 915169e79..43a343b66 100644 --- a/pkg/tfshim/sdk-v2/provider.go +++ b/pkg/tfshim/sdk-v2/provider.go @@ -77,6 +77,10 @@ func (p v2Provider) Schema() shim.SchemaMap { return v2SchemaMap(p.tf.Schema) } +func (p v2Provider) ActionsMap() shim.ActionMap { + return v2ActionMap{} +} + func (p v2Provider) DataSourcesMap() shim.ResourceMap { return v2ResourceMap(p.tf.DataSourcesMap) } diff --git a/pkg/tfshim/shim.go b/pkg/tfshim/shim.go index ab6b56a80..a694b6be9 100644 --- a/pkg/tfshim/shim.go +++ b/pkg/tfshim/shim.go @@ -296,10 +296,29 @@ func CloneResource(rm ResourceMap, oldKey string, newKey string) error { } } +type Action interface { + Schema() SchemaMap + Metadata() (typeName string) + Invoke( + ctx context.Context, + inputs resource.PropertyMap, + ) (resource.PropertyMap, error) +} + +type ActionMap interface { + Len() int + Get(key string) Action + GetOk(key string) (Action, bool) + Range(each func(key string, value Action) bool) + + Set(key string, value Action) +} + type Provider interface { Schema() SchemaMap ResourcesMap() ResourceMap DataSourcesMap() ResourceMap + ActionsMap() ActionMap 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..28ba79fe4 100644 --- a/pkg/tfshim/util/filter.go +++ b/pkg/tfshim/util/filter.go @@ -44,6 +44,7 @@ type FilteringProvider struct { Provider shim.Provider ResourceFilter func(token string) bool DataSourceFilter func(token string) bool + ActionFilter 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) ActionsMap() shim.ActionMap { + return &filteringActionMap{p.Provider.ActionsMap(), p.ActionFilter} +} + func (p *FilteringProvider) InternalValidate() error { return p.Provider.InternalValidate() } @@ -189,3 +194,40 @@ func (f *filteringMap) Set(key string, value shim.Resource) { } var _ shim.ResourceMap = (*filteringMap)(nil) + +type filteringActionMap struct { + inner shim.ActionMap + tokenFilter func(string) bool +} + +func (f *filteringActionMap) Range(each func(key string, value shim.Action) bool) { + f.inner.Range(func(key string, value shim.Action) bool { + if f.tokenFilter != nil && !f.tokenFilter(key) { + return true + } + return each(key, value) + }) +} + +func (f *filteringActionMap) Len() int { + n := 0 + f.Range(func(key string, value shim.Action) bool { + n = n + 1 + return true + }) + return n +} + +func (f *filteringActionMap) Get(key string) shim.Action { + return f.inner.Get(key) +} + +func (f *filteringActionMap) GetOk(key string) (shim.Action, bool) { + return f.inner.GetOk(key) +} + +func (f *filteringActionMap) Set(key string, value shim.Action) { + f.inner.Set(key, value) +} + +var _ shim.ActionMap = (*filteringActionMap)(nil) diff --git a/pkg/tfshim/util/util.go b/pkg/tfshim/util/util.go index d526d9b37..f305b04f5 100644 --- a/pkg/tfshim/util/util.go +++ b/pkg/tfshim/util/util.go @@ -31,6 +31,7 @@ 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) ActionsMap() shim.ActionMap { panic("unimplemented") } func (UnimplementedProvider) InternalValidate() error { panic("unimplemented") }