Documentation
¶
Overview ¶
Package gcpclient resolves the Google Cloud credential a service client is built from, and hands it over as client options — once and shared, or afresh per operation, as the caller chooses.
What this yields, and why it is not a client ¶
GCP is the provider where building a service client is *not* free. secretmanager.NewClient establishes a gRPC connection, returns an error, and must be Closed. It would be tempting to build the client here and hand it over ready-made — and org 0003 originally said so, before its R1 revision.
It does not, for a reason that only shows up with more than one consumer: the config family alone has three GCP adapters taking three different client types (*storage.Client, *parametermanager.Client, *secretmanager.Client). One accessor serves one of them; three would drag three GCP service SDKs into this module and inherit that union to every consumer.
So what crosses the boundary is the credential, expressed as option.ClientOption values. Detecting Application Default Credentials is the expensive, failure-prone, shareable part; constructing the client is the adapter's own job, and the adapter is where the Close obligation lands.
opts, err := src.GCPClientOptions(ctx) client, err := secretmanager.NewClient(ctx, opts...) // adapter owns this, and Closes it
Choosing a rung ¶
gcpclient.FromOptions(opts) // you have client options already; used as-is gcpclient.Ambient() // Application Default Credentials, detected once and shared gcpclient.PerCall() // the same, detected afresh every time
Ambient resolves lazily on first use, at most once concurrently, and — unlike sync.OnceValues — never caches a failure.
Scopes are required and never guessed ¶
Detecting default credentials needs at least one OAuth scope, and there is no safe default: cloud-platform grants everything the caller can do, and a narrower scope silently fails only when a call needs more. So Ambient and PerCall require WithScopes, and refuse with ErrNoScopes rather than choosing on the caller's behalf. Each adapter knows the scope its service needs; this module does not.
Specified by org 0003 (P-2 as revised by R1, P-3, P-12, P-13, P-14).
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoScopes reports an ambient rung constructed without [WithScopes]. // // There is no safe default scope, and guessing one either over-grants — a // cloud-platform token can do everything its principal can — or under-grants // in a way that only surfaces when some later call needs more. ErrNoScopes = errors.NewSentinel("gcpclient.no_scopes", "no OAuth scopes configured; pass WithScopes with the scope the service needs") // ErrNoOptions reports an empty or nil slice supplied to [FromOptions]. ErrNoOptions = errors.NewSentinel("gcpclient.no_options", "no client options supplied; pass at least one, or use Ambient to detect credentials") // ErrNoCredentials reports a detector that returned neither credentials nor // an error — a pair that would otherwise reach a service constructor and fail // there instead of here. ErrNoCredentials = errors.NewSentinel("gcpclient.no_credentials", "credential detection returned nothing and no error") )
Functions ¶
This section is empty.
Types ¶
type Option ¶
type Option func(*options)
Option configures a source.
func WithBuildTimeout ¶
WithBuildTimeout bounds a single detection attempt. Per attempt, not a total budget: a failure caches nothing, so a later call gets a fresh allowance.
func WithDetectOptions ¶
func WithDetectOptions(opts *credentials.DetectOptions) Option
WithDetectOptions passes the SDK's own options through to credentials.DetectDefault — an explicit credentials file, a self-signed JWT, a custom HTTP client.
It is the escape hatch for everything WithScopes does not cover. Scopes set by WithScopes are applied over whatever this carries, so the two compose in the order they read.
func WithLifetimeContext ¶
WithLifetimeContext scopes an Ambient source's detection attempts to the life of whatever owns it. PerCall ignores it, being already scoped to its caller.
func WithLogger ¶
WithLogger enables DEBUG diagnostics. nil disables them, which is the default.
Records carry only non-secret identifiers — the scopes requested and how long detection took. Nothing is read off the credential itself, because those accessors can make a network call and a log statement that reaches the network is a hidden failure mode. No token is requested here, and nothing is logged above DEBUG.
type Source ¶
type Source interface {
// GCPClientOptions returns the options, detecting credentials if the source's
// strategy says to. It is safe for concurrent use.
//
// The returned slice is a copy: a caller appending to it — which every service
// constructor invites — cannot disturb a shared source's own value.
GCPClientOptions(ctx context.Context) ([]option.ClientOption, error)
}
Source yields the client options a GCP service client is built from.
The method is named for what it returns rather than a generic Get, so a call site reads as what it is and two providers' sources are not accidentally interchangeable.
func Ambient ¶
Ambient returns a source that detects Application Default Credentials the first time it is asked, and shares the result.
It returns immediately and performs no I/O.
Example ¶
ExampleAmbient shows the usual wiring. No Output because detection needs real Application Default Credentials; it is here to be read and kept compiling.
Note where the client is built: this module hands over the credential, and the adapter constructs — and Closes — its own service client.
package main
import (
"context"
"gitlab.com/phpboyscout/go/gcpclient"
)
func main() {
src := gcpclient.Ambient(
gcpclient.WithScopes("https://www.googleapis.com/auth/cloud-platform"),
)
opts, err := src.GCPClientOptions(context.Background())
if err != nil {
return
}
// client, err := secretmanager.NewClient(ctx, opts...)
// defer client.Close()
_ = opts
}
Output:
Example (NoScopes) ¶
ExampleAmbient_noScopes shows the one thing this module refuses to guess. There is no safe default scope: cloud-platform grants everything the principal can do, and a narrower one fails only when some later call needs more.
package main
import (
"context"
"errors"
"fmt"
"gitlab.com/phpboyscout/go/gcpclient"
)
func main() {
_, err := gcpclient.Ambient().GCPClientOptions(context.Background())
fmt.Println(errors.Is(err, gcpclient.ErrNoScopes))
}
Output: true
func FromOptions ¶
func FromOptions(clientOpts []option.ClientOption, opts ...Option) (Source, error)
FromOptions returns a source over client options the caller already holds — an explicit credentials file, an emulator endpoint, a pre-detected credential.
The options are used as-is and nothing is detected, so this rung has no build policy to apply.
Example ¶
ExampleFromOptions shows the injecting rung — an emulator endpoint, say.
package main
import (
"context"
"fmt"
"gitlab.com/phpboyscout/go/gcpclient"
"google.golang.org/api/option"
)
func main() {
src, err := gcpclient.FromOptions([]option.ClientOption{
option.WithEndpoint("http://localhost:8085"),
})
if err != nil {
fmt.Println("construct:", err)
return
}
opts, err := src.GCPClientOptions(context.Background())
if err != nil {
fmt.Println("resolve:", err)
return
}
fmt.Println(len(opts))
}
Output: 1