Skip to main content

max / makenotwork

6.4 KB · 198 lines History Blame Raw
1 //! The typed step vocabulary.
2 //!
3 //! Steps are *typed actions*, not raw command strings, so a capability check
4 //! means something: an executor granted only `deploy`+`restart` rejects a
5 //! `sign` step before it ever dispatches. The actual command to run still
6 //! travels in `argv` (and `env`/`cwd`); the `action` is the capability label.
7
8 use serde::{Deserialize, Serialize};
9 use std::path::PathBuf;
10
11 /// What a step *does*, for capability gating. The label is independent of the
12 /// concrete command in `Step::argv` — two different `sign` recipes are both
13 /// `Action::Sign` and both gated by the `sign` grant.
14 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
15 #[serde(rename_all = "snake_case")]
16 pub enum Action {
17 // Bento (app release)
18 Build,
19 Sign,
20 Notarize,
21 Staple,
22 Package,
23 // Sando (server promotion)
24 Deploy,
25 Restart,
26 Rollback,
27 /// Read-only host inspection. Never mutates; gated by the `observe` plane.
28 Observe(ObserveKind),
29 /// Escape hatch for one-off steps. Still grant-gated: a `Custom` action is
30 /// only permitted if the grant explicitly lists that same custom name.
31 Custom(String),
32 }
33
34 impl Action {
35 /// Is this an actuating (mutating) action, as opposed to observe?
36 pub fn is_actuate(&self) -> bool {
37 !matches!(self, Action::Observe(_))
38 }
39
40 /// The lower-case token used in topology config (`actuate = ["deploy", ...]`).
41 /// `None` for `Observe` (those live in the `observe` list under their kind).
42 pub fn token(&self) -> Option<String> {
43 Some(match self {
44 Action::Build => "build".into(),
45 Action::Sign => "sign".into(),
46 Action::Notarize => "notarize".into(),
47 Action::Staple => "staple".into(),
48 Action::Package => "package".into(),
49 Action::Deploy => "deploy".into(),
50 Action::Restart => "restart".into(),
51 Action::Rollback => "rollback".into(),
52 Action::Custom(s) => s.clone(),
53 Action::Observe(_) => return None,
54 })
55 }
56
57 /// Parse an actuate token from topology config. Unknown tokens become
58 /// `Custom` so a config typo is a denied capability, never a silent build
59 /// action.
60 pub fn actuate_from_token(token: &str) -> Action {
61 match token {
62 "build" => Action::Build,
63 "sign" => Action::Sign,
64 "notarize" => Action::Notarize,
65 "staple" => Action::Staple,
66 "package" => Action::Package,
67 "deploy" => Action::Deploy,
68 "restart" => Action::Restart,
69 "rollback" => Action::Rollback,
70 other => Action::Custom(other.to_string()),
71 }
72 }
73 }
74
75 /// The kinds of read-only host inspection an `observe` grant can cover.
76 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
77 #[serde(rename_all = "snake_case")]
78 pub enum ObserveKind {
79 Journal,
80 Metrics,
81 Health,
82 BuildLog,
83 Custom(String),
84 }
85
86 impl ObserveKind {
87 pub fn token(&self) -> String {
88 match self {
89 ObserveKind::Journal => "journal".into(),
90 ObserveKind::Metrics => "metrics".into(),
91 ObserveKind::Health => "health".into(),
92 ObserveKind::BuildLog => "build-log".into(),
93 ObserveKind::Custom(s) => s.clone(),
94 }
95 }
96
97 pub fn from_token(token: &str) -> ObserveKind {
98 match token {
99 "journal" => ObserveKind::Journal,
100 "metrics" => ObserveKind::Metrics,
101 "health" => ObserveKind::Health,
102 "build-log" | "build_log" => ObserveKind::BuildLog,
103 other => ObserveKind::Custom(other.to_string()),
104 }
105 }
106 }
107
108 /// One executable step: a typed action plus the command to run for it.
109 ///
110 /// `argv` is the command and its arguments (argv[0] is the program). A shell
111 /// script is just `["/bin/sh", "-c", "<script>"]` — see [`Step::shell`], which
112 /// is how Sando's multi-statement deploy scripts ride this type.
113 #[derive(Clone, Debug, Serialize, Deserialize)]
114 pub struct Step {
115 pub action: Action,
116 pub argv: Vec<String>,
117 #[serde(default)]
118 pub env: Vec<(String, String)>,
119 #[serde(default)]
120 pub cwd: Option<PathBuf>,
121 }
122
123 impl Step {
124 /// A step that runs a literal `argv` (no shell).
125 pub fn new(action: Action, argv: impl IntoIterator<Item = impl Into<String>>) -> Self {
126 Self {
127 action,
128 argv: argv.into_iter().map(Into::into).collect(),
129 env: Vec::new(),
130 cwd: None,
131 }
132 }
133
134 /// A step that runs `script` through `/bin/sh -c` — pipes, `&&`, and
135 /// `set -e` all work as written. This is the Sando deploy idiom.
136 pub fn shell(action: Action, script: impl Into<String>) -> Self {
137 Self {
138 action,
139 argv: vec!["/bin/sh".into(), "-c".into(), script.into()],
140 env: Vec::new(),
141 cwd: None,
142 }
143 }
144
145 pub fn with_env(mut self, key: impl Into<String>, val: impl Into<String>) -> Self {
146 self.env.push((key.into(), val.into()));
147 self
148 }
149
150 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
151 self.cwd = Some(cwd.into());
152 self
153 }
154
155 /// True when this is a `/bin/sh -c <script>` shell step.
156 pub(crate) fn shell_script(&self) -> Option<&str> {
157 match self.argv.as_slice() {
158 [sh, dash_c, script] if (sh == "/bin/sh" || sh == "sh") && dash_c == "-c" => {
159 Some(script.as_str())
160 }
161 _ => None,
162 }
163 }
164 }
165
166 #[cfg(test)]
167 mod tests {
168 use super::*;
169
170 #[test]
171 fn token_roundtrip() {
172 for tok in ["build", "sign", "deploy", "restart", "rollback", "package"] {
173 assert_eq!(Action::actuate_from_token(tok).token().as_deref(), Some(tok));
174 }
175 }
176
177 #[test]
178 fn unknown_actuate_token_is_custom_not_build() {
179 let a = Action::actuate_from_token("frobnicate");
180 assert_eq!(a, Action::Custom("frobnicate".into()));
181 assert!(a.is_actuate());
182 }
183
184 #[test]
185 fn observe_is_not_actuate() {
186 assert!(!Action::Observe(ObserveKind::Health).is_actuate());
187 assert_eq!(Action::Observe(ObserveKind::Health).token(), None);
188 }
189
190 #[test]
191 fn shell_step_detected() {
192 let s = Step::shell(Action::Deploy, "set -e; echo hi");
193 assert_eq!(s.shell_script(), Some("set -e; echo hi"));
194 let p = Step::new(Action::Build, ["cargo", "build"]);
195 assert_eq!(p.shell_script(), None);
196 }
197 }
198