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}