diff --git a/internal/providers/schemas.go b/internal/providers/schemas.go index 73c6de6552..4d02c8bdc8 100644 --- a/internal/providers/schemas.go +++ b/internal/providers/schemas.go @@ -37,3 +37,13 @@ func (ss ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema Sche } type ResourceIdentitySchemas = GetResourceIdentitySchemasResponse + +// SchemaForActionType attempts to find a schema for the given type. Returns an +// empty schema if none is available. +func (ss ProviderSchema) SchemaForActionType(typeName string) (schema ActionSchema) { + schema, ok := ss.Actions[typeName] + if ok { + return schema + } + return ActionSchema{} +} diff --git a/internal/providers/testing/provider_mock.go b/internal/providers/testing/provider_mock.go index ca5e4e1b6a..1e9ac5c161 100644 --- a/internal/providers/testing/provider_mock.go +++ b/internal/providers/testing/provider_mock.go @@ -161,7 +161,7 @@ type MockProvider struct { InvokeActionRequest providers.InvokeActionRequest InvokeActionFn func(providers.InvokeActionRequest) providers.InvokeActionResponse - ValidateActionCalled bool + ValidateActionConfigCalled bool ValidateActionConfigRequest providers.ValidateActionConfigRequest ValidateActionConfigResponse *providers.ValidateActionConfigResponse ValidateActionConfigFn func(providers.ValidateActionConfigRequest) providers.ValidateActionConfigResponse @@ -1055,11 +1055,23 @@ func (p *MockProvider) beginWrite() func() { } func (p *MockProvider) ValidateActionConfig(r providers.ValidateActionConfigRequest) (resp providers.ValidateActionConfigResponse) { - defer p.beginWrite() + defer p.beginWrite()() - p.ValidateActionCalled = true + p.ValidateActionConfigCalled = true p.ValidateActionConfigRequest = r + // Marshall the value to replicate behavior by the GRPC protocol + actionSchema, ok := p.getProviderSchema().Actions[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("no schema found for %q", r.TypeName)) + return resp + } + _, err := msgpack.Marshal(r.Config, actionSchema.ConfigSchema.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + if p.ValidateActionConfigFn != nil { return p.ValidateActionConfigFn(r) } diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index c96d1c5dd4..48cf132602 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -3571,3 +3571,145 @@ func TestContext2Validate_queryList(t *testing.T) { }) } } + +// Action Validation is largely exercised in context_plan_actions_test.go +func TestContext2Validate_action(t *testing.T) { + tests := map[string]struct { + config string + wantErr string + }{ + "valid config": { + ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } +} +action "test_register" "foo" { + config { + host = "cmdb.snot" + } +} +resource "test_instance" "foo" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.test_register.foo] + } + } +} +`, + "", + }, + "missing required config": { + ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } +} +action "test_register" "foo" { + config {} +} +resource "test_instance" "foo" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.test_register.foo] + } + } +} +`, + "host is null", + }, + "invalid nil config config": { + ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } +} +action "test_register" "foo" { +} +resource "test_instance" "foo" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.test_register.foo] + } + } +} +`, + "config is null", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + m := testModuleInline(t, map[string]string{"main.tf": test.config}) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_instance": { + Attributes: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true}, + "num": {Type: cty.String, Optional: true}, + }, + }, + }, + Actions: map[string]providers.ActionSchema{ + "test_register": { + ConfigSchema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "host": {Type: cty.String, Optional: true}, + }, + }, + }, + }, + }) + p.ValidateActionConfigFn = func(r providers.ValidateActionConfigRequest) (resp providers.ValidateActionConfigResponse) { + if r.Config.IsNull() { + resp.Diagnostics = resp.Diagnostics.Append(errors.New("config is null")) + return + } + if r.Config.GetAttr("host").IsNull() { + resp.Diagnostics = resp.Diagnostics.Append(errors.New("host is null")) + } + return + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if !p.ValidateActionConfigCalled { + t.Fatal("ValidateAction RPC was not called") + } + + if test.wantErr != "" { + if !diags.HasErrors() { + t.Errorf("unexpected success\nwant errors: %s", test.wantErr) + } else if got, want := diags.Err().Error(), test.wantErr; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } + } else { + if diags.HasErrors() { + t.Errorf("unexpected error\ngot: %s", diags.Err().Error()) + } + } + }) + } +} diff --git a/internal/terraform/eval_for_each.go b/internal/terraform/eval_for_each.go index ec61b4829c..62bca4ffbd 100644 --- a/internal/terraform/eval_for_each.go +++ b/internal/terraform/eval_for_each.go @@ -85,7 +85,7 @@ func (ev *forEachEvaluator) ResourceValue() (map[string]cty.Value, bool, tfdiags } // validate the for_each value for use in resource expansion - diags = diags.Append(ev.validateResource(forEachVal)) + diags = diags.Append(ev.validateResourceOrActionForEach(forEachVal, "resource")) if diags.HasErrors() { return res, false, diags } @@ -287,21 +287,39 @@ func (ev *forEachEvaluator) ValidateResourceValue() tfdiags.Diagnostics { return diags } - return diags.Append(ev.validateResource(val)) + return diags.Append(ev.validateResourceOrActionForEach(val, "resource")) } -// validateResource validates the type and values of the forEachVal, while -// still allowing unknown values for use within the validation walk. -func (ev *forEachEvaluator) validateResource(forEachVal cty.Value) tfdiags.Diagnostics { +// ValidateActionValue is used from validation walks to verify the validity of +// the action for_Each expression, while still allowing for unknown values. +func (ev *forEachEvaluator) ValidateActionValue() tfdiags.Diagnostics { + val, diags := ev.Value() + if diags.HasErrors() { + return diags + } + + return diags.Append(ev.validateResourceOrActionForEach(val, "action")) +} + +// validateResourceOrActionForEach validates the type and values of the +// forEachVal, while still allowing unknown values for use within the validation +// walk. The "blocktype" parameter is used to craft the diagnostic messages and +// indicates if the block was a resource or action. You can also use the +// ValidateActionValue or ValidateResourceValue helper methods to avoid this. +func (ev *forEachEvaluator) validateResourceOrActionForEach(forEachVal cty.Value, blocktype string) tfdiags.Diagnostics { var diags tfdiags.Diagnostics // Sensitive values are not allowed because otherwise the sensitive keys // would get exposed as part of the instance addresses. + msg := "a resource" + if blocktype == "action" { + msg = "an action" + } if forEachVal.HasMark(marks.Sensitive) { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid for_each argument", - Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.", + Detail: fmt.Sprintf("Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as %s instance key.", msg), Subject: ev.expr.Range().Ptr(), Expression: ev.expr, EvalContext: ev.hclCtx, diff --git a/internal/terraform/graph_builder_plan.go b/internal/terraform/graph_builder_plan.go index 2c40213216..36c90211c3 100644 --- a/internal/terraform/graph_builder_plan.go +++ b/internal/terraform/graph_builder_plan.go @@ -80,6 +80,9 @@ type PlanGraphBuilder struct { ConcreteResourceOrphan ConcreteResourceInstanceNodeFunc ConcreteResourceInstanceDeposed ConcreteResourceInstanceDeposedNodeFunc ConcreteModule ConcreteModuleNodeFunc + // ConcreteAction is only used by the ConfigTransformer during the Validate + // Graph walk; otherwise we fall back to the DefaultConcreteActionFunc. + ConcreteAction ConcreteActionNodeFunc // Plan Operation this graph will be used for. Operation walkOperation @@ -156,9 +159,10 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { steps := []GraphTransformer{ // Creates all the resources represented in the config &ConfigTransformer{ - Concrete: b.ConcreteResource, - Config: b.Config, - destroy: b.Operation == walkDestroy || b.Operation == walkPlanDestroy, + Concrete: b.ConcreteResource, + ConcreteAction: b.ConcreteAction, + Config: b.Config, + destroy: b.Operation == walkDestroy || b.Operation == walkPlanDestroy, resourceMatcher: func(mode addrs.ResourceMode) bool { // all resources are included during validation. if b.Operation == walkValidate { @@ -381,6 +385,12 @@ func (b *PlanGraphBuilder) initValidate() { nodeExpandModule: *n, } } + + b.ConcreteAction = func(a *NodeAbstractAction) dag.Vertex { + return &NodeValidatableAction{ + NodeAbstractAction: a, + } + } } func (b *PlanGraphBuilder) initImport() { diff --git a/internal/terraform/node_action.go b/internal/terraform/node_action.go index 8528bcd986..5176aad995 100644 --- a/internal/terraform/node_action.go +++ b/internal/terraform/node_action.go @@ -5,10 +5,6 @@ package terraform import ( "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/dag" - "github.com/hashicorp/terraform/internal/lang/langrefs" - "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -22,68 +18,17 @@ type GraphNodeConfigAction interface { // nodeExpandActionDeclaration represents an action config block in a configuration module, // which has not yet been expanded. type nodeExpandActionDeclaration struct { - Addr addrs.ConfigAction - Config *configs.Action - - Schema *providers.ActionSchema - ResolvedProvider addrs.AbsProviderConfig + *NodeAbstractAction } var ( - _ GraphNodeConfigAction = (*nodeExpandActionDeclaration)(nil) - _ GraphNodeReferenceable = (*nodeExpandActionDeclaration)(nil) - _ GraphNodeReferencer = (*nodeExpandActionDeclaration)(nil) - _ GraphNodeDynamicExpandable = (*nodeExpandActionDeclaration)(nil) - _ GraphNodeProviderConsumer = (*nodeExpandActionDeclaration)(nil) - _ GraphNodeAttachActionSchema = (*nodeExpandActionDeclaration)(nil) + _ GraphNodeDynamicExpandable = (*nodeExpandActionDeclaration)(nil) ) func (n *nodeExpandActionDeclaration) Name() string { return n.Addr.String() + " (expand)" } -func (n *nodeExpandActionDeclaration) ActionAddr() addrs.ConfigAction { - return n.Addr -} - -func (n *nodeExpandActionDeclaration) ReferenceableAddrs() []addrs.Referenceable { - return []addrs.Referenceable{n.Addr.Action} -} - -// GraphNodeModulePath -func (n *nodeExpandActionDeclaration) ModulePath() addrs.Module { - return n.Addr.Module -} - -// GraphNodeAttachActionSchema -func (n *nodeExpandActionDeclaration) AttachActionSchema(schema *providers.ActionSchema) { - n.Schema = schema -} - -func (n *nodeExpandActionDeclaration) DotNode(string, *dag.DotOpts) *dag.DotNode { - return &dag.DotNode{ - Name: n.Name(), - } -} - -// GraphNodeReferencer -func (n *nodeExpandActionDeclaration) References() []*addrs.Reference { - var result []*addrs.Reference - c := n.Config - - refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, c.Count) - result = append(result, refs...) - refs, _ = langrefs.ReferencesInExpr(addrs.ParseRef, c.ForEach) - result = append(result, refs...) - - if n.Schema != nil { - refs, _ = langrefs.ReferencesInBlock(addrs.ParseRef, c.Config, n.Schema.ConfigSchema) - result = append(result, refs...) - } - - return result -} - func (n *nodeExpandActionDeclaration) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { var g Graph var diags tfdiags.Diagnostics @@ -109,7 +54,7 @@ func (n *nodeExpandActionDeclaration) DynamicExpand(ctx EvalContext) (*Graph, tf for _, absActInstance := range expander.ExpandAction(absActAddr) { node := NodeActionDeclarationInstance{ Addr: absActInstance, - Config: n.Config, + Config: &n.Config, Schema: n.Schema, ResolvedProvider: n.ResolvedProvider, } @@ -175,28 +120,3 @@ func (n *nodeExpandActionDeclaration) recordActionData(ctx EvalContext, addr add return diags } - -// GraphNodeProviderConsumer -func (n *nodeExpandActionDeclaration) ProvidedBy() (addrs.ProviderConfig, bool) { - // Once the provider is fully resolved, we can return the known value. - if n.ResolvedProvider.Provider.Type != "" { - return n.ResolvedProvider, true - } - - // Since we always have a config, we can use it - relAddr := n.Config.ProviderConfigAddr() - return addrs.LocalProviderConfig{ - LocalName: relAddr.LocalName, - Alias: relAddr.Alias, - }, false -} - -// GraphNodeProviderConsumer -func (n *nodeExpandActionDeclaration) Provider() addrs.Provider { - return n.Config.Provider -} - -// GraphNodeProviderConsumer -func (n *nodeExpandActionDeclaration) SetProvider(p addrs.AbsProviderConfig) { - n.ResolvedProvider = p -} diff --git a/internal/terraform/node_action_abstract.go b/internal/terraform/node_action_abstract.go new file mode 100644 index 0000000000..b5c2c7a1b5 --- /dev/null +++ b/internal/terraform/node_action_abstract.go @@ -0,0 +1,112 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/providers" +) + +// NodeAbstractAction represents an action that has no associated +// operations. +type NodeAbstractAction struct { + Addr addrs.ConfigAction + Config configs.Action + + // The fields below will be automatically set using the Attach interfaces if + // you're running those transforms, but also can be explicitly set if you + // already have that information. + + // The address of the provider this action will use + ResolvedProvider addrs.AbsProviderConfig + Schema *providers.ActionSchema +} + +var ( + _ GraphNodeReferenceable = (*NodeAbstractAction)(nil) + _ GraphNodeReferencer = (*NodeAbstractAction)(nil) + _ GraphNodeConfigAction = (*NodeAbstractAction)(nil) + _ GraphNodeAttachActionSchema = (*NodeAbstractAction)(nil) + _ GraphNodeProviderConsumer = (*NodeAbstractAction)(nil) +) + +func (n NodeAbstractAction) Name() string { + return n.Addr.String() +} + +// ConcreteActionNodeFunc is a callback type used to convert an +// abstract action to a concrete one of some type. +type ConcreteActionNodeFunc func(*NodeAbstractAction) dag.Vertex + +// I'm not sure why my ConcreteActionNodeFUnction kept being nil in tests, but +// this is much more robust. If it isn't a validate walk, we need +// nodeExpandActionDeclaration. +func DefaultConcreteActionNodeFunc(a *NodeAbstractAction) dag.Vertex { + return &nodeExpandActionDeclaration{ + NodeAbstractAction: a, + } +} + +// GraphNodeConfigAction +func (n NodeAbstractAction) ActionAddr() addrs.ConfigAction { + return n.Addr +} + +func (n NodeAbstractAction) ModulePath() addrs.Module { + return n.Addr.Module +} + +func (n *NodeAbstractAction) ReferenceableAddrs() []addrs.Referenceable { + return []addrs.Referenceable{n.Addr.Action} +} + +func (n *NodeAbstractAction) References() []*addrs.Reference { + var result []*addrs.Reference + c := n.Config + + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, c.Count) + result = append(result, refs...) + refs, _ = langrefs.ReferencesInExpr(addrs.ParseRef, c.ForEach) + result = append(result, refs...) + + if n.Schema != nil { + refs, _ = langrefs.ReferencesInBlock(addrs.ParseRef, c.Config, n.Schema.ConfigSchema) + result = append(result, refs...) + } + + return result +} + +func (n *NodeAbstractAction) AttachActionSchema(schema *providers.ActionSchema) { + n.Schema = schema +} + +func (n *NodeAbstractAction) ProvidedBy() (addrs.ProviderConfig, bool) { + // If the resolvedProvider is set, use that + if n.ResolvedProvider.Provider.Type != "" { + return n.ResolvedProvider, true + } + + // otherwise refer back to the config + relAddr := n.Config.ProviderConfigAddr() + return addrs.LocalProviderConfig{ + LocalName: relAddr.LocalName, + Alias: relAddr.Alias, + }, false +} + +func (n *NodeAbstractAction) Provider() addrs.Provider { + if n.Config.Provider.Type != "" { + return n.Config.Provider + } + + return addrs.ImpliedProviderForUnqualifiedType(n.Addr.Action.ImpliedProvider()) +} + +func (n *NodeAbstractAction) SetProvider(p addrs.AbsProviderConfig) { + n.ResolvedProvider = p +} diff --git a/internal/terraform/node_action_validate.go b/internal/terraform/node_action_validate.go new file mode 100644 index 0000000000..f9c67b519e --- /dev/null +++ b/internal/terraform/node_action_validate.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "log" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// NodeValidatableAction represents an action that is used for validation only. +type NodeValidatableAction struct { + *NodeAbstractAction +} + +var ( + _ GraphNodeModuleInstance = (*NodeValidatableAction)(nil) + _ GraphNodeExecutable = (*NodeValidatableAction)(nil) + _ GraphNodeReferenceable = (*NodeValidatableAction)(nil) + _ GraphNodeReferencer = (*NodeValidatableAction)(nil) + _ GraphNodeConfigAction = (*NodeValidatableAction)(nil) + _ GraphNodeAttachActionSchema = (*NodeValidatableAction)(nil) +) + +func (n *NodeValidatableAction) Path() addrs.ModuleInstance { + // There is no expansion during validation, so we evaluate everything as + // single module instances. + return n.Addr.Module.UnkeyedInstanceShim() +} + +func (n *NodeValidatableAction) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) + diags = diags.Append(err) + if diags.HasErrors() { + return diags + } + + keyData := EvalDataForNoInstanceKey + + switch { + case n.Config.Count != nil: + // If the config block has count, we'll evaluate with an unknown + // number as count.index so we can still type check even though + // we won't expand count until the plan phase. + keyData = InstanceKeyEvalData{ + CountIndex: cty.UnknownVal(cty.Number), + } + + // Basic type-checking of the count argument. More complete validation + // of this will happen when we DynamicExpand during the plan walk. + _, countDiags := evaluateCountExpressionValue(n.Config.Count, ctx) + diags = diags.Append(countDiags) + + case n.Config.ForEach != nil: + keyData = InstanceKeyEvalData{ + EachKey: cty.UnknownVal(cty.String), + EachValue: cty.UnknownVal(cty.DynamicPseudoType), + } + + // Evaluate the for_each expression here so we can expose the diagnostics + forEachDiags := newForEachEvaluator(n.Config.ForEach, ctx, false).ValidateActionValue() + diags = diags.Append(forEachDiags) + } + + schema := providerSchema.SchemaForActionType(n.Config.Type) + if schema.ConfigSchema == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid action type", + Detail: fmt.Sprintf("The provider %s does not support action type %q.", n.Provider().ForDisplay(), n.Config.Type), + Subject: &n.Config.TypeRange, + }) + return diags + } + + // We currently only support unlinked actions, so we send a diagnostic for other types + if n.Schema.Lifecycle != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Lifecycle actions are not supported", + Detail: "This version of Terraform does not support lifecycle actions", + Subject: n.Config.DeclRange.Ptr(), + }) + return diags + } + + if n.Schema.Linked != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Linked actions are not supported", + Detail: "This version of Terraform does not support linked actions", + Subject: n.Config.DeclRange.Ptr(), + }) + return diags + } + + var configVal cty.Value + var valDiags tfdiags.Diagnostics + if n.Config.Config != nil { + configVal, _, valDiags = ctx.EvaluateBlock(n.Config.Config, schema.ConfigSchema, nil, keyData) + diags = diags.Append(valDiags) + if valDiags.HasErrors() { + return diags + } + } else { + configVal = cty.NullVal(n.Schema.ConfigSchema.ImpliedType()) + } + + // Use unmarked value for validate request + unmarkedConfigVal, _ := configVal.UnmarkDeep() + log.Printf("[TRACE] Validating config for %q", n.Addr) + req := providers.ValidateActionConfigRequest{ + TypeName: n.Config.Type, + Config: unmarkedConfigVal, + } + + resp := provider.ValidateActionConfig(req) + diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) + + return diags +} diff --git a/internal/terraform/resource_provider_mock_test.go b/internal/terraform/resource_provider_mock_test.go index 020dba92a2..693d53e37d 100644 --- a/internal/terraform/resource_provider_mock_test.go +++ b/internal/terraform/resource_provider_mock_test.go @@ -128,6 +128,7 @@ type providerSchema struct { IdentityTypeSchemaVersions map[string]uint64 ListResourceTypes map[string]*configschema.Block ListResourceTypeSchemaVersions map[string]uint64 + Actions map[string]providers.ActionSchema } // getProviderSchemaResponseFromProviderSchema is a test helper to convert a @@ -139,6 +140,7 @@ func getProviderSchemaResponseFromProviderSchema(providerSchema *providerSchema) ResourceTypes: map[string]providers.Schema{}, DataSources: map[string]providers.Schema{}, ListResourceTypes: map[string]providers.Schema{}, + Actions: map[string]providers.ActionSchema{}, } for name, schema := range providerSchema.ResourceTypes { @@ -168,5 +170,9 @@ func getProviderSchemaResponseFromProviderSchema(providerSchema *providerSchema) resp.ListResourceTypes[name] = ps } + for name, schema := range providerSchema.Actions { + resp.Actions[name] = schema + } + return resp } diff --git a/internal/terraform/transform_config.go b/internal/terraform/transform_config.go index 25760b07d3..0f7b69e038 100644 --- a/internal/terraform/transform_config.go +++ b/internal/terraform/transform_config.go @@ -15,19 +15,20 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" ) -// ConfigTransformer is a GraphTransformer that adds all the resources -// from the configuration to the graph. +// ConfigTransformer is a GraphTransformer that adds all the resources and +// action declarations from the configuration to the graph. // // The module used to configure this transformer must be the root module. // -// Only resources are added to the graph. Variables, outputs, and -// providers must be added via other transforms. +// Only resources and action declarations are added to the graph. Variables, +// outputs, and providers must be added via other transforms. // -// Unlike ConfigTransformerOld, this transformer creates a graph with -// all resources including module resources, rather than creating module -// nodes that are then "flattened". +// Unlike ConfigTransformerOld, this transformer creates a graph with all +// resources including module resources, rather than creating module nodes that +// are then "flattened". type ConfigTransformer struct { - Concrete ConcreteResourceNodeFunc + Concrete ConcreteResourceNodeFunc + ConcreteAction ConcreteActionNodeFunc // Module is the module to add resources from. Config *configs.Config @@ -134,9 +135,15 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er if a != nil { addr := a.Addr().InModule(path) log.Printf("[TRACE] ConfigTransformer: Adding action %s", addr) - node := &nodeExpandActionDeclaration{ + abstract := &NodeAbstractAction{ Addr: addr, - Config: a, + Config: *a, + } + var node dag.Vertex + if f := t.ConcreteAction; f != nil { + node = f(abstract) + } else { + node = DefaultConcreteActionNodeFunc(abstract) } g.Add(node) }