Skip to main content

olai_stack_state/
model.rs

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