//! Runs the executor end to end against a fake `kubectl`, so the command //! sequence is checked without a cluster. use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; use knotless_runner_k8s::cli::{self, Command}; use knotless_runner_k8s::runner; #[test] fn runs_steps_through_kubectl() { let root = temp_dir(); let work = root.join("work"); fs::create_dir_all(&work).unwrap(); fs::write(work.join("hello.txt"), "hello\n").unwrap(); let steps = root.join("steps.tsv"); fs::write(&steps, "build\tcat hello.txt\ntest\ttest -f hello.txt\n").unwrap(); let fake = root.join("kubectl"); fs::write( &fake, r#"#!/bin/sh printf '%s\n' "$*" >> "$FAKE_KUBECTL_LOG" case "$*" in *" apply "*) cat >/dev/null ;; *" get pods "*) printf 'pod-1' ;; *" exec "*) case "$*" in *"tar -xf"*) cat >/dev/null ;; esac ;; esac exit 0 "#, ) .unwrap(); let mut mode = fs::metadata(&fake).unwrap().permissions(); mode.set_mode(0o755); fs::set_permissions(&fake, mode).unwrap(); let log = root.join("kubectl.log"); let result = root.join("result.json"); let run_log = root.join("run.log"); let args: Vec = vec![ "run".into(), "--name".into(), "fake run".into(), "--image".into(), "busybox:1.36".into(), "--kubectl".into(), fake.display().to_string(), "--workdir".into(), work.display().to_string(), "--steps-file".into(), steps.display().to_string(), "--runner".into(), "did:plc:runner".into(), "--log".into(), run_log.display().to_string(), "--result".into(), result.display().to_string(), ]; let Command::Run(options) = cli::parse(&args).unwrap() else { panic!("not a run"); }; // The fake client writes here, through the environment it inherits. unsafe { std::env::set_var("FAKE_KUBECTL_LOG", &log); } let outcome = runner::run(&options).unwrap(); assert_eq!(outcome.status, "success", "{:?}", outcome.error); assert_eq!(outcome.steps.len(), 2); assert!(outcome.steps.iter().all(|s| s.status == "success")); assert!(outcome.job_deleted); assert!(result.exists()); let calls = fs::read_to_string(&log).unwrap(); for wanted in [ "apply -f -", "wait --for=condition=Ready", "get pods", "exec", "delete job", ] { assert!(calls.contains(wanted), "missing {wanted:?} in:\n{calls}"); } let logged = fs::read_to_string(&run_log).unwrap(); assert!(logged.contains("==> step build")); assert!(logged.contains("==> step test")); let _ = fs::remove_dir_all(&root); } fn temp_dir() -> PathBuf { let stamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); let dir = std::env::current_dir() .unwrap() .join("target") .join("test-tmp") .join(format!("{}-{stamp}", std::process::id())); fs::create_dir_all(&dir).unwrap(); dir }