dynamic-config-firestore 0.3.0

Read dynamic-config configuration from a Google Cloud Firestore document.
Documentation

dynamic-config-firestore

Read dynamic-config configuration from a Google Cloud Firestore document.

[dependencies]
dynamic-config = "0.2.0"
dynamic-config-firestore = "0.2.0"
use dynamic_config_firestore::{Auth, Firestore};

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

DbConfig::refresh_remote()?;

Firestore's REST API is plain HTTP, so this implements the blocking RemoteSource trait: nothing here needs an async runtime, and neither does using it.

What it reads

One document, at a path like config/db — collection, then document. Its fields become the configuration, wrapped under the section key, which is the same shape dynamic-config-vault uses and for the same reason: Firestore stores a map of named fields, so the natural unit is the field.

Firestore does not store JSON

It stores a tagged encoding, where every value names its own type and integers arrive as strings — because JSON's number is a double and Firestore's is not:

{"port": {"integerValue": "5432"}}

Handing that to serde would mean every configuration struct growing a #[serde(with = ..)] per field, so it is decoded here, once, into the shape a configuration file would have had. timestampValue, bytesValue and referenceValue keep their string form, because a configuration file has no better answer for one either.

Authenticating

Method Constructor For
Workload identity Auth::metadata_server() GKE, Cloud Run, GCE — no secret to distribute
An access token Auth::access_token(..) anything that already has one, including gcloud auth print-access-token
None Auth::Emulator the Firestore emulator

A token from the metadata server is cached and replaced as it approaches expiry; a 401 replaces one early, once. A supplied token cannot be renewed — there is nothing here to renew it from — so a long-running process should use workload identity.

A service-account JSON key is deliberately not supported, and that is a recommendation rather than a gap: signing one means an RS256 stack in a configuration library, and Google's own guidance is that a downloaded key is the option of last resort. Workload identity covers GKE, Cloud Run, GCE and Cloud Functions; for anything else, mint a token outside the process and pass it in.

Watching

Firestore can push — the real-time API is a gRPC stream — and this deliberately does not use it: that would put a gRPC stack in a crate whose whole point is a plain HTTP read. Polling reads one small document and compares updateTime, which for a configuration document checked every thirty seconds is a rounding error against a project's quota.

firestore.watch(&watching, Duration::from_secs(30), move |document| sink.apply(document))
  • The current document is not delivered at startup.
  • A failed check does not end the watch. Stopping is noticed within a quarter second whatever the interval is.

Builders

Method Default
with_key(..) "db" — must match the key given to builder(..)
with_database(..) (default)
with_auth(..) Auth::Emulator, which sends nothing
with_endpoint(..) https://firestore.googleapis.com
with_timeout(..) 10 seconds
with_agent(..) one built per source

Testing

The test suite drives Google's own Firestore emulator in a container, pulled from gcr.io rather than Docker Hub so a test run does not hit anonymous pull limits. What is being checked is Firestore's value encoding and its REST surface, and a mock of those would only confirm what we already believed about them.

cargo test -p dynamic-config-firestore    # needs a working Docker daemon

MSRV

1.85 — higher than dynamic-config's own 1.71, because an HTTP client stack and a container-driving test harness both move faster than that crate wants to.

License

MIT