Adding a Store
Can the agent learn a tenth store — Google Cloud Secret Manager, say? Yes, and what follows is the map. What it is NOT is a plugin system: stores are compiled in on purpose (a config agent that loads plugins is its own supply-chain problem), and the organisation's standing rule is stores land by demand, not by roadmap — the ask starts as an issue, and the walkthrough below is what the work looks like once agreed.
What a store is
One Rust type implementing one of the engine's two traits:
impl AsyncRemoteSource for GcpSecretManager {
fn fetch(&self) -> BoxFuture<'_, Result<Fetched, Error>>; // text + format
fn describe(&self) -> String; // REDACTED — errors quote it
}
fetch answers the document's text and format; describe names the
store in errors and logs and must never carry a credential. Blocking
clients implement RemoteSource instead; the agent drives both — the
blocking six run under a blocking task, the async three (etcd, NATS,
S3) on the runtime directly. A new store picks whichever its client
dialect is.
The walkthrough, with GCP Secret Manager as the worked example
The 0.1.1 change that landed etcd, NATS and S3 is the living template —
git log --grep "async three" in the two repositories shows every
touchpoint below with real diffs.
1. The store crate (dynamic-config-remote repository,
dynamic-config-gcpsm/). The Firestore crate is the closest twin:
Google auth, same project-scoped shape. The heart is small:
pub struct GcpSecretManager { project: String, secret: String, auth: Auth, /* … */ }
impl AsyncRemoteSource for GcpSecretManager {
fn fetch(&self) -> BoxFuture<'_, Result<Fetched, Error>> {
Box::pin(async move {
// GET https://secretmanager.googleapis.com/v1/projects/{p}
// /secrets/{s}/versions/latest:access
// Authorization: Bearer <metadata-server token>
let text = self.access_latest().await?;
Ok(Fetched { text, format: self.format })
})
}
fn describe(&self) -> String {
format!("gcp-sm {}/{}", self.project, self.secret) // no token, ever
}
}
Identity-first applies: the default auth is the metadata server
(Workload Identity — the pod's own identity, no distributed secret),
exactly as the Firestore crate already does; an access-token arm
stays a supported peer. House rules the crate must keep: describe
redacted, TLS through the shared TlsConfig vocabulary, errors mapped
to the engine's kinds (auth for 401/403, remote for the rest),
#![forbid(unsafe_code)].
2. The agent (dynamic-config-agent/src/):
sources.rs— one async constructor arm besideetcd/nats/s3, returningBuilt::Async(Arc::new(store)).spec.rs— the source joins thevalidated()list, its auth rules joinvalidated_auth()(refusals name the fix; that is the house voice), andUSAGElearns the flags. Tests beside the etcd ones.
3. The webhook (dynamic-config-webhook/src/annotations.rs) — the
source name joins the admission allowlist, and its auth annotations
join the validation match so a typo is refused at admission, in
the pod's events, not twenty minutes later in a crashloop. A golden
test pins the happy path.
4. The paper trail — a store page in this book (auth methods, full
manifests, the identity-first default stated), a ready-to-apply file
in examples/, a row in the annotation reference's source table, and
— for a store with a live server in CI — an e2e leg shaped like
e2e/stores-smoke.sh — one cluster, every live store, a stage per source.
5. The gates — just check in both repositories, the webhook
goldens, the kind smoke. Nothing about the release train changes: the
store crate ships from dynamic-config-remote, the agent picks it up
on the next k8s release.
A store in another language
Three lanes, all of them already open:
- In-process, Python: subclass the binding's
RemoteSourceABC —fetch()anddescribe()— and hand the instance toDynamicConfig.remote(...). The Python book owns the chapter. - In-process, Node: any JS object with
fetch()anddescribe()throughuseStore(config, store)— the Node remote package's contract. - Out-of-process, ANY language — the one the agent can use: the
agent cannot load Go or Java, but it speaks the
config server's small HTTP contract. Implement
GET /{application}/{profile}(bearer-token auth, the resolved document as the body) in your language, and every consumer here — the agent's--source config-server, the engine, both bindings — reads it like any other store. A Go service fronting your in-house store is an afternoon: one endpoint, one auth header, and TLS. The remote book's server chapter tables the full route surface,/streamincluded for push.
Today, without code
Two honest compositions cover most "we need store X now" cases:
- External Secrets Operator in front: ESO syncs GCP SM (or any of
its many backends) into a Kubernetes Secret, and everything on
The Three Deliveries consumes that Secret —
mounted as a live file,
envFrom, orsecretKeyRef. You trade the document model for breadth, which is exactly the trade the comparison table prices. - A mirror job: a CronJob copies the document from the unsupported store into one the agent speaks (S3, git, Consul). Crude, visible, and often all a migration window needs.