Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
45 changes: 34 additions & 11 deletions apps/framework-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,16 @@ fn override_project_config_from_url(
Ok(())
}

/// Formats a [`RoutineFailure`] from local infrastructure for surfacing in `anyhow` errors, preserving
/// both routine message details and the source error when present.
fn format_infrastructure_routine_failure(e: &RoutineFailure) -> String {
match &e.error {
// Blank line before the source error so multi-line guidance (e.g. container-runtime options) reads clearly.
Some(err) => format!("{}: {}\n\n{err:#}", e.message.action, e.message.details),
None => format!("{}: {}", e.message.action, e.message.details),
}
}

/// Runs local infrastructure with a configurable timeout
async fn run_local_infrastructure_with_timeout(
project: &Arc<Project>,
Expand All @@ -437,15 +447,8 @@ async fn run_local_infrastructure_with_timeout(
let project = project.clone();
let settings = settings.clone();
move || {
run_local_infrastructure(&project, &settings, provider.as_ref()).map_err(|e| {
anyhow::anyhow!(
"{}: {}",
e.message.action,
e.error
.map(|err| format!("{err:#}"))
.unwrap_or_else(|| e.message.details)
)
})
run_local_infrastructure(&project, &settings, provider.as_ref())
.map_err(|e| anyhow::anyhow!(format_infrastructure_routine_failure(&e)))
}
});

Expand Down Expand Up @@ -744,7 +747,7 @@ pub async fn top_command_handler(
.map_err(|e| {
RoutineFailure::error(Message {
action: "Dev".to_string(),
details: format!("Failed to run local infrastructure: {e:?}"),
details: format!("Local infrastructure could not start:\n\n{e:#}"),
})
})?;
} else {
Expand Down Expand Up @@ -1027,7 +1030,7 @@ pub async fn top_command_handler(
.map_err(|e| {
RoutineFailure::error(Message {
action: "Prod".to_string(),
details: format!("Failed to run local infrastructure: {e:?}"),
details: format!("Local infrastructure could not start:\n\n{e:#}"),
})
})?;
}
Expand Down Expand Up @@ -2486,4 +2489,24 @@ mod tests {
assert!(success_message.contains("- typescript (typescript)"));
assert!(success_message.contains("- python (python)"));
}

#[test]
fn format_infrastructure_routine_failure_preserves_details_and_error() {
use crate::cli::display::Message;
let rf = RoutineFailure::new(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Message::new(
"Failed".to_string(),
"to ensure docker is running".to_string(),
),
std::io::Error::new(std::io::ErrorKind::NotFound, "os error 2"),
);
let s = super::format_infrastructure_routine_failure(&rf);
assert!(s.contains("to ensure docker is running"), "{}", s);
assert!(
s.starts_with("Failed: to ensure docker is running\n\n"),
"{}",
s
);
assert!(s.contains("os error 2") || s.contains("NotFound"), "{}", s);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
59 changes: 58 additions & 1 deletion apps/framework-cli/src/cli/routines/util.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
use crate::utilities::docker::DockerClient;
use std::io::ErrorKind;

/// Guidance when the configured container CLI cannot be executed (commonly: binary missing from PATH).
/// `Command::spawn` reports `ErrorKind::NotFound` without including the program name.
fn container_runtime_not_found_message(configured: &str) -> String {
format!(
"Could not find or run the container CLI `{configured}`.\n\
\n\
Choose one of the following:\n\
\n\
• Install a Docker-compatible CLI (Docker Desktop, Docker Engine, or Finch) and ensure the command is on your `PATH`.\n\
Comment thread
okane16 marked this conversation as resolved.
Outdated
\n\
• Point Moose at a specific binary: set `container_cli_path` under `[dev]` in `~/.moose/config.toml`, or set the\n\
environment variable `MOOSE_DEV__CONTAINER_CLI_PATH` to the full path of your `docker`, `finch`, or `nerdctl` executable.\n\
\n\
• Run without Docker for local services: `moose dev --dockerless` (native ClickHouse/Temporal; see the docs for details)."
)
}

pub fn ensure_docker_running(docker_client: &DockerClient) -> anyhow::Result<()> {
let errors = docker_client.check_status()?;
let errors = docker_client.check_status().map_err(|e| {
if e.kind() == ErrorKind::NotFound {
anyhow::anyhow!(container_runtime_not_found_message(
docker_client.container_cli()
))
} else {
e.into()
}
})?;

if errors.is_empty() {
Ok(())
Expand All @@ -14,3 +40,34 @@ pub fn ensure_docker_running(docker_client: &DockerClient) -> anyhow::Result<()>
anyhow::bail!("Failed to run docker commands. {}", errors.join("\n"))
}
}

#[cfg(test)]
mod tests {
use super::*;

/// Uses a real missing executable path so `Command::spawn` returns `ErrorKind::NotFound`
/// (same shape as a missing `docker`/`finch` on PATH).
#[test]
fn ensure_docker_running_missing_cli_includes_path_and_dockerless_hint() {
let path = std::env::temp_dir().join(format!(
"moose-missing-container-cli-{}",
std::process::id()
));
let _ = std::fs::remove_file(&path);
let client = DockerClient::new_for_test(path.to_string_lossy().to_string());
let err = ensure_docker_running(&client).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("Could not find or run the container CLI"),
"msg: {msg}"
);
let path_str = path.to_string_lossy();
assert!(
msg.contains(path_str.as_ref()) || msg.contains("moose-missing-container-cli"),
"msg: {msg}"
);
assert!(msg.contains("Choose one of the following"), "msg: {msg}");
assert!(msg.contains("--dockerless"), "msg: {msg}");
assert!(msg.contains("container_cli_path"), "msg: {msg}");
}
}
14 changes: 14 additions & 0 deletions apps/framework-cli/src/utilities/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ impl DockerClient {
Self { cli_command }
}

/// Returns the configured container runtime executable name or path (e.g. `docker`, `finch`).
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
#[must_use]
pub fn container_cli(&self) -> &str {
&self.cli_command
}

/// Test-only constructor for unit tests that need a specific container CLI path.
#[cfg(test)]
pub(crate) fn new_for_test(cli_command: impl Into<String>) -> Self {
Self {
cli_command: cli_command.into(),
}
}

/// Creates a new Command using the configured container CLI
fn create_command(&self) -> Command {
Command::new(&self.cli_command)
Expand Down
Loading