Skip to main content
Version: v0.1.0

Provider adapters

An adapter's whole job is to make three clouds indistinguishable to the domain, and to make sure nothing sensitive crosses the boundary while doing it.

The narrow read surface

The domain sees only:

  • list — paginated, prefix-scoped, with an opaque cursor;
  • get/open — bounded reads of small metadata objects; and
  • stat/head — existence plus allowlisted metadata (size, modification time, an opaque ETag).

There is no write, create, upload, copy, delete, multipart, lifecycle, tagging-mutation, bucket-creation, or restore operation anywhere — not unexported, not behind a flag. hack/check-readonly.sh fails the build if a mutation-shaped identifier appears in cmd/ or internal/, and make lint runs a dedicated TestReaderSurface guard over the interface itself.

Supported providers

ProviderDESTINATION_PATHCredential modesEndpoint override
S3s3://bucket/prefixmounted static key pair (+ optional session token), or the SDK workload-identity chain; in sidecar mode an explicit web-identity token/roleENDPOINT_URL (+ ENDPOINT_CA_FILE) for MinIO and other S3-compatible stores
Azure Blob Storageazure://container/prefixmounted connection string; or account + exactly one of key/SAS file; or account alone with DefaultAzureCredentialnot applicable
GCSgs://bucket/prefixmounted service-account JSON, or Application Default Credentialsnot applicable

Error normalization

Raw SDK errors never leave the adapter. Each one is mapped into the small, stable fault vocabulary shared by logs, HTTP output, and the evidence API:

unknown, canceled, timeout, invalid_configuration, authentication, authorization, throttled, unavailable, not_found, incompatible_format, safety_limit.

This is why a wrong region reads as unavailable and a missing policy statement reads as authorization — the adapter refuses to forward a message that might carry a signed URL, an SAS parameter, or an authorization header. Each adapter has a dedicated test proving it (TestS3ContractMapsErrorsWithoutDisclosingSDKDetails, TestSafeErrorRedacts for Azure and GCS).

Cursors

Pagination state is an opaque cursor produced by internal/provider/cursor. A cursor is process-local, bounded, and cannot escape its configured store, destination prefix, and scope. Keys are opaque: they are never mapped onto local filesystem paths, and path cleaning is never used as an authorization boundary.

Parity is proven, not assumed

All three adapters implement one shared contract test suite (internal/store/storetest), and a shared six-state Barman journey (internal/provider/providertest) runs against pinned MinIO, Azurite, and fake-gcs-server images:

make test-s3 # pinned MinIO
make test-azure # pinned Azurite
make test-gcs # pinned fake-gcs-server
make test-provider-parity

make test-provider-parity requires byte-identical normalized semantic output across the three providers for the same repository. Any place where a provider's behavior would leak into a conclusion shows up there as a diff.

Adding a provider

A new adapter must: implement the read surface and pass internal/store/storetest; map every error it can produce into fault without forwarding a message; accept credentials only as mounted files or an ambient workload identity; keep its SDK types inside the package; produce bounded, cancelable operations; and join the shared Barman journey with a pinned local emulator so parity stays provable.