Plugin-framework provider covering projects, environments, applications, Compose stacks, managed databases, domains, mounts, ports, redirects, basic auth, registries, SSH keys, certificates and backup destinations, over Dokploy's tRPC-over-REST API. The shim package exposes the provider to other Go modules, which is how pulumi-dokploy bridges it.
530 lines
16 KiB
Markdown
530 lines
16 KiB
Markdown
---
|
|
name: provider-actions
|
|
description: Implement Terraform Provider actions using the Plugin Framework. Use when developing imperative operations that execute at lifecycle events (before/after create, update, destroy).
|
|
metadata:
|
|
copyright: Copyright IBM Corp. 2026
|
|
version: "0.0.1"
|
|
---
|
|
|
|
# Terraform Provider Actions Implementation Guide
|
|
|
|
## Overview
|
|
|
|
Terraform Actions enable imperative operations during the Terraform lifecycle. Actions are experimental features that allow performing provider operations at specific lifecycle events (before/after create, update, destroy).
|
|
|
|
**References:**
|
|
- [Terraform Plugin Framework](https://developer.hashicorp.com/terraform/plugin/framework)
|
|
- [Terraform Actions RFC](https://github.com/hashicorp/terraform/blob/main/docs/plugin-protocol/actions.md)
|
|
|
|
## First Action Setup
|
|
|
|
When adding the first action to a provider that has never had one, several one-time scaffolding steps are required:
|
|
|
|
1. **Implement `ProviderWithActions`** — add an `Actions()` method to the provider that returns `[]func() action.Action`.
|
|
2. **Set `ActionData` in `Configure`** — the provider's `Configure` method must set `resp.ActionData = v` alongside the existing `ResourceData`, `DataSourceData`, and `EphemeralResourceData` assignments.
|
|
3. **Create `ActionWithConfigure` base type** — if the provider uses embedded base types (e.g. `ResourceWithConfigure`), create an equivalent `ActionWithConfigure` type implementing `action.ConfigureRequest` / `action.ConfigureResponse`.
|
|
4. **Action-schema helper variants** — if the provider injects common schema attributes (e.g. `namespace`) via helper functions, action-schema variants are needed since `action/schema` types differ from `resource/schema` types.
|
|
|
|
## File Structure
|
|
|
|
Actions follow the standard service package structure:
|
|
|
|
```
|
|
internal/service/<service>/
|
|
├── <action_name>_action.go # Action implementation
|
|
├── <action_name>_action_test.go # Action tests
|
|
└── service_package_gen.go # Auto-generated service registration
|
|
```
|
|
|
|
Documentation structure:
|
|
```
|
|
website/docs/actions/
|
|
└── <service>_<action_name>.html.markdown # User-facing documentation
|
|
```
|
|
|
|
Changelog entry:
|
|
```
|
|
.changelog/
|
|
└── <pr_number_or_description>.txt # Release note entry
|
|
```
|
|
|
|
## Action Schema Definition
|
|
|
|
Actions use the Terraform Plugin Framework with a standard schema pattern:
|
|
|
|
```go
|
|
func (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,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
```
|
|
|
|
### Common Schema Issues
|
|
|
|
**Pay special attention to the schema definition** - common issues after a first draft:
|
|
|
|
1. **Type Mismatches**
|
|
- Using `types.String` instead of `fwtypes.String` in model structs
|
|
- Using `types.StringType` instead of `fwtypes.StringType` in schema
|
|
- Mixing framework types with plugin-framework types
|
|
|
|
2. **List/Map Element Types**
|
|
```go
|
|
// WRONG - missing ElementType
|
|
"items": schema.ListAttribute{
|
|
Optional: true,
|
|
}
|
|
|
|
// CORRECT
|
|
"items": schema.ListAttribute{
|
|
Optional: true,
|
|
ElementType: fwtypes.StringType,
|
|
}
|
|
```
|
|
|
|
3. **Computed vs Optional**
|
|
- Attributes with defaults must be both `Optional: true` and `Computed: true`
|
|
- Don't mark action inputs as `Computed` unless they have defaults
|
|
|
|
4. **Validator Imports**
|
|
```go
|
|
// Ensure proper imports
|
|
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
|
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
|
```
|
|
|
|
5. **Region/Provider Attribute**
|
|
- Use framework-provided region handling when available
|
|
- Don't manually define provider-specific config in schema if framework handles it
|
|
|
|
6. **Nested Attributes**
|
|
- Use appropriate nested object types for complex structures
|
|
- Ensure nested types are properly defined
|
|
|
|
### Schema Validation Checklist
|
|
|
|
Before submitting, verify:
|
|
- [ ] All attributes have descriptions
|
|
- [ ] List/Map attributes have ElementType defined
|
|
- [ ] Validators are imported and applied correctly
|
|
- [ ] Model struct uses correct framework types
|
|
- [ ] Optional attributes with defaults are marked Computed
|
|
- [ ] Code compiles without type errors
|
|
- [ ] Run `go build` to catch type mismatches
|
|
|
|
## Action Invoke Method
|
|
|
|
The Invoke method contains the action logic:
|
|
|
|
```go
|
|
func (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")
|
|
}
|
|
```
|
|
|
|
## Key Implementation Requirements
|
|
|
|
### 1. Progress Reporting
|
|
|
|
- Use `resp.SendProgress(action.InvokeProgressEvent{...})` for real-time updates
|
|
- Provide meaningful progress messages during long operations
|
|
- Update progress at key milestones
|
|
- Include elapsed time for long operations
|
|
|
|
### 2. Timeout Management
|
|
|
|
- Always include configurable timeout parameter (default: 1800s)
|
|
- Use `context.WithTimeout()` for API calls
|
|
- Handle timeout errors gracefully
|
|
- Validate timeout ranges (typically 60-7200 seconds)
|
|
|
|
### 3. Error Handling
|
|
|
|
- Add diagnostics with `resp.Diagnostics.AddError()`
|
|
- Provide clear error messages with context
|
|
- Include API error details when relevant
|
|
- Map provider error types to user-friendly messages
|
|
- Document all possible error cases
|
|
|
|
Example error handling:
|
|
```go
|
|
// 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),
|
|
)
|
|
```
|
|
|
|
### 4. Provider SDK Integration
|
|
|
|
- Use provider SDK clients from `a.Meta().<Service>Client(ctx)`
|
|
- Handle pagination for list operations
|
|
- Implement retry logic for transient failures
|
|
- Use appropriate error types
|
|
|
|
### 5. Parameter Validation
|
|
|
|
- Use framework validators for input validation
|
|
- Validate resource existence before operations
|
|
- Check for conflicting parameters
|
|
- Validate against provider naming requirements
|
|
|
|
### 6. Polling and Waiting
|
|
|
|
For operations that require waiting for completion:
|
|
|
|
```go
|
|
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)),
|
|
})
|
|
},
|
|
},
|
|
)
|
|
```
|
|
|
|
## Common Action Patterns
|
|
|
|
### Batch Operations
|
|
- Process items in configurable batches
|
|
- Report progress per batch
|
|
- Handle partial failures gracefully
|
|
- Support prefix/filter parameters
|
|
|
|
### Command Execution
|
|
- Submit command and get operation ID
|
|
- Poll for completion status
|
|
- Retrieve and report output
|
|
- Handle timeout during polling
|
|
- Validate resources exist before execution
|
|
|
|
### Service Invocation
|
|
- Invoke service with parameters
|
|
- Wait for completion (if synchronous)
|
|
- Return output/results
|
|
- Handle service-specific errors
|
|
|
|
### Resource State Changes
|
|
- Validate current state
|
|
- Apply state change
|
|
- Poll for target state
|
|
- Handle transitional states
|
|
|
|
### Async Job Submission
|
|
- Submit job with configuration
|
|
- Get job ID
|
|
- Optionally wait for completion
|
|
- Report job status
|
|
|
|
## Action Triggers
|
|
|
|
Actions are invoked via `action_trigger` lifecycle blocks in Terraform configurations. A standalone `action` block without a corresponding trigger is declared but never executed.
|
|
|
|
### HCL Syntax
|
|
|
|
Action parameters must be wrapped in a `config {}` block. Trigger references use the `action.` prefix, and `actions` is a list. Events are bare identifiers, not quoted strings.
|
|
|
|
```hcl
|
|
action "provider_service_action" "name" {
|
|
config {
|
|
parameter = value
|
|
}
|
|
}
|
|
|
|
resource "terraform_data" "trigger" {
|
|
lifecycle {
|
|
action_trigger {
|
|
events = [after_create]
|
|
actions = [action.provider_service_action.name]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Available Trigger Events
|
|
|
|
**Terraform 1.14.0 Supported Events:**
|
|
- `before_create` - Before resource creation
|
|
- `after_create` - After resource creation
|
|
- `before_update` - Before resource update
|
|
- `after_update` - After resource update
|
|
|
|
**Not Supported in Terraform 1.14.0:**
|
|
- `before_destroy` - Not available (will cause validation error)
|
|
- `after_destroy` - Not available (will cause validation error)
|
|
|
|
## Testing Actions
|
|
|
|
### Acceptance Tests
|
|
|
|
- Test action invocation with valid parameters
|
|
- Test timeout scenarios
|
|
- Test error conditions
|
|
- Verify provider state changes
|
|
- Test progress reporting
|
|
- Test with custom parameters
|
|
- Test trigger-based invocation
|
|
|
|
### Test Pattern
|
|
|
|
```go
|
|
func 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"),
|
|
),
|
|
},
|
|
},
|
|
})
|
|
}
|
|
```
|
|
|
|
### Test Cleanup with Sweep Functions
|
|
|
|
Add sweep functions to clean up test resources:
|
|
|
|
```go
|
|
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()
|
|
}
|
|
```
|
|
|
|
### Using `terraform_data` as a No-Op Trigger
|
|
|
|
`terraform_data` can serve as a no-op trigger resource for action tests that don't need real infrastructure. This is valuable for error-case and validation tests:
|
|
|
|
```hcl
|
|
resource "terraform_data" "trigger" {
|
|
lifecycle {
|
|
action_trigger {
|
|
events = [after_create]
|
|
actions = [action.provider_service_action.test]
|
|
}
|
|
}
|
|
}
|
|
|
|
action "provider_service_action" "test" {
|
|
config {
|
|
param = "invalid-value"
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using `PostApplyFunc` to Verify Side Effects
|
|
|
|
Actions don't produce state that can be checked with `resource.TestCheckResourceAttr`. Use `PostApplyFunc` on `resource.TestStep` to query the API after apply and confirm the action produced the expected side effect:
|
|
|
|
```go
|
|
Steps: []resource.TestStep{
|
|
{
|
|
Config: testConfig,
|
|
PostApplyFunc: func() {
|
|
// query the API to verify the action's side effect occurred
|
|
},
|
|
},
|
|
},
|
|
```
|
|
|
|
### Testing Best Practices
|
|
|
|
**Service-Specific Prerequisites**
|
|
- Always check for service-specific prerequisites that must be met before actions can succeed
|
|
- Document prerequisites in action documentation and test configurations
|
|
|
|
**Error Pattern Matching**
|
|
- Terraform wraps action errors with additional context
|
|
- Use flexible regex patterns: `regexache.MustCompile(\`(?s)Error Title.*key phrase\`)`
|
|
|
|
**Test Patterns Not Applicable to Actions**
|
|
1. Actions trigger on lifecycle events, not config reapplication
|
|
2. Before/After Destroy Tests: Not supported in Terraform 1.14.0
|
|
|
|
### Running Tests
|
|
|
|
Compile test to check for errors:
|
|
```bash
|
|
go test -c -o /dev/null ./internal/service/<service>
|
|
```
|
|
|
|
Run specific action tests:
|
|
```bash
|
|
TF_ACC=1 go test ./internal/service/<service> -run TestAccServiceAction_ -v
|
|
```
|
|
|
|
Run sweep to clean up test resources:
|
|
```bash
|
|
TF_ACC=1 go test ./internal/service/<service> -sweep=<region> -v
|
|
```
|
|
|
|
## Documentation Standards
|
|
|
|
Each action documentation file must include:
|
|
|
|
1. **Front Matter**
|
|
```yaml
|
|
---
|
|
subcategory: "Service Name"
|
|
layout: "provider"
|
|
page_title: "Provider: provider_service_action"
|
|
description: |-
|
|
Brief description of what the action does.
|
|
---
|
|
```
|
|
|
|
2. **Header with Warnings**
|
|
- Beta/Alpha notice about experimental status
|
|
- Warning about potential unintended consequences
|
|
- Link to provider documentation
|
|
|
|
3. **Example Usage**
|
|
- Basic usage example
|
|
- Advanced usage with all options
|
|
- Trigger-based example with `terraform_data`
|
|
- Real-world use case examples
|
|
|
|
4. **Argument Reference**
|
|
- List all required and optional arguments
|
|
- Include descriptions and defaults
|
|
- Note any validation rules
|
|
|
|
5. **Documentation Linting**
|
|
- Run `terrafmt fmt` before submission
|
|
- Verify with `terrafmt diff`
|
|
|
|
## Changelog Entry Format
|
|
|
|
Create a changelog entry in `.changelog/` directory:
|
|
|
|
```
|
|
.changelog/<pr_number_or_description>.txt
|
|
```
|
|
|
|
Content format:
|
|
```release-note:new-action
|
|
action/provider_service_action: Brief description of the action
|
|
```
|
|
|
|
## Pre-Submission Checklist
|
|
|
|
Before submitting your action implementation:
|
|
|
|
- [ ] Code compiles: `go build -o /dev/null .`
|
|
- [ ] Tests compile: `go test -c -o /dev/null ./internal/service/<service>`
|
|
- [ ] Code formatted: `make fmt`
|
|
- [ ] Documentation formatted: `terrafmt fmt website/docs/actions/<action>.html.markdown`
|
|
- [ ] Changelog entry created
|
|
- [ ] Schema uses correct types
|
|
- [ ] All List/Map attributes have ElementType
|
|
- [ ] Progress updates implemented for long operations
|
|
- [ ] Error messages include context and resource identifiers
|
|
- [ ] Documentation includes multiple examples
|
|
- [ ] Documentation includes prerequisites and warnings
|
|
|
|
## References
|
|
|
|
- [Terraform Plugin Framework Documentation](https://developer.hashicorp.com/terraform/plugin/framework)
|
|
- [Terraform Provider Development](https://developer.hashicorp.com/terraform/plugin)
|
|
- [terraform-plugin-framework GitHub](https://github.com/hashicorp/terraform-plugin-framework)
|
|
- [terraform-plugin-testing](https://github.com/hashicorp/terraform-plugin-testing)
|
|
- [Writing a Terraform Action (blog)](https://danielmschmidt.de/posts/2025-09-26-writing-a-terraform-action/)
|
|
- Reference implementations: `terraform-provider-tfe` (`action_query_run.go`, `action_query_run_test.go`), `terraform-provider-vault` (`action_rotate_root.go`)
|