GCP secret manager resolver

Config resolver that swaps gcp-secret: placeholders for live values fetched from Google Cloud Secret Manager.

Overview

The Piko config loader resolves <prefix>:<value> placeholder strings against any registered resolver. The config_resolver_gcp package handles the gcp-secret: prefix. The loader passes the placeholder body to GCP Secret Manager through the official secretmanager client, and the secret version's value replaces the placeholder in your config struct. The placeholder body must be the full secret version resource name, for example projects/my-project/secrets/db-password/versions/latest. A #<key> suffix selects a field inside a JSON-shaped secret.

The resolver implements the shared config.Resolver port, verified by a compile-time assertion in the package. It drops into the same placeholder pipeline as the built-in env:, base64:, and file: resolvers with no bespoke glue. Resolve calls run inside the loader's circuit breaker, so the loader contains a flapping Secret Manager backend with machinery you do not write.

The resolver uses Application Default Credentials, GOOGLE_APPLICATION_CREDENTIALS, gcloud auth application-default, or a workload identity binding when running on GKE. The constructor accepts no static credentials.

Requirements

  • A separate Go module dependency. The package lives in its own module, piko.sh/piko/wdk/config/config_resolver_gcp, and pulls the full cloud.google.com/go/secretmanager gRPC stack. Add it as a distinct dependency. The built-in env:, base64:, and file: resolvers carry no such dependency.
  • GCP Application Default Credentials available at startup. On GKE this typically means a workload-identity-bound service account. Locally it means a JSON key file pointed to by GOOGLE_APPLICATION_CREDENTIALS.
  • IAM role roles/secretmanager.secretAccessor, or the more granular secretmanager.versions.access permission, on the secret resources you reference.
  • Network egress to secretmanager.googleapis.com.

The package is pure Go with no build tags and no CGO. It runs in any Piko run mode, including interpreted dev-i mode.

Configuration

NewResolver takes a context.Context because the GCP client constructor accepts one (used during transport setup).

import (
    "context"

    "piko.sh/piko/wdk/config/config_resolver_gcp"
)

resolver, err := config_resolver_gcp.NewResolver(ctx)
if err != nil {
    return err
}

Reference secrets in your config struct using the gcp-secret: prefix:

database:
  password: gcp-secret:projects/my-project/secrets/db-password/versions/latest
stripe:
  secret_key: gcp-secret:projects/my-project/secrets/api-keys/versions/3#stripe

Bootstrap

Pass the resolver to the container with piko.WithConfigResolvers. The resolver satisfies the piko.ConfigResolver type directly, so you write no adapter.

import (
    "piko.sh/piko"
    "piko.sh/piko/wdk/config/config_resolver_gcp"
)

resolver, err := config_resolver_gcp.NewResolver(ctx)
if err != nil {
    return err
}

ssr := piko.New(
    piko.WithConfigResolvers(resolver),
)
if err := ssr.Run(command); err != nil {
    return err
}

The package also exports a Register(ctx) shortcut. It constructs a resolver and registers it in the global resolver registry, which suits an init() call. Register and WithConfigResolvers are alternatives, not both required. Use WithConfigResolvers to pass the resolver straight to the container, or call Register once to make it available to every loader that inherits the global registry.

See also

Sibling resolvers:

Companion provider:

Framework docs:

External: