configgcpparameter

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 15 Imported by: 0

README

config-gcp-parameter

Read GCP Parameter Manager as a config layer 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


config reads files. GCP Parameter Manager — GCP's purpose-built store for workload parameters (application configuration as versioned key-value data) — is a sibling backend module like this one, so a consumer who configures from it takes it and one who does not pays nothing for it.

Parameter Manager is the configuration service, not the deprecated Runtime Configurator and not Secret Manager (which holds secrets). This adapter is therefore read-only and not sensitive: it returns the raw, unrendered payload and never resolves Secret Manager references, so no secret enters a layer.

You build and configure the client — that is where every credential, project, scope and (for a regional parameter) endpoint decision lives — and hand it in with the project, location and the parameter or prefix to read:

import (
	parametermanager "cloud.google.com/go/parametermanager/apiv1"
	"gitlab.com/phpboyscout/go/config"
	configgcpparameter "gitlab.com/phpboyscout/go/config-gcp-parameter"
	configjson "gitlab.com/phpboyscout/go/config-json"
)

client, _ := parametermanager.NewClient(ctx)

store, err := config.NewStore(ctx,
	config.WithFiles(fsys, "/etc/app.yaml"),                              // YAML defaults
	config.WithBackend(configgcpparameter.FromClient(                     // Parameter Manager outranks them
		client, "my-proj", "global", "app-config",
		configgcpparameter.WithValueCodec(configjson.Codec{}))),
)

A Parameter Manager layer takes part in precedence, per-key merge, provenance and hot-reload exactly as a file does.

Two data-model modes

A Parameter Manager parameter is a heavyweight resource (its own IAM, format, versions, CMEK), designed to hold a whole configuration document. So there are two ways to read:

Single-document (default, New / FromClient). One named parameter's current version is the whole layer's source document, decoded into the nested tree:

parameter "app-config" holds  {"server":{"host":"localhost","port":8080}}
                          →    store.View().GetString("server.host") // "localhost"

Prefix mode (NewPrefix / FromClientPrefix). For consumers who keep many small parameters, every parameter whose id begins with a prefix is scanned and nested as a leaf — the id, with the prefix stripped and -// split into path segments, is the key path:

app-server-host = "localhost"        store.View().GetString("server.host") // "localhost"
app-server-port = "8080"        →    store.View().GetInt("server.port")    // 8080

Prefix mode costs one payload fetch per matching parameter, which is why single-document is the default.

Values: strings, or decoded documents

Parameter Manager stores bytes. By default every payload is a scalar string and the View's typed accessors coerce it (GetInt parses "8080"). Parameter Manager declares a format (Unformatted / YAML / JSON) — surfaced on Parameter.Format for diagnostics — but the adapter never uses that to pick a decoder. To decode a JSON or YAML document, pass a value codec — any config.Codec, which the sibling format adapters implement:

configgcpparameter.FromClient(client, "my-proj", "global", "app-config",
	configgcpparameter.WithValueCodec(configjson.Codec{}))

A payload that decodes to an object becomes a subtree; a payload the codec rejects stays a string, so a store mixing documents and scalars reads correctly. You inject the one format your store actually holds, so this module takes no codec dependency of its own.

Watching

Parameter Manager has no native change feed, so Watch is a poll, and it says so (Capabilities.NativeWatch is false). Each tick re-resolves the current version fingerprint and reports a change. The cadence is WithPollInterval, defaulting to 60s — deliberately conservative, because unlike a long-poll every tick is a billed API call.

The backend advertises that cadence to the Store through config.PollIntervalHinter, so a default Store.Watch adopts it instead of the eager 2s file-poll default. An explicit config.WithPollInterval on the Store overrides the hint and flows straight through to Watch.

Injecting the client

FromClient / FromClientPrefix wrap the real SDK client. For testing, or to supply your own wrapper, New / NewPrefix take the narrow PM interface directly and Wrap adapts a *parametermanager.Client to it — so the whole unit suite runs against a fake, with no GCP.

What it costs

The config graph plus the full Google Cloud Go client stack behind cloud.google.com/go/parametermanager — gRPC, the API transport, genproto, protobuf, OAuth2/auth and OpenTelemetry. This is the heaviest graph in the config family, the honest cost of a first-party cloud SDK, asserted by an allowlist test so an unforeseen transitive addition fails the build rather than arriving quietly.

Because Parameter Manager has no emulator, the real-service integration suite (under ./test/integration) is gated on INT_TEST_INTEGRATION=1 and a real project — it never runs in the merge gate and is a test-only dependency that reaches no consumer.

License

See LICENSE.

Documentation

Overview

Package configgcpparameter contributes configuration from Google Cloud Parameter Manager as a first-class config layer: precedence, per-key merge, provenance and safe hot-reload, exactly like a file layer.

Parameter Manager is GCP's purpose-built store for workload parameters — application configuration as versioned key-value data — not the deprecated Runtime Configurator and not Secret Manager, which holds secrets. This adapter is therefore configuration, read-only, and not sensitive: it returns the raw, unrendered payload and never resolves Secret Manager references, so no secret enters a layer. See the config-gcp-parameter spec, D2 and D5.

Two data-model modes select how one or many parameters become a layer:

  • Single-document (default, New/FromClient): one named parameter's current version payload is the whole layer's source document, decoded into the nested tree through an injected config.Codec (WithValueCodec).
  • Prefix mode (NewPrefix/FromClientPrefix): every parameter whose id begins with a prefix is scanned and nested as a leaf — the Consul-shaped model — at one payload fetch per matching parameter.

Parameter Manager carries a declared Format field (Unformatted, YAML, JSON), surfaced on Parameter for diagnostics, but it never selects a codec: decoding is driven by an explicitly injected config.Codec only, so this module takes no codec dependency of its own (spec D4). A payload the codec rejects stays a scalar string, so enabling a codec never turns a value into a load error.

The service is reached through the narrow PM interface, which Wrap adapts from a configured *parametermanager.Client. Injecting the client keeps every credential, project, location, endpoint and scope decision with the consumer and lets the whole unit suite run against a fake, needing no GCP.

Index

Examples

Constants

View Source
const SourceKind = config.SourceKind("gcpparameter")

SourceKind is what a Parameter Manager layer reports as, so provenance can name it — a value renders as "gcpparameter:projects/.../versions/3".

Variables

View Source
var (
	// 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
	// parameters to read.
	ErrNoProject = errors.NewSentinel("configgcpparameter.no_project",
		"no Google Cloud project supplied; pass the project the parameters live in")

	// ErrNoLocation reports a rung called with no location.
	//
	// Parameter Manager, unlike Secret Manager, has no project-level parent: every
	// parameter lives under a location, and "global" is the ordinary one. An empty
	// location would address nothing.
	ErrNoLocation = errors.NewSentinel("configgcpparameter.no_location",
		`no Google Cloud location supplied; pass "global" unless the parameters are regionalised`)
)

Functions

func FromClient

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

FromClient is the common single-document path: New over a Wrap-ped Parameter Manager client, so a consumer writes configgcpparameter.FromClient(client, "my-proj", "global", "app-config") without touching the narrow interface.

Example

ExampleFromClient reads a single configuration document from GCP Parameter Manager. You build and configure the client — project, credentials, and (for a regional parameter) the endpoint all stay yours — and hand it in with the project, location and parameter id. A value codec decodes the parameter's JSON or YAML payload into the nested tree.

package main

import (
	"context"
	"fmt"
	"log"

	parametermanager "cloud.google.com/go/parametermanager/apiv1"
	"gitlab.com/phpboyscout/go/config"

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

	configgcpparameter "gitlab.com/phpboyscout/go/config-gcp-parameter"
)

func main() {
	ctx := context.Background()

	client, err := parametermanager.NewClient(ctx)
	if err != nil {
		log.Fatal(err)
	}

	store, err := config.NewStore(ctx,
		config.WithBackend(configgcpparameter.FromClient(client, "my-proj", "global", "app-config",
			configgcpparameter.WithValueCodec(configjson.Codec{}))),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(store.View().GetInt("server.port"))
}

func FromClientPrefix

func FromClientPrefix(client *parametermanager.Client, project, location, prefix string, opts ...Option) config.Backend

FromClientPrefix is the common prefix-mode path: NewPrefix over a Wrap-ped client, scanning every parameter under prefix.

Example

ExampleFromClientPrefix reads many small parameters under a prefix, nesting each as a leaf: app-server-host and app-server-port become server.host and server.port.

package main

import (
	"context"
	"fmt"
	"log"

	parametermanager "cloud.google.com/go/parametermanager/apiv1"
	"gitlab.com/phpboyscout/go/config"

	configgcpparameter "gitlab.com/phpboyscout/go/config-gcp-parameter"
)

func main() {
	ctx := context.Background()

	client, err := parametermanager.NewClient(ctx)
	if err != nil {
		log.Fatal(err)
	}

	store, err := config.NewStore(ctx,
		config.WithBackend(configgcpparameter.FromClientPrefix(client, "my-proj", "global", "app-")),
	)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(store.View().GetString("server.host"))
}

func New

func New(pm PM, parameter string, opts ...Option) config.Backend

New returns a backend contributing one named parameter's current version as a config layer — single-document mode (spec D3a, the default). pm is the injected client — a fake in tests, a Wrap-ped client in production; parameter is the parameter id, resolved to its latest enabled version at Load.

func NewPrefix

func NewPrefix(pm PM, prefix string, opts ...Option) config.Backend

NewPrefix returns a backend scanning every parameter whose id begins with prefix and nesting each as a leaf — prefix mode (spec D3b). It costs one payload fetch per matching parameter, the honest price of the fan-out.

Types

type Format

type Format int

Format mirrors the service's immutable ParameterFormat, so the adapter never exposes the raw protobuf enum. It is surfaced for diagnostics and provenance but never used to pick a codec (spec D4).

const (
	// Unformatted is plain text or a custom format.
	Unformatted Format = iota
	// YAML is a declared YAML document.
	YAML
	// JSON is a declared JSON document.
	JSON
)

The parameter formats Parameter Manager declares. The values are this module's own; Wrap maps the service enum onto them.

func (Format) String

func (f Format) String() string

String renders a Format for diagnostics.

type Option

type Option func(*backend)

Option configures a backend.

func WithPollInterval

func WithPollInterval(d time.Duration) Option

WithPollInterval sets this backend's own poll cadence — how often config.WatchableBackend Watch re-resolves the change fingerprint. It is also the cadence the backend advertises through config.PollIntervalHinter, so a default config.Store.Watch adopts it; an explicit config.WithPollInterval on the Store still overrides it. Omitted, the default is 60s, deliberately conservative because each tick is a billed API call (spec D7).

func WithValueCodec

func WithValueCodec(codec config.Codec) Option

WithValueCodec decodes a parameter's payload through codec: a payload that decodes to a mapping becomes a subtree (in single-document mode, the whole layer's tree; in prefix mode, that parameter's leaf), and a payload the codec rejects — a bare scalar, or bytes that are not a document — stays a scalar string, so a store mixing documents and scalars reads correctly. Omitted, every payload is a scalar string.

codec is any config.Codec — the interface the sibling format adapters implement — so a JSON-document store is read with configjson.Codec{} and a YAML-document store with the core's, and this module takes no codec dependency of its own (spec D4).

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 Parameter Manager client and is therefore responsible for closing it.

parametermanager.NewClient 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 := configgcpparameter.Default(ctx, "my-project", "global", "app-config")
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, NewPrefix, Wrap, FromClient and FromClientPrefix — 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, parameter string, opts ...Option,
) (*OwnedBackend, error)

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

This is the zero-conf rung. Unlike its AWS and Azure counterparts it still needs the project and location, because ADC authenticates a principal without naming the parameters to read; guessing would read another project's.

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

func DefaultPrefix added in v0.3.0

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

DefaultPrefix is Default for the prefix shape.

func FromOptions added in v0.3.0

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

FromOptions builds a client from client options the caller holds — an explicit credentials file, an emulator endpoint, or a credential resolved once with go/gcpclient and shared across the GCP siblings — and returns a single-parameter backend that owns it.

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

func FromOptionsPrefix added in v0.3.0

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

FromOptionsPrefix is FromOptions for the prefix shape: every parameter under the prefix becomes one layer.

func (*OwnedBackend) Close added in v0.3.0

func (b *OwnedBackend) Close() error

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

type PM

type PM interface {
	// Get returns one parameter's declared format and current (latest enabled)
	// version — its resource name and raw payload bytes. A parameter or version
	// that does not exist returns fs.ErrNotExist. Drives single-document mode.
	Get(ctx context.Context, parameter string) (Parameter, error)

	// List returns every parameter whose id begins with prefix, each resolved to
	// its current version — id, format, version name and raw payload. An empty
	// result is not an error. Drives prefix mode; it is the fan-out whose
	// per-parameter payload fetch is the honest cost of that mode.
	List(ctx context.Context, prefix string) ([]Parameter, error)
}

PM is the slice of Parameter 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 (spec D5): there is no write method.

func Wrap

func Wrap(client *parametermanager.Client, project, location string) PM

Wrap adapts a configured Parameter Manager SDK client to the narrow PM interface. The client carries every credential, scope and — for a regional parameter — endpoint decision; Wrap adds none. project and location compose the resource path; location is the segment the consumer's client endpoint must match (spec D6, D8), which the adapter trusts rather than validates.

type Parameter

type Parameter struct {
	// ID is the parameter id (the last path segment), for prefix-mode nesting.
	ID string
	// Format is the format the parameter declares — informational only.
	Format Format
	// VersionName is the full version resource name — the watch change
	// fingerprint and the layer's provenance.
	VersionName string
	// Payload is the raw, unrendered version payload bytes.
	Payload []byte
}

Parameter is one resolved read: the parameter's id, the format it declares (surfaced but not used to pick a codec), and its current version.

Jump to

Keyboard shortcuts

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