Share one credential across components¶
You are using more than one Google Cloud component — secrets from Secret Manager, parameters from Parameter Manager, files from GCS — and you want the credential detected once between them.
Detect once, build each client¶
import (
configgcpsecret "gitlab.com/phpboyscout/go/config-gcp-secret"
"gitlab.com/phpboyscout/go/gcpclient"
)
src := gcpclient.Ambient(
gcpclient.WithScopes("https://www.googleapis.com/auth/cloud-platform"),
)
opts, err := src.GCPClientOptions(ctx)
if err != nil {
return err
}
secrets, err := configgcpsecret.FromOptions(ctx, "my-project", "", opts)
if err != nil {
return err
}
defer secrets.Close()
One detection; each adapter builds the service client it actually needs. That is the shape this module's design forces, and it is the right one — the three GCP config adapters take three different client types.
Closing is yours, and the type says so¶
Unlike the AWS and Azure adapters, the GCP config adapters return a concrete
type carrying Close — *OwnedBackend, *OwnedFS — rather than the bare
interface.
That is not an inconsistency. secretmanager.NewClient opens a gRPC connection
that must be closed, and config.Backend has no Close method to hang that on.
So the rung that builds the client returns something that can be closed, and
the obligation follows whoever built it:
- you passed a client in → you close it
- the adapter built it from your options → the returned type closes it
Different services need different scopes¶
Nothing here guesses. Give the scope the service actually needs:
// Secret Manager / Parameter Manager
gcpclient.WithScopes("https://www.googleapis.com/auth/cloud-platform")
// Cloud Storage, read-only
gcpclient.WithScopes("https://www.googleapis.com/auth/devstorage.read_only")
Scopes accumulate across calls, and empty strings are dropped — so an unset flag cannot silently introduce a blank scope.
Everything the SDK offers¶
WithDetectOptions passes credentials.DetectOptions straight through — an
explicit credentials file, a self-signed JWT, a custom HTTP client. Scopes set by
WithScopes are applied over whatever it carries, so the two compose in the
order they read.
Bounding and scoping¶
src := gcpclient.Ambient(
gcpclient.WithScopes(scope),
gcpclient.WithBuildTimeout(5*time.Second),
gcpclient.WithLifetimeContext(appCtx),
)
Per attempt, not a total budget, and cooperative — it cancels the context detection is given.