use microvms_core::ErrorKind;
use microvms_core::control::artifact::{
BaseImage, WrapOptions, default_dockerfile, dockerfile_agentd_port, dockerfile_cmd,
dockerfile_entrypoint, dockerfile_from_ref, require_daemon_cmd, require_matching_agentd_port,
require_matching_from, wrap_dockerfile,
};
#[derive(Debug, bolero::TypeGenerator)]
enum Piece {
From(u8),
Run,
User(u8),
Workdir(bool),
Env(u8, u16),
Entrypoint(bool),
Cmd(bool),
Comment,
Blank,
Continued,
HeredocOpen,
HeredocClose,
Raw(Vec<u8>),
}
#[derive(Debug, bolero::TypeGenerator)]
struct Case {
backtick_escape: bool,
pieces: Vec<Piece>,
trailing_newline: bool,
crlf: bool,
workdir: u8,
inherit_workdir: bool,
port: u16,
}
const MAX_PIECES: usize = 16;
const FROMS: [&str; 6] = [
"FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal",
"FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal@sha256:c439fb4994ea7ca529233d6256446d3f8b7b4efb58956073e015303a170011de",
"FROM python:3.12-slim",
"from --platform=linux/arm64 golang:1.23 AS build",
" FROM ${BASE}",
"FROM",
];
const USERS: [&str; 6] = ["root", "0", "app", "root:root", "1000:1000", "${USER}"];
struct Rendered {
text: String,
known_from: Option<bool>,
known_unfinished: Option<bool>,
has_user_line: bool,
}
fn render(case: &Case) -> Rendered {
let escape = if case.backtick_escape { '`' } else { '\\' };
let newline = if case.crlf { "\r\n" } else { "\n" };
let mut lines: Vec<String> = Vec::new();
if case.backtick_escape {
lines.push("# escape=`".to_string());
}
let mut raw = false;
let mut first_from: Option<bool> = None;
let mut ambiguous = false;
let mut continued = false;
let mut heredoc_open = false;
let mut has_user_line = false;
for piece in case.pieces.iter().take(MAX_PIECES) {
let line = match piece {
Piece::From(pick) => {
let from = FROMS[usize::from(*pick) % FROMS.len()];
first_from.get_or_insert(from != "FROM");
from.to_string()
}
Piece::Run => "RUN echo hello".to_string(),
Piece::User(pick) => {
has_user_line = true;
format!("USER {}", USERS[usize::from(*pick) % USERS.len()])
}
Piece::Workdir(with) => {
if *with {
"WORKDIR /app".to_string()
} else {
"WORKDIR".to_string()
}
}
Piece::Env(which, value) => match which % 3 {
0 => format!("ENV AGENTD_PORT={value}"),
1 => format!("ENV AGENTD_SSE_KEEPALIVE_SECS={}", value % 120),
_ => "ENV TASK=1".to_string(),
},
Piece::Entrypoint(empty) => {
if *empty {
"ENTRYPOINT []".to_string()
} else {
r#"ENTRYPOINT ["/bin/sh", "-c"]"#.to_string()
}
}
Piece::Cmd(daemon) => {
if *daemon {
r#"CMD ["/agentd"]"#.to_string()
} else {
r#"CMD ["python", "serve.py"]"#.to_string()
}
}
Piece::Comment => "# a comment".to_string(),
Piece::Blank => String::new(),
Piece::Continued => format!("RUN echo one {escape}"),
Piece::HeredocOpen => "RUN <<EOF".to_string(),
Piece::HeredocClose => "EOF".to_string(),
Piece::Raw(bytes) => {
raw = true;
String::from_utf8_lossy(bytes).into_owned()
}
};
match piece {
Piece::Continued if !heredoc_open => continued = true,
Piece::HeredocOpen if continued => ambiguous = true,
Piece::HeredocOpen if !heredoc_open => heredoc_open = true,
Piece::HeredocClose if heredoc_open => heredoc_open = false,
Piece::Blank | Piece::Comment => {}
_ if heredoc_open => {}
_ => continued = false,
}
lines.push(line);
}
let mut text = lines.join(newline);
if case.trailing_newline && !text.is_empty() {
text.push_str(newline);
}
Rendered {
text,
known_from: (!raw).then_some(first_from.unwrap_or(false)),
known_unfinished: (!raw && !ambiguous).then_some(continued || heredoc_open),
has_user_line: has_user_line || raw,
}
}
fn options(case: &Case) -> WrapOptions {
WrapOptions {
port: case.port.max(1),
workdir: match case.workdir % 5 {
0 => None,
1 => Some("/srv/task".to_string()),
2 => Some("relative/dir".to_string()),
3 => Some("/srv\nRUN evil".to_string()),
_ => Some(String::new()),
},
inherit_workdir: case.inherit_workdir,
}
}
fn default_stanza(port: u16, workdir: Option<&str>) -> String {
let full = default_dockerfile(port, workdir, &BaseImage::al2023(), None);
let (_from, rest) = full.split_once('\n').expect("the default has lines");
rest.to_string()
}
fn check(case: &Case) {
let rendered = render(case);
let task = rendered.text.as_str();
let opts = options(case);
let result = wrap_dockerfile(task, &opts);
let out = match result {
Err(error) => {
assert_eq!(
error.kind(),
ErrorKind::InvalidArg,
"IMAGE-3: a refusal is an invalid argument: {error}"
);
return;
}
Ok(out) => out,
};
assert_ne!(
rendered.known_from,
Some(false),
"IMAGE-3: a task with no FROM was wrapped:\n{task}"
);
assert_ne!(
rendered.known_unfinished,
Some(true),
"IMAGE-3: a task ending inside an unfinished instruction was wrapped:\n{task}"
);
let added = out
.strip_prefix(task)
.unwrap_or_else(|| panic!("IMAGE-1: the result starts with the task text:\n{out}"));
let added = if task.is_empty() || task.ends_with('\n') {
added
} else {
added
.strip_prefix('\n')
.unwrap_or_else(|| panic!("IMAGE-2: the missing newline was supplied:\n{out}"))
};
let workdir = opts.workdir.as_deref().filter(|dir| !dir.is_empty());
let stanza = default_stanza(opts.port, workdir);
let restored = match added.strip_prefix("USER root\n") {
Some(rest) => {
assert_eq!(rest, stanza, "IMAGE-1: the stanza is the default's:\n{out}");
true
}
None => {
assert_eq!(
added, stanza,
"IMAGE-1: the stanza is the default's:\n{out}"
);
false
}
};
assert!(
!restored || rendered.has_user_line,
"IMAGE-2: USER root was added to a task that sets no user:\n{task}"
);
assert_eq!(dockerfile_entrypoint(&out), Some("[]"), "{out}");
assert_eq!(dockerfile_cmd(&out), Some(r#"["/agentd"]"#), "{out}");
assert_eq!(dockerfile_agentd_port(&out), Some(opts.port), "{out}");
require_daemon_cmd(&out).expect("IMAGE-2: the daemon is the CMD");
require_matching_agentd_port(opts.port, &out).expect("IMAGE-2: the port agrees");
assert_eq!(
dockerfile_from_ref(&out),
dockerfile_from_ref(task),
"IMAGE-2: wrapping keeps the first FROM"
);
let base = BaseImage::from_dockerfile(&out).expect("IMAGE-4: a wrapped Dockerfile has a FROM");
assert_eq!(base.name, BaseImage::al2023().name);
assert_eq!(Some(base.docker_ref.as_str()), dockerfile_from_ref(&out));
require_matching_from(&base, &out).expect("IMAGE-4: the FROM guard accepts the pair");
}
#[test]
fn wrap_case() {
bolero::check!().with_type::<Case>().for_each(check);
}