Why options, not a client¶
Every provider module in this estate hands out the thing you build a service
client from rather than a client. For AWS that is an aws.Config; for Azure an
azcore.TokenCredential. For Google Cloud it is a slice of
option.ClientOption.
That last one looks like a missed opportunity. GCP is the provider where building
a client is genuinely expensive — secretmanager.NewClient establishes a gRPC
connection, returns an error, and must be closed. If anywhere would benefit from
a module that hands over a ready-made client, surely here.
The specification originally said exactly that. It was revised before this module was written, and the reason is worth keeping.
One accessor cannot serve three consumers¶
The config family alone has three GCP adapters:
| Adapter | Client type |
|---|---|
config-gcp-secret |
*secretmanager.Client |
config-gcp-parameter |
*parametermanager.Client |
config-gcp-gcs |
*storage.Client |
A module yielding "the GCP client" would have to pick one. Yielding all three would drag three Google service SDKs into this module's dependency graph — and every consumer of this module would inherit that union, including consumers who use exactly one of them.
That would invert the whole point. These modules exist to let a consumer share a credential without inheriting a graph they did not ask for.
The credential is the shareable part¶
Splitting the work along the right seam makes the answer obvious:
- Detecting Application Default Credentials is expensive, failure-prone, and identical for every consumer. Shareable.
- Constructing a service client is cheap, specific to one service, and
carries a
Closeobligation. Not shareable.
So the credential crosses the boundary, expressed as client options, and the client is built where it is used:
opts, err := src.GCPClientOptions(ctx)
client, err := secretmanager.NewClient(ctx, opts...) // adapter's job, adapter's Close
Where the Close obligation lands¶
Because the adapter builds the client, the adapter owns closing it — which is why
the GCP config adapters return a concrete *OwnedBackend or *OwnedFS carrying
Close, rather than the bare config.Backend interface the other adapters
return.
The general rule the estate settled on: the obligation to close follows whoever built the client, not which rung was used. Pass a client in and it stays yours; let the adapter build one and the returned type will close it.
An io.Closer on config.Backend itself was considered and rejected — too large
a change to a core interface for one provider's one rung.
The copy that is not paranoia¶
GCPClientOptions returns a copy of its slice every time.
Every Google service constructor takes opts ...option.ClientOption and invites
callers to append their own. A shared source handing out one slice would let a
caller appending an endpoint override silently disturb every other consumer of
the same source. The copy costs an allocation and removes the class of bug.
What is deliberately not logged¶
This module logs at DEBUG, and records only the scopes requested and how long detection took.
It does not read anything off the credential itself — not the universe domain, not the project, nothing. Those accessors can make a network call, and a log statement that reaches the network is a hidden failure mode in the one place nobody looks for one. An early version did exactly that and a linter caught it.
Where this is specified¶
org spec 0003 — P-2 as revised by R1, plus P-10 on closeable connections and P-14 on logging.