1use super::{native_json, native_status::inspect_route, observation::API};
5use serde::{Deserialize, Serialize};
6type Result<T> = std::result::Result<T, &'static str>;
7
8#[derive(Debug)]
9pub struct HealthPlan {
10 container: String,
11 argv: Vec<String>,
12}
13#[derive(Serialize)]
14#[serde(rename_all = "PascalCase")]
15struct ExecCreate<'a> {
16 user: &'a str,
17 privileged: bool,
18 tty: bool,
19 attach_stdin: bool,
20 attach_stderr: bool,
21 attach_stdout: bool,
22 detach_keys: &'a str,
23 env: [&'a str; 0],
24 working_dir: &'a str,
25 cmd: &'a [String],
26}
27impl HealthPlan {
28 pub fn new(container: &str, argv: Vec<String>) -> Result<Self> {
29 inspect_route(container)?;
30 if argv.is_empty()
31 || argv.len() > 128
32 || argv[0].is_empty()
33 || argv.iter().any(|a| a.len() > 8192 || a.contains('\0'))
34 || argv.iter().map(String::len).sum::<usize>() > 32768
35 {
36 return Err("bounded explicit health argv required");
37 }
38 Ok(Self {
39 container: container.into(),
40 argv,
41 })
42 }
43 pub fn create_route(&self) -> String {
44 format!("/v{API}/libpod/containers/{}/exec", self.container)
45 }
46 pub fn create_body(&self) -> Result<Vec<u8>> {
47 serde_json::to_vec(&ExecCreate {
48 user: "",
49 privileged: false,
50 tty: false,
51 attach_stdin: false,
52 attach_stderr: false,
53 attach_stdout: false,
54 detach_keys: "",
55 env: [],
56 working_dir: "",
57 cmd: &self.argv,
58 })
59 .map_err(|_| "exec encoding failed")
60 }
61 pub fn bind_created(&self, status: u16, body: &[u8]) -> Result<String> {
62 let v = decode_created(status, body, false)?;
63 Ok(v)
64 }
65 pub fn inspect(&self, exec: &str, status: u16, body: &[u8]) -> Result<HealthObservation> {
68 exec_route(exec, false)?;
69 if status != 200 {
70 return Err("exec inspect unavailable");
71 }
72 let i: ExecInspect = serde_json::from_value(native_json::decode(body)?)
73 .map_err(|_| "exec inspect fields rejected")?;
74 let p = i.process_config;
75 if i.id != exec
76 || i.container_id != self.container
77 || i.open_stdin
78 || i.open_stdout
79 || i.open_stderr
80 || p.privileged
81 || p.tty
82 || !p.user.is_empty()
83 || p.entrypoint != self.argv[0]
84 || p.arguments.unwrap_or_default() != self.argv[1..]
85 {
86 return Err("exec identity or process mismatch");
87 }
88 if i.running {
89 if i.can_remove {
90 return Err("inconsistent exec state");
91 }
92 return Ok(HealthObservation::Pending);
93 }
94 if !i.can_remove {
95 return Ok(HealthObservation::Pending);
96 }
97 if !(0..=255).contains(&i.exit_code) {
98 return Err("invalid exec exit code");
99 }
100 Ok(HealthObservation::Completed(i.exit_code as u8))
101 }
102}
103#[derive(Debug, PartialEq, Eq)]
104pub enum HealthObservation {
105 Pending,
106 Completed(u8),
107}
108
109#[derive(Deserialize)]
110#[serde(deny_unknown_fields)]
111struct IdResponse {
112 #[serde(rename = "Id")]
113 id: String,
114}
115#[derive(Deserialize)]
116#[serde(deny_unknown_fields)]
117struct CreateResponse {
118 #[serde(rename = "Id")]
119 id: String,
120 #[serde(rename = "Warnings")]
121 warnings: Vec<String>,
122}
123pub fn decode_created(status: u16, body: &[u8], container: bool) -> Result<String> {
126 if status != 201 {
127 return Err("mutation response not created");
128 }
129 let value = native_json::decode(body)?;
130 let id = if container {
131 let r: CreateResponse =
132 serde_json::from_value(value).map_err(|_| "container create fields rejected")?;
133 if !r.warnings.is_empty() {
134 return Err("container create warnings require review");
135 }
136 r.id
137 } else {
138 serde_json::from_value::<IdResponse>(value)
139 .map_err(|_| "exec create fields rejected")?
140 .id
141 };
142 inspect_route(&id)?;
143 Ok(id)
144}
145pub fn exec_route(exec: &str, start: bool) -> Result<String> {
146 inspect_route(exec)?;
147 Ok(format!(
148 "/v{API}/libpod/exec/{exec}/{}",
149 if start { "start" } else { "json" }
150 ))
151}
152pub const DETACHED_START_BODY: &[u8] = br#"{"Detach":true,"Tty":false,"h":0,"w":0}"#;
153pub fn check_started(status: u16, body: &[u8]) -> Result<()> {
155 if status == 200 && body.is_empty() {
156 Ok(())
157 } else {
158 Err("exec start outcome unknown")
159 }
160}
161#[derive(Deserialize)]
162#[serde(rename_all = "PascalCase")]
163struct ExecInspect {
164 #[serde(rename = "ID")]
165 id: String,
166 #[serde(rename = "ContainerID")]
167 container_id: String,
168 can_remove: bool,
169 running: bool,
170 exit_code: i32,
171 open_stdin: bool,
172 open_stdout: bool,
173 open_stderr: bool,
174 process_config: Process,
175}
176#[derive(Deserialize)]
177struct Process {
178 entrypoint: String,
179 #[serde(deserialize_with = "required_arguments")]
180 arguments: Option<Vec<String>>,
181 privileged: bool,
182 tty: bool,
183 user: String,
184}
185
186fn required_arguments<'de, D: serde::Deserializer<'de>>(
188 d: D,
189) -> std::result::Result<Option<Vec<String>>, D::Error> {
190 Option::<Vec<String>>::deserialize(d)
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use serde_json::{json, Value};
197 fn cid() -> String {
198 "a".repeat(64)
199 }
200 fn eid() -> String {
201 "b".repeat(64)
202 }
203 fn plan() -> HealthPlan {
204 HealthPlan::new(&cid(), vec!["/bin/check".into(), "arg".into()]).unwrap()
205 }
206 fn fixture() -> Value {
207 json!({"ID":eid(),"ContainerID":cid(),"CanRemove":true,"Running":false,"ExitCode":0,"OpenStdin":false,"OpenStdout":false,"OpenStderr":false,"ProcessConfig":{"entrypoint":"/bin/check","arguments":["arg"],"privileged":false,"tty":false,"user":""}})
208 }
209 fn inspect(v: &Value) -> Result<HealthObservation> {
210 plan().inspect(&eid(), 200, &serde_json::to_vec(v).unwrap())
211 }
212 #[test]
213 fn exact_request_vectors() {
214 assert_eq!(plan().create_body().unwrap(),br#"{"User":"","Privileged":false,"Tty":false,"AttachStdin":false,"AttachStderr":false,"AttachStdout":false,"DetachKeys":"","Env":[],"WorkingDir":"","Cmd":["/bin/check","arg"]}"#);
215 assert_eq!(
216 plan().create_route(),
217 format!("/v6.1.4/libpod/containers/{}/exec", cid())
218 );
219 assert_eq!(
220 exec_route(&eid(), true).unwrap(),
221 format!("/v6.1.4/libpod/exec/{}/start", eid())
222 );
223 assert_eq!(
224 exec_route(&eid(), false).unwrap(),
225 format!("/v6.1.4/libpod/exec/{}/json", eid())
226 );
227 assert_eq!(
228 DETACHED_START_BODY,
229 br#"{"Detach":true,"Tty":false,"h":0,"w":0}"#
230 );
231 }
232 #[test]
233 fn created_identity_and_warning_boundaries() {
234 for container in [false, true] {
235 let mut v = json!({"Id":eid()});
236 if container {
237 v["Warnings"] = json!([]);
238 }
239 assert_eq!(
240 decode_created(201, &serde_json::to_vec(&v).unwrap(), container).unwrap(),
241 eid()
242 );
243 for status in [200, 204, 400, 404, 409, 500] {
244 assert!(
245 decode_created(status, &serde_json::to_vec(&v).unwrap(), container).is_err()
246 );
247 }
248 for bad in ["name".into(), "b".repeat(63), "B".repeat(64)] {
249 v["Id"] = bad.into();
250 assert!(decode_created(201, &serde_json::to_vec(&v).unwrap(), container).is_err());
251 }
252 }
253 for w in [Value::Null, json!(["warning"]), json!(false)] {
254 assert!(decode_created(
255 201,
256 &serde_json::to_vec(&json!({"Id":eid(),"Warnings":w})).unwrap(),
257 true
258 )
259 .is_err());
260 }
261 assert!(decode_created(
262 201,
263 &serde_json::to_vec(&json!({"Id":eid()})).unwrap(),
264 true
265 )
266 .is_err());
267 assert!(decode_created(
268 201,
269 format!("{{\"Id\":\"{}\",\"Id\":\"{}\"}}", eid(), eid()).as_bytes(),
270 false
271 )
272 .is_err());
273 }
274 #[test]
275 fn start_ack_is_exact_not_health() {
276 assert!(check_started(200, b"").is_ok());
277 for status in [201, 204, 404, 409, 500] {
278 assert!(check_started(status, b"").is_err());
279 }
280 for body in [b"\n".as_slice(), b"{}", b"null"] {
281 assert!(check_started(200, body).is_err());
282 }
283 }
284 #[test]
285 fn unstarted_and_running_are_not_health_success() {
286 let mut v = fixture();
287 v["CanRemove"] = false.into();
288 assert_eq!(inspect(&v).unwrap(), HealthObservation::Pending);
289 v["Running"] = true.into();
290 assert_eq!(inspect(&v).unwrap(), HealthObservation::Pending);
291 v["CanRemove"] = true.into();
292 assert!(inspect(&v).is_err());
293 for code in [0, 1, 7, 255] {
294 let mut v = fixture();
295 v["ExitCode"] = code.into();
296 assert_eq!(
297 inspect(&v).unwrap(),
298 HealthObservation::Completed(code as u8)
299 );
300 }
301 }
302 #[test]
303 fn selected_fields_and_command_identity_are_required() {
304 for key in [
305 "ID",
306 "ContainerID",
307 "CanRemove",
308 "Running",
309 "ExitCode",
310 "OpenStdin",
311 "OpenStdout",
312 "OpenStderr",
313 "ProcessConfig",
314 ] {
315 let mut v = fixture();
316 v.as_object_mut().unwrap().remove(key);
317 assert!(inspect(&v).is_err());
318 }
319 for key in ["ID", "ContainerID"] {
320 let mut v = fixture();
321 v[key] = "c".repeat(64).into();
322 assert!(inspect(&v).is_err());
323 }
324 for key in ["OpenStdin", "OpenStdout", "OpenStderr"] {
325 let mut v = fixture();
326 v[key] = true.into();
327 assert!(inspect(&v).is_err());
328 }
329 for (key, value) in [
330 ("entrypoint", json!("other")),
331 ("arguments", json!([])),
332 ("privileged", json!(true)),
333 ("tty", json!(true)),
334 ("user", json!("root")),
335 ] {
336 let mut v = fixture();
337 v["ProcessConfig"][key] = value;
338 assert!(inspect(&v).is_err());
339 }
340 for code in [-1, 256] {
341 let mut v = fixture();
342 v["ExitCode"] = code.into();
343 assert!(inspect(&v).is_err());
344 }
345 }
346 #[test]
347 fn request_bounds_and_short_ids_refused() {
348 assert!(HealthPlan::new("name", vec!["true".into()]).is_err());
349 for argv in [
350 vec![],
351 vec!["".into()],
352 vec!["bad\0arg".into()],
353 vec!["x".repeat(8193)],
354 vec!["x".into(); 129],
355 ] {
356 assert!(HealthPlan::new(&cid(), argv).is_err());
357 }
358 assert!(exec_route("short", true).is_err());
359 }
360 #[test]
361 fn zero_arguments_accepts_explicit_null_but_not_missing() {
362 let p = HealthPlan::new(&cid(), vec!["/bin/check".into()]).unwrap();
363 let mut v = fixture();
364 v["ProcessConfig"]["arguments"] = Value::Null;
365 assert_eq!(
366 p.inspect(&eid(), 200, &serde_json::to_vec(&v).unwrap())
367 .unwrap(),
368 HealthObservation::Completed(0)
369 );
370 v["ProcessConfig"]
371 .as_object_mut()
372 .unwrap()
373 .remove("arguments");
374 assert!(p
375 .inspect(&eid(), 200, &serde_json::to_vec(&v).unwrap())
376 .is_err());
377 }
378 #[test]
379 fn health_duplicates_status_and_missing_process_fields_refused() {
380 for key in ["entrypoint", "arguments", "privileged", "tty", "user"] {
381 let mut v = fixture();
382 v["ProcessConfig"].as_object_mut().unwrap().remove(key);
383 assert!(inspect(&v).is_err());
384 }
385 let raw = serde_json::to_string(&fixture()).unwrap().replace(
386 "\"privileged\":false",
387 "\"privileged\":false,\"privileged\":true",
388 );
389 assert!(plan().inspect(&eid(), 200, raw.as_bytes()).is_err());
390 for status in [201, 204, 404, 409, 500] {
391 assert!(plan()
392 .inspect(&eid(), status, &serde_json::to_vec(&fixture()).unwrap())
393 .is_err());
394 }
395 }
396}