~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

knotless-run: A container that fails to start leaves its log

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:run/start-failure-log
crates/knotless-run/src/executor.rs+3−1
@@ -352,13 +352,15 @@ async fn container(
// Kept running until the run ends, for each step to be run in.
cmd.args([image, "tail", "-f", "/dev/null"])
.stdin(Stdio::null())
.stderr(log.try_clone()?)
.kill_on_drop(true);
let started = tokio::time::timeout(timeout, cmd.output()).await;
let out = match started {
Ok(out) => out.context("starting a container")?,
Err(_) => bail!("{image} did not start in {}s", timeout.as_secs()),
};
// Taking its output pipes what docker says too, whatever the command
// was given: it goes to the log here.
std::io::Write::write_all(&mut &*log, &out.stderr)?;
if !out.status.success() {
bail!("{image} did not start: see the log");
}
crates/knotless-run/src/lib.rs+14−1
@@ -2974,7 +2974,20 @@ impl Runner {
};
let timeout = self.opts.step_timeout;
let _tracked = Tracked::new(self, setup.owner.run);
let session = self.opts.executor.start(&setup, &log, timeout).await?;
// A start that fails says so in the log its error points at, and
// the log is kept before the check is filed as an error.
let session = match self.opts.executor.start(&setup, &log, timeout).await {
Ok(session) => session,
Err(err) => {
// The start's error is what the check says, whatever
// keeping the log comes to.
let _ = log.write_all(format!("==> {err:#}\n").as_bytes());
let text = std::fs::read(&log_path).unwrap_or_default();
let text = check::redact(&text, &secrets);
check.log = self.reporter.upload_log(&text).await.ok();
return Err(err);
}
};
// A run on its schedule is held to the runner's limits, as one
// that recurs on its own: only its owner's own ask goes past them.
let scheduled = given(subject, wf).any(|(k, _)| k == SCHEDULE_VAR);
crates/knotless-run/tests/run.rs+57−0
@@ -897,6 +897,63 @@ fn check_on(runner: &Sandbox, owner: &Sandbox, key: &str, r: &str, wf: &str) ->
.unwrap_or_default()
}
/// A container that fails to start leaves its log, with docker's words and
/// the runner's, where the check's error says to look.
#[test]
fn a_start_that_fails_leaves_its_log() {
let owner = Sandbox::new();
let mut runner = Sandbox::new();
let handle = runner.account.handle.clone();
runner.set("KNOTLESS_IDENTIFIER", &handle);
// A docker that refuses everything, found before any other.
let bin = runner.path("bin");
std::fs::create_dir_all(&bin).unwrap();
let fake = bin.join("docker");
std::fs::write(
&fake,
"#!/bin/sh\necho 'docker: no space left' >&2\nexit 1\n",
)
.unwrap();
std::fs::set_permissions(&fake, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap();
let path = format!("{}:{}", bin.display(), std::env::var("PATH").unwrap());
// Only a prefix of the path, so the other tests here see no change.
unsafe { std::env::set_var("PATH", path) };
// The repository runner_opts watches.
let work = owner.work("proj", 1);
owner.git(&work, &["remote", "add", "origin", &owner.url("proj")]);
owner.git(&work, &["push", "-q", "origin", "main"]);
let (frames, _) = tokio::sync::mpsc::unbounded_channel::<Message>();
let js = Jetstream {
url: "ws://127.0.0.1:9".into(),
frames,
};
let mut opts = runner_opts(&runner, &owner, &js);
opts.executor = Executor::parse("docker", "alpine").unwrap();
let rt = tokio::runtime::Runtime::new().unwrap();
let (run, watches) = rt
.block_on(knotless_run::Runner::start(opts, common::ctx(&runner)))
.unwrap();
let watch = &watches[0];
// The first pass only learns where the refs are: the push after it runs.
rt.block_on(run.reconcile(watch)).unwrap();
let dir = work.join(".knotless/workflows");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("boot.yml"), "steps: [{run: 'true'}]\n").unwrap();
owner.git(&work, &["add", "."]);
owner.git(&work, &["commit", "-q", "-m", "workflow"]);
owner.git(&work, &["push", "-q", "origin", "main"]);
rt.block_on(run.reconcile(watch)).unwrap();
rt.block_on(run.idle());
let boot = check_on(&runner, &owner, &watch.key, "refs/heads/main", "boot");
assert_eq!(boot["status"], "error", "{boot}");
let log = blob(
&runner.account.did,
boot["log"]["ref"]["$link"].as_str().unwrap(),
);
assert!(log.contains("docker: no space left"), "{log}");
assert!(log.contains("see the log"), "{log}");
}
#[test]
fn what_goes_wrong_is_reported() {
let owner = Sandbox::new();
docs/design/runner.md+2−1
@@ -570,7 +570,8 @@ exec` is given the secrets that step may see, the same way, so the
container never holds one and a step cannot read another's from
`/proc/1/environ`. Starting the container, image pull included, writes
to the run's log and is limited to `--step-timeout`. A container that does
not start makes the check `error`.
not start makes the check `error`, and the runner ends the log with why
and uploads it, so the check has a log to read.
Under `shell`, each step is `sh -ec <script>` in the checkout. When the
run ends, the container is removed, which kills anything a step