Skip to content
Draft
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
8 changes: 6 additions & 2 deletions crates/cli/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use std::io::IsTerminal;
use clap::{CommandFactory, Parser, Subcommand};
use microsandbox_cli::{
commands::{
create, exec, image, inspect, install, list, logs, metrics, ps, pull, registry, remove,
run, self_cmd, snapshot, start, stop, uninstall, volume,
create, exec, image, inspect, install, list, logs, metrics, profile, ps, pull, registry,
remove, run, self_cmd, snapshot, start, stop, uninstall, volume,
},
log_args::{self, LogArgs},
sandbox_cmd::{self, SandboxArgs},
Expand Down Expand Up @@ -113,6 +113,9 @@ enum Commands {
/// Manage the msb installation.
#[command(name = "self")]
Self_(self_cmd::SelfArgs),

/// Manage SDK backend profiles (local vs cloud).
Profile(profile::ProfileArgs),
}

//--------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -291,6 +294,7 @@ fn run_async_command_anyhow(
Commands::Install(args) => install::run(args).await,
Commands::Uninstall(args) => uninstall::run(args).await,
Commands::Self_(args) => self_cmd::run(args).await,
Commands::Profile(args) => profile::run(args).await,
}
})
}
31 changes: 31 additions & 0 deletions crates/cli/lib/commands/common.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
//! Common sandbox configuration flags shared between commands.

use std::path::PathBuf;
use std::sync::Arc;

use clap::Args;
use microsandbox::backend::{Backend, LocalBackend};
use microsandbox::sandbox::SandboxBuilder;

use crate::ui;

//--------------------------------------------------------------------------------------------------
// Functions: Backend resolution
//--------------------------------------------------------------------------------------------------

/// Resolve the process-wide local backend exactly once at the CLI entry point.
///
/// CLI commands always operate against the active default backend. Returns
/// an `Arc<dyn Backend>` plus a borrow of the `LocalBackend` inside it so
/// callers can dispatch through either the trait or the local-only APIs.
/// Errors when the resolved default backend isn't a local one (e.g. when
/// the user has installed a cloud profile but is running a local-only
/// command).
pub fn resolve_local_backend() -> anyhow::Result<Arc<dyn Backend>> {
let backend = microsandbox::backend::default_backend();
if backend.as_local().is_none() {
anyhow::bail!(
"this command requires a local backend, but the active default is a cloud backend"
);
}
Ok(backend)
}

/// Borrow the `LocalBackend` inside the resolved default backend, or error.
pub fn local_backend_ref(backend: &Arc<dyn Backend>) -> anyhow::Result<&LocalBackend> {
backend
.as_local()
.ok_or_else(|| anyhow::anyhow!("this command requires a local backend"))
}

//--------------------------------------------------------------------------------------------------
// Types
//--------------------------------------------------------------------------------------------------
Expand Down
31 changes: 20 additions & 11 deletions crates/cli/lib/commands/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,10 @@ async fn run_pull_inner(
) -> anyhow::Result<()> {
let start = Instant::now();

let global = microsandbox::config::config();
let cache = microsandbox_image::GlobalCache::new(&global.cache_dir())?;
let backend = crate::commands::common::resolve_local_backend()?;
let local = crate::commands::common::local_backend_ref(&backend)?;
let global = local.config();
let cache = microsandbox_image::GlobalCache::new(&local.cache_dir())?;
let platform = microsandbox_image::Platform::host_linux();
let image_ref: microsandbox_image::Reference = reference
.parse()
Expand All @@ -141,7 +143,7 @@ async fn run_pull_inner(
if let Some((result, metadata)) =
microsandbox_image::Registry::pull_cached(&cache, &image_ref, &options)?
{
if let Err(e) = Image::persist(&reference, metadata).await {
if let Err(e) = Image::persist(local, &reference, metadata).await {
tracing::warn!(error = %e, "failed to persist image metadata to database");
}

Expand Down Expand Up @@ -231,10 +233,10 @@ async fn run_pull_inner(
}

// Persist to database.
let cache = microsandbox_image::GlobalCache::new(&global.cache_dir())?;
let cache = microsandbox_image::GlobalCache::new(&local.cache_dir())?;
match cache.read_image_metadata(&image_ref) {
Ok(Some(metadata)) => {
if let Err(e) = Image::persist(&reference, metadata).await {
if let Err(e) = Image::persist(local, &reference, metadata).await {
tracing::warn!(error = %e, "failed to persist image metadata to database");
}
}
Expand Down Expand Up @@ -281,8 +283,9 @@ pub(crate) async fn pull_if_missing(reference: &str, quiet: bool) -> anyhow::Res
return Ok(());
}

let global = microsandbox::config::config();
let cache = microsandbox_image::GlobalCache::new(&global.cache_dir())?;
let backend = crate::commands::common::resolve_local_backend()?;
let local = crate::commands::common::local_backend_ref(&backend)?;
let cache = microsandbox_image::GlobalCache::new(&local.cache_dir())?;
let image_ref: microsandbox_image::Reference = reference
.parse()
.map_err(|e| anyhow::anyhow!("invalid image reference: {e}"))?;
Expand All @@ -294,7 +297,7 @@ pub(crate) async fn pull_if_missing(reference: &str, quiet: bool) -> anyhow::Res
if let Some((_, metadata)) =
microsandbox_image::Registry::pull_cached(&cache, &image_ref, &options)?
{
if let Err(e) = Image::persist(reference, metadata).await {
if let Err(e) = Image::persist(local, reference, metadata).await {
tracing::warn!(error = %e, "failed to persist image metadata to database");
}
return Ok(());
Expand All @@ -313,7 +316,9 @@ pub(crate) async fn pull_if_missing(reference: &str, quiet: bool) -> anyhow::Res

/// Execute `msb image list` / `msb images`.
pub async fn run_list(args: ImageListArgs) -> anyhow::Result<()> {
let images = Image::list().await?;
let backend = crate::commands::common::resolve_local_backend()?;
let local = crate::commands::common::local_backend_ref(&backend)?;
let images = Image::list(local).await?;

if args.format.as_deref() == Some("json") {
let entries: Vec<serde_json::Value> = images
Expand Down Expand Up @@ -372,7 +377,9 @@ pub async fn run_list(args: ImageListArgs) -> anyhow::Result<()> {

/// Execute `msb image inspect`.
pub async fn run_inspect(args: ImageInspectArgs) -> anyhow::Result<()> {
let detail = Image::inspect(&args.reference).await?;
let backend = crate::commands::common::resolve_local_backend()?;
let local = crate::commands::common::local_backend_ref(&backend)?;
let detail = Image::inspect(local, &args.reference).await?;

if args.format.as_deref() == Some("json") {
let layers_json: Vec<serde_json::Value> = detail
Expand Down Expand Up @@ -494,6 +501,8 @@ pub async fn run_inspect(args: ImageInspectArgs) -> anyhow::Result<()> {

/// Execute `msb image rm` / `msb rmi`.
pub async fn run_remove(args: ImageRemoveArgs) -> anyhow::Result<()> {
let backend = crate::commands::common::resolve_local_backend()?;
let local = crate::commands::common::local_backend_ref(&backend)?;
let mut failed = false;

for reference in &args.references {
Expand All @@ -503,7 +512,7 @@ pub async fn run_remove(args: ImageRemoveArgs) -> anyhow::Result<()> {
ui::Spinner::start("Removing", reference)
};

match Image::remove(reference, args.force).await {
match Image::remove(local, reference, args.force).await {
Ok(()) => {
spinner.finish_success("Removed");
}
Expand Down
4 changes: 2 additions & 2 deletions crates/cli/lib/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub async fn run(args: InspectArgs) -> anyhow::Result<()> {
serde_json::from_str(handle.config_json()).unwrap_or(serde_json::Value::Null);
let json = serde_json::json!({
"name": handle.name(),
"status": format!("{:?}", handle.status()),
"status": format!("{:?}", handle.status_snapshot()),
"config": config,
"created_at": handle.created_at().map(|dt| ui::format_datetime(&dt)),
"updated_at": handle.updated_at().map(|dt| ui::format_datetime(&dt)),
Expand All @@ -42,7 +42,7 @@ pub async fn run(args: InspectArgs) -> anyhow::Result<()> {
return Ok(());
}

let status = format!("{:?}", handle.status());
let status = format!("{:?}", handle.status_snapshot());

ui::detail_kv("Name", handle.name());
ui::detail_kv("Status", &ui::format_status(&status));
Expand Down
8 changes: 6 additions & 2 deletions crates/cli/lib/commands/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

use clap::Args;
use microsandbox::config;

use crate::ui;

Expand Down Expand Up @@ -131,7 +130,12 @@ pub async fn run(args: InstallArgs) -> anyhow::Result<()> {

/// Resolve the bin directory for installed aliases.
fn resolve_bin_dir() -> PathBuf {
config::config().home().join("bin")
let backend = microsandbox::backend::default_backend();
let home = match backend.as_local() {
Some(local) => local.config().home(),
None => microsandbox_utils::resolve_home(),
};
home.join("bin")
}

/// Validate that an alias name is safe to use as a filename in the bin directory.
Expand Down
8 changes: 4 additions & 4 deletions crates/cli/lib/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ pub async fn run(args: ListArgs) -> anyhow::Result<()> {
.into_iter()
.filter(|s| {
if args.running {
s.status() == SandboxStatus::Running
s.status_snapshot() == SandboxStatus::Running
} else if args.stopped {
s.status() == SandboxStatus::Stopped
s.status_snapshot() == SandboxStatus::Stopped
} else {
true
}
Expand Down Expand Up @@ -71,7 +71,7 @@ pub async fn run(args: ListArgs) -> anyhow::Result<()> {

for s in &filtered {
let image = extract_image(s.config_json());
let status = format!("{:?}", s.status());
let status = format!("{:?}", s.status_snapshot());
let created = s
.created_at()
.as_ref()
Expand Down Expand Up @@ -110,7 +110,7 @@ fn print_json(sandboxes: &[SandboxHandle]) -> anyhow::Result<()> {
.map(|s| {
serde_json::json!({
"name": s.name(),
"status": format!("{:?}", s.status()),
"status": format!("{:?}", s.status_snapshot()),
"created_at": s.created_at().map(|dt| ui::format_datetime(&dt)),
"image": extract_image(s.config_json()),
})
Expand Down
4 changes: 3 additions & 1 deletion crates/cli/lib/commands/logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ struct LogEntry {

/// Execute the `msb logs` command.
pub async fn run(args: LogsArgs) -> anyhow::Result<()> {
let log_dir = microsandbox::sandbox::logs::log_dir_for(&args.name);
let backend = crate::commands::common::resolve_local_backend()?;
let local = crate::commands::common::local_backend_ref(&backend)?;
let log_dir = microsandbox::sandbox::logs::log_dir_for(local, &args.name);
if !log_dir.exists() {
return Err(anyhow!(
"no logs directory for sandbox {:?} (sandbox not found?)",
Expand Down
4 changes: 3 additions & 1 deletion crates/cli/lib/commands/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ pub async fn run(args: MetricsArgs) -> anyhow::Result<()> {
return Ok(());
}

let mut metrics = all_sandbox_metrics()
let backend = crate::commands::common::resolve_local_backend()?;
let local = crate::commands::common::local_backend_ref(&backend)?;
let mut metrics = all_sandbox_metrics(local)
.await?
.into_iter()
.collect::<Vec<(String, SandboxMetrics)>>();
Expand Down
7 changes: 4 additions & 3 deletions crates/cli/lib/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod install;
pub mod list;
pub mod logs;
pub mod metrics;
pub mod profile;
pub mod ps;
pub mod pull;
pub mod registry;
Expand Down Expand Up @@ -55,7 +56,7 @@ pub async fn maybe_stop(sandbox: &Sandbox) {
pub async fn resolve_and_start(name: &str, quiet: bool) -> anyhow::Result<Sandbox> {
let handle = Sandbox::get(name).await?;

match handle.status() {
match handle.status_snapshot() {
SandboxStatus::Running | SandboxStatus::Draining => {
// Connect to the running sandbox process via the agent relay.
Ok(handle.connect().await?)
Expand Down Expand Up @@ -83,11 +84,11 @@ pub async fn resolve_and_start(name: &str, quiet: bool) -> anyhow::Result<Sandbo
}
}
}
SandboxStatus::Paused => {
SandboxStatus::Created | SandboxStatus::Starting | SandboxStatus::Paused => {
anyhow::bail!(
"sandbox '{}' is in state {:?} and cannot be started",
name,
handle.status()
handle.status_snapshot()
);
}
}
Expand Down
Loading
Loading