use std::io::Write as _;
use std::time::Duration;
use crate::error::{Error, ErrorKind};
const DOCKERFILE_ENTRY: &str = "Dockerfile";
const AGENTD_ENTRY: &str = "agentd";
const AGENTD_MODE: u32 = 0o755;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Ecosystem {
Uv,
Npm,
Cargo,
}
impl Ecosystem {
pub const ALL: [Self; 3] = [Self::Uv, Self::Npm, Self::Cargo];
pub fn manifest_name(self) -> &'static str {
match self {
Self::Uv => "pyproject.toml",
Self::Npm => "package.json",
Self::Cargo => "Cargo.toml",
}
}
pub fn lockfile_name(self) -> &'static str {
match self {
Self::Uv => "uv.lock",
Self::Npm => "package-lock.json",
Self::Cargo => "Cargo.lock",
}
}
pub fn install_run_line(self) -> &'static str {
match self {
Self::Uv => {
"RUN dnf -y --setopt=install_weak_deps=0 install python3.12 python3.12-pip \
&& dnf clean all && pip3.12 install --no-cache-dir uv && uv sync --locked"
}
Self::Npm => {
"RUN dnf -y --setopt=install_weak_deps=0 install nodejs22-npm \
&& dnf clean all && npm ci"
}
Self::Cargo => {
"RUN dnf -y --setopt=install_weak_deps=0 install cargo \
&& dnf clean all && mkdir -p src && touch src/main.rs \
&& cargo fetch --locked"
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProjectFiles {
pub ecosystem: Ecosystem,
pub manifest: Vec<u8>,
pub lockfile: Vec<u8>,
}
pub fn build_artifact(
binary: &[u8],
dockerfile: &str,
project: Option<&ProjectFiles>,
) -> Result<Vec<u8>, Error> {
build_artifact_with_context(binary, dockerfile, project, None)
}
const FILE_MODE: u32 = 0o644;
pub fn build_artifact_with_context(
binary: &[u8],
dockerfile: &str,
project: Option<&ProjectFiles>,
context: Option<&super::context::BuildContext>,
) -> Result<Vec<u8>, Error> {
use zip::write::{SimpleFileOptions, ZipWriter};
microvms_domain::provision::require_aarch64(binary)?;
if let (Some(project), Some(context)) = (project, context)
&& let Some(entry) = context.entries().iter().find(|entry| {
entry.name == project.ecosystem.manifest_name()
|| entry.name == project.ecosystem.lockfile_name()
})
{
return Err(Error::invalid_arg(format!(
"the build context has {:?} at its root, which the project files already carry; drop one of the two.",
entry.name
)));
}
let mut writer = ZipWriter::new(std::io::Cursor::new(Vec::new()));
let options = |mode: u32| {
SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.last_modified_time(zip::DateTime::default())
.unix_permissions(mode)
};
let mut write_entry = |name: &str, bytes: &[u8], mode: u32| {
writer
.start_file(name, options(mode))
.and_then(|()| writer.write_all(bytes).map_err(Into::into))
.map_err(|error| {
Error::new(
ErrorKind::Unexpected,
format!("could not add {name} to the build artifact: {error}"),
)
})
};
write_entry(DOCKERFILE_ENTRY, dockerfile.as_bytes(), FILE_MODE)?;
write_entry(AGENTD_ENTRY, binary, AGENTD_MODE)?;
if let Some(project) = project {
write_entry(
project.ecosystem.manifest_name(),
&project.manifest,
FILE_MODE,
)?;
write_entry(
project.ecosystem.lockfile_name(),
&project.lockfile,
FILE_MODE,
)?;
}
if let Some(context) = context {
for entry in context.entries() {
write_entry(&entry.name, &entry.bytes, entry.mode)?;
}
}
let bytes = writer
.finish()
.map_err(|error| {
Error::new(
ErrorKind::Unexpected,
format!("could not finish the build artifact: {error}"),
)
})?
.into_inner();
Ok(bytes)
}
pub fn artifact_content_hash(
binary: &[u8],
dockerfile: &str,
project: Option<&ProjectFiles>,
) -> String {
artifact_content_hash_with_context(binary, dockerfile, project, None)
}
const CONTEXT_TAG: &[u8] = b"microvms-build-context/1";
pub fn artifact_content_hash_with_context(
binary: &[u8],
dockerfile: &str,
project: Option<&ProjectFiles>,
context: Option<&super::context::BuildContext>,
) -> String {
use sha2::{Digest as _, Sha256};
let mut hasher = Sha256::new();
hasher.update((binary.len() as u64).to_be_bytes());
hasher.update(binary);
hasher.update((dockerfile.len() as u64).to_be_bytes());
hasher.update(dockerfile.as_bytes());
if let Some(project) = project {
for (name, bytes) in [
(project.ecosystem.manifest_name(), &project.manifest),
(project.ecosystem.lockfile_name(), &project.lockfile),
] {
hasher.update((name.len() as u64).to_be_bytes());
hasher.update(name.as_bytes());
hasher.update((bytes.len() as u64).to_be_bytes());
hasher.update(bytes);
}
}
if let Some(context) = context.filter(|context| !context.entries().is_empty()) {
hasher.update((CONTEXT_TAG.len() as u64).to_be_bytes());
hasher.update(CONTEXT_TAG);
hasher.update((context.entries().len() as u64).to_be_bytes());
for entry in context.entries() {
hasher.update((entry.name.len() as u64).to_be_bytes());
hasher.update(entry.name.as_bytes());
hasher.update(entry.mode.to_be_bytes());
hasher.update((entry.bytes.len() as u64).to_be_bytes());
hasher.update(&entry.bytes);
}
}
const_hex::encode(hasher.finalize())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BaseImage {
pub name: String,
pub docker_ref: String,
pub working_dir: String,
}
impl BaseImage {
pub fn al2023() -> Self {
Self {
name: "al2023-1".to_string(),
docker_ref: "public.ecr.aws/amazonlinux/amazonlinux:2023-minimal".to_string(),
working_dir: String::new(),
}
}
pub fn from_dockerfile(dockerfile: &str) -> Result<Self, Error> {
match dockerfile_from_ref(dockerfile) {
Some(docker_ref) => Ok(Self {
docker_ref: docker_ref.to_string(),
..Self::al2023()
}),
None => Err(Error::invalid_arg(
"the Dockerfile has no FROM, so there is no image reference to pair the \
managed base with. Derive the base from the Dockerfile the task builds \
from."
.to_string(),
)),
}
}
pub fn arn(&self, region: &crate::region::Region) -> String {
format!(
"arn:aws:lambda:{}:aws:microvm-image:{}",
region.as_str(),
self.name
)
}
}
impl Default for BaseImage {
fn default() -> Self {
Self::al2023()
}
}
pub const DEFAULT_PROJECT_WORKDIR: &str = "/project";
pub fn default_dockerfile(
port: u16,
workdir: Option<&str>,
base: &BaseImage,
project: Option<Ecosystem>,
) -> String {
format!(
"FROM {}\n{}",
base.docker_ref,
agentd_stanza(port, workdir, project)
)
}
fn agentd_stanza(port: u16, workdir: Option<&str>, project: Option<Ecosystem>) -> String {
let mut lines = vec![
"COPY agentd /agentd".to_string(),
"RUN chmod 0755 /agentd".to_string(),
];
let workdir = match (workdir.filter(|dir| !dir.is_empty()), project) {
(Some(dir), _) => Some(dir),
(None, Some(_)) => Some(DEFAULT_PROJECT_WORKDIR),
(None, None) => None,
};
if let Some(workdir) = workdir {
lines.push(format!("RUN mkdir -p {workdir}"));
lines.push(format!("WORKDIR {workdir}"));
}
if let Some(ecosystem) = project {
lines.push(format!(
"COPY {} {} ./",
ecosystem.manifest_name(),
ecosystem.lockfile_name(),
));
lines.push(ecosystem.install_run_line().to_string());
}
lines.extend([
format!("ENV AGENTD_PORT={port}"),
"ENV AGENTD_LOG=info".to_string(),
format!("EXPOSE {port}"),
"ENTRYPOINT []".to_string(),
r#"CMD ["/agentd"]"#.to_string(),
String::new(),
]);
lines.join("\n")
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct WrapOptions {
pub port: u16,
pub workdir: Option<String>,
pub inherit_workdir: bool,
}
impl Default for WrapOptions {
fn default() -> Self {
Self {
port: super::DEFAULT_AGENT_PORT,
workdir: None,
inherit_workdir: false,
}
}
}
pub fn wrap_dockerfile(task: &str, opts: &WrapOptions) -> Result<String, Error> {
if dockerfile_from_ref(task).is_none() {
return Err(Error::invalid_arg(
"the task Dockerfile has no FROM, so there is no image to append the agentd \
stanza to and no base image to pair with it. Wrap the Dockerfile the task \
builds from, or pass `FROM <image>` for a task that names only an image."
.to_string(),
));
}
let scan = Instructions::scan(task);
if let Some(terminator) = scan.open_heredoc {
return Err(Error::invalid_arg(format!(
"the task Dockerfile ends inside a heredoc that is never terminated (no line \
{terminator:?} closes it), so every line appended after it — the whole agentd \
stanza — would become the heredoc's body. The image would build with no daemon \
in it and fail as a run-hook timeout. Terminate the heredoc."
)));
}
if scan.continued {
return Err(Error::invalid_arg(format!(
"the task Dockerfile's last instruction ends with a line continuation ({:?}), so \
the first line appended after it — the agentd stanza's `COPY agentd /agentd` — \
would be joined into that instruction instead of running. The image would build \
with no daemon in it and fail as a run-hook timeout. Finish the instruction, or \
drop the trailing {:?}.",
scan.escape, scan.escape,
)));
}
require_keepalive_under_idle_timeout(crate::session::exec::DEFAULT_STREAM_IDLE_TIMEOUT, task)?;
super::require_valid_port("port", opts.port)?;
let workdir = opts.workdir.as_deref().filter(|dir| !dir.is_empty());
if let Some(dir) = workdir
&& (!dir.starts_with('/') || dir.chars().any(|c| c.is_whitespace() || c.is_control()))
{
return Err(Error::invalid_arg(format!(
"the workdir {dir:?} is not one absolute path. The stanza writes it as \
`RUN mkdir -p {dir}` and `WORKDIR {dir}`: a relative path resolves against \
whatever WORKDIR the task left, whitespace splits the mkdir into several \
directories, and a line break writes an instruction nobody reviewed. Pass an \
absolute path such as /workspace."
)));
}
if opts.inherit_workdir && workdir.is_none() && !dockerfile_declares_workdir(task) {
return Err(Error::invalid_arg(
"inherit_workdir was requested but the task Dockerfile sets no WORKDIR and no \
workdir option was given, so there is nothing to inherit: every exec with no cwd \
would run in `/`, and every relative path in it would resolve there. Pass a \
workdir, or set WORKDIR in the task Dockerfile."
.to_string(),
));
}
let stanza = agentd_stanza(opts.port, workdir, None);
let mut wrapped = String::with_capacity(task.len() + stanza.len() + 16);
wrapped.push_str(task);
if !task.ends_with('\n') {
wrapped.push('\n');
}
if scan
.last_user
.as_deref()
.is_some_and(|user| !is_root_user(user))
{
wrapped.push_str("USER root\n");
}
wrapped.push_str(&stanza);
Ok(wrapped)
}
fn is_root_user(value: &str) -> bool {
let is_root = |part: &str| part == "root" || part == "0";
match value.split_once(':') {
Some((user, group)) => is_root(user) && is_root(group),
None => is_root(value),
}
}
fn dockerfile_declares_workdir(dockerfile: &str) -> bool {
dockerfile.lines().any(|line| {
let mut words = line.split_whitespace();
words
.next()
.is_some_and(|first| first.eq_ignore_ascii_case("WORKDIR"))
&& words.next().is_some()
})
}
struct Instructions {
escape: char,
continued: bool,
open_heredoc: Option<String>,
last_user: Option<String>,
}
impl Instructions {
fn scan(dockerfile: &str) -> Self {
let escape = escape_directive(dockerfile);
let mut heredocs: std::collections::VecDeque<(String, bool)> = Default::default();
let mut pending = String::new();
let mut continued = false;
let mut last_user = None;
for line in dockerfile.lines() {
if let Some((terminator, strip_tabs)) = heredocs.front() {
let candidate = if *strip_tabs {
line.trim_start_matches('\t')
} else {
line
};
if candidate.trim_end_matches('\r') == terminator {
heredocs.pop_front();
}
continue;
}
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let body = line.trim_end();
if let Some(joined) = body.strip_suffix(escape) {
pending.push_str(joined);
pending.push(' ');
continued = true;
continue;
}
continued = false;
pending.push_str(body);
let instruction = std::mem::take(&mut pending);
let mut words = instruction.split_whitespace();
let Some(keyword) = words.next() else {
continue;
};
if keyword.eq_ignore_ascii_case("USER") {
last_user = words.next().map(str::to_string);
} else if ["RUN", "COPY", "ADD"]
.iter()
.any(|name| keyword.eq_ignore_ascii_case(name))
{
heredocs.extend(words.filter_map(heredoc_marker));
}
}
Self {
escape,
continued,
open_heredoc: heredocs.pop_front().map(|(terminator, _)| terminator),
last_user,
}
}
}
fn escape_directive(dockerfile: &str) -> char {
for line in dockerfile.lines() {
let Some(directive) = line.trim().strip_prefix('#') else {
break;
};
let Some((key, value)) = directive.split_once('=') else {
break;
};
let key = key.trim().to_ascii_lowercase();
if key == "escape" {
return match value.trim() {
"`" => '`',
_ => '\\',
};
}
if !["syntax", "check"].contains(&key.as_str()) {
break;
}
}
'\\'
}
fn heredoc_marker(word: &str) -> Option<(String, bool)> {
let rest = word.strip_prefix("<<")?;
if rest.starts_with('<') {
return None;
}
let (rest, strip_tabs) = match rest.strip_prefix('-') {
Some(rest) => (rest, true),
None => (rest, false),
};
let name = rest
.strip_prefix('"')
.and_then(|name| name.strip_suffix('"'))
.or_else(|| {
rest.strip_prefix('\'')
.and_then(|name| name.strip_suffix('\''))
})
.unwrap_or(rest);
let valid = name
.chars()
.next()
.is_some_and(|first| first.is_ascii_alphabetic() || first == '_')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
valid.then(|| (name.to_string(), strip_tabs))
}
pub fn dockerfile_from_ref(dockerfile: &str) -> Option<&str> {
for line in dockerfile.lines() {
let mut words = line.split_whitespace();
let Some(first) = words.next() else { continue };
if !first.eq_ignore_ascii_case("FROM") {
continue;
}
return words.find(|word| !word.starts_with("--"));
}
None
}
pub fn require_workdir(base: &BaseImage, dockerfile: Option<&str>) -> Result<(), Error> {
if !base.working_dir.is_empty() {
return Ok(());
}
let declares_workdir = dockerfile.is_some_and(|text| {
text.lines().any(|line| {
let mut words = line.split_whitespace();
words
.next()
.is_some_and(|first| first.eq_ignore_ascii_case("WORKDIR"))
&& words.next().is_some()
})
});
if declares_workdir {
return Ok(());
}
Err(Error::invalid_arg(format!(
"inherit_workdir was requested but base image {:?} declares no WorkingDir and the \
Dockerfile sets none. Most public ARM64 base images leave it empty \
(docs/PLATFORM.md, 'Most public ARM64 base images have no WORKDIR'), so there is \
nothing to inherit and every relative path would resolve against `/`. Pass a workdir \
to default_dockerfile, or set WORKDIR in your own Dockerfile.",
base.name
)))
}
pub fn dockerfile_agentd_port(dockerfile: &str) -> Option<u16> {
let mut found = None;
for line in dockerfile.lines() {
let mut words = line.split_whitespace();
let Some(first) = words.next() else { continue };
if !first.eq_ignore_ascii_case("ENV") {
continue;
}
let Some(assignment) = words.next() else {
continue;
};
let value = match assignment.split_once('=') {
Some(("AGENTD_PORT", value)) => value,
Some(_) => continue,
None if assignment == "AGENTD_PORT" => match words.next() {
Some(value) => value,
None => continue,
},
None => continue,
};
if let Ok(port) = value.parse() {
found = Some(port);
}
}
found
}
pub fn require_matching_agentd_port(port: u16, dockerfile: &str) -> Result<(), Error> {
let Some(found) = dockerfile_agentd_port(dockerfile) else {
if port == super::DEFAULT_AGENT_PORT {
return Ok(());
}
return Err(Error::invalid_arg(format!(
"this client sends hooks.port={port} but the Dockerfile sets no usable \
AGENTD_PORT, so the daemon will listen on its own default of {default}. These \
must agree: the platform calls the build-time ready/validate hooks on the port \
in the create call, and a daemon listening on {default} answers none of them — \
the docker build succeeds, the daemon logs that it is listening, and the image \
still fails with CREATE_FAILED naming no port. Add ENV AGENTD_PORT={port} to \
the Dockerfile, or build it with default_dockerfile, which derives the value \
from the same port.",
default = super::DEFAULT_AGENT_PORT,
)));
};
if found == port {
return Ok(());
}
Err(Error::invalid_arg(format!(
"the Dockerfile sets ENV AGENTD_PORT={found} but this client sends hooks.port={port}. \
These must agree: the platform calls the build-time ready/validate hooks on the port \
in the create call, and a daemon listening on {found} answers none of them — the \
docker build succeeds, the daemon logs that it is listening, and the image still \
fails with CREATE_FAILED naming no port. Set ENV AGENTD_PORT={port} in the \
Dockerfile, or build it with default_dockerfile, which derives the value from the \
same port."
)))
}
pub fn dockerfile_env_u64(dockerfile: &str, key: &str) -> Option<u64> {
let mut found = None;
for line in dockerfile.lines() {
let mut words = line.split_whitespace();
let Some(first) = words.next() else { continue };
if !first.eq_ignore_ascii_case("ENV") {
continue;
}
let Some(assignment) = words.next() else {
continue;
};
let value = match assignment.split_once('=') {
Some((name, value)) if name == key => value,
Some(_) => continue,
None if assignment == key => match words.next() {
Some(value) => value,
None => continue,
},
None => continue,
};
if let Ok(parsed) = value.parse() {
found = Some(parsed);
}
}
found
}
pub fn require_keepalive_under_idle_timeout(
idle_timeout: Duration,
dockerfile: &str,
) -> Result<(), Error> {
let Some(secs) = dockerfile_env_u64(dockerfile, "AGENTD_SSE_KEEPALIVE_SECS") else {
return Ok(());
};
if secs < idle_timeout.as_secs() {
return Ok(());
}
Err(Error::invalid_arg(format!(
"the Dockerfile sets ENV AGENTD_SSE_KEEPALIVE_SECS={secs} but this client treats a \
stream as dead after {timeout}s of silence. The keepalive must be shorter: the \
daemon sends nothing between events except that keepalive, so an interval of \
{secs}s makes a healthy stream look dead, and every attach reconnects until it \
gives up. The error it raises then reports the client's own {timeout}s as though it \
were the keepalive interval, so it names neither the Dockerfile nor {secs}. Leave \
the variable unset for the daemon's 15s default, or keep it under {timeout}.",
timeout = idle_timeout.as_secs(),
)))
}
pub fn dockerfile_entrypoint(dockerfile: &str) -> Option<&str> {
last_instruction_value(dockerfile, "ENTRYPOINT")
}
pub fn dockerfile_cmd(dockerfile: &str) -> Option<&str> {
last_instruction_value(dockerfile, "CMD")
}
fn last_instruction_value<'a>(dockerfile: &'a str, keyword: &str) -> Option<&'a str> {
let mut found = None;
for line in dockerfile.lines() {
let trimmed = line.trim_start();
let Some(first) = trimmed.split_whitespace().next() else {
continue;
};
if first.eq_ignore_ascii_case(keyword) {
found = Some(trimmed[first.len()..].trim());
}
}
found
}
fn is_empty_exec_form(value: &str) -> bool {
let mut meaningful = value.chars().filter(|c| !c.is_whitespace());
meaningful.next() == Some('[') && meaningful.next() == Some(']') && meaningful.next().is_none()
}
pub fn require_daemon_cmd(dockerfile: &str) -> Result<(), Error> {
if let Some(entrypoint) = dockerfile_entrypoint(dockerfile)
&& !is_empty_exec_form(entrypoint)
{
return Err(Error::invalid_arg(format!(
"the Dockerfile sets ENTRYPOINT {entrypoint}, which makes any CMD its arguments \
rather than the container's process — so the daemon this client uploads never \
starts. The build succeeds anyway: the ready/validate hooks just go unanswered \
and the image fails as CREATE_FAILED, or the launch dies as a run-hook timeout, \
and neither symptom names ENTRYPOINT. Set `ENTRYPOINT []` alongside \
`CMD [\"/agentd\"]`, or build with default_dockerfile, which sets both."
)));
}
match dockerfile_cmd(dockerfile) {
Some(cmd) if !cmd.is_empty() && !is_empty_exec_form(cmd) => Ok(()),
_ => Err(Error::invalid_arg(
"the Dockerfile has no CMD, so the container runs the base image's default \
process and the daemon this client uploads never starts. The build succeeds \
anyway: the ready/validate hooks just go unanswered and the image fails as \
CREATE_FAILED, or the launch dies as a run-hook timeout, and neither symptom \
names CMD. Add `ENTRYPOINT []` and `CMD [\"/agentd\"]`, or build with \
default_dockerfile, which sets both."
.to_string(),
)),
}
}
pub fn require_project_install(project: &ProjectFiles, dockerfile: &str) -> Result<(), Error> {
let lockfile = project.ecosystem.lockfile_name();
if dockerfile.contains(lockfile) {
return Ok(());
}
Err(Error::invalid_arg(format!(
"the request carries project files ({lockfile} and {manifest}) but the Dockerfile \
never mentions {lockfile}, so the image would bake no environment layer: the build \
succeeds, and every launch still installs dependencies inside the guest with \
nothing naming the COPY that was never written. Copy the pair and install from \
the lockfile ({install}), or build with default_dockerfile, which derives both \
lines — or drop the project files if the layer is not wanted.",
manifest = project.ecosystem.manifest_name(),
install = match project.ecosystem {
Ecosystem::Uv => "uv sync --locked",
Ecosystem::Npm => "npm ci",
Ecosystem::Cargo => "cargo fetch --locked",
},
)))
}
pub fn require_matching_from(base: &BaseImage, dockerfile: &str) -> Result<(), Error> {
let Some(found) = dockerfile_from_ref(dockerfile) else {
return Ok(());
};
if found == base.docker_ref {
return Ok(());
}
if let Some(digest) = found
.strip_prefix(base.docker_ref.as_str())
.and_then(|rest| rest.strip_prefix("@sha256:"))
&& digest.len() == 64
&& digest.bytes().all(|b| b.is_ascii_hexdigit())
{
return Ok(());
}
Err(Error::invalid_arg(format!(
"the Dockerfile's FROM is {found:?} but base image {:?} pairs with {:?}. These must \
agree: baseImageArn and the FROM select the same base, and a mismatch builds against \
a base none of the measured platform behaviour applies to. Use default_dockerfile \
with this base, or pass a BaseImage whose docker_ref matches.",
base.name, base.docker_ref
)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::aarch64_daemon;
const DEFAULT_AGENT_PORT_FOR_TESTS: u16 = crate::control::DEFAULT_AGENT_PORT;
#[test]
fn the_artifact_holds_a_dockerfile_and_the_daemon() {
let bytes =
build_artifact(&aarch64_daemon(b"fake daemon"), "FROM scratch\n", None).expect("zips");
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("a readable zip");
let mut names: Vec<String> = (0..archive.len())
.map(|index| archive.by_index(index).expect("entry").name().to_string())
.collect();
names.sort();
assert_eq!(names, ["Dockerfile", "agentd"]);
}
#[test]
fn an_artifact_refuses_a_daemon_that_is_not_an_aarch64_elf() {
for (binary, why) in [
(crate::testing::elf_daemon(0x3E, b"x86"), "ELF machine 0x3e"),
(b"#!/bin/sh\n".to_vec(), "not an ELF binary"),
(Vec::new(), "not an ELF binary"),
] {
let error = build_artifact(&binary, "FROM scratch\n", None).expect_err(why);
assert_eq!(error.kind(), ErrorKind::Precondition, "{error}");
assert!(error.to_string().contains(why), "{error}");
}
}
#[test]
fn the_daemon_entry_carries_the_execute_bit() {
let bytes =
build_artifact(&aarch64_daemon(b"binary"), "FROM scratch\n", None).expect("zips");
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("a readable zip");
let mode = archive
.by_name("agentd")
.expect("the daemon entry")
.unix_mode()
.expect("a mode was recorded");
assert_eq!(
mode & 0o777,
AGENTD_MODE,
"a non-executable binary surfaces as a run-hook timeout, not a permission error \
(mode was {mode:o})"
);
assert!(
mode & 0o111 != 0,
"the execute bit is the whole point: {mode:o}"
);
}
#[test]
fn the_daemon_bytes_survive_the_round_trip() {
use std::io::Read as _;
let binary = aarch64_daemon(&(0u8..=255).cycle().take(4096).collect::<Vec<u8>>());
let bytes = build_artifact(&binary, "FROM scratch\n", None).expect("zips");
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("a readable zip");
let mut read = Vec::new();
archive
.by_name("agentd")
.expect("entry")
.read_to_end(&mut read)
.expect("reads");
assert_eq!(read, binary);
}
#[test]
fn the_artifact_never_carries_the_agent_token() {
use std::io::Read as _;
let token = "s3cr3t-agent-token-do-not-bake-me";
let dockerfile = default_dockerfile(9000, Some("/opt/work"), &BaseImage::al2023(), None);
assert!(
!dockerfile.contains(token),
"the default Dockerfile must not mention a token"
);
let project = ProjectFiles {
ecosystem: Ecosystem::Uv,
manifest: b"[project]\nname = \"demo\"\n".to_vec(),
lockfile: b"version = 1\n".to_vec(),
};
let bytes =
build_artifact(&aarch64_daemon(b"binary"), &dockerfile, Some(&project)).expect("zips");
assert!(
!bytes
.windows(token.len())
.any(|window| window == token.as_bytes()),
"the token appears in the artifact's raw bytes"
);
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("a readable zip");
assert_eq!(
archive.len(),
4,
"the scan below must sweep every member this artifact can hold"
);
for index in 0..archive.len() {
let mut entry = archive.by_index(index).expect("entry");
let name = entry.name().to_string();
let mut content = Vec::new();
entry.read_to_end(&mut content).expect("reads");
assert!(
!content
.windows(token.len())
.any(|window| window == token.as_bytes()),
"the token appears inside {name} — the image snapshot is shared by every VM \
launched from it, so a token here is a token shared with every VM"
);
}
}
#[test]
fn project_files_enter_the_artifact_under_their_ecosystem_names() {
use std::io::Read as _;
for (ecosystem, manifest_name, lockfile_name) in [
(Ecosystem::Uv, "pyproject.toml", "uv.lock"),
(Ecosystem::Npm, "package.json", "package-lock.json"),
(Ecosystem::Cargo, "Cargo.toml", "Cargo.lock"),
] {
let project = ProjectFiles {
ecosystem,
manifest: b"manifest-bytes".to_vec(),
lockfile: b"lockfile-bytes".to_vec(),
};
let bytes =
build_artifact(&aarch64_daemon(b"binary"), "FROM scratch\n", Some(&project))
.expect("zips");
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("a readable zip");
let mut names: Vec<String> = (0..archive.len())
.map(|index| archive.by_index(index).expect("entry").name().to_string())
.collect();
names.sort();
let mut expected = vec![
"Dockerfile".to_string(),
"agentd".to_string(),
manifest_name.to_string(),
lockfile_name.to_string(),
];
expected.sort();
assert_eq!(names, expected, "{ecosystem:?}");
let mut lockfile = Vec::new();
archive
.by_name(lockfile_name)
.expect("the lockfile entry")
.read_to_end(&mut lockfile)
.expect("reads");
assert_eq!(lockfile, b"lockfile-bytes", "{ecosystem:?}");
}
}
#[test]
fn project_files_carry_no_caller_named_entry() {
let ProjectFiles {
ecosystem: _,
manifest: _,
lockfile: _,
} = ProjectFiles {
ecosystem: Ecosystem::Npm,
manifest: Vec::new(),
lockfile: Vec::new(),
};
}
#[test]
fn the_content_hash_follows_the_inputs_and_only_the_inputs() {
let hash = artifact_content_hash(b"binary-bytes", "FROM scratch\n", None);
assert_eq!(
hash,
artifact_content_hash(b"binary-bytes", "FROM scratch\n", None)
);
assert_eq!(hash.len(), 64, "sha256 as lowercase hex");
assert!(
hash.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
);
assert_ne!(
hash,
artifact_content_hash(b"other-bytes", "FROM scratch\n", None)
);
assert_ne!(
hash,
artifact_content_hash(b"binary-bytes", "FROM scratch\nRUN true\n", None)
);
assert_ne!(
artifact_content_hash(b"ab", "c", None),
artifact_content_hash(b"a", "bc", None),
"the input boundary is part of the identity"
);
assert_eq!(
hash, "90a0ef925a4696b0c0f142241b2c714e63f40071d0d02951e26cb2a1d8bb24fc",
"the no-project digest must stay what it was before #74"
);
}
#[test]
fn the_content_hash_keys_on_the_lockfile() {
let project = |ecosystem, lockfile: &[u8]| ProjectFiles {
ecosystem,
manifest: b"[project]".to_vec(),
lockfile: lockfile.to_vec(),
};
let hash = artifact_content_hash(
b"binary",
"FROM scratch\n",
Some(&project(Ecosystem::Uv, b"version = 1")),
);
assert_eq!(
hash,
artifact_content_hash(
b"binary",
"FROM scratch\n",
Some(&project(Ecosystem::Uv, b"version = 1")),
),
"identical dependency files share an image"
);
assert_ne!(
hash,
artifact_content_hash(
b"binary",
"FROM scratch\n",
Some(&project(Ecosystem::Uv, b"version = 2")),
),
"a lockfile edit is a new image name and a fresh build"
);
assert_ne!(
hash,
artifact_content_hash(b"binary", "FROM scratch\n", None),
"carrying a layer is a different identity from carrying none"
);
assert_ne!(
artifact_content_hash(
b"binary",
"FROM scratch\n",
Some(&project(Ecosystem::Npm, b"version = 1")),
),
hash,
"the entry names count: the same bytes under another ecosystem install a \
different environment"
);
}
#[test]
fn the_default_dockerfile_derives_its_from_from_the_base_image() {
let base = BaseImage::al2023();
let dockerfile = default_dockerfile(9000, None, &base, None);
assert_eq!(
dockerfile_from_ref(&dockerfile),
Some(base.docker_ref.as_str())
);
assert!(dockerfile.contains(r#"CMD ["/agentd"]"#));
assert!(
dockerfile.contains("ENTRYPOINT []"),
"the trust boundary rests on this line"
);
assert!(dockerfile.contains("ENV AGENTD_PORT=9000"));
}
#[test]
fn a_workdir_is_created_and_set_or_absent_entirely() {
let base = BaseImage::al2023();
let with = default_dockerfile(9000, Some("/opt/baked-workdir"), &base, None);
assert!(with.contains("RUN mkdir -p /opt/baked-workdir"));
assert!(with.contains("WORKDIR /opt/baked-workdir"));
for none in [None, Some("")] {
let without = default_dockerfile(9000, none, &base, None);
assert!(!without.contains("WORKDIR"), "{without}");
}
}
#[test]
fn a_project_dockerfile_installs_from_the_lockfile_it_can_see() {
let base = BaseImage::al2023();
for (ecosystem, install) in [
(Ecosystem::Uv, "uv sync --locked"),
(Ecosystem::Npm, "npm ci"),
(Ecosystem::Cargo, "cargo fetch --locked"),
] {
let dockerfile = default_dockerfile(9000, None, &base, Some(ecosystem));
assert!(
dockerfile.contains(&format!(
"COPY {} {} ./",
ecosystem.manifest_name(),
ecosystem.lockfile_name(),
)),
"copies its pair: {dockerfile}"
);
assert!(dockerfile.contains(install), "{dockerfile}");
assert!(
dockerfile.contains(&format!("WORKDIR {DEFAULT_PROJECT_WORKDIR}")),
"{dockerfile}"
);
let placed = default_dockerfile(9000, Some("/srv/app"), &base, Some(ecosystem));
assert!(placed.contains("WORKDIR /srv/app"), "{placed}");
assert!(!placed.contains(DEFAULT_PROJECT_WORKDIR), "{placed}");
}
let without = default_dockerfile(9000, None, &base, None);
for install in ["uv sync", "npm ci", "cargo fetch", "COPY pyproject"] {
assert!(!without.contains(install), "{without}");
}
}
#[test]
fn the_project_dockerfile_passes_its_own_guards() {
let base = BaseImage::al2023();
for ecosystem in Ecosystem::ALL {
let dockerfile = default_dockerfile(9000, None, &base, Some(ecosystem));
require_matching_from(&base, &dockerfile).expect("its FROM is its base");
require_matching_agentd_port(9000, &dockerfile).expect("its port agrees");
require_daemon_cmd(&dockerfile).expect("the daemon is still the CMD");
let project = ProjectFiles {
ecosystem,
manifest: Vec::new(),
lockfile: Vec::new(),
};
require_project_install(&project, &dockerfile)
.expect("it copies the pair it was derived for");
}
}
#[test]
fn a_dockerfile_that_ignores_the_project_files_is_refused() {
let project = ProjectFiles {
ecosystem: Ecosystem::Npm,
manifest: b"{}".to_vec(),
lockfile: b"{}".to_vec(),
};
let error = require_project_install(
&project,
"FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal\n\
COPY agentd /agentd\n\
ENTRYPOINT []\nCMD [\"/agentd\"]\n",
)
.expect_err("no mention of the lockfile bakes no layer");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("package-lock.json"), "{message}");
assert!(message.contains("npm ci"), "{message}");
assert!(message.contains("no environment layer"), "{message}");
require_project_install(
&project,
"FROM x\nCOPY package.json package-lock.json ./\nRUN npm ci\nCMD [\"/agentd\"]\n",
)
.expect("a Dockerfile that copies the pair agrees");
}
#[test]
fn the_from_parser_ignores_decoration_rather_than_validating_syntax() {
assert_eq!(dockerfile_from_ref("FROM alpine\n"), Some("alpine"));
assert_eq!(dockerfile_from_ref(" from alpine:3\n"), Some("alpine:3"));
assert_eq!(
dockerfile_from_ref("FROM --platform=linux/arm64 alpine AS build\n"),
Some("alpine")
);
assert_eq!(dockerfile_from_ref("RUN echo from nowhere\n"), None);
assert_eq!(dockerfile_from_ref(""), None);
}
#[test]
fn a_dockerfile_from_that_disagrees_with_the_base_is_refused() {
let base = BaseImage::al2023();
let error = require_matching_from(&base, "FROM ubuntu:24.04\nCOPY agentd /agentd\n")
.expect_err("ubuntu is not the managed base");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("ubuntu:24.04"), "{message}");
assert!(message.contains(&base.docker_ref), "{message}");
assert!(
message.contains("none of the measured platform behaviour applies"),
"{message}"
);
require_matching_from(&base, &default_dockerfile(9000, None, &base, None))
.expect("the derived Dockerfile agrees with its own base");
}
#[test]
fn a_digest_pinned_from_of_the_agreeing_ref_passes() {
let base = BaseImage::al2023();
let digest = "c439fb4994ea7ca529233d6256446d3f8b7b4efb58956073e015303a170011de";
let pinned = format!(
"FROM {}@sha256:{digest}\nCOPY agentd /agentd\n",
base.docker_ref
);
require_matching_from(&base, &pinned).expect("a digest pin of the same ref agrees");
require_matching_from(&base, &format!("FROM ubuntu:24.04@sha256:{digest}\n"))
.expect_err("a digest does not launder a different ref");
require_matching_from(&base, &format!("FROM {}@sha256:abc123\n", base.docker_ref))
.expect_err("a 6-character digest is not a digest");
require_matching_from(
&base,
&format!("FROM {}@sha256:{}\n", base.docker_ref, "g".repeat(64)),
)
.expect_err("64 non-hex characters are not a digest");
}
#[test]
fn a_dockerfile_with_no_from_is_left_to_the_build() {
require_matching_from(&BaseImage::al2023(), "COPY agentd /agentd\n")
.expect("no FROM is not a disagreement");
}
#[test]
fn a_dockerfile_port_that_disagrees_with_the_hook_port_is_refused() {
let error = require_matching_agentd_port(
crate::control::DEFAULT_AGENT_PORT,
"FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal\n\
COPY agentd /agentd\n\
ENV AGENTD_PORT=8080\n\
EXPOSE 8080\n\
CMD [\"/agentd\"]\n",
)
.expect_err("8080 is not the port the create call sends");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("8080"), "{message}");
assert!(message.contains("9000"), "{message}");
assert!(message.contains("CREATE_FAILED"), "{message}");
assert!(message.contains("ready/validate"), "{message}");
require_matching_agentd_port(
crate::control::DEFAULT_AGENT_PORT,
&default_dockerfile(
crate::control::DEFAULT_AGENT_PORT,
Some("/work"),
&BaseImage::al2023(),
None,
),
)
.expect("the derived Dockerfile agrees with the port it was derived from");
}
#[test]
fn the_port_scan_reads_both_env_spellings_and_takes_the_last() {
assert_eq!(dockerfile_agentd_port("ENV AGENTD_PORT=9000\n"), Some(9000));
assert_eq!(dockerfile_agentd_port("ENV AGENTD_PORT 8080\n"), Some(8080));
assert_eq!(dockerfile_agentd_port("env agentd_port=7000\n"), None);
assert_eq!(
dockerfile_agentd_port("ENV AGENTD_PORT=8080\nENV AGENTD_PORT=9000\n"),
Some(9000),
);
assert_eq!(dockerfile_agentd_port("ENV AGENTD_LOG=info\n"), None);
assert_eq!(
dockerfile_agentd_port("FROM x\nCOPY agentd /agentd\n"),
None
);
assert_eq!(dockerfile_agentd_port("ENV AGENTD_PORT=nine\n"), None);
require_matching_agentd_port(
crate::control::DEFAULT_AGENT_PORT,
"FROM x\nCOPY agentd /agentd\n",
)
.expect("no variable agrees with the default, which is what the daemon will use");
}
#[test]
fn a_dockerfile_naming_no_port_is_refused_when_the_client_moved_off_the_default() {
let error = require_matching_agentd_port(
8080,
"FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal\n\
COPY agentd /agentd\n\
CMD [\"/agentd\"]\n",
)
.expect_err("a silent Dockerfile leaves the daemon on 9000, not on 8080");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("8080"), "{message}");
assert!(
message.contains(&crate::control::DEFAULT_AGENT_PORT.to_string()),
"the daemon's default is the value the reader has to learn: {message}"
);
assert!(message.contains("CREATE_FAILED"), "{message}");
assert!(message.contains("ready/validate"), "{message}");
require_matching_agentd_port(8080, "FROM x\nENV AGENTD_PORT=nine\n")
.expect_err("a value the daemon cannot parse leaves it on its own default");
require_matching_agentd_port(
crate::control::DEFAULT_AGENT_PORT,
"FROM x\nCOPY agentd /agentd\n",
)
.expect("silence agrees with the default");
}
#[test]
fn a_keepalive_at_or_over_the_client_idle_timeout_is_refused() {
let timeout = crate::session::exec::DEFAULT_STREAM_IDLE_TIMEOUT;
assert_eq!(timeout.as_secs(), 60, "the guard's arithmetic assumes this");
let error =
require_keepalive_under_idle_timeout(timeout, "ENV AGENTD_SSE_KEEPALIVE_SECS=90\n")
.expect_err("90s of scheduled silence exceeds a 60s tolerance");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("90"), "{message}");
assert!(message.contains("60"), "{message}");
require_keepalive_under_idle_timeout(timeout, "ENV AGENTD_SSE_KEEPALIVE_SECS=60\n")
.expect_err("an interval equal to the tolerance is a race, not a margin");
require_keepalive_under_idle_timeout(timeout, "ENV AGENTD_SSE_KEEPALIVE_SECS 30\n")
.expect("30s leaves a margin, in the legacy spelling");
require_keepalive_under_idle_timeout(timeout, "FROM x\nCOPY agentd /agentd\n")
.expect("an unset keepalive leaves the daemon at 15s");
require_keepalive_under_idle_timeout(
timeout,
&default_dockerfile(
crate::control::DEFAULT_AGENT_PORT,
Some("/work"),
&BaseImage::al2023(),
None,
),
)
.expect("the derived Dockerfile sets no keepalive");
}
#[test]
fn a_dockerfile_that_never_runs_the_daemon_is_refused_naming_the_symptom() {
let error = require_daemon_cmd(
"FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal\n\
COPY agentd /agentd\n\
ENV AGENTD_PORT=9000\n",
)
.expect_err("no CMD means the daemon never starts");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("no CMD"), "{message}");
assert!(message.contains("run-hook timeout"), "{message}");
assert!(message.contains("CREATE_FAILED"), "{message}");
let error = require_daemon_cmd(
"FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal\n\
COPY agentd /agentd\n\
ENTRYPOINT [\"/bin/sh\", \"-c\"]\n\
CMD [\"/agentd\"]\n",
)
.expect_err("a non-empty ENTRYPOINT makes CMD its arguments");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("ENTRYPOINT"), "{message}");
assert!(message.contains("run-hook timeout"), "{message}");
require_daemon_cmd("FROM x\nCMD []\n").expect_err("CMD [] runs nothing");
}
#[test]
fn the_deployment_invariant_passes_the_daemon_cmd_guard() {
require_daemon_cmd(&default_dockerfile(
9000,
Some("/work"),
&BaseImage::al2023(),
None,
))
.expect("the derived Dockerfile is the invariant");
require_daemon_cmd("FROM x\nENTRYPOINT []\nCMD [\"/agentd\"]\n").expect("the invariant");
require_daemon_cmd("FROM x\nENTRYPOINT [ ]\nCMD [\"/agentd\"]\n")
.expect("interior whitespace is still the empty exec form");
require_daemon_cmd("FROM x\ncmd [\"/agentd\"]\n").expect("case-insensitive, no ENTRYPOINT");
require_daemon_cmd("FROM x\nENTRYPOINT [\"/bin/sh\"]\nENTRYPOINT []\nCMD [\"/agentd\"]\n")
.expect("the last ENTRYPOINT is the one the build uses");
require_daemon_cmd("FROM x\nENTRYPOINT []\nENTRYPOINT [\"/bin/sh\"]\nCMD [\"/agentd\"]\n")
.expect_err("the last ENTRYPOINT is non-empty");
}
#[test]
fn inheriting_a_workdir_nothing_declares_is_refused() {
let base = BaseImage::al2023();
assert!(base.working_dir.is_empty(), "the measured case");
let error = require_workdir(&base, None).expect_err("nothing declares a workdir");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
let message = error.to_string();
assert!(message.contains("no WorkingDir"), "{message}");
assert!(message.contains("nothing to inherit"), "{message}");
assert!(message.contains("docs/PLATFORM.md"), "{message}");
}
#[test]
fn either_the_dockerfile_or_the_base_may_supply_the_workdir() {
let base = BaseImage::al2023();
require_workdir(&base, Some("FROM x\nWORKDIR /srv\n"))
.expect("the Dockerfile declares one");
let purpose_built = BaseImage {
working_dir: "/app".to_string(),
..BaseImage::al2023()
};
require_workdir(&purpose_built, None).expect("the base image declares one");
}
#[test]
fn a_workdir_with_no_argument_does_not_count_as_a_declaration() {
let error = require_workdir(&BaseImage::al2023(), Some("FROM x\nWORKDIR\n"))
.expect_err("WORKDIR with no path declares nothing");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
}
#[test]
fn wrapping_a_bare_from_is_the_default_dockerfile_minus_nothing() {
for (port, workdir) in [
(DEFAULT_AGENT_PORT_FOR_TESTS, None),
(DEFAULT_AGENT_PORT_FOR_TESTS, Some("/srv/task")),
(8080, None),
] {
let base = BaseImage {
docker_ref: "x".to_string(),
..BaseImage::al2023()
};
let default = default_dockerfile(port, workdir, &base, None);
let opts = WrapOptions {
port,
workdir: workdir.map(str::to_string),
..WrapOptions::default()
};
assert_eq!(
wrap_dockerfile("FROM x\n", &opts).expect("a bare FROM wraps"),
default,
"IMAGE-1: port {port} workdir {workdir:?}"
);
let (from, stanza) = default.split_once('\n').expect("lines");
assert_eq!(from, "FROM x");
assert!(stanza.ends_with("CMD [\"/agentd\"]\n"), "{stanza}");
}
}
#[test]
fn a_wrapped_task_ends_on_the_invariant_with_root_restored_when_needed() {
let task = "FROM python:3.12-slim\nWORKDIR /app\nENV AGENTD_PORT=8080\n\
ENTRYPOINT [\"/bin/sh\", \"-c\"]\nCMD [\"python\", \"serve.py\"]\nUSER app\n";
let wrapped = wrap_dockerfile(task, &WrapOptions::default()).expect("wraps");
let added = wrapped
.strip_prefix(task)
.expect("the task text is kept verbatim");
assert!(
added.starts_with("USER root\nCOPY agentd /agentd\n"),
"{added}"
);
assert_eq!(dockerfile_entrypoint(&wrapped), Some("[]"));
assert_eq!(dockerfile_cmd(&wrapped), Some(r#"["/agentd"]"#));
assert_eq!(dockerfile_agentd_port(&wrapped), Some(9000));
require_daemon_cmd(&wrapped).expect("the daemon is the CMD");
require_matching_agentd_port(9000, &wrapped).expect("the stanza's port wins");
for (last_user, restored) in [
("USER root", false),
("USER 0", false),
("USER root:root", false),
("USER 0:0", false),
("user app", true),
("USER 1000:1000", true),
("USER root:staff", true),
("USER ${TASK_USER}", true),
] {
let task = format!("FROM x\n{last_user}\n");
let added = wrap_dockerfile(&task, &WrapOptions::default())
.expect("wraps")
.strip_prefix(task.as_str())
.expect("verbatim")
.to_string();
assert_eq!(
added.starts_with("USER root\n"),
restored,
"IMAGE-2: {last_user:?}: {added}"
);
}
let task = "FROM x\nUSER app\nUSER root\n";
let wrapped = wrap_dockerfile(task, &WrapOptions::default()).expect("wraps");
let added = wrapped.strip_prefix(task).expect("verbatim");
assert!(added.starts_with("COPY agentd /agentd\n"), "{added}");
}
#[test]
fn a_task_without_a_trailing_newline_is_normalized() {
let wrapped = wrap_dockerfile("FROM x", &WrapOptions::default()).expect("wraps");
assert!(
wrapped.starts_with("FROM x\nCOPY agentd /agentd\n"),
"{wrapped}"
);
let wrapped =
wrap_dockerfile("FROM x\r\nRUN true", &WrapOptions::default()).expect("wraps");
assert!(
wrapped.starts_with("FROM x\r\nRUN true\nCOPY agentd /agentd\n"),
"{wrapped}"
);
let wrapped = wrap_dockerfile("FROM x\r\n", &WrapOptions::default()).expect("wraps");
assert!(wrapped.starts_with("FROM x\r\nCOPY agentd"), "{wrapped}");
}
#[test]
fn a_task_the_stanza_cannot_follow_is_refused_naming_the_cause() {
let refused = |task: &str, opts: WrapOptions, cause: &str| {
let error = wrap_dockerfile(task, &opts).expect_err(cause);
assert_eq!(error.kind(), ErrorKind::InvalidArg, "{error}");
let message = error.to_string();
assert!(message.contains(cause), "IMAGE-3 {cause:?}: {message}");
};
let none = WrapOptions::default;
refused("RUN echo hello\n", none(), "no FROM");
refused("", none(), "no FROM");
refused("FROM\n", none(), "no FROM");
refused(
"FROM x\nRUN apt-get update && \\\n",
none(),
"line continuation",
);
refused("FROM x\nRUN a \\ \n", none(), "line continuation");
refused("FROM x\nRUN a \\\r\n", none(), "line continuation");
refused(
"FROM x\nRUN a \\\n\n# comment\n",
none(),
"line continuation",
);
refused("# escape=`\nFROM x\nRUN a `\n", none(), "line continuation");
refused("FROM x\nRUN <<EOF\necho never closed\n", none(), "heredoc");
refused("FROM x\nRUN <<-\"END\"\necho x\n", none(), "heredoc");
refused("FROM x\nCOPY <<A <<B /dst/\na\nA\nb\n", none(), "heredoc");
refused(
"FROM x\nENV AGENTD_SSE_KEEPALIVE_SECS=90\n",
none(),
"AGENTD_SSE_KEEPALIVE_SECS=90",
);
for workdir in ["relative/dir", "/srv\nRUN evil", "/my dir", "/tab\there"] {
refused(
"FROM x\n",
WrapOptions {
workdir: Some(workdir.to_string()),
..none()
},
"absolute path",
);
}
refused("FROM x\n", WrapOptions { port: 0, ..none() }, "port");
refused(
"FROM x\n",
WrapOptions {
inherit_workdir: true,
..none()
},
"nothing to inherit",
);
}
#[test]
fn a_finished_task_is_wrapped_even_when_it_looks_unfinished() {
let none = WrapOptions::default;
for task in [
"# escape=`\nFROM x\nRUN a \\\n",
"FROM x\nRUN a \\\n && b\n",
"FROM x\nRUN <<EOF\necho closed\nEOF\n",
"FROM x\nRUN <<-EOF\n\techo closed\n\tEOF\n",
"FROM x\nCOPY <<A <<B /dst/\na\nA\nb\nB\n",
"FROM x\nRUN cat <<< word\n",
"FROM x\n# a trailing comment \\\n",
"FROM x\nENV AGENTD_SSE_KEEPALIVE_SECS=30\n",
] {
wrap_dockerfile(task, &none())
.unwrap_or_else(|error| panic!("{task:?} was refused: {error}"));
}
wrap_dockerfile(
"FROM x\nWORKDIR /app\n",
&WrapOptions {
inherit_workdir: true,
..none()
},
)
.expect("the task declares one");
wrap_dockerfile(
"FROM x\n",
&WrapOptions {
inherit_workdir: true,
workdir: Some("/srv".to_string()),
..none()
},
)
.expect("the options declare one");
wrap_dockerfile(
"FROM x\n",
&WrapOptions {
workdir: Some(String::new()),
..none()
},
)
.expect("an empty workdir is none, as default_dockerfile reads it");
}
#[test]
fn a_base_derived_from_a_dockerfile_pairs_with_it_by_construction() {
let digest = "c439fb4994ea7ca529233d6256446d3f8b7b4efb58956073e015303a170011de";
for (task, docker_ref) in [
("FROM python:3.12-slim\n", "python:3.12-slim".to_string()),
(
"FROM --platform=linux/arm64 golang:1.23 AS build\nFROM python:3.12-slim\n",
"golang:1.23".to_string(),
),
(
&*format!("FROM ubuntu:24.04@sha256:{digest}\n"),
format!("ubuntu:24.04@sha256:{digest}"),
),
] {
let base = BaseImage::from_dockerfile(task).expect("a FROM");
assert_eq!(base.docker_ref, docker_ref);
assert_eq!(
base.name,
BaseImage::al2023().name,
"IMAGE-4 keeps the managed base"
);
assert_eq!(base.working_dir, "");
let wrapped = wrap_dockerfile(task, &WrapOptions::default()).expect("wraps");
require_matching_from(&base, &wrapped).expect("IMAGE-4: the pair agrees");
assert_eq!(BaseImage::from_dockerfile(&wrapped).expect("a FROM"), base);
}
require_matching_from(
&BaseImage::al2023(),
&wrap_dockerfile("FROM python:3.12-slim\n", &WrapOptions::default()).expect("wraps"),
)
.expect_err("the managed base does not pair with python:3.12-slim");
let error = BaseImage::from_dockerfile("RUN echo hello\n").expect_err("no FROM");
assert_eq!(error.kind(), ErrorKind::InvalidArg);
assert!(error.to_string().contains("no FROM"), "{error}");
}
#[test]
fn wrap_agrees_with_the_model_table() {
#[derive(Debug)]
enum Expect {
Refused(&'static str),
Wrapped { user_root: bool },
}
let digest = "c439fb4994ea7ca529233d6256446d3f8b7b4efb58956073e015303a170011de";
let managed = BaseImage::al2023().docker_ref;
let froms = [
None,
Some(managed.clone()),
Some(format!("{managed}@sha256:{digest}")),
Some("python:3.12-slim".to_string()),
];
let mut checked = 0;
for from in &froms {
for tail in 0..3 {
for user in 0..3 {
for declares_workdir in [false, true] {
for keepalive_too_long in [false, true] {
for workdir in 0..3 {
for inherit_workdir in [false, true] {
let mut text = String::new();
match from {
Some(from) => text.push_str(&format!("FROM {from}\n")),
None => text.push_str("RUN echo no base\n"),
}
if declares_workdir {
text.push_str("WORKDIR /app\n");
}
if keepalive_too_long {
text.push_str("ENV AGENTD_SSE_KEEPALIVE_SECS=60\n");
}
match user {
1 => text.push_str("USER root\n"),
2 => text.push_str("USER app\n"),
_ => {}
}
match tail {
1 => text.push_str("RUN make \\\n"),
2 => text.push_str("RUN <<EOF\necho open\n"),
_ => {}
}
let opts = WrapOptions {
workdir: match workdir {
1 => Some("/srv".to_string()),
2 => Some("srv".to_string()),
_ => None,
},
inherit_workdir,
..WrapOptions::default()
};
let expected = if from.is_none() {
Expect::Refused("no FROM")
} else if tail != 0 {
Expect::Refused(if tail == 1 {
"line continuation"
} else {
"heredoc"
})
} else if keepalive_too_long {
Expect::Refused("AGENTD_SSE_KEEPALIVE_SECS")
} else if workdir == 2 {
Expect::Refused("absolute path")
} else if inherit_workdir && !declares_workdir && workdir == 0 {
Expect::Refused("nothing to inherit")
} else {
Expect::Wrapped {
user_root: user == 2,
}
};
let got = wrap_dockerfile(&text, &opts);
match (&expected, &got) {
(Expect::Refused(cause), Err(error)) => assert!(
error.to_string().contains(cause),
"{expected:?} for {text:?} {opts:?}: {error}"
),
(Expect::Wrapped { user_root }, Ok(out)) => assert_eq!(
out.strip_prefix(text.as_str())
.expect("verbatim")
.starts_with("USER root\n"),
*user_root,
"{text:?} {opts:?}"
),
_ => panic!(
"{expected:?} for {text:?} {opts:?}, got {got:?}"
),
}
checked += 1;
}
}
}
}
}
}
}
assert_eq!(
checked,
4 * 3 * 3 * 2 * 2 * 3 * 2,
"every combination the model has"
);
}
fn context_of(entries: &[(&str, u32, &[u8])]) -> crate::control::context::BuildContext {
crate::control::context::BuildContext::from_entries(
entries
.iter()
.map(
|(name, mode, bytes)| crate::control::context::ContextEntry {
name: (*name).to_string(),
mode: *mode,
bytes: bytes.to_vec(),
},
)
.collect(),
)
.expect("valid entries")
}
#[test]
fn a_request_without_a_context_keeps_its_digest() {
let pinned = "90a0ef925a4696b0c0f142241b2c714e63f40071d0d02951e26cb2a1d8bb24fc";
assert_eq!(
artifact_content_hash_with_context(b"binary-bytes", "FROM scratch\n", None, None),
pinned,
"IMAGE-6: the no-context digest is the pre-#221 digest"
);
let empty = crate::control::context::BuildContext::default();
assert_eq!(
artifact_content_hash_with_context(
b"binary-bytes",
"FROM scratch\n",
None,
Some(&empty)
),
pinned,
"an empty context carries no extra entries, so it is the same artifact"
);
assert_eq!(
artifact_content_hash(b"binary-bytes", "FROM scratch\n", None),
pinned
);
}
#[test]
fn the_context_hash_follows_every_entry_and_not_their_order() {
let hash = |context: &crate::control::context::BuildContext| {
artifact_content_hash_with_context(b"bin", "FROM x\n", None, Some(context))
};
let base = hash(&context_of(&[
("a.txt", 0o644, b"a"),
("b.txt", 0o644, b"b"),
]));
assert_ne!(
base,
artifact_content_hash_with_context(b"bin", "FROM x\n", None, None),
"IMAGE-6: a context is part of the identity"
);
assert_eq!(
base,
hash(&context_of(&[
("b.txt", 0o644, b"b"),
("a.txt", 0o644, b"a")
])),
"the read order is not"
);
for (changed, what) in [
(
context_of(&[("a.txt", 0o644, b"A"), ("b.txt", 0o644, b"b")]),
"bytes",
),
(
context_of(&[("a.txt", 0o755, b"a"), ("b.txt", 0o644, b"b")]),
"mode",
),
(
context_of(&[("c.txt", 0o644, b"a"), ("b.txt", 0o644, b"b")]),
"name",
),
(context_of(&[("a.txt", 0o644, b"ab")]), "boundary"),
] {
assert_ne!(base, hash(&changed), "IMAGE-6: the entry {what} counts");
}
}
#[test]
fn the_artifact_is_deterministic_with_fixed_dates_and_modes() {
use std::io::Read as _;
let context = context_of(&[("app/run.sh", 0o755, b"#!/bin/sh\n"), ("data", 0o644, b"d")]);
let build = || {
build_artifact_with_context(
&aarch64_daemon(b"daemon"),
"FROM x\n",
None,
Some(&context),
)
.expect("zips")
};
let first = build();
assert_eq!(first, build(), "IMAGE-7: equal inputs, identical bytes");
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(first)).expect("a zip");
let mut names = Vec::new();
for index in 0..archive.len() {
let entry = archive.by_index(index).expect("entry");
names.push(entry.name().to_string());
let modified = entry.last_modified().expect("a date");
assert_eq!(
(modified.year(), modified.month(), modified.day()),
(1980, 1, 1),
"IMAGE-7: {} carries a fixed date",
entry.name()
);
let mode = entry.unix_mode().expect("a mode") & 0o777;
let expected = match entry.name() {
"agentd" | "app/run.sh" => 0o755,
_ => 0o644,
};
assert_eq!(mode, expected, "IMAGE-7: {} mode {mode:o}", entry.name());
}
assert_eq!(names, ["Dockerfile", "agentd", "app/run.sh", "data"]);
let mut dockerfile = String::new();
archive
.by_name("Dockerfile")
.expect("entry")
.read_to_string(&mut dockerfile)
.expect("reads");
assert_eq!(dockerfile, "FROM x\n");
}
#[test]
fn a_context_artifact_never_carries_the_agent_token() {
let token = "s3cr3t-agent-token-do-not-bake-me";
let context = context_of(&[("app/main.py", 0o644, b"print('hi')\n")]);
let bytes = build_artifact_with_context(
&aarch64_daemon(b"binary"),
&wrap_dockerfile("FROM x\n", &WrapOptions::default()).expect("wraps"),
None,
Some(&context),
)
.expect("zips");
assert!(
!bytes
.windows(token.len())
.any(|window| window == token.as_bytes())
);
}
#[test]
fn the_base_image_arn_names_the_request_region_with_a_colon_separator() {
assert_eq!(
BaseImage::al2023().arn(&crate::region::Region::UsEast1),
"arn:aws:lambda:us-east-1:aws:microvm-image:al2023-1"
);
assert_eq!(
BaseImage::al2023().arn(&crate::region::Region::ApNortheast1),
"arn:aws:lambda:ap-northeast-1:aws:microvm-image:al2023-1"
);
assert!(
!BaseImage::al2023()
.arn(&crate::region::Region::UsEast1)
.contains("microvm-image/"),
"the slash form answers AccessDeniedException, not a validation error"
);
}
}