Skip to content

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 Close obligation. 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.