Skip to content

Type to search pages.

View .md

Writing a provider

Providers are the main extension point, and they are meant to be written by people outside this repository. A provider decides where an environment’s database comes from: a container on the developer’s machine, a branch on a hosted Postgres, a snapshot on infrastructure you already run.

You do not have to ask permission and you do not have to be a contributor here. Implement one interface, run one suite, and if the suite is green your provider does what Antifailure promises its users.

A provider implements provider.Database. The interface is in engine/pkg/provider/db.go and every method carries the rule it has to keep. Two of those rules are worth reading before you write any code, because they are the ones that are easy to miss and expensive to get wrong.

Every method is idempotent by its identifying argument. Branching twice for one environment returns one branch. Destroying something already gone succeeds. This is not tidiness. The engine retries after a timeout, and a retry that creates a second resource is how an orphan is made: a database nothing owns, that nothing will ever clean up, that costs money until somebody notices.

A version that is not verified is never branched. Masking is a claim and verification is a check, and the whole product rests on the check. A provider that publishes an unverified version, or branches one, has broken the promise that a preview environment cannot contain real customer data.

Four packages, and no others. They are the four the stability page names as stable, and they are stable together because an interface is only as usable as the types its signatures name.

Package Why you need it
engine/pkg/provider The interface you implement.
engine/pkg/secret Database.ConnString returns a secret.Value, so you have to name the type. Build one with secret.New; it renders as [redacted] through every path that turns a value into text.
engine/pkg/schema The manifest types the interfaces carry.
engine/conformance The suite.

Anything under engine/internal is not importable from your module, and that is the toolchain refusing it rather than a convention. If you find yourself needing something in there, that is a gap in these four packages worth raising rather than a barrier to work around.

import "github.com/antifailure/antifailure/engine/conformance"
func TestMyProvider(t *testing.T) {
conformance.RunDatabase(t, func(t *testing.T) provider.Database {
return myprovider.New(...)
}, conformance.Options{})
}

That is the whole harness. It runs twenty three behaviours against your provider and each one is a property a user depends on.

engine/internal/testutil/fakes/inmemory.go is a complete provider.Database in 180 lines, with no database behind it. It is the shortest thing in the tree that passes the suite, and it is worth reading before you write your own, because it makes the shape of the interface obvious without any of a real service’s noise.

Four things in it are worth copying rather than inventing.

It declares only what it can do. Capabilities() returns provider.Caps{Branching: true} and nothing else. It has no rows, so it does not claim Reset, and it has no pooler, so it does not claim pooled endpoints. The suite skips those behaviours and says which capability was missing as it skips them.

It refuses rather than pretends. Branching from a version that does not exist, or from one that failed verification, returns an error. A provider that invents a branch for a golden it does not have will pass a shallow test and lose somebody’s data on the real one.

Destroy is idempotent, and so is everything teardown touches. Removing a branch twice succeeds, because teardown retries and a crash leaves a partial state. It keeps a destroyed set for exactly that.

Health answers rather than errors. A destroyed branch is unreachable, not a failure. af down asks for health, and a provider that errors on a branch it has just removed makes a successful teardown look like a failure.

It is also the provider the suite’s own negative controls run against, which is the other reason it exists: a control that needs infrastructure gets skipped, and a skipped control is a false green rather than a proof. That is the subject of the next two sections.

Capabilities, and why skipping has to be loud

Section titled “Capabilities, and why skipping has to be loud”

Not every provider can do everything. A provider without copy-on-write cannot make branch time independent of database size; a provider without a pooler has no pooled connection string to hand out.

Say so in Capabilities(). The suite reads it and skips the behaviours that need what you do not have, naming the missing capability as it goes.

Declaring a capability you do not have is the failure worth guarding against, and it fails loudly: Capabilities_AreSelfConsistent checks the declarations against each other, and the behaviours themselves check the declarations against reality. A silent skip is how a provider ends up claiming conformance it does not have, so the suite is built to make skipping visible rather than convenient.

A green conformance run is worth exactly as much as your confidence that the suite could have gone red. That confidence is not free, and the usual way a suite quietly stops checking is undramatic: a helper starts skipping, an assertion starts comparing a value against itself, a behaviour asserts on state an earlier behaviour already established. All of those still print ok.

So engine/internal/testutil/fakes gives you fault injection. fakes.Break takes a provider that works and returns one that violates exactly one guarantee: publishing an unverified golden, making Branch non-idempotent, making a second Destroy an error, under-reporting the inventory.

p := fakes.Break(myprovider.New(...), fakes.BranchIsNotIdempotent)

Point the suite at that and it must go red in Branch_IsIdempotentByEnvironment. fakes.Catches() maps every fault to the behaviour that is supposed to catch it. If a fault goes undetected, the suite has a hole and you have found it.

This is worth doing once for your own provider before you trust a green run. It takes ten minutes and it is the difference between a suite that passes and a suite that checks.

What cannot be broken, and why that is fine

Section titled “What cannot be broken, and why that is fine”

ConnString_IsASecret has no fault, deliberately. Connection strings are secrets.Value, whose String, GoString and Format all return the redacted marker, so there is no value of that type that renders its plaintext. The guarantee is enforced by the type rather than by the suite.

That distinction is worth carrying into your own code: a rule the compiler enforces does not need a test, and a rule only a comment enforces needs two.

Run against a real database. A provider tested only against a fake proves that your code does what you expected, which is the thing you were least uncertain about.

The Docker provider is the reference implementation. Its conformance test is in engine/internal/db/docker/conformance_test.go and it is short, because the suite does the work.

Start the test Postgres with just db. It is started with pg_stat_statements preloaded, which matters more than it sounds: without the preload CREATE EXTENSION succeeds, the view exists, and it records nothing, so tests skip and the suite reports ok.

Two failure modes to watch for, both of which produce a green run that proved nothing:

  • Skip only for “there is no Docker here”. Any other reason to skip should be a failure with the container’s log attached. A container that starts, publishes a port and then answers nothing is not an absent Docker.
  • An open port is not an accepting database. The Postgres image runs initdb against a temporary server and shuts it down before starting the real one, so both nc -z and pg_isready answer yes during a window where the next query fails.

Run just gate. It runs everything CI runs, in CI’s order, so a green gate means a green CI.

If your provider talks to a hosted service, say in the pull request which behaviours you ran against the real thing and which you did not. written and proven are different words here and the distinction is kept on purpose.