tag2upload_service_manager/
config.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283

use crate::prelude::*;

define_derive_deftly! {
    DefaultViaSerde:

    impl Default for $ttype {
        fn default() -> $ttype {
            serde_json::from_value(json!({})).expect("defaults all OK")
        }
    }
}

#[derive(Deserialize, Debug)]
pub struct Config {
    pub t2u: T2u,

    #[serde(default)]
    pub intervals: Intervals,

    #[serde(default)]
    pub timeouts: Timeouts,

    #[serde(default)]
    pub limits: Limits,

    pub files: Files,

    #[serde(default)]
    pub log: Log,

    #[serde(default)]
    pub testing: Testing,

    // NB, see the special transformations additions we make in `startup`;
    // just deserialising this and passing a `rocket::Config` to rocket
    // produces a non-working config.
    pub rocket: rocket::Config,
}

#[derive(Deserialize, Debug, Deftly)]
#[derive_deftly(DefaultViaSerde)]
pub struct Testing {
    /// Amount to adjust the wall clock by
    ///
    /// Positive values get added to the real clodk,
    /// meaning we'll think we're running in the future.
    ///
    /// Usually, this would be set to a negative value
    /// since test data is in the past.
    //
    // See also `test::GlobalSupplement.simulated_time_advance`,
    // which is mutable (and is added to this).
    #[serde(default)]
    pub time_offset: i64,

    /// Bodge the forge URL in the webhook request
    ///
    /// Forge URLs are expected to be
    /// `file://<fake_https_dir>/<hostname>/...`
    /// instead of `https://<hostname>/...`.
    ///
    /// API call are made by reading files under this directory
    /// rather than by making queries to `https://`.
    ///
    // See also `test::GlobalSupplement.url_map`, which can do
    // arbitrary prefix mapping on requests we make, but no
    // inbound URL mapping.
    pub fake_https_dir: Option<String>,
}

#[derive(Deserialize, Debug)]
pub struct T2u {
    pub distro: String,
    pub forges: Vec<Forge>,
}

#[derive(Deserialize, Debug, Deftly)]
#[derive_deftly(DefaultViaSerde)]
pub struct Intervals {
    #[serde(default = "days::<3>")]
    pub max_tag_age: HtDuration,

    #[serde(default = "secs::<1000>")]
    pub max_tag_age_skew: HtDuration,

    #[serde(default = "days::<15>")]
    pub expire: HtDuration,

    #[serde(default = "hours::<5>")]
    pub expire_every: HtDuration,

    #[serde(default = "days::<1>")]
    pub show_recent: HtDuration,
}

#[derive(Deserialize, Debug, Deftly)]
pub struct Files {
    pub db: String,

    pub o2m_socket: String,

    pub scratch_dir: Option<String>,

    pub archive_dir: String,

    /// If not set, uses compiled-in templates
    ///
    /// Useful during dev, for quick turnaround while editing templates.
    /// NB, this is not automatically reloaded.
    pub template_dir: Option<String>,

    /// Write the port to this file (in decimal) when we've finished startup
    ///
    /// The file is only opened when we're ready.
    /// If opening or writing fails, the t2usm immediately crashes.
    pub port_report_file: Option<String>,
}

#[derive(Deserialize, Debug, Deftly)]
#[derive_deftly(DefaultViaSerde)]
pub struct Log {
    #[serde(default = "logging::default_level_filter")]
    #[serde(with = "serde_log_level")]
    pub level: logging::LevelFilter,

    /// Extra tracing filter directives
    ///
    /// See
    /// <https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives>
    ///
    /// Appended to the default, which is
    /// `tag2upload_service_manager=LEVEL,info,"
    /// where LEVEL is the value `level`, above.
    #[serde(default)]
    pub tracing: String,
}

#[derive(Deserialize, Debug, Deftly)]
#[derive_deftly(DefaultViaSerde)]
pub struct Timeouts {
    #[serde(default = "secs::<100>")]
    pub http_request: HtDuration,

    #[serde(default = "secs::<500>")]
    pub git_clone: HtDuration,

    /// Interval to check if the `o2m_socket` is removed, and if so exit
    ///
    /// This is a backstop cleanup approach for the dgit test suite.
    #[serde(default)]
    pub socket_stat_interval: Option<HtDuration>,
}

#[derive(Deserialize, Debug, Deftly)]
#[derive_deftly(DefaultViaSerde)]
pub struct Limits {
    #[serde(default = "usize_::<16384>")]
    pub o2m_line: usize,
}

#[derive(Deserialize, Debug)]
pub struct Forge {
    pub host: Hostname,
    pub kind: String,
    pub allow: Vec<dns::AllowedCaller>,
}

//---------- impls (eg defaults) ----------

type HtD = HtDuration;

// serde default requires a path, but we can trick it with const generics
fn secs<const SECS: u64>() -> HtD { Duration::from_secs(SECS).into() }
fn days<const DAYS: u64>() -> HtD { Duration::from_secs(DAYS * 86400).into() }
fn hours<const HRS: u64>() -> HtD { Duration::from_secs(HRS * 3600).into() }
fn usize_<const U: usize>() -> usize { U }

impl Config {
    pub fn check(&self) -> Result<(), StartupError> {
        let mut errs = vec![];
        self.t2u.check_inner(&mut errs);
        self.intervals.check_inner(&mut errs);
        self.testing.check_inner(&mut errs);
        self.files.check_inner(&mut errs);

        if errs.is_empty() {
            return Ok(());
        }
        for e in errs {
            eprintln!("configuration error: {e:#}");
        }
        Err(StartupError::InvalidConfig)
    }
}

impl Files {
    fn check_inner(&self, errs: &mut Vec<AE>) {
        let archive_dir = &self.archive_dir;
        match (|| {
            let md = fs::metadata(archive_dir).context("stat")?;
            if !md.is_dir() {
                return Err(anyhow!("is not a directory"));
            }
            unix_access(&archive_dir, libc::W_OK | libc::X_OK)
                .context("check writeability")?;
            Ok(())
        })() {
            Err(e) => errs.push(
                e
                    .context(archive_dir.clone())
                    .context("config.files.archive_dir")
            ),
            Ok(()) => {},
        }
    }
}

impl T2u {
    fn check_inner(&self, errs: &mut Vec<AE>) {
        if self.forges.is_empty() {
            errs.push(anyhow!("no forges configured!"));
        }
        for (host, kind) in self.forges.iter()
            .map(|f| (&f.host, &f.kind))
            .duplicates()
        {
            errs.push(anyhow!("duplicate forge kind and host {kind} {host}"));
        }
    }
}
    
impl Testing {
    fn check_inner(&self, errs: &mut Vec<AE>) {
        if let Some(fake) = &self.fake_https_dir {
            if !fake.starts_with('/') {
                errs.push(anyhow!("t2u.fake_https_dir must be absolute"));
            }
        }
    }
}

impl Intervals {
    fn check_inner(&self, errs: &mut Vec<AE>) {
        let Intervals { max_tag_age, max_tag_age_skew, expire, .. } = *self;
        let min_expire = HtDuration::from(
            max_tag_age.checked_add(*max_tag_age_skew)
                .unwrap_or_else(|| {
                    errs.push(anyhow!(
 "max_tag_age and/or max_tag_age_slew far too large"
                    ));
                    Duration::ZERO
                })
        );
        if !(*expire > *min_expire) {
            errs.push(anyhow!(
                "expiry {expire} too short, must be > max_tag_age {max_tag_age} +  max_tag_age_skew {max_tag_age_skew}, > {min_expire}"
            ))
        }
    }
}

#[test]
fn timeouts_defaults() {
    let _: Timeouts = Timeouts::default();
}

mod serde_log_level {
    use super::*;
    use logging::*;

    pub(super) fn deserialize<'de, D: Deserializer<'de>>(
        deser: D,
    ) -> Result<LevelFilter, D::Error> {
        let s: String = String::deserialize(deser)?.to_ascii_uppercase();
        let l: LevelFilter = s.parse()
            .map_err(|_| D::Error::invalid_value(
                serde::de::Unexpected::Str(&s),
                &"log level",
            ))?;
        Ok(l)
    }
}