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.
The shape
Section titled “The shape”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.
What you import
Section titled “What you import”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.
Getting started
Section titled “Getting started”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.
A worked example, in one sitting
Section titled “A worked example, in one sitting”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.
Proving the suite can fail
Section titled “Proving the suite can fail”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.
Testing against the real thing
Section titled “Testing against the real thing”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
initdbagainst a temporary server and shuts it down before starting the real one, so bothnc -zandpg_isreadyanswer yes during a window where the next query fails.
Before you open a pull request
Section titled “Before you open a pull request”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.