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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions coast-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ path = "src/bin/coast-dev.rs"

[dependencies]
coast-core = { path = "../coast-core" }
coast-docker = { path = "../coast-docker" }
coast-i18n = { path = "../coast-i18n" }
coast-update = { path = "../coast-update" }
rust-i18n = { workspace = true }
Expand Down
21 changes: 19 additions & 2 deletions coast-cli/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,25 @@ pub async fn execute(args: &DoctorArgs) -> Result<()> {

let db = rusqlite::Connection::open(&db_path).context("Failed to open state database")?;

let docker = bollard::Docker::connect_with_local_defaults()
.context("Failed to connect to Docker. Is Docker running?")?;
let probe = coast_docker::host::probe_host_docker();
let docker = match probe.docker {
Ok(docker) => docker,
Err(error) => {
let mut detail =
"Failed to connect to Docker. Is Docker running and is your active Docker context reachable?".to_string();
if let Some(endpoint) = probe.endpoint {
detail.push_str(&format!(
"\nResolved endpoint source: {}\nResolved endpoint host: {}",
coast_docker::host::docker_endpoint_source_label(&endpoint.source),
endpoint.host
));
if let Some(context) = endpoint.context {
detail.push_str(&format!("\nResolved context: {context}"));
}
}
return Err(anyhow::anyhow!("{detail}\n{error}"));
}
};

let mut fixes: Vec<String> = Vec::new();
let mut findings: Vec<String> = Vec::new();
Expand Down
2 changes: 1 addition & 1 deletion coast-cli/src/commands/nuke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub async fn execute(args: &NukeArgs) -> Result<()> {
..Default::default()
};

match bollard::Docker::connect_with_local_defaults() {
match coast_docker::host::connect_to_host_docker() {
Ok(docker) => {
report.containers_removed = remove_containers(&docker).await;
report.volumes_removed = remove_volumes(&docker).await;
Expand Down
4 changes: 4 additions & 0 deletions coast-core/src/protocol/api_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ pub struct DockerInfoResponse {
pub server_version: String,
pub can_adjust: bool,
pub provider: String,
pub endpoint_source: Option<String>,
pub endpoint_host: Option<String>,
pub context_name: Option<String>,
pub connect_error: Option<String>,
}

/// Response after requesting Docker Desktop settings to be opened.
Expand Down
10 changes: 10 additions & 0 deletions coast-core/src/protocol/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1026,13 +1026,23 @@ fn test_docker_info_response_serialization() {
server_version: "28.3.3".to_string(),
can_adjust: true,
provider: "docker-desktop".to_string(),
endpoint_source: Some("config_context".to_string()),
endpoint_host: Some("unix:///Users/test/.orbstack/run/docker.sock".to_string()),
context_name: Some("orbstack".to_string()),
connect_error: None,
};
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["mem_total_bytes"], 8_589_934_592u64);
assert_eq!(json["cpus"], 4);
assert_eq!(json["os"], "Docker Desktop");
assert_eq!(json["server_version"], "28.3.3");
assert_eq!(json["can_adjust"], true);
assert_eq!(json["endpoint_source"], "config_context");
assert_eq!(
json["endpoint_host"],
"unix:///Users/test/.orbstack/run/docker.sock"
);
assert_eq!(json["context_name"], "orbstack");
}

#[test]
Expand Down
27 changes: 27 additions & 0 deletions coast-daemon/src/api/query/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use axum::{Json, Router};
use tokio::sync::OnceCell;

use coast_core::protocol::{DockerInfoResponse, OpenDockerSettingsResponse};
use coast_docker::host::docker_endpoint_source_label;

use crate::server::AppState;

Expand Down Expand Up @@ -53,6 +54,19 @@ async fn docker_info(State(state): State<Arc<AppState>>) -> Json<DockerInfoRespo
server_version: String::new(),
can_adjust: false,
provider: String::new(),
endpoint_source: state
.docker_endpoint
.as_ref()
.map(|endpoint| docker_endpoint_source_label(&endpoint.source).to_string()),
endpoint_host: state
.docker_endpoint
.as_ref()
.map(|endpoint| endpoint.host.clone()),
context_name: state
.docker_endpoint
.as_ref()
.and_then(|endpoint| endpoint.context.clone()),
connect_error: state.docker_connect_error.clone(),
});

let Some(docker) = state.docker.as_ref() else {
Expand Down Expand Up @@ -81,6 +95,19 @@ async fn docker_info(State(state): State<Arc<AppState>>) -> Json<DockerInfoRespo
server_version,
can_adjust,
provider,
endpoint_source: state
.docker_endpoint
.as_ref()
.map(|endpoint| docker_endpoint_source_label(&endpoint.source).to_string()),
endpoint_host: state
.docker_endpoint
.as_ref()
.map(|endpoint| endpoint.host.clone()),
context_name: state
.docker_endpoint
.as_ref()
.and_then(|endpoint| endpoint.context.clone()),
connect_error: state.docker_connect_error.clone(),
})
}

Expand Down
4 changes: 4 additions & 0 deletions coast-daemon/src/api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,10 @@ mod tests {
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["connected"], false);
assert!(json.get("endpoint_source").is_some());
assert!(json.get("endpoint_host").is_some());
assert!(json.get("context_name").is_some());
assert!(json.get("connect_error").is_some());
}

#[tokio::test]
Expand Down
34 changes: 33 additions & 1 deletion coast-daemon/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::analytics::{self, AnalyticsClient, CommandSource};
use crate::api::streaming::spawn_agent_shell_if_configured;
use crate::handlers;
use crate::state::StateDb;
use coast_docker::host::{docker_endpoint_source_label, DockerEndpoint};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpdateOperationKind {
Expand Down Expand Up @@ -120,6 +121,10 @@ pub struct AppState {
/// Bollard Docker client connected to the host daemon.
/// None in test environments where Docker is not available.
pub docker: Option<bollard::Docker>,
/// Resolved Docker endpoint metadata, if endpoint resolution succeeded.
pub docker_endpoint: Option<DockerEndpoint>,
/// Last Docker connection error captured at daemon startup, if any.
pub docker_connect_error: Option<String>,
/// Broadcast channel for WebSocket event notifications.
pub event_bus: tokio::sync::broadcast::Sender<CoastEvent>,
/// Persistent PTY sessions for the host terminal feature.
Expand Down Expand Up @@ -186,7 +191,25 @@ pub struct AppState {
impl AppState {
/// Create a new `AppState` with the given state database and Docker client.
pub fn new(db: StateDb) -> Self {
let docker = bollard::Docker::connect_with_local_defaults().ok();
let probe = coast_docker::host::probe_host_docker();
let docker_endpoint = probe.endpoint.clone();
let (docker, docker_connect_error) = match probe.docker {
Ok(docker) => (Some(docker), None),
Err(error) => {
if let Some(ref endpoint) = docker_endpoint {
warn!(
source = docker_endpoint_source_label(&endpoint.source),
host = %endpoint.host,
context = endpoint.context.as_deref().unwrap_or(""),
error = %error,
"Docker is unavailable at daemon startup"
);
} else {
warn!(error = %error, "Docker is unavailable at daemon startup");
}
(None, Some(error.to_string()))
}
};
let (event_bus, _) = tokio::sync::broadcast::channel(256);
let initial_lang = db.get_language().unwrap_or_else(|_| "en".to_string());
let (language_tx, language_rx) = tokio::sync::watch::channel(initial_lang);
Expand All @@ -208,6 +231,8 @@ impl AppState {
Self {
db: Mutex::new(db),
docker,
docker_endpoint,
docker_connect_error,
event_bus,
pty_sessions: Mutex::new(std::collections::HashMap::new()),
exec_sessions: Mutex::new(std::collections::HashMap::new()),
Expand Down Expand Up @@ -243,6 +268,8 @@ impl AppState {
Self {
db: Mutex::new(db),
docker: None,
docker_endpoint: None,
docker_connect_error: None,
event_bus,
pty_sessions: Mutex::new(std::collections::HashMap::new()),
exec_sessions: Mutex::new(std::collections::HashMap::new()),
Expand Down Expand Up @@ -284,6 +311,11 @@ impl AppState {
)
.expect("bollard stub client creation should not fail"),
);
s.docker_endpoint = Some(DockerEndpoint {
host: "http://127.0.0.1:0".to_string(),
source: coast_docker::host::DockerEndpointSource::EnvHost,
context: None,
});
s
}

Expand Down
6 changes: 2 additions & 4 deletions coast-docker/src/dind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use tracing::{debug, info};

use coast_core::error::{CoastError, Result};

use crate::host::connect_to_host_docker;
use crate::runtime::{BindMount, ContainerConfig, ExecResult, Runtime, VolumeMount};

/// The default Docker image used for DinD coast containers.
Expand All @@ -34,10 +35,7 @@ pub struct DindRuntime {
impl DindRuntime {
/// Create a new DinD runtime connected to the default Docker socket.
pub fn new() -> Result<Self> {
let docker = Docker::connect_with_local_defaults().map_err(|e| CoastError::Docker {
message: format!("Failed to connect to Docker daemon. Is Docker running? Error: {e}"),
source: Some(Box::new(e)),
})?;
let docker = connect_to_host_docker()?;
Ok(Self { docker })
}

Expand Down
Loading
Loading