tag2upload_service_manager/
config.rs

1
2use crate::prelude::*;
3
4define_derive_deftly! {
5    DefaultViaSerde:
6
7    impl Default for $ttype {
8        fn default() -> $ttype {
9            serde_json::from_value(json!({})).expect("defaults all OK")
10        }
11    }
12}
13
14#[derive(Deserialize, derive_more::Debug)]
15pub struct Config {
16    pub t2u: T2u,
17
18    #[serde(default)]
19    pub intervals: Intervals,
20
21    #[serde(default)]
22    pub timeouts: Timeouts,
23
24    #[serde(default)]
25    pub limits: Limits,
26
27    pub files: Files,
28
29    pub vhosts: ui_vhost::Vhosts,
30
31    #[serde(default)]
32    pub log: Log,
33
34    #[serde(default)]
35    pub testing: Testing,
36
37    #[serde(default)]
38    pub retry: RetryPolicy,
39
40    // NB, see the special transformations additions we make in `startup`;
41    // just deserialising this and passing a `rocket::Config` to rocket
42    // produces a non-working config.
43    #[debug(skip)]
44    pub rocket: rocket::Config,
45}
46
47/// Configuration information computed from `Config`
48#[derive(Deserialize, Debug)]
49pub struct ComputedConfig {
50    pub unified_webhook_acl: Vec<dns::AllowedClient>,
51    pub bsql_timeout: bsql::Timeout,
52}
53
54#[derive(Deserialize, Debug, Deftly)]
55#[derive_deftly(DefaultViaSerde)]
56pub struct Testing {
57    /// Amount to adjust the wall clock by
58    ///
59    /// Positive values get added to the real clodk,
60    /// meaning we'll think we're running in the future.
61    ///
62    /// Usually, this would be set to a negative value
63    /// since test data is in the past.
64    //
65    // See also `test::GlobalSupplement.simulated_time_advance`,
66    // which is mutable (and is added to this).
67    #[serde(default)]
68    pub time_offset: i64,
69
70    /// Bodge the forge URL in the webhook request
71    ///
72    /// Forge URLs are expected to be
73    /// `file://<fake_https_dir>/<hostname>/...`
74    /// instead of `https://<hostname>/...`.
75    ///
76    /// API call are made by reading files under this directory
77    /// rather than by making queries to `https://`.
78    ///
79    // See also `test::GlobalSupplement.url_map`, which can do
80    // arbitrary prefix mapping on requests we make, but no
81    // inbound URL mapping.
82    pub fake_https_dir: Option<String>,
83
84    /// Only allow source packages matching these glob patterns
85    ///
86    /// `None` means allow any.
87    #[serde(default)]
88    pub allowed_source_packages: Option<Vec<AllowedSourcePackage>>,
89}
90
91#[derive(Debug, Clone, Deref, Deftly)]
92#[derive_deftly(DeserializeViaFromStr)]
93#[deftly(deser(inner, expect = "glob pattern"))]
94pub struct AllowedSourcePackage(glob::Pattern);
95
96#[derive(Deserialize, Debug)]
97pub struct T2u {
98    pub distro: String,
99    pub forges: Vec<Forge>,
100}
101
102#[derive(Deserialize, Debug, Deftly)]
103#[derive_deftly(DefaultViaSerde)]
104pub struct Intervals {
105    #[serde(default = "days::<3>")]
106    pub max_tag_age: HtDuration,
107
108    #[serde(default = "secs::<1000>")]
109    pub max_tag_age_skew: HtDuration,
110
111    #[serde(default = "days::<32>")]
112    pub expire: HtDuration,
113
114    #[serde(default = "hours::<5>")]
115    pub expire_every: HtDuration,
116
117    #[serde(default = "days::<1>")]
118    pub show_recent: HtDuration,
119}
120
121#[derive(Deserialize, Debug, Deftly)]
122pub struct Files {
123    pub db: String,
124
125    pub o2m_socket: String,
126
127    pub scratch_dir: Option<String>,
128
129    pub archive_dir: String,
130
131    /// If not set, uses compiled-in templates
132    ///
133    /// Useful during dev, for quick turnaround while editing templates.
134    /// NB, this is not automatically reloaded.
135    pub template_dir: Option<String>,
136
137    /// Write the port to this file (in decimal) when we've finished startup
138    ///
139    /// The file is only opened when we're ready.
140    /// If opening or writing fails, the t2usm immediately crashes.
141    pub port_report_file: Option<String>,
142
143    /// `.git` directory for us ourselves
144    ///
145    /// Used for `maint/build-repro describe`
146    pub self_git_dir: Option<String>,
147}
148
149#[derive(Deserialize, Debug, Deftly)]
150#[derive_deftly(DefaultViaSerde)]
151pub struct Log {
152    /// Log message level(s) to generate
153    ///
154    /// Default is:
155    ///  * If `dir` is specified, the level needed by `schedule`
156    ///  * Otherwise, `INFO`
157    #[serde(with = "serde_log_level")]
158    #[serde(default)]
159    pub level: Option<logging::LevelFilter>,
160
161    /// Extra tracing filter directives
162    ///
163    /// See
164    /// <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>
165    ///
166    /// Appended to the default, which is
167    /// `tag2upload_service_manager=LEVEL,info,"
168    /// where LEVEL is the value `level`, above.
169    #[serde(default)]
170    pub tracing: String,
171
172    /// Log directory
173    ///
174    /// If unspecified, use stdout.
175    ///
176    /// Files here will be named `t2usm.YYYY-MM-DD.log`
177    /// and kept for `days` days.
178    pub dir: Option<String>,
179
180    /// How much keep and when to rotate, for each level
181    ///
182    /// Ignored if `dir` is not set.
183    #[serde(default)]
184    pub schedule: tracing_logrotate::Config,
185}
186
187#[derive(Deserialize, Debug, Deftly)]
188#[derive_deftly(DefaultViaSerde)]
189pub struct Timeouts {
190    #[serde(default = "secs::<100>")]
191    pub http_request: HtDuration,
192
193    #[serde(default = "secs::<100>")]
194    pub git_query: HtDuration,
195
196    #[serde(default = "secs::<500>")]
197    pub git_clone: HtDuration,
198
199    #[serde(default = "secs::<10>")]
200    pub unpause_poll: HtDuration,
201
202    /// Should be > the oracle's worker restart timeout
203    #[serde(default = "secs::<100>")]
204    pub disconnected_worker_expire: HtDuration,
205
206    /// Interval to check if the `o2m_socket` is removed, and if so exit
207    ///
208    /// This is a backstop cleanup approach for the dgit test suite.
209    #[serde(default)]
210    pub socket_stat_interval: Option<HtDuration>,
211
212    /// sqlite overall database timeout
213    #[serde(default = "secs::<10>")]
214    pub db_timeout: HtDuration,
215
216    /// sqlite retries
217    ///
218    /// Number of times we can get BUSY before giving up.
219    /// This is the maximum number of deadlocks we'll tolerate.
220    ///
221    /// Each attempt will have a timeout of `db_timeout`/`db_retries`.
222    #[serde(default = "u32_::<100>")]
223    pub db_retries: u32,
224}
225
226#[derive(Deserialize, Debug, Deftly)]
227#[derive_deftly(DefaultViaSerde)]
228pub struct RetryPolicy {
229    #[serde(default = "u32_::<15>")]
230    pub min_retries: u32,
231    #[serde(default = "u32_::<10>")]
232    pub min_salient_retries: u32,
233    #[serde(default = "secs::<100>")]
234    pub timeout_initial: HtDuration,
235    #[serde(default = "f32_per_mil::<12_000>")]
236    pub timeout_increase: f32,
237    #[serde(default = "hours::<12>")]
238    pub timeout_mintotal: HtDuration,
239}
240
241#[derive(Deserialize, Debug, Deftly)]
242#[derive_deftly(DefaultViaSerde)]
243pub struct Limits {
244    #[serde(default = "usize_::<16384>")]
245    pub o2m_line: usize,
246}
247
248#[derive(Deserialize, Debug)]
249pub struct Forge {
250    pub host: Hostname,
251    pub kind: String,
252    pub allow: Vec<dns::AllowedClient>,
253    #[serde(default = "u32_::<3>")]
254    pub max_concurrent_fetch: u32,
255}
256
257const MAX_MAX_CONCURRENT_FETCH: u32 = 16;
258
259//---------- impls (eg defaults) ----------
260
261type HtD = HtDuration;
262
263// serde default requires a path, but we can trick it with const generics
264fn htd_from_secs(secs: u64) -> HtD { Duration::from_secs(secs).into() }
265pub fn secs<const SECS: u64>() -> HtD { htd_from_secs(SECS        ) }
266pub fn hours<const HRS: u64>() -> HtD { htd_from_secs( HRS * 3600 ) }
267pub fn days<const DAYS: u64>() -> HtD { htd_from_secs(DAYS * 86400) }
268// sadly Rust won't let us make this generic
269pub fn u32_  <const U: u32  >() -> u32   { U }
270pub fn usize_<const U: usize>() -> usize { U }
271pub fn f32_per_mil<const M: u64>() -> f32 { M as f32 / 1000. }
272
273impl Config {
274    pub fn check(&self) -> Result<(), StartupError> {
275        let mut errs = vec![];
276        self.t2u.check_inner(&mut errs);
277        self.intervals.check_inner(&mut errs);
278        self.testing.check_inner(&mut errs);
279        self.files.check_inner(&mut errs);
280
281        if errs.is_empty() {
282            return Ok(());
283        }
284        for e in errs {
285            eprintln!("configuration error: {e:#}");
286        }
287        Err(StartupError::InvalidConfig)
288    }
289}
290
291impl Files {
292    fn check_inner(&self, errs: &mut Vec<AE>) {
293        let archive_dir = &self.archive_dir;
294        match (|| {
295            let md = fs::metadata(archive_dir).context("stat")?;
296            if !md.is_dir() {
297                return Err(anyhow!("is not a directory"));
298            }
299            unix_access(&archive_dir, libc::W_OK | libc::X_OK)
300                .context("check writeability")?;
301            Ok(())
302        })() {
303            Err(e) => errs.push(
304                e
305                    .context(archive_dir.clone())
306                    .context("config.files.archive_dir")
307            ),
308            Ok(()) => {},
309        }
310    }
311}
312
313impl T2u {
314    fn check_inner(&self, errs: &mut Vec<AE>) {
315        if self.forges.is_empty() {
316            errs.push(anyhow!("no forges configured!"));
317        }
318        for (host, kind) in self.forges.iter()
319            .map(|f| (&f.host, &f.kind))
320            .duplicates()
321        {
322            errs.push(anyhow!("duplicate forge kind and host {kind} {host}"));
323        }
324        for forge in &self.forges {
325            forge.check_inner(errs);
326        }
327    }
328}
329
330impl Forge {
331    fn check_inner(&self, errs: &mut Vec<AE>) {
332        if self.max_concurrent_fetch > MAX_MAX_CONCURRENT_FETCH {
333            errs.push(anyhow!(
334                "forge {} max_concurrent_fetch={} > hardcoded limit {}",
335                self.host, self. max_concurrent_fetch,
336                MAX_MAX_CONCURRENT_FETCH,
337            ));
338        }
339    }
340}
341    
342impl Testing {
343    fn check_inner(&self, errs: &mut Vec<AE>) {
344        if let Some(fake) = &self.fake_https_dir {
345            if !fake.starts_with('/') {
346                errs.push(anyhow!("t2u.fake_https_dir must be absolute"));
347            }
348        }
349    }
350}
351
352impl Intervals {
353    fn check_inner(&self, errs: &mut Vec<AE>) {
354        let Intervals { max_tag_age, max_tag_age_skew, expire, .. } = *self;
355        let min_expire = HtDuration::from(
356            max_tag_age.checked_add(*max_tag_age_skew)
357                .unwrap_or_else(|| {
358                    errs.push(anyhow!(
359 "max_tag_age and/or max_tag_age_slew far too large"
360                    ));
361                    Duration::ZERO
362                })
363        );
364        if !(*expire > *min_expire) {
365            errs.push(anyhow!(
366                "expiry {expire} too short, must be > max_tag_age {max_tag_age} +  max_tag_age_skew {max_tag_age_skew}, > {min_expire}"
367            ))
368        }
369    }
370}
371
372impl TryFrom<&Config> for ComputedConfig {
373    type Error = StartupError;
374    fn try_from(config: &Config) -> Result<ComputedConfig, StartupError> {
375        let unified_webhook_acl = config.t2u.forges.iter()
376            .map(|forge| &forge.allow)
377            .flatten()
378            .cloned()
379            .collect_vec();
380        let bsql_timeout = bsql::Timeout::new(
381            *config.timeouts.db_timeout,
382            config.timeouts.db_retries,
383        );
384        Ok(ComputedConfig {
385            unified_webhook_acl,
386            bsql_timeout,
387        })
388    }
389}
390
391#[test]
392fn timeouts_defaults() {
393    let _: Timeouts = Timeouts::default();
394}
395
396mod serde_log_level {
397    use super::*;
398    use logging::*;
399
400    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
401        deser: D,
402    ) -> Result<Option<LevelFilter>, D::Error> {
403        let s: String = String::deserialize(deser)?.to_ascii_uppercase();
404        let l: LevelFilter = s.parse()
405            .map_err(|_| D::Error::invalid_value(
406                serde::de::Unexpected::Str(&s),
407                &"log level",
408            ))?;
409        Ok(Some(l))
410    }
411}