From a33a767db1e49c588417830eb7ac339b5bd00eeb Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Thu, 15 Jan 2026 23:32:05 +0100 Subject: [PATCH 1/3] fix(infra): pass DOCKER_HOST to scanner CLI for multi-runtime support --- src/infra/component_factory_impl.rs | 20 ++- src/infra/docker_image_builder.rs | 20 +-- src/infra/docker_socket_discovery.rs | 238 +++++++++++++++++++++++++++ src/infra/mod.rs | 2 + src/infra/sysdig_image_scanner.rs | 21 ++- 5 files changed, 282 insertions(+), 19 deletions(-) create mode 100644 src/infra/docker_socket_discovery.rs diff --git a/src/infra/component_factory_impl.rs b/src/infra/component_factory_impl.rs index 0611515..1479c55 100644 --- a/src/infra/component_factory_impl.rs +++ b/src/infra/component_factory_impl.rs @@ -1,8 +1,6 @@ -use bollard::Docker; - use crate::{ app::component_factory::{ComponentFactory, ComponentFactoryError, Components, Config}, - infra::{DockerImageBuilder, SysdigAPIToken, SysdigImageScanner}, + infra::{DockerImageBuilder, SysdigAPIToken, SysdigImageScanner, connect_to_docker}, }; pub struct ConcreteComponentFactory; @@ -17,11 +15,19 @@ impl ComponentFactory for ConcreteComponentFactory { .unwrap_or_else(|| std::env::var("SECURE_API_TOKEN")) .map(SysdigAPIToken)?; - let scanner = SysdigImageScanner::new(config.sysdig.api_url.clone(), token); - - let docker_client = Docker::connect_with_local_defaults() + // Get Docker connection with socket path + let docker_connection = connect_to_docker() .map_err(|e| ComponentFactoryError::DockerClientError(e.to_string()))?; - let builder = DockerImageBuilder::new(docker_client); + + // Create scanner WITH the docker_host so CLI subprocess uses the same socket + let scanner = SysdigImageScanner::with_docker_host( + config.sysdig.api_url.clone(), + token, + docker_connection.socket_path.clone(), + ); + + // Create builder with the Docker client + let builder = DockerImageBuilder::new(docker_connection.client); Ok(Components { scanner: Box::new(scanner), diff --git a/src/infra/docker_image_builder.rs b/src/infra/docker_image_builder.rs index d241713..91f8972 100644 --- a/src/infra/docker_image_builder.rs +++ b/src/infra/docker_image_builder.rs @@ -123,17 +123,15 @@ impl ImageBuilder for DockerImageBuilder { mod tests { use std::{path::PathBuf, str::FromStr}; - use bollard::Docker; - use crate::{ app::{ImageBuildError, ImageBuilder}, - infra::DockerImageBuilder, + infra::{DockerImageBuilder, connect_to_docker}, }; #[tokio::test] async fn it_builds_a_dockerfile() { - let docker_client = Docker::connect_with_local_defaults().unwrap(); - let image_builder = DockerImageBuilder::new(docker_client); + let docker_connection = connect_to_docker().unwrap(); + let image_builder = DockerImageBuilder::new(docker_connection.client); let image_built = image_builder .build_image(&PathBuf::from_str("tests/fixtures/Dockerfile").unwrap()) @@ -150,8 +148,8 @@ mod tests { #[tokio::test] async fn it_builds_a_containerfile() { - let docker_client = Docker::connect_with_local_defaults().unwrap(); - let image_builder = DockerImageBuilder::new(docker_client); + let docker_connection = connect_to_docker().unwrap(); + let image_builder = DockerImageBuilder::new(docker_connection.client); let image_built = image_builder .build_image(&PathBuf::from_str("tests/fixtures/Containerfile").unwrap()) @@ -168,8 +166,8 @@ mod tests { #[tokio::test] async fn it_fails_to_build_non_existent_dockerfile() { - let docker_client = Docker::connect_with_local_defaults().unwrap(); - let image_builder = DockerImageBuilder::new(docker_client); + let docker_connection = connect_to_docker().unwrap(); + let image_builder = DockerImageBuilder::new(docker_connection.client); let image_built = image_builder .build_image(&PathBuf::from_str("tests/fixtures/Nonexistent.dockerfile").unwrap()) @@ -184,8 +182,8 @@ mod tests { #[tokio::test] async fn it_builds_an_invalid_dockerfile_and_fails() { - let docker_client = Docker::connect_with_local_defaults().unwrap(); - let image_builder = DockerImageBuilder::new(docker_client); + let docker_connection = connect_to_docker().unwrap(); + let image_builder = DockerImageBuilder::new(docker_connection.client); let image_built = image_builder .build_image(&PathBuf::from_str("tests/fixtures/Invalid.dockerfile").unwrap()) diff --git a/src/infra/docker_socket_discovery.rs b/src/infra/docker_socket_discovery.rs new file mode 100644 index 0000000..8f2535b --- /dev/null +++ b/src/infra/docker_socket_discovery.rs @@ -0,0 +1,238 @@ +use std::path::PathBuf; + +use bollard::Docker; +use tracing::{debug, info, warn}; + +/// Result of a successful Docker connection, including the socket path used. +pub struct DockerConnection { + /// The connected Docker client + pub client: Docker, + /// The socket path that was used to connect. + /// Format: "unix:///path/to/socket" for Unix sockets, or the DOCKER_HOST value if set. + pub socket_path: String, +} + +/// List of Docker socket paths to try, in order of preference. +/// The first successful connection will be used. +fn get_candidate_socket_paths() -> Vec { + let mut paths = vec![ + // Standard Docker socket location (Linux/macOS) + PathBuf::from("/var/run/docker.sock"), + ]; + + // Add Colima socket paths if HOME is available + if let Ok(home) = std::env::var("HOME") { + let home_path = PathBuf::from(&home); + + // Colima Docker sockets (various locations) + paths.push(home_path.join(".colima/docker.sock")); + paths.push(home_path.join(".colima/default/docker.sock")); + + // Colima containerd socket - Note: This uses Docker-compatible API + // when Colima is configured with Docker compatibility layer + paths.push(home_path.join(".colima/default/containerd.sock")); + + // Lima default socket (used by some Colima configurations) + paths.push(home_path.join(".lima/default/sock/docker.sock")); + } + + // Podman socket (for potential future compatibility) + if let Ok(xdg_runtime_dir) = std::env::var("XDG_RUNTIME_DIR") { + paths.push(PathBuf::from(xdg_runtime_dir).join("podman/podman.sock")); + } + + paths +} + +/// Attempts to connect to Docker using multiple socket paths. +/// +/// This function tries the following in order: +/// 1. `DOCKER_HOST` environment variable (if set) +/// 2. Standard Docker socket at `/var/run/docker.sock` +/// 3. Colima sockets at `$HOME/.colima/docker.sock`, `$HOME/.colima/default/docker.sock` +/// 4. Colima containerd socket at `$HOME/.colima/default/containerd.sock` +/// 5. Lima socket at `$HOME/.lima/default/sock/docker.sock` +/// +/// Returns a `DockerConnection` containing both the client and the socket path used, +/// or an error if no socket could be connected. +pub fn connect_to_docker() -> Result { + // First, check if DOCKER_HOST is set - if so, use Bollard's default behavior + if let Ok(docker_host) = std::env::var("DOCKER_HOST") { + debug!("DOCKER_HOST environment variable is set: {}", docker_host); + match Docker::connect_with_local_defaults() { + Ok(client) => { + info!("Connected to Docker via DOCKER_HOST: {}", docker_host); + return Ok(DockerConnection { + client, + socket_path: docker_host, + }); + } + Err(e) => { + warn!("Failed to connect via DOCKER_HOST ({}): {}", docker_host, e); + // Continue to try other sockets + } + } + } + + // Try each candidate socket path + let candidate_paths = get_candidate_socket_paths(); + let mut last_error = None; + + for socket_path in &candidate_paths { + if !socket_path.exists() { + debug!("Socket path does not exist: {:?}", socket_path); + continue; + } + + debug!("Attempting to connect to Docker socket: {:?}", socket_path); + + let socket_path_str = match socket_path.to_str() { + Some(s) => s, + None => { + warn!("Invalid socket path (non-UTF8): {:?}", socket_path); + continue; + } + }; + + match Docker::connect_with_unix(socket_path_str, 120, bollard::API_DEFAULT_VERSION) { + Ok(client) => { + info!("Successfully connected to Docker socket: {:?}", socket_path); + return Ok(DockerConnection { + client, + socket_path: format!("unix://{}", socket_path_str), + }); + } + Err(e) => { + debug!("Failed to connect to socket {:?}: {}", socket_path, e); + last_error = Some((socket_path.clone(), e)); + } + } + } + + // If no socket worked, return an error with helpful information + let tried_paths: Vec = candidate_paths + .iter() + .map(|p| p.display().to_string()) + .collect(); + + Err(DockerConnectionError { + tried_paths, + last_error: last_error.map(|(path, err)| format!("{}: {}", path.display(), err)), + }) +} + +/// Error returned when no Docker socket could be connected. +#[derive(Debug)] +pub struct DockerConnectionError { + /// List of socket paths that were attempted + pub tried_paths: Vec, + /// The last error encountered (if any) + pub last_error: Option, +} + +impl std::fmt::Display for DockerConnectionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Failed to connect to Docker. Tried sockets: [{}]", + self.tried_paths.join(", ") + )?; + if let Some(ref last_err) = self.last_error { + write!(f, ". Last error: {}", last_err)?; + } + Ok(()) + } +} + +impl std::error::Error for DockerConnectionError {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_candidate_socket_paths_includes_standard_path() { + let paths = get_candidate_socket_paths(); + assert!(paths.contains(&PathBuf::from("/var/run/docker.sock"))); + } + + #[test] + fn test_get_candidate_socket_paths_includes_colima_paths() { + if std::env::var("HOME").is_ok() { + let paths = get_candidate_socket_paths(); + let home = std::env::var("HOME").unwrap(); + + assert!(paths.contains(&PathBuf::from(format!("{}/.colima/docker.sock", home)))); + assert!(paths.contains(&PathBuf::from(format!( + "{}/.colima/default/docker.sock", + home + )))); + assert!(paths.contains(&PathBuf::from(format!( + "{}/.colima/default/containerd.sock", + home + )))); + } + } + + #[test] + fn test_docker_connection_error_display() { + let error = DockerConnectionError { + tried_paths: vec![ + "/var/run/docker.sock".to_string(), + "/home/user/.colima/docker.sock".to_string(), + ], + last_error: Some("Connection refused".to_string()), + }; + + let display = format!("{}", error); + assert!(display.contains("/var/run/docker.sock")); + assert!(display.contains(".colima/docker.sock")); + assert!(display.contains("Connection refused")); + } + + // Integration test - only runs if Docker is available + #[tokio::test] + async fn test_connect_to_docker_succeeds_when_docker_available() { + // This test will pass if any Docker socket is available + let result = connect_to_docker(); + + // We can't guarantee Docker is available in CI, so we just verify the function runs + match result { + Ok(connection) => { + // Verify the connection works by pinging + let ping_result = connection.client.ping().await; + assert!( + ping_result.is_ok(), + "Connected to Docker but ping failed: {:?}", + ping_result.err() + ); + + // Verify socket_path is not empty and in expected format + assert!( + !connection.socket_path.is_empty(), + "socket_path should not be empty" + ); + assert!( + connection.socket_path.starts_with("unix://") + || connection.socket_path.starts_with("tcp://") + || connection.socket_path.contains("docker"), + "socket_path should be in expected format: {}", + connection.socket_path + ); + } + Err(e) => { + // If no Docker is available, that's OK - just verify error is informative + assert!(!e.tried_paths.is_empty()); + eprintln!("No Docker available (expected in some environments): {}", e); + } + } + } + + #[test] + fn test_socket_path_format_for_unix_socket() { + // Validates the format logic for Unix sockets + let raw_path = "/var/run/docker.sock"; + let formatted = format!("unix://{}", raw_path); + assert_eq!(formatted, "unix:///var/run/docker.sock"); + } +} diff --git a/src/infra/mod.rs b/src/infra/mod.rs index c350f32..8ae00fa 100644 --- a/src/infra/mod.rs +++ b/src/infra/mod.rs @@ -1,6 +1,7 @@ mod component_factory_impl; mod compose_ast_parser; mod docker_image_builder; +mod docker_socket_discovery; mod dockerfile_ast_parser; mod k8s_manifest_ast_parser; mod scanner_binary_manager; @@ -12,5 +13,6 @@ pub mod lsp_logger; pub use component_factory_impl::ConcreteComponentFactory; pub use compose_ast_parser::parse_compose_file; pub use docker_image_builder::DockerImageBuilder; +pub use docker_socket_discovery::connect_to_docker; pub use dockerfile_ast_parser::parse_dockerfile; pub use k8s_manifest_ast_parser::parse_k8s_manifest; diff --git a/src/infra/sysdig_image_scanner.rs b/src/infra/sysdig_image_scanner.rs index 948a00d..1a9f7c3 100644 --- a/src/infra/sysdig_image_scanner.rs +++ b/src/infra/sysdig_image_scanner.rs @@ -21,6 +21,7 @@ pub struct SysdigImageScanner { url: String, api_token: SysdigAPIToken, scanner_binary_manager: Arc>, + docker_host: Option, } #[derive(Clone, Deserialize)] @@ -68,6 +69,18 @@ impl SysdigImageScanner { url, api_token, scanner_binary_manager: Default::default(), + docker_host: None, + } + } + + /// Creates a new scanner with a specific Docker host. + /// The docker_host should be in DOCKER_HOST format (e.g., "unix:///var/run/docker.sock"). + pub fn with_docker_host(url: String, api_token: SysdigAPIToken, docker_host: String) -> Self { + Self { + url, + api_token, + scanner_binary_manager: Default::default(), + docker_host: Some(docker_host), } } @@ -94,7 +107,13 @@ impl SysdigImageScanner { self.url.as_str(), ]; - let env_vars = [("SECURE_API_TOKEN", self.api_token.0.as_str())]; + // Build environment variables dynamically + let mut env_vars: Vec<(&str, &str)> = vec![("SECURE_API_TOKEN", self.api_token.0.as_str())]; + + // Add DOCKER_HOST if we have a socket path configured + if let Some(ref docker_host) = self.docker_host { + env_vars.push(("DOCKER_HOST", docker_host.as_str())); + } let output = Command::new(path_to_cli) .args(args) From f20632d5f6ce64b1ef1bf3522060677c35081ea3 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Thu, 15 Jan 2026 23:32:18 +0100 Subject: [PATCH 2/3] docs: add docker socket discovery documentation --- AGENTS.md | 6 ++++++ README.md | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index a49cc56..82110a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,6 +89,12 @@ Key components: * **`DockerImageBuilder`** * Builds container images using Bollard (Docker API client). +* **`docker_socket_discovery`** + * Automatically discovers and connects to Docker-compatible sockets. + * Supports multiple socket locations: standard Docker, Colima, Lima, containerd, and Podman. + * Checks sockets in priority order: `DOCKER_HOST` env var, `/var/run/docker.sock`, `$HOME/.colima/docker.sock`, `$HOME/.colima/default/docker.sock`, `$HOME/.colima/default/containerd.sock`, `$HOME/.lima/default/sock/docker.sock`, and `$XDG_RUNTIME_DIR/podman/podman.sock`. + * Uses the first available and connectable socket. + * **Dockerfile / Compose / K8s Manifest AST Parsers** * Parse Dockerfiles to extract image references from `FROM` instructions (including multi-stage builds). * Parse Docker Compose YAML (e.g. service `image:` fields). diff --git a/README.md b/README.md index 0e05da0..acc8203 100644 --- a/README.md +++ b/README.md @@ -114,13 +114,29 @@ The result of the compilation will be saved in `./result/bin`. ## Configuration Options -Sysdig LSP supports two configuration options for connecting to Sysdig’s services: +Sysdig LSP supports two configuration options for connecting to Sysdig's services: | **Option** | **Description** | **Example Value** | |--------------------|------------------------------------------------------------------------------------------------------------|-----------------------------------------| | `sysdig.api_url` | The URL endpoint for Sysdig's API. Set this to your instance's API endpoint. | `https://secure.sysdig.com` | | `sysdig.api_token` | The API token for authentication. If omitted, the `SECURE_API_TOKEN` environment variable is used instead. | `"your token"` (if required) | +### Docker Socket Discovery + +For features that require building Docker images (e.g., "Build and Scan"), Sysdig LSP automatically discovers and connects to available Docker-compatible sockets. The following locations are checked in order: + +| **Priority** | **Socket Path** | **Description** | +|--------------|----------------------------------------------|-------------------------------------------| +| 1 | `DOCKER_HOST` env var | If set, uses the specified socket/URL | +| 2 | `/var/run/docker.sock` | Standard Docker socket (Linux/macOS) | +| 3 | `$HOME/.colima/docker.sock` | Colima Docker socket | +| 4 | `$HOME/.colima/default/docker.sock` | Colima default profile Docker socket | +| 5 | `$HOME/.colima/default/containerd.sock` | Colima containerd socket (Docker-compat) | +| 6 | `$HOME/.lima/default/sock/docker.sock` | Lima Docker socket | +| 7 | `$XDG_RUNTIME_DIR/podman/podman.sock` | Podman socket | + +The first available and connectable socket will be used. If you're using Colima or another Docker-compatible runtime, no additional configuration is needed. + ## Editor Configurations Below are detailed instructions for configuring Sysdig LSP in various editors. From 260bdc94acbd17488f1844028660d8c4de74e9a7 Mon Sep 17 00:00:00 2001 From: Fede Barcelona Date: Thu, 15 Jan 2026 23:32:26 +0100 Subject: [PATCH 3/3] chore: bump version to 0.8.1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4411ea..660c54d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1956,7 +1956,7 @@ dependencies = [ [[package]] name = "sysdig-lsp" -version = "0.8.0" +version = "0.8.1" dependencies = [ "async-trait", "bollard", diff --git a/Cargo.toml b/Cargo.toml index cc2fc54..9681dae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sysdig-lsp" -version = "0.8.0" +version = "0.8.1" edition = "2024" authors = [ "Sysdig Inc." ] readme = "README.md"