Read & write config in Cloud Storage¶
When a tool's configuration file lives in a GCP Cloud Storage bucket, the
config-gcp-gcs sibling module wraps a
*storage.Client as a config.FS.
You build and configure the storage client — credentials, project, options all stay yours — and hand it in with the bucket name:
import (
"cloud.google.com/go/storage"
"gitlab.com/phpboyscout/go/config"
configgcpgcs "gitlab.com/phpboyscout/go/config-gcp-gcs"
)
client, _ := storage.NewClient(ctx) // your credentials, your options
defer client.Close()
store, err := config.NewStore(ctx,
config.WithFiles(configgcpgcs.Wrap(client, "my-bucket"), "config.yaml"),
config.WithEnv("APP"),
)
The config.FS name is the object name — Wrap(client, "my-bucket") reads and writes the object
config.yaml.
When you do not need this¶
If you read one object once at startup, fetching it and handing the bytes to
WithReaders needs no adapter.
Reach for config-gcp-gcs when the bucket should behave like a filesystem — several
files, writes routed back, or hot-reload noticing a change made elsewhere.
A base context, because the SDK needs one¶
Every storage call takes a context.Context, but the config.FS methods do not, so Wrap
defaults to context.Background(). If you want the adapter's operations bound to a cancellable
context, pass one:
Note that a cancelled base context makes every subsequent read fail — bind a context whose lifetime matches the store's.
The write is a real atomic move¶
Unlike S3 and Azure Blob, Cloud Storage has a native server-side move, so the commit is a single
atomic ObjectHandle.Move — no copy-then-delete, and no window where a crash could orphan a staging
object. Conflict detection is the core's SHA-256 content fingerprint, captured at load: a write is
refused if the object changed underneath it.
Hot-reload is polled, at a calm cadence¶
An object store has no local path, so it is watched by polling. Because each poll is a billed
object read, config-gcp-gcs declares a 60-second default through config.PollIntervalHinter,
overridable with WithPollInterval.
Getting a client¶
Building the client yourself is the default. Two further rungs exist:
// You hold client options — an emulator endpoint, an explicit credentials file,
// or a credential resolved once with go/gcpclient and shared across adapters.
fsys, err := configgcpgcs.FromOptions(ctx, "my-bucket", opts)
// Application Default Credentials.
fsys, err := configgcpgcs.Default(ctx, "my-bucket")
defer fsys.Close()
To detect Application Default Credentials once and feed several GCP adapters
from it, resolve with
go/gcpclient and pass the
options it yields to FromOptions. It hands out client options rather than a
client precisely because the three GCP adapters need three different client
types.
Both return *OwnedFS, which you can Close. The obligation is real but
weaker than its Secret Manager sibling's: storage.Client is HTTP-backed and its
documentation says Close "need not be called at program exit", where
secretmanager.Client says it "must be Closed".
This is the simplest zero-conf rung in the family: a bucket name is globally unique, so unlike Secret Manager and Parameter Manager there is no project or location to supply.
What it costs¶
| Modules added | 54 — 45 for the Cloud Storage SDK, 9 for the config graph |
The Cloud Storage SDK — the heaviest dependency graph in the adapter family, and the honest cost of
the client — plus config, asserted by an allowlist test. Because it is its own module, only a
consumer reading from GCS compiles it. The tests run against
fake-gcs-server under testcontainers, so the suite needs
no GCP project.
Related¶
- How filesystem adapters work — the object-store commit model and polled reload
- Read & write config in S3 · Azure Blob — the sibling object stores
- Backends & capabilities — the
config.FSinterface