use std::ffi::{OsStr, OsString};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::repository::copy_repository;
use crate::{CheckResult, CheckStage, CheckStageResult, Error, Result};
const CONTAINERFILE: &str = include_str!("../Containerfile");
static RUN_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) fn check_repository(repository: &Path, work_root: &Path) -> Result<CheckResult> {
check_repository_with(repository, work_root, OsStr::new("podman"), &tool_image())
}
fn check_repository_with(
repository: &Path,
work_root: &Path,
podman: &OsStr,
image: &str,
) -> Result<CheckResult> {
ensure_image(work_root, podman, image)?;
let run = RunDirectory::new(repository, work_root, "check")?;
let mut stages = Vec::new();
let specifications: &[(CheckStage, &[&str], bool)] = &[
(CheckStage::Fetch, &["fetch", "--color", "never"], true),
(
CheckStage::Format,
&["fmt", "--all", "--", "--check"],
false,
),
(
CheckStage::Build,
&[
"build",
"--workspace",
"--all-targets",
"--all-features",
"--locked",
"--offline",
"--color",
"never",
],
false,
),
(
CheckStage::Clippy,
&[
"clippy",
"--workspace",
"--all-targets",
"--all-features",
"--locked",
"--offline",
"--color",
"never",
"--",
"-D",
"warnings",
],
false,
),
(
CheckStage::Test,
&[
"test",
"--workspace",
"--all-targets",
"--all-features",
"--locked",
"--offline",
"--no-fail-fast",
"--color",
"never",
],
false,
),
(
CheckStage::DocTest,
&[
"test",
"--workspace",
"--all-features",
"--doc",
"--locked",
"--offline",
"--no-fail-fast",
"--color",
"never",
],
false,
),
];
for &(stage, arguments, network) in specifications {
let result = run_stage(podman, image, &run, stage, arguments, network)?;
let passed = result.passed();
stages.push(result);
if !passed {
break;
}
}
Ok(CheckResult { stages })
}
fn run_stage(
podman: &OsStr,
image: &str,
run: &RunDirectory,
stage: CheckStage,
arguments: &[&str],
network: bool,
) -> Result<CheckStageResult> {
let mut command = podman_run_command(podman, run, "/workspace", network);
command.arg(image).arg("cargo").args(arguments);
let output = command.output().map_err(|source| Error::Sandbox {
stage: stage.name().to_owned(),
message: format!("could not start {podman:?}: {source}"),
})?;
if matches!(output.status.code(), Some(125..=127)) {
return Err(Error::Sandbox {
stage: stage.name().to_owned(),
message: command_failure("Podman validation container", &output),
});
}
Ok(CheckStageResult {
stage,
command: format!("cargo {}", arguments.join(" ")),
exit_code: output.status.code(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}
pub(crate) fn publish_repository(
repository: &Path,
work_root: &Path,
registry_token: &str,
) -> Result<()> {
publish_repository_with(
repository,
work_root,
registry_token,
OsStr::new("podman"),
&tool_image(),
)
}
fn publish_repository_with(
repository: &Path,
work_root: &Path,
registry_token: &str,
podman: &OsStr,
image: &str,
) -> Result<()> {
ensure_image(work_root, podman, image)?;
let run = RunDirectory::new(repository, work_root, "publish")?;
let mut command = podman_run_command(podman, &run, "/tmp", true);
command
.env("CARGO_REGISTRY_TOKEN", registry_token)
.arg("--env=CARGO_REGISTRY_TOKEN")
.arg(image)
.arg("cargo")
.arg("--config")
.arg("registry.global-credential-providers=[\"cargo:token\"]")
.arg("publish")
.arg("--manifest-path=/workspace/Cargo.toml")
.arg("--registry=crates-io")
.arg("--no-verify")
.arg("--color=never");
let output = command.output().map_err(|source| Error::Sandbox {
stage: "publish".to_owned(),
message: format!("could not start {podman:?}: {source}"),
})?;
if matches!(output.status.code(), Some(125..=127)) {
return Err(Error::Sandbox {
stage: "publish".to_owned(),
message: redact_secret(
command_failure("Podman publication container", &output),
registry_token,
),
});
}
if !output.status.success() {
return Err(Error::Publish(redact_secret(
command_failure("cargo publish", &output),
registry_token,
)));
}
Ok(())
}
fn podman_run_command(podman: &OsStr, run: &RunDirectory, workdir: &str, network: bool) -> Command {
let mut command = Command::new(podman);
command
.arg("run")
.arg("--rm")
.arg("--read-only")
.arg("--cap-drop=all")
.arg("--security-opt=no-new-privileges")
.arg("--userns=keep-id")
.arg("--tmpfs=/tmp:rw,nosuid,nodev")
.arg("--workdir")
.arg(workdir)
.arg("--env=CARGO_HOME=/cargo-home")
.arg("--env=CARGO_TARGET_DIR=/target")
.arg("--env=CARGO_TERM_COLOR=never");
if !network {
command.arg("--network=none");
}
command
.arg("--pull=never")
.arg("--volume")
.arg(volume_spec(&run.workspace, "/workspace"))
.arg("--volume")
.arg(volume_spec(&run.cargo_home, "/cargo-home"))
.arg("--volume")
.arg(volume_spec(&run.target, "/target"));
command
}
fn volume_spec(source: &Path, destination: &str) -> OsString {
let mut value = source.as_os_str().to_os_string();
value.push(":");
value.push(destination);
value.push(":rw,Z");
value
}
fn tool_image() -> String {
let mut hash = 0xcbf29ce484222325_u64;
for byte in CONTAINERFILE.bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100000001b3);
}
format!(
"localhost/kcode-rust-libs-v2-toolchain:{}-{hash:016x}",
env!("CARGO_PKG_VERSION")
)
}
fn ensure_image(work_root: &Path, podman: &OsStr, image: &str) -> Result<()> {
let inspect = Command::new(podman)
.arg("image")
.arg("exists")
.arg(image)
.output()
.map_err(|source| Error::Sandbox {
stage: "prepare-image".to_owned(),
message: format!("could not start {podman:?}: {source}"),
})?;
if inspect.status.success() {
return Ok(());
}
if inspect.status.code() != Some(1) {
return Err(Error::Sandbox {
stage: "prepare-image".to_owned(),
message: command_failure("Podman image inspection", &inspect),
});
}
let builds = work_root.join("image-builds");
fs::create_dir_all(&builds)
.map_err(|source| Error::io("create image-build root", &builds, source))?;
let build = TemporaryDirectory::new(&builds, "build")?;
let containerfile = build.path().join("Containerfile");
fs::write(&containerfile, CONTAINERFILE)
.map_err(|source| Error::io("write embedded Containerfile", &containerfile, source))?;
let output = Command::new(podman)
.arg("build")
.arg("--tag")
.arg(image)
.arg("--file")
.arg(&containerfile)
.arg(build.path())
.output()
.map_err(|source| Error::Sandbox {
stage: "prepare-image".to_owned(),
message: format!("could not start {podman:?}: {source}"),
})?;
if !output.status.success() {
return Err(Error::Sandbox {
stage: "prepare-image".to_owned(),
message: command_failure("Podman image build", &output),
});
}
Ok(())
}
struct RunDirectory {
_root: TemporaryDirectory,
workspace: PathBuf,
cargo_home: PathBuf,
target: PathBuf,
}
impl RunDirectory {
fn new(repository: &Path, work_root: &Path, label: &str) -> Result<Self> {
let runs = work_root.join("runs");
fs::create_dir_all(&runs).map_err(|source| Error::io("create run root", &runs, source))?;
let root = TemporaryDirectory::new(&runs, label)?;
let workspace = root.path().join("workspace");
let cargo_home = root.path().join("cargo-home");
let target = root.path().join("target");
copy_repository(repository, &workspace)?;
fs::create_dir(&cargo_home)
.map_err(|source| Error::io("create Cargo home", &cargo_home, source))?;
fs::create_dir(&target)
.map_err(|source| Error::io("create target directory", &target, source))?;
Ok(Self {
_root: root,
workspace,
cargo_home,
target,
})
}
}
struct TemporaryDirectory(PathBuf);
impl TemporaryDirectory {
fn new(parent: &Path, label: &str) -> Result<Self> {
for _ in 0..100 {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let counter = RUN_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = parent.join(format!(
"{label}-{}-{timestamp}-{counter}",
std::process::id()
));
match fs::create_dir(&path) {
Ok(()) => return Ok(Self(path)),
Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => continue,
Err(source) => {
return Err(Error::io("create temporary directory", path, source));
}
}
}
Err(Error::Sandbox {
stage: "prepare-workspace".to_owned(),
message: "could not allocate a unique temporary directory".to_owned(),
})
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TemporaryDirectory {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
fn command_failure(operation: &str, output: &std::process::Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let detail = if stderr.trim().is_empty() {
stdout.trim()
} else {
stderr.trim()
};
format!(
"{operation} exited with status {}; {detail}",
output
.status
.code()
.map_or_else(|| "signal".to_owned(), |code| code.to_string())
)
}
fn redact_secret(message: String, secret: &str) -> String {
message.replace(secret, "[REDACTED]")
}
#[cfg(all(test, unix))]
mod tests {
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use super::{TemporaryDirectory, check_repository_with, publish_repository_with};
struct Fixture {
root: TemporaryDirectory,
}
impl Fixture {
fn new(label: &str) -> Self {
let parent = std::env::temp_dir();
let root = TemporaryDirectory::new(&parent, label).unwrap();
Self { root }
}
fn path(&self) -> &Path {
self.root.path()
}
fn repository(&self) -> PathBuf {
let repository = self.path().join("repository");
fs::create_dir_all(repository.join("src")).unwrap();
fs::write(
repository.join("Cargo.toml"),
"[package]\nname = \"test\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
fs::write(repository.join("Cargo.lock"), "version = 4\n").unwrap();
fs::write(repository.join("Documentation.md"), "docs\n").unwrap();
fs::write(repository.join("src/lib.rs"), "").unwrap();
repository
}
fn fake_podman(&self, script: &str) -> PathBuf {
let path = self.path().join("fake-podman");
fs::write(&path, script).unwrap();
let mut permissions = fs::metadata(&path).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&path, permissions).unwrap();
path
}
}
#[test]
fn runs_the_six_fixed_stages_in_disposable_workspaces() {
let fixture = Fixture::new("pipeline");
let log = fixture.path().join("podman.log");
let script = format!(
"#!/bin/sh\nif [ \"$1\" = image ]; then exit 0; fi\nprintf '%s\\n' \"$*\" >> '{}'\nexit 0\n",
log.display()
);
let podman = fixture.fake_podman(&script);
let work = fixture.path().join("work");
fs::create_dir(&work).unwrap();
let result = check_repository_with(
&fixture.repository(),
&work,
podman.as_os_str(),
"test-image",
)
.unwrap();
assert!(result.passed());
assert_eq!(fs::read_to_string(log).unwrap().lines().count(), 6);
assert!(fs::read_dir(work.join("runs")).unwrap().next().is_none());
}
#[test]
fn publication_keeps_the_token_off_the_command_line_and_redacts_errors() {
let fixture = Fixture::new("publish");
let podman = fixture.fake_podman(
"#!/bin/sh\nif [ \"$1\" = image ]; then exit 0; fi\ncase \"$*\" in *publish-secret*) exit 91 ;; esac\necho \"rejected $CARGO_REGISTRY_TOKEN\" >&2\nexit 1\n",
);
let work = fixture.path().join("work");
fs::create_dir(&work).unwrap();
let error = publish_repository_with(
&fixture.repository(),
&work,
"publish-secret",
podman.as_os_str(),
"test-image",
)
.unwrap_err();
let message = error.to_string();
assert!(!message.contains("publish-secret"));
assert!(message.contains("[REDACTED]"));
}
}