From 02e1113cf7ca5a8a38ff5c3f5e48d4e33be57467 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 13 May 2024 16:20:03 +0200 Subject: [PATCH] `run` can now build, sign, and push a Nix attr. --- README.md | 44 ++++++++++-------- src/lib/thebacknd/client/cli.rs | 7 ++- src/lib/thebacknd/client/run.rs | 81 ++++++++++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b1ae7c7..de7baf0 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,39 @@ # Thebacknd -Thebacknd runs ephemeral virtual machines in the cloud in one command. - -Thebacknd is a proof-of-concept to run a NixOS system as a DigitalOcean virtual -machine in a single command. +Thebacknd is a proof-of-concept to run a NixOS system as an ephemeral +DigitalOcean virtual machine in a single command: ``` -$ scripts/build-toplevels.sh -/nix/store/lk6igl2f0i137q36wscfrc6n9r0jn52l-nixos-system-unnamed-23.05pre-git -$ scripts/thebacknd-run /nix/store/lk6igl2f0i137q36wscfrc6n9r0jn52l-nixos-system-unnamed-23.05pre-git +$ thebacknd run -A toplevels.example ``` -The first script is responsible to build the toplevel derivation, and also sign -its closure and push it to a binary cache. The Nix store path is then -displayed. This is pretty standard NixOS stuff; nothing new here. - -The second command takes a Nix store path to a toplevel that should exist in a -binary cache. It creates a DigitalOcean virtual machine, then switches it base -NixOs system to the given toplevel. +This builds, signs, and pushes to a binary cache the Nix attribute +`toplevels.example` defined in the `default.nix` file present in the current +working directory. It then creates a DigitalOcean virtual machine, and switches +its base NixOs system to the given toplevel. # Variants -The `scripts/thebacknd-run` can also be run without an argument. The resulting -virtual machine uses the base image without switching to a new toplevel. +``` +$ thebacknd run /nix/store/qqzn1jfjgxipzz4g4qqvv5cilk0x0hy7-nixos-system-unnamed-23.05pre-git +$ thebacknd run /nix/store/r7cylmrxj0nj2901vy33wqnfdflaf7fb-program-0.1.0/bin/program +$ thebacknd run +$ thebacknd run --A toplevels.base +$ thebacknd run --attr toplevels.base +$ thebacknd run default.nix --attr toplevels.base +``` + +The first call doesn't build or sign anything. It only takes a Nix store path +that is expected to be existing (and signed) in the binary cache, and uses it +as the desired system. + +It can also be given a path to a binary within the Nix store. Again, this uses +the base image, and it runs the binary once the machine has booted. -It can also be given a path to a binary withing the Nix store. Again, this uses -the base image. +It can also be run without an argument. The resulting virtual machine uses the +base image without switching to a new toplevel. This is similar to `thebacknd +run -A toplevels.base`, except it picks a pre-made base image from DigitalOcean +custom images, instead of building it. In the future, I'd like to experiment with loading a Nix shell, or to build a toplevel from a Git repository. In that case, building it could be done either diff --git a/src/lib/thebacknd/client/cli.rs b/src/lib/thebacknd/client/cli.rs index 66c973a..0d8379d 100644 --- a/src/lib/thebacknd/client/cli.rs +++ b/src/lib/thebacknd/client/cli.rs @@ -20,9 +20,14 @@ pub enum Commands { /// Run a toplevel or a binary in a cloud virtual machine #[derive(Parser)] pub struct RunCmd { - /// The full path to a binary or toplevel store path + /// The full path to a binary or toplevel store path, or to a Nix file (when an attribute is + /// given). pub full_path: Option, + /// The attribute path to build + #[arg(short = 'A', long)] + pub attr: Option, + /// Enable verbose output #[arg(short, long)] pub verbose: bool, diff --git a/src/lib/thebacknd/client/run.rs b/src/lib/thebacknd/client/run.rs index 02e200b..1c9aec2 100644 --- a/src/lib/thebacknd/client/run.rs +++ b/src/lib/thebacknd/client/run.rs @@ -1,12 +1,22 @@ /// Implement thebacknd CLI. use std::process::{Command as ProcessCommand, exit}; +use std::str; use regex::Regex; use super::cli::{RunCmd}; pub fn handle_run(args: &RunCmd) { - let RunCmd{ full_path, verbose } = args; - if let Some(full_path) = full_path { + let RunCmd{ full_path, attr, verbose } = args; + if let Some(attr) = attr { + let full_path = full_path.clone().unwrap_or("default.nix".to_string()); + let store_path = build_nix_attr(&full_path, &attr, *verbose); + + sign_store_path(&store_path, *verbose); + cache_store_path(&store_path, *verbose); + + let param = format!("nix_toplevel:{}", store_path); + invoke_create(Some(¶m), *verbose); + } else if let Some(full_path) = full_path { let re = Regex::new(r"(/nix/store/[^/]+)/.+").unwrap(); let store_path = re.captures(full_path) .map(|caps| caps.get(1).unwrap().as_str()) @@ -24,6 +34,73 @@ pub fn handle_run(args: &RunCmd) { } } +fn build_nix_attr(nix_file: &str, attr: &str, verbose: bool) -> String { + let mut command = ProcessCommand::new("nix-build"); + command.args(&[nix_file, "--attr", attr]); + + if verbose { + println!("Executing: {:?}", command); + } + + let output = command + .output() + .expect("Failed to execute nix-build command"); + + if !output.status.success() { + println!("Command failed with error: {}", str::from_utf8(&output.stderr).unwrap_or("Unknown error")); + exit(output.status.code().unwrap_or(1)); + } + + // Return the first line of stdout + let first_line = str::from_utf8(&output.stdout) + .unwrap_or("") + .lines() + .next() + .unwrap_or("") + .to_string(); + + if !first_line.starts_with("/nix/store") { + eprintln!("Error: Expected output to start with '/nix/store', got: {}", first_line); + exit(1); + } + + first_line +} + +fn sign_store_path(store_path: &str, verbose: bool) { + let mut command = ProcessCommand::new("nix"); + command.args(&["store", "sign", "--recursive", "--key-file", "../thebacknd/signing-keys/cache-priv-key.pem", store_path]); + + if verbose { + println!("Executing: {:?}", command); + } + + let status = command + .status() + .expect("Failed to execute nix store sign command"); + + if !status.success() { + exit(status.code().unwrap_or(1)); + } +} + +fn cache_store_path(store_path: &str, verbose: bool) { + let mut command = ProcessCommand::new("nix"); + command.args(&["copy", "--to", "s3://hypered-private-store/cache?endpoint=s3.eu-central-003.backblazeb2.com", store_path]); + + if verbose { + println!("Executing: {:?}", command); + } + + let status = command + .status() + .expect("Failed to execute nix copy --to command"); + + if !status.success() { + exit(status.code().unwrap_or(1)); + } +} + /// Call the `thebacknd/create` serverless function using the `doctl` command-line tool. fn invoke_create(param: Option<&str>, verbose: bool) { let mut command = ProcessCommand::new("doctl");