//! The Podman runtime against a fake `podman`: what it is given, and what each way a container can //! end becomes (M3b spec, section 6). Every call goes through `runner::run`, as in `brokerd`. //! Do not edit. #[path = "support/build.rs"] mod build; #[path = "support/fake_podman.rs"] mod fake_podman; use std::time::{Duration, Instant}; use brokerd::container::{ CANNOT_START, COULD_NOT_RUN, KILLED, Podman, RUNBOOK, TIMED_OUT, UNEXPECTED, }; use brokerd::policy::{Outcome, SessionState, decide}; use brokerd::runner::run; use build::{grant, now, read, request, set}; use fake_podman::{Fake, Lines, serial}; use proto::{DataClass, Mode, ToolRequest, ToolResponse}; fn call(podman: &Podman, req: ToolRequest, grants: Vec) -> ToolResponse { let decision = match decide(req, &set(grants), SessionState::default(), now()) { Outcome::Allowed(d) => d, other => panic!("not allowed: {other:?}"), }; run(decision, podman) } fn notes() -> Vec { vec![grant("notes", "read_file", Mode::Auto).paths(&["/n"])] } fn podman(fake: &Fake, extra: &str, log: &Lines) -> Podman { Podman::new(fake.runner(extra), fake.dir.join("egress"), log.sink()) } fn failed(message: &str) -> ToolResponse { ToolResponse::Failed { message: message.to_string(), } } #[test] fn a_tool_that_succeeds_is_a_result_labelled_by_its_grant() { let _s = serial(); let fake = Fake::new("ok", r#"cat > "$D/stdin"; printf 'the file text'; exit 0"#); let log = Lines::default(); let got = call(&podman(&fake, "", &log), read("/n/a.md"), notes()); assert_eq!( got, ToolResponse::Result { content: "the file text".to_string(), class: DataClass::Private, untrusted: true, truncated: false, } ); assert_eq!( fake.stdin(), r#"{"path":"/n/a.md"}"#, "the arguments go on standard input" ); } #[test] fn podman_is_given_the_tool_argument_list_and_the_first_container_is_numbered_0() { let _s = serial(); let fake = Fake::new("args", r#"cat > /dev/null; exit 0"#); let log = Lines::default(); let p = podman(&fake, "", &log); call(&p, read("/n/a.md"), notes()); call(&p, read("/n/b.md"), notes()); let calls = fake.calls(); assert_eq!( calls.len(), 2, "one podman run per call and nothing else: {calls:?}" ); assert_eq!(calls[0][3], "--name=boxmaker-s1-1-0"); assert_eq!(calls[1][3], "--name=boxmaker-s1-1-1"); // The whole list is `podman::tool_args`, tested as golden files in podman_args.rs. assert_eq!(calls[0].first().map(String::as_str), Some("run")); assert_eq!(calls[0].last().map(String::as_str), Some("read_file")); assert!(calls[0].contains(&"--volume=/n:/n:ro".to_string())); } #[test] fn exit_1_is_the_tools_own_error_and_still_a_result() { let _s = serial(); let fake = Fake::new( "e1", "cat > /dev/null; printf 'read_file: /n/x: no such file'; exit 1", ); let log = Lines::default(); let got = call(&podman(&fake, "", &log), read("/n/x"), notes()); assert!( matches!(&got, ToolResponse::Result { content, .. } if content == "read_file: /n/x: no such file"), "{got:?}" ); } #[test] fn every_other_ending_is_a_fixed_sentence() { let cases = [ ("2", COULD_NOT_RUN), ("125", CANNOT_START), ("126", CANNOT_START), ("127", CANNOT_START), ("137", KILLED), ("3", UNEXPECTED), ("124", UNEXPECTED), ]; for (code, sentence) in cases { let _s = serial(); let body = format!( "cat > /dev/null; printf 'secret tool output'; echo 'podman said this' >&2; exit {code}" ); let fake = Fake::new("codes", &body); let log = Lines::default(); let got = call(&podman(&fake, "", &log), read("/n/a"), notes()); assert_eq!(got, failed(sentence), "exit {code}"); } } #[test] fn a_podman_failure_is_logged_with_the_runbook_pointer_and_its_stderr() { let _s = serial(); let fake = Fake::new( "125", "cat > /dev/null; echo 'Error: image not known' >&2; exit 125", ); let log = Lines::default(); call(&podman(&fake, "", &log), read("/n/a"), notes()); let text = log.all(); assert!(text.contains("Error: image not known"), "{text}"); assert!(text.contains(RUNBOOK), "{text}"); } #[test] fn a_podman_that_cannot_be_started_is_unavailable_and_logged() { let _s = serial(); let fake = Fake::new("missing", "exit 0"); let mut runner = fake.runner(""); runner.podman = fake.dir.join("no-such-podman"); let log = Lines::default(); let p = Podman::new(runner, fake.dir.join("egress"), log.sink()); assert_eq!(call(&p, read("/n/a"), notes()), failed(CANNOT_START)); assert!(log.all().contains(RUNBOOK), "{}", log.all()); } #[test] fn output_past_the_cap_is_cut_and_marked() { let _s = serial(); let fake = Fake::new( "cap", "cat > /dev/null; head -c 1000 /dev/zero | tr '\\0' x; exit 0", ); let log = Lines::default(); let got = call( &podman(&fake, "output_cap = 100", &log), read("/n/a"), notes(), ); assert!( matches!(&got, ToolResponse::Result { content, truncated: true, .. } if *content == "x".repeat(100)), "{got:?}" ); let exact = Fake::new( "cap-exact", "cat > /dev/null; head -c 100 /dev/zero | tr '\\0' x; exit 0", ); let got = call( &podman(&exact, "output_cap = 100", &log), read("/n/a"), notes(), ); assert!( matches!( &got, ToolResponse::Result { truncated: false, .. } ), "{got:?}" ); } #[test] fn output_that_is_not_utf8_is_replaced() { let _s = serial(); let fake = Fake::new("utf8", "cat > /dev/null; printf 'a\\377b'; exit 0"); let log = Lines::default(); let got = call(&podman(&fake, "", &log), read("/n/a"), notes()); assert!( matches!(&got, ToolResponse::Result { content, .. } if content == "a\u{fffd}b"), "{got:?}" ); } #[test] fn a_tool_past_its_time_limit_is_killed_removed_and_failed() { let _s = serial(); // `exec`, so killing the process kills the sleep and nothing holds the pipes open. let fake = Fake::new("slow", "cat > /dev/null; exec sleep 30"); let log = Lines::default(); let started = Instant::now(); let got = call( &podman(&fake, "read_file_ms = 300", &log), read("/n/a"), notes(), ); let took = started.elapsed(); assert_eq!(got, failed(TIMED_OUT)); assert!(took >= Duration::from_millis(300), "{took:?}"); assert!(took < Duration::from_secs(5), "{took:?}"); let calls = fake.calls(); assert_eq!(calls.len(), 3, "{calls:?}"); assert_eq!(calls[1], ["kill", "boxmaker-s1-1-0"]); assert_eq!(calls[2], ["rm", "-f", "boxmaker-s1-1-0"]); } #[test] fn a_large_argument_is_written_whole_while_the_tool_reads_it() { let _s = serial(); let fake = Fake::new("big", r#"cat > "$D/stdin"; printf done; exit 0"#); let log = Lines::default(); let content = "y".repeat(900_000); let req = request( "write_file", &format!(r#"{{"path":"/w/big.txt","content":"{content}"}}"#), ); let got = call( &podman(&fake, "", &log), req, vec![grant("w", "write_file", Mode::Auto).paths(&["/w"])], ); assert!( matches!(&got, ToolResponse::Result { content, .. } if content == "done"), "{got:?}" ); assert_eq!( fake.stdin().len(), content.len() + r#"{"path":"/w/big.txt","content":""}"#.len() ); } #[test] fn a_tool_that_never_reads_its_input_still_ends() { let _s = serial(); let fake = Fake::new("noread", "printf ignored; exit 0"); let log = Lines::default(); let req = request( "write_file", &format!( r#"{{"path":"/w/big.txt","content":"{}"}}"#, "z".repeat(900_000) ), ); let started = Instant::now(); let got = call( &podman(&fake, "write_file_ms = 5000", &log), req, vec![grant("w", "write_file", Mode::Auto).paths(&["/w"])], ); assert!( matches!(&got, ToolResponse::Result { content, .. } if content == "ignored"), "{got:?}" ); assert!(started.elapsed() < Duration::from_secs(4)); }