Terraform Provider Development
Build provider behavior that is correct under Terraform's plan/apply model, not just Go code that compiles. Most serious bugs are state-model bugs: perpetual diffs, duplicate creates, orphaned remote objects, secrets in state, and "provider produced inconsistent result after apply."
Starting Decisions
- Use the Terraform Plugin Framework patterns already in this repository.
- Keep API wrapper code in
internal/client/**separate from provider framework code ininternal/provider/**. - Treat
internal/api/go/**as a copied/generated API subset. Do not hand-edit it for style-only changes. - Keep docs and examples in sync with schemas by running
make generatewhen relevant.
Mental Model
- Terraform operations move three documents: Config, Plan, and State. Use Plan in Create/Update, prior State in Read/Delete, and always write observed truth back to State.
- Values can be known, null, or unknown. Do not collapse unknown into null or a Go zero value.
- Each resource method handles one resource instance. Terraform Core already resolved graph references into known or unknown values.
- Idempotency depends on truthful state. Persist remote IDs as soon as create succeeds, and make Read remove state only after a definitive not-found for the exact remote object.
Workflow
Load the relevant reference before editing provider behavior. Use:
references/concepts-and-lifecycle.mdbefore changing lifecycle code or reasoning about Config, Plan, State, known, null, or unknown values.references/core-implementation.mdwhen implementing provider, resource, data source, import, diagnostics, validators, or plan-modifier behavior.references/advanced-primitives.mdwhen considering ephemeral resources, provider-defined functions, write-only arguments, resource identity, actions, or list resources.references/secrets-and-sensitive-data.mdbefore adding or changing token, key, credential, or secret handling.references/naming-conventions.mdwhen adding or renaming provider types, data sources, resource files, or model structs.references/testing.mdbefore changing tests or acceptance-test coverage.references/logging.mdbefore adding provider logs or diagnostics.references/pitfalls.mdandreferences/state-safety.mdbefore opening or reviewing a provider PR.
- Model the API boundary first. Decide whether the behavior is a managed resource, data source, or ephemeral resource.
- Add or update client wrapper behavior in
internal/client/**when provider code needs a stable API abstraction. - Implement provider code in the existing package structure under
internal/provider/**. - Register resources, data sources, and ephemeral resources in
internal/provider/provider.go. - Align schema, model structs, Terraform field names, validators, plan modifiers, diagnostics, and import state behavior.
- Add tests near the changed behavior.
- Update examples and docs sources when users need new Terraform configuration.
- Add every new managed resource to
dev/local-devloop. Include a safe representative configuration, any input variable needed to avoid hard-coded environment-specific values, a useful output such as its ID, and the resource address indev/local-devloop/README.md. - Run generation and verification commands that match the change.
Terraform Query
Terraform Query discovers existing managed objects through provider-defined list resources. A query-enabled managed resource needs all of these pieces:
- Implement
resource.ResourceWithIdentitywith the smallest immutable key that uniquely identifies the remote object. Set that identity after successful Create, Read, and Update operations. - Keep
resource.ResourceWithImportState. Continue accepting the existing string import ID and also accept structured identity imports so Query-generated import blocks seed the ordinary state fields used by Read. - Implement and register a matching
list.ListResource, and add its constructor toOnaProvider.ListResources. Provider configuration must setConfigureResponse.ListResourceDataso list resources receive the same authenticated client as managed resources. - Treat
list.ListRequest.IncludeResourceas a Terraform protocol request, not an Ona API field. Always return identity and display name. PopulateListResult.Resourcefrom the same API-to-model mapping used by Read only whenIncludeResourceis true. - Respect
ListRequest.Limit, paginate collection and parent lookups, emit deterministic results, and return API or mapping failures as diagnostics rather than incomplete success. - Never call token-issuing or secret-value endpoints during discovery. Keep write-only and unrecoverable values null in listed resource models, even if an API response includes them.
- Add a
.tfquery.hclsource example, generated list-resource docs, focused mapping/import unit tests, and a hermetic Terraform 1.14+ Query acceptance test that checks identity, display name, filters, limits, resource values, and secret omission where applicable. - Add
templates/list-resources/<resource_name>.md.tmplfor every list resource. Set a non-emptysubcategorythat matches the corresponding managed-resource template unless there is a documented reason to differ. Preserve the standard list-resource title, description, examples, and schema layout. - After
make generate, verify everydocs/list-resources/*.mdhas a matching template and non-emptysubcategory. Treat an empty category or missing template as incomplete documentation.
Shared list helpers belong in internal/provider/listutil; API-specific
discovery stays in the resource package or an internal/client wrapper. Run
make generate after adding list schemas or examples.
Golden Rules
- Persist ID fields immediately after a create API succeeds, before follow-up calls that can fail. See
references/state-safety.mdfor the detailed rule and examples. Readmust refresh every tracked attribute and remove state only when the remote API reports a definitive not-found for the exact object.- After every framework
GetorSet, append diagnostics and return onHasError(). - Use
UseStateForUnknown()on stable computed values that should not churn as "known after apply." - Mark fields requiring recreation with replacement plan modifiers.
- Use sets for unordered remote collections so API ordering does not create diffs.
- Return planned known values consistently after apply unless the API intentionally canonicalizes them and the schema accounts for it.
- Use diagnostics for user-facing failures; do not panic.
- Mark sensitive attributes as sensitive, but use
references/secrets-and-sensitive-data.mdfor the state decision tree andreferences/logging.mdfor log masking rules. - Prefer ephemeral resources for issued temporary tokens that should not persist in state.
Local Validation
- Install dependencies:
make install-dependencies. - Format:
make fmt. - Tests:
make test. - Build:
make build. - Lint:
make lintwhen code changes warrant it. - Generation:
make generate, thengit diff --exit-codewhen schemas, examples, docs, or codegen inputs change. - List-resource docs: inventory
docs/list-resources/againsttemplates/list-resources/and verify each category matches its related managed resource.
Local Terraform Dev Loop
Use the README workflow when exercising the provider against Terraform:
mkdir -p .bin
go build -o .bin/terraform-provider-ona .
cat > terraformrc <<EOF
provider_installation {
dev_overrides {
"gitpod-io/ona" = "${PWD}/.bin"
}
direct {}
}
EOF \
TF_CLI_CONFIG_FILE="${PWD}/terraformrc" \
terraform -chdir=dev/local-devloop plan -input=false
Do not commit .bin/, terraformrc, Terraform state, or real tokens.
Done Criteria
A provider change is done when it has correct lifecycle behavior, tests for changed behavior, generated docs/examples when needed, every new managed resource is exercised by dev/local-devloop and listed in its README, there is no unintended generated diff, and the result clearly notes whether acceptance tests were run.
When Stuck
- If Terraform shows a perpetual diff, compare schema flags, plan modifiers, API canonicalization, and collection ordering.
- If state is wrong after apply, inspect Create/Update return values and whether Read refreshes all tracked attributes.
- If a secret appears in output or state, use
references/secrets-and-sensitive-data.mdto decide whether it belongs in state, should become an ephemeral resource, or should be exposed only by reference. - If generated docs are stale, change the schema/example source and rerun
make generate.
Reference Index
references/concepts-and-lifecycle.md— the execution model and lifecycle rules.references/core-implementation.md— provider, resource, data source, schema, diagnostics, import, validators, and plan modifiers.references/advanced-primitives.md— newer framework/protocol features and when to use them.references/secrets-and-sensitive-data.md— canonical decision tree for sensitive inputs, secret outputs, and state exposure.references/naming-conventions.md— Terraform type names, Go type names, and file naming.references/testing.md— unit tests, acceptance tests, plan checks, import checks, and sweepers.references/logging.md— canonical provider logging, diagnostics, and log-masking guidance.references/pitfalls.md— common state-model failure modes.references/state-safety.md— canonical immediate-ID persistence rule and review findings from this provider translated into reusable checks.