Skip to main content

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}