Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Firestore

dynamic-config-firestore reads configuration from a Google Cloud Firestore document, over its REST API: the blocking RemoteSource.

[dependencies]
dynamic-config = "<version>"
dynamic-config-firestore = "<version>"
#![allow(unused)]
fn main() {
use dynamic_config_firestore::{Auth, Firestore};

DbConfig::set_remote(
    Firestore::new("my-project", "config/db").with_auth(Auth::metadata_server()),
);
DbConfig::refresh_remote()?;
}

What it reads: one document at collection/document; its fields become the configuration, wrapped under the section key — the same shape as Vault, for the same reason: a map of named fields.

Several documents, one section: Keys::several(["config/db", "overrides/db"]) reads both and merges them under that same section key, in call order — later wins. That is one :batchGet request, which is Firestore's own answer to a set rather than a loop wearing a batch's name, and two things follow from what the API promises. The service returns the documents in whatever order it likes and says so, so they are put back into call order here — the order a caller wrote is the precedence, the order a service replies in is not. And one request is not one snapshot: without a transaction each document is read at its own time, and none is opened, because an open read-only transaction is state on the service a configuration read would have to remember to release.

There is deliberately no collection form, and documents.list is not what is missing. The reason in full.

Firestore does not store JSON. It stores a tagged encoding where every value names its type and integers arrive as strings ({"port": {"integerValue": "5432"}}) — because JSON's number is a double and Firestore's is not. The crate decodes that once, into the shape a configuration file would have had, so no struct grows a #[serde(with = ..)] per field. timestampValue, bytesValue and referenceValue keep their string form — a config file has no better answer for one either.

Authenticating: Auth::metadata_server() (workload identity on GKE, Cloud Run, GCE — no secret to distribute), Auth::access_token(..) (anything that already has one, gcloud auth print-access-token included), or Auth::Emulator, which sends nothing. A metadata-server token is cached and replaced as it approaches expiry; a 401 replaces one early, once.

TLS as data: .with_tls(TlsConfig::new().with_ca_certificate_file(..)) takes a private certificate authority and a client certificate as paths or PEM bytes, with no ureq type in the calling code. Rarely wanted against Google's own endpoint, whose certificates chain to an authority the platform already trusts — it is for the deployments that do not go there directly: an enterprise TLS-inspecting proxy, or an emulator behind with_endpoint. Setting with_agent as well is refused at the first request rather than resolved. TLS, and the one vocabulary all eight speak

Watching: polling — Firestore's REST surface offers nothing better — but by the document's updateTime field, not by re-reading the document; a document with no updateTime refuses at watch() time rather than polling forever. A multi-document source refuses there too: that updateTime belongs to one document, and a set of them has none. Stopping is noticed within a quarter second.

A failing watch says so: .reporting_to(sink) hands the loop the same RemoteSink its callback applies documents through, and every poll that comes back with nothing is recorded on it — a read that failed, a document that came back without the updateTime this watch is built on. A poll is where the two cases are indistinguishable from outside: a document nobody has edited today and a project that stopped answering an hour ago deliver the same nothing, and without this dynamic_config_remote_up reports the last delivery rather than the last attempt. A failed attempt moves the failure streak and nothing else, so dynamic_config_remote_last_fetch_seconds keeps ageing while remote_up goes to zero — the pair that says both the store is not answering and how stale what it last said has become. Reporting is infallible and silent: a loop is never handed a failure to report a failure. A fetch() needs none of it, because a fetch already records itself. The remote store's own numbers

The README carries the full story, tested against Google's own emulator; MSRV 1.85.