Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion action/doc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

// TODO:Actions: Eventual package docs for actions
// Package action contains all interfaces, request types, and response
// types for an action implementation.
//
// In Terraform, an action is a concept which enables provider developers
// to offer practitioners ad-hoc side-effects to be used in their configuration.
// Depending on the type of action defined (unlinked, lifecycle, or linked), practitioners
// can trigger actions through the Terraform CLI and as part of the plan / apply lifecycle.
// Actions do not produce any data for practitioners to consume in their configurations, but
// can modify attributes of pre-defined managed resources (referred to as linked resources).
//
// The main starting point for implementations in this package is the
// [Action] type which represents an instance of an action that has its
// own configuration, plan, and invoke logic. The [Action] implementations
// are referenced by the [provider.ProviderWithActions] type Actions method,
// which enables the action practitioner usage.
package action
56 changes: 51 additions & 5 deletions action/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,33 @@ type InvokeRequest struct {
// Config is the configuration the user supplied for the action.
Config tfsdk.Config

// TODO:Actions: Add linked resources once lifecycle/linked actions are implemented
// LinkedResources contains the data of the managed resource types that are linked to this action.
//
// - If the action schema type is Unlinked, this field will be empty.
// - If the action schema type is Lifecycle, this field will contain a single linked resource.
// - If the action schema type is Linked, this field will be one or more linked resources, which
// will be in the same order as the linked resource schemas are defined in the action schema.
//
// For Lifecycle actions, the provider may only change computed-only attributes of the linked resources.
// For Linked actions, the provider may change any attributes of the linked resources.
LinkedResources []InvokeRequestLinkedResource
}

// InvokeRequestLinkedResource represents linked resource data before the action is invoked.
type InvokeRequestLinkedResource struct {
Copy link
Member Author

@austinvalle austinvalle Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing not represented in this request that is normally in the plan for resources, is private state.... Should that be in the protocol? Can an action also return new private state? 🤔 (same thought for InvokeAction)

// Config is the configuration the user supplied for the linked resource.
Config tfsdk.Config

// State is the current state of the linked resource.
State tfsdk.State

// Identity is the planned identity of the linked resource. If the linked resource does not
// support identity, this value will not be set.
Identity *tfsdk.ResourceIdentity

// Plan is the latest planned new state for the linked resource. This could
// be the original plan, the result of the linked resource apply, or an invoke from a predecessor action.
Plan tfsdk.Plan
}

// InvokeResponse represents a response to an InvokeRequest. An
Expand All @@ -28,13 +54,33 @@ type InvokeResponse struct {
// generated.
Diagnostics diag.Diagnostics

// SendProgress will immediately send a progress update to Terraform core during action invocation.
// This function is provided by the framework and can be called multiple times while action logic is running.
// LinkedResources contains the provider modified data of the managed resource types that are linked to this action.
//
// TODO:Actions: More documentation about when you should use this / when you shouldn't
// For Lifecycle actions, the provider may only change computed-only attributes of the linked resources.
// For Linked actions, the provider may change any attributes of the linked resources.
LinkedResources []InvokeResponseLinkedResource

// SendProgress will immediately send a progress update to Terraform core during action invocation.
// This function is pre-populated by the framework and can be called multiple times while action logic is running.
SendProgress func(event InvokeProgressEvent)
}

// InvokeResponseLinkedResource represents linked resource data that was changed during Invoke and returned.
type InvokeResponseLinkedResource struct {
// State is the state of the linked resource following the Invoke operation.
// This field is pre-populated from InvokeRequest.Plan and
// should be set during the action's Invoke operation.
State tfsdk.State

// Identity is the identity of the linked resource following the Invoke operation.
// This field is pre-populated from InvokeRequest.Identity and
// should be set during the action's Invoke operation.
Identity *tfsdk.ResourceIdentity

// TODO:Actions: Add linked resources once lifecycle/linked actions are implemented
// RequiresReplace indicates that the linked resource must be replaced as a result of an action invocation error.
// This field can only be set to true if diagnostics are returned in [InvokeResponse], otherwise Framework will append
// a provider implementation diagnostic to [InvokeResponse].
RequiresReplace bool
}

// InvokeProgressEvent is the event returned to Terraform while an action is being invoked.
Expand Down
51 changes: 49 additions & 2 deletions action/modify_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,43 @@ type ModifyPlanRequest struct {
// from knowing the value at request time.
Config tfsdk.Config

// TODO:Actions: Add linked resources once lifecycle/linked actions are implemented
// LinkedResources contains the data of the managed resource types that are linked to this action.
//
// - If the action schema type is Unlinked, this field will be empty.
// - If the action schema type is Lifecycle, this field will contain a single linked resource.
// - If the action schema type is Linked, this field will be one or more linked resources, which
// will be in the same order as the linked resource schemas are defined in the action schema.
//
// For Lifecycle actions, the provider may only change computed-only attributes of the linked resources.
// For Linked actions, the provider may change any attributes of the linked resources.
LinkedResources []ModifyPlanRequestLinkedResource

// ClientCapabilities defines optionally supported protocol features for the
// PlanAction RPC, such as forward-compatible Terraform behavior changes.
ClientCapabilities ModifyPlanClientCapabilities
}

// ModifyPlanRequestLinkedResource represents linked resource data prior to the action plan.
type ModifyPlanRequestLinkedResource struct {
// Config is the configuration the user supplied for the linked resource.
//
// This configuration may contain unknown values if a user uses
// interpolation or other functionality that would prevent Terraform
// from knowing the value at request time.
Config tfsdk.Config

// State is the current state of the linked resource.
State tfsdk.State

// Identity is the current identity of the linked resource. If the linked resource does not
// support identity, this value will not be set.
Identity *tfsdk.ResourceIdentity

// Plan is the latest planned new state for the linked resource. This could
// be the result of the linked resource plan or a plan from a predecessor action.
Plan tfsdk.Plan
}

// ModifyPlanResponse represents a response to a
// ModifyPlanRequest. An instance of this response struct is supplied
// as an argument to the action's ModifyPlan function, in which the provider
Expand All @@ -48,7 +78,11 @@ type ModifyPlanResponse struct {
// generated.
Diagnostics diag.Diagnostics

// TODO:Actions: Add linked resources once lifecycle/linked actions are implemented
// LinkedResources contains the provider modified data of the managed resource types that are linked to this action.
//
// For Lifecycle actions, the provider may only change computed-only attributes of the linked resources.
// For Linked actions, the provider may change any attributes of the linked resources.
LinkedResources []ModifyPlanResponseLinkedResource

// Deferred indicates that Terraform should defer planning this
// action until a follow-up apply operation.
Expand All @@ -60,3 +94,16 @@ type ModifyPlanResponse struct {
// to change or break without warning. It is not protected by version compatibility guarantees.
Deferred *Deferred
}

// ModifyPlanResponseLinkedResource represents linked resource data that was planned by the action.
type ModifyPlanResponseLinkedResource struct {
// Plan is the planned new state for the linked resource.
//
// For Lifecycle actions, the provider may only change computed-only attributes of the linked resources.
// For Linked actions, the provider may change any attributes of the linked resources.
Plan tfsdk.Plan

// Identity is the planned new identity of the resource.
// This field is pre-populated from ModifyPlanRequest.Identity.
Identity *tfsdk.ResourceIdentity
}
33 changes: 33 additions & 0 deletions action/schema/execution_order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package schema

const (
// ExecutionOrderInvalid is used to indicate an invalid [ExecutionOrder].
// Provider developers should not use it.
ExecutionOrderInvalid ExecutionOrder = 0

// ExecutionOrderBefore is used to indicate that the action must be invoked before it's
// linked resource's plan/apply.
ExecutionOrderBefore ExecutionOrder = 1

// ExecutionOrderAfter is used to indicate that the action must be invoked after it's
// linked resource's plan/apply.
ExecutionOrderAfter ExecutionOrder = 2
)

// ExecutionOrder is an enum that represents when an action is invoked relative to it's linked resource.
type ExecutionOrder int32

func (d ExecutionOrder) String() string {
switch d {
case 0:
return "Invalid"
case 1:
return "Before"
case 2:
return "After"
}
return "Unknown"
}
187 changes: 187 additions & 0 deletions action/schema/lifecycle_schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package schema

import (
"context"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

var _ SchemaType = LifecycleSchema{}

// LifecycleSchema defines the structure and value types of a lifecycle action. A lifecycle action
// can cause changes to exactly one resource state, defined as a linked resource.
type LifecycleSchema struct {
// ExecutionOrder defines when the lifecycle action must be executed in relation to the linked resource,
// either before or after the linked resource's plan/apply.
ExecutionOrder ExecutionOrder

// LinkedResource represents the managed resource type that this action can make state changes to. The linked
// resource must be defined in the same provider as the action is defined.
//
// - If the managed resource is built with terraform-plugin-framework, use [LinkedResource].
// - If the managed resource is built with terraform-plugin-sdk/v2 or the terraform-plugin-go tfprotov5 package, use [RawV5LinkedResource].
// - If the managed resource is built with the terraform-plugin-go tfprotov6 package, use [RawV6LinkedResource].
//
// As a lifecycle action can only have a single linked resource, this linked resource data will always be at index 0
// in the ModifyPlan and Invoke LinkedResources slice.
LinkedResource LinkedResourceType

// Attributes is the mapping of underlying attribute names to attribute
// definitions.
//
// Names must only contain lowercase letters, numbers, and underscores.
// Names must not collide with any Blocks names.
Attributes map[string]Attribute

// Blocks is the mapping of underlying block names to block definitions.
//
// Names must only contain lowercase letters, numbers, and underscores.
// Names must not collide with any Attributes names.
Blocks map[string]Block

// Description is used in various tooling, like the language server, to
// give practitioners more information about what this action is,
// what it's for, and how it should be used. It should be written as
// plain text, with no special formatting.
Description string

// MarkdownDescription is used in various tooling, like the
// documentation generator, to give practitioners more information
// about what this action is, what it's for, and how it should be
// used. It should be formatted using Markdown.
MarkdownDescription string

// DeprecationMessage defines warning diagnostic details to display when
// practitioner configurations use this action. The warning diagnostic
// summary is automatically set to "Action Deprecated" along with
// configuration source file and line information.
//
// Set this field to a practitioner actionable message such as:
//
// - "Use examplecloud_do_thing action instead. This action
// will be removed in the next major version of the provider."
// - "Remove this action as it no longer is valid and
// will be removed in the next major version of the provider."
//
DeprecationMessage string
}

func (s LifecycleSchema) LinkedResourceTypes() []LinkedResourceType {
return []LinkedResourceType{
s.LinkedResource,
}
}

func (s LifecycleSchema) isActionSchemaType() {}

// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the
// schema.
func (s LifecycleSchema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) {
return fwschema.SchemaApplyTerraform5AttributePathStep(s, step)
}

// AttributeAtPath returns the Attribute at the passed path. If the path points
// to an element or attribute of a complex type, rather than to an Attribute,
// it will return an ErrPathInsideAtomicAttribute error.
func (s LifecycleSchema) AttributeAtPath(ctx context.Context, p path.Path) (fwschema.Attribute, diag.Diagnostics) {
return fwschema.SchemaAttributeAtPath(ctx, s, p)
}

// AttributeAtPath returns the Attribute at the passed path. If the path points
// to an element or attribute of a complex type, rather than to an Attribute,
// it will return an ErrPathInsideAtomicAttribute error.
func (s LifecycleSchema) AttributeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) {
return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p)
}

// GetAttributes returns the Attributes field value.
func (s LifecycleSchema) GetAttributes() map[string]fwschema.Attribute {
return schemaAttributes(s.Attributes)
}

// GetBlocks returns the Blocks field value.
func (s LifecycleSchema) GetBlocks() map[string]fwschema.Block {
return schemaBlocks(s.Blocks)
}

// GetDeprecationMessage returns the DeprecationMessage field value.
func (s LifecycleSchema) GetDeprecationMessage() string {
return s.DeprecationMessage
}

// GetDescription returns the Description field value.
func (s LifecycleSchema) GetDescription() string {
return s.Description
}

// GetMarkdownDescription returns the MarkdownDescription field value.
func (s LifecycleSchema) GetMarkdownDescription() string {
return s.MarkdownDescription
}

// GetVersion always returns 0 as action schemas cannot be versioned.
func (s LifecycleSchema) GetVersion() int64 {
return 0
}

// Type returns the framework type of the schema.
func (s LifecycleSchema) Type() attr.Type {
return fwschema.SchemaType(s)
}

// TypeAtPath returns the framework type at the given schema path.
func (s LifecycleSchema) TypeAtPath(ctx context.Context, p path.Path) (attr.Type, diag.Diagnostics) {
return fwschema.SchemaTypeAtPath(ctx, s, p)
}

// TypeAtTerraformPath returns the framework type at the given tftypes path.
func (s LifecycleSchema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) {
return fwschema.SchemaTypeAtTerraformPath(ctx, s, p)
}

// ValidateImplementation contains logic for validating the provider-defined
// implementation of the schema and underlying attributes and blocks to prevent
// unexpected errors or panics. This logic runs during the GetProviderSchema RPC,
// or via provider-defined unit testing, and should never include false positives.
func (s LifecycleSchema) ValidateImplementation(ctx context.Context) diag.Diagnostics {
var diags diag.Diagnostics

// TODO:Actions: Implement validation to ensure valid lifecycle "execute" enum and linked resource definitions

for attributeName, attribute := range s.GetAttributes() {
req := fwschema.ValidateImplementationRequest{
Name: attributeName,
Path: path.Root(attributeName),
}

// TODO:Actions: We should confirm with core, but we should be able to remove this next line.
//
// Action schemas define a specific "config" nested block in the action block, which means there
// shouldn't be any conflict with existing or future Terraform core attributes.
diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...)
diags.Append(fwschema.ValidateAttributeImplementation(ctx, attribute, req)...)
}

for blockName, block := range s.GetBlocks() {
req := fwschema.ValidateImplementationRequest{
Name: blockName,
Path: path.Root(blockName),
}

// TODO:Actions: We should confirm with core, but we should be able to remove this next line.
//
// Action schemas define a specific "config" nested block in the action block, which means there
// shouldn't be any conflict with existing or future Terraform core attributes.
diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...)
diags.Append(fwschema.ValidateBlockImplementation(ctx, block, req)...)
}

return diags
}
Loading
Loading