Loading...
Loading...
Implement Terraform Provider actions using the Plugin Framework. Use when developing imperative operations that execute at lifecycle events (before/after create, update, destroy).
npx skill4agent add hashicorp/agent-skills provider-actionsinternal/service/<service>/
├── <action_name>_action.go # Action implementation
├── <action_name>_action_test.go # Action tests
└── service_package_gen.go # Auto-generated service registrationwebsite/docs/actions/
└── <service>_<action_name>.html.markdown # User-facing documentation.changelog/
└── <pr_number_or_description>.txt # Release note entryfunc (a *actionType) Schema(ctx context.Context, req action.SchemaRequest, resp *action.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
// Required configuration parameters
"resource_id": schema.StringAttribute{
Required: true,
Description: "ID of the resource to operate on",
},
// Optional parameters with defaults
"timeout": schema.Int64Attribute{
Optional: true,
Description: "Operation timeout in seconds",
Default: int64default.StaticInt64(1800),
Computed: true,
},
},
}
}types.Stringfwtypes.Stringtypes.StringTypefwtypes.StringType// WRONG - missing ElementType
"items": schema.ListAttribute{
Optional: true,
}
// CORRECT
"items": schema.ListAttribute{
Optional: true,
ElementType: fwtypes.StringType,
}Optional: trueComputed: trueComputed// Ensure proper imports
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"go buildfunc (a *actionType) Invoke(ctx context.Context, req action.InvokeRequest, resp *action.InvokeResponse) {
var data actionModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
// Create provider client
conn := a.Meta().Client(ctx)
// Progress updates for long-running operations
resp.Progress.Set(ctx, "Starting operation...")
// Implement action logic with error handling
// Use context for timeout management
// Poll for completion if async operation
resp.Progress.Set(ctx, "Operation completed")
}resp.SendProgress(action.InvokeProgressEvent{...})context.WithTimeout()resp.Diagnostics.AddError()// Handle specific errors
var notFound *types.ResourceNotFoundException
if errors.As(err, ¬Found) {
resp.Diagnostics.AddError(
"Resource Not Found",
fmt.Sprintf("Resource %s was not found", resourceID),
)
return
}
// Generic error handling
resp.Diagnostics.AddError(
"Operation Failed",
fmt.Sprintf("Could not complete operation for %s: %s", resourceID, err),
)a.Meta().<Service>Client(ctx)result, err := wait.WaitForStatus(ctx,
func(ctx context.Context) (wait.FetchResult[*ResourceType], error) {
// Fetch current status
resource, err := findResource(ctx, conn, id)
if err != nil {
return wait.FetchResult[*ResourceType]{}, err
}
return wait.FetchResult[*ResourceType]{
Status: wait.Status(resource.Status),
Value: resource,
}, nil
},
wait.Options[*ResourceType]{
Timeout: timeout,
Interval: wait.FixedInterval(5 * time.Second),
SuccessStates: []wait.Status{"AVAILABLE", "COMPLETED"},
TransitionalStates: []wait.Status{"CREATING", "PENDING"},
ProgressInterval: 30 * time.Second,
ProgressSink: func(fr wait.FetchResult[any], meta wait.ProgressMeta) {
resp.SendProgress(action.InvokeProgressEvent{
Message: fmt.Sprintf("Status: %s, Elapsed: %v", fr.Status, meta.Elapsed.Round(time.Second)),
})
},
},
)action_triggeraction "provider_service_action" "name" {
config {
parameter = value
}
}
resource "terraform_data" "trigger" {
lifecycle {
action_trigger {
events = [after_create]
actions = [action.provider_service_action.name]
}
}
}before_createafter_createbefore_updateafter_updatebefore_destroyafter_destroyfunc TestAccServiceAction_basic(t *testing.T) {
ctx := acctest.Context(t)
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(ctx, t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_14_0),
},
Steps: []resource.TestStep{
{
Config: testAccActionConfig_basic(),
Check: resource.ComposeTestCheckFunc(
testAccCheckResourceExists(ctx, "provider_resource.test"),
),
},
},
})
}func sweepResources(region string) error {
ctx := context.Background()
client := /* get client for region */
input := &service.ListInput{
// Filter for test resources
}
var sweeperErrs *multierror.Error
pages := service.NewListPaginator(client, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)
if err != nil {
sweeperErrs = multierror.Append(sweeperErrs, err)
continue
}
for _, item := range page.Items {
id := item.Id
// Skip non-test resources
if !strings.HasPrefix(id, "tf-acc-test") {
continue
}
_, err := client.Delete(ctx, &service.DeleteInput{
Id: id,
})
if err != nil {
sweeperErrs = multierror.Append(sweeperErrs, err)
}
}
}
return sweeperErrs.ErrorOrNil()
}regexache.MustCompile(\go test -c -o /dev/null ./internal/service/<service>TF_ACC=1 go test ./internal/service/<service> -run TestAccServiceAction_ -vTF_ACC=1 go test ./internal/service/<service> -sweep=<region> -v---
subcategory: "Service Name"
layout: "provider"
page_title: "Provider: provider_service_action"
description: |-
Brief description of what the action does.
---terraform_dataterrafmt fmtterrafmt diff.changelog/.changelog/<pr_number_or_description>.txtaction/provider_service_action: Brief description of the actiongo build -o /dev/null .go test -c -o /dev/null ./internal/service/<service>make fmtterrafmt fmt website/docs/actions/<action>.html.markdown