Changelog
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
2.0.1 - 2026-08-18
Section titled “2.0.1 - 2026-08-18”Documentation and dependencies. No change to the provisioner’s or the stub’s behaviour.
-
docs/ARCHITECTURE.md, describing how the components interact, with diagrams: the local stack topology, the credential and token-exchange sequence, dual targeting, package layering, section ordering, the reconciliation loop, and what deliberately does not exist.
The stack diagram makes one thing explicit that the Compose file does not: the token stub has no outgoing arrows. It reads no files and holds no state, which is why it does not mount the secrets volume — it never needs the key it hands out tokens for.
-
Dependabot version updates for Go modules, GitHub Actions and the Dockerfile builder images, on top of the security updates that were already running. Minor and patch updates are grouped into one pull request per ecosystem; majors arrive individually.
No
target-branchis set, because specifying it would stop Dependabot raising security updates, and the branch prefix stays at the default becauseci.yamltriggers ondependabot/**.
Changed
Section titled “Changed”-
The token stub’s Compose service now declares its configuration. It previously set nothing, which hid both the available settings and a trap: the healthcheck runs the binary with no flags, so it resolves the port from
TOKEN_STUB_ADDR. Moving the server with--addrinstead leaves the probe checking:8099, and the container reports unhealthy while serving perfectly well. All three variables are now shown at their defaults, and the reason is stated in the--pingflag help and the README. -
Dependency updates:
google.golang.org/api0.287.1 to 0.293.0google.golang.org/grpc1.82.1 to 1.83.0google.golang.org/protobuf1.36.11 to 1.36.12golangbuilder image 1.25-alpine to 1.26-alpineactions/checkout,actions/setup-goandactions/upload-artifact6 to 7,codecov/codecov-action5 to 7,docker/setup-buildx-action3 to 4
2.0.0 - 2026-08-18
Section titled “2.0.0 - 2026-08-18”Local stacks can now satisfy client libraries that insist on a credential.
Spring Cloud GCP parses GOOGLE_APPLICATION_CREDENTIALS eagerly at startup, and
google-cloud-storage for Java has no STORAGE_EMULATOR_HOST equivalent — it
authenticates for real even against an emulator that checks nothing. This
release covers both halves locally: a generated key file, and a token endpoint
for it to talk to. Together they replace the OpenSSL and nginx sidecars such a
stack would otherwise need.
The provisioner’s contract is unchanged: still one-shot, still no long-running process, still no health endpoint. The token stub is a second, separate binary with its own lifecycle.
-
Fake service account keys. A
credentials:section writes local key files for libraries that refuse to start withoutGOOGLE_APPLICATION_CREDENTIALS— Spring Cloud GCP parses the private key eagerly at startup, so a placeholder string fails where a real keypair succeeds. The RSA keypair is generated locally with stdlib crypto, so Google holds no matching public key and the file authenticates to nothing; it is the credential equivalent of a self-signed certificate for localhost, and is verified as accepted by Google’s own auth library.private_key_idandclient_idare fixed, obviously non-Google values so the file cannot be mistaken for a real credential.token_uripoints every OAuth URL in the file at one address, so a library that does try to mint a token reaches a local stub rather thanaccounts.google.com.The file is never rewritten once it exists — its contents are a fresh keypair, so rewriting would rotate the credential underneath whatever already loaded it and could never converge. This is the one setting that does not inherit the global strategy;
strategy: updateon the entry forces a new key.The key is written
0644into a0755directory. That is deliberate: it exists to be read by other containers, which routinely run as a different UID than the provisioner, and0600would deny the only consumer it has. -
gcp-token-stub, a second binary serving a static OAuth2 token endpoint for local stacks. Some Google client libraries insist on obtaining a token before issuing any request, even against an emulator that ignores authentication:google-cloud-storagefor Java has noSTORAGE_EMULATOR_HOSTequivalent, so a JVM application reaches fake-gcs-server throughspring.cloud.gcp.storage.hostwhile still holding realServiceAccountCredentials, signs a JWT, and exchanges it at thetoken_urifrom its key file. Pointing that at the stub keeps the exchange on the machine.The assertion is deliberately not verified, because nothing downstream verifies signatures either. It ships as its own
scratchimage — a few megabytes in place of an nginx container serving static JSON — and, sincescratchhas no shell, probes itself with--pingfor container healthchecks. The provisioner remains one-shot; the stub is a separate binary with its own lifecycle. -
The release pipeline publishes two images:
ghcr.io/datarocks-ag/gcp-emulator-provisionerandghcr.io/datarocks-ag/gcp-token-stub, each scanned by Trivy before it is pushed. GoReleaser emits a separate archive per binary. -
--section=credentials, which touches no endpoint and so runs with no emulator up. In a full run credentials are written first, because the applications that need the key file usually start alongside the provisioner.
1.0.0 - 2026-08-16
Section titled “1.0.0 - 2026-08-16”Initial release.
-
Idempotent provisioning of Google Cloud Pub/Sub topics and subscriptions and Cloud Storage buckets from a YAML config file. Additive only — resources absent from the config are never deleted.
-
Two provisioning sections (
--section/GCP_SECTION):pubsub,storage, orall(default). A section with no configured resources is skipped and its client is never constructed, so a stack running only one emulator needs only that endpoint. -
Emulator and Google Cloud from one binary.
PUBSUB_EMULATOR_HOSTandSTORAGE_EMULATOR_HOSTselect the emulators; with neither set the Google client libraries fall back to Application Default Credentials. -
Topics: labels and message retention, reconciled through a minimal update mask so only drifted fields are sent. Labels already on a topic are merged rather than replaced.
-
Subscriptions: ack deadline, message retention, retain-acked-messages, exactly-once delivery, expiration policy (including
never), labels, dead letter policy and retry policy.filterandenable_message_orderingare applied at creation. -
Push subscriptions: endpoint and attributes, plus
push_oidc_token(service_account_email,audience) for endpoints that require an OIDC token, andpush_wrapper(pubsub|none) withpush_write_metadatato select the delivered payload shape. All of them fold into the singlepush_configupdate mask path, and attributes already on the subscription are merged rather than replaced — the mask overwrites the whole message, so anything not carried across would be deleted. -
Optional fields leave live settings alone.
versioning,retain_acked_messages,enable_message_ordering,enable_exactly_once_deliveryandpush_wrapperare all tri-state: omitting one leaves whatever the resource has, and only an explicit value changes it. A resource configured out of band is never silently reset, and never reported as drift the provisioner was not asked to fix. -
Immutable field drift is a warning, not a failure. Pub/Sub fixes
filter,enable_message_orderingand the subscription’s topic at creation; the provisioner reports the difference and leaves the resource intact rather than recreating it and discarding its backlog. -
Emulator gaps are tolerated. The Pub/Sub emulator rejects update masks naming
labelsorexpiration_policythat Google Cloud accepts; those specificInvalidArgumentresponses are downgraded to a warning, while every other one stays fatal. Because the emulator validates the whole mask before touching the resource, a refused path would take every field batched with it down too — so an update is sent as one call and, if refused, retried path by path. The supported paths still apply, and the warning names the exact path that did not. -
Buckets: existence, versioning, and create-time location and storage class. A live bucket whose
locationorstorage_classdisagrees with the config is reported as a warning — Cloud Storage cannot move a bucket, so the difference is never applied. That comparison is off against an emulator, which reportsUS-CENTRAL1/STANDARDfor every bucket whatever it was created with;STORAGE_EMULATOR_HOSTdecides. Bucket labels, lifecycle rules and CORS are deliberately not managed, because fake-gcs-server accepts and then discards them, which would produce drift that never converges. -
Topics are provisioned in a full pass before any subscription, so a subscription may reference a dead letter topic declared later in the file. A dead letter topic that is not declared anywhere is rejected at config load.
-
The config surface is deliberately a subset of the API. IAM belongs in Terraform, and Pub/Sub schemas, BigQuery/Cloud Storage/Bigtable export subscriptions, message transforms and import topics are out of scope because the emulator implements none of them — a config using them could not be exercised locally, which is the point of one binary targeting both.
-
Dry-run mode (
--dry-run/DRY_RUN) that previews every mutation against live state without applying it. -
YAML config with
${VAR}environment variable expansion, including inside label and push-attribute maps. -
Configurable strategy (
updateby default, orcreateto skip existing resources), settable globally or per topic, subscription and bucket. -
Path-prefixed config validation covering Pub/Sub and Cloud Storage naming rules, duration syntax, dead letter delivery-attempt bounds, retry backoff ordering, and push settings declared without a push endpoint.
-
Duration fields are checked against the Google Cloud limits at config load, rather than failing on the first API call. The emulator is more permissive, so without this a config could pass locally and then fail in production:
Field Range ack_deadline10s – 600s, whole seconds only message_retention(topic and subscription)10m – 31d expiration_ttl1d – 365d, or neverretry.minimum_backoff,retry.maximum_backoff0s – 600s dead_letter.max_delivery_attempts5 – 100 ack_deadlineis restricted to whole seconds because Pub/Sub stores it asack_deadline_seconds:10s500mswould be truncated on the wire, read back as10s, and then reported as drift on every subsequent run. -
Exponential backoff retry (15 attempts, 1s doubling to 30s, 5 minute ceiling) while the emulators come up, plus a 30s deadline on every bucket call so fake-gcs-server’s retryable 500 for an unsupported field cannot hang the run indefinitely.
-
A clear error when
versioningis requested against a fake-gcs-server started on its default filesystem backend, which does not implement it. -
Structured JSON logging via
log/slog, withLOG_LEVELcontrol. -
Docker Compose stack running both emulators with healthchecks, and a multi-stage
scratchcontainer image. -
Integration tests that boot both emulators with
testcontainers-goand exercise provisioning, idempotency, drift reconciliation, dry-run, immutable field drift, the push config oneofs, and the emulator label gap including that a refused path does not discard the rest of its batch.
Docs for v2.0.1 · built from the release tag