use super::{native_json, native_status::inspect_route, observation::API};
use serde::{Deserialize, Serialize};
type Result<T> = std::result::Result<T, &'static str>;
#[derive(Debug)]
pub struct HealthPlan {
container: String,
argv: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
struct ExecCreate<'a> {
user: &'a str,
privileged: bool,
tty: bool,
attach_stdin: bool,
attach_stderr: bool,
attach_stdout: bool,
detach_keys: &'a str,
env: [&'a str; 0],
working_dir: &'a str,
cmd: &'a [String],
}
impl HealthPlan {
pub fn new(container: &str, argv: Vec<String>) -> Result<Self> {
inspect_route(container)?;
if argv.is_empty()
|| argv.len() > 128
|| argv[0].is_empty()
|| argv.iter().any(|a| a.len() > 8192 || a.contains('\0'))
|| argv.iter().map(String::len).sum::<usize>() > 32768
{
return Err("bounded explicit health argv required");
}
Ok(Self {
container: container.into(),
argv,
})
}
pub fn create_route(&self) -> String {
format!("/v{API}/libpod/containers/{}/exec", self.container)
}
pub fn create_body(&self) -> Result<Vec<u8>> {
serde_json::to_vec(&ExecCreate {
user: "",
privileged: false,
tty: false,
attach_stdin: false,
attach_stderr: false,
attach_stdout: false,
detach_keys: "",
env: [],
working_dir: "",
cmd: &self.argv,
})
.map_err(|_| "exec encoding failed")
}
pub fn bind_created(&self, status: u16, body: &[u8]) -> Result<String> {
let v = decode_created(status, body, false)?;
Ok(v)
}
pub fn inspect(&self, exec: &str, status: u16, body: &[u8]) -> Result<HealthObservation> {
exec_route(exec, false)?;
if status != 200 {
return Err("exec inspect unavailable");
}
let i: ExecInspect = serde_json::from_value(native_json::decode(body)?)
.map_err(|_| "exec inspect fields rejected")?;
let p = i.process_config;
if i.id != exec
|| i.container_id != self.container
|| i.open_stdin
|| i.open_stdout
|| i.open_stderr
|| p.privileged
|| p.tty
|| !p.user.is_empty()
|| p.entrypoint != self.argv[0]
|| p.arguments.unwrap_or_default() != self.argv[1..]
{
return Err("exec identity or process mismatch");
}
if i.running {
if i.can_remove {
return Err("inconsistent exec state");
}
return Ok(HealthObservation::Pending);
}
if !i.can_remove {
return Ok(HealthObservation::Pending);
}
if !(0..=255).contains(&i.exit_code) {
return Err("invalid exec exit code");
}
Ok(HealthObservation::Completed(i.exit_code as u8))
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum HealthObservation {
Pending,
Completed(u8),
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct IdResponse {
#[serde(rename = "Id")]
id: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct CreateResponse {
#[serde(rename = "Id")]
id: String,
#[serde(rename = "Warnings")]
warnings: Vec<String>,
}
pub fn decode_created(status: u16, body: &[u8], container: bool) -> Result<String> {
if status != 201 {
return Err("mutation response not created");
}
let value = native_json::decode(body)?;
let id = if container {
let r: CreateResponse =
serde_json::from_value(value).map_err(|_| "container create fields rejected")?;
if !r.warnings.is_empty() {
return Err("container create warnings require review");
}
r.id
} else {
serde_json::from_value::<IdResponse>(value)
.map_err(|_| "exec create fields rejected")?
.id
};
inspect_route(&id)?;
Ok(id)
}
pub fn exec_route(exec: &str, start: bool) -> Result<String> {
inspect_route(exec)?;
Ok(format!(
"/v{API}/libpod/exec/{exec}/{}",
if start { "start" } else { "json" }
))
}
pub const DETACHED_START_BODY: &[u8] = br#"{"Detach":true,"Tty":false,"h":0,"w":0}"#;
pub fn check_started(status: u16, body: &[u8]) -> Result<()> {
if status == 200 && body.is_empty() {
Ok(())
} else {
Err("exec start outcome unknown")
}
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct ExecInspect {
#[serde(rename = "ID")]
id: String,
#[serde(rename = "ContainerID")]
container_id: String,
can_remove: bool,
running: bool,
exit_code: i32,
open_stdin: bool,
open_stdout: bool,
open_stderr: bool,
process_config: Process,
}
#[derive(Deserialize)]
struct Process {
entrypoint: String,
#[serde(deserialize_with = "required_arguments")]
arguments: Option<Vec<String>>,
privileged: bool,
tty: bool,
user: String,
}
fn required_arguments<'de, D: serde::Deserializer<'de>>(
d: D,
) -> std::result::Result<Option<Vec<String>>, D::Error> {
Option::<Vec<String>>::deserialize(d)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{json, Value};
fn cid() -> String {
"a".repeat(64)
}
fn eid() -> String {
"b".repeat(64)
}
fn plan() -> HealthPlan {
HealthPlan::new(&cid(), vec!["/bin/check".into(), "arg".into()]).unwrap()
}
fn fixture() -> Value {
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":""}})
}
fn inspect(v: &Value) -> Result<HealthObservation> {
plan().inspect(&eid(), 200, &serde_json::to_vec(v).unwrap())
}
#[test]
fn exact_request_vectors() {
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"]}"#);
assert_eq!(
plan().create_route(),
format!("/v6.1.4/libpod/containers/{}/exec", cid())
);
assert_eq!(
exec_route(&eid(), true).unwrap(),
format!("/v6.1.4/libpod/exec/{}/start", eid())
);
assert_eq!(
exec_route(&eid(), false).unwrap(),
format!("/v6.1.4/libpod/exec/{}/json", eid())
);
assert_eq!(
DETACHED_START_BODY,
br#"{"Detach":true,"Tty":false,"h":0,"w":0}"#
);
}
#[test]
fn created_identity_and_warning_boundaries() {
for container in [false, true] {
let mut v = json!({"Id":eid()});
if container {
v["Warnings"] = json!([]);
}
assert_eq!(
decode_created(201, &serde_json::to_vec(&v).unwrap(), container).unwrap(),
eid()
);
for status in [200, 204, 400, 404, 409, 500] {
assert!(
decode_created(status, &serde_json::to_vec(&v).unwrap(), container).is_err()
);
}
for bad in ["name".into(), "b".repeat(63), "B".repeat(64)] {
v["Id"] = bad.into();
assert!(decode_created(201, &serde_json::to_vec(&v).unwrap(), container).is_err());
}
}
for w in [Value::Null, json!(["warning"]), json!(false)] {
assert!(decode_created(
201,
&serde_json::to_vec(&json!({"Id":eid(),"Warnings":w})).unwrap(),
true
)
.is_err());
}
assert!(decode_created(
201,
&serde_json::to_vec(&json!({"Id":eid()})).unwrap(),
true
)
.is_err());
assert!(decode_created(
201,
format!("{{\"Id\":\"{}\",\"Id\":\"{}\"}}", eid(), eid()).as_bytes(),
false
)
.is_err());
}
#[test]
fn start_ack_is_exact_not_health() {
assert!(check_started(200, b"").is_ok());
for status in [201, 204, 404, 409, 500] {
assert!(check_started(status, b"").is_err());
}
for body in [b"\n".as_slice(), b"{}", b"null"] {
assert!(check_started(200, body).is_err());
}
}
#[test]
fn unstarted_and_running_are_not_health_success() {
let mut v = fixture();
v["CanRemove"] = false.into();
assert_eq!(inspect(&v).unwrap(), HealthObservation::Pending);
v["Running"] = true.into();
assert_eq!(inspect(&v).unwrap(), HealthObservation::Pending);
v["CanRemove"] = true.into();
assert!(inspect(&v).is_err());
for code in [0, 1, 7, 255] {
let mut v = fixture();
v["ExitCode"] = code.into();
assert_eq!(
inspect(&v).unwrap(),
HealthObservation::Completed(code as u8)
);
}
}
#[test]
fn selected_fields_and_command_identity_are_required() {
for key in [
"ID",
"ContainerID",
"CanRemove",
"Running",
"ExitCode",
"OpenStdin",
"OpenStdout",
"OpenStderr",
"ProcessConfig",
] {
let mut v = fixture();
v.as_object_mut().unwrap().remove(key);
assert!(inspect(&v).is_err());
}
for key in ["ID", "ContainerID"] {
let mut v = fixture();
v[key] = "c".repeat(64).into();
assert!(inspect(&v).is_err());
}
for key in ["OpenStdin", "OpenStdout", "OpenStderr"] {
let mut v = fixture();
v[key] = true.into();
assert!(inspect(&v).is_err());
}
for (key, value) in [
("entrypoint", json!("other")),
("arguments", json!([])),
("privileged", json!(true)),
("tty", json!(true)),
("user", json!("root")),
] {
let mut v = fixture();
v["ProcessConfig"][key] = value;
assert!(inspect(&v).is_err());
}
for code in [-1, 256] {
let mut v = fixture();
v["ExitCode"] = code.into();
assert!(inspect(&v).is_err());
}
}
#[test]
fn request_bounds_and_short_ids_refused() {
assert!(HealthPlan::new("name", vec!["true".into()]).is_err());
for argv in [
vec![],
vec!["".into()],
vec!["bad\0arg".into()],
vec!["x".repeat(8193)],
vec!["x".into(); 129],
] {
assert!(HealthPlan::new(&cid(), argv).is_err());
}
assert!(exec_route("short", true).is_err());
}
#[test]
fn zero_arguments_accepts_explicit_null_but_not_missing() {
let p = HealthPlan::new(&cid(), vec!["/bin/check".into()]).unwrap();
let mut v = fixture();
v["ProcessConfig"]["arguments"] = Value::Null;
assert_eq!(
p.inspect(&eid(), 200, &serde_json::to_vec(&v).unwrap())
.unwrap(),
HealthObservation::Completed(0)
);
v["ProcessConfig"]
.as_object_mut()
.unwrap()
.remove("arguments");
assert!(p
.inspect(&eid(), 200, &serde_json::to_vec(&v).unwrap())
.is_err());
}
#[test]
fn health_duplicates_status_and_missing_process_fields_refused() {
for key in ["entrypoint", "arguments", "privileged", "tty", "user"] {
let mut v = fixture();
v["ProcessConfig"].as_object_mut().unwrap().remove(key);
assert!(inspect(&v).is_err());
}
let raw = serde_json::to_string(&fixture()).unwrap().replace(
"\"privileged\":false",
"\"privileged\":false,\"privileged\":true",
);
assert!(plan().inspect(&eid(), 200, raw.as_bytes()).is_err());
for status in [201, 204, 404, 409, 500] {
assert!(plan()
.inspect(&eid(), status, &serde_json::to_vec(&fixture()).unwrap())
.is_err());
}
}
}