Skip to main content

isb_core/spec/
env.rs

1//! A service's `environment`: plain values and secret variables.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use super::{OnChange, SecretAs};
8use crate::flex;
9
10/// A service's environment: plain values, and variables whose value is a
11/// top-level secret (`KEY: {secret: NAME}`).
12#[derive(Debug, Clone, Default, PartialEq)]
13pub struct Environment {
14    /// `KEY: VALUE`: instance config (`environment.KEY`).
15    pub vars: BTreeMap<String, String>,
16    /// `KEY: {secret: NAME}`: variable to top-level secret key.
17    pub secrets: BTreeMap<String, String>,
18    /// `KEY: {secret: NAME, as: file}`: variable to top-level secret key.
19    /// The value is the file [`Environment::file_path`] and `KEY_FILE` holds
20    /// that path; neither the value nor `KEY` is instance config.
21    pub files: BTreeMap<String, String>,
22    /// `KEY: {secret: NAME, on_change: ...}`: the variable's own setting.
23    pub on_change: BTreeMap<String, OnChange>,
24}
25
26impl Environment {
27    pub fn is_empty(&self) -> bool {
28        self.vars.is_empty() && self.secrets.is_empty() && self.files.is_empty()
29    }
30
31    /// Where an `as: file` secret's value is written.
32    pub fn file_path(key: &str) -> String {
33        format!("/run/secrets/{key}")
34    }
35
36    /// The `KEY_FILE` variables of `as: file` secrets, with their paths.
37    pub fn file_vars(&self) -> impl Iterator<Item = (String, String)> + '_ {
38        self.files
39            .iter()
40            .map(|(var, key)| (format!("{var}_FILE"), Self::file_path(key)))
41    }
42}
43
44/// The plain values, so `spec.env` reads as the map it mostly is.
45impl std::ops::Deref for Environment {
46    type Target = BTreeMap<String, String>;
47    fn deref(&self) -> &Self::Target {
48        &self.vars
49    }
50}
51
52impl std::ops::DerefMut for Environment {
53    fn deref_mut(&mut self) -> &mut Self::Target {
54        &mut self.vars
55    }
56}
57
58impl<'a> IntoIterator for &'a Environment {
59    type Item = (&'a String, &'a String);
60    type IntoIter = std::collections::btree_map::Iter<'a, String, String>;
61    fn into_iter(self) -> Self::IntoIter {
62        self.vars.iter()
63    }
64}
65
66impl From<BTreeMap<String, String>> for Environment {
67    fn from(vars: BTreeMap<String, String>) -> Self {
68        Environment {
69            vars,
70            secrets: BTreeMap::new(),
71            files: BTreeMap::new(),
72            on_change: BTreeMap::new(),
73        }
74    }
75}
76
77impl Serialize for Environment {
78    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
79        use serde::ser::SerializeMap;
80        let mut m = s.serialize_map(None)?;
81        let mut keys: Vec<&String> = self
82            .vars
83            .keys()
84            .chain(self.secrets.keys())
85            .chain(self.files.keys())
86            .collect();
87        keys.sort();
88        keys.dedup();
89        for k in keys {
90            let (sec, file) = match (self.vars.get(k), self.secrets.get(k), self.files.get(k)) {
91                (Some(v), _, _) => {
92                    m.serialize_entry(k, v)?;
93                    continue;
94                }
95                (None, Some(sec), _) => (sec, false),
96                (None, None, Some(sec)) => (sec, true),
97                (None, None, None) => continue,
98            };
99            let mut v = BTreeMap::from([("secret", sec.as_str())]);
100            if let Some(o) = self.on_change.get(k) {
101                v.insert("on_change", o.as_str());
102            }
103            // Only when set, so an `as: env` variable serializes (and
104            // hashes) exactly as before the field existed.
105            if file {
106                v.insert("as", "file");
107            }
108            m.serialize_entry(k, &v)?
109        }
110        m.end()
111    }
112}
113
114impl<'de> Deserialize<'de> for Environment {
115    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
116        use serde::de::Error as _;
117        let mut env = Environment::default();
118        match flex::EnvMapOrList::deserialize(d)? {
119            flex::EnvMapOrList::Map(m) => {
120                for (k, v) in m {
121                    match v {
122                        flex::EnvValue::Scalar(v) => {
123                            env.vars.insert(k, v.into_string());
124                        }
125                        flex::EnvValue::Secret { secret, .. } if secret.is_empty() => {
126                            return Err(D::Error::custom(format!(
127                                "environment {k}: secret needs a top-level secret's name"
128                            )));
129                        }
130                        flex::EnvValue::Secret {
131                            secret,
132                            on_change,
133                            delivery,
134                        } => {
135                            if let Some(o) = on_change {
136                                env.on_change.insert(k.clone(), o);
137                            }
138                            match delivery.unwrap_or_default() {
139                                SecretAs::Env => env.secrets.insert(k, secret),
140                                SecretAs::File => env.files.insert(k, secret),
141                            };
142                        }
143                    }
144                }
145            }
146            flex::EnvMapOrList::List(l) => {
147                for item in l {
148                    let Some((k, v)) = item.split_once('=') else {
149                        return Err(D::Error::custom(format!(
150                            "environment entry {item:?} has no value: write {item}=VALUE"
151                        )));
152                    };
153                    env.vars.insert(k.to_string(), v.to_string());
154                }
155            }
156        }
157        Ok(env)
158    }
159}