dynamic_config_nats/lib.rs
1//! Read [`dynamic-config`] configuration from a NATS JetStream key/value bucket.
2//!
3//! NATS is a streaming protocol and its client is async throughout, so this
4//! implements the **async** [`AsyncRemoteSource`] trait rather than the
5//! blocking one.
6//!
7//! ```no_run
8//! use dynamic_config_nats::Nats;
9//!
10//! # struct DbConfig;
11//! # impl DbConfig {
12//! # fn set_remote_async(_: Nats) {}
13//! # async fn refresh_remote_async() -> Result<(), dynamic_config::Error> { Ok(()) }
14//! # }
15//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
16//! DbConfig::set_remote_async(
17//! Nats::new("nats://nats.internal:4222", "config", "db.json").await?,
18//! );
19//!
20//! // Fetching is explicit; the load that follows touches no network.
21//! DbConfig::refresh_remote_async().await?;
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! # What it reads
27//!
28//! One key in one bucket, whose value is **a whole configuration document** —
29//! the same bytes that would be in a config file. The format comes from the
30//! key's extension, or from [`with_format`](Nats::with_format).
31//!
32//! Like Consul and unlike Vault, that is a deliberate difference: a KV bucket
33//! stores opaque bytes, so the natural unit is the document. Vault's KV v2
34//! stores a JSON object of fields, so the natural unit there is the field.
35//!
36//! # Several keys as one document
37//!
38//! A deployment that splits its configuration across several keys of one
39//! bucket can have one source read the lot, and [`Keys`] says which:
40//!
41//! ```no_run
42//! # use dynamic_config_nats::{Keys, Nats};
43//! # async fn example() -> Result<(), dynamic_config::Error> {
44//! // Named keys: a list of layers, merged in the order given, later wins.
45//! let nats = Nats::new(
46//! "nats://nats.internal:4222",
47//! "config",
48//! Keys::several(["base.json", "local.json"]),
49//! )
50//! .await?;
51//! # Ok(())
52//! # }
53//! ```
54//!
55//! **A named list is one get per key**, and a bucket read is a request to the
56//! stream: there is no batch get in the KV API, so the set is **not** read
57//! atomically. A write landing between two of the gets can produce a document
58//! that never existed as a whole.
59//!
60//! **There is deliberately no prefix form**, and the reason is the client's
61//! rather than a preference. `Store::keys()` is the only listing there is, and
62//! it lists the **whole bucket**: it builds an ordered consumer filtered on
63//! `$KV.{bucket}.>` and streams a header for every key in it. `async-nats`
64//! keeps the filtered constructor behind a private method, so a prefix here
65//! would be a full-bucket scan wearing a prefix's name — the 512-key bound
66//! would have to be a bound on the bucket, and a bucket of a hundred thousand
67//! keys would stream a hundred thousand headers to find three. Name the keys,
68//! or put the set in its own bucket, which is the partition NATS actually
69//! offers. [`dynamic-config-consul`] and [`dynamic-config-etcd`] have a real
70//! range read and take a prefix for that reason.
71//!
72//! Two consequences the multi-key form shares with the rest of the family:
73//!
74//! - **Provenance becomes store-grained.** The merged document is one layer,
75//! so `source_of` names the set rather than which key supplied a value.
76//! - **One unreadable key fails the whole fetch.** A configuration quietly
77//! missing a section is worse than a refresh that failed and left the last
78//! document serving.
79//!
80//! # JetStream must be enabled
81//!
82//! A key/value bucket is a JetStream feature. A NATS server started without it
83//! answers with a "JetStream is not enabled" error, which is reported as it
84//! arrives rather than translated into something vaguer.
85//!
86//! # The connection is made once
87//!
88//! [`Nats::new`] connects and resolves the bucket; [`fetch`](AsyncRemoteSource::fetch)
89//! reuses that handle. Unlike a gRPC client this connects eagerly, so an
90//! unreachable server *is* a construction failure.
91//!
92//! The store handle is `Clone` and its reads take `&self`, so — unlike etcd —
93//! nothing here needs a lock.
94//!
95//! # Reconnecting is the client's job, and it does it
96//!
97//! `async-nats` reconnects on its own, indefinitely, and re-establishes
98//! subscriptions when it does. So there is deliberately no retry logic here:
99//! adding one would mean a second, worse implementation of something the client
100//! already does properly, layered on top of it.
101//!
102//! Two consequences worth knowing. A [`fetch`](AsyncRemoteSource::fetch) during
103//! a disconnect fails rather than blocking until the connection returns —
104//! configuration that hangs is worse than configuration that reports. And a
105//! [`watch`](Nats::watch) survives a reconnect without the caller noticing,
106//! which is why it ending at all is treated as an error.
107//!
108//! # Credentials
109//!
110//! Everything NATS understands — a token, a user and password, an NKey, a JWT,
111//! a `.creds` file, TLS — goes through [`ConnectOptions`], which is NATS' own
112//! type re-exported. See [`Nats::with_options`].
113//!
114//! A credential the server refuses fails at *construction*, and reports as
115//! `ErrorKind::Auth` rather than `Remote` — the one distinction that separates
116//! "the password is wrong" from "the server is down", and the only place
117//! `async-nats` draws it. A later read refused for want of permission arrives
118//! as an undifferentiated KV error, so it stays `Remote`: guessing there would
119//! stop a watch loop that a reconnect would have fixed.
120//!
121//! A credential in the *URL* — `nats://token@host:4222` is a shape NATS
122//! accepts — is redacted before the address is stored, because the address is
123//! quoted into every error message and into `Debug`.
124//!
125//! # Timeouts
126//!
127//! [`Nats::with_timeout`] is the deadline for a single fetch attempt,
128//! excluding retries the underlying client performs — the sentence every store
129//! in this family answers to. Ten seconds by default.
130//!
131//! `ConnectOptions::request_timeout` is its twin on the connection side, set
132//! through [`Nats::with_options`] before there is a connection to bound.
133//! Neither applies to [`Nats::watch`], which is long-lived on purpose.
134//!
135//! # Watching
136//!
137//! A KV bucket is a stream, so [`Nats::watch`] is a future the caller spawns and
138//! cancels by dropping — no runtime is imposed and no flag is polled.
139//!
140//! A **multi-key source cannot be watched**, and refuses rather than pretending
141//! to: what a watch delivers here is the document that changed, and for a
142//! merged document that means re-reading the whole set on every event. Poll
143//! `refresh_remote_async()` on a timer instead.
144//!
145//! ```no_run
146//! # use dynamic_config_nats::Nats;
147//! # async fn example(nats: Nats) {
148//! # let sink = |_: dynamic_config::Fetched| -> Result<(), dynamic_config::Error> { Ok(()) };
149//! let task = tokio::spawn(async move {
150//! nats.watch(move |document| sink(document)).await
151//! });
152//!
153//! // Dropping or aborting the task stops the watch.
154//! task.abort();
155//! # }
156//! ```
157//!
158//! # A watch that is failing says so
159//!
160//! A watch is the half of a store `dynamic-config` cannot see: a delivery keeps
161//! `RemoteStatus` current, and a stream that broke delivers nothing and would
162//! otherwise report nothing — so `dynamic_config_remote_up` would describe the
163//! last *delivery* rather than the last *attempt*.
164//! [`reporting_to`](Nats::reporting_to) closes that: the sink the loop already
165//! holds is told about every attempt that came back with nothing, and a store
166//! that stopped answering an hour ago reads as down without anything having to
167//! call `refresh_remote_async()`.
168//!
169//! What that covers here follows from the section above: a *server* that goes
170//! away is not a failed watch, because `async-nats` keeps recreating the
171//! subscription for as long as it takes and the loop waits through it. What
172//! reaches this crate is a stream that stopped — a deleted bucket, a consumer
173//! that is gone, a value that is not a document — and that is what is reported.
174//!
175//!
176//! # Every failure branch of the watch loop, and what it reports
177//!
178//! A watch is the half of a store `dynamic-config` cannot see, and
179//! [`reporting_to`](Nats::reporting_to) is what lets it speak: the sink the
180//! loop already holds is told about every attempt that came back with
181//! nothing. Which attempts those are is a table rather than prose, because
182//! the question an operator asks is *which* silence is deliberate.
183//!
184//! Three rules decide the column, and they are the same three in all seven
185//! store crates:
186//!
187//! 1. **A failure the loop survives by retrying reports.** That is the case
188//! the whole feature exists for: the stream is down, the last delivery is
189//! old, and nothing else would ever say so out loud.
190//! 2. **A recovery that worked stays silent.** Only a delivery or a fetch
191//! clears the streak, so reporting a five-minute token turning over on a
192//! healthy cluster would drive `remote_up` to zero and leave it there.
193//! 3. **A refusal that never asked the store reports nowhere.** No format, a
194//! key shape that cannot be watched, material that will not build a
195//! client: `RemoteStatus::reachable()` is *whether the store answered the
196//! last time it was asked*, and these never ask. They are returned to the
197//! caller, who is the one holding the mistake — and a status cannot
198//! correct them, since it carries a kind and a path and no message.
199//!
200//! | Branch | Reports |
201//! |---|---|
202//! | the format is missing, or the source names several keys | no — rule 3: nothing has been asked of the server |
203//! | the bucket refuses the watch | **yes** — the first round trip |
204//! | the stream errors | **yes**, and the watch ends — `async-nats` reconnects on its own, so reaching here means it could not |
205//! | an operation that is not a `Put` | no — nothing changed |
206//! | the value is not UTF-8 | **yes** — the same failure a `fetch` of it would have recorded |
207//! | `on_change` refuses the document | no — the store answered; `apply` counted the delivery, and what the document did next is `ConfigStatus`'s half |
208//! | the stream ends without an error | **yes** — the connection went away, or the bucket did |
209//!
210//! [`dynamic-config`]: https://docs.rs/dynamic-config
211//! [`dynamic-config-consul`]: https://docs.rs/dynamic-config-consul
212//! [`dynamic-config-etcd`]: https://docs.rs/dynamic-config-etcd
213
214#![forbid(unsafe_code)]
215#![deny(missing_docs)]
216
217use std::future::Future;
218use std::pin::Pin;
219use std::time::Duration;
220
221use async_nats::jetstream::kv::{Operation, Store};
222use dynamic_config::{AsyncRemoteSource, Error, Fetched, Format, RemoteSink};
223use dynamic_config_store_core::attempts::Attempts;
224use dynamic_config_store_core::documents::{self, Overlap};
225use dynamic_config_store_core::{guarded, LoneAuthority};
226
227/// NATS' own connection options, re-exported so authenticating needs no direct
228/// dependency on `async-nats`.
229///
230/// Every credential NATS understands lives here: a token, a user and password,
231/// an NKey, a JWT, a `.creds` file, TLS. There is no second vocabulary to learn,
232/// and options this crate has never heard of keep working.
233pub use async_nats::{Client, ConnectOptions};
234use futures_util::StreamExt;
235
236use dynamic_config_store_core::tls as tls_core;
237/// A private certificate authority and a client certificate, as data.
238///
239/// The shared vocabulary all seven store crates take, so that reaching TLS
240/// never means naming an `async-nats` type — see [`Nats::with_tls`]. NATS is
241/// the one store here that cannot express the whole of it: its client takes
242/// **file paths**, so the PEM-bytes spellings are refused rather than written
243/// to a temporary file.
244pub use dynamic_config_store_core::tls::TlsConfig;
245
246/// How long one fetch may take before it is given up on.
247///
248/// Ten seconds, matching the rest of the family. A configuration fetch that
249/// hangs is worse than one that fails: the caller can retry a failure.
250const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
251
252/// What a source reads: one key, or several named ones.
253///
254/// Every constructor takes one, and a bare `&str` or `String` is
255/// [`Keys::one`] — so the single-key spelling every caller already wrote keeps
256/// working unchanged.
257///
258/// There is no prefix variant, and that is the client's doing rather than a
259/// preference: the only listing `async-nats` exposes walks the whole bucket.
260/// The crate documentation says the whole of it.
261#[derive(Clone, Debug, PartialEq, Eq)]
262pub enum Keys {
263 /// One key, whose value is the whole document.
264 One(String),
265 /// Several named keys, merged **in the order given — later wins**.
266 ///
267 /// The rule a list of `.file(..)` calls already teaches: the caller wrote
268 /// the list, so the list is the precedence. One get per key, because the
269 /// KV API has no batch read — so the set is **not** read atomically.
270 Several(Vec<String>),
271}
272
273impl Keys {
274 /// One key, whose value is the whole document.
275 #[must_use]
276 pub fn one(key: impl Into<String>) -> Self {
277 Self::One(key.into())
278 }
279
280 /// Several named keys, merged in the order given — later wins.
281 #[must_use]
282 pub fn several<I, S>(keys: I) -> Self
283 where
284 I: IntoIterator<Item = S>,
285 S: Into<String>,
286 {
287 Self::Several(keys.into_iter().map(Into::into).collect())
288 }
289
290 /// The keys as a slice, in the order they are read.
291 fn named(&self) -> &[String] {
292 match self {
293 Self::One(key) => std::slice::from_ref(key),
294 Self::Several(keys) => keys,
295 }
296 }
297
298 /// How a diagnostic names what this source reads.
299 ///
300 /// One key renders as `key {name}`, so every message a single-key source
301 /// has ever produced is unchanged.
302 fn describe(&self) -> String {
303 match self {
304 Self::One(key) => format!("key {key}"),
305 Self::Several(keys) => format!("keys {}", keys.join(", ")),
306 }
307 }
308}
309
310impl From<&str> for Keys {
311 fn from(key: &str) -> Self {
312 Self::one(key)
313 }
314}
315
316impl From<String> for Keys {
317 fn from(key: String) -> Self {
318 Self::One(key)
319 }
320}
321
322impl From<&String> for Keys {
323 fn from(key: &String) -> Self {
324 Self::one(key)
325 }
326}
327
328/// A key in a JetStream bucket, as a configuration source.
329pub struct Nats {
330 store: Store,
331 keys: Keys,
332 format: Option<Format>,
333 /// Why the keys' own extensions could not settle the format between them.
334 ///
335 /// Kept rather than reported at construction because the constructors
336 /// report only what they reached, and because `with_format` is allowed to
337 /// settle it afterwards.
338 disagreement: Option<String>,
339 server: String,
340 bucket: String,
341 timeout: Duration,
342 /// Where the watch loop reports an attempt that came back with nothing.
343 ///
344 /// Nobody, unless [`reporting_to`](Nats::reporting_to) said otherwise —
345 /// which is what makes reporting free for a caller who never asked for it.
346 attempts: Attempts,
347}
348
349impl Nats {
350 /// Connects to `server` and resolves `key` in `bucket`.
351 ///
352 /// The format is taken from the key's extension — `db.json` is JSON. A key
353 /// without one needs [`with_format`](Self::with_format).
354 ///
355 /// # Errors
356 ///
357 /// If the server cannot be reached, if JetStream is not enabled, or if the
358 /// bucket does not exist. This crate deliberately does not create the
359 /// bucket: a configuration reader that provisions storage would hide a
360 /// misconfigured deployment behind an empty one.
361 pub async fn new(
362 server: impl Into<String>,
363 bucket: impl Into<String>,
364 key: impl Into<Keys>,
365 ) -> Result<Self, Error> {
366 Self::with_options(server, bucket, key, ConnectOptions::new()).await
367 }
368
369 /// As [`new`](Self::new), with NATS' own connection options.
370 ///
371 /// This is where credentials live, because that is where `async-nats` puts
372 /// them:
373 ///
374 /// ```no_run
375 /// # use dynamic_config_nats::{ConnectOptions, Nats};
376 /// # async fn example() -> Result<(), dynamic_config::Error> {
377 /// // A `.creds` file, which is how a NATS account usually authenticates.
378 /// let nats = Nats::with_options(
379 /// "nats://nats.internal:4222",
380 /// "config",
381 /// "db.json",
382 /// ConnectOptions::with_credentials_file("/etc/myapp/nats.creds")
383 /// .await
384 /// .map_err(|error| dynamic_config::Error::remote(error.to_string()))?,
385 /// )
386 /// .await?;
387 /// # Ok(())
388 /// # }
389 /// ```
390 ///
391 /// Token, user and password, NKey, JWT and TLS all live on the same type.
392 ///
393 /// # Errors
394 ///
395 /// As [`new`](Self::new).
396 pub async fn with_options(
397 server: impl Into<String>,
398 bucket: impl Into<String>,
399 key: impl Into<Keys>,
400 options: ConnectOptions,
401 ) -> Result<Self, Error> {
402 let server = server.into();
403 let bucket = bucket.into();
404 let keys = key.into();
405
406 // Everything a person or a log ever sees is the redacted form. A NATS
407 // URL may carry a token or a password in its authority, and `server`
408 // is quoted by `describe()` — which means by every error message and
409 // by `Debug`.
410 let described = redacted(&server);
411
412 let client = options.connect(&server).await.map_err(|error| {
413 let described = format!("nats {described}: {error}");
414
415 // The one place `async-nats` names an auth failure as such: a
416 // signed nonce the server would not take, or an outright
417 // authorization violation. Both survive any amount of retrying,
418 // which is exactly what `Auth` tells a caller.
419 match error.kind() {
420 async_nats::ConnectErrorKind::Authentication
421 | async_nats::ConnectErrorKind::AuthorizationViolation => Error::auth(described),
422 _ => Error::remote(described),
423 }
424 })?;
425
426 let store = async_nats::jetstream::new(client)
427 .get_key_value(&bucket)
428 .await
429 .map_err(|error| Error::remote(format!("nats {described} bucket {bucket}: {error}")))?;
430
431 let (format, disagreement) = agreed(&keys);
432
433 Ok(Self {
434 store,
435 keys,
436 format,
437 disagreement,
438 server: described,
439 bucket,
440 timeout: DEFAULT_TIMEOUT,
441 attempts: Attempts::default(),
442 })
443 }
444
445 /// As [`with_options`](Self::with_options), with a private certificate
446 /// authority or a client certificate from the shared vocabulary.
447 ///
448 /// The same three settings, spelled the same way, in all seven store
449 /// crates — and spelled as *data*, so nothing here names an `async-nats`
450 /// type:
451 ///
452 /// ```no_run
453 /// # use dynamic_config_nats::{ConnectOptions, Nats, TlsConfig};
454 /// # async fn example() -> Result<(), dynamic_config::Error> {
455 /// let nats = Nats::with_tls(
456 /// "tls://nats.internal:4222",
457 /// "config",
458 /// "db.json",
459 /// ConnectOptions::new(),
460 /// &TlsConfig::new().with_ca_certificate_file("/etc/nats/ca.pem"),
461 /// )
462 /// .await?;
463 /// # Ok(())
464 /// # }
465 /// ```
466 ///
467 /// # What NATS cannot express
468 ///
469 /// **PEM bytes.** `async-nats` takes paths and opens the files itself;
470 /// there is no byte-taking door short of handing it a whole
471 /// `rustls::ClientConfig`, which would put a direct `rustls` dependency
472 /// and a crypto-provider decision in this crate for one spelling. So
473 /// [`with_ca_certificate_pem`] and [`with_client_certificate_pem`] are
474 /// **refused here**, naming the call and pointing at the file spelling —
475 /// not ignored, because a caller who supplied a CA and got the public
476 /// trust store has a program that believes it is pinned and is not. The
477 /// obvious workaround, writing the bytes to a temporary file, is
478 /// deliberately not taken: it would put a private key on a disk that never
479 /// asked for one.
480 ///
481 /// Everything else is there: a CA file and a client certificate and key,
482 /// which is what a NATS deployment with `tls` in its configuration file
483 /// hands out.
484 ///
485 /// **Naming a CA turns TLS on.** `require_tls(true)` is set, so a
486 /// `nats://` URL that would have negotiated plaintext fails instead of
487 /// quietly connecting without the authority the caller just named.
488 ///
489 /// `options` carries everything that is not TLS: the token, the NKey, the
490 /// `.creds` file. **The `tls` argument owns the TLS slot**; if `options`
491 /// also names root certificates, both sets are added, because that is what
492 /// `async-nats` does with them.
493 ///
494 /// There is no way to turn verification off; [`TlsConfig`]'s own
495 /// documentation argues that one.
496 ///
497 /// # Errors
498 ///
499 /// If the configuration names PEM bytes, or as [`new`](Self::new).
500 ///
501 /// [`with_ca_certificate_pem`]: TlsConfig::with_ca_certificate_pem
502 /// [`with_client_certificate_pem`]: TlsConfig::with_client_certificate_pem
503 pub async fn with_tls(
504 server: impl Into<String>,
505 bucket: impl Into<String>,
506 key: impl Into<Keys>,
507 options: ConnectOptions,
508 tls: &TlsConfig,
509 ) -> Result<Self, Error> {
510 let server = server.into();
511 let described = format!("nats {}", redacted(&server));
512
513 let options = with_tls_options(options, tls, &described)?;
514
515 Self::with_options(server, bucket, key, options).await
516 }
517
518 /// Uses a client the program already has.
519 ///
520 /// For a caller already connected to NATS: reusing the connection beats
521 /// opening a second one to the same server, and the client is `Clone` —
522 /// cheaply, it is a handle — so sharing costs nothing.
523 ///
524 /// ```no_run
525 /// # use dynamic_config_nats::{Client, Nats};
526 /// # async fn example(client: Client) -> Result<(), dynamic_config::Error> {
527 /// let nats = Nats::from_client(client, "config", "db.json").await?;
528 /// # Ok(())
529 /// # }
530 /// ```
531 ///
532 /// # Errors
533 ///
534 /// If JetStream is not enabled, or the bucket does not exist.
535 pub async fn from_client(
536 client: Client,
537 bucket: impl Into<String>,
538 key: impl Into<Keys>,
539 ) -> Result<Self, Error> {
540 let bucket = bucket.into();
541
542 let store = async_nats::jetstream::new(client)
543 .get_key_value(&bucket)
544 .await
545 .map_err(|error| Error::remote(format!("nats bucket {bucket}: {error}")))?;
546
547 Ok(Self::from_store(store, key))
548 }
549
550 /// Uses an already-resolved bucket.
551 ///
552 /// One step further than [`from_client`](Self::from_client), for a program
553 /// that already holds the `Store` itself.
554 #[must_use]
555 pub fn from_store(store: Store, key: impl Into<Keys>) -> Self {
556 let keys = key.into();
557 let bucket = store.name.clone();
558
559 let (format, disagreement) = agreed(&keys);
560
561 Self {
562 store,
563 keys,
564 format,
565 disagreement,
566 // The store does not carry the address it was reached through, and
567 // inventing one would put a wrong server in every error message.
568 server: "<an existing connection>".to_owned(),
569 bucket,
570 timeout: DEFAULT_TIMEOUT,
571 attempts: Attempts::default(),
572 }
573 }
574
575 /// Reports the watch loop's failed attempts to `sink`.
576 ///
577 /// Without this a watch is the half of a store `dynamic-config` cannot
578 /// see. [`RemoteSink::apply`] records a delivery, so a *working* watch
579 /// keeps the status current — but a loop whose stream broke or whose
580 /// bucket went away delivers nothing, and so says nothing:
581 /// `dynamic_config_remote_up` reports the last delivery rather than the
582 /// last attempt, and a store that stopped answering an hour ago looks
583 /// healthy until something calls `refresh_remote_async()`.
584 ///
585 /// ```no_run
586 /// # use dynamic_config_nats::Nats;
587 /// # struct DbConfig;
588 /// # impl DbConfig {
589 /// # fn remote_sink() -> dynamic_config::RemoteSink { unimplemented!() }
590 /// # }
591 /// # async fn example(nats: Nats) -> Result<(), dynamic_config::Error> {
592 /// let sink = DbConfig::remote_sink();
593 ///
594 /// // The same sink delivers and reports: one generation, one fence.
595 /// nats.reporting_to(sink)
596 /// .watch(move |document| sink.apply(document))
597 /// .await
598 /// # }
599 /// ```
600 ///
601 /// A sink is `Copy` and captures its source's generation when it is taken,
602 /// which is what keeps a loop winding down after its source was replaced
603 /// from charging its failures to the replacement — so take it once, where
604 /// the watch is wired, exactly as the delivering half already does.
605 ///
606 /// **Only the watch.** A [`fetch`](AsyncRemoteSource::fetch) records itself
607 /// through `refresh_remote_async()` already, and what is reported here is
608 /// the failure streak and the last failure and nothing else: the staleness
609 /// clock keeps ageing while `remote_up` goes to zero, which is the pair an
610 /// alert wants.
611 ///
612 /// The error's kind is what travels. Nothing that names this store — no
613 /// server, no bucket, no key, and certainly not a token in a `nats://`
614 /// URL — enters a `RemoteStatus`.
615 #[must_use]
616 pub fn reporting_to(mut self, sink: RemoteSink) -> Self {
617 self.attempts = Attempts::to(sink);
618 self
619 }
620
621 /// Reports `error` to whatever asked to hear about failed attempts, and
622 /// hands it straight back.
623 ///
624 /// Every failure the watch loop ends on goes through here, so reporting is
625 /// one word at each site rather than a branch that can be left out of the
626 /// next one. It cannot fail and it does not touch the error: a loop must
627 /// never have to handle a failure to report a failure, and the caller sees
628 /// exactly what it always saw.
629 fn failing(&self, error: Error) -> Error {
630 self.attempts.failed(&error);
631
632 error
633 }
634
635 /// States the format, for a key whose name does not.
636 ///
637 /// It also settles a list whose keys name two different formats.
638 #[must_use]
639 pub fn with_format(mut self, format: Format) -> Self {
640 self.format = Some(format);
641 // The caller has now said which format wins, so the keys no longer
642 // have to agree between themselves.
643 self.disagreement = None;
644 self
645 }
646
647 /// The format, or an error naming the call that supplies one.
648 fn format(&self) -> Result<Format, Error> {
649 if let Some(complaint) = &self.disagreement {
650 return Err(Error::remote(format!("{}: {complaint}", self.describe())));
651 }
652
653 self.format.ok_or_else(|| {
654 Error::remote(format!(
655 "{}: the key names no format; call `with_format`",
656 self.describe()
657 ))
658 })
659 }
660
661 /// The one key this source reads, or an error saying it reads several.
662 ///
663 /// A watch delivers *the document that changed*; for a merged document
664 /// that means re-reading the whole set on every event, which is a
665 /// different loop with different failure modes and belongs behind its own
666 /// decision rather than behind this one.
667 fn single_key(&self) -> Result<&str, Error> {
668 match &self.keys {
669 Keys::One(key) => Ok(key),
670 Keys::Several(_) => Err(Error::remote(format!(
671 "{}: a source that reads several keys cannot be watched; \
672 poll `refresh_remote_async()` on a timer instead",
673 self.describe()
674 ))),
675 }
676 }
677
678 /// What two of this source's keys supplying one path means.
679 ///
680 /// Only [`Overlap::LaterWins`] here: a caller who wrote the list wrote the
681 /// precedence with it, and there is no prefix form whose order nobody
682 /// chose.
683 fn overlap(&self) -> Overlap {
684 Overlap::LaterWins
685 }
686
687 /// The `(key, document)` pairs this source reads, in merge order.
688 ///
689 /// One get per key and **every one of them must answer**: merging the four
690 /// that did would leave a process running a configuration with a section
691 /// quietly missing from it.
692 async fn documents(&self) -> Result<Vec<(String, String)>, Error> {
693 let keys = self.keys.named();
694 let mut documents = Vec::with_capacity(keys.len());
695
696 for key in keys {
697 documents.push((key.clone(), self.read(key).await?));
698 }
699
700 Ok(documents)
701 }
702
703 /// One key's value, as text.
704 async fn read(&self, key: &str) -> Result<String, Error> {
705 let read = tokio::time::timeout(self.timeout, self.store.get(key))
706 .await
707 .map_err(|_| {
708 Error::remote(format!(
709 "{}: `{key}` timed out after {:?}",
710 self.describe(),
711 self.timeout
712 ))
713 })?;
714
715 let value = read
716 .map_err(|error| Error::remote(format!("{}: {error}", self.describe())))?
717 .ok_or_else(|| Error::remote(format!("{}: `{key}` holds no value", self.describe())))?;
718
719 String::from_utf8(value.to_vec()).map_err(|error| {
720 Error::remote(format!(
721 "{}: `{key}` is not UTF-8: {error}",
722 self.describe()
723 ))
724 })
725 }
726
727 /// How long a single fetch may take before it is given up on. Ten seconds
728 /// by default.
729 ///
730 /// The deadline for **one fetch attempt**, excluding retries the
731 /// underlying client performs — the same sentence every store in this
732 /// family answers to. `async-nats` reconnects on its own, so a fetch
733 /// crossing a reconnect is exactly the case this bounds.
734 ///
735 /// It bounds **each get**, so a source reading several keys reads each of
736 /// them under this deadline rather than sharing one between them: the KV
737 /// API has no batch read, and a deadline divided by however many keys a
738 /// caller listed would be a different promise per source.
739 ///
740 /// It is the *second* of two timeouts, and they cover different halves.
741 /// `ConnectOptions::request_timeout`, passed to
742 /// [`with_options`](Self::with_options), bounds the client's own requests
743 /// and is set before there is a connection to bound. This one wraps the
744 /// KV read, so a server that accepted the request and then went quiet
745 /// still ends the fetch rather than parking it.
746 ///
747 /// It does not cover [`watch`](Self::watch), which is long-lived by
748 /// definition.
749 #[must_use]
750 pub fn with_timeout(mut self, timeout: Duration) -> Self {
751 self.timeout = timeout;
752 self
753 }
754
755 /// Calls `on_change` every time the key's value moves, forever.
756 ///
757 /// The first call happens when the *first change* arrives, not at startup:
758 /// a watch reports changes, and reporting the current value as one would
759 /// make every restart look like an edit. Fetch first if the starting value
760 /// matters, which it usually does:
761 ///
762 /// ```no_run
763 /// # use dynamic_config::AsyncRemoteSource;
764 /// # use dynamic_config_nats::Nats;
765 /// # struct Sink;
766 /// # impl Sink {
767 /// # fn apply(&self, _: dynamic_config::Fetched) -> Result<(), dynamic_config::Error> { Ok(()) }
768 /// # }
769 /// # async fn example(nats: Nats) -> Result<(), dynamic_config::Error> {
770 /// # let sink = Sink;
771 /// sink.apply(nats.fetch().await?)?;
772 /// nats.watch(move |document| sink.apply(document)).await
773 /// # }
774 /// ```
775 ///
776 /// **Cancellation is dropping the future.** There is no stop flag, because
777 /// there is nothing to poll one between: this suspends on the stream, so
778 /// any executor's cancellation already ends it immediately.
779 ///
780 /// Deletes and purges are not changes this reports. The key holding no
781 /// value is not a configuration, and calling back with the last one — or
782 /// with nothing — would both be worse than leaving the running snapshot
783 /// alone.
784 ///
785 /// # Errors
786 ///
787 /// If the watch cannot be established, if the connection fails or the
788 /// stream ends, or if `on_change` returns an error, which ends the watch —
789 /// so a caller that wants to survive a bad document should log it and
790 /// return `Ok`.
791 ///
792 /// This never returns `Ok`: a watch either runs or has failed, and a silent
793 /// success would leave a spawned task finished and a configuration frozen
794 /// with nothing said about either. Callers that want to reconnect should
795 /// loop around it.
796 ///
797 /// Every one of those failures is also reported to the sink
798 /// [`reporting_to`](Self::reporting_to) was given, if one was — because a
799 /// watch is normally spawned and its `JoinHandle` dropped, so the error
800 /// returned here has nowhere else to go. The one failure not charged to the
801 /// store is `on_change`'s own refusal: the store answered, `apply` recorded
802 /// the delivery, and whether the document then installs is
803 /// `ConfigStatus`'s business.
804 pub async fn watch<F>(&self, mut on_change: F) -> Result<(), Error>
805 where
806 F: FnMut(Fetched) -> Result<(), Error> + Send,
807 {
808 // Neither of these is recorded: no request has left the process, and
809 // `RemoteStatus::reachable()` is *whether the store answered the last
810 // time it was asked*. Everything below the first round trip reports;
811 // see the table in this crate's documentation.
812 let format = self.format()?;
813 // Refused up front, so a multi-key source fails at `watch` rather than
814 // on the first change, hours later.
815 let key = self.single_key()?;
816
817 let mut entries = self.store.watch(key).await.map_err(|error| {
818 self.failing(Error::remote(format!(
819 "{}: cannot watch: {error}",
820 self.describe()
821 )))
822 })?;
823
824 while let Some(entry) = entries.next().await {
825 // The stream itself failed. `async-nats` reconnects on its own, so
826 // reaching here means it could not — which is exactly the state an
827 // operator is asking about when they ask whether the store is up.
828 let entry = entry.map_err(|error| {
829 self.failing(Error::remote(format!(
830 "{}: the watch failed: {error}",
831 self.describe()
832 )))
833 })?;
834
835 if entry.operation != Operation::Put {
836 continue;
837 }
838
839 // What the store put in the key is not a document, which is the
840 // same failure a `fetch` of it would have recorded.
841 let text = String::from_utf8(entry.value.to_vec()).map_err(|error| {
842 self.failing(Error::remote(format!(
843 "{}: the value is not UTF-8: {error}",
844 self.describe()
845 )))
846 })?;
847
848 // `on_change`'s own refusal is deliberately *not* reported: the
849 // store answered, `apply` already counted the delivery, and
850 // whether the document installs is `ConfigStatus`'s half of the
851 // picture.
852 guarded(&mut on_change, Fetched::new(text, format), &self.describe())?;
853 }
854
855 // The stream ended without an error: the connection went away, or the
856 // bucket did. Also a failure — a watch that stops quietly is a
857 // configuration that stops updating quietly.
858 Err(self.failing(Error::remote(format!(
859 "{}: the watch ended; the stream was closed",
860 self.describe()
861 ))))
862 }
863}
864
865impl AsyncRemoteSource for Nats {
866 fn fetch(&self) -> Pin<Box<dyn Future<Output = Result<Fetched, Error>> + Send + '_>> {
867 Box::pin(async move {
868 let format = self.format()?;
869
870 let documents = self.documents().await?;
871
872 // Read in call order, which is the order the rule wants — so
873 // nothing is sorted here.
874 documents::merged(&documents, format, self.overlap(), &self.describe())
875 })
876 }
877
878 fn describe(&self) -> String {
879 format!(
880 "nats {} bucket {} {}",
881 self.server,
882 self.bucket,
883 self.keys.describe()
884 )
885 }
886}
887
888/// The shared vocabulary, applied to NATS' own connection options.
889///
890/// `async-nats` opens the files itself, so this passes paths through and
891/// refuses bytes. The refusal is the point: a store that quietly dropped a CA
892/// would leave a program believing it had pinned a private authority.
893fn with_tls_options(
894 mut options: ConnectOptions,
895 tls: &TlsConfig,
896 described: &str,
897) -> Result<ConnectOptions, Error> {
898 if let Some(ca) = tls.ca_certificate() {
899 let path = ca.path().ok_or_else(|| {
900 tls_core::unsupported(
901 described,
902 "a certificate authority from PEM bytes",
903 "`async-nats` opens the file itself; name it with \
904 `with_ca_certificate_file`",
905 )
906 })?;
907
908 options = options.add_root_certificates(path.to_path_buf());
909 }
910
911 if let Some(client) = tls.client_certificate() {
912 let (certificate, key) = match (client.certificate().path(), client.key().path()) {
913 (Some(certificate), Some(key)) => (certificate, key),
914 _ => {
915 return Err(tls_core::unsupported(
916 described,
917 "a client certificate from PEM bytes",
918 "`async-nats` opens the files itself; name them with \
919 `with_client_certificate_files`",
920 ))
921 }
922 };
923
924 options = options.add_client_certificate(certificate.to_path_buf(), key.to_path_buf());
925 }
926
927 // A caller who named an authority expects the connection to be
928 // authenticated against it. Without this a `nats://` URL negotiates
929 // plaintext and the authority is never consulted — a program believing it
930 // is pinned and is not, which is the failure this surface exists to
931 // prevent.
932 if !tls.is_empty() {
933 options = options.require_tls(true);
934 }
935
936 Ok(options)
937}
938
939impl std::fmt::Debug for Nats {
940 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
941 f.debug_struct("Nats")
942 .field("server", &self.server)
943 .field("bucket", &self.bucket)
944 .field("keys", &self.keys)
945 .field("format", &self.format)
946 .finish_non_exhaustive()
947 }
948}
949
950/// The format the keys' own extensions agree on, and the complaint if they do
951/// not.
952///
953/// Kept rather than reported at construction: `new` already fails for the
954/// things it reached — a server, a bucket — and a key list is not one of them.
955/// `with_format` settles it afterwards, which is exactly what the complaint
956/// tells the caller to do.
957fn agreed(keys: &Keys) -> (Option<Format>, Option<String>) {
958 match documents::agreed_format(keys.named()) {
959 Ok(format) => (format, None),
960 Err(complaint) => (None, Some(complaint)),
961 }
962}
963
964/// A server URL with its credentials removed, for error messages.
965///
966/// `nats://s3cr3t@host:4222` and `nats://user:s3cr3t@host:4222` are both
967/// ordinary ways to point this at a server, and both put a credential in a
968/// string that `describe()` quotes into every error. A comma-separated list
969/// is redacted server by server, because that is also a shape NATS accepts.
970///
971/// [`LoneAuthority::Secret`] is the NATS-specific half: an authority with no
972/// colon in it is a *token* here, and keeping it would keep the credential.
973/// The Redis crate reads the same shape as a user name, which is why the two
974/// pass different arguments to one implementation rather than keeping two.
975fn redacted(servers: &str) -> String {
976 dynamic_config_store_core::redacted_list(servers, LoneAuthority::Secret)
977}
978
979#[cfg(test)]
980mod tests {
981 use std::io::{Read, Write};
982 use std::net::TcpListener;
983
984 use super::*;
985
986 /// Speaks just enough of the NATS protocol to greet a client and then
987 /// answer it with `reply`. No Docker, no JetStream — the handshake is all
988 /// this needs, because the handshake is where a credential is refused.
989 fn scripted(reply: &'static str) -> (String, std::thread::JoinHandle<()>) {
990 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
991 let address = format!("nats://{}", listener.local_addr().unwrap());
992
993 let server = std::thread::spawn(move || {
994 let Ok((mut stream, _)) = listener.accept() else {
995 return;
996 };
997
998 let info = r#"{"server_id":"scripted","server_name":"scripted","version":"2.10.0","proto":1,"go":"","host":"127.0.0.1","port":4222,"headers":true,"max_payload":1048576}"#;
999 let _ = stream.write_all(format!("INFO {info}\r\n").as_bytes());
1000
1001 // The client answers with CONNECT and PING; anything up to the
1002 // end of the PING is enough to know it is our turn again.
1003 let mut seen = Vec::new();
1004 let mut byte = [0u8; 1];
1005
1006 while !seen.ends_with(b"PING\r\n") && stream.read(&mut byte).is_ok_and(|n| n == 1) {
1007 seen.push(byte[0]);
1008 }
1009
1010 let _ = stream.write_all(reply.as_bytes());
1011 });
1012
1013 (address, server)
1014 }
1015
1016 /// The credential half of the promise: a server that says no is `Auth`,
1017 /// and no amount of waiting changes its mind.
1018 #[tokio::test]
1019 async fn a_refused_credential_is_an_auth_failure() {
1020 let (address, server) = scripted("-ERR 'Authorization Violation'\r\n");
1021
1022 let error = Nats::with_options(
1023 &address,
1024 "config",
1025 "db.json",
1026 ConnectOptions::new().token("hunter2-nats-token".to_owned()),
1027 )
1028 .await
1029 .expect_err("the server refused the token");
1030
1031 let _ = server.join();
1032
1033 assert_eq!(error.kind(), dynamic_config::ErrorKind::Auth);
1034 assert!(
1035 !error.to_string().contains("hunter2"),
1036 "a refused credential must not be echoed back: {error}"
1037 );
1038 }
1039
1040 /// `nats://token@host` is an ordinary way to point this at a server, and
1041 /// the address it produces is quoted into every error message. The token
1042 /// must not survive that trip.
1043 #[test]
1044 fn a_credential_in_the_url_never_reaches_an_error_message() {
1045 assert_eq!(
1046 redacted("nats://hunter2-token@nats.internal:4222"),
1047 "nats://***@nats.internal:4222"
1048 );
1049 assert_eq!(
1050 redacted("nats://app:hunter2@nats.internal:4222"),
1051 "nats://app:***@nats.internal:4222"
1052 );
1053 // A password may contain `@`; splitting on the first one would leave
1054 // its tail in the "redacted" output.
1055 assert_eq!(
1056 redacted("nats://app:p@ss@w@rd@nats.internal:4222"),
1057 "nats://app:***@nats.internal:4222"
1058 );
1059 // A list is a shape NATS accepts, so each server is redacted.
1060 assert_eq!(
1061 redacted("nats://hunter2@a:4222,nats://hunter2@b:4222"),
1062 "nats://***@a:4222,nats://***@b:4222"
1063 );
1064 // Nothing to redact is left exactly alone.
1065 assert_eq!(
1066 redacted("nats://nats.internal:4222"),
1067 "nats://nats.internal:4222"
1068 );
1069 assert_eq!(redacted("not a url"), "not a url");
1070 }
1071
1072 /// And end to end: the address the source keeps, and every error it
1073 /// renders, carry the redacted form.
1074 #[tokio::test]
1075 async fn a_credential_in_the_url_never_reaches_a_failed_connection() {
1076 // Port 9 is discard; nothing listens there.
1077 let error = Nats::new("nats://hunter2-token@127.0.0.1:9", "config", "db.json")
1078 .await
1079 .expect_err("nothing is listening");
1080
1081 let printed = format!("{error} {error:?}");
1082
1083 assert!(!printed.contains("hunter2"), "{printed}");
1084 assert!(printed.contains("127.0.0.1:9"), "{printed}");
1085 }
1086
1087 /// Two keys naming two formats is the confusing failure worth catching by
1088 /// name: `server.toml` parsed as JSON is a syntax error about a file that
1089 /// has no syntax error in it. It is kept rather than raised at
1090 /// construction because `with_format` is allowed to settle it.
1091 #[test]
1092 fn keys_naming_two_formats_are_reported_rather_than_guessed() {
1093 let (format, disagreement) = agreed(&Keys::several(["base.json", "local.toml"]));
1094
1095 assert_eq!(format, None);
1096
1097 let complaint = disagreement.expect("json and toml cannot both be it");
1098
1099 assert!(complaint.contains("base.json"), "{complaint}");
1100 assert!(complaint.contains("local.toml"), "{complaint}");
1101 assert!(complaint.contains("with_format"), "{complaint}");
1102
1103 // One format between them is no disagreement at all.
1104 assert_eq!(
1105 agreed(&Keys::several(["base.json", "local.json"])),
1106 (Some(Format::Json), None)
1107 );
1108 }
1109
1110 /// The diagnostic names the whole set, because the merged document is one
1111 /// layer and one layer cannot say more. A single key must keep the
1112 /// wording it always had.
1113 #[test]
1114 fn a_diagnostic_names_the_whole_set_and_one_key_reads_as_it_always_did() {
1115 assert_eq!(Keys::one("db.json").describe(), "key db.json");
1116 assert_eq!(
1117 Keys::several(["base.json", "local.json"]).describe(),
1118 "keys base.json, local.json"
1119 );
1120 }
1121
1122 /// The other half, and the one that costs more to get wrong: a server
1123 /// that is simply not there must stay `Remote`, so a watch loop backs off
1124 /// instead of stopping.
1125 #[tokio::test]
1126 async fn an_unreachable_server_is_remote_rather_than_auth() {
1127 // Port 9 is discard; nothing listens there.
1128 let error = Nats::with_options(
1129 "nats://127.0.0.1:9",
1130 "config",
1131 "db.json",
1132 ConnectOptions::new()
1133 .token("hunter2-nats-token".to_owned())
1134 .retry_on_initial_connect()
1135 .max_reconnects(Some(0)),
1136 )
1137 .await
1138 .expect_err("nothing is listening");
1139
1140 assert_eq!(error.kind(), dynamic_config::ErrorKind::Remote);
1141 assert!(!error.to_string().contains("hunter2"), "{error}");
1142 }
1143 // -----------------------------------------------------------------------
1144 // TLS: the shared vocabulary, and the half NATS cannot express.
1145 //
1146 // `with_tls_options` is the whole translation and is a pure function, so
1147 // it is tested directly: the refusals are the interesting part, and a
1148 // refusal that only happened after a connection attempt would be a
1149 // refusal nobody sees in a unit test.
1150 // -----------------------------------------------------------------------
1151
1152 /// `async-nats` opens the CA file itself, so there is no byte-taking door
1153 /// to forward to. Refused, and told where to go — never ignored: a caller
1154 /// who supplied an authority and got the platform trust store has a
1155 /// program that believes it is pinned and is not.
1156 #[test]
1157 fn a_certificate_authority_from_bytes_is_refused_and_says_what_to_use() {
1158 let error = with_tls_options(
1159 ConnectOptions::new(),
1160 &TlsConfig::new().with_ca_certificate_pem("-----BEGIN CERTIFICATE-----\n"),
1161 "nats nats://nats.internal:4222 key db.json",
1162 )
1163 .expect_err("async-nats takes paths");
1164
1165 assert!(error.to_string().contains("PEM bytes"), "{error}");
1166 assert!(
1167 error.to_string().contains("with_ca_certificate_file"),
1168 "{error}"
1169 );
1170 assert!(
1171 error.to_string().contains("refused rather than ignored"),
1172 "{error}"
1173 );
1174 }
1175
1176 /// The same for the client certificate, and the private key is why the
1177 /// obvious workaround is not taken: writing the bytes to a temporary file
1178 /// would put a private key on a disk that never asked for one.
1179 #[test]
1180 fn a_client_certificate_from_bytes_is_refused_and_never_quotes_the_key() {
1181 const PLANTED: &str = "PLANTED-PRIVATE-KEY-MATERIAL";
1182
1183 let error = with_tls_options(
1184 ConnectOptions::new(),
1185 &TlsConfig::new().with_client_certificate_pem("cert", PLANTED),
1186 "nats nats://nats.internal:4222 key db.json",
1187 )
1188 .expect_err("async-nats takes paths");
1189
1190 assert!(!error.to_string().contains(PLANTED), "{error}");
1191 assert!(
1192 error.to_string().contains("with_client_certificate_files"),
1193 "{error}"
1194 );
1195 }
1196
1197 /// The file spellings are the ones NATS has, and they pass through — the
1198 /// files themselves are opened by `async-nats` at connect time, so
1199 /// nothing here has to exist yet.
1200 #[test]
1201 fn the_file_spellings_are_accepted_and_turn_tls_on() {
1202 with_tls_options(
1203 ConnectOptions::new(),
1204 &TlsConfig::new()
1205 .with_ca_certificate_file("/etc/nats/ca.pem")
1206 .with_client_certificate_files("/etc/nats/client.crt", "/etc/nats/client.key"),
1207 "nats nats://nats.internal:4222 key db.json",
1208 )
1209 .expect("paths are what this client takes");
1210 }
1211
1212 /// An empty configuration must not turn `require_tls` on behind a
1213 /// caller's back: it means "the platform defaults", which for a
1214 /// `nats://` URL is the connection they already had.
1215 #[test]
1216 fn an_empty_configuration_changes_nothing() {
1217 with_tls_options(
1218 ConnectOptions::new(),
1219 &TlsConfig::new(),
1220 "nats nats://nats.internal:4222 key db.json",
1221 )
1222 .expect("nothing was asked for");
1223 }
1224
1225 /// A NATS URL carries its credential in the authority, so a refusal that
1226 /// quoted the description raw would put a token in a log. The description
1227 /// reaching this module is already redacted, and this pins that the
1228 /// refusal adds nothing back.
1229 #[test]
1230 fn a_refusal_carries_the_redacted_server_and_not_the_token() {
1231 let described = format!(
1232 "nats {}",
1233 redacted("nats://hunter2-token@nats.internal:4222")
1234 );
1235
1236 let error = with_tls_options(
1237 ConnectOptions::new(),
1238 &TlsConfig::new().with_ca_certificate_pem("x"),
1239 &described,
1240 )
1241 .expect_err("async-nats takes paths");
1242
1243 assert!(!error.to_string().contains("hunter2"), "{error}");
1244 assert!(error.to_string().contains("nats.internal:4222"), "{error}");
1245 }
1246}