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