@opencode-cloud/core 13.0.1 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -1
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/config/schema.rs +0 -3
- package/src/docker/Dockerfile +2 -2
- package/src/docker/client.rs +58 -0
- package/src/docker/container.rs +15 -7
- package/src/docker/image.rs +114 -28
- package/src/docker/mod.rs +39 -2
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -191,6 +191,16 @@ occ logs
|
|
|
191
191
|
# Follow logs in real-time
|
|
192
192
|
occ logs -f
|
|
193
193
|
|
|
194
|
+
# View opencode-broker logs (systemd/journald required)
|
|
195
|
+
occ logs --broker
|
|
196
|
+
|
|
197
|
+
# Dump opencode-broker logs (no follow)
|
|
198
|
+
occ logs --broker --no-follow
|
|
199
|
+
|
|
200
|
+
# Note: Broker logs require systemd/journald. This is enabled by default on supported Linux
|
|
201
|
+
# hosts. Docker Desktop/macOS/Windows use Tini, so broker logs aren't available there.
|
|
202
|
+
# Existing containers may need to be recreated after upgrading.
|
|
203
|
+
|
|
194
204
|
# Stop the service
|
|
195
205
|
occ stop
|
|
196
206
|
|
package/package.json
CHANGED
package/src/config/schema.rs
CHANGED
|
@@ -100,13 +100,10 @@ pub struct Config {
|
|
|
100
100
|
/// Enable Cockpit web console (default: false)
|
|
101
101
|
///
|
|
102
102
|
/// When enabled:
|
|
103
|
-
/// - Container uses systemd as init (required for Cockpit)
|
|
104
103
|
/// - Requires Linux host with native Docker (does NOT work on macOS Docker Desktop)
|
|
105
104
|
/// - Cockpit web UI accessible at cockpit_port
|
|
106
105
|
///
|
|
107
106
|
/// When disabled (default):
|
|
108
|
-
/// - Container uses tini as init (lightweight, works everywhere)
|
|
109
|
-
/// - Works on macOS, Linux, and Windows
|
|
110
107
|
/// - No Cockpit web UI
|
|
111
108
|
#[serde(default = "default_cockpit_enabled")]
|
|
112
109
|
pub cockpit_enabled: bool,
|
package/src/docker/Dockerfile
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
FROM ubuntu:25.10 AS base
|
|
36
36
|
|
|
37
37
|
# OCI Labels for image metadata
|
|
38
|
-
LABEL org.opencontainers.image.title="opencode-cloud"
|
|
38
|
+
LABEL org.opencontainers.image.title="opencode-cloud-sandbox"
|
|
39
39
|
# NOTE: This exact label format is parsed by scripts/extract-oci-description.py
|
|
40
40
|
# (called from .github/workflows/docker-publish.yml) to populate multi-arch
|
|
41
41
|
# manifest annotations for GHCR. If you change this line (format, quoting, or
|
|
@@ -535,7 +535,7 @@ USER opencode
|
|
|
535
535
|
# commit on the main branch of https://github.com/pRizz/opencode.
|
|
536
536
|
# Update it by running: ./scripts/update-opencode-commit.sh
|
|
537
537
|
# Build opencode from source (BuildKit cache mounts disabled for now)
|
|
538
|
-
RUN OPENCODE_COMMIT="
|
|
538
|
+
RUN OPENCODE_COMMIT="3010d7e55e1f26a18684234ea12428d3f91db392" \
|
|
539
539
|
&& rm -rf /tmp/opencode-repo \
|
|
540
540
|
&& git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
|
|
541
541
|
&& cd /tmp/opencode-repo \
|
package/src/docker/client.rs
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
//! errors gracefully and provides clear error messages.
|
|
5
5
|
|
|
6
6
|
use bollard::Docker;
|
|
7
|
+
use std::path::PathBuf;
|
|
7
8
|
use std::time::Duration;
|
|
8
9
|
|
|
9
10
|
use super::error::DockerError;
|
|
10
11
|
use crate::host::{HostConfig, SshTunnel};
|
|
11
12
|
|
|
13
|
+
/// Default Unix socket path used when `DOCKER_HOST` does not specify a socket.
|
|
14
|
+
const DEFAULT_UNIX_SOCKET: &str = "/var/run/docker.sock";
|
|
15
|
+
|
|
12
16
|
/// Docker client wrapper with connection handling
|
|
13
17
|
pub struct DockerClient {
|
|
14
18
|
inner: Docker,
|
|
@@ -16,6 +20,22 @@ pub struct DockerClient {
|
|
|
16
20
|
_tunnel: Option<SshTunnel>,
|
|
17
21
|
/// Host name for remote connections (None = local)
|
|
18
22
|
host_name: Option<String>,
|
|
23
|
+
/// Connection info for raw HTTP calls that bypass Bollard models.
|
|
24
|
+
endpoint: DockerEndpoint,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Docker API endpoint details for raw HTTP calls.
|
|
28
|
+
///
|
|
29
|
+
/// We keep this alongside the Bollard client because some Docker API responses
|
|
30
|
+
/// (notably `/system/df` in API v1.52+) do not deserialize cleanly into the
|
|
31
|
+
/// Bollard-generated models. The CLI uses this endpoint to fetch and parse
|
|
32
|
+
/// those responses directly.
|
|
33
|
+
#[derive(Clone, Debug)]
|
|
34
|
+
pub enum DockerEndpoint {
|
|
35
|
+
/// Unix domain socket path (local Docker).
|
|
36
|
+
Unix(PathBuf),
|
|
37
|
+
/// HTTP base URL (remote Docker via SSH tunnel).
|
|
38
|
+
Http(String),
|
|
19
39
|
}
|
|
20
40
|
|
|
21
41
|
impl DockerClient {
|
|
@@ -24,6 +44,7 @@ impl DockerClient {
|
|
|
24
44
|
/// Uses platform-appropriate socket (Unix socket on Linux/macOS).
|
|
25
45
|
/// Returns a clear error if Docker is not running or accessible.
|
|
26
46
|
pub fn new() -> Result<Self, DockerError> {
|
|
47
|
+
let endpoint = Self::resolve_local_endpoint();
|
|
27
48
|
let docker = Docker::connect_with_local_defaults()
|
|
28
49
|
.map_err(|e| DockerError::Connection(e.to_string()))?;
|
|
29
50
|
|
|
@@ -31,6 +52,7 @@ impl DockerClient {
|
|
|
31
52
|
inner: docker,
|
|
32
53
|
_tunnel: None,
|
|
33
54
|
host_name: None,
|
|
55
|
+
endpoint,
|
|
34
56
|
})
|
|
35
57
|
}
|
|
36
58
|
|
|
@@ -39,6 +61,7 @@ impl DockerClient {
|
|
|
39
61
|
/// Use for long-running operations like image builds.
|
|
40
62
|
/// Default timeout is 120 seconds; build timeout should be 600+ seconds.
|
|
41
63
|
pub fn with_timeout(timeout_secs: u64) -> Result<Self, DockerError> {
|
|
64
|
+
let endpoint = Self::resolve_local_endpoint();
|
|
42
65
|
let docker = Docker::connect_with_local_defaults()
|
|
43
66
|
.map_err(|e| DockerError::Connection(e.to_string()))?
|
|
44
67
|
.with_timeout(Duration::from_secs(timeout_secs));
|
|
@@ -47,6 +70,7 @@ impl DockerClient {
|
|
|
47
70
|
inner: docker,
|
|
48
71
|
_tunnel: None,
|
|
49
72
|
host_name: None,
|
|
73
|
+
endpoint,
|
|
50
74
|
})
|
|
51
75
|
}
|
|
52
76
|
|
|
@@ -62,6 +86,7 @@ impl DockerClient {
|
|
|
62
86
|
// Create SSH tunnel
|
|
63
87
|
let tunnel = SshTunnel::new(host, host_name)
|
|
64
88
|
.map_err(|e| DockerError::Connection(format!("SSH tunnel failed: {e}")))?;
|
|
89
|
+
let endpoint = Self::endpoint_from_tunnel(&tunnel);
|
|
65
90
|
|
|
66
91
|
// Wait for tunnel to be ready with exponential backoff
|
|
67
92
|
tunnel
|
|
@@ -94,6 +119,7 @@ impl DockerClient {
|
|
|
94
119
|
inner: docker,
|
|
95
120
|
_tunnel: Some(tunnel),
|
|
96
121
|
host_name: Some(host_name.to_string()),
|
|
122
|
+
endpoint,
|
|
97
123
|
});
|
|
98
124
|
}
|
|
99
125
|
Err(e) => {
|
|
@@ -124,6 +150,7 @@ impl DockerClient {
|
|
|
124
150
|
) -> Result<Self, DockerError> {
|
|
125
151
|
let tunnel = SshTunnel::new(host, host_name)
|
|
126
152
|
.map_err(|e| DockerError::Connection(format!("SSH tunnel failed: {e}")))?;
|
|
153
|
+
let endpoint = Self::endpoint_from_tunnel(&tunnel);
|
|
127
154
|
|
|
128
155
|
tunnel
|
|
129
156
|
.wait_ready()
|
|
@@ -143,6 +170,7 @@ impl DockerClient {
|
|
|
143
170
|
inner: docker,
|
|
144
171
|
_tunnel: Some(tunnel),
|
|
145
172
|
host_name: Some(host_name.to_string()),
|
|
173
|
+
endpoint,
|
|
146
174
|
})
|
|
147
175
|
}
|
|
148
176
|
|
|
@@ -177,10 +205,40 @@ impl DockerClient {
|
|
|
177
205
|
self._tunnel.is_some()
|
|
178
206
|
}
|
|
179
207
|
|
|
208
|
+
/// Return the endpoint details used for raw Docker API calls.
|
|
209
|
+
///
|
|
210
|
+
/// This exists to support endpoints whose response schemas are newer than
|
|
211
|
+
/// the Bollard-generated models (e.g., `/system/df` in newer Docker APIs).
|
|
212
|
+
pub fn endpoint(&self) -> &DockerEndpoint {
|
|
213
|
+
&self.endpoint
|
|
214
|
+
}
|
|
215
|
+
|
|
180
216
|
/// Access inner Bollard client for advanced operations
|
|
181
217
|
pub fn inner(&self) -> &Docker {
|
|
182
218
|
&self.inner
|
|
183
219
|
}
|
|
220
|
+
|
|
221
|
+
/// Resolve the local Unix socket path used by the Docker client.
|
|
222
|
+
///
|
|
223
|
+
/// Bollard's `connect_with_local_defaults` only honors `DOCKER_HOST` when it
|
|
224
|
+
/// starts with `unix://`. We mirror that logic so our raw HTTP calls use the
|
|
225
|
+
/// same socket, which is necessary because Bollard's `/system/df` models
|
|
226
|
+
/// don't match newer Docker API responses.
|
|
227
|
+
fn resolve_local_endpoint() -> DockerEndpoint {
|
|
228
|
+
let socket = std::env::var("DOCKER_HOST")
|
|
229
|
+
.ok()
|
|
230
|
+
.and_then(|host| host.strip_prefix("unix://").map(|path| path.to_string()))
|
|
231
|
+
.unwrap_or_else(|| DEFAULT_UNIX_SOCKET.to_string());
|
|
232
|
+
DockerEndpoint::Unix(PathBuf::from(socket))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Build an HTTP base URL for a Docker daemon reachable via SSH tunnel.
|
|
236
|
+
///
|
|
237
|
+
/// We store this so the CLI can query `/system/df` directly when Bollard's
|
|
238
|
+
/// data-usage models are out of date.
|
|
239
|
+
fn endpoint_from_tunnel(tunnel: &SshTunnel) -> DockerEndpoint {
|
|
240
|
+
DockerEndpoint::Http(format!("http://127.0.0.1:{}", tunnel.local_port()))
|
|
241
|
+
}
|
|
184
242
|
}
|
|
185
243
|
|
|
186
244
|
#[cfg(test)]
|
package/src/docker/container.rs
CHANGED
|
@@ -45,6 +45,7 @@ fn has_env_key(env: &[String], key: &str) -> bool {
|
|
|
45
45
|
/// * `bind_address` - IP address to bind on host (defaults to "127.0.0.1")
|
|
46
46
|
/// * `cockpit_port` - Port to bind on host for Cockpit (defaults to 9090)
|
|
47
47
|
/// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to false)
|
|
48
|
+
/// * `systemd_enabled` - Whether to use systemd as init (defaults to false)
|
|
48
49
|
/// * `bind_mounts` - User-defined bind mounts from config and CLI flags (optional)
|
|
49
50
|
#[allow(clippy::too_many_arguments)]
|
|
50
51
|
pub async fn create_container(
|
|
@@ -56,6 +57,7 @@ pub async fn create_container(
|
|
|
56
57
|
bind_address: Option<&str>,
|
|
57
58
|
cockpit_port: Option<u16>,
|
|
58
59
|
cockpit_enabled: Option<bool>,
|
|
60
|
+
systemd_enabled: Option<bool>,
|
|
59
61
|
bind_mounts: Option<Vec<ParsedMount>>,
|
|
60
62
|
) -> Result<String, DockerError> {
|
|
61
63
|
let container_name = name.unwrap_or(CONTAINER_NAME);
|
|
@@ -64,10 +66,16 @@ pub async fn create_container(
|
|
|
64
66
|
let port = opencode_web_port.unwrap_or(OPENCODE_WEB_PORT);
|
|
65
67
|
let cockpit_port_val = cockpit_port.unwrap_or(9090);
|
|
66
68
|
let cockpit_enabled_val = cockpit_enabled.unwrap_or(false);
|
|
69
|
+
let systemd_enabled_val = systemd_enabled.unwrap_or(false);
|
|
67
70
|
|
|
68
71
|
debug!(
|
|
69
|
-
"Creating container {} from image {} with port {} and cockpit_port {} (enabled: {})",
|
|
70
|
-
container_name,
|
|
72
|
+
"Creating container {} from image {} with port {} and cockpit_port {} (enabled: {}, systemd: {})",
|
|
73
|
+
container_name,
|
|
74
|
+
image_name,
|
|
75
|
+
port,
|
|
76
|
+
cockpit_port_val,
|
|
77
|
+
cockpit_enabled_val,
|
|
78
|
+
systemd_enabled_val
|
|
71
79
|
);
|
|
72
80
|
|
|
73
81
|
// Check if container already exists
|
|
@@ -162,9 +170,9 @@ pub async fn create_container(
|
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
// Create host config
|
|
165
|
-
// When
|
|
166
|
-
// When
|
|
167
|
-
let host_config = if
|
|
173
|
+
// When systemd is enabled, add systemd-specific settings (requires Linux host)
|
|
174
|
+
// When systemd is disabled, use simpler tini-based config (works everywhere)
|
|
175
|
+
let host_config = if systemd_enabled_val {
|
|
168
176
|
HostConfig {
|
|
169
177
|
mounts: Some(mounts),
|
|
170
178
|
port_bindings: Some(port_bindings),
|
|
@@ -217,8 +225,8 @@ pub async fn create_container(
|
|
|
217
225
|
if !has_env_key(&env, "XDG_CACHE_HOME") {
|
|
218
226
|
env.push("XDG_CACHE_HOME=/home/opencode/.cache".to_string());
|
|
219
227
|
}
|
|
220
|
-
// Add USE_SYSTEMD=1 when
|
|
221
|
-
if
|
|
228
|
+
// Add USE_SYSTEMD=1 when systemd is enabled to tell entrypoint to use systemd
|
|
229
|
+
if systemd_enabled_val && !has_env_key(&env, "USE_SYSTEMD") {
|
|
222
230
|
env.push("USE_SYSTEMD=1".to_string());
|
|
223
231
|
}
|
|
224
232
|
let final_env = if env.is_empty() { None } else { Some(env) };
|
package/src/docker/image.rs
CHANGED
|
@@ -70,9 +70,9 @@ pub async fn image_exists(
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/// Remove all images whose tags or
|
|
73
|
+
/// Remove all images whose tags, digests, or labels match the provided name fragment
|
|
74
74
|
///
|
|
75
|
-
/// Returns the number of
|
|
75
|
+
/// Returns the number of images removed.
|
|
76
76
|
pub async fn remove_images_by_name(
|
|
77
77
|
client: &DockerClient,
|
|
78
78
|
name_fragment: &str,
|
|
@@ -82,8 +82,8 @@ pub async fn remove_images_by_name(
|
|
|
82
82
|
|
|
83
83
|
let images = list_docker_images(client).await?;
|
|
84
84
|
|
|
85
|
-
let
|
|
86
|
-
|
|
85
|
+
let image_ids = collect_image_ids(&images, name_fragment);
|
|
86
|
+
remove_image_ids(client, image_ids, force).await
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/// List all local Docker images (including intermediate layers).
|
|
@@ -98,47 +98,87 @@ async fn list_docker_images(
|
|
|
98
98
|
.map_err(|e| DockerError::Image(format!("Failed to list images: {e}")))
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
const LABEL_TITLE: &str = "org.opencontainers.image.title";
|
|
102
|
+
const LABEL_SOURCE: &str = "org.opencontainers.image.source";
|
|
103
|
+
const LABEL_URL: &str = "org.opencontainers.image.url";
|
|
104
|
+
|
|
105
|
+
const LABEL_TITLE_VALUE: &str = "opencode-cloud-sandbox";
|
|
106
|
+
const LABEL_SOURCE_VALUE: &str = "https://github.com/pRizz/opencode-cloud";
|
|
107
|
+
const LABEL_URL_VALUE: &str = "https://github.com/pRizz/opencode-cloud";
|
|
108
|
+
|
|
109
|
+
/// Collect image IDs that contain the provided name fragment or match opencode labels.
|
|
110
|
+
fn collect_image_ids(
|
|
103
111
|
images: &[bollard::models::ImageSummary],
|
|
104
112
|
name_fragment: &str,
|
|
105
113
|
) -> HashSet<String> {
|
|
106
|
-
let mut
|
|
114
|
+
let mut image_ids = HashSet::new();
|
|
107
115
|
for image in images {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
references.insert(tag.to_string());
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
for digest in &image.repo_digests {
|
|
115
|
-
if digest.contains(name_fragment) {
|
|
116
|
-
references.insert(digest.to_string());
|
|
117
|
-
}
|
|
116
|
+
if image_matches_fragment_or_labels(image, name_fragment) {
|
|
117
|
+
image_ids.insert(image.id.clone());
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
image_ids
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn image_matches_fragment_or_labels(
|
|
124
|
+
image: &bollard::models::ImageSummary,
|
|
125
|
+
name_fragment: &str,
|
|
126
|
+
) -> bool {
|
|
127
|
+
let tag_match = image
|
|
128
|
+
.repo_tags
|
|
129
|
+
.iter()
|
|
130
|
+
.any(|tag| tag != "<none>:<none>" && tag.contains(name_fragment));
|
|
131
|
+
let digest_match = image
|
|
132
|
+
.repo_digests
|
|
133
|
+
.iter()
|
|
134
|
+
.any(|digest| digest.contains(name_fragment));
|
|
135
|
+
let label_match = image_labels_match(&image.labels);
|
|
136
|
+
|
|
137
|
+
tag_match || digest_match || label_match
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
fn image_labels_match(labels: &HashMap<String, String>) -> bool {
|
|
141
|
+
labels
|
|
142
|
+
.get(LABEL_SOURCE)
|
|
143
|
+
.is_some_and(|value| value == LABEL_SOURCE_VALUE)
|
|
144
|
+
|| labels
|
|
145
|
+
.get(LABEL_URL)
|
|
146
|
+
.is_some_and(|value| value == LABEL_URL_VALUE)
|
|
147
|
+
|| labels
|
|
148
|
+
.get(LABEL_TITLE)
|
|
149
|
+
.is_some_and(|value| value == LABEL_TITLE_VALUE)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Remove image IDs, returning the number removed.
|
|
153
|
+
async fn remove_image_ids(
|
|
125
154
|
client: &DockerClient,
|
|
126
|
-
|
|
155
|
+
image_ids: HashSet<String>,
|
|
127
156
|
force: bool,
|
|
128
157
|
) -> Result<usize, DockerError> {
|
|
129
|
-
if
|
|
158
|
+
if image_ids.is_empty() {
|
|
130
159
|
return Ok(0);
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
let remove_options = RemoveImageOptionsBuilder::new().force(force).build();
|
|
134
163
|
let mut removed = 0usize;
|
|
135
|
-
for
|
|
136
|
-
client
|
|
164
|
+
for image_id in image_ids {
|
|
165
|
+
let result = client
|
|
137
166
|
.inner()
|
|
138
|
-
.remove_image(&
|
|
139
|
-
.await
|
|
140
|
-
|
|
141
|
-
|
|
167
|
+
.remove_image(&image_id, Some(remove_options.clone()), None)
|
|
168
|
+
.await;
|
|
169
|
+
match result {
|
|
170
|
+
Ok(_) => removed += 1,
|
|
171
|
+
Err(bollard::errors::Error::DockerResponseServerError {
|
|
172
|
+
status_code: 404, ..
|
|
173
|
+
}) => {
|
|
174
|
+
debug!("Docker image already removed: {}", image_id);
|
|
175
|
+
}
|
|
176
|
+
Err(err) => {
|
|
177
|
+
return Err(DockerError::Image(format!(
|
|
178
|
+
"Failed to remove image {image_id}: {err}"
|
|
179
|
+
)));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
142
182
|
}
|
|
143
183
|
|
|
144
184
|
Ok(removed)
|
|
@@ -867,6 +907,32 @@ fn create_build_context() -> Result<Vec<u8>, std::io::Error> {
|
|
|
867
907
|
#[cfg(test)]
|
|
868
908
|
mod tests {
|
|
869
909
|
use super::*;
|
|
910
|
+
use bollard::models::ImageSummary;
|
|
911
|
+
use std::collections::HashMap;
|
|
912
|
+
|
|
913
|
+
fn make_image_summary(
|
|
914
|
+
id: &str,
|
|
915
|
+
tags: Vec<&str>,
|
|
916
|
+
digests: Vec<&str>,
|
|
917
|
+
labels: HashMap<String, String>,
|
|
918
|
+
) -> ImageSummary {
|
|
919
|
+
ImageSummary {
|
|
920
|
+
id: id.to_string(),
|
|
921
|
+
parent_id: String::new(),
|
|
922
|
+
repo_tags: tags.into_iter().map(|tag| tag.to_string()).collect(),
|
|
923
|
+
repo_digests: digests
|
|
924
|
+
.into_iter()
|
|
925
|
+
.map(|digest| digest.to_string())
|
|
926
|
+
.collect(),
|
|
927
|
+
created: 0,
|
|
928
|
+
size: 0,
|
|
929
|
+
shared_size: -1,
|
|
930
|
+
labels,
|
|
931
|
+
containers: 0,
|
|
932
|
+
manifests: None,
|
|
933
|
+
descriptor: None,
|
|
934
|
+
}
|
|
935
|
+
}
|
|
870
936
|
|
|
871
937
|
#[test]
|
|
872
938
|
fn create_build_context_succeeds() {
|
|
@@ -980,4 +1046,24 @@ mod tests {
|
|
|
980
1046
|
assert!(!is_error_line("Compiling foo v1.0"));
|
|
981
1047
|
assert!(!is_error_line("Successfully installed"));
|
|
982
1048
|
}
|
|
1049
|
+
|
|
1050
|
+
#[test]
|
|
1051
|
+
fn collect_image_ids_matches_labels() {
|
|
1052
|
+
let mut labels = HashMap::new();
|
|
1053
|
+
labels.insert(LABEL_SOURCE.to_string(), LABEL_SOURCE_VALUE.to_string());
|
|
1054
|
+
|
|
1055
|
+
let images = vec![
|
|
1056
|
+
make_image_summary("sha256:opencode", vec![], vec![], labels),
|
|
1057
|
+
make_image_summary(
|
|
1058
|
+
"sha256:other",
|
|
1059
|
+
vec!["busybox:latest"],
|
|
1060
|
+
vec![],
|
|
1061
|
+
HashMap::new(),
|
|
1062
|
+
),
|
|
1063
|
+
];
|
|
1064
|
+
|
|
1065
|
+
let ids = collect_image_ids(&images, "opencode-cloud-sandbox");
|
|
1066
|
+
assert!(ids.contains("sha256:opencode"));
|
|
1067
|
+
assert!(!ids.contains("sha256:other"));
|
|
1068
|
+
}
|
|
983
1069
|
}
|
package/src/docker/mod.rs
CHANGED
|
@@ -29,7 +29,7 @@ mod version;
|
|
|
29
29
|
pub mod volume;
|
|
30
30
|
|
|
31
31
|
// Core types
|
|
32
|
-
pub use client::DockerClient;
|
|
32
|
+
pub use client::{DockerClient, DockerEndpoint};
|
|
33
33
|
pub use error::DockerError;
|
|
34
34
|
pub use progress::ProgressReporter;
|
|
35
35
|
|
|
@@ -71,6 +71,39 @@ pub use volume::{
|
|
|
71
71
|
VOLUME_USERS, ensure_volumes_exist, remove_all_volumes, remove_volume, volume_exists,
|
|
72
72
|
};
|
|
73
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.
|
|
77
|
+
pub 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
|
+
|
|
74
107
|
// Bind mount parsing and validation
|
|
75
108
|
pub use mount::{MountError, ParsedMount, check_container_path_warning, validate_mount_path};
|
|
76
109
|
|
|
@@ -95,8 +128,10 @@ pub use state::{ImageState, clear_state, get_state_path, load_state, save_state}
|
|
|
95
128
|
/// * `env_vars` - Additional environment variables (optional)
|
|
96
129
|
/// * `bind_address` - IP address to bind on host (defaults to "127.0.0.1")
|
|
97
130
|
/// * `cockpit_port` - Port to bind on host for Cockpit (defaults to 9090)
|
|
98
|
-
/// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to
|
|
131
|
+
/// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to false)
|
|
132
|
+
/// * `systemd_enabled` - Whether to use systemd as init (defaults to false)
|
|
99
133
|
/// * `bind_mounts` - User-defined bind mounts from config and CLI flags (optional)
|
|
134
|
+
#[allow(clippy::too_many_arguments)]
|
|
100
135
|
pub async fn setup_and_start(
|
|
101
136
|
client: &DockerClient,
|
|
102
137
|
opencode_web_port: Option<u16>,
|
|
@@ -104,6 +139,7 @@ pub async fn setup_and_start(
|
|
|
104
139
|
bind_address: Option<&str>,
|
|
105
140
|
cockpit_port: Option<u16>,
|
|
106
141
|
cockpit_enabled: Option<bool>,
|
|
142
|
+
systemd_enabled: Option<bool>,
|
|
107
143
|
bind_mounts: Option<Vec<mount::ParsedMount>>,
|
|
108
144
|
) -> Result<String, DockerError> {
|
|
109
145
|
// Ensure volumes exist first
|
|
@@ -132,6 +168,7 @@ pub async fn setup_and_start(
|
|
|
132
168
|
bind_address,
|
|
133
169
|
cockpit_port,
|
|
134
170
|
cockpit_enabled,
|
|
171
|
+
systemd_enabled,
|
|
135
172
|
bind_mounts,
|
|
136
173
|
)
|
|
137
174
|
.await?
|