Skip to content

Commit

Permalink
run can now build, sign, and push a Nix attr.
Browse files Browse the repository at this point in the history
  • Loading branch information
noteed committed May 13, 2024
1 parent 401c00c commit 02e1113
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 21 deletions.
44 changes: 26 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 6 additions & 1 deletion src/lib/thebacknd/client/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// The attribute path to build
#[arg(short = 'A', long)]
pub attr: Option<String>,

/// Enable verbose output
#[arg(short, long)]
pub verbose: bool,
Expand Down
81 changes: 79 additions & 2 deletions src/lib/thebacknd/client/run.rs
Original file line number Diff line number Diff line change
@@ -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(&param), *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())
Expand All @@ -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");
Expand Down

0 comments on commit 02e1113

Please sign in to comment.