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