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    /// Whether there are none.
97    pub fn is_empty(&self) -> bool {
98        self.0.is_empty()
99    }
100}
101
102/// A set of environments sharing a Docker host, their vars, and whether their gateways share a
103/// mesh network.
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct Workspace {
106    /// Its id.
107    pub id: WorkspaceId,
108    /// Its name: the prefix of its instances' compose projects.
109    pub name: String,
110    /// Whether its environments' gateways join a shared network.
111    pub mesh: bool,
112    /// Vars every environment in the workspace sees.
113    pub vars: VarSet,
114}
115
116/// What an environment's next revision will be: its manifest, the module manifests it lists
117/// and the vars a commit freezes. Editable; [`commit`](crate::Store::commit_draft)ting it makes
118/// a [`Revision`].
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct Draft {
121    /// The environment manifest. Stored without placement, as `env.toml` is.
122    pub manifest: EnvManifest,
123    /// The texts of the `module.yaml` files the manifest lists (`[catalog] manifests`), by
124    /// the path it lists them under.
125    pub modules: BTreeMap<String, String>,
126    /// Vars the revision carries.
127    pub vars: VarSet,
128}
129
130impl Draft {
131    /// A draft of `manifest` with no module manifests or vars.
132    pub fn new(manifest: EnvManifest) -> Self {
133        Draft {
134            manifest,
135            modules: BTreeMap::new(),
136            vars: VarSet::new(),
137        }
138    }
139}
140
141/// An environment of a workspace: its editable draft, the vars all its revisions see, and its
142/// latest revision.
143#[derive(Clone, Debug, PartialEq, Eq)]
144pub struct Environment {
145    /// Its id.
146    pub id: EnvironmentId,
147    /// Its workspace.
148    pub workspace: WorkspaceId,
149    /// Its name (the manifest's `env_name`).
150    pub name: String,
151    /// What the next revision will be.
152    pub draft: Draft,
153    /// Vars every revision of the environment sees.
154    pub vars: VarSet,
155    /// The number of its latest revision, if it has one.
156    pub head: Option<u32>,
157}
158
159/// A numbered, immutable snapshot of an environment: the placement-free manifest, the module
160/// manifests it lists and its vars, with a digest of all three. Nothing in the API changes a
161/// revision once committed; the type has no setters, and the store refuses an update.
162#[derive(Clone, Debug, PartialEq, Eq)]
163pub struct Revision {
164    pub(crate) environment: EnvironmentId,
165    pub(crate) number: u32,
166    pub(crate) manifest: EnvManifest,
167    pub(crate) manifest_toml: String,
168    pub(crate) modules: BTreeMap<String, String>,
169    pub(crate) vars: VarSet,
170    pub(crate) digest: String,
171}
172
173impl Revision {
174    /// The environment it is a revision of.
175    pub fn environment(&self) -> &EnvironmentId {
176        &self.environment
177    }
178
179    /// Its number: 1 for the first, counting up per environment.
180    pub fn number(&self) -> u32 {
181        self.number
182    }
183
184    /// The environment manifest, without placement.
185    pub fn manifest(&self) -> &EnvManifest {
186        &self.manifest
187    }
188
189    /// The manifest as stored (`env.toml` text).
190    pub fn manifest_toml(&self) -> &str {
191        &self.manifest_toml
192    }
193
194    /// The `module.yaml` texts the manifest lists, by path.
195    pub fn modules(&self) -> &BTreeMap<String, String> {
196        &self.modules
197    }
198
199    /// The vars it carries.
200    pub fn vars(&self) -> &VarSet {
201        &self.vars
202    }
203
204    /// The SHA-256 (hex) over the manifest text, the module texts and the vars.
205    pub fn digest(&self) -> &str {
206        &self.digest
207    }
208}
209
210/// The digest of a revision's content (see [`Revision::digest`]).
211pub(crate) fn digest(
212    manifest_toml: &str,
213    modules: &BTreeMap<String, String>,
214    vars: &VarSet,
215) -> Result<String, serde_json::Error> {
216    #[derive(Serialize)]
217    struct Content<'a> {
218        manifest: &'a str,
219        modules: &'a BTreeMap<String, String>,
220        vars: &'a VarSet,
221    }
222    let bytes = serde_json::to_vec(&Content {
223        manifest: manifest_toml,
224        modules,
225        vars,
226    })?;
227    Ok(hex::encode(Sha256::digest(bytes)))
228}
229
230/// Whether an instance should be running.
231#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
232#[serde(rename_all = "snake_case")]
233pub enum Desired {
234    /// Running.
235    Running,
236    /// Stopped.
237    Stopped,
238}
239
240impl Desired {
241    pub(crate) fn as_str(self) -> &'static str {
242        match self {
243            Desired::Running => "running",
244            Desired::Stopped => "stopped",
245        }
246    }
247
248    pub(crate) fn parse(s: &str) -> Self {
249        if s == "running" {
250            Desired::Running
251        } else {
252            Desired::Stopped
253        }
254    }
255}
256
257/// A revision as something to run: which one, whether it should be running, the overrides on
258/// top of its vars and the sources whose overlays it takes. Where it runs is its
259/// [`SlotLease`] (none until it is leased one).
260#[derive(Clone, Debug, PartialEq, Eq)]
261pub struct Instance {
262    /// Its id.
263    pub id: InstanceId,
264    /// The environment.
265    pub environment: EnvironmentId,
266    /// The revision it runs.
267    pub revision: u32,
268    /// Whether it should be running.
269    pub desired: Desired,
270    /// Vars that override every other layer.
271    pub overrides: VarSet,
272    /// The overlay sources it takes, by name (e.g. `user`), in order.
273    pub sources: Vec<String>,
274    /// Its slot, when leased.
275    pub slot: Option<u16>,
276}
277
278/// A slot held by an instance, host-wide: no two instances on the host share one, whatever
279/// their workspace.
280#[derive(Clone, Debug, PartialEq, Eq)]
281pub struct SlotLease {
282    /// The slot.
283    pub slot: u16,
284    /// Who holds it.
285    pub instance: InstanceId,
286}