Skip to content

Commit

Permalink
Merge pull request #401 from boinkor-net/use-preflight-safety
Browse files Browse the repository at this point in the history
Infer the preflight activation script from preroll-safety's default
  • Loading branch information
antifuchs authored Nov 22, 2024
2 parents b1d2486 + 783057a commit 61292b4
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 13 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ What `deploy-flake` does that I think are advantages over `deploy-rs`:

* Better system activation story: The flake configuration is first applied via `nixos-rebuild test`, and only if that works, added to the boot entries via the equivalent of `nixos-rebuild boot`.

* Optional system closure self-check script: If you use `system.extraSystemBuilderCmds` to write a self-test program into your system closure, `deploy-flake` can optionally invoke it via the `--pre-activate-script=relative-pathname` option and will not kick off a deploy on the machine if that program returns a non-0 status code.
* Optional system closure self-check script: If you use `system.extraSystemBuilderCmds` to write a self-test program into your system closure, `deploy-flake` can optionally invoke it via the `--pre-activate-script=relative-pathname` option and will not kick off a deploy on the machine if that program returns a non-0 status code. If your system configuration uses [preroll-safety](https://github.com/boinkor-net/preroll-safety), the script it emits by default is detected and used for this self-check automatically.

* Nicer story around running the test process in the background: It uses `systemd-run` to spawn the activation as a systemd unit, which will allow the control process to get disconnected at any point in time & the deployment can continue.

Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ impl SystemConfiguration {
}

#[instrument(level="DEBUG", skip(self) err)]
pub async fn preflight_check_closure(&self, script: &Path) -> Result<(), anyhow::Error> {
pub async fn preflight_check_closure(
&self,
script: Option<&Path>,
) -> Result<(), anyhow::Error> {
self.system
.preflight_check_closure(&self.path, script)
.await
Expand Down
13 changes: 7 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,10 @@ struct Opts {

/// A program contained in the new system closure, run on the
/// system being deployed, that checks whether the system closure
/// is deployable. This program should be created with
/// `system.extraSystemBuilderCmds` for NixOS.
/// is deployable. This program can be created with
/// `system.extraSystemBuilderCmds` for NixOS. See the
/// https://github.com/boinkor-net/preroll-safety library for an
/// example of pre-activation safety checks.
#[clap(long, require_equals = true, value_name = "PROGRAM")]
pre_activate_script: Option<PathBuf>,

Expand Down Expand Up @@ -213,10 +215,9 @@ async fn deploy(
log::event!(log::Level::DEBUG, dest=?destination.hostname, "Skipping system health check");
}

if let Some(script) = pre_activate_script {
log::event!(log::Level::INFO, dest=?destination.hostname, script=?script, "Running pre-activation script");
built.preflight_check_closure(&script).await?;
}
built
.preflight_check_closure(pre_activate_script.as_deref())
.await?;

if do_test == Behavior::Run {
log::event!(log::Level::DEBUG, configuration=?built.configuration(), system_name=?built.for_system(), "Testing");
Expand Down
2 changes: 1 addition & 1 deletion src/os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub(crate) trait NixOperatingSystem: fmt::Debug {
async fn preflight_check_closure(
&self,
derivation: &Path,
script: &Path,
script: Option<&Path>,
) -> Result<(), anyhow::Error>;

/// Builds a system configuration closure from the flake and
Expand Down
39 changes: 35 additions & 4 deletions src/os/nixos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub struct Nixos {
session: openssh::Session,
}

pub const DEFAULT_PREFLIGHT_SCRIPT_NAME: &str = "pre-activate-safety-checks";

fn strip_shell_output(output: Output) -> String {
let len = &output.stdout.len();
let last_byte = output.stdout[len - 1];
Expand Down Expand Up @@ -75,6 +77,25 @@ impl Nixos {
Ok(strip_shell_output(output))
}

#[instrument(level = "DEBUG", fields(pathname), err)]
async fn test_file_existence<'s>(&self, path: &Path) -> Result<bool, anyhow::Error> {
let mut cmd = self.session.command("test");
cmd.arg("-f").raw_arg(path);
cmd.stdout(Stdio::null())
.stderr(Stdio::piped())
.stdin(Stdio::inherit());
log::event!(log::Level::DEBUG, command=?cmd, "Running");
let mut child = cmd.spawn().await?;
let stderr_read = tokio::task::spawn(
read_and_log_messages("E", child.stderr().take().unwrap())
.instrument(log::Span::current()),
);
let status = futures::join!(child.wait(), stderr_read);
let exit_status = status.0?;
log::event!(log::Level::DEBUG, command=?cmd, ?exit_status, "Finished");
Ok(exit_status.success())
}

#[instrument(level = "DEBUG", fields(cmd), err)]
async fn run_command<'s>(&self, mut cmd: Command<'s>) -> Result<(), anyhow::Error> {
cmd.stdout(Stdio::piped())
Expand Down Expand Up @@ -143,12 +164,22 @@ impl NixOperatingSystem for Nixos {
async fn preflight_check_closure(
&self,
derivation: &Path,
script: &Path,
script: Option<&Path>,
) -> Result<(), anyhow::Error> {
let script_path = derivation.join(script);

let script_path = if script.is_none() {
// Try to use the default pre-activation script name emitted by preflight-safety:
let script_path = derivation.join(DEFAULT_PREFLIGHT_SCRIPT_NAME);
log::event!(log::Level::DEBUG, dest=?self.host, script=?script_path.file_name(), "Checking for existence of inferred pre-activation script");
if !self.test_file_existence(&script_path).await? {
return Ok(());
}
script_path
} else {
derivation.join(script.unwrap())
};
log::event!(log::Level::INFO, dest=?self.host, script=?script_path.file_name(), "Running pre-activation script");
let mut cmd = self.session.command("sudo");
cmd.arg(script_path.to_string_lossy());
cmd.raw_arg(script_path);
self.run_command(cmd)
.await
.context("System closure self-checks failed")?;
Expand Down

0 comments on commit 61292b4

Please sign in to comment.