use std::error::Error;
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::fs::{self, DirBuilder};
use std::io;
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
const FETCH: &str = "mkdir -p /tmp/home && cargo generate-lockfile && cargo fetch --locked";
const CHECK: &str = "mkdir -p /tmp/home && cargo fmt --all --check && cargo build --workspace --all-targets --all-features --locked --offline && cargo clippy --workspace --all-targets --all-features --locked --offline -- -D warnings && cargo test --workspace --all-targets --all-features --locked --offline --no-fail-fast && cargo test --doc --workspace --all-features --locked --offline --no-fail-fast";
const BUILD: &str = "mkdir -p /tmp/home && cargo build --release --locked --offline --bin \"$K1_BINARY\" && cp -- \"/target/release/$K1_BINARY\" /output/binary";
#[derive(Debug)]
pub struct CommandDiagnostics {
pub status: ExitStatus,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
#[derive(Debug)]
pub struct FileFailure {
pub operation: &'static str,
pub path: PathBuf,
pub source: io::Error,
}
#[derive(Debug)]
pub enum RustPodmanError {
InvalidInput {
field: &'static str,
reason: String,
},
File(FileFailure),
Spawn(io::Error),
CommandFailed(CommandDiagnostics),
AfterCommand {
diagnostics: CommandDiagnostics,
failure: FileFailure,
},
}
impl fmt::Display for RustPodmanError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl Error for RustPodmanError {}
#[derive(Clone, Debug)]
pub struct RustPodmanPaths {
pub workspace: PathBuf,
pub cargo_home: PathBuf,
pub target: PathBuf,
pub local_registry: PathBuf,
pub cargo_config: PathBuf,
}
impl RustPodmanPaths {
pub fn new(
workspace: impl Into<PathBuf>,
cargo_home: impl Into<PathBuf>,
target: impl Into<PathBuf>,
local_registry: impl Into<PathBuf>,
cargo_config: impl Into<PathBuf>,
) -> Self {
Self {
workspace: workspace.into(),
cargo_home: cargo_home.into(),
target: target.into(),
local_registry: local_registry.into(),
cargo_config: cargo_config.into(),
}
}
}
#[derive(Clone, Debug)]
pub struct RustPodman {
program: OsString,
image: OsString,
}
impl RustPodman {
pub fn new(
program: impl Into<OsString>,
image: impl Into<OsString>,
) -> Result<Self, RustPodmanError> {
let program = program.into();
let image = image.into();
require_nonempty(&program, "program")?;
require_nonempty(&image, "image")?;
Ok(Self { program, image })
}
pub fn fetch(&self, paths: &RustPodmanPaths) -> Result<CommandDiagnostics, RustPodmanError> {
let paths = ResolvedPaths::new(paths)?;
let diagnostics = self.run(self.command(&paths, true, false, false), FETCH)?;
if let Err(failure) = ordinary_file(&paths.workspace.join("Cargo.lock")) {
return Err(after(diagnostics, failure));
}
Ok(diagnostics)
}
pub fn check(&self, paths: &RustPodmanPaths) -> Result<CommandDiagnostics, RustPodmanError> {
let paths = ResolvedPaths::new(paths)?;
ordinary_file(&paths.workspace.join("Cargo.lock")).map_err(RustPodmanError::File)?;
self.run(self.command(&paths, false, true, true), CHECK)
}
pub fn build_binary(
&self,
paths: &RustPodmanPaths,
binary: &str,
output: impl AsRef<Path>,
) -> Result<CommandDiagnostics, RustPodmanError> {
valid_binary(binary)?;
let paths = ResolvedPaths::new(paths)?;
ordinary_file(&paths.workspace.join("Cargo.lock")).map_err(RustPodmanError::File)?;
let output = output_location(output.as_ref())?;
validate_output(&output).map_err(RustPodmanError::File)?;
let mut stage = StageDir::create(output.parent().unwrap(), output.file_name().unwrap())?;
let mut command = self.command(&paths, false, true, true);
command.arg(format!("--env=K1_BINARY={binary}"));
volume(&mut command, &stage.path, "/output:rw");
let diagnostics = self.run(command, BUILD)?;
let staged = stage.path.join("binary");
if let Err(failure) = ordinary_file(&staged) {
return Err(after(diagnostics, failure));
}
if let Err(failure) = validate_output(&output) {
return Err(after(diagnostics, failure));
}
if let Err(source) = fs::rename(&staged, &output) {
return Err(after_file(diagnostics, "rename output", &output, source));
}
let _ = stage.cleanup();
Ok(diagnostics)
}
fn command(
&self,
paths: &ResolvedPaths,
workspace_write: bool,
target: bool,
no_network: bool,
) -> Command {
let mut command = Command::new(&self.program);
command.args([
"run",
"--rm",
"--read-only",
"--userns=keep-id",
"--cap-drop=ALL",
"--security-opt=no-new-privileges",
"--workdir=/workspace",
"--env=HOME=/tmp/home",
"--env=CARGO_HOME=/cargo-home",
"--env=CARGO_TARGET_DIR=/target",
"--tmpfs=/tmp:rw,nosuid,nodev",
]);
if no_network {
command.arg("--network=none");
}
volume(
&mut command,
&paths.workspace,
if workspace_write {
"/workspace:rw"
} else {
"/workspace:ro"
},
);
volume(&mut command, &paths.local_registry, "/k1/local-registry:ro");
volume(&mut command, &paths.cargo_home, "/cargo-home:rw");
if target {
volume(&mut command, &paths.target, "/target:rw");
}
volume(
&mut command,
&paths.cargo_config,
"/cargo-home/config.toml:ro",
);
command
}
fn run(
&self,
mut command: Command,
script: &'static str,
) -> Result<CommandDiagnostics, RustPodmanError> {
let output = command
.arg("--")
.arg(&self.image)
.args(["sh", "-c", script])
.output()
.map_err(RustPodmanError::Spawn)?;
let diagnostics = CommandDiagnostics {
status: output.status,
stdout: output.stdout,
stderr: output.stderr,
};
if diagnostics.status.success() {
Ok(diagnostics)
} else {
Err(RustPodmanError::CommandFailed(diagnostics))
}
}
}
struct ResolvedPaths {
workspace: PathBuf,
cargo_home: PathBuf,
target: PathBuf,
local_registry: PathBuf,
cargo_config: PathBuf,
}
impl ResolvedPaths {
fn new(paths: &RustPodmanPaths) -> Result<Self, RustPodmanError> {
let roots = [
canonical_dir(&paths.workspace, "workspace")?,
canonical_dir(&paths.cargo_home, "cargo_home")?,
canonical_dir(&paths.target, "target")?,
canonical_dir(&paths.local_registry, "local_registry")?,
];
for left in 0..roots.len() {
for right in left + 1..roots.len() {
if roots[left].starts_with(&roots[right]) || roots[right].starts_with(&roots[left])
{
return Err(invalid("paths", "directory roots overlap"));
}
}
}
ordinary_file(&paths.cargo_config).map_err(RustPodmanError::File)?;
let cargo_config = fs::canonicalize(&paths.cargo_config)
.map_err(|source| file("canonicalize", &paths.cargo_config, source))?;
Ok(Self {
workspace: roots[0].clone(),
cargo_home: roots[1].clone(),
target: roots[2].clone(),
local_registry: roots[3].clone(),
cargo_config,
})
}
}
fn require_nonempty(value: &OsStr, field: &'static str) -> Result<(), RustPodmanError> {
if value.is_empty() {
Err(invalid(field, "must not be empty"))
} else {
Ok(())
}
}
fn valid_binary(value: &str) -> Result<(), RustPodmanError> {
let bytes = value.as_bytes();
let alphanumeric = |byte: &u8| byte.is_ascii_lowercase() || byte.is_ascii_digit();
let valid = (1..=36).contains(&bytes.len())
&& alphanumeric(&bytes[0])
&& alphanumeric(&bytes[bytes.len() - 1])
&& bytes.iter().all(|byte| alphanumeric(byte) || *byte == b'-')
&& !bytes.windows(2).any(|pair| pair == b"--");
if valid {
Ok(())
} else {
Err(invalid(
"binary",
"must be lowercase kebab-case of length 1-36",
))
}
}
fn canonical_dir(path: &Path, field: &'static str) -> Result<PathBuf, RustPodmanError> {
let metadata = fs::symlink_metadata(path).map_err(|source| file("metadata", path, source))?;
if metadata.file_type().is_symlink() || !metadata.is_dir() {
return Err(invalid(field, "must be an ordinary nonsymlink directory"));
}
fs::canonicalize(path).map_err(|source| file("canonicalize", path, source))
}
fn ordinary_file(path: &Path) -> Result<(), FileFailure> {
let metadata =
fs::symlink_metadata(path).map_err(|source| failure("metadata", path, source))?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(failure(
"validate ordinary file",
path,
io::Error::new(
io::ErrorKind::InvalidInput,
"not an ordinary nonsymlink file",
),
));
}
Ok(())
}
fn output_location(path: &Path) -> Result<PathBuf, RustPodmanError> {
let name = path
.file_name()
.ok_or_else(|| invalid("output", "must name a file"))?;
let parent = path
.parent()
.filter(|value| !value.as_os_str().is_empty())
.unwrap_or(Path::new("."));
let parent = canonical_dir(parent, "output parent")?;
Ok(parent.join(name))
}
fn validate_output(path: &Path) -> Result<(), FileFailure> {
match fs::symlink_metadata(path) {
Ok(metadata) if !metadata.file_type().is_symlink() && metadata.is_file() => Ok(()),
Ok(_) => Err(failure(
"validate output",
path,
io::Error::new(
io::ErrorKind::InvalidInput,
"not absent or an ordinary nonsymlink file",
),
)),
Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(failure("metadata", path, source)),
}
}
fn volume(command: &mut Command, host: &Path, destination: &str) {
let mut value = host.as_os_str().to_os_string();
value.push(":");
value.push(destination);
command.arg("--volume").arg(value);
}
fn invalid(field: &'static str, reason: impl Into<String>) -> RustPodmanError {
RustPodmanError::InvalidInput {
field,
reason: reason.into(),
}
}
fn failure(operation: &'static str, path: &Path, source: io::Error) -> FileFailure {
FileFailure {
operation,
path: path.to_path_buf(),
source,
}
}
fn file(operation: &'static str, path: &Path, source: io::Error) -> RustPodmanError {
RustPodmanError::File(failure(operation, path, source))
}
fn after(diagnostics: CommandDiagnostics, failure: FileFailure) -> RustPodmanError {
RustPodmanError::AfterCommand {
diagnostics,
failure,
}
}
fn after_file(
diagnostics: CommandDiagnostics,
operation: &'static str,
path: &Path,
source: io::Error,
) -> RustPodmanError {
after(diagnostics, failure(operation, path, source))
}
struct StageDir {
path: PathBuf,
armed: bool,
}
impl StageDir {
fn create(parent: &Path, output_name: &OsStr) -> Result<Self, RustPodmanError> {
let mut name = OsString::from(".");
name.push(output_name);
name.push(format!(".k1-stage-{}", std::process::id()));
let path = parent.join(name);
match fs::symlink_metadata(&path) {
Ok(metadata) if !metadata.file_type().is_symlink() && metadata.is_dir() => {
fs::remove_dir_all(&path)
.map_err(|source| file("remove stale stage directory", &path, source))?;
}
Ok(_) => {
return Err(file(
"validate stage directory",
&path,
io::Error::new(
io::ErrorKind::InvalidInput,
"not an ordinary nonsymlink directory",
),
));
}
Err(source) if source.kind() == io::ErrorKind::NotFound => {}
Err(source) => return Err(file("metadata", &path, source)),
}
DirBuilder::new()
.mode(0o700)
.create(&path)
.map_err(|source| file("create stage directory", &path, source))?;
if let Err(source) = fs::set_permissions(&path, fs::Permissions::from_mode(0o700)) {
let _ = fs::remove_dir(&path);
return Err(file("set stage permissions", &path, source));
}
Ok(Self { path, armed: true })
}
fn cleanup(&mut self) -> io::Result<()> {
match fs::remove_dir_all(&self.path) {
Ok(()) => {
self.armed = false;
Ok(())
}
Err(source) if source.kind() == io::ErrorKind::NotFound => {
self.armed = false;
Ok(())
}
Err(source) => Err(source),
}
}
}
impl Drop for StageDir {
fn drop(&mut self) {
if self.armed {
let _ = self.cleanup();
if self.armed {
let _ = self.cleanup();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binary_names_are_strict() {
for valid in [
"a",
"1",
"a1",
"one-binary",
"a23456789012345678901234567890123456",
] {
assert!(valid_binary(valid).is_ok(), "{valid}");
}
for invalid_name in [
"",
"A",
"-a",
"a-",
"a--b",
"a_b",
"a234567890123456789012345678901234567",
] {
assert!(valid_binary(invalid_name).is_err(), "{invalid_name}");
}
}
}