tools/agents/skills/tf-resource-migration/SKILL.md
Migrate existing Terraform resources from SDK provider (sdkprovider) to Plugin Framework. Use when converting legacy resources, ensuring state compatibility, or when the user asks to port/migrate existing resources.
npx skillsauth add aiven/terraform-provider-aiven tf-resource-migrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Migrate existing SDK-based resources to Plugin Framework while maintaining state compatibility and behavior parity.
Always drive generation, formatting, and linting through Task:
task generate — generates code and runs task fmt automatically at the endtask fmt — formats Go, Terraform, and whitespacetask lint — runs all lintersDo NOT use go run ./generators/..., go generate, gofmt, goimports, golangci-lint, or make for these workflows. See AGENTS.md and Taskfile.dist.yml (run task --list) for the full command surface.
This skill guides migration of resources from:
internal/sdkprovider/ (terraform-plugin-sdk/v2)internal/plugin/ (terraform-plugin-framework) with YAML-generated codeKey Challenge: Preserve exact behavior and state compatibility so users don't experience breaking changes.
Prerequisites: This skill builds on tf-resource-generator. For YAML syntax, adapter API, modifier patterns, custom view overrides, write-only fields, and all implementation details, see that skill.
Use this skill when:
aiven_* resources from SDK to Plugin FrameworkUse tf-resource-generator skill when:
Find the SDK resource:
# Find resource file
find internal/sdkprovider -name "*resource_*.go" | grep -i "resource_name"
# Find data source file
find internal/sdkprovider -name "*datasource_*.go" | grep -i "resource_name"
Read and document:
Check what API operations the SDK resource uses:
# Search for API client calls in the resource
grep -A 5 "client\." internal/sdkprovider/service/resource_name.go
# IMPORTANT: Also check the data source — it may use a different API operation
grep -A 5 "client\." internal/sdkprovider/service/resource_name_data_source.go
Then find corresponding OpenAPI operation IDs. See tf-resource-generator for OpenAPI search patterns.
Determine clientHandler: The clientHandler YAML value is the Go package name under github.com/aiven/go-client-codegen/handler/. Find it by searching for the operation ID in the module cache:
grep -r "OperationID" $(go env GOMODCACHE)/github.com/aiven/go-client-codegen*/handler/
For example, ServiceFlinkCreateApplication lives in handler/flinkapplication/, so clientHandler: flinkapplication.
| SDK Type | YAML Type |
|----------|-----------|
| schema.TypeString | type: string |
| schema.TypeInt | type: integer |
| schema.TypeFloat | type: number |
| schema.TypeBool | type: boolean |
| schema.TypeList | type: arrayOrdered |
| schema.TypeSet | type: array (or arrayOrdered for performance) |
| schema.TypeMap | additionalProperties: {type: string} |
| SDK Attribute | YAML Attribute |
|---------------|----------------|
| Required: true | required: true |
| Optional: true | optional: true |
| Computed: true | computed: true |
| Sensitive: true | sensitive: true |
| ForceNew: true | forceNew: true |
| ConflictsWith: [] | conflictsWith: [] |
| ExactlyOneOf: [] | exactlyOneOf: [] |
SDK Set of objects:
"tags": {
Type: schema.TypeSet,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"key": {Type: schema.TypeString},
"value": {Type: schema.TypeString},
},
},
}
YAML (use arrayOrdered for performance):
schema:
tags:
type: arrayOrdered
items:
type: object
properties:
key:
type: string
value:
type: string
CRITICAL: The ID format MUST stay the same for state compatibility.
Find the ID format in SDK code:
# Look for ResourceData.SetId calls
grep -A 2 "SetId" internal/sdkprovider/service/resource_name.go
# Look for ID builder functions
grep -B 5 "buildResourceID\|parseResourceID" internal/sdkprovider/service/resource_name.go
Common ID patterns:
projectproject/service_name/database_nameSet in YAML:
idAttributeComposed: [project, service_name, database_name]
Identify in SDK code and map to generator features:
| SDK Pattern | Generator Feature |
|-------------|------------------|
| StateUpgraders | May need version in YAML + state upgrader |
| CustomizeDiff | planModifier: true or ModifyPlan |
| Flatten/Expand functions | expandModifier: true / flattenModifier: true |
| DiffSuppressFunc | planModifier: true |
| Multiple API calls in Update | Custom updateView via init() override |
| Complex delete (cancel + delete) | Custom deleteView via init() override |
| Data source looks up by alt key (e.g. name) | A second read op with datasourceLookup: true + resultListLookupKeys; add resultIDField: <GoField> when the lookup endpoint differs in shape from the canonical read and should only resolve the id |
| Sensitive field not stored in state | writeOnly: true |
For implementation details of each, see tf-resource-generator skill.
Create definitions/aiven_resource_name.yml. For complete YAML syntax reference, see tf-resource-generator skill.
IMPORTANT: Definition files MUST have the aiven_ prefix. The filename becomes the resource name directly.
Focus on migration-specific concerns:
Sensitive field as sensitive: true in YAML (no regressions)task generate
task build
task lint
CRITICAL: Ensure state is compatible between SDK and Plugin Framework versions.
Check schema version in SDK:
grep -A 3 "SchemaVersion" internal/sdkprovider/service/resource_name.go
If SDK has SchemaVersion > 0, you MUST handle state upgrades.
CRITICAL: Test that existing state from SDK version works with Plugin Framework version.
Use acc.BackwardCompatibilitySteps() helper:
func TestAccAivenResource_backwardCompat(t *testing.T) {
resourceName := "aiven_resource_name.test"
projectName := acc.ProjectName()
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acc.TestAccPreCheck(t) },
Steps: acc.BackwardCompatibilitySteps(t, acc.BackwardCompatConfig{
TFConfig: testAccResourceConfig(projectName),
OldProviderVersion: "4.47.0", // Check CHANGELOG.md for latest
Checks: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "project", projectName),
// Add all key attribute checks
),
}),
})
}
Find the latest version:
head -20 CHANGELOG.md
What this test does:
Examples:
internal/plugin/service/mysql/database/database_test.go - Basic backward compatibilityinternal/plugin/service/pg/user/user_test.go - Complex resource with custom update logicCRITICAL: Verify behavior matches SDK resource exactly.
Find SDK tests:
ls internal/sdkprovider/service/*resource_name*_test.go
Ensure Plugin Framework tests cover:
Update (mutable fields that are not all ForceNew), acceptance tests MUST include a Terraform apply that changes at least one updatable attribute after create — i.e. an explicit update step in the test sequence, not only create + destroy. Skip this only when the resource is create/delete-only or every mutable change forces replacement.Before marking migration complete:
sensitive: true in YAMLacc.BackwardCompatibilitySteps()CHANGELOG.md| Issue | Solution |
|-------|----------|
| Sensitive field no longer marked sensitive | Set sensitive: true on the attribute (and nested fields if applicable); match SDK Sensitive: true exactly |
| ID format changed accidentally | Verify idAttributeComposed matches SDK's ID builder |
| Set ordering causes diffs | Use arrayOrdered instead of array |
| Computed field becomes required | Keep as computed: true if API provides it |
| Custom validation lost | Implement in custom modifier or use schema validation |
| State upgrade needed | Implement state upgrader in Plugin Framework |
| DiffSuppressFunc behavior | Use planModifier: true for custom diff logic |
| Renamed ID field missing in old state | Use planModifier: true to extract from composite ID |
| Read fails with 404 after migration | Likely a renamed ID field is empty — use planModifier |
| "was null, but now cty.X" error | Add computed: true + useStateForUnknown: true (see generator skill) |
| Field nested differently in API | Use expandModifier and flattenModifier (see generator skill) |
| Multiple update operations needed | Override updateView via init() (see generator skill) |
| Data source lookup key differs | Add a second read operation with datasourceLookup: true + resultListLookupKeys. If the lookup endpoint returns a different shape than the canonical read (e.g. a directory of ids), also set resultIDField: <GoField> (see generator skill) |
# Find SDK resource
find internal/sdkprovider -name "*resource_*.go" | grep -i "name"
# Analyze SDK schema
grep -A 20 "Schema:" internal/sdkprovider/service/resource.go
# Find SDK ID format
grep -A 2 "SetId" internal/sdkprovider/service/resource.go
# Compare implementations
diff internal/sdkprovider/service/resource.go internal/plugin/service/resource/zz_resource.go
# Run backward compatibility test
task test-acc -- -run TestAccAivenResource_backwardCompat
Once all tests pass and state compatibility is verified:
internal/sdkprovider/ and remove provider registrationCHANGELOG.md under the unreleased section:
- Migrate `aiven_resource_name` to the Plugin Framework
- Change `aiven_resource_name`: deprecate `termination_protection` field. Instead, use [prevent_destroy](https://developer.hashicorp.com/terraform/tutorials/state/resource-lifecycle#prevent-resource-deletion)
Do not maintain both versions - this creates maintenance burden and user confusion.
Sensitive in SDK must stay sensitive in Plugin Framework (sensitive: true); never expose secrets in plan or state output by omissiontools
Generate new Terraform resources and data sources from YAML definitions and OpenAPI specs. Use when adding resources, creating YAML definitions, working with the Plugin Framework generator, or when the user asks about code generation.
tools
Use when work should span one or more detached tasks but still behave like one job with a single owner context. TaskFlow is the durable flow substrate under authoring layers like Lobster, ACPX, plugins, or plain code. Keep conditional logic in the caller; use TaskFlow for flow identity, child-task linkage, waiting state, revision-checked mutations, and user-facing emergence.
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------
tools
# Lobster Lobster executes multi-step workflows with approval checkpoints. Use it when: - User wants a repeatable automation (triage, monitor, sync) - Actions need human approval before executing (send, post, delete) - Multiple tool calls should run as one deterministic operation ## When to use Lobster | User intent | Use Lobster? | | ------------------------------------------------------ | --------------------------