Skip to content

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.