configgcpsecret

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 20 Imported by: 0

README

config-gcp-secret

Read secrets from Google Cloud Secret Manager through config

Go Reference Pipeline Coverage phpboyscout Go toolkit

Part of the phpboyscout Go toolkit — small, framework-free Go modules extracted from go-tool-base. Documented with the parent module at config.go.phpboyscout.uk


[!WARNING] Read "What it costs" before adopting. This adapter pulls 39 modules into your dependency graph — five times the AWS or Azure secrets adapter, and by a wide margin the heaviest module in this toolkit. Almost none of it is this adapter's doing, and if you already talk to any Google API you have substantially all of it already. But it is the one thing worth weighing first rather than discovering in your go.sum.

config reads files. GCP Secret Manager is a sibling backend module like this one, so a consumer who reads secrets from it takes this module and one who does not pays nothing.

You build the client — credentials and endpoint both — and hand it in with the project it addresses:

import (
	secretmanager "cloud.google.com/go/secretmanager/apiv1"
	"gitlab.com/phpboyscout/go/config"
	configgcpsecret "gitlab.com/phpboyscout/go/config-gcp-secret"
)

client, _ := secretmanager.NewClient(ctx)

store, err := config.NewStore(ctx,
	config.WithFiles(fsys, "/etc/app.yaml"),                                  // YAML defaults
	config.WithBackend(configgcpsecret.FromClient(client, "my-project", "")), // secrets outrank them
)

The third argument is the location. Leave it empty for ordinary global secrets; give it a region ("europe-west2") for regionalised secrets. The two have genuinely different resource paths — the location segment is absent for global secrets rather than being the literal global — which is why one argument covers both.

Secret IDs are keys, verbatim

A secret ID may contain only letters, digits, hyphens and underscores — no dots, no slashes. There is no hierarchy to map, so an ID becomes a config key exactly as it appears in the console:

db-password  = "s3cr3t"        store.View().GetString("db-password")
api_key      = "k-123"    →    store.View().GetString("api_key")

Hyphens and underscores are not separators. my-service-key is one key called my-service-key, not three levels of nesting. That is deliberate: both characters are legal in ordinary secret IDs, Secret Manager offers no third character to escape with, and GCP's own conventions push operators towards hyphenated names — so treating either as a separator would silently restructure the common case with no way to opt out.

Structure comes from a document, not from the IDs

Since the key space cannot carry a tree, put one in a secret and decode it:

import configjson "gitlab.com/phpboyscout/go/config-json"

config.WithBackend(configgcpsecret.FromClientSecret(
	client, "my-project", "", "app-config", configjson.Codec{}))
// {"db":{"host":"db.internal","port":5432}}  →  GetString("db.host"), GetInt("db.port")

The codec is a parameter here, not an option: a payload is one opaque blob, so without something to decode it there is no tree to contribute, and requiring it at compile time beats failing at startup. Reading one named secret also costs a single request rather than the project-wide N+1 below.

Everything that is skipped in flat mode fails in document mode — a secret with no enabled version, and a payload that is not valid UTF-8. In flat mode the rest of the project still loads and the missing ID is its own explanation; here you named one secret as your entire configuration source, so an empty layer would be a silence rather than an answer.

In flat mode the codec stays optional. WithValueCodec decodes any secret that holds a document into a subtree under its own ID, and a plain password beside it stays a string:

config.WithBackend(configgcpsecret.FromClient(client, "my-project", "",
	configgcpsecret.WithValueCodec(configjson.Codec{})))

Filtering happens server-side

This is where Secret Manager is genuinely better than Key Vault, and worth exploiting rather than flattening for symmetry. Both filters are applied before the listing leaves the service, so on the N+1 path every secret they remove is one payload read never made — the cost that actually scales on a shared project.

configgcpsecret.FromClient(client, "my-project", "",
	configgcpsecret.WithNamePrefix("app-"),                    // name:app-
	configgcpsecret.WithLabel("environment", "production"),    // labels.environment=production
	configgcpsecret.WithLabel("team", "core"))                 // …AND labels.team=core

WithLabel is repeatable and ANDed, and label-scoping is how GCP teams already partition a shared project. WithNamePrefix is the one that needs care: name: is documented as case-insensitive substring containment, not prefix matching, so name:app- matches legacy-app-token too. The adapter therefore sends it as a narrowing hint and re-checks the prefix client-side, so the option means what its name says. The prefix is not stripped from the key.

There is deliberately no raw WithFilter(expr) at v0.1.0. It would expose Google's full filter grammar at the cost of coupling this module's public surface to a dialect it cannot validate, so a typo would become an opaque INVALID_ARGUMENT at startup rather than a compile error. It is additive later if someone needs it.

Pinning a version

WithVersion("7") — or a version alias an operator has defined on the secret — reads that version and nothing else.

A pinned version never falls back. If it is disabled or destroyed, the Load fails. You named a specific version, so silently substituting another would defeat the point of naming it. That asymmetry with latest is exactly the difference between the adapter choosing and you having chosen.

A disabled newest version is served from an older one

[!IMPORTANT] You may be running on an older version than the console shows.

This is the most consequential thing to know about this adapter, and it has no equivalent in the AWS or Azure siblings.

projects/*/secrets/*/versions/latest is an alias to the most recently created version — not the most recently created enabled one. Google's API reference says exactly that, and the DISABLED state is documented as "the SecretVersion may not be accessed". So if an operator adds version 8 and then disables it, latest still resolves to version 8 and accessing it fails with FAILED_PRECONDITION, even though version 7 holds a perfectly good credential.

This adapter falls back to the newest ENABLED version. Version 7 is served, and your application keeps working.

That was chosen knowing exactly what it trades:

  • it silently serves a credential somebody deliberately disabled — most often a rotation that half-completed, so the application keeps working while nobody learns the rotation did not finish;
  • it overrules the servicelatest means version 8, the console shows version 8 as current, and you are running on version 7.

It is still the right default, because the alternative — an application failing to start, or a key vanishing from a running configuration, because someone disabled a version — is both more common and more disruptive. A stale-but-working credential gives you time to notice. A missing one does not.

So the mitigation is part of the deal, not a footnote. WithFallbackObserver fires once per secret that did not resolve to latest, at the moment it happens:

config.WithBackend(configgcpsecret.FromClient(client, "my-project", "",
	configgcpsecret.WithFallbackObserver(func(f configgcpsecret.Fallback) {
		// f.ID, f.LatestVersion, f.LatestState, f.ServedVersion
		alerting.Warn("secret %s served from %s; latest (%s) is %s",
			f.ID, f.ServedVersion, f.LatestVersion, f.LatestState)
	})))

Wire it to whatever you already alert on. A half-completed rotation then becomes a signal instead of a silence.

It is called synchronously during Load, on the Store's goroutine, so it must not block and must not call back into the Store.

Provenance cannot tell you this, and that is a limitation rather than an oversight. config.Source has four fields and is per-layer, while a fallback is per-key — in flat mode one layer carries every secret in the project, so no single string could say which of them fell back. Origin and Explain both return a Source, so neither can surface it either. The callback is the next best thing, and is offered as that rather than as an equivalent.

A secret with no enabled version at all is a different case: there is no fallback target, so it is skipped — it contributes no key, the rest of the project loads, and the observer fires with an empty ServedVersion so the omission is signalled rather than silent.

What else is skipped, and what fails

Situation Behaviour Why
Latest version disabled or destroyed Falls back to the newest enabled version, observer fires Above
No enabled version anywhere Skipped, observer fires with empty ServedVersion No usable value at any version, so no choice is being made for you
Payload is not valid UTF-8 Skipped Secret Manager stores bytes and declares no type; a PKCS#12 keystore in a shared project must not break an unrelated application's startup
Payload checksum mismatch Fails the read (ErrChecksumMismatch) The adapter cannot trust what it received, and there is no honest way to carry on
Secret listed but gone by the time it is read Skipped A change to notice on the next poll, not a broken configuration
Service unreachable, or permission denied Fails the read The service saying no is not an operator saying no

Two of those deserve a sentence each.

The UTF-8 test is a genuine heuristic, and it is stated rather than papered over: a short binary payload can be accidentally valid UTF-8 and will be admitted as a nonsense string. That is the honest failure mode of a store that does not declare its types. Testing the data still beats trusting a label about it — which is also why SecretType: CERTIFICATE is not a skip condition: it is an operator-set declaration, not a statement that the service generated key material, and a PEM chain is serviceable text you may legitimately want.

The checksum check is belt-and-braces over TLS, and worth it anyway. A silently corrupted password does not fail here — it fails as an authentication error somewhere downstream, minutes later, at maximum distance from its cause, and the first hypothesis anybody forms is "the credential was rotated". Six lines of hash/crc32 turn that into an immediate, named failure. When the service supplies no checksum the check is skipped rather than failed.

Reading a project costs one request per secret

ListSecrets returns metadata carrying neither payloads nor version information, and there is no batch equivalent of AWS's BatchGetSecretValue, so loading a project of n secrets is one listing plus n AccessSecretVersion calls. That is the service's shape, not this adapter's, and a test pins the call counts so a refactor cannot change it quietly.

The watch polls metadata, not payloads

AccessSecretVersion is classified DATA_READ in Cloud Audit Logs — the stream an operator watches to answer "who read my secrets". GetSecretVersion and ListSecrets are ADMIN_READ. So a poll that re-read every payload every tick would bury genuine access in poll noise, in exactly the stream where it matters most.

Instead each tick reads version metadata and accesses only the secrets whose served version actually moved. For a project of n secrets, of which f are currently being served by a fallback and k change on a given tick:

Calls of which DATA_READ
Initial Load n + 1 n
Quiet poll n + 1 0
Poll with f in fallback n + f + 1 0
Poll with k changed n + f + k + 1 k

The steady state costs the same n + 1 either way; the extra call is paid only per changed secret, which was going to fetch a payload regardless. DATA_READ volume drops from n per tick to zero for a stable project. Note that Data Access logs are not enabled by default — this design earns its place with the security-conscious operator who has turned them on, who is also the one most likely to be running a secrets backend.

The change marker is the version actually SERVED, not the version latest names — the distinction the fallback forces. If version 8 is disabled and 7 is being served, the marker is 7, so enabling 8 fires a change and so does disabling 7. Marking latest would leave the marker on 8 throughout and notice neither. A newly appearing fallback also fires a change, even when the served version has not moved, because that is precisely the half-completed rotation you want to hear about.

The default interval is five minutes, slower than the rest of this toolkit, because each poll pays the per-secret cost above. WithPollInterval overrides it.

Secret Manager does have a change feed — Secret.Topics publishes control-plane operations to Pub/Sub — so this is the one adapter in its family that declines a real one. It declines it because the topics are configured on each secret by whoever provisions it, so requiring them would have the adapter dictating how your secrets are provisioned, and because a subscription brings acks, redelivery and dead-lettering that a config backend has no place owning. An opt-in Pub/Sub watch is tracked as a follow-on in its own module, so a consumer who polls never resolves a Pub/Sub client.

Credentials are yours

This adapter never authenticates, resolves a credential, or reads the environment. It uses the client you give it, which is what lets application default credentials, workload identity, a service account key or an impersonated principal all work without the adapter knowing any of them. It also configures no endpoint: it takes the location and trusts your client to match it.

The service account needs secretmanager.secrets.list, secretmanager.versions.access and secretmanager.versions.get — the last because the adapter reads a version's State field rather than inferring it from an error code.

A secrets layer is sensitive, so writes to it are refused

Every value here is a secret, so this backend declares itself Sensitive, and it is read-only. Because the layer is read-only, a write to a key it provides cannot land in Secret Manager — so it would otherwise fall through to the next writable layer, typically a plain YAML file. config refuses that write:

_, err := store.Apply(ctx, config.Set("db-password", "rotated"))
// errors.Is(err, config.ErrSensitiveLeak) — refused, NOT written to app.yaml

If a key needs to be writable, do not source it from Secret Manager.

What it costs

Modules added 39 — 31 for the Secret Manager SDK, 9 for the config graph, sharing one
Requires config v0.9.2+

Against the siblings, counting the SDK-attributable graph the same way:

Adapter SDK modules
config-aws-secrets 5
config-azure-keyvault 6
config-vault 17
config-gcp-secret 31

Stated without softening, because a reader comparing the four should know it before choosing. Three things make it defensible rather than merely regrettable:

  • It is not the adapter's doing. It is the irreducible Google Cloud Go client stack — gRPC, the API transport, genproto, protobuf, the auth stack and OpenTelemetry. Any first-party GCP integration pays it.
  • It is not additive for a GCP consumer. A workload already talking to any Google API has substantially all of this resolved, so the marginal cost is small even though the absolute figure is large.
  • It is not silently incurred. An allowlist test pins the set in both directions — and pins the SDK's 31 separately — so a version bump that widens or narrows the graph fails the build rather than arriving quietly.

Install

go get gitlab.com/phpboyscout/go/config-gcp-secret

Running the integration suite

Secret Manager has no emulator, so ./test/integration talks to a real project. It skips cleanly without one — an ordinary go test ./... needs no GCP account — and stays compiled either way.

export INT_TEST_INTEGRATION=1
export GCP_SECRET_PROJECT=my-sandbox-project    # not GOOGLE_CLOUD_PROJECT — see below
export GCP_SECRET_LOCATION=europe-west2         # optional; omit for global secrets
go test ./test/integration/ -v

Credentials come from application default credentials (gcloud auth application-default login, a service account key, workload identity — whatever you already use). The service account needs create, access, list, get, disable, destroy and delete on secrets, since the suite builds and tears down its own fixtures.

The project is named by its own variable rather than GOOGLE_CLOUD_PROJECT on purpose. That one is exported on any machine where somebody has run gcloud config set project, so honouring it would mean enabling the gate for some other module's suite silently created and deleted secrets in whatever project your shell happened to point at. This suite bills real resources; it targets a project you named deliberately or none at all.

Every secret is namespaced cfgint-<run-id>-<test> and removed via t.Cleanup, because anything left behind costs money every month it survives.

[!NOTE] This suite is the release gate. It is not a smoke test: each case asserts one specific claim the specification records — that latest resolves to the most recently created version even when disabled, that the failure is FAILED_PRECONDITION and not PERMISSION_DENIED, that GetSecretVersion still reports a disabled version's state, that the listing carries no payloads, that name: really is substring containment — and says in its comment what it would mean for that claim to be false. A failure is a dated specification revision, not necessarily a bug.

Documentation

Full documentation lives with the parent module at config.go.phpboyscout.uk. The Go API reference is on pkg.go.dev.

Licence

MIT

Documentation

Overview

Package configgcpsecret contributes secrets from Google Cloud Secret Manager as a first-class config layer: precedence, per-key merge, provenance and safe hot-reload, exactly like a file layer — but marked sensitive, so the core refuses to write a value sourced from here into a plainer layer beneath.

A secret ID may contain only letters, digits, hyphens and underscores, so there is no hierarchy to map: an ID becomes a config key verbatim, hyphens and all. Structure comes from the other mode instead, where one secret holds a whole document.

Reading a project costs one listing plus one request per secret, because the listing carries neither payloads nor version information. That is the service's shape, not this adapter's.

A disabled newest version is served from an older one

`projects/*/secrets/*/versions/latest` is an alias to the most recently CREATED version — not the most recently created ENABLED one. So an operator who adds version 8 and then disables it leaves `latest` pointing at a version that cannot be accessed, even though version 7 holds a working credential.

This adapter falls back: it serves the newest ENABLED version instead. That keeps an application running through a half-completed rotation, and it means the adapter may be serving a credential somebody deliberately withdrew, while the console shows version 8 as current. WithFallbackObserver is how a consumer learns it happened, because provenance cannot carry a per-key fact.

The project is reached through the narrow API interface, which Wrap adapts from a configured *secretmanager.Client. Injecting the client keeps every credential, project and endpoint decision with the consumer — the adapter never authenticates — and lets the whole unit suite run against a fake.

See the config-gcp-secret spec for the decisions behind all of this.

Index

Constants

View Source
const DefaultPollInterval = 5 * time.Minute

DefaultPollInterval is how often the backend polls for change when the consumer does not say otherwise.

Five minutes, deliberately slower than most of this family. A poll costs one listing plus one metadata read per secret, so a sixty-second cadence over a fifty-secret project would be fifty-one requests a minute, indefinitely.

View Source
const SourceKind = config.SourceKind("gcp-secret")

SourceKind is what a layer from this backend reports as, so provenance can name Secret Manager as the origin of a value.

Variables

View Source
var ErrChecksumMismatch = errors.NewSentinel("config-gcp-secret.checksum_mismatch", "configgcpsecret: payload checksum mismatch")

ErrChecksumMismatch reports that a payload's bytes did not match the checksum the service stored with them.

It fails the read rather than skipping the secret, which is the opposite of how a withdrawn version or a binary payload is treated. Those are cases where the store is saying something coherent; a checksum mismatch means the adapter cannot trust what it received, and there is no honest way to carry on past that. A silently corrupted password does not fail here — it fails as an authentication error somewhere downstream, minutes later, at maximum distance from its cause.

View Source
var ErrNoProject = errors.NewSentinel("configgcpsecret.no_project",
	"no Google Cloud project supplied; pass the project the secrets live in")

ErrNoProject reports a rung called with no project id. Unlike a region on AWS, there is no ambient convention that can supply one: Application Default Credentials authenticate a principal, they do not name the project whose secrets to read.

Functions

func FromClient

func FromClient(
	client *secretmanager.Client, project, location string, opts ...Option,
) config.Backend

FromClient returns a backend reading a project's secrets through a configured client — the convenience path over New and Wrap.

client must already carry its credentials and its endpoint. This adapter never authenticates and never resolves a credential: application default credentials, workload identity, a service account key or an impersonated principal all work here precisely because the adapter knows about none of them.

location is the region for a regionalised secret, or empty for the ordinary project-level parent. The adapter takes it and trusts the client's endpoint to match; it validates neither, because that is the consumer's to configure, and a mismatch surfaces naturally as a NotFound.

func FromClientSecret

func FromClientSecret(
	client *secretmanager.Client, project, location, id string,
	codec config.Codec, opts ...Option,
) config.Backend

FromClientSecret returns a backend reading ONE secret, whose payload is a whole document, through a configured client — the convenience path over NewSecret and Wrap. The same client rules as FromClient apply.

func New

func New(api API, opts ...Option) config.Backend

New returns a backend contributing a project's secrets as one config layer, each secret's ID becoming a key verbatim.

api is the injected client: a fake in tests, a Wrap-ped *secretmanager.Client in production.

func NewSecret

func NewSecret(api API, id string, codec config.Codec, opts ...Option) config.Backend

NewSecret returns a backend contributing ONE secret, whose payload is a whole document, as a config layer.

This is how structure is expressed against a store whose IDs cannot carry it: the tree comes from the document rather than from the key space. It also costs a single request, where reading a project costs one per secret.

The codec is a parameter rather than an option because it is not optional here — a payload is one opaque blob, so without something to decode it there is no tree to contribute, and expressing a non-optional dependency as an Option would only move the failure from compile time to run time.

The rules that SKIP a secret in flat mode FAIL the Load here, for the same reason: the consumer named one secret as their entire configuration source, so an empty layer with no explanation is worse than a refusal. That applies to a secret with no enabled version and to a payload that is not valid UTF-8.

Types

type API

type API interface {
	// Access reads one secret's payload at the given version ("" means
	// "latest"). It returns the resolved version resource name alongside the
	// bytes, so a single call yields both the value and the watch's change
	// marker.
	//
	// A secret or version that does not exist returns fs.ErrNotExist. A version
	// that exists but is not ENABLED returns some other error, deliberately not
	// one this adapter matches on: usability is established from Describe's
	// State field, because a field is a contract and an error code is a hope.
	Access(ctx context.Context, id, version string) (Payload, error)

	// Describe reads one version's metadata — its resolved name and state —
	// without the payload. It drives the poll and the fallback resolution, and
	// it succeeds for a version that Access would refuse.
	Describe(ctx context.Context, id, version string) (Version, error)

	// Versions lists a secret's versions, newest first, for the fallback walk.
	// It is called only once Describe has reported that latest is not ENABLED,
	// so the common path never pays for it.
	Versions(ctx context.Context, id string) ([]Version, error)

	// List returns every secret's metadata under the configured parent,
	// narrowed by filter server-side. It carries neither payloads nor version
	// information — the service does not include them.
	List(ctx context.Context, filter string) ([]Secret, error)
}

API is the slice of Secret Manager this adapter uses, behind an interface it owns so a fake drives the unit suite and the real client is adapted by Wrap.

Read-only: there is no write method, because writing a secret from a configuration library waits for a spec that justifies it.

func Wrap

func Wrap(client *secretmanager.Client, project, location string) API

Wrap adapts a real Secret Manager client to the narrow API interface.

The two parent forms differ in shape rather than in value: a global secret is projects/{project}/secrets/{id}, while a regionalised one is projects/{project}/locations/{location}/secrets/{id}. The location segment is ABSENT for the ordinary case rather than being the literal "global", so location == "" means the project-level parent.

type Fallback

type Fallback struct {
	// ID is the secret ID, and the config key.
	ID string

	// LatestVersion is the resource name `latest` resolved to.
	LatestVersion string

	// LatestState is that version's state: DISABLED or DESTROYED.
	LatestState string

	// ServedVersion is the resource name of the version actually served, or
	// empty when no version was enabled at all and the secret was skipped.
	ServedVersion string
}

Fallback reports one secret whose latest version was not usable.

It is this module's answer to a limitation it cannot fix. config.Source has four fields and is per-layer, while a fallback is per-key — in flat mode one layer carries every secret in the project, so no single string could say which of them fell back. Origin and Explain both return a Source, so neither can surface it either. A callback at the moment it happens is the next best thing, and is offered as that rather than as an equivalent.

type Option

type Option func(*backend)

Option configures a backend.

func WithFallbackObserver

func WithFallbackObserver(fn func(Fallback)) Option

WithFallbackObserver reports every secret that did not resolve to `latest`.

This is the mitigation that made the fallback acceptable rather than a footnote to it. Without it, a rotation that half-completed — the new version written and then withdrawn — keeps the application working while nobody learns the rotation did not finish, and the failure surfaces later when the old credential is revoked in its turn.

fn is called synchronously during Load, on the Store's goroutine, once per affected secret. It must not block and must not call back into the Store. A callback rather than a log line because this module has no logger of its own; wire it to whatever you already alert on.

func WithLabel

func WithLabel(key, value string) Option

WithLabel reads only the secrets carrying label key=value, composing into an ANDed server-side filter.

This is a real capability the Key Vault sibling could not have, and it is the one that matters most on the N+1 path: the filter is applied before the listing leaves the service, so every secret it removes is one payload read never made. Label-scoping is also how GCP teams already partition a shared project.

Repeatable. Two calls narrow to secrets carrying both.

func WithNamePrefix

func WithNamePrefix(prefix string) Option

WithNamePrefix reads only the secrets whose IDs begin with prefix.

Both halves of this are load-bearing. The service's `name:` filter is case-insensitive SUBSTRING containment, not prefix matching — `name:app` matches `legacy-app-token` as readily as `app-db-password` — so the filter is sent as a server-side narrowing HINT and the prefix is then re-checked client-side. The hint saves the per-secret fetches that scale; the re-check keeps the promise this option's name makes.

The prefix is not stripped: a secret ID is a config key verbatim, and removing part of it here would reintroduce, through the back door, exactly the name-rewriting this adapter refuses to do.

func WithPollInterval

func WithPollInterval(d time.Duration) Option

WithPollInterval sets how often the backend polls for change. The default is DefaultPollInterval.

func WithValueCodec

func WithValueCodec(codec config.Codec) Option

WithValueCodec decodes each secret'"'"'s payload through codec in the flat mode: a payload that decodes to a mapping becomes a subtree under the secret'"'"'s own ID, and a payload the codec rejects — a bare password — stays a scalar string, so a project mixing plain secrets and documents reads correctly.

The ID is still the key either way: this adds structure BENEATH a key, never structure between keys, which the naming rules forbid.

Optional here and required in document mode, and the asymmetry is deliberate: there the names supply no structure, so one opaque blob has no tree without a codec, while here a rejected payload can honestly fall back to a scalar.

func WithVersion

func WithVersion(version string) Option

WithVersion pins the version read for every secret — a number ("7") or an alias an operator has defined on the secret.

A pinned version is honoured exactly and NEVER falls back. If it is disabled or destroyed, the Load fails. The consumer named a specific version, so silently substituting another would defeat the point of naming it and would turn an explicit instruction into a guess. The asymmetry between `latest` (fall back, and say so) and a pinned version (fail) is exactly the difference between the adapter choosing and the consumer having chosen.

The default is `latest`, which is what a config layer wants: pinning by default would freeze the layer past a rotation.

type OwnedBackend added in v0.3.0

type OwnedBackend struct {
	config.Backend
	// contains filtered or unexported fields
}

OwnedBackend is a backend that built its own Secret Manager client and is therefore responsible for closing it.

It exists because secretmanager.NewClient is not like its AWS and Azure counterparts: it opens a gRPC connection, returns an error, and its own documentation says the client "must be Closed when it is done being used to clean up its underlying connections". config.Backend has no Close, and a Store drops a withdrawn backend without teardown, so a client an adapter resolved for itself would otherwise live for the whole process.

A one-shot CLI can ignore Close and let process exit reclaim it. A long-lived service that adds and withdraws backends should not:

b, err := configgcpsecret.Default(ctx, "my-project", "")
if err != nil { return err }
defer b.Close()

store, err := config.NewStore(ctx, config.WithBackend(b))

The rungs that do NOT build a client — New, Wrap and FromClient — return a plain config.Backend, because there the consumer built it and still owns it (spec 0012 L-7, R2).

func Default added in v0.3.0

func Default(ctx context.Context, project, location string, opts ...Option) (*OwnedBackend, error)

Default builds a Secret Manager client from Application Default Credentials — the metadata server, a workload identity, GOOGLE_APPLICATION_CREDENTIALS, or gcloud's own login — and returns a backend that owns it.

This is the zero-conf rung. Unlike its AWS and Azure counterparts it still needs the project, because ADC authenticates a principal without naming the project whose secrets to read; there is no ambient convention that supplies one, and guessing would read another project's secrets.

To share ONE credential across this adapter and its GCP siblings, resolve it with gitlab.com/phpboyscout/go/gcpclient and pass the options it yields to FromOptions — that collapses three credential detections to one.

The returned backend owns the client and must be Closed; see OwnedBackend.

func FromOptions added in v0.3.0

func FromOptions(
	ctx context.Context, project, location string, clientOpts []option.ClientOption, opts ...Option,
) (*OwnedBackend, error)

FromOptions builds a Secret Manager client from client options the caller holds — an explicit credentials file, an emulator endpoint, a credential resolved once and shared — and returns a backend that owns it.

location is the regionalised parent, or "" for the ordinary project-level one.

The returned backend owns the client and must be Closed; see OwnedBackend.

func (*OwnedBackend) Close added in v0.3.0

func (b *OwnedBackend) Close() error

Close releases the Secret Manager client this backend built. It is safe to call more than once.

type Payload

type Payload struct {
	// VersionName is the resolved resource name of the version that served
	// these bytes. It is the watch's change marker: a rotation moves it, and
	// so does a fallback resolving somewhere new.
	VersionName string

	// Data is the payload. Secret Manager stores bytes with nothing declaring
	// whether they are text, so a payload that is not valid UTF-8 contributes
	// no key.
	Data []byte

	// CRC32C is the Castagnoli checksum the service stored with the payload,
	// encoded as an int64 for wire compatibility. It is verified when non-nil
	// and skipped when nil.
	CRC32C *int64
}

Payload is one resolved access.

type Secret

type Secret struct {
	// ID is the last path segment, and the config key.
	ID string

	Labels      map[string]string
	Annotations map[string]string
	Etag        string
}

Secret is one listed secret's metadata.

Labels and Annotations are surfaced for the consumer and, for labels, for server-side filtering. Neither ever selects a decoder: a free-text hint is not a contract, and decoding on one would be guessing at a format the store does not enforce.

type Version

type Version struct {
	// Name is the resolved resource name — projects/*/secrets/*/versions/7 —
	// and never the literal string "latest".
	Name string

	// State is ENABLED, DISABLED or DESTROYED. Only ENABLED can be accessed.
	State string
}

Version is one version's metadata, without its payload.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL