acme_proxy_server/assembly.rs
1//! What one configuration generation hands its profiles ([`GenerationParts`]),
2//! and what outlives it ([`Assembly`]). What it dials through is
3//! [`acme_proxy_net::egress::Egress`].
4
5use std::sync::Arc;
6
7use acme_proxy_core::config;
8use acme_proxy_core::config::Config;
9use acme_proxy_jobs::metrics;
10use acme_proxy_jobs::notify;
11use acme_proxy_net::egress::Egress;
12use acme_proxy_signer as signer;
13use acme_proxy_store::db::Database;
14
15/// The three things one configuration generation contributes to its profiles,
16/// built before any of them is published.
17///
18/// A struct because [`profile::build_all_with`](super::profile::build_all_with)
19/// would otherwise take three same-shaped values positionally, and because the
20/// three are built together and must be published together —
21/// `server::generation::publish_reload` swaps the notifier map and the signer
22/// set in the same uninterruptible run as the routers built from them.
23pub struct GenerationParts {
24 pub egress: Arc<Egress>,
25 pub dispatchers: notify::DispatcherMap,
26 /// The backends, which the job handlers sign and revoke through.
27 pub signers: signer::SignerSet,
28 /// Their read sides, which the profiles serve from.
29 pub infos: signer::SignerSet<dyn signer::SignerInfo>,
30}
31
32/// What survives a configuration reload.
33///
34/// Every generation rebuilds its profiles, its routers, its job registry, its
35/// egress clients and any signer backend whose configuration moved. The things
36/// here are built once for the life of the process and handed to each generation
37/// instead — and after three rounds of moving things *off* this list, everything
38/// left is here because rebuilding it would lose something, never because
39/// rebuilding it would merely cost something:
40///
41/// - `database` and `jobs` are the pool and its enqueue side. `database.url` is
42/// the one key [`crate::reload`] still refuses, and this is why.
43/// - `metrics` is a **correctness** requirement. A registry rebuilt per
44/// generation would reset every counter on `SIGHUP`, and a counter going
45/// backwards is precisely how Prometheus recognises a process restart — so
46/// `rate()` would report the whole pre-reload total as a spike on every
47/// configuration change.
48/// - `signers` is the *previous* generation's backend set, kept so the next
49/// reload can reuse a backend whose configuration did not move (see
50/// [`signer::build_backends`]); `infos` the same for their read sides. Behind
51/// a `Mutex` because each is written once per generation; nothing reads
52/// either to serve a request, since a `Profile` holds its own read side.
53/// - `roles` decides whether there are backends at all: only a process running
54/// the `worker` role builds one, so the others never read a CA key, log in to
55/// a token or contact a relay's upstream — not at startup, not on reload.
56/// - `notifiers` is a handle rather than a map, so `[notify]` can reload
57/// underneath the backends that captured it.
58///
59/// `resolver` and `proxies` used to be here, justified by the signers caching
60/// them at construction. They moved to [`Egress`] when that stopped being a
61/// reason to freeze `[dns]`/`[proxy]` and became a reason to rebuild a signer.
62pub struct Assembly {
63 pub database: Arc<Database>,
64 pub jobs: acme_proxy_jobs::jobs::JobQueue,
65 pub metrics: Arc<metrics::Metrics>,
66 pub notifiers: notify::Notifiers,
67 notifiers_tx: notify::NotifiersSender,
68 signers: std::sync::Mutex<signer::SignerSet>,
69 /// The previous generation's read sides, kept for `signers`' reason.
70 infos: std::sync::Mutex<signer::SignerSet<dyn signer::SignerInfo>>,
71 roles: super::RoleSet,
72}
73
74impl Assembly {
75 /// Builds everything that outlives a generation, plus the first generation's
76 /// own parts.
77 ///
78 /// Those come back rather than being kept here because they are *not*
79 /// long-lived: the caller hands them to
80 /// [`profile::build_all_with`](super::profile::build_all_with) and then
81 /// forgets them, and every later generation builds its own through
82 /// [`build_parts`](Self::build_parts).
83 pub fn new(
84 roles: super::RoleSet,
85 resolved: &[config::ProfileConfig],
86 database: Arc<Database>,
87 jobs: acme_proxy_jobs::jobs::JobQueue,
88 config: &Config,
89 ) -> anyhow::Result<(Self, GenerationParts)> {
90 // Built before the signers, because the `relay` backend settles an
91 // issuance from a background task that has no request and no `Auditor`,
92 // so it counts that issuance through a handle it was given at
93 // construction.
94 let metrics = Arc::new(metrics::Metrics::new(database.clone()).with_roles(&roles.labels()));
95 // Opened over an empty map and republished immediately below, so the
96 // handle the signers capture is the one every later generation writes
97 // into.
98 let (notifiers_tx, notifiers) = notify::notifiers_channel(notify::DispatcherMap::new());
99
100 let assembly = Self {
101 database,
102 jobs,
103 metrics,
104 notifiers,
105 notifiers_tx,
106 signers: std::sync::Mutex::new(signer::SignerSet::default()),
107 infos: std::sync::Mutex::new(signer::SignerSet::default()),
108 roles,
109 };
110 let parts = assembly.build_parts(resolved, config)?;
111 // The first generation's map has to reach the handle before anything
112 // dispatches through it; every later one goes through `publish` in the
113 // reload's own synchronous run.
114 assembly.publish_notifiers(parts.dispatchers.clone());
115 assembly.publish_signers(parts.signers.clone(), parts.infos.clone());
116 Ok((assembly, parts))
117 }
118
119 /// Builds one generation's egress, dispatchers and signer backends, without
120 /// publishing any of them.
121 ///
122 /// Separate from [`publish_notifiers`](Self::publish_notifiers) and
123 /// [`publish_signers`](Self::publish_signers) because a reload must be able
124 /// to fail *after* building all three and still leave the running generation
125 /// untouched. Everything fallible is here; everything published is there.
126 ///
127 /// May block: `RelaySigner::from_config` contacts the upstream the first
128 /// time it is built for an account with no `kid` sidecar yet, which is why
129 /// `server::supervisor::supervise_reloads` runs this on a blocking thread.
130 pub fn build_parts(
131 &self,
132 resolved: &[config::ProfileConfig],
133 config: &Config,
134 ) -> anyhow::Result<GenerationParts> {
135 // Resolved before anything can dial: a proxy URL that cannot be
136 // understood must stop the process, and the `relay` backend below makes
137 // a real network call on its very first startup.
138 let egress = Arc::new(Egress::from_config(config)?);
139 // Built before the signer backends: the `relay` backend's background
140 // completion task has no `Profile`/`AppState` to reach a notifier
141 // through (it outlives any single request, the same reason it is handed
142 // `database`), so it is instead handed the whole `profile name ->
143 // dispatcher` map and looks up the right one by `Order.profile` once an
144 // issuance settles.
145 let mut dispatchers = notify::build_registry(resolved, egress.outbound(), &self.jobs)?;
146 // The process-wide web-admin security dispatcher, registered under a
147 // reserved key that no profile name can collide with. Built only when
148 // the panel is on; `NotifyJob` routes a `notify_deliver` row naming it
149 // here with no special case, and a reload republishes it in this same
150 // map.
151 if config.admin.enabled {
152 dispatchers.insert(
153 notify::ADMIN_DISPATCHER_KEY.to_string(),
154 notify::from_config(
155 notify::ADMIN_DISPATCHER_KEY,
156 &config.admin.notify,
157 egress.outbound(),
158 &self.jobs,
159 )?,
160 );
161 }
162 let signer_parts = signer::SignerParts {
163 database: self.database.clone(),
164 notifiers: self.notifiers.clone(),
165 metrics: self.metrics.clone(),
166 egress: egress.clone(),
167 jobs: self.jobs.clone(),
168 };
169 let previous = self
170 .signers
171 .lock()
172 .unwrap_or_else(std::sync::PoisonError::into_inner)
173 .clone();
174 // The worker's alone. Every other role serves from the read sides below
175 // and queues what needs a key, so it never reads `ca.key`, never logs
176 // in to a PKCS#11 token and never registers with a relay's upstream —
177 // which is also what makes the worker the one process that generates
178 // first-run material.
179 let signers = if self.roles.has(super::ProcessRole::Worker) {
180 signer::build_backends(resolved, &signer_parts, &previous)?
181 } else {
182 signer::SignerSet::default()
183 };
184 // After the backends, so that on a fresh directory the one that
185 // generates a CA has written its certificate before its read side
186 // looks for it. A process without the worker role finding none is
187 // refused by name: it was started before the process that makes it.
188 let previous = self
189 .infos
190 .lock()
191 .unwrap_or_else(std::sync::PoisonError::into_inner)
192 .clone();
193 let infos = signer::build_infos(resolved, &signer_parts, &previous)?;
194
195 Ok(GenerationParts {
196 egress,
197 dispatchers,
198 signers,
199 infos,
200 })
201 }
202
203 /// Makes `dispatchers` the generation every long-lived reader sees.
204 ///
205 /// Synchronous, and deliberately: it is one of the sends a reload makes
206 /// back-to-back so no task can observe a half-swapped generation.
207 pub fn publish_notifiers(&self, dispatchers: notify::DispatcherMap) {
208 self.notifiers_tx.send_replace(Arc::new(dispatchers));
209 }
210
211 /// Records `signers` as what the *next* reload compares against, and drops
212 /// whatever the generation before it held.
213 ///
214 /// That drop is the point at which a backend nobody references any more —
215 /// an unmounted profile's, or the instance a `[signer]` edit replaced — is
216 /// finally released. Deliberately after its replacement was built and has
217 /// adopted its state, never before.
218 pub fn publish_signers(
219 &self,
220 signers: signer::SignerSet,
221 infos: signer::SignerSet<dyn signer::SignerInfo>,
222 ) {
223 *self
224 .signers
225 .lock()
226 .unwrap_or_else(std::sync::PoisonError::into_inner) = signers;
227 *self
228 .infos
229 .lock()
230 .unwrap_or_else(std::sync::PoisonError::into_inner) = infos;
231 }
232}