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

Consul

dynamic-config-consul reads configuration from Consul's KV store. The API is plain HTTP, so this is the blocking RemoteSource: nothing here needs an async runtime, and neither does using it.

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

DbConfig::set_remote(
    Consul::new("http://consul.internal:8500", "myapp/db.json")
        .with_auth(Auth::from_environment()),
);
DbConfig::refresh_remote()?;
}

What it reads: one key, base64-decoded, holding a whole configuration document — the opposite of Vault, which wraps a map of fields; each store is used as what it already is.

Several keys as one document: Keys::several([..]) merges named keys in call order — later wins, the rule .file(..) already teaches — and Keys::prefix("myapp/") merges the sections under a prefix, where an overlap between two of them is an error naming both keys and the paths. A prefix is one ?recurse request answered at one index, so the subtree is consistent; a named list is one request per key, because Consul's KV API has no batch read of a caller-chosen set, and is therefore not atomic. Prefer a prefix where the keys are disjoint anyway. A key ending in / with no value is a Consul folder and is skipped, not treated as a missing document. See several keys as one document for what this costs in provenance and in watching.

Getting a token: Auth::Anonymous (ACLs off — ordinary in development), Auth::token(..), Auth::from_environment() (CONSUL_HTTP_TOKEN, falling back to anonymous rather than failing — that fallback is the convenience's whole point), Auth::kubernetes(..) and Auth::jwt(..) (both log in at /v1/acl/login; with_meta feeds the audit log). Logging in is lazy, and a Kubernetes bearer token is re-read at every login because the kubelet rotates it.

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. Consul's own agent CA — consul tls ca create — is exactly the case: an authority no public trust store has heard of. No feature to turn on; ureq already carries rustls. Setting with_agent as well is refused at the first request rather than resolved. TLS, and the one vocabulary all eight speak

Expiry: Consul does not renew — it expects a fresh login. A token within thirty seconds of expiry is replaced proactively; a 403 replaces one early, once. Auth::token is the variant that cannot recover — there are no credentials to log in again with.

Watching: a blocking query — genuinely change-driven, not a poll with extra steps: the agent holds the request open and answers the moment the key moves. It runs on a thread and takes a Watching stop token; the first query primes the index and reports nothing, an identical rewrite is not reported twice, and a failed query retries rather than ending the loop. Stopping is bounded by with_wait (a minute by default — fewer requests, slower exit; the trade is yours). An agent supplied via with_agent needs its own timeout above with_wait, or every blocking query dies as a client timeout.

A prefix can be watched; a named list cannot. A prefix watch is the cheapest correct watch on a set anywhere in this family, and the only one that re-reads nothing: a recursive blocking query's answer is the subtree at one index, so the document is folded from the very bytes the agent blocked to send, and there is no window between "the set changed" and "read the set". A named list has no batch read to fold — Consul's KV API has none — so it would mean blocking on one key and then reading the others, and it is refused at watch(): spurious, never torn.

A failing watch says so: .reporting_to(sink) hands the loop the same RemoteSink its callback applies documents through, and every attempt that comes back with nothing is recorded on it — a blocking query that errored, a watched key that holds no value, a subtree that cannot be folded into a document. Without it, dynamic_config_remote_up reports the last delivery rather than the last attempt, and the two look identical from outside: a key nobody changed today and an agent that stopped answering an hour ago both deliver nothing, and only one of them is healthy. 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 agent 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, the builder-defaults table and the consul_kubernetes example; MSRV 1.85.