skip to content
wihan.dev

Creating a Dokploy Terraform provider

What it takes to write a Terraform provider in Go for a young API - and why the acceptance tests talk to a real server.

2 mingo · terraform · dokploy

I wanted terraform apply to be the only deploy button on my personal infrastructure. Dokploy had an API and no living provider, so now there is vanillauys/dokploy: written in Go on the terraform-plugin-framework, at v0.10.0 after ten minor releases.

Here’s the path from an empty repo to a provider on the registry, with the lessons where they hurt.

1. Scaffold on the plugin framework

Start from HashiCorp’s scaffolding template and keep the standard shape: a thin API client package that knows nothing about Terraform, and a resource layer that maps schema to client calls.

go
func (r *ProjectResource) Create(ctx context.Context,
	req resource.CreateRequest, resp *resource.CreateResponse) {
	var plan ProjectModel
	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)

	project, err := r.client.CreateProject(ctx, plan.Name.ValueString())
	// map the response back into state, nothing clever
}

The framework does the heavy lifting: plan diffs, state upgrades, unknown-value propagation. Your job is to be honest about the API you are wrapping.

2. Test against the real thing

The acceptance suite runs against an actual Dokploy instance in CI on every pull request - a container on the runner’s own Docker, no docker-in-docker:

bash
TF_ACC=1 go test ./internal/provider/ -run TestAcc -v

Mocks would have passed builds that failed against the real server, because a young API has opinions the docs do not mention. Every one of these came out of a failing acceptance test, not a bug report:

  • Computed collections need UseStateForUnknown, or every plan recomputes them and downstream references cascade into unknowns.
  • Where the API recomputes a collection anyway, the answer is a data source and documentation, not wishful state.
  • Some deletes are asynchronous. Delete has to poll until the resource is actually gone, or the next test’s Create collides with a name that still exists.

3. Release through the registry

The Terraform registry wants tagged releases with signed artifacts and a manifest; goreleaser produces the whole matrix from one config, and the registry ingests each tag within minutes:

bash
git tag v0.10.0
git push origin v0.10.0   # CI runs goreleaser; the registry picks up the tag

After that, releasing often is cheap - six of the ten minors shipped in single-day bursts as the resource surface grew. Generate the docs with tfplugindocs so the registry pages and the repo never drift.

4. Hold v1.0 until it earns it

v1.0 is deliberately not tagged yet. The rule: the provider must survive one Dokploy minor upgrade without breaking changes first. A 1.0 is a promise about the future, and I want at least one data point before making it.