Skip to content
Merged
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
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sysdig-lsp"
version = "0.8.0"
version = "0.8.1"
edition = "2024"
authors = [ "Sysdig Inc." ]
readme = "README.md"
Expand Down
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 Sysdigs 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.
Expand Down
20 changes: 13 additions & 7 deletions src/infra/component_factory_impl.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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),
Expand Down
20 changes: 9 additions & 11 deletions src/infra/docker_image_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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())
Expand All @@ -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())
Expand All @@ -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())
Expand Down
238 changes: 238 additions & 0 deletions src/infra/docker_socket_discovery.rs
Original file line number Diff line number Diff line change
@@ -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<PathBuf> {
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<DockerConnection, DockerConnectionError> {
// 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<String> = 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<String>,
/// The last error encountered (if any)
pub last_error: Option<String>,
}

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");
}
}
Loading