gatehouse_runtime_protocol/lifecycle_v2/
native_status.rs1use super::{native_json, observation::API};
4use serde::Deserialize;
5
6#[derive(Debug, PartialEq, Eq)]
7pub enum InspectOutcome {
8 Present(State),
9 Missing,
11}
12
13#[derive(Debug, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "PascalCase")]
15pub struct State {
16 pub status: String,
17 pub running: bool,
18 pub paused: bool,
19 pub restarting: bool,
20 pub dead: bool,
21 pub exit_code: i32,
22}
23
24#[derive(Deserialize)]
25#[serde(rename_all = "PascalCase")]
26struct Inspect {
27 id: String,
28 state: State,
29}
30#[derive(Deserialize)]
31#[serde(deny_unknown_fields)]
32struct Failure {
33 cause: String,
34 message: String,
35 response: u16,
36}
37
38pub fn inspect_route(id: &str) -> Result<String, &'static str> {
40 if id.len() != 64
41 || !id
42 .bytes()
43 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
44 {
45 return Err("full container ID required");
46 }
47 Ok(format!("/v{API}/libpod/containers/{id}/json"))
48}
49
50pub fn decode_inspect(
53 id: &str,
54 method: &str,
55 route: &str,
56 status: u16,
57 bytes: &[u8],
58) -> Result<InspectOutcome, &'static str> {
59 if method != "GET" || route != inspect_route(id)? {
60 return Err("exact native inspect request required");
61 }
62 let value = native_json::decode(bytes)?;
63 match status {
64 200 => {
65 let inspect: Inspect =
66 serde_json::from_value(value).map_err(|_| "inspect fields rejected")?;
67 if inspect.id != id {
68 return Err("inspect target mismatch");
69 }
70 let s = inspect.state;
71 if !matches!(
74 s.status.as_str(),
75 "initialized" | "created" | "running" | "stopped" | "exited" | "paused"
76 ) || s.running != (s.status == "running")
77 || s.paused != (s.status == "paused")
78 || s.restarting
79 || s.dead
80 {
81 return Err("unsupported or inconsistent native state");
82 }
83 Ok(InspectOutcome::Present(s))
84 }
85 404 => {
86 let error: Failure =
87 serde_json::from_value(value).map_err(|_| "inspect error fields rejected")?;
88 let expected =
89 format!("no container with name or ID \"{id}\" found: no such container");
90 if error.response != 404
91 || error.cause != "no such container"
92 || error.message != expected
93 {
94 return Err("inspect absence not proved");
95 }
96 Ok(InspectOutcome::Missing)
97 }
98 _ => Err("native inspect unavailable"),
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use serde_json::{json, Value};
106 fn id() -> String {
107 "a".repeat(64)
108 }
109 fn present(state: &str) -> Value {
110 json!({"Id":id(),"State":{"Status":state,"Running":state=="running","Paused":state=="paused","Restarting":false,"Dead":false,"ExitCode":0},"Metric":{"float":1.25}})
111 }
112 fn missing() -> Value {
113 json!({"cause":"no such container","message":format!("no container with name or ID \"{}\" found: no such container",id()),"response":404})
114 }
115 fn parse(status: u16, v: &Value) -> Result<InspectOutcome, &'static str> {
116 decode_inspect(
117 &id(),
118 "GET",
119 &inspect_route(&id()).unwrap(),
120 status,
121 &serde_json::to_vec(v).unwrap(),
122 )
123 }
124 #[test]
125 fn full_id_native_route_only() {
126 for bad in [
127 "fixture".into(),
128 "a".repeat(63),
129 "A".repeat(64),
130 "g".repeat(64),
131 format!("{}/", id()),
132 ] {
133 assert!(inspect_route(&bad).is_err());
134 }
135 let body = serde_json::to_vec(&missing()).unwrap();
136 let route = inspect_route(&id()).unwrap();
137 for wrong in [
138 route.replace("/libpod", ""),
139 format!("{route}?size=true"),
140 route.replace("6.1.4", "6.1.3"),
141 route.replace(&id(), &"b".repeat(64)),
142 ] {
143 assert!(decode_inspect(&id(), "GET", &wrong, 404, &body).is_err());
144 }
145 assert!(decode_inspect(&id(), "DELETE", &route, 404, &body).is_err());
146 }
147 #[test]
148 fn supported_present_states_and_exact_missing() {
149 for state in [
150 "initialized",
151 "created",
152 "running",
153 "paused",
154 "stopped",
155 "exited",
156 ] {
157 assert!(matches!(
158 parse(200, &present(state)),
159 Ok(InspectOutcome::Present(_))
160 ));
161 }
162 assert_eq!(parse(404, &missing()).unwrap(), InspectOutcome::Missing);
163 }
164 #[test]
165 fn missing_fields_wrong_target_and_inconsistent_state_refused() {
166 let mut v = present("running");
167 v["Id"] = "b".repeat(64).into();
168 assert!(parse(200, &v).is_err());
169 for field in [
170 "Status",
171 "Running",
172 "Paused",
173 "Restarting",
174 "Dead",
175 "ExitCode",
176 ] {
177 let mut v = present("running");
178 v["State"].as_object_mut().unwrap().remove(field);
179 assert!(parse(200, &v).is_err());
180 }
181 for field in ["Running", "Paused", "Restarting", "Dead"] {
182 let mut v = present("running");
183 v["State"][field] = (!v["State"][field].as_bool().unwrap()).into();
184 assert!(parse(200, &v).is_err());
185 }
186 for state in [
187 "configured",
188 "unknown",
189 "bad state",
190 "removing",
191 "stopping",
192 "",
193 ] {
194 assert!(parse(200, &present(state)).is_err());
195 }
196 }
197 #[test]
198 fn generic_docker_ambiguous_wrong_id_and_status_errors_are_not_absence() {
199 for field in ["cause", "message", "response"] {
200 let mut v = missing();
201 v.as_object_mut().unwrap().remove(field);
202 assert!(parse(404, &v).is_err());
203 let mut v = missing();
204 v[field] = Value::Null;
205 assert!(parse(404, &v).is_err());
206 }
207 for message in [
208 "not found".into(),
209 format!("No such container: {}", id()),
210 missing()["message"]
211 .as_str()
212 .unwrap()
213 .replace(&id(), &"b".repeat(64)),
214 ] {
215 let mut v = missing();
216 v["message"] = message.into();
217 assert!(parse(404, &v).is_err());
218 }
219 let mut v = missing();
220 v["cause"] = "container already exists".into();
221 assert!(parse(404, &v).is_err());
222 let mut v = missing();
223 v["response"] = 500.into();
224 assert!(parse(404, &v).is_err());
225 let mut v = missing();
226 v["extra"] = true.into();
227 assert!(parse(404, &v).is_err());
228 for status in [200, 204, 301, 400, 401, 403, 409, 500, 503] {
229 assert!(parse(status, &missing()).is_err());
230 }
231 assert!(parse(404, &present("running")).is_err());
232 }
233 #[test]
234 fn duplicate_trailing_oversize_and_deep_bodies_refused() {
235 let good = serde_json::to_string(&present("running")).unwrap();
236 let bodies = vec![
237 good.replace("\"Running\":true", "\"Running\":true,\"Running\":true")
238 .into_bytes(),
239 good.replace("1.25", "1.25,\"float\":2").into_bytes(),
240 format!("{good} {{}}").into_bytes(),
241 vec![b' '; native_json::LIMIT + 1],
242 format!("{}0{}", "[".repeat(200), "]".repeat(200)).into_bytes(),
243 Vec::new(),
244 ];
245 for bytes in bodies {
246 assert!(
247 decode_inspect(&id(), "GET", &inspect_route(&id()).unwrap(), 200, &bytes).is_err()
248 );
249 }
250 }
251}