dynamic-config-consul
Read dynamic-config configuration from Consul's key/value store.
[]
= "0.2.0"
= "0.2.0"
use ;
set_remote;
// Fetching is explicit; the load that follows touches no network.
refresh_remote?;
builder.init?;
Consul's KV 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
GET {address}/v1/kv/{key}, base64-decoding the single Value Consul returns.
The stored value is a whole configuration document — the same bytes that
would be in a config file — so the format comes from the key's extension, or
from with_format.
That is the opposite of dynamic-config-vault, which wraps a secret's fields
under a section key. Vault stores a map of named secrets; Consul stores an
opaque blob; each is easiest to use as what it already is.
Getting a token
| Method | Constructor | For |
|---|---|---|
| None | Auth::Anonymous |
a Consul with ACLs off, which is ordinary in development |
| Token | Auth::token(..) |
whatever the operator issued |
| Environment | Auth::from_environment() |
CONSUL_HTTP_TOKEN, falling back to anonymous |
| Kubernetes | Auth::kubernetes(method) |
a pod, with no secret to distribute |
| JWT / OIDC | Auth::jwt(method, token) |
anything with a signed identity token |
The last two log in at /v1/acl/login, presenting a bearer token to a named
auth method. Consul's Meta is carried through for the audit log:
kubernetes.with_meta
Logging in is lazy: building a Consul reaches nothing, and the first read
does it. A Kubernetes bearer token is re-read at every login, because the
kubelet rotates projected service-account tokens and a copy taken at startup
expires with the pod still running.
Auth::from_environment() falls back to anonymous rather than failing when the
variable is unset — a Consul with ACLs disabled is exactly what the convenience
is useful for, and failing there would make it useless.
Expiry
Consul has no renewal: it issues login tokens with an expiry and expects you to
log in again. So a token within thirty seconds of expiry is replaced, and a 403
replaces one early — once, not in a loop, because a second refusal means the
policy is wrong and retrying would turn a clear failure into a hang.
Auth::token is the one variant that cannot recover on its own; there are no
credentials here to log in again with.
Watching
Consul cannot push, but it can hold a request open until something changes — a
blocking query. watch is that loop, and it is genuinely change-driven rather
than a poll with extra steps: the agent answers the moment the key moves.
let watch = new;
let watching = watch.watching;
spawn;
// Dropping `watch` — or calling `watch.stop()` — ends the loop.
- The current value is not delivered at startup. The first query carries index 0 and Consul answers it immediately with whatever is stored; that value primes the index and reports nothing, the same way a file watcher does not announce an edit when it starts.
- An identical write is not reported twice. Consul bumps its index on every write, including one that changed nothing.
- A failed query does not end the watch — the agent restarting, a network blip, or a key that does not exist yet are all what a watch is there to survive.
- Stopping is bounded by
with_wait, one minute by default. Longer means fewer requests and a slower exit; that trade is yours to make.
Bringing your own HTTP client
new.with_agent
For a caller with its own proxy settings, a private CA, a client certificate, or
a connection pool it would rather not have a second copy of. The agent's own
timeout applies — including to the long blocking query watch issues, so an
agent used for watching needs a timeout above with_wait.
Builders
| Method | Default |
|---|---|
with_format(..) |
from the key's extension |
with_auth(..) / with_token(..) |
anonymous |
with_datacenter(..) |
the agent's own |
with_timeout(..) |
10 seconds |
with_wait(..) |
60 seconds (blocking queries; Consul's ceiling is 10 minutes) |
with_agent(..) |
one built per source |
Example
| Example | Shows |
|---|---|
consul_kubernetes |
Logging in against an auth method, reading a document, and watching with a blocking query. |
It needs a server, and its own doc comment says how to start one in a container and put a document in it.
Testing
The test suite drives a real Consul in a container — no mocks, including one
started with ACLs enabled and default_policy = deny. That is how the
first-query behaviour above was found: the initial version reported the starting
value as a change, so beginning to watch looked like an edit.
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.
A companion pays for what it pulls in; the core stays where it is.
License
MIT