#[cfg(unix)]
use super::{DELIVERED, KEPT_DIR, from_environment, host_environment};
use super::{compose_variable, of_containers, place, receive, step, wait};
use crate::testing;
use isoloom_plan::SecretFile;
use isoloom_spec::model::Target;
const LAB: &str = r"
version: 1
name: s
inputs: [FLAG]
secrets:
api-key: { env: API_KEY }
db-password: { env: DB_PASSWORD }
networks: { lan: { cidr: 10.9.0.0/24 } }
machines:
web:
networks: { lan: 10 }
app: { image: nginx }
files: [{ path: /run/secrets/db, secret: db-password }, { path: /flag, input: FLAG }]
vm:
networks: { lan: 11 }
os: windows-server@2022
files: [{ path: 'C:\key', secret: api-key }]
";
fn file(name: &str, path: &str, uid: Option<u32>, mode: Option<u32>) -> SecretFile {
SecretFile {
source: isoloom_plan::ValueSource::Secret(name.to_owned()),
name: name.into(),
path: path.into(),
uid,
gid: None,
mode,
}
}
#[cfg(unix)]
fn run(script: &str, input: &str, tag: &str) -> (std::process::Output, std::path::PathBuf) {
use std::io::Write as _;
let dir = std::env::temp_dir().join(format!("isoloom-secrets-{tag}-{}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
let script = script.replace(KEPT_DIR, &dir.display().to_string());
let mut child = std::process::Command::new("sh")
.arg("-c")
.arg(script)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(input.as_bytes()).unwrap();
(child.wait_with_output().unwrap(), dir)
}
#[cfg(unix)]
fn b64(s: &str) -> String {
let out = std::process::Command::new("sh")
.arg("-c")
.arg("printf '%s' \"$V\" | base64 | tr -d '\\n'")
.env("V", s)
.output()
.unwrap();
String::from_utf8(out.stdout).unwrap()
}
#[cfg(unix)]
#[test]
fn delivery_keeps_each_value_as_sent_then_marks_it() {
let names = vec!["db-password".to_owned(), "api-key".to_owned(), "empty".to_owned()];
let values = ["p@ss 'word'\n", "line1\nline2", ""];
let input: String = values.iter().map(|v| b64(v) + "\n").collect();
let (out, dir) = run(&receive(&names, &[]), &input, "receive");
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
for (n, v) in names.iter().zip(values) {
assert_eq!(std::fs::read_to_string(dir.join(n)).unwrap(), v);
}
assert!(dir.join(".delivered").exists());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
assert_eq!(std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777, 0o700);
assert_eq!(std::fs::metadata(dir.join("api-key")).unwrap().permissions().mode() & 0o777, 0o600);
}
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(unix)]
#[test]
fn a_missing_value_fails_the_delivery_without_the_mark() {
let names = vec!["a".to_owned(), "b".to_owned()];
let (out, dir) = run(&receive(&names, &[]), &format!("{}\n", b64("x")), "missing");
assert!(!out.status.success());
assert!(!dir.join(".delivered").exists());
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn no_value_is_in_a_script() {
let files = [file("db", "/etc/app/db", Some(1000), Some(0o400))];
let sh = receive(&["db".to_owned()], &files);
assert!(sh.contains("install -D -o 1000 -g 0 -m 0400 /var/lib/isoloom/secrets/db '/etc/app/db'"), "{sh}");
assert_eq!(
place(&[file("x", "/run/secrets/x", None, None)]),
"install -D -o 0 -g 0 -m 0400 /var/lib/isoloom/secrets/x '/run/secrets/x'\n"
);
}
#[test]
fn the_step_waits_then_places_at_every_boot() {
let sh = step("web", &[file("db", "/run/secrets/db", None, None)]);
assert!(sh.starts_with("set -e\nn=0; until [ -f /var/lib/isoloom/secrets/.delivered ]"), "{sh}");
assert!(sh.contains("systemctl enable isoloom-apply-secrets.service"), "{sh}");
assert!(sh.contains("install -D -o 0 -g 0 -m 0400"), "{sh}");
let check = std::process::Command::new("sh").arg("-n").arg("-c").arg(&sh).status().unwrap();
assert!(check.success());
}
#[cfg(unix)]
#[test]
fn the_wait_passes_once_delivered() {
let (out, dir) = run(&format!("mkdir -p {KEPT_DIR} && touch {DELIVERED} && {}", wait("web")), "", "wait");
assert!(out.status.success());
assert!(out.stdout.is_empty(), "no waiting line once delivered");
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(unix)]
#[test]
fn a_docker_host_reads_each_kept_value_into_its_variable() {
let spec = testing::spec(LAB);
assert_eq!(host_environment(&spec, &[]), "");
let names = vec!["db-password".to_owned()];
let script = format!(
"mkdir -p {KEPT_DIR} && printf 'v\\n\\n' > {KEPT_DIR}/db-password && touch {DELIVERED}\n{}printf '%s|' \"$ISOLOOM_SECRET_DB_PASSWORD\"",
host_environment(&spec, &names)
);
let (out, dir) = run(&script, "", "host");
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
assert_eq!(String::from_utf8_lossy(&out.stdout), "v\n\n|");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn the_wait_gives_up_after_half_an_hour_of_five_second_tries() {
let sh = wait("web");
assert!(sh.contains("if [ \"$n\" -gt 360 ]; then echo"), "{sh}");
assert!(sh.ends_with("sleep 5; done\n"), "{sh}");
assert!(sh.contains("'web: waiting for its secrets, which the runner (isothrum up) delivers'"), "{sh}");
assert!(!sh.contains('`'), "no backquote, which shellcheck reads as an expansion left unexpanded: {sh}");
}
#[test]
fn a_docker_host_gets_the_secrets_of_its_containers_in_the_spec_s_order() {
let env = testing::env(LAB, Target::Hybrid);
assert_eq!(of_containers(&env), ["db-password", "input-flag"]);
assert_eq!(compose_variable(env.spec(), "db-password"), "ISOLOOM_SECRET_DB_PASSWORD");
assert_eq!(compose_variable(env.spec(), "input-flag"), "FLAG", "an input's file reads the input itself");
}
#[cfg(unix)]
#[test]
fn values_from_the_environment_are_kept_and_an_empty_one_keeps_the_kept_value() {
let script = from_environment(&[("db".to_owned(), "PW_B64".to_owned()), ("api".to_owned(), "API_B64".to_owned())], &[]);
let (keeping, placing) = script.split_once(&format!("touch {DELIVERED}\n")).expect("the mark");
assert!(placing.contains("systemctl enable isoloom-apply-secrets.service"), "{placing}");
let first = format!("export PW_B64={}; {keeping}touch {DELIVERED}\n", b64("p@ss 'word'\n"));
let (fresh, fresh_dir) = run(&first, "", "env-first");
assert!(fresh.status.success(), "{}", String::from_utf8_lossy(&fresh.stderr));
assert_eq!(std::fs::read_to_string(fresh_dir.join("db")).unwrap(), "p@ss 'word'\n");
let unset = std::fs::read_to_string(fresh_dir.join("api")).unwrap();
assert_eq!(unset, "", "an unset variable writes an empty value the first time");
assert!(fresh_dir.join(".delivered").exists());
std::fs::remove_dir_all(&fresh_dir).ok();
let again = format!("mkdir -p {KEPT_DIR} && printf kept > {KEPT_DIR}/db && {keeping}");
let (kept, kept_dir) = run(&again, "", "env-again");
assert!(kept.status.success(), "{}", String::from_utf8_lossy(&kept.stderr));
assert_eq!(std::fs::read_to_string(kept_dir.join("db")).unwrap(), "kept");
std::fs::remove_dir_all(&kept_dir).ok();
}