Skip to main content

max / makenotwork

3.8 KB · 125 lines History Blame Raw
1 use crate::domain::{GateKind, NodeId, TierId};
2 use anyhow::{Context, Result};
3 use serde::{Deserialize, Serialize};
4 use std::path::Path;
5
6 #[derive(Debug, Clone, Serialize, Deserialize)]
7 pub struct Topology {
8 pub repo: RepoConfig,
9 pub backup: BackupConfig,
10 #[serde(rename = "tier")]
11 pub tiers: Vec<Tier>,
12 }
13
14 #[derive(Debug, Clone, Serialize, Deserialize)]
15 pub struct RepoConfig {
16 pub bare_path: String,
17 pub branch: String,
18 }
19
20 #[derive(Debug, Clone, Serialize, Deserialize)]
21 pub struct BackupConfig {
22 pub source: String,
23 pub local_path: String,
24 }
25
26 #[derive(Debug, Clone, Serialize, Deserialize)]
27 pub struct Tier {
28 pub name: TierId,
29 #[serde(default)]
30 pub provisioned: bool,
31 pub gates: Vec<Gate>,
32 #[serde(default)]
33 pub canary: CanaryPolicy,
34 #[serde(default, rename = "node")]
35 pub nodes: Vec<Node>,
36 }
37
38 #[derive(Debug, Clone, Serialize, Deserialize)]
39 pub struct Node {
40 pub name: NodeId,
41 pub ssh_target: String,
42 pub release_root: String,
43 /// systemd unit name to reload-or-restart after the symlink swap.
44 /// Defaults to "makenotwork.service" because that's MNW's prod unit.
45 #[serde(default = "default_service_name")]
46 pub service_name: String,
47 /// Capability grant for this node's executor (see `ops_exec`). Defaults to
48 /// the current behavior of every Sando node — actuate deploy+restart,
49 /// observe health — so an existing `sando.toml` keeps working unedited.
50 #[serde(default = "default_actuate")]
51 pub actuate: Vec<String>,
52 #[serde(default = "default_observe")]
53 pub observe: Vec<String>,
54 }
55
56 fn default_service_name() -> String { "makenotwork.service".into() }
57
58 /// The capability set every pre-existing Sando node implicitly had: it deploys
59 /// and restarts, and is health-observed. Keeping these as the defaults is what
60 /// lets `sando.toml` stay unchanged through the executor refactor.
61 pub fn default_actuate() -> Vec<String> { vec!["deploy".into(), "restart".into()] }
62 pub fn default_observe() -> Vec<String> { vec!["health".into()] }
63
64 #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
65 #[serde(rename_all = "snake_case")]
66 pub enum CanaryPolicy {
67 #[default]
68 Sequential,
69 Parallel,
70 }
71
72 impl CanaryPolicy {
73 pub fn as_str(self) -> &'static str {
74 match self {
75 CanaryPolicy::Sequential => "sequential",
76 CanaryPolicy::Parallel => "parallel",
77 }
78 }
79 }
80
81 #[derive(Debug, Clone, Serialize, Deserialize)]
82 #[serde(tag = "kind", rename_all = "snake_case")]
83 pub enum Gate {
84 CargoTest,
85 MigrationDryRun,
86 BootSmoke,
87 BurnIn { hours: u32 },
88 ManualConfirm,
89 }
90
91 impl Gate {
92 /// The discriminant — the identifier we use in events, schema columns,
93 /// and the TUI. Gate parameters (e.g. `BurnIn.hours`) stay with `Gate`
94 /// and are not carried into `gate_runs` history.
95 pub fn kind(&self) -> GateKind {
96 match self {
97 Gate::CargoTest => GateKind::CargoTest,
98 Gate::MigrationDryRun => GateKind::MigrationDryRun,
99 Gate::BootSmoke => GateKind::BootSmoke,
100 Gate::BurnIn { .. } => GateKind::BurnIn,
101 Gate::ManualConfirm => GateKind::ManualConfirm,
102 }
103 }
104 }
105
106 impl Topology {
107 pub fn load(path: &Path) -> Result<Self> {
108 let raw = std::fs::read_to_string(path)
109 .with_context(|| format!("reading topology at {}", path.display()))?;
110 let topo: Topology = toml::from_str(&raw)?;
111 topo.validate()?;
112 Ok(topo)
113 }
114
115 fn validate(&self) -> Result<()> {
116 anyhow::ensure!(!self.tiers.is_empty(), "topology must declare at least one tier");
117 for t in &self.tiers {
118 if t.provisioned && t.nodes.is_empty() && t.name.as_str() != "host" {
119 anyhow::bail!("tier {} is provisioned but has no nodes", t.name);
120 }
121 }
122 Ok(())
123 }
124 }
125