Skip to content

Commit 74b7622

Browse files
authored
fix(infra): pass DOCKER_HOST to scanner CLI for multi-runtime support (#34)
Enables docker socket discovery, so the LSP works with colima, lima and podman as well.
1 parent 0558187 commit 74b7622

9 files changed

Lines changed: 307 additions & 22 deletions

AGENTS.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ Key components:
8989
* **`DockerImageBuilder`**
9090
* Builds container images using Bollard (Docker API client).
9191

92+
* **`docker_socket_discovery`**
93+
* Automatically discovers and connects to Docker-compatible sockets.
94+
* Supports multiple socket locations: standard Docker, Colima, Lima, containerd, and Podman.
95+
* 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`.
96+
* Uses the first available and connectable socket.
97+
9298
* **Dockerfile / Compose / K8s Manifest AST Parsers**
9399
* Parse Dockerfiles to extract image references from `FROM` instructions (including multi-stage builds).
94100
* Parse Docker Compose YAML (e.g. service `image:` fields).

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sysdig-lsp"
3-
version = "0.8.0"
3+
version = "0.8.1"
44
edition = "2024"
55
authors = [ "Sysdig Inc." ]
66
readme = "README.md"

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,29 @@ The result of the compilation will be saved in `./result/bin`.
114114

115115
## Configuration Options
116116

117-
Sysdig LSP supports two configuration options for connecting to Sysdigs services:
117+
Sysdig LSP supports two configuration options for connecting to Sysdig's services:
118118

119119
| **Option** | **Description** | **Example Value** |
120120
|--------------------|------------------------------------------------------------------------------------------------------------|-----------------------------------------|
121121
| `sysdig.api_url` | The URL endpoint for Sysdig's API. Set this to your instance's API endpoint. | `https://secure.sysdig.com` |
122122
| `sysdig.api_token` | The API token for authentication. If omitted, the `SECURE_API_TOKEN` environment variable is used instead. | `"your token"` (if required) |
123123

124+
### Docker Socket Discovery
125+
126+
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:
127+
128+
| **Priority** | **Socket Path** | **Description** |
129+
|--------------|----------------------------------------------|-------------------------------------------|
130+
| 1 | `DOCKER_HOST` env var | If set, uses the specified socket/URL |
131+
| 2 | `/var/run/docker.sock` | Standard Docker socket (Linux/macOS) |
132+
| 3 | `$HOME/.colima/docker.sock` | Colima Docker socket |
133+
| 4 | `$HOME/.colima/default/docker.sock` | Colima default profile Docker socket |
134+
| 5 | `$HOME/.colima/default/containerd.sock` | Colima containerd socket (Docker-compat) |
135+
| 6 | `$HOME/.lima/default/sock/docker.sock` | Lima Docker socket |
136+
| 7 | `$XDG_RUNTIME_DIR/podman/podman.sock` | Podman socket |
137+
138+
The first available and connectable socket will be used. If you're using Colima or another Docker-compatible runtime, no additional configuration is needed.
139+
124140
## Editor Configurations
125141

126142
Below are detailed instructions for configuring Sysdig LSP in various editors.

src/infra/component_factory_impl.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
use bollard::Docker;
2-
31
use crate::{
42
app::component_factory::{ComponentFactory, ComponentFactoryError, Components, Config},
5-
infra::{DockerImageBuilder, SysdigAPIToken, SysdigImageScanner},
3+
infra::{DockerImageBuilder, SysdigAPIToken, SysdigImageScanner, connect_to_docker},
64
};
75

86
pub struct ConcreteComponentFactory;
@@ -17,11 +15,19 @@ impl ComponentFactory for ConcreteComponentFactory {
1715
.unwrap_or_else(|| std::env::var("SECURE_API_TOKEN"))
1816
.map(SysdigAPIToken)?;
1917

20-
let scanner = SysdigImageScanner::new(config.sysdig.api_url.clone(), token);
21-
22-
let docker_client = Docker::connect_with_local_defaults()
18+
// Get Docker connection with socket path
19+
let docker_connection = connect_to_docker()
2320
.map_err(|e| ComponentFactoryError::DockerClientError(e.to_string()))?;
24-
let builder = DockerImageBuilder::new(docker_client);
21+
22+
// Create scanner WITH the docker_host so CLI subprocess uses the same socket
23+
let scanner = SysdigImageScanner::with_docker_host(
24+
config.sysdig.api_url.clone(),
25+
token,
26+
docker_connection.socket_path.clone(),
27+
);
28+
29+
// Create builder with the Docker client
30+
let builder = DockerImageBuilder::new(docker_connection.client);
2531

2632
Ok(Components {
2733
scanner: Box::new(scanner),

src/infra/docker_image_builder.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,15 @@ impl ImageBuilder for DockerImageBuilder {
123123
mod tests {
124124
use std::{path::PathBuf, str::FromStr};
125125

126-
use bollard::Docker;
127-
128126
use crate::{
129127
app::{ImageBuildError, ImageBuilder},
130-
infra::DockerImageBuilder,
128+
infra::{DockerImageBuilder, connect_to_docker},
131129
};
132130

133131
#[tokio::test]
134132
async fn it_builds_a_dockerfile() {
135-
let docker_client = Docker::connect_with_local_defaults().unwrap();
136-
let image_builder = DockerImageBuilder::new(docker_client);
133+
let docker_connection = connect_to_docker().unwrap();
134+
let image_builder = DockerImageBuilder::new(docker_connection.client);
137135

138136
let image_built = image_builder
139137
.build_image(&PathBuf::from_str("tests/fixtures/Dockerfile").unwrap())
@@ -150,8 +148,8 @@ mod tests {
150148

151149
#[tokio::test]
152150
async fn it_builds_a_containerfile() {
153-
let docker_client = Docker::connect_with_local_defaults().unwrap();
154-
let image_builder = DockerImageBuilder::new(docker_client);
151+
let docker_connection = connect_to_docker().unwrap();
152+
let image_builder = DockerImageBuilder::new(docker_connection.client);
155153

156154
let image_built = image_builder
157155
.build_image(&PathBuf::from_str("tests/fixtures/Containerfile").unwrap())
@@ -168,8 +166,8 @@ mod tests {
168166

169167
#[tokio::test]
170168
async fn it_fails_to_build_non_existent_dockerfile() {
171-
let docker_client = Docker::connect_with_local_defaults().unwrap();
172-
let image_builder = DockerImageBuilder::new(docker_client);
169+
let docker_connection = connect_to_docker().unwrap();
170+
let image_builder = DockerImageBuilder::new(docker_connection.client);
173171

174172
let image_built = image_builder
175173
.build_image(&PathBuf::from_str("tests/fixtures/Nonexistent.dockerfile").unwrap())
@@ -184,8 +182,8 @@ mod tests {
184182

185183
#[tokio::test]
186184
async fn it_builds_an_invalid_dockerfile_and_fails() {
187-
let docker_client = Docker::connect_with_local_defaults().unwrap();
188-
let image_builder = DockerImageBuilder::new(docker_client);
185+
let docker_connection = connect_to_docker().unwrap();
186+
let image_builder = DockerImageBuilder::new(docker_connection.client);
189187

190188
let image_built = image_builder
191189
.build_image(&PathBuf::from_str("tests/fixtures/Invalid.dockerfile").unwrap())
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
use std::path::PathBuf;
2+
3+
use bollard::Docker;
4+
use tracing::{debug, info, warn};
5+
6+
/// Result of a successful Docker connection, including the socket path used.
7+
pub struct DockerConnection {
8+
/// The connected Docker client
9+
pub client: Docker,
10+
/// The socket path that was used to connect.
11+
/// Format: "unix:///path/to/socket" for Unix sockets, or the DOCKER_HOST value if set.
12+
pub socket_path: String,
13+
}
14+
15+
/// List of Docker socket paths to try, in order of preference.
16+
/// The first successful connection will be used.
17+
fn get_candidate_socket_paths() -> Vec<PathBuf> {
18+
let mut paths = vec![
19+
// Standard Docker socket location (Linux/macOS)
20+
PathBuf::from("/var/run/docker.sock"),
21+
];
22+
23+
// Add Colima socket paths if HOME is available
24+
if let Ok(home) = std::env::var("HOME") {
25+
let home_path = PathBuf::from(&home);
26+
27+
// Colima Docker sockets (various locations)
28+
paths.push(home_path.join(".colima/docker.sock"));
29+
paths.push(home_path.join(".colima/default/docker.sock"));
30+
31+
// Colima containerd socket - Note: This uses Docker-compatible API
32+
// when Colima is configured with Docker compatibility layer
33+
paths.push(home_path.join(".colima/default/containerd.sock"));
34+
35+
// Lima default socket (used by some Colima configurations)
36+
paths.push(home_path.join(".lima/default/sock/docker.sock"));
37+
}
38+
39+
// Podman socket (for potential future compatibility)
40+
if let Ok(xdg_runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
41+
paths.push(PathBuf::from(xdg_runtime_dir).join("podman/podman.sock"));
42+
}
43+
44+
paths
45+
}
46+
47+
/// Attempts to connect to Docker using multiple socket paths.
48+
///
49+
/// This function tries the following in order:
50+
/// 1. `DOCKER_HOST` environment variable (if set)
51+
/// 2. Standard Docker socket at `/var/run/docker.sock`
52+
/// 3. Colima sockets at `$HOME/.colima/docker.sock`, `$HOME/.colima/default/docker.sock`
53+
/// 4. Colima containerd socket at `$HOME/.colima/default/containerd.sock`
54+
/// 5. Lima socket at `$HOME/.lima/default/sock/docker.sock`
55+
///
56+
/// Returns a `DockerConnection` containing both the client and the socket path used,
57+
/// or an error if no socket could be connected.
58+
pub fn connect_to_docker() -> Result<DockerConnection, DockerConnectionError> {
59+
// First, check if DOCKER_HOST is set - if so, use Bollard's default behavior
60+
if let Ok(docker_host) = std::env::var("DOCKER_HOST") {
61+
debug!("DOCKER_HOST environment variable is set: {}", docker_host);
62+
match Docker::connect_with_local_defaults() {
63+
Ok(client) => {
64+
info!("Connected to Docker via DOCKER_HOST: {}", docker_host);
65+
return Ok(DockerConnection {
66+
client,
67+
socket_path: docker_host,
68+
});
69+
}
70+
Err(e) => {
71+
warn!("Failed to connect via DOCKER_HOST ({}): {}", docker_host, e);
72+
// Continue to try other sockets
73+
}
74+
}
75+
}
76+
77+
// Try each candidate socket path
78+
let candidate_paths = get_candidate_socket_paths();
79+
let mut last_error = None;
80+
81+
for socket_path in &candidate_paths {
82+
if !socket_path.exists() {
83+
debug!("Socket path does not exist: {:?}", socket_path);
84+
continue;
85+
}
86+
87+
debug!("Attempting to connect to Docker socket: {:?}", socket_path);
88+
89+
let socket_path_str = match socket_path.to_str() {
90+
Some(s) => s,
91+
None => {
92+
warn!("Invalid socket path (non-UTF8): {:?}", socket_path);
93+
continue;
94+
}
95+
};
96+
97+
match Docker::connect_with_unix(socket_path_str, 120, bollard::API_DEFAULT_VERSION) {
98+
Ok(client) => {
99+
info!("Successfully connected to Docker socket: {:?}", socket_path);
100+
return Ok(DockerConnection {
101+
client,
102+
socket_path: format!("unix://{}", socket_path_str),
103+
});
104+
}
105+
Err(e) => {
106+
debug!("Failed to connect to socket {:?}: {}", socket_path, e);
107+
last_error = Some((socket_path.clone(), e));
108+
}
109+
}
110+
}
111+
112+
// If no socket worked, return an error with helpful information
113+
let tried_paths: Vec<String> = candidate_paths
114+
.iter()
115+
.map(|p| p.display().to_string())
116+
.collect();
117+
118+
Err(DockerConnectionError {
119+
tried_paths,
120+
last_error: last_error.map(|(path, err)| format!("{}: {}", path.display(), err)),
121+
})
122+
}
123+
124+
/// Error returned when no Docker socket could be connected.
125+
#[derive(Debug)]
126+
pub struct DockerConnectionError {
127+
/// List of socket paths that were attempted
128+
pub tried_paths: Vec<String>,
129+
/// The last error encountered (if any)
130+
pub last_error: Option<String>,
131+
}
132+
133+
impl std::fmt::Display for DockerConnectionError {
134+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135+
write!(
136+
f,
137+
"Failed to connect to Docker. Tried sockets: [{}]",
138+
self.tried_paths.join(", ")
139+
)?;
140+
if let Some(ref last_err) = self.last_error {
141+
write!(f, ". Last error: {}", last_err)?;
142+
}
143+
Ok(())
144+
}
145+
}
146+
147+
impl std::error::Error for DockerConnectionError {}
148+
149+
#[cfg(test)]
150+
mod tests {
151+
use super::*;
152+
153+
#[test]
154+
fn test_get_candidate_socket_paths_includes_standard_path() {
155+
let paths = get_candidate_socket_paths();
156+
assert!(paths.contains(&PathBuf::from("/var/run/docker.sock")));
157+
}
158+
159+
#[test]
160+
fn test_get_candidate_socket_paths_includes_colima_paths() {
161+
if std::env::var("HOME").is_ok() {
162+
let paths = get_candidate_socket_paths();
163+
let home = std::env::var("HOME").unwrap();
164+
165+
assert!(paths.contains(&PathBuf::from(format!("{}/.colima/docker.sock", home))));
166+
assert!(paths.contains(&PathBuf::from(format!(
167+
"{}/.colima/default/docker.sock",
168+
home
169+
))));
170+
assert!(paths.contains(&PathBuf::from(format!(
171+
"{}/.colima/default/containerd.sock",
172+
home
173+
))));
174+
}
175+
}
176+
177+
#[test]
178+
fn test_docker_connection_error_display() {
179+
let error = DockerConnectionError {
180+
tried_paths: vec![
181+
"/var/run/docker.sock".to_string(),
182+
"/home/user/.colima/docker.sock".to_string(),
183+
],
184+
last_error: Some("Connection refused".to_string()),
185+
};
186+
187+
let display = format!("{}", error);
188+
assert!(display.contains("/var/run/docker.sock"));
189+
assert!(display.contains(".colima/docker.sock"));
190+
assert!(display.contains("Connection refused"));
191+
}
192+
193+
// Integration test - only runs if Docker is available
194+
#[tokio::test]
195+
async fn test_connect_to_docker_succeeds_when_docker_available() {
196+
// This test will pass if any Docker socket is available
197+
let result = connect_to_docker();
198+
199+
// We can't guarantee Docker is available in CI, so we just verify the function runs
200+
match result {
201+
Ok(connection) => {
202+
// Verify the connection works by pinging
203+
let ping_result = connection.client.ping().await;
204+
assert!(
205+
ping_result.is_ok(),
206+
"Connected to Docker but ping failed: {:?}",
207+
ping_result.err()
208+
);
209+
210+
// Verify socket_path is not empty and in expected format
211+
assert!(
212+
!connection.socket_path.is_empty(),
213+
"socket_path should not be empty"
214+
);
215+
assert!(
216+
connection.socket_path.starts_with("unix://")
217+
|| connection.socket_path.starts_with("tcp://")
218+
|| connection.socket_path.contains("docker"),
219+
"socket_path should be in expected format: {}",
220+
connection.socket_path
221+
);
222+
}
223+
Err(e) => {
224+
// If no Docker is available, that's OK - just verify error is informative
225+
assert!(!e.tried_paths.is_empty());
226+
eprintln!("No Docker available (expected in some environments): {}", e);
227+
}
228+
}
229+
}
230+
231+
#[test]
232+
fn test_socket_path_format_for_unix_socket() {
233+
// Validates the format logic for Unix sockets
234+
let raw_path = "/var/run/docker.sock";
235+
let formatted = format!("unix://{}", raw_path);
236+
assert_eq!(formatted, "unix:///var/run/docker.sock");
237+
}
238+
}

0 commit comments

Comments
 (0)