Skip to main content

opencode_cloud_core/docker/
mod.rs

1//! Docker operations module
2//!
3//! This module provides Docker container management functionality including:
4//! - Docker client wrapper with connection handling
5//! - Docker-specific error types
6//! - Embedded Dockerfile for building the opencode image
7//! - Progress reporting for build and pull operations
8//! - Image build and pull operations
9//! - Volume management for persistent storage
10//! - Container lifecycle (create, start, stop, remove)
11//! - Container exec for running commands inside containers
12//! - User management operations (create, delete, lock/unlock users)
13//! - Image update and rollback operations
14
15mod client;
16pub mod container;
17mod dockerfile;
18mod error;
19pub mod exec;
20mod health;
21pub mod image;
22pub mod mount;
23pub mod progress;
24mod registry;
25pub mod state;
26pub mod update;
27pub mod users;
28mod version;
29pub mod volume;
30
31// Core types
32pub use client::{DockerClient, DockerEndpoint};
33pub use error::DockerError;
34pub use progress::ProgressReporter;
35
36// Health check operations
37pub use health::{
38    ExtendedHealthResponse, HealthError, HealthResponse, check_health, check_health_extended,
39};
40
41// Dockerfile constants
42pub use dockerfile::{DOCKERFILE, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
43
44// Image operations
45pub use image::{build_image, image_exists, pull_image, remove_images_by_name};
46
47// Update operations
48pub use update::{UpdateResult, has_previous_image, rollback_image, update_image};
49
50// Version detection
51pub use version::{
52    VERSION_LABEL, get_cli_version, get_image_version, get_registry_latest_version,
53    versions_compatible,
54};
55
56// Container exec operations
57pub use exec::{
58    exec_command, exec_command_exit_code, exec_command_with_status, exec_command_with_stdin,
59};
60
61// User management operations
62pub use users::{
63    UserInfo, create_user, delete_user, list_users, lock_user, persist_user, remove_persisted_user,
64    restore_persisted_users, set_user_password, unlock_user, user_exists,
65};
66
67// Volume management
68pub use volume::{
69    MOUNT_CACHE, MOUNT_CONFIG, MOUNT_PROJECTS, MOUNT_SESSION, MOUNT_STATE, MOUNT_USERS,
70    VOLUME_CACHE, VOLUME_CONFIG, VOLUME_NAMES, VOLUME_PROJECTS, VOLUME_SESSION, VOLUME_STATE,
71    VOLUME_USERS, ensure_volumes_exist, remove_all_volumes, remove_volume, volume_exists,
72};
73
74/// Determine whether the Docker host supports systemd-in-container.
75///
76/// Returns true only for Linux hosts that are not Docker Desktop and not rootless.
77pub async fn docker_supports_systemd(client: &DockerClient) -> Result<bool, DockerError> {
78    let info = client.inner().info().await.map_err(DockerError::from)?;
79
80    let os_type = info.os_type.unwrap_or_default();
81    if os_type.to_lowercase() != "linux" {
82        return Ok(false);
83    }
84
85    let operating_system = match info.operating_system {
86        Some(value) => value,
87        None => return Ok(false),
88    };
89    if operating_system.to_lowercase().contains("docker desktop") {
90        return Ok(false);
91    }
92
93    let security_options = match info.security_options {
94        Some(options) => options,
95        None => return Ok(false),
96    };
97    let is_rootless = security_options
98        .iter()
99        .any(|opt| opt.to_lowercase().contains("name=rootless"));
100    if is_rootless {
101        return Ok(false);
102    }
103
104    Ok(true)
105}
106
107// Bind mount parsing and validation
108pub use mount::{MountError, ParsedMount, check_container_path_warning, validate_mount_path};
109
110// Container lifecycle
111pub use container::{
112    CONTAINER_NAME, ContainerBindMount, ContainerPorts, OPENCODE_WEB_PORT, container_exists,
113    container_is_running, container_state, create_container, get_container_bind_mounts,
114    get_container_ports, remove_container, start_container, stop_container,
115};
116
117// Image state tracking
118pub use state::{ImageState, clear_state, get_state_path, load_state, save_state};
119
120/// Full setup: ensure volumes exist, create container if needed, start it
121///
122/// This is the primary entry point for starting the opencode service.
123/// Returns the container ID on success.
124///
125/// # Arguments
126/// * `client` - Docker client
127/// * `opencode_web_port` - Port to bind on host for opencode web UI (defaults to OPENCODE_WEB_PORT)
128/// * `env_vars` - Additional environment variables (optional)
129/// * `bind_address` - IP address to bind on host (defaults to "127.0.0.1")
130/// * `cockpit_port` - Port to bind on host for Cockpit (defaults to 9090)
131/// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to false)
132/// * `systemd_enabled` - Whether to use systemd as init (defaults to false)
133/// * `bind_mounts` - User-defined bind mounts from config and CLI flags (optional)
134#[allow(clippy::too_many_arguments)]
135pub async fn setup_and_start(
136    client: &DockerClient,
137    opencode_web_port: Option<u16>,
138    env_vars: Option<Vec<String>>,
139    bind_address: Option<&str>,
140    cockpit_port: Option<u16>,
141    cockpit_enabled: Option<bool>,
142    systemd_enabled: Option<bool>,
143    bind_mounts: Option<Vec<mount::ParsedMount>>,
144) -> Result<String, DockerError> {
145    // Ensure volumes exist first
146    volume::ensure_volumes_exist(client).await?;
147
148    // Check if container already exists
149    let container_id = if container::container_exists(client, container::CONTAINER_NAME).await? {
150        // Get existing container ID
151        let info = client
152            .inner()
153            .inspect_container(container::CONTAINER_NAME, None)
154            .await
155            .map_err(|e| {
156                DockerError::Container(format!("Failed to inspect existing container: {e}"))
157            })?;
158        info.id
159            .unwrap_or_else(|| container::CONTAINER_NAME.to_string())
160    } else {
161        // Create new container
162        container::create_container(
163            client,
164            None,
165            None,
166            opencode_web_port,
167            env_vars,
168            bind_address,
169            cockpit_port,
170            cockpit_enabled,
171            systemd_enabled,
172            bind_mounts,
173        )
174        .await?
175    };
176
177    // Start if not running
178    if !container::container_is_running(client, container::CONTAINER_NAME).await? {
179        container::start_container(client, container::CONTAINER_NAME).await?;
180    }
181
182    // Restore persisted users after the container is running
183    users::restore_persisted_users(client, container::CONTAINER_NAME).await?;
184
185    Ok(container_id)
186}
187
188/// Default graceful shutdown timeout in seconds
189pub const DEFAULT_STOP_TIMEOUT_SECS: i64 = 30;
190
191/// Stop and optionally remove the opencode container
192///
193/// # Arguments
194/// * `client` - Docker client
195/// * `remove` - Also remove the container after stopping
196/// * `timeout_secs` - Graceful shutdown timeout (default: 30 seconds)
197pub async fn stop_service(
198    client: &DockerClient,
199    remove: bool,
200    timeout_secs: Option<i64>,
201) -> Result<(), DockerError> {
202    let name = container::CONTAINER_NAME;
203    let timeout = timeout_secs.unwrap_or(DEFAULT_STOP_TIMEOUT_SECS);
204
205    // Check if container exists
206    if !container::container_exists(client, name).await? {
207        return Err(DockerError::Container(format!(
208            "Container '{name}' does not exist"
209        )));
210    }
211
212    // Stop if running
213    if container::container_is_running(client, name).await? {
214        container::stop_container(client, name, Some(timeout)).await?;
215    }
216
217    // Remove if requested
218    if remove {
219        container::remove_container(client, name, false).await?;
220    }
221
222    Ok(())
223}