opencode_cloud_core/docker/
mod.rs1mod 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
33pub use client::{DockerClient, DockerEndpoint};
35pub use error::DockerError;
36pub use progress::ProgressReporter;
37
38pub use health::{
40 ExtendedHealthResponse, HealthError, HealthResponse, check_health, check_health_extended,
41};
42
43pub use assets::{ENTRYPOINT_SH, HEALTHCHECK_SH, OPENCODE_CLOUD_BOOTSTRAP_SH};
45pub use dockerfile::{DOCKERFILE, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
46
47pub 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
54pub use update::{UpdateResult, has_previous_image, rollback_image, update_image};
56
57pub use version::{
59 VERSION_LABEL, get_cli_version, get_image_version, get_registry_latest_version,
60 versions_compatible,
61};
62
63pub use exec::{
65 exec_command, exec_command_exit_code, exec_command_with_status, exec_command_with_stdin,
66};
67
68pub 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
74pub 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
82pub 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
115pub use mount::{MountError, ParsedMount, check_container_path_warning, validate_mount_path};
117
118pub 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
125pub use state::{ImageState, clear_state, get_state_path, load_state, save_state};
127
128#[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 volume::ensure_volumes_exist(client).await?;
157
158 let container_id = if container::container_exists(client, &names.container_name).await? {
160 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 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 if !container::container_is_running(client, &names.container_name).await? {
188 container::start_container(client, &names.container_name).await?;
189 }
190
191 users::restore_persisted_users(client, &names.container_name).await?;
193
194 Ok(container_id)
195}
196
197pub const DEFAULT_STOP_TIMEOUT_SECS: i64 = 30;
199
200pub 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 if !container::container_exists(client, name).await? {
217 return Err(DockerError::Container(format!(
218 "Container '{name}' does not exist"
219 )));
220 }
221
222 if container::container_is_running(client, name).await? {
224 container::stop_container(client, name, Some(timeout)).await?;
225 }
226
227 if remove {
229 container::remove_container(client, name, false).await?;
230 }
231
232 Ok(())
233}