Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions crates/navigator-bootstrap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::docker::{
check_existing_cluster, create_ssh_docker_client, destroy_cluster_resources, ensure_container,
ensure_image, ensure_network, ensure_volume, start_container, stop_container,
};
use crate::kubeconfig::{rewrite_kubeconfig, rewrite_kubeconfig_remote, store_kubeconfig};
use crate::kubeconfig::rewrite_kubeconfig_remote;
use crate::metadata::{
create_cluster_metadata, create_cluster_metadata_with_host, extract_host_from_ssh_destination,
local_gateway_host, resolve_ssh_hostname,
Expand All @@ -38,8 +38,8 @@ use crate::runtime::{

pub use crate::docker::ExistingClusterInfo;
pub use crate::kubeconfig::{
default_local_kubeconfig_path, print_kubeconfig, stored_kubeconfig_path,
update_local_kubeconfig,
default_local_kubeconfig_path, print_kubeconfig, rewrite_kubeconfig, store_kubeconfig,
stored_kubeconfig_path, update_local_kubeconfig,
};
pub use crate::metadata::{
ClusterMetadata, clear_active_cluster, get_cluster_metadata, list_clusters,
Expand Down
1 change: 1 addition & 0 deletions crates/navigator-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ path = "src/main.rs"
[dependencies]
navigator-bootstrap = { path = "../navigator-bootstrap" }
navigator-core = { path = "../navigator-core" }
navigator-gateway = { path = "../navigator-gateway" }
navigator-policy = { path = "../navigator-policy" }
navigator-providers = { path = "../navigator-providers" }
navigator-tui = { path = "../navigator-tui" }
Expand Down
41 changes: 41 additions & 0 deletions crates/navigator-cli/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::process::Command;

fn main() {
// On macOS, embed rpath entries for libkrun and libkrunfw so the binary
// can find them at runtime without DYLD_LIBRARY_PATH.
//
// Background: navigator-gateway links against libkrun (a system cdylib
// installed via Homebrew). At runtime libkrun loads libkrunfw via dlopen.
// The gateway crate's build.rs already emits link-search paths so the
// *linker* can find the dylibs, but cargo:rustc-link-arg from a library
// crate does NOT propagate to the final binary. We must emit the rpath
// flags from the binary crate's build.rs.
#[cfg(target_os = "macos")]
{
for formula in &["libkrun", "libkrunfw"] {
if let Some(lib_dir) = brew_lib_path(formula) {
println!("cargo:rustc-link-arg=-Wl,-rpath,{lib_dir}");
}
}
}
}

/// Ask Homebrew for the install prefix of a formula and return its `lib/` path.
#[cfg(target_os = "macos")]
fn brew_lib_path(formula: &str) -> Option<String> {
let output = Command::new("brew")
.args(["--prefix", formula])
.output()
.ok()?;

if !output.status.success() {
return None;
}

let prefix = String::from_utf8_lossy(&output.stdout).trim().to_string();
if prefix.is_empty() {
return None;
}

Some(format!("{prefix}/lib"))
}
136 changes: 136 additions & 0 deletions crates/navigator-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ enum Commands {
command: ProviderCommands,
},

/// Hardware-isolated microVM gateway.
Gateway {
#[command(subcommand)]
command: GatewayCommands,
},

/// Launch the Gator interactive TUI.
Gator,

Expand Down Expand Up @@ -468,6 +474,94 @@ enum ClusterAdminCommands {
},
}

#[derive(Subcommand, Debug)]
enum GatewayCommands {
/// Run a command inside a hardware-isolated microVM.
///
/// Boots a lightweight microVM using libkrun (Apple Hypervisor.framework on
/// macOS ARM64, KVM on Linux) and executes the specified command inside it.
/// The rootfs directory is mapped into the VM via virtio-fs.
///
/// NOTE: This command takes over the current process. The process will exit
/// with the guest workload's exit code when the VM shuts down.
Run {
/// Path to the root filesystem directory (aarch64 Linux userspace).
///
/// Must contain the executable specified by EXEC_PATH. For a quick
/// start, download the Alpine minirootfs:
///
/// curl -L https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/aarch64/alpine-minirootfs-3.21.3-aarch64.tar.gz | tar xz -C ./rootfs
#[arg(long)]
rootfs: PathBuf,

/// Number of virtual CPUs for the microVM.
#[arg(long, default_value_t = 1)]
vcpus: u8,

/// Amount of RAM in MiB for the microVM.
#[arg(long, default_value_t = 128)]
mem: u32,

/// Working directory inside the VM (relative to rootfs).
#[arg(long, default_value = "/")]
workdir: String,

/// libkrun log level (0=Off, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Trace).
#[arg(long, default_value_t = 2)]
krun_log_level: u32,

/// Path to the executable inside the rootfs.
exec_path: String,

/// Arguments passed to the executable.
#[arg(trailing_var_arg = true)]
args: Vec<String>,
},

/// Boot the cluster container in a hardware-isolated microVM.
///
/// Extracts a rootfs from the cluster Docker image, then boots k3s inside
/// a libkrun microVM with port forwarding and persistent storage.
/// The parent process stays alive to monitor the VM.
Cluster {
/// Cluster name for kubeconfig context naming.
#[arg(long, default_value = "gateway")]
name: String,

/// Cluster Docker image to extract rootfs from.
///
/// Defaults to the same image used by `cluster admin deploy`.
#[arg(long)]
image: Option<String>,

/// Host port for the navigator gateway (mapped to guest port 30051).
#[arg(long, default_value_t = 8080)]
port: u16,

/// Host port for the k3s API server (mapped to guest port 6443).
/// If not set, an ephemeral port is used for health checking only.
#[arg(long)]
kube_port: Option<u16>,

/// Number of virtual CPUs for the microVM.
#[arg(long, default_value_t = 2)]
vcpus: u8,

/// Amount of RAM in MiB for the microVM.
#[arg(long, default_value_t = 2048)]
mem: u32,

/// Directory for persistent k3s state. Created if it doesn't exist.
/// Defaults to $XDG_DATA_HOME/navigator/gateway-cluster/k3s-state.
#[arg(long)]
state_dir: Option<PathBuf>,

/// libkrun log level (0=Off, 1=Error, 2=Warn, 3=Info, 4=Debug, 5=Trace).
#[arg(long, default_value_t = 2)]
krun_log_level: u32,
},
}

#[derive(Subcommand, Debug)]
enum SandboxCommands {
/// Create a sandbox.
Expand Down Expand Up @@ -1262,6 +1356,48 @@ async fn main() -> Result<()> {
}
}
}
Some(Commands::Gateway { command }) => match command {
GatewayCommands::Run {
rootfs,
vcpus,
mem,
workdir,
krun_log_level,
exec_path,
args,
} => {
run::gateway_run(
&rootfs,
vcpus,
mem,
&workdir,
krun_log_level,
&exec_path,
&args,
)?;
}
GatewayCommands::Cluster {
name,
image,
port,
kube_port,
vcpus,
mem,
state_dir,
krun_log_level,
} => {
run::gateway_cluster(
&name,
image.as_deref(),
port,
kube_port,
vcpus,
mem,
state_dir.as_deref(),
krun_log_level,
)?;
}
},
Some(Commands::Gator) => {
let ctx = resolve_cluster(&cli.cluster)?;
let tls = tls.with_cluster_name(&ctx.name);
Expand Down
Loading
Loading