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