Skip to main content

dynamic_config_server/
server.rs

1//! The served sections, and the server that owns them.
2//!
3//! Each served section is a [`Dynamic<Document>`] — this crate is a *user*
4//! of the library, not a reimplementation of it. That is the constraint the
5//! design is built around: the same loader resolves the section, the same
6//! watcher notices the file change, the same last-known-good behaviour keeps
7//! a bad edit from taking the section down, and the same
8//! [`ConfigStatus`](dynamic_config::ConfigStatus) answers for it. If this
9//! server ever needs something the library cannot do, that is a library
10//! change, not a server one.
11//!
12//! Nothing here polls. A section reloads because the file watcher said so,
13//! and `/status` is a handful of atomic loads — so an idle server with a
14//! thousand sections costs a thousand idle inotify registrations and no CPU.
15
16use std::collections::BTreeMap;
17use std::fmt;
18use std::net::SocketAddr;
19use std::sync::atomic::{AtomicUsize, Ordering};
20use std::sync::Arc;
21use std::time::Duration;
22
23use dynamic_config::{Builder, Changes, ConfigStatus, Dynamic};
24
25use crate::audit::{AuditEntry, AuditSink, StderrAudit};
26use crate::auth::{Authenticator, Principal, Token};
27use crate::config::{Refusal, SectionConfig, ServerConfig};
28use crate::document::Document;
29
30/// One served application-and-profile pair.
31pub struct Section {
32    application: String,
33    profile: String,
34    config: Dynamic<Document>,
35    /// Dropping this stops the watch, so the handle lives as long as the
36    /// section does and is never read.
37    _watch: Option<dynamic_config::watch::WatchHandle>,
38}
39
40impl Section {
41    /// The application this serves.
42    #[must_use]
43    pub fn application(&self) -> &str {
44        &self.application
45    }
46
47    /// The profile this serves.
48    #[must_use]
49    pub fn profile(&self) -> &str {
50        &self.profile
51    }
52
53    /// The document currently serving, or `None` before the first install.
54    ///
55    /// One atomic load. A handler takes it once and reuses the `Arc`, so a
56    /// reload landing mid-request cannot show one response two generations.
57    #[must_use]
58    pub fn current(&self) -> Option<Arc<Document>> {
59        self.config.current()
60    }
61
62    /// Installs since this section was created.
63    #[must_use]
64    pub fn generation(&self) -> u64 {
65        self.config.generation()
66    }
67
68    /// The serving document together with a generation that is never ahead
69    /// of it.
70    ///
71    /// Two atomic loads, and their order is the whole point.
72    /// [`ConfigCell`](dynamic_config::ConfigCell) publishes a snapshot's
73    /// metadata *after* the snapshot itself — deliberately, so that reading
74    /// configuration stays one load with nothing to project out of it — so
75    /// a generation read first may lag the document and can never lead it.
76    ///
77    /// Reading the document first inverts that, and the inversion is the
78    /// harmful direction: a reload landing between the two loads would send
79    /// the *previous* document under the *new* number, and a client that
80    /// records it — or resumes its change stream with it — has been told it
81    /// consumed an update whose contents it never received. This way round,
82    /// the worst case is a response labelled one install behind its own
83    /// contents: the client is told about that install again and fetches
84    /// once more, which costs a round trip and loses nothing.
85    #[must_use]
86    pub fn installed(&self) -> Option<(u64, Arc<Document>)> {
87        let generation = self.generation();
88
89        self.current().map(|document| (generation, document))
90    }
91
92    /// What is true of this section right now. No I/O.
93    #[must_use]
94    pub fn status(&self) -> ConfigStatus {
95        self.config.status()
96    }
97
98    /// Whether this section can be served.
99    ///
100    /// A section is ready when it has a document *and* the last reload
101    /// installed one. The second half is the point of fronting a store: a
102    /// bad edit upstream leaves the previous document serving — callers see
103    /// no outage — and says so here, so a deployment pipeline notices
104    /// before the next restart turns "stale but working" into "will not
105    /// start".
106    #[must_use]
107    pub fn is_ready(&self) -> bool {
108        self.current().is_some() && self.status().is_healthy()
109    }
110
111    /// A handle woken by every later install of this section.
112    ///
113    /// What the change stream awaits. It costs one `Arc` clone and a `u64`
114    /// — no document, no diff, no queue — which is what lets a connection
115    /// per pod be an ordinary number rather than a memory bound.
116    #[must_use]
117    pub fn changes(&self) -> Changes<Document> {
118        self.config.changes()
119    }
120
121    /// One reload: read the sources, install if they are good.
122    ///
123    /// # Errors
124    ///
125    /// Whatever the load reports. A failure installs nothing and is counted
126    /// in [`status`](Self::status).
127    pub fn reload(&self) -> Result<(), dynamic_config::Error> {
128        self.config.reload()
129    }
130
131    /// This section's sources, for the diagnostics that re-read them.
132    ///
133    /// Cloned rather than borrowed because `check` and `explain` run on the
134    /// blocking pool, which needs to own what it reads.
135    #[must_use]
136    pub fn sources(&self) -> Builder<Document> {
137        self.config.builder().clone()
138    }
139}
140
141/// Keys and shape, never the document: a section holds resolved
142/// configuration and this type is the obvious thing to `{:?}` in a handler.
143impl fmt::Debug for Section {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        f.debug_struct("Section")
146            .field("application", &self.application)
147            .field("profile", &self.profile)
148            .field("generation", &self.generation())
149            .finish_non_exhaustive()
150    }
151}
152
153/// Everything the HTTP layer serves from.
154///
155/// Built by [`Server::start`], shared behind an `Arc` by the router, and
156/// immutable afterwards: sections are established at startup and the set
157/// does not change while the process runs. Adding one is a restart, which is
158/// the same answer the rest of this workspace gives to "where do the sources
159/// live".
160pub struct Server {
161    sections: BTreeMap<(String, String), Arc<Section>>,
162    authenticator: Authenticator,
163    audit: Arc<dyn AuditSink>,
164    address: SocketAddr,
165    /// The loaded certificate, key and client verifier, when this server
166    /// terminates TLS itself. Loaded at startup so that a key that cannot be
167    /// read is a refusal rather than the first connection's problem.
168    #[cfg(feature = "tls")]
169    tls: Option<crate::tls::Tls>,
170    /// The ceiling on open change streams, and how many are open. Zero as a
171    /// ceiling means the endpoint is not served at all.
172    max_streams: usize,
173    streams: Arc<AtomicUsize>,
174}
175
176/// One open change stream's place in the ceiling.
177///
178/// Held by the stream and released when it is dropped — which is when the
179/// connection ends, however it ends: a client that goes away, a proxy that
180/// times out, or a process that shuts down all drop the response body, and
181/// the body owns this.
182#[derive(Debug)]
183pub struct StreamPermit {
184    open: Arc<AtomicUsize>,
185}
186
187impl Drop for StreamPermit {
188    fn drop(&mut self) {
189        self.open.fetch_sub(1, Ordering::Release);
190    }
191}
192
193impl Server {
194    /// Loads every section and refuses to start if anything is wrong.
195    ///
196    /// The order matters: [`ServerConfig::validate`] runs first and nothing
197    /// is opened if it refuses, so a server that would have been
198    /// world-readable never gets as far as reading a secret off disk.
199    ///
200    /// A section that will not load at startup is fatal. A config server
201    /// that comes up serving nothing for one application is a silent outage
202    /// for whoever needed it — better to fail the deployment.
203    ///
204    /// # Errors
205    ///
206    /// A [`Refusal`], a section that will not load, or a watch that will not
207    /// start.
208    pub fn start(config: &ServerConfig) -> Result<Self, StartupError> {
209        Self::start_with(config, StderrAudit)
210    }
211
212    /// [`start`](Self::start), with somewhere else for the audit log to go.
213    ///
214    /// # Errors
215    ///
216    /// As [`start`](Self::start).
217    pub fn start_with(config: &ServerConfig, audit: impl AuditSink) -> Result<Self, StartupError> {
218        config.validate()?;
219
220        let address = config.address()?;
221        // Before any section is opened: a private key that cannot be read,
222        // or that anybody on the host can read, is the same class of problem
223        // as a roster that would serve `billing` to everyone, and it is
224        // answered in the same place — at startup, with nothing yet open.
225        #[cfg(feature = "tls")]
226        let tls = config
227            .tls
228            .as_ref()
229            .map(crate::tls::Tls::load)
230            .transpose()
231            .map_err(StartupError::Tls)?;
232        let debounce = Duration::from_millis(config.watch_debounce_ms);
233        let mut sections = BTreeMap::new();
234
235        for described in &config.sections {
236            let section = load_section(described, debounce)?;
237
238            sections.insert(
239                (described.application.clone(), described.profile.clone()),
240                Arc::new(section),
241            );
242        }
243
244        let mut clients: Vec<(Token, Principal)> = Vec::new();
245        let mut anonymous = None;
246
247        for client in &config.clients {
248            let principal = Principal::new(&client.name, client.applications.clone());
249
250            match &client.token {
251                Some(token) => clients.push((token.clone(), principal)),
252                None => anonymous = Some(principal),
253            }
254        }
255
256        #[allow(unused_mut)]
257        let mut authenticator = Authenticator::new(clients, anonymous);
258
259        #[cfg(feature = "kubernetes-auth")]
260        if let Some(kubernetes) = &config.kubernetes {
261            if kubernetes.grants.is_empty() {
262                return Err(StartupError::Refused(Refusal::KubernetesAuth {
263                    reason: "no grants: an auth mode that admits nobody is a \
264                             typo, not a policy"
265                        .to_owned(),
266                }));
267            }
268
269            let mut grants = Vec::new();
270
271            for grant in &kubernetes.grants {
272                let Some((namespace, account)) = grant.service_account.split_once(':') else {
273                    return Err(StartupError::Refused(Refusal::KubernetesAuth {
274                        reason: format!(
275                            "service_account {:?} — the form is \
276                             <namespace>:<serviceaccount>",
277                            grant.service_account
278                        ),
279                    }));
280                };
281
282                if namespace.is_empty() || account.is_empty() {
283                    return Err(StartupError::Refused(Refusal::KubernetesAuth {
284                        reason: format!(
285                            "service_account {:?} names an empty half",
286                            grant.service_account
287                        ),
288                    }));
289                }
290
291                grants.push((
292                    grant.service_account.clone(),
293                    Principal::new(
294                        format!("k8s:{}", grant.service_account),
295                        grant.applications.clone(),
296                    ),
297                ));
298            }
299
300            let verifier = crate::kubernetes::KubernetesVerifier::in_cluster(
301                kubernetes.audience.clone(),
302                grants,
303            )
304            .map_err(|reason| StartupError::Refused(Refusal::KubernetesAuth { reason }))?;
305
306            authenticator = authenticator.with_kubernetes(verifier);
307        }
308
309        Ok(Self {
310            sections,
311            authenticator,
312            audit: Arc::new(audit),
313            address,
314            #[cfg(feature = "tls")]
315            tls,
316            max_streams: config.max_stream_connections,
317            streams: Arc::new(AtomicUsize::new(0)),
318        })
319    }
320
321    /// The address the binary listens on.
322    #[must_use]
323    pub fn address(&self) -> SocketAddr {
324        self.address
325    }
326
327    /// The loaded TLS configuration, or `None` for a server that expects a
328    /// terminator in front of it.
329    ///
330    /// What [`serve_tls`](crate::serve_tls) needs, and what tells an
331    /// embedder which of the two serving paths to take.
332    #[cfg(feature = "tls")]
333    #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
334    #[must_use]
335    pub fn tls(&self) -> Option<&crate::tls::Tls> {
336        self.tls.as_ref()
337    }
338
339    /// Where this server's audit lines go, for the parts of the serving path
340    /// that are outside a request — a TLS handshake that was refused has no
341    /// request to be recorded against, and is still something an operator
342    /// looks for in the audit trail.
343    #[cfg(feature = "tls")]
344    pub(crate) fn audit_sink(&self) -> Arc<dyn AuditSink> {
345        Arc::clone(&self.audit)
346    }
347
348    /// Who is calling, given the raw `Authorization` header.
349    #[must_use]
350    pub fn authenticate(&self, authorization: Option<&str>) -> Option<Principal> {
351        self.authenticator.authenticate(authorization)
352    }
353
354    /// The section serving `application` at `profile`, if one does.
355    ///
356    /// **Call this only after authorising.** It is the lookup that would
357    /// otherwise tell a caller whether a section exists, and the whole
358    /// not-an-oracle property rests on nothing reaching it that has not
359    /// already been granted the application.
360    #[must_use]
361    pub fn section(&self, application: &str, profile: &str) -> Option<&Arc<Section>> {
362        // Owned keys because `BTreeMap<(String, String), _>` cannot be
363        // probed with a pair of `&str` without a `Borrow` impl that does not
364        // exist. The map is small — one entry per served pair — and this is
365        // not the read path; the document itself comes from an atomic load.
366        self.sections
367            .get(&(application.to_owned(), profile.to_owned()))
368    }
369
370    /// Every served section.
371    pub fn sections(&self) -> impl Iterator<Item = &Arc<Section>> {
372        self.sections.values()
373    }
374
375    /// Whether every section is ready. What `/readyz` answers.
376    #[must_use]
377    pub fn is_ready(&self) -> bool {
378        self.sections().all(|section| section.is_ready())
379    }
380
381    /// Records one request.
382    pub fn record(&self, entry: &AuditEntry) {
383        self.audit.record(entry);
384    }
385
386    /// Whether this server serves change streams at all.
387    #[must_use]
388    pub fn streams_enabled(&self) -> bool {
389        self.max_streams > 0
390    }
391
392    /// A place in the change-stream ceiling, if there is one free.
393    ///
394    /// A compare-and-swap loop rather than a fetch-add and a refund: an
395    /// increment that overshoots is briefly visible to another request,
396    /// which would let two callers each see the ceiling exceeded and both
397    /// back off.
398    #[must_use]
399    pub fn open_stream(&self) -> Option<StreamPermit> {
400        let mut open = self.streams.load(Ordering::Acquire);
401
402        loop {
403            if open >= self.max_streams {
404                return None;
405            }
406
407            match self.streams.compare_exchange_weak(
408                open,
409                open + 1,
410                Ordering::AcqRel,
411                Ordering::Acquire,
412            ) {
413                Ok(_) => {
414                    return Some(StreamPermit {
415                        open: Arc::clone(&self.streams),
416                    })
417                }
418                Err(current) => open = current,
419            }
420        }
421    }
422
423    /// How many change streams are open. For the tests and for a deployment
424    /// that wants the number in its own metrics.
425    #[must_use]
426    pub fn open_streams(&self) -> usize {
427        self.streams.load(Ordering::Acquire)
428    }
429}
430
431impl fmt::Debug for Server {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        f.debug_struct("Server")
434            .field("address", &self.address)
435            .field("sections", &self.sections.len())
436            .field("anonymous", &self.authenticator.allows_anonymous())
437            // The posture, never the material: `Tls`'s own `Debug` is
438            // hand-written for the same reason this one is.
439            .field("tls", &self.posture())
440            .finish_non_exhaustive()
441    }
442}
443
444impl Server {
445    /// How this server's own socket is protected, in a few words — for a
446    /// startup line, a `Debug` and an operator's first question.
447    ///
448    /// One of `none`, `tls` or `tls, client certificate required`. Never a
449    /// path, never a subject, never a key: it says which of the three shapes
450    /// this process is in and nothing about the material it is in it with.
451    #[cfg(feature = "tls")]
452    #[must_use]
453    pub fn posture(&self) -> &'static str {
454        match self.tls.as_ref().map(crate::tls::Tls::is_mutual) {
455            Some(true) => "tls, client certificate required",
456            Some(false) => "tls",
457            None => "none",
458        }
459    }
460
461    /// The same, for a build with no TLS in it: there is one answer.
462    #[cfg(not(feature = "tls"))]
463    #[must_use]
464    pub fn posture(&self) -> &'static str {
465        "none"
466    }
467}
468
469fn load_section(described: &SectionConfig, debounce: Duration) -> Result<Section, StartupError> {
470    let mut builder = Builder::<Document>::new(described.application.as_str());
471
472    if described.whole_document {
473        builder = builder.whole_document();
474    }
475
476    for file in &described.files {
477        builder = builder.file(file.as_str());
478    }
479    if let Some(prefix) = &described.env_prefix {
480        builder = builder.env(prefix.as_str());
481    }
482
483    let config = Dynamic::new(builder);
484
485    config.init().map_err(|source| StartupError::Section {
486        application: described.application.clone(),
487        profile: described.profile.clone(),
488        source,
489    })?;
490
491    // Zero disables watching, for a deployment that reloads by restarting or
492    // by a means of its own. Anything else is the library's watcher, which
493    // is event-driven — this server has no polling loop of its own and does
494    // not want one.
495    let watch = if debounce.is_zero() {
496        None
497    } else {
498        Some(
499            config
500                .watch(debounce)
501                .map_err(|source| StartupError::Watch {
502                    application: described.application.clone(),
503                    profile: described.profile.clone(),
504                    source,
505                })?,
506        )
507    };
508
509    Ok(Section {
510        application: described.application.clone(),
511        profile: described.profile.clone(),
512        config,
513        _watch: watch,
514    })
515}
516
517/// Why a server did not start.
518#[derive(Debug)]
519#[non_exhaustive]
520pub enum StartupError {
521    /// The configuration was refused before anything was opened.
522    Refused(Refusal),
523    /// A section's sources would not load.
524    Section {
525        /// The application.
526        application: String,
527        /// The profile.
528        profile: String,
529        /// What the load reported. Value-free, like every error in the
530        /// library.
531        source: dynamic_config::Error,
532    },
533    /// A section's watch would not start.
534    Watch {
535        /// The application.
536        application: String,
537        /// The profile.
538        profile: String,
539        /// What the watcher reported.
540        source: std::io::Error,
541    },
542    /// The TLS material would not load. Carries no key material: see
543    /// [`TlsError`](crate::tls::TlsError).
544    #[cfg(feature = "tls")]
545    #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
546    Tls(crate::tls::TlsError),
547}
548
549impl From<Refusal> for StartupError {
550    fn from(refusal: Refusal) -> Self {
551        Self::Refused(refusal)
552    }
553}
554
555impl fmt::Display for StartupError {
556    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
557        match self {
558            Self::Refused(refusal) => write!(f, "refusing to start: {refusal}"),
559            Self::Section {
560                application,
561                profile,
562                source,
563            } => write!(
564                f,
565                "refusing to start: the section `{application}`/`{profile}` will not load: \
566                 {source}"
567            ),
568            Self::Watch {
569                application,
570                profile,
571                source,
572            } => write!(
573                f,
574                "refusing to start: the section `{application}`/`{profile}` cannot be \
575                 watched: {source}"
576            ),
577            #[cfg(feature = "tls")]
578            Self::Tls(error) => write!(f, "refusing to start: {error}"),
579        }
580    }
581}
582
583impl std::error::Error for StartupError {
584    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
585        match self {
586            Self::Refused(refusal) => Some(refusal),
587            Self::Section { source, .. } => Some(source),
588            Self::Watch { source, .. } => Some(source),
589            #[cfg(feature = "tls")]
590            Self::Tls(error) => Some(error),
591        }
592    }
593}