Skip to main content

olai_stack_state/
model.rs

1//! The records the store holds.
2
3use std::collections::BTreeMap;
4use std::fmt;
5
6use olai_stack_topology::EnvManifest;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10macro_rules! id {
11    ($(#[$doc:meta])* $name:ident) => {
12        $(#[$doc])*
13        #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14        #[serde(transparent)]
15        pub struct $name(pub String);
16
17        impl $name {
18            pub(crate) fn generate() -> Self {
19                $name(uuid::Uuid::new_v4().to_string())
20            }
21        }
22
23        impl fmt::Display for $name {
24            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25                f.write_str(&self.0)
26            }
27        }
28    };
29}
30
31id!(
32    /// A [`Workspace`]'s id.
33    WorkspaceId
34);
35id!(
36    /// An [`Environment`]'s id.
37    EnvironmentId
38);
39id!(
40    /// An [`Instance`]'s id.
41    InstanceId
42);
43
44/// Where a secret's value lives. The store keeps only this reference; the value stays with
45/// the provider (the OS keychain, the process environment).
46#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
47pub struct SecretRef {
48    /// The provider that resolves it (e.g. `env`, `keychain`).
49    pub provider: String,
50    /// The key the provider knows the value by.
51    pub key: String,
52}
53
54/// One variable's value: a literal, or a reference to a secret.
55#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum VarValue {
58    /// The value itself.
59    Literal(String),
60    /// Where the value lives.
61    Secret(SecretRef),
62}
63
64impl fmt::Debug for VarValue {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            // A literal may still be something the user would rather not see in a log.
68            VarValue::Literal(_) => f.write_str("Literal(<redacted>)"),
69            VarValue::Secret(r) => f.debug_tuple("Secret").field(r).finish(),
70        }
71    }
72}
73
74/// Named variables, as a workspace, an environment, a revision or an instance sets them.
75#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(transparent)]
77pub struct VarSet(pub BTreeMap<String, VarValue>);
78
79impl VarSet {
80    /// No variables.
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    /// Set `name` to `value`.
86    pub fn set(&mut self, name: impl Into<String>, value: VarValue) -> &mut Self {
87        self.0.insert(name.into(), value);
88        self
89    }
90
91    /// The value of `name`.
92    pub fn get(&self, name: &str) -> Option<&VarValue> {
93        self.0.get(name)
94    }
95
96    /// Remove `name`, returning its value if it was set.
97    pub fn remove(&mut self, name: &str) -> Option<VarValue> {
98        self.0.remove(name)
99    }
100
101    /// Whether there are none.
102    pub fn is_empty(&self) -> bool {
103        self.0.is_empty()
104    }
105}
106
107/// Which mutable [`VarSet`] a change applies to.
108#[derive(Clone, Debug, PartialEq, Eq)]
109pub enum VarScope {
110    /// The vars every environment of the workspace sees.
111    Workspace(WorkspaceId),
112    /// The vars every revision of the environment sees.
113    Environment(EnvironmentId),
114    /// The vars the environment's next revision carries.
115    Draft(EnvironmentId),
116}
117
118/// An overlay source an instance takes: its name, and what the source needs to know about the
119/// instance (which branch, say). The config is merged over the workspace's config for the same
120/// source. Neither holds secrets.
121#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
122pub struct SourceBinding {
123    /// The source's name (e.g. `user`).
124    pub name: String,
125    /// What the source is told about this instance; `null` for nothing.
126    #[serde(default)]
127    pub config: serde_json::Value,
128}
129
130impl SourceBinding {
131    /// A binding to `name` with no config.
132    pub fn new(name: impl Into<String>) -> Self {
133        SourceBinding {
134            name: name.into(),
135            config: serde_json::Value::Null,
136        }
137    }
138
139    /// The same binding with `config`.
140    pub fn with_config(mut self, config: serde_json::Value) -> Self {
141        self.config = config;
142        self
143    }
144}
145
146/// A set of environments sharing a Docker host, their vars, and whether their gateways share a
147/// mesh network.
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct Workspace {
150    /// Its id.
151    pub id: WorkspaceId,
152    /// Its name: the prefix of its instances' compose projects.
153    pub name: String,
154    /// Whether its environments' gateways join a shared network.
155    pub mesh: bool,
156    /// Vars every environment in the workspace sees.
157    pub vars: VarSet,
158    /// Per overlay source, the config every instance of the workspace binds it with (e.g. which
159    /// project), below the instance's own [`SourceBinding::config`].
160    pub sources: BTreeMap<String, serde_json::Value>,
161}
162
163/// What an environment's next revision will be: its manifest, the module manifests it lists
164/// and the vars a commit freezes. Editable; [`commit`](crate::Store::commit_draft)ting it makes
165/// a [`Revision`].
166#[derive(Clone, Debug, PartialEq, Eq)]
167pub struct Draft {
168    /// The environment manifest. Stored without placement, as `env.toml` is.
169    pub manifest: EnvManifest,
170    /// The texts of the `module.yaml` files the manifest lists (`[catalog] manifests`), by
171    /// the path it lists them under.
172    pub modules: BTreeMap<String, String>,
173    /// Vars the revision carries.
174    pub vars: VarSet,
175}
176
177impl Draft {
178    /// A draft of `manifest` with no module manifests or vars.
179    pub fn new(manifest: EnvManifest) -> Self {
180        Draft {
181            manifest,
182            modules: BTreeMap::new(),
183            vars: VarSet::new(),
184        }
185    }
186}
187
188/// An environment of a workspace: its editable draft, the vars all its revisions see, and its
189/// latest revision.
190#[derive(Clone, Debug, PartialEq, Eq)]
191pub struct Environment {
192    /// Its id.
193    pub id: EnvironmentId,
194    /// Its workspace.
195    pub workspace: WorkspaceId,
196    /// Its name (the manifest's `env_name`).
197    pub name: String,
198    /// What the next revision will be.
199    pub draft: Draft,
200    /// Vars every revision of the environment sees.
201    pub vars: VarSet,
202    /// The number of its latest revision, if it has one.
203    pub head: Option<u32>,
204}
205
206/// A numbered, immutable snapshot of an environment: the placement-free manifest, the module
207/// manifests it lists and its vars, with a digest of all three. Nothing in the API changes a
208/// revision once committed; the type has no setters, and the store refuses an update.
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct Revision {
211    pub(crate) environment: EnvironmentId,
212    pub(crate) number: u32,
213    pub(crate) manifest: EnvManifest,
214    pub(crate) manifest_toml: String,
215    pub(crate) modules: BTreeMap<String, String>,
216    pub(crate) vars: VarSet,
217    pub(crate) digest: String,
218}
219
220impl Revision {
221    /// The environment it is a revision of.
222    pub fn environment(&self) -> &EnvironmentId {
223        &self.environment
224    }
225
226    /// Its number: 1 for the first, counting up per environment.
227    pub fn number(&self) -> u32 {
228        self.number
229    }
230
231    /// The environment manifest, without placement.
232    pub fn manifest(&self) -> &EnvManifest {
233        &self.manifest
234    }
235
236    /// The manifest as stored (`env.toml` text).
237    pub fn manifest_toml(&self) -> &str {
238        &self.manifest_toml
239    }
240
241    /// The `module.yaml` texts the manifest lists, by path.
242    pub fn modules(&self) -> &BTreeMap<String, String> {
243        &self.modules
244    }
245
246    /// The vars it carries.
247    pub fn vars(&self) -> &VarSet {
248        &self.vars
249    }
250
251    /// The SHA-256 (hex) over the manifest text, the module texts and the vars.
252    pub fn digest(&self) -> &str {
253        &self.digest
254    }
255}
256
257/// The digest of a revision's content (see [`Revision::digest`]).
258pub(crate) fn digest(
259    manifest_toml: &str,
260    modules: &BTreeMap<String, String>,
261    vars: &VarSet,
262) -> Result<String, serde_json::Error> {
263    #[derive(Serialize)]
264    struct Content<'a> {
265        manifest: &'a str,
266        modules: &'a BTreeMap<String, String>,
267        vars: &'a VarSet,
268    }
269    let bytes = serde_json::to_vec(&Content {
270        manifest: manifest_toml,
271        modules,
272        vars,
273    })?;
274    Ok(hex::encode(Sha256::digest(bytes)))
275}
276
277/// Whether an instance should be running.
278#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
279#[serde(rename_all = "snake_case")]
280pub enum Desired {
281    /// Running.
282    Running,
283    /// Stopped.
284    Stopped,
285}
286
287impl Desired {
288    pub(crate) fn as_str(self) -> &'static str {
289        match self {
290            Desired::Running => "running",
291            Desired::Stopped => "stopped",
292        }
293    }
294
295    pub(crate) fn parse(s: &str) -> Self {
296        if s == "running" {
297            Desired::Running
298        } else {
299            Desired::Stopped
300        }
301    }
302}
303
304/// A revision as something to run: which one, whether it should be running, the overrides on
305/// top of its vars and the sources whose overlays it takes. Where it runs is its
306/// [`SlotLease`] (none until it is leased one).
307#[derive(Clone, Debug, PartialEq, Eq)]
308pub struct Instance {
309    /// Its id.
310    pub id: InstanceId,
311    /// The environment.
312    pub environment: EnvironmentId,
313    /// The revision it runs.
314    pub revision: u32,
315    /// Whether it should be running.
316    pub desired: Desired,
317    /// Vars that override every other layer.
318    pub overrides: VarSet,
319    /// The overlay sources it takes, in order.
320    pub sources: Vec<SourceBinding>,
321    /// Its slot, when leased.
322    pub slot: Option<u16>,
323}
324
325/// A slot held by an instance, host-wide: no two instances on the host share one, whatever
326/// their workspace.
327#[derive(Clone, Debug, PartialEq, Eq)]
328pub struct SlotLease {
329    /// The slot.
330    pub slot: u16,
331    /// Who holds it.
332    pub instance: InstanceId,
333}