1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//! 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<String> = 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
}