@opencode-cloud/core 12.0.2 → 13.0.1
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 +11 -11
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/docker/Dockerfile +10 -15
- package/src/docker/container.rs +19 -14
- package/src/docker/error.rs +4 -0
- package/src/docker/image.rs +98 -13
- package/src/docker/mod.rs +1 -1
- package/src/docker/update.rs +5 -5
- package/src/docker/volume.rs +13 -8
- package/src/host/schema.rs +1 -1
- package/src/platform/mod.rs +19 -4
- package/src/platform/systemd.rs +29 -3
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "opencode-cloud-core"
|
|
3
|
-
version = "
|
|
3
|
+
version = "13.0.1"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.89"
|
|
6
6
|
license = "MIT"
|
|
@@ -23,11 +23,11 @@ napi = ["dep:napi", "dep:napi-derive"]
|
|
|
23
23
|
|
|
24
24
|
[dependencies]
|
|
25
25
|
clap = { version = "4.5", features = ["derive"] }
|
|
26
|
-
tokio = { version = "1.
|
|
26
|
+
tokio = { version = "1.49", features = ["rt-multi-thread", "macros"] }
|
|
27
27
|
serde = { version = "1.0", features = ["derive"] }
|
|
28
28
|
serde_json = "1.0"
|
|
29
29
|
jsonc-parser = { version = "0.29", features = ["serde"] }
|
|
30
|
-
directories = "
|
|
30
|
+
directories = "6"
|
|
31
31
|
thiserror = "2"
|
|
32
32
|
anyhow = "1"
|
|
33
33
|
tracing = "0.1"
|
|
@@ -35,25 +35,25 @@ console = "0.16"
|
|
|
35
35
|
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
|
|
36
36
|
|
|
37
37
|
# NAPI dependencies (optional - only for Node bindings)
|
|
38
|
-
napi = { version = "
|
|
39
|
-
napi-derive = { version = "
|
|
38
|
+
napi = { version = "3", features = ["tokio_rt", "napi9"], optional = true }
|
|
39
|
+
napi-derive = { version = "3", optional = true }
|
|
40
40
|
|
|
41
41
|
# Docker integration
|
|
42
|
-
bollard = { version = "0.
|
|
42
|
+
bollard = { version = "0.20.1", features = ["chrono", "buildkit"] }
|
|
43
43
|
futures-util = "0.3"
|
|
44
44
|
tar = "0.4"
|
|
45
|
-
flate2 = "1.
|
|
45
|
+
flate2 = "1.1"
|
|
46
46
|
tokio-retry = "0.3"
|
|
47
|
-
indicatif = { version = "0.
|
|
47
|
+
indicatif = { version = "0.18", features = ["tokio", "futures"] }
|
|
48
48
|
http-body-util = "0.1"
|
|
49
|
-
bytes = "1.
|
|
50
|
-
reqwest = { version = "0.
|
|
49
|
+
bytes = "1.11"
|
|
50
|
+
reqwest = { version = "0.13", default-features = false, features = ["rustls", "webpki-roots", "json"] }
|
|
51
51
|
|
|
52
52
|
# Platform service management (macOS)
|
|
53
53
|
plist = "1.8"
|
|
54
54
|
|
|
55
55
|
# Host management
|
|
56
|
-
whoami = "1
|
|
56
|
+
whoami = "2.1"
|
|
57
57
|
ssh2-config-rs = "0.7.2"
|
|
58
58
|
dirs = "6"
|
|
59
59
|
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://crates.io/crates/opencode-cloud)
|
|
6
6
|
[](https://github.com/pRizz/opencode-cloud/pkgs/container/opencode-cloud-sandbox)
|
|
7
7
|
[](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
|
|
8
|
+
[](https://hub.docker.com/r/prizz/opencode-cloud-sandbox)
|
|
8
9
|
[](https://docs.rs/opencode-cloud)
|
|
9
10
|
[](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
|
|
10
11
|
[](https://opensource.org/licenses/MIT)
|
package/package.json
CHANGED
package/src/docker/Dockerfile
CHANGED
|
@@ -71,7 +71,6 @@ RUN --mount=type=cache,target=/var/lib/apt/lists \
|
|
|
71
71
|
systemd-sysv=257.* \
|
|
72
72
|
dbus=1.16.* \
|
|
73
73
|
# Shell and terminal
|
|
74
|
-
zsh=5.9-* \
|
|
75
74
|
tmux=3.5a-* \
|
|
76
75
|
# Editors
|
|
77
76
|
vim=2:9.1.* \
|
|
@@ -155,7 +154,7 @@ RUN --mount=type=cache,target=/var/lib/apt/lists \
|
|
|
155
154
|
# Create Non-Root User
|
|
156
155
|
# -----------------------------------------------------------------------------
|
|
157
156
|
# Create 'opencode' user with passwordless sudo
|
|
158
|
-
RUN useradd -m -s /bin/
|
|
157
|
+
RUN useradd -m -s /bin/bash -G sudo opencode \
|
|
159
158
|
&& echo "opencode ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/opencode \
|
|
160
159
|
&& chmod 0440 /etc/sudoers.d/opencode
|
|
161
160
|
|
|
@@ -176,27 +175,23 @@ RUN mkdir -p \
|
|
|
176
175
|
ENV PATH="/home/opencode/.local/bin:${PATH}"
|
|
177
176
|
|
|
178
177
|
# -----------------------------------------------------------------------------
|
|
179
|
-
# Shell Setup:
|
|
178
|
+
# Shell Setup: Bash + Starship
|
|
180
179
|
# -----------------------------------------------------------------------------
|
|
181
|
-
# Oh My Zsh - self-managing installer, trusted to handle versions
|
|
182
|
-
# Disabled temporarily to reduce Docker build time.
|
|
183
|
-
# RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
|
|
184
|
-
|
|
185
180
|
# Starship prompt - self-managing installer, trusted to handle versions
|
|
186
181
|
# Disabled temporarily to reduce Docker build time.
|
|
187
182
|
# RUN curl -sS https://starship.rs/install.sh | sh -s -- --yes --bin-dir /home/opencode/.local/bin
|
|
188
183
|
|
|
189
|
-
# Configure
|
|
184
|
+
# Configure bash with starship
|
|
190
185
|
# Disabled temporarily to reduce Docker build time.
|
|
191
|
-
# RUN echo 'eval "$(starship init
|
|
192
|
-
# && echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.
|
|
186
|
+
# RUN echo 'eval "$(starship init bash)"' >> /home/opencode/.bashrc \
|
|
187
|
+
# && echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.bashrc
|
|
193
188
|
|
|
194
189
|
# -----------------------------------------------------------------------------
|
|
195
190
|
# mise: Universal Version Manager
|
|
196
191
|
# -----------------------------------------------------------------------------
|
|
197
192
|
# mise - self-managing installer, trusted to handle versions
|
|
198
193
|
RUN curl https://mise.run | sh \
|
|
199
|
-
&& echo 'eval "$(/home/opencode/.local/bin/mise activate
|
|
194
|
+
&& echo 'eval "$(/home/opencode/.local/bin/mise activate bash)"' >> /home/opencode/.bashrc
|
|
200
195
|
|
|
201
196
|
# Install language runtimes via mise (2026-02-03)
|
|
202
197
|
# - node@25: pinned to major version
|
|
@@ -306,7 +301,7 @@ RUN mkdir -p /home/opencode/.cargo/registry /home/opencode/.cargo/git \
|
|
|
306
301
|
# RUN apt-get update && apt-get install -y --no-install-recommends direnv=2.32.* \
|
|
307
302
|
# && rm -rf /var/lib/apt/lists/*
|
|
308
303
|
# USER opencode
|
|
309
|
-
# RUN echo 'eval "$(direnv hook
|
|
304
|
+
# RUN echo 'eval "$(direnv hook bash)"' >> /home/opencode/.bashrc
|
|
310
305
|
|
|
311
306
|
# Install HTTPie
|
|
312
307
|
# Disabled temporarily to reduce Docker build time.
|
|
@@ -508,10 +503,10 @@ RUN printf '%s\n' \
|
|
|
508
503
|
'alias d="docker"' \
|
|
509
504
|
'alias dc="docker compose"' \
|
|
510
505
|
'' \
|
|
511
|
-
>> /home/opencode/.
|
|
506
|
+
>> /home/opencode/.bashrc
|
|
512
507
|
|
|
513
508
|
# Set up pipx path
|
|
514
|
-
RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.
|
|
509
|
+
RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.bashrc
|
|
515
510
|
|
|
516
511
|
# -----------------------------------------------------------------------------
|
|
517
512
|
# Stage 2: opencode build
|
|
@@ -540,7 +535,7 @@ USER opencode
|
|
|
540
535
|
# commit on the main branch of https://github.com/pRizz/opencode.
|
|
541
536
|
# Update it by running: ./scripts/update-opencode-commit.sh
|
|
542
537
|
# Build opencode from source (BuildKit cache mounts disabled for now)
|
|
543
|
-
RUN OPENCODE_COMMIT="
|
|
538
|
+
RUN OPENCODE_COMMIT="ce0a5f0f2a8abfb9a00e335a41e7cfd3487dbb18" \
|
|
544
539
|
&& rm -rf /tmp/opencode-repo \
|
|
545
540
|
&& git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
|
|
546
541
|
&& cd /tmp/opencode-repo \
|
package/src/docker/container.rs
CHANGED
|
@@ -10,9 +10,9 @@ use super::volume::{
|
|
|
10
10
|
VOLUME_CACHE, VOLUME_CONFIG, VOLUME_PROJECTS, VOLUME_SESSION, VOLUME_STATE, VOLUME_USERS,
|
|
11
11
|
};
|
|
12
12
|
use super::{DockerClient, DockerError};
|
|
13
|
-
use bollard::
|
|
14
|
-
|
|
15
|
-
StopContainerOptions,
|
|
13
|
+
use bollard::models::ContainerCreateBody;
|
|
14
|
+
use bollard::query_parameters::{
|
|
15
|
+
CreateContainerOptions, RemoveContainerOptions, StartContainerOptions, StopContainerOptions,
|
|
16
16
|
};
|
|
17
17
|
use bollard::service::{
|
|
18
18
|
HostConfig, Mount, MountPointTypeEnum, MountTypeEnum, PortBinding, PortMap,
|
|
@@ -155,11 +155,10 @@ pub async fn create_container(
|
|
|
155
155
|
);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
// Create exposed ports
|
|
159
|
-
let mut exposed_ports =
|
|
160
|
-
exposed_ports.insert("3000/tcp".to_string(), HashMap::new());
|
|
158
|
+
// Create exposed ports list (bollard v0.20+ uses Vec<String>)
|
|
159
|
+
let mut exposed_ports = vec!["3000/tcp".to_string()];
|
|
161
160
|
if cockpit_enabled_val {
|
|
162
|
-
exposed_ports.
|
|
161
|
+
exposed_ports.push("9090/tcp".to_string());
|
|
163
162
|
}
|
|
164
163
|
|
|
165
164
|
// Create host config
|
|
@@ -197,6 +196,9 @@ pub async fn create_container(
|
|
|
197
196
|
mounts: Some(mounts),
|
|
198
197
|
port_bindings: Some(port_bindings),
|
|
199
198
|
auto_remove: Some(false),
|
|
199
|
+
// CAP_SETUID and CAP_SETGID required for opencode-broker to spawn
|
|
200
|
+
// PTY processes as different users via setuid/setgid syscalls
|
|
201
|
+
cap_add: Some(vec!["SETUID".to_string(), "SETGID".to_string()]),
|
|
200
202
|
..Default::default()
|
|
201
203
|
}
|
|
202
204
|
};
|
|
@@ -221,8 +223,8 @@ pub async fn create_container(
|
|
|
221
223
|
}
|
|
222
224
|
let final_env = if env.is_empty() { None } else { Some(env) };
|
|
223
225
|
|
|
224
|
-
// Create container config
|
|
225
|
-
let config =
|
|
226
|
+
// Create container config (bollard v0.20+ uses ContainerCreateBody)
|
|
227
|
+
let config = ContainerCreateBody {
|
|
226
228
|
image: Some(image_name.to_string()),
|
|
227
229
|
hostname: Some(CONTAINER_NAME.to_string()),
|
|
228
230
|
working_dir: Some("/home/opencode/workspace".to_string()),
|
|
@@ -234,8 +236,8 @@ pub async fn create_container(
|
|
|
234
236
|
|
|
235
237
|
// Create container
|
|
236
238
|
let options = CreateContainerOptions {
|
|
237
|
-
name: container_name,
|
|
238
|
-
platform:
|
|
239
|
+
name: Some(container_name.to_string()),
|
|
240
|
+
platform: String::new(),
|
|
239
241
|
};
|
|
240
242
|
|
|
241
243
|
let response = client
|
|
@@ -263,7 +265,7 @@ pub async fn start_container(client: &DockerClient, name: &str) -> Result<(), Do
|
|
|
263
265
|
|
|
264
266
|
client
|
|
265
267
|
.inner()
|
|
266
|
-
.start_container(name, None::<StartContainerOptions
|
|
268
|
+
.start_container(name, None::<StartContainerOptions>)
|
|
267
269
|
.await
|
|
268
270
|
.map_err(|e| DockerError::Container(format!("Failed to start container {name}: {e}")))?;
|
|
269
271
|
|
|
@@ -282,10 +284,13 @@ pub async fn stop_container(
|
|
|
282
284
|
name: &str,
|
|
283
285
|
timeout_secs: Option<i64>,
|
|
284
286
|
) -> Result<(), DockerError> {
|
|
285
|
-
let timeout = timeout_secs.unwrap_or(10);
|
|
287
|
+
let timeout = timeout_secs.unwrap_or(10) as i32;
|
|
286
288
|
debug!("Stopping container {} with {}s timeout", name, timeout);
|
|
287
289
|
|
|
288
|
-
let options = StopContainerOptions {
|
|
290
|
+
let options = StopContainerOptions {
|
|
291
|
+
signal: None,
|
|
292
|
+
t: Some(timeout),
|
|
293
|
+
};
|
|
289
294
|
|
|
290
295
|
client
|
|
291
296
|
.inner()
|
package/src/docker/error.rs
CHANGED
|
@@ -34,6 +34,10 @@ pub enum DockerError {
|
|
|
34
34
|
#[error("Docker pull failed: {0}")]
|
|
35
35
|
Pull(String),
|
|
36
36
|
|
|
37
|
+
/// Image operation failed
|
|
38
|
+
#[error("Docker image operation failed: {0}")]
|
|
39
|
+
Image(String),
|
|
40
|
+
|
|
37
41
|
/// Container operation failed
|
|
38
42
|
#[error("Container operation failed: {0}")]
|
|
39
43
|
Container(String),
|
package/src/docker/image.rs
CHANGED
|
@@ -7,14 +7,18 @@ use super::progress::ProgressReporter;
|
|
|
7
7
|
use super::{
|
|
8
8
|
DOCKERFILE, DockerClient, DockerError, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT,
|
|
9
9
|
};
|
|
10
|
-
use bollard::image::{BuildImageOptions, BuilderVersion, CreateImageOptions};
|
|
11
10
|
use bollard::moby::buildkit::v1::StatusResponse as BuildkitStatusResponse;
|
|
12
11
|
use bollard::models::BuildInfoAux;
|
|
12
|
+
use bollard::query_parameters::{
|
|
13
|
+
BuildImageOptions, BuilderVersion, CreateImageOptions, ListImagesOptionsBuilder,
|
|
14
|
+
RemoveImageOptionsBuilder,
|
|
15
|
+
};
|
|
13
16
|
use bytes::Bytes;
|
|
14
17
|
use flate2::Compression;
|
|
15
18
|
use flate2::write::GzEncoder;
|
|
16
19
|
use futures_util::StreamExt;
|
|
17
|
-
use
|
|
20
|
+
use http_body_util::{Either, Full};
|
|
21
|
+
use std::collections::{HashMap, HashSet, VecDeque};
|
|
18
22
|
use std::env;
|
|
19
23
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
20
24
|
use tar::Builder as TarBuilder;
|
|
@@ -66,6 +70,80 @@ pub async fn image_exists(
|
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
/// Remove all images whose tags or digests contain the provided name fragment
|
|
74
|
+
///
|
|
75
|
+
/// Returns the number of image references removed.
|
|
76
|
+
pub async fn remove_images_by_name(
|
|
77
|
+
client: &DockerClient,
|
|
78
|
+
name_fragment: &str,
|
|
79
|
+
force: bool,
|
|
80
|
+
) -> Result<usize, DockerError> {
|
|
81
|
+
debug!("Removing Docker images matching '{name_fragment}'");
|
|
82
|
+
|
|
83
|
+
let images = list_docker_images(client).await?;
|
|
84
|
+
|
|
85
|
+
let references = collect_image_references(&images, name_fragment);
|
|
86
|
+
remove_image_references(client, references, force).await
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// List all local Docker images (including intermediate layers).
|
|
90
|
+
async fn list_docker_images(
|
|
91
|
+
client: &DockerClient,
|
|
92
|
+
) -> Result<Vec<bollard::models::ImageSummary>, DockerError> {
|
|
93
|
+
let list_options = ListImagesOptionsBuilder::new().all(true).build();
|
|
94
|
+
client
|
|
95
|
+
.inner()
|
|
96
|
+
.list_images(Some(list_options))
|
|
97
|
+
.await
|
|
98
|
+
.map_err(|e| DockerError::Image(format!("Failed to list images: {e}")))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Collect tags and digests that contain the provided name fragment.
|
|
102
|
+
fn collect_image_references(
|
|
103
|
+
images: &[bollard::models::ImageSummary],
|
|
104
|
+
name_fragment: &str,
|
|
105
|
+
) -> HashSet<String> {
|
|
106
|
+
let mut references = HashSet::new();
|
|
107
|
+
for image in images {
|
|
108
|
+
for tag in &image.repo_tags {
|
|
109
|
+
if tag != "<none>:<none>" && tag.contains(name_fragment) {
|
|
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
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
references
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Remove image references (tags/digests), returning the number removed.
|
|
124
|
+
async fn remove_image_references(
|
|
125
|
+
client: &DockerClient,
|
|
126
|
+
references: HashSet<String>,
|
|
127
|
+
force: bool,
|
|
128
|
+
) -> Result<usize, DockerError> {
|
|
129
|
+
if references.is_empty() {
|
|
130
|
+
return Ok(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let remove_options = RemoveImageOptionsBuilder::new().force(force).build();
|
|
134
|
+
let mut removed = 0usize;
|
|
135
|
+
for reference in references {
|
|
136
|
+
client
|
|
137
|
+
.inner()
|
|
138
|
+
.remove_image(&reference, Some(remove_options.clone()), None)
|
|
139
|
+
.await
|
|
140
|
+
.map_err(|e| DockerError::Image(format!("Failed to remove image {reference}: {e}")))?;
|
|
141
|
+
removed += 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Ok(removed)
|
|
145
|
+
}
|
|
146
|
+
|
|
69
147
|
/// Build the opencode image from embedded Dockerfile
|
|
70
148
|
///
|
|
71
149
|
/// Shows real-time build progress with streaming output.
|
|
@@ -103,18 +181,20 @@ pub async fn build_image(
|
|
|
103
181
|
);
|
|
104
182
|
let build_args = build_args.unwrap_or_default();
|
|
105
183
|
let options = BuildImageOptions {
|
|
106
|
-
t: full_name.clone(),
|
|
184
|
+
t: Some(full_name.clone()),
|
|
107
185
|
dockerfile: "Dockerfile".to_string(),
|
|
108
186
|
version: BuilderVersion::BuilderBuildKit,
|
|
109
187
|
session: Some(session_id),
|
|
110
188
|
rm: true,
|
|
111
189
|
nocache: no_cache,
|
|
112
|
-
buildargs: build_args,
|
|
190
|
+
buildargs: Some(build_args),
|
|
191
|
+
platform: String::new(),
|
|
192
|
+
target: String::new(),
|
|
113
193
|
..Default::default()
|
|
114
194
|
};
|
|
115
195
|
|
|
116
196
|
// Create build body from context
|
|
117
|
-
let body = Bytes::from(context);
|
|
197
|
+
let body: Either<Full<Bytes>, _> = Either::Left(Full::new(Bytes::from(context)));
|
|
118
198
|
|
|
119
199
|
// Start build with streaming output
|
|
120
200
|
let mut stream = client.inner().build_image(options, None, Some(body));
|
|
@@ -137,10 +217,12 @@ pub async fn build_image(
|
|
|
137
217
|
|
|
138
218
|
handle_stream_message(&info, progress, &mut log_state);
|
|
139
219
|
|
|
140
|
-
if let Some(
|
|
141
|
-
|
|
220
|
+
if let Some(error_detail) = &info.error_detail
|
|
221
|
+
&& let Some(error_msg) = &error_detail.message
|
|
222
|
+
{
|
|
223
|
+
progress.abandon_all(error_msg);
|
|
142
224
|
let context = format_build_error_with_context(
|
|
143
|
-
|
|
225
|
+
error_msg,
|
|
144
226
|
&log_state.recent_logs,
|
|
145
227
|
&log_state.error_logs,
|
|
146
228
|
&log_state.recent_buildkit_logs,
|
|
@@ -620,8 +702,9 @@ async fn do_pull(
|
|
|
620
702
|
let full_name = format!("{image}:{tag}");
|
|
621
703
|
|
|
622
704
|
let options = CreateImageOptions {
|
|
623
|
-
from_image: image,
|
|
624
|
-
tag,
|
|
705
|
+
from_image: Some(image.to_string()),
|
|
706
|
+
tag: Some(tag.to_string()),
|
|
707
|
+
platform: String::new(),
|
|
625
708
|
..Default::default()
|
|
626
709
|
};
|
|
627
710
|
|
|
@@ -634,9 +717,11 @@ async fn do_pull(
|
|
|
634
717
|
match result {
|
|
635
718
|
Ok(info) => {
|
|
636
719
|
// Handle errors from the stream
|
|
637
|
-
if let Some(
|
|
638
|
-
|
|
639
|
-
|
|
720
|
+
if let Some(error_detail) = &info.error_detail
|
|
721
|
+
&& let Some(error_msg) = &error_detail.message
|
|
722
|
+
{
|
|
723
|
+
progress.abandon_all(error_msg);
|
|
724
|
+
return Err(DockerError::Pull(error_msg.to_string()));
|
|
640
725
|
}
|
|
641
726
|
|
|
642
727
|
// Handle layer progress
|
package/src/docker/mod.rs
CHANGED
|
@@ -42,7 +42,7 @@ pub use health::{
|
|
|
42
42
|
pub use dockerfile::{DOCKERFILE, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
|
|
43
43
|
|
|
44
44
|
// Image operations
|
|
45
|
-
pub use image::{build_image, image_exists, pull_image};
|
|
45
|
+
pub use image::{build_image, image_exists, pull_image, remove_images_by_name};
|
|
46
46
|
|
|
47
47
|
// Update operations
|
|
48
48
|
pub use update::{UpdateResult, has_previous_image, rollback_image, update_image};
|
package/src/docker/update.rs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
use super::image::{image_exists, pull_image};
|
|
7
7
|
use super::progress::ProgressReporter;
|
|
8
8
|
use super::{DockerClient, DockerError, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
|
|
9
|
-
use bollard::
|
|
9
|
+
use bollard::query_parameters::TagImageOptions;
|
|
10
10
|
use tracing::debug;
|
|
11
11
|
|
|
12
12
|
/// Tag for the previous image version (used for rollback)
|
|
@@ -45,8 +45,8 @@ pub async fn tag_current_as_previous(client: &DockerClient) -> Result<(), Docker
|
|
|
45
45
|
|
|
46
46
|
// Tag current as previous
|
|
47
47
|
let options = TagImageOptions {
|
|
48
|
-
repo: IMAGE_NAME_GHCR,
|
|
49
|
-
tag: PREVIOUS_TAG,
|
|
48
|
+
repo: Some(IMAGE_NAME_GHCR.to_string()),
|
|
49
|
+
tag: Some(PREVIOUS_TAG.to_string()),
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
client
|
|
@@ -124,8 +124,8 @@ pub async fn rollback_image(client: &DockerClient) -> Result<(), DockerError> {
|
|
|
124
124
|
|
|
125
125
|
// Re-tag previous as latest
|
|
126
126
|
let options = TagImageOptions {
|
|
127
|
-
repo: IMAGE_NAME_GHCR,
|
|
128
|
-
tag: IMAGE_TAG_DEFAULT,
|
|
127
|
+
repo: Some(IMAGE_NAME_GHCR.to_string()),
|
|
128
|
+
tag: Some(IMAGE_TAG_DEFAULT.to_string()),
|
|
129
129
|
};
|
|
130
130
|
|
|
131
131
|
client
|
package/src/docker/volume.rs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
//! for persistent storage across container restarts.
|
|
5
5
|
|
|
6
6
|
use super::{DockerClient, DockerError};
|
|
7
|
-
use bollard::
|
|
7
|
+
use bollard::models::VolumeCreateRequest;
|
|
8
|
+
use bollard::query_parameters::RemoveVolumeOptions;
|
|
8
9
|
use std::collections::HashMap;
|
|
9
10
|
use tracing::debug;
|
|
10
11
|
|
|
@@ -73,12 +74,16 @@ pub async fn ensure_volumes_exist(client: &DockerClient) -> Result<(), DockerErr
|
|
|
73
74
|
async fn ensure_volume_exists(client: &DockerClient, name: &str) -> Result<(), DockerError> {
|
|
74
75
|
debug!("Checking volume: {}", name);
|
|
75
76
|
|
|
76
|
-
// Create volume
|
|
77
|
-
let options =
|
|
78
|
-
name,
|
|
79
|
-
driver: "local",
|
|
80
|
-
driver_opts: HashMap::new(),
|
|
81
|
-
labels: HashMap::from([(
|
|
77
|
+
// Create volume request with default local driver (bollard v0.20+ uses VolumeCreateRequest)
|
|
78
|
+
let options = VolumeCreateRequest {
|
|
79
|
+
name: Some(name.to_string()),
|
|
80
|
+
driver: Some("local".to_string()),
|
|
81
|
+
driver_opts: Some(HashMap::new()),
|
|
82
|
+
labels: Some(HashMap::from([(
|
|
83
|
+
"managed-by".to_string(),
|
|
84
|
+
"opencode-cloud".to_string(),
|
|
85
|
+
)])),
|
|
86
|
+
cluster_volume_spec: None,
|
|
82
87
|
};
|
|
83
88
|
|
|
84
89
|
// create_volume is idempotent - returns existing volume if it exists
|
|
@@ -116,7 +121,7 @@ pub async fn remove_volume(client: &DockerClient, name: &str) -> Result<(), Dock
|
|
|
116
121
|
|
|
117
122
|
client
|
|
118
123
|
.inner()
|
|
119
|
-
.remove_volume(name, None)
|
|
124
|
+
.remove_volume(name, None::<RemoveVolumeOptions>)
|
|
120
125
|
.await
|
|
121
126
|
.map_err(|e| DockerError::Volume(format!("Failed to remove volume {name}: {e}")))?;
|
|
122
127
|
|
package/src/host/schema.rs
CHANGED
package/src/platform/mod.rs
CHANGED
|
@@ -85,9 +85,12 @@ pub trait ServiceManager: Send + Sync {
|
|
|
85
85
|
|
|
86
86
|
/// Get the appropriate service manager for the current platform
|
|
87
87
|
///
|
|
88
|
+
/// # Arguments
|
|
89
|
+
/// * `boot_mode` - "user" for user-level service (default), "system" for system-level
|
|
90
|
+
///
|
|
88
91
|
/// Returns an error if the platform is not supported or if the
|
|
89
92
|
/// service manager implementation is not yet available.
|
|
90
|
-
pub fn get_service_manager() -> Result<Box<dyn ServiceManager>> {
|
|
93
|
+
pub fn get_service_manager(boot_mode: &str) -> Result<Box<dyn ServiceManager>> {
|
|
91
94
|
#[cfg(target_os = "linux")]
|
|
92
95
|
{
|
|
93
96
|
if !systemd::systemd_available() {
|
|
@@ -96,11 +99,11 @@ pub fn get_service_manager() -> Result<Box<dyn ServiceManager>> {
|
|
|
96
99
|
Service registration requires systemd as the init system."
|
|
97
100
|
));
|
|
98
101
|
}
|
|
99
|
-
Ok(Box::new(systemd::SystemdManager::new(
|
|
102
|
+
Ok(Box::new(systemd::SystemdManager::new(boot_mode)))
|
|
100
103
|
}
|
|
101
104
|
#[cfg(target_os = "macos")]
|
|
102
105
|
{
|
|
103
|
-
Ok(Box::new(launchd::LaunchdManager::new(
|
|
106
|
+
Ok(Box::new(launchd::LaunchdManager::new(boot_mode)))
|
|
104
107
|
}
|
|
105
108
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
|
106
109
|
{
|
|
@@ -164,7 +167,7 @@ mod tests {
|
|
|
164
167
|
|
|
165
168
|
#[test]
|
|
166
169
|
fn test_get_service_manager_behavior() {
|
|
167
|
-
let result = get_service_manager();
|
|
170
|
+
let result = get_service_manager("user");
|
|
168
171
|
|
|
169
172
|
// On Linux with systemd: returns Ok(SystemdManager)
|
|
170
173
|
// On Linux without systemd: returns Err (systemd not available)
|
|
@@ -188,4 +191,16 @@ mod tests {
|
|
|
188
191
|
assert!(result.is_err());
|
|
189
192
|
}
|
|
190
193
|
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn test_get_service_manager_respects_boot_mode() {
|
|
197
|
+
// Test that boot_mode parameter is passed through
|
|
198
|
+
let user_result = get_service_manager("user");
|
|
199
|
+
let system_result = get_service_manager("system");
|
|
200
|
+
|
|
201
|
+
// Both should either succeed or fail based on platform support,
|
|
202
|
+
// but they should not panic
|
|
203
|
+
let _ = user_result;
|
|
204
|
+
let _ = system_result;
|
|
205
|
+
}
|
|
191
206
|
}
|
package/src/platform/systemd.rs
CHANGED
|
@@ -130,11 +130,37 @@ pub fn systemd_available() -> bool {
|
|
|
130
130
|
Path::new("/run/systemd/system").exists()
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/// Check if systemd user session is available for the current user
|
|
134
|
+
///
|
|
135
|
+
/// Returns true if XDG_RUNTIME_DIR is set and the user's systemd directory exists.
|
|
136
|
+
/// This is needed for `systemctl --user` commands to work.
|
|
137
|
+
pub fn systemd_user_session_available() -> bool {
|
|
138
|
+
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
|
139
|
+
// Check if the user's systemd directory exists
|
|
140
|
+
Path::new(&runtime_dir).join("systemd").exists()
|
|
141
|
+
} else {
|
|
142
|
+
false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
impl ServiceManager for SystemdManager {
|
|
134
147
|
fn install(&self, config: &ServiceConfig) -> Result<InstallResult> {
|
|
135
|
-
// Check permissions
|
|
136
|
-
if
|
|
137
|
-
//
|
|
148
|
+
// Check permissions and session availability based on mode
|
|
149
|
+
if self.user_mode {
|
|
150
|
+
// User-level installation requires an active systemd user session
|
|
151
|
+
if !systemd_user_session_available() {
|
|
152
|
+
return Err(anyhow!(
|
|
153
|
+
"User-level systemd session not available.\n\
|
|
154
|
+
This typically happens during cloud-init or when running as a \
|
|
155
|
+
different user without an active login session.\n\n\
|
|
156
|
+
Solutions:\n\
|
|
157
|
+
1. Use system-level installation: occ config set boot_mode system\n\
|
|
158
|
+
2. Run the command from an interactive login session\n\
|
|
159
|
+
3. Ensure XDG_RUNTIME_DIR is set and the user has an active systemd session"
|
|
160
|
+
));
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
// System-level installation requires root privileges
|
|
138
164
|
let test_path = self.service_dir().join(".opencode-cloud-test");
|
|
139
165
|
if fs::write(&test_path, "").is_err() {
|
|
140
166
|
return Err(anyhow!(
|