@opencode-cloud/core 4.0.0 → 4.0.2
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 +9 -2
- package/package.json +1 -1
- package/src/config/schema.rs +5 -2
- package/src/docker/Dockerfile +18 -96
- package/src/docker/README.dockerhub.md +10 -2
- package/src/docker/container.rs +5 -5
- package/src/docker/exec.rs +2 -2
- package/src/docker/health.rs +24 -4
- package/src/docker/image.rs +64 -18
- package/src/docker/mount.rs +3 -3
- package/src/docker/users.rs +2 -2
- package/src/docker/volume.rs +12 -12
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
[](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
|
|
11
|
+
> [!WARNING]
|
|
12
|
+
> This project is a work in progress and evolving rapidly. Use with caution.
|
|
13
|
+
|
|
11
14
|
A production-ready toolkit for deploying and managing [opencode](https://github.com/anomalyco/opencode) as a persistent cloud service, **sandboxed inside a Docker container** for isolation and security.
|
|
12
15
|
|
|
16
|
+
This project uses the opencode fork at https://github.com/pRizz/opencode, which adds additional authentication and security features.
|
|
17
|
+
|
|
13
18
|
## Quick install (cargo)
|
|
14
19
|
|
|
15
20
|
```bash
|
|
@@ -19,12 +24,14 @@ opencode-cloud --version
|
|
|
19
24
|
|
|
20
25
|
## Deploy to AWS
|
|
21
26
|
|
|
22
|
-
[](https://console.aws.amazon.com/cloudformation/home#/stacks/create/review?templateURL=https://
|
|
27
|
+
[](https://console.aws.amazon.com/cloudformation/home#/stacks/create/review?templateURL=https://opencode-cloud-templates.s3.us-east-2.amazonaws.com/cloudformation/opencode-cloud-quick.yaml)
|
|
23
28
|
|
|
24
29
|
Quick deploy provisions a private EC2 instance behind a public ALB with HTTPS.
|
|
25
30
|
**A domain name is required** for ACM certificate validation.
|
|
31
|
+
**A Route53 hosted zone ID is required** for automated DNS validation.
|
|
26
32
|
|
|
27
|
-
Docs: `docs/deploy/aws.md` (includes teardown steps)
|
|
33
|
+
Docs: `docs/deploy/aws.md` (includes teardown steps and S3 hosting setup for forks)
|
|
34
|
+
Credentials: `docs/deploy/aws.md#retrieving-credentials`
|
|
28
35
|
|
|
29
36
|
## Features
|
|
30
37
|
|
package/package.json
CHANGED
package/src/config/schema.rs
CHANGED
|
@@ -700,7 +700,7 @@ mod tests {
|
|
|
700
700
|
fn test_serialize_deserialize_with_mounts() {
|
|
701
701
|
let config = Config {
|
|
702
702
|
mounts: vec![
|
|
703
|
-
"/home/user/data:/workspace/data".to_string(),
|
|
703
|
+
"/home/user/data:/home/opencode/workspace/data".to_string(),
|
|
704
704
|
"/home/user/config:/etc/app:ro".to_string(),
|
|
705
705
|
],
|
|
706
706
|
..Config::default()
|
|
@@ -708,7 +708,10 @@ mod tests {
|
|
|
708
708
|
let json = serde_json::to_string(&config).unwrap();
|
|
709
709
|
let parsed: Config = serde_json::from_str(&json).unwrap();
|
|
710
710
|
assert_eq!(parsed.mounts.len(), 2);
|
|
711
|
-
assert_eq!(
|
|
711
|
+
assert_eq!(
|
|
712
|
+
parsed.mounts[0],
|
|
713
|
+
"/home/user/data:/home/opencode/workspace/data"
|
|
714
|
+
);
|
|
712
715
|
assert_eq!(parsed.mounts[1], "/home/user/config:/etc/app:ro");
|
|
713
716
|
}
|
|
714
717
|
|
package/src/docker/Dockerfile
CHANGED
|
@@ -104,8 +104,6 @@ RUN --mount=type=cache,target=/var/lib/apt/lists \
|
|
|
104
104
|
netcat-openbsd=1.226-* \
|
|
105
105
|
iputils-ping=3:20240117-* \
|
|
106
106
|
dnsutils=1:9.18.* \
|
|
107
|
-
# Reverse proxy for opencode UI + API
|
|
108
|
-
nginx=1.24.* \
|
|
109
107
|
# Compression
|
|
110
108
|
zip=3.0-* \
|
|
111
109
|
unzip=6.0-* \
|
|
@@ -505,7 +503,7 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
|
|
|
505
503
|
# This block includes:
|
|
506
504
|
# - opencode build (backend binary) + app build (frontend dist)
|
|
507
505
|
# - opencode-broker build
|
|
508
|
-
# -
|
|
506
|
+
# - opencode web build + runtime
|
|
509
507
|
# - PAM configuration + systemd services
|
|
510
508
|
# - opencode config file
|
|
511
509
|
#
|
|
@@ -517,7 +515,7 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
|
|
|
517
515
|
# Clone the fork and build opencode from source (as non-root user)
|
|
518
516
|
# Pin to specific commit for reproducibility
|
|
519
517
|
# Build opencode from source (BuildKit cache mounts disabled for now)
|
|
520
|
-
RUN OPENCODE_COMMIT="
|
|
518
|
+
RUN OPENCODE_COMMIT="9b91eb17f5ca1b0ee99cfaa0b4c87da6dbe9e784" \
|
|
521
519
|
&& rm -rf /tmp/opencode-repo \
|
|
522
520
|
&& git clone --depth 1 https://github.com/pRizz/opencode.git /tmp/opencode-repo \
|
|
523
521
|
&& cd /tmp/opencode-repo \
|
|
@@ -526,13 +524,15 @@ RUN OPENCODE_COMMIT="3a4eccc7e883575e0d5a508f46036a9f243c06e8" \
|
|
|
526
524
|
&& curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \
|
|
527
525
|
&& export PATH="/home/opencode/.bun/bin:${PATH}" \
|
|
528
526
|
&& bun install --frozen-lockfile \
|
|
529
|
-
&&
|
|
530
|
-
&&
|
|
531
|
-
&& bun run build \
|
|
527
|
+
&& cd packages/opencode \
|
|
528
|
+
&& export VITE_OPENCODE_SERVER_URL="http://localhost:3000" \
|
|
529
|
+
&& bun run build-single-ui \
|
|
532
530
|
&& rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun \
|
|
533
531
|
&& cd /tmp/opencode-repo \
|
|
534
532
|
&& mkdir -p /home/opencode/.local/share/opencode/bin \
|
|
533
|
+
&& mkdir -p /home/opencode/.local/share/opencode/ui \
|
|
535
534
|
&& cp /tmp/opencode-repo/packages/opencode/dist/opencode-*/bin/opencode /home/opencode/.local/share/opencode/bin/opencode \
|
|
535
|
+
&& cp -R /tmp/opencode-repo/packages/opencode/dist/opencode-*/ui/. /home/opencode/.local/share/opencode/ui/ \
|
|
536
536
|
&& chown -R opencode:opencode /home/opencode/.local/share/opencode \
|
|
537
537
|
&& chmod +x /home/opencode/.local/share/opencode/bin/opencode \
|
|
538
538
|
&& /home/opencode/.local/share/opencode/bin/opencode --version
|
|
@@ -540,14 +540,6 @@ RUN OPENCODE_COMMIT="3a4eccc7e883575e0d5a508f46036a9f243c06e8" \
|
|
|
540
540
|
# Add opencode to PATH
|
|
541
541
|
ENV PATH="/home/opencode/.local/share/opencode/bin:${PATH}"
|
|
542
542
|
|
|
543
|
-
# Copy UI assets to standard web root (requires root)
|
|
544
|
-
USER root
|
|
545
|
-
RUN mkdir -p /var/www/opencode \
|
|
546
|
-
&& cp -R /tmp/opencode-repo/packages/app/dist/. /var/www/opencode/ \
|
|
547
|
-
&& chown -R root:root /var/www/opencode \
|
|
548
|
-
&& chmod 755 /var/www /var/www/opencode \
|
|
549
|
-
&& chmod -R a+rX /var/www/opencode
|
|
550
|
-
|
|
551
543
|
# -----------------------------------------------------------------------------
|
|
552
544
|
# opencode-broker Installation
|
|
553
545
|
# -----------------------------------------------------------------------------
|
|
@@ -568,49 +560,6 @@ RUN ls -la /usr/local/bin/opencode-broker \
|
|
|
568
560
|
&& test -x /usr/local/bin/opencode-broker \
|
|
569
561
|
&& echo "Broker installed"
|
|
570
562
|
|
|
571
|
-
# -----------------------------------------------------------------------------
|
|
572
|
-
# Nginx Reverse Proxy for UI + API
|
|
573
|
-
# -----------------------------------------------------------------------------
|
|
574
|
-
# Serve the built UI from /var/www/opencode and proxy all 3000 traffic to backend.
|
|
575
|
-
RUN rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf 2>/dev/null || true \
|
|
576
|
-
&& printf '%s\n' \
|
|
577
|
-
'server {' \
|
|
578
|
-
' listen 3000;' \
|
|
579
|
-
' server_name _;' \
|
|
580
|
-
'' \
|
|
581
|
-
' location / {' \
|
|
582
|
-
' proxy_pass http://127.0.0.1:3001;' \
|
|
583
|
-
' proxy_http_version 1.1;' \
|
|
584
|
-
' proxy_set_header Upgrade $http_upgrade;' \
|
|
585
|
-
' proxy_set_header Connection "upgrade";' \
|
|
586
|
-
' proxy_set_header Host $host;' \
|
|
587
|
-
' proxy_set_header X-Real-IP $remote_addr;' \
|
|
588
|
-
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
|
|
589
|
-
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
|
590
|
-
' }' \
|
|
591
|
-
'}' \
|
|
592
|
-
'' \
|
|
593
|
-
'server {' \
|
|
594
|
-
' listen 3002;' \
|
|
595
|
-
' server_name _;' \
|
|
596
|
-
'' \
|
|
597
|
-
' root /var/www/opencode;' \
|
|
598
|
-
' index index.html;' \
|
|
599
|
-
'' \
|
|
600
|
-
' location /assets/ {' \
|
|
601
|
-
' try_files $uri =404;' \
|
|
602
|
-
' }' \
|
|
603
|
-
'' \
|
|
604
|
-
' location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|ico|webp|woff2?|ttf|map)$ {' \
|
|
605
|
-
' try_files $uri =404;' \
|
|
606
|
-
' }' \
|
|
607
|
-
'' \
|
|
608
|
-
' location / {' \
|
|
609
|
-
' try_files $uri $uri/ /index.html;' \
|
|
610
|
-
' }' \
|
|
611
|
-
'}' \
|
|
612
|
-
> /etc/nginx/conf.d/opencode.conf
|
|
613
|
-
|
|
614
563
|
# -----------------------------------------------------------------------------
|
|
615
564
|
# PAM Configuration
|
|
616
565
|
# -----------------------------------------------------------------------------
|
|
@@ -690,7 +639,7 @@ RUN mkdir -p /home/opencode/.npm \
|
|
|
690
639
|
# -----------------------------------------------------------------------------
|
|
691
640
|
# opencode systemd Service (2026-01-22)
|
|
692
641
|
# -----------------------------------------------------------------------------
|
|
693
|
-
# Create opencode as a systemd service for Cockpit integration
|
|
642
|
+
# Create opencode as a systemd service for Cockpit integration
|
|
694
643
|
# NOTE: Requires root privileges to write to /etc/systemd/system/
|
|
695
644
|
USER root
|
|
696
645
|
RUN printf '%s\n' \
|
|
@@ -702,7 +651,7 @@ RUN printf '%s\n' \
|
|
|
702
651
|
'Type=simple' \
|
|
703
652
|
'User=opencode' \
|
|
704
653
|
'WorkingDirectory=/home/opencode/workspace' \
|
|
705
|
-
'ExecStart=/home/opencode/.local/share/opencode/bin/opencode --port
|
|
654
|
+
'ExecStart=/home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
|
|
706
655
|
'Restart=always' \
|
|
707
656
|
'RestartSec=5' \
|
|
708
657
|
'Environment=PATH=/home/opencode/.local/share/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
|
|
@@ -715,44 +664,15 @@ RUN printf '%s\n' \
|
|
|
715
664
|
RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
|
|
716
665
|
&& ln -sf /etc/systemd/system/opencode.service /etc/systemd/system/multi-user.target.wants/opencode.service
|
|
717
666
|
|
|
718
|
-
# Nginx service for serving UI + proxying API
|
|
719
|
-
RUN printf '%s\n' \
|
|
720
|
-
'[Unit]' \
|
|
721
|
-
'Description=Nginx reverse proxy for opencode UI' \
|
|
722
|
-
'After=network.target opencode.service' \
|
|
723
|
-
'' \
|
|
724
|
-
'[Service]' \
|
|
725
|
-
'Type=simple' \
|
|
726
|
-
'ExecStart=/usr/sbin/nginx -g "daemon off;"' \
|
|
727
|
-
'ExecReload=/usr/sbin/nginx -s reload' \
|
|
728
|
-
'Restart=always' \
|
|
729
|
-
'RestartSec=5' \
|
|
730
|
-
'' \
|
|
731
|
-
'[Install]' \
|
|
732
|
-
'WantedBy=multi-user.target' \
|
|
733
|
-
> /etc/systemd/system/opencode-nginx.service
|
|
734
|
-
|
|
735
|
-
# Enable nginx service
|
|
736
|
-
RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
|
|
737
|
-
&& ln -sf /etc/systemd/system/opencode-nginx.service /etc/systemd/system/multi-user.target.wants/opencode-nginx.service
|
|
738
|
-
|
|
739
|
-
# Prevent the distro nginx service from also starting (port 3000 conflict)
|
|
740
|
-
RUN rm -f /etc/systemd/system/multi-user.target.wants/nginx.service \
|
|
741
|
-
&& ln -sf /dev/null /etc/systemd/system/nginx.service
|
|
742
|
-
|
|
743
667
|
# -----------------------------------------------------------------------------
|
|
744
668
|
# opencode Configuration
|
|
745
669
|
# -----------------------------------------------------------------------------
|
|
746
|
-
# Create opencode.jsonc config file with PAM authentication enabled
|
|
670
|
+
# Create opencode.jsonc config file with PAM authentication enabled
|
|
747
671
|
RUN mkdir -p /home/opencode/.config/opencode \
|
|
748
672
|
&& printf '%s\n' \
|
|
749
673
|
'{' \
|
|
750
|
-
' // Container UI served via nginx on 3002' \
|
|
751
674
|
' "auth": {' \
|
|
752
675
|
' "enabled": true' \
|
|
753
|
-
' },' \
|
|
754
|
-
' "server": {' \
|
|
755
|
-
' "uiUrl": "http://localhost:3002"' \
|
|
756
676
|
' }' \
|
|
757
677
|
'}' \
|
|
758
678
|
> /home/opencode/.config/opencode/opencode.jsonc \
|
|
@@ -776,9 +696,11 @@ RUN printf '%s\n' \
|
|
|
776
696
|
'if [ "${USE_SYSTEMD}" = "1" ]; then' \
|
|
777
697
|
' exec /sbin/init' \
|
|
778
698
|
'else' \
|
|
699
|
+
' # Ensure broker socket directory exists' \
|
|
700
|
+
' install -d -m 0755 /run/opencode' \
|
|
701
|
+
' /usr/local/bin/opencode-broker &' \
|
|
779
702
|
' # Use runuser to switch to opencode user without password prompt' \
|
|
780
|
-
' runuser -u opencode -- /home/opencode/.local/share/opencode/bin/opencode --port
|
|
781
|
-
' exec /usr/sbin/nginx -g "daemon off;"' \
|
|
703
|
+
' exec runuser -u opencode -- sh -lc "cd /home/opencode/workspace && /home/opencode/.local/share/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0"' \
|
|
782
704
|
'fi' \
|
|
783
705
|
> /usr/local/bin/entrypoint.sh && chmod +x /usr/local/bin/entrypoint.sh
|
|
784
706
|
|
|
@@ -788,10 +710,10 @@ RUN printf '%s\n' \
|
|
|
788
710
|
# -----------------------------------------------------------------------------
|
|
789
711
|
# Health Check
|
|
790
712
|
# -----------------------------------------------------------------------------
|
|
791
|
-
# Check that opencode
|
|
713
|
+
# Check that opencode main page responds
|
|
792
714
|
# Works for both tini and systemd modes
|
|
793
715
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
794
|
-
CMD curl -f http://localhost:3000/
|
|
716
|
+
CMD curl -f http://localhost:3000/ || exit 1
|
|
795
717
|
|
|
796
718
|
# -----------------------------------------------------------------------------
|
|
797
719
|
# Version File
|
|
@@ -812,8 +734,8 @@ RUN echo "${OPENCODE_CLOUD_VERSION}" > /etc/opencode-cloud-version
|
|
|
812
734
|
# -----------------------------------------------------------------------------
|
|
813
735
|
WORKDIR /home/opencode/workspace
|
|
814
736
|
|
|
815
|
-
# Expose opencode
|
|
816
|
-
EXPOSE 3000
|
|
737
|
+
# Expose opencode web (3000) and Cockpit (9090)
|
|
738
|
+
EXPOSE 3000 9090
|
|
817
739
|
|
|
818
740
|
# Hybrid init: entrypoint script chooses tini or systemd based on USE_SYSTEMD env
|
|
819
741
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
@@ -28,10 +28,18 @@ docker pull ghcr.io/prizz/opencode-cloud-sandbox:latest
|
|
|
28
28
|
Run the container:
|
|
29
29
|
|
|
30
30
|
```
|
|
31
|
-
docker run --rm -it -p 3000:3000 -p
|
|
31
|
+
docker run --rm -it -p 3000:3000 -p 9090:9090 ghcr.io/prizz/opencode-cloud-sandbox:latest
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
The opencode web UI is available at `http://localhost:3000`.
|
|
34
|
+
The opencode web UI is available at `http://localhost:3000`. Cockpit runs on `http://localhost:9090`.
|
|
35
|
+
|
|
36
|
+
## opencode build and serve flow
|
|
37
|
+
|
|
38
|
+
The Docker image builds opencode directly from the fork and runs the web server without nginx:
|
|
39
|
+
|
|
40
|
+
1. `cd packages/opencode`
|
|
41
|
+
2. `bun run build` to generate `packages/opencode/dist`
|
|
42
|
+
3. Run the server with `./bin/opencode web`
|
|
35
43
|
|
|
36
44
|
## Source
|
|
37
45
|
|
package/src/docker/container.rs
CHANGED
|
@@ -20,7 +20,7 @@ use std::collections::HashMap;
|
|
|
20
20
|
use tracing::debug;
|
|
21
21
|
|
|
22
22
|
/// Default container name
|
|
23
|
-
pub const CONTAINER_NAME: &str = "opencode-cloud";
|
|
23
|
+
pub const CONTAINER_NAME: &str = "opencode-cloud-sandbox";
|
|
24
24
|
|
|
25
25
|
/// Default port for opencode web UI
|
|
26
26
|
pub const OPENCODE_WEB_PORT: u16 = 3000;
|
|
@@ -38,7 +38,7 @@ pub const OPENCODE_WEB_PORT: u16 = 3000;
|
|
|
38
38
|
/// * `env_vars` - Additional environment variables (optional)
|
|
39
39
|
/// * `bind_address` - IP address to bind on host (defaults to "127.0.0.1")
|
|
40
40
|
/// * `cockpit_port` - Port to bind on host for Cockpit (defaults to 9090)
|
|
41
|
-
/// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to
|
|
41
|
+
/// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to false)
|
|
42
42
|
/// * `bind_mounts` - User-defined bind mounts from config and CLI flags (optional)
|
|
43
43
|
#[allow(clippy::too_many_arguments)]
|
|
44
44
|
pub async fn create_container(
|
|
@@ -57,7 +57,7 @@ pub async fn create_container(
|
|
|
57
57
|
let image_name = image.unwrap_or(&default_image);
|
|
58
58
|
let port = opencode_web_port.unwrap_or(OPENCODE_WEB_PORT);
|
|
59
59
|
let cockpit_port_val = cockpit_port.unwrap_or(9090);
|
|
60
|
-
let cockpit_enabled_val = cockpit_enabled.unwrap_or(
|
|
60
|
+
let cockpit_enabled_val = cockpit_enabled.unwrap_or(false);
|
|
61
61
|
|
|
62
62
|
debug!(
|
|
63
63
|
"Creating container {} from image {} with port {} and cockpit_port {} (enabled: {})",
|
|
@@ -202,7 +202,7 @@ pub async fn create_container(
|
|
|
202
202
|
let config = Config {
|
|
203
203
|
image: Some(image_name.to_string()),
|
|
204
204
|
hostname: Some(CONTAINER_NAME.to_string()),
|
|
205
|
-
working_dir: Some("/workspace".to_string()),
|
|
205
|
+
working_dir: Some("/home/opencode/workspace".to_string()),
|
|
206
206
|
exposed_ports: Some(exposed_ports),
|
|
207
207
|
env: final_env,
|
|
208
208
|
host_config: Some(host_config),
|
|
@@ -473,7 +473,7 @@ mod tests {
|
|
|
473
473
|
|
|
474
474
|
#[test]
|
|
475
475
|
fn container_constants_are_correct() {
|
|
476
|
-
assert_eq!(CONTAINER_NAME, "opencode-cloud");
|
|
476
|
+
assert_eq!(CONTAINER_NAME, "opencode-cloud-sandbox");
|
|
477
477
|
assert_eq!(OPENCODE_WEB_PORT, 3000);
|
|
478
478
|
}
|
|
479
479
|
|
package/src/docker/exec.rs
CHANGED
|
@@ -22,7 +22,7 @@ use super::{DockerClient, DockerError};
|
|
|
22
22
|
///
|
|
23
23
|
/// # Example
|
|
24
24
|
/// ```ignore
|
|
25
|
-
/// let output = exec_command(&client, "opencode-cloud", vec!["whoami"]).await?;
|
|
25
|
+
/// let output = exec_command(&client, "opencode-cloud-sandbox", vec!["whoami"]).await?;
|
|
26
26
|
/// ```
|
|
27
27
|
pub async fn exec_command(
|
|
28
28
|
client: &DockerClient,
|
|
@@ -104,7 +104,7 @@ pub async fn exec_command(
|
|
|
104
104
|
/// // Set password via chpasswd (secure, non-interactive)
|
|
105
105
|
/// exec_command_with_stdin(
|
|
106
106
|
/// &client,
|
|
107
|
-
/// "opencode-cloud",
|
|
107
|
+
/// "opencode-cloud-sandbox",
|
|
108
108
|
/// vec!["chpasswd"],
|
|
109
109
|
/// "username:password\n"
|
|
110
110
|
/// ).await?;
|
package/src/docker/health.rs
CHANGED
|
@@ -53,12 +53,21 @@ pub enum HealthError {
|
|
|
53
53
|
Timeout,
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
fn format_host(bind_addr: &str) -> String {
|
|
57
|
+
if bind_addr.contains(':') && !bind_addr.starts_with('[') {
|
|
58
|
+
format!("[{bind_addr}]")
|
|
59
|
+
} else {
|
|
60
|
+
bind_addr.to_string()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
/// Check health by querying OpenCode's /global/health endpoint
|
|
57
65
|
///
|
|
58
66
|
/// Returns the health response on success (HTTP 200).
|
|
59
67
|
/// Returns an error for connection issues, timeouts, or non-200 responses.
|
|
60
|
-
pub async fn check_health(port: u16) -> Result<HealthResponse, HealthError> {
|
|
61
|
-
let
|
|
68
|
+
pub async fn check_health(bind_addr: &str, port: u16) -> Result<HealthResponse, HealthError> {
|
|
69
|
+
let host = format_host(bind_addr);
|
|
70
|
+
let url = format!("http://{host}:{port}/global/health");
|
|
62
71
|
|
|
63
72
|
let client = reqwest::Client::builder()
|
|
64
73
|
.timeout(Duration::from_secs(5))
|
|
@@ -95,10 +104,11 @@ pub async fn check_health(port: u16) -> Result<HealthResponse, HealthError> {
|
|
|
95
104
|
/// If container stats fail, still returns response with container_state = "unknown".
|
|
96
105
|
pub async fn check_health_extended(
|
|
97
106
|
client: &DockerClient,
|
|
107
|
+
bind_addr: &str,
|
|
98
108
|
port: u16,
|
|
99
109
|
) -> Result<ExtendedHealthResponse, HealthError> {
|
|
100
110
|
// Get basic health info
|
|
101
|
-
let health = check_health(port).await?;
|
|
111
|
+
let health = check_health(bind_addr, port).await?;
|
|
102
112
|
|
|
103
113
|
// Get container stats
|
|
104
114
|
let container_name = super::CONTAINER_NAME;
|
|
@@ -155,11 +165,21 @@ mod tests {
|
|
|
155
165
|
#[tokio::test]
|
|
156
166
|
async fn test_health_check_connection_refused() {
|
|
157
167
|
// Port 1 should always refuse connection
|
|
158
|
-
let result = check_health(1).await;
|
|
168
|
+
let result = check_health("127.0.0.1", 1).await;
|
|
159
169
|
assert!(result.is_err());
|
|
160
170
|
match result.unwrap_err() {
|
|
161
171
|
HealthError::ConnectionRefused => {}
|
|
162
172
|
other => panic!("Expected ConnectionRefused, got: {other:?}"),
|
|
163
173
|
}
|
|
164
174
|
}
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn format_host_wraps_ipv6() {
|
|
178
|
+
assert_eq!(format_host("::1"), "[::1]");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
fn format_host_preserves_ipv4() {
|
|
183
|
+
assert_eq!(format_host("127.0.0.1"), "127.0.0.1");
|
|
184
|
+
}
|
|
165
185
|
}
|
package/src/docker/image.rs
CHANGED
|
@@ -177,6 +177,8 @@ struct BuildLogState {
|
|
|
177
177
|
error_log_buffer_size: usize,
|
|
178
178
|
last_buildkit_vertex: Option<String>,
|
|
179
179
|
last_buildkit_vertex_id: Option<String>,
|
|
180
|
+
export_vertex_id: Option<String>,
|
|
181
|
+
export_vertex_name: Option<String>,
|
|
180
182
|
buildkit_logs_by_vertex_id: HashMap<String, String>,
|
|
181
183
|
vertex_name_by_vertex_id: HashMap<String, String>,
|
|
182
184
|
}
|
|
@@ -199,6 +201,8 @@ impl BuildLogState {
|
|
|
199
201
|
error_log_buffer_size,
|
|
200
202
|
last_buildkit_vertex: None,
|
|
201
203
|
last_buildkit_vertex_id: None,
|
|
204
|
+
export_vertex_id: None,
|
|
205
|
+
export_vertex_name: None,
|
|
202
206
|
buildkit_logs_by_vertex_id: HashMap::new(),
|
|
203
207
|
vertex_name_by_vertex_id: HashMap::new(),
|
|
204
208
|
}
|
|
@@ -255,31 +259,45 @@ fn handle_buildkit_status(
|
|
|
255
259
|
) {
|
|
256
260
|
let latest_logs = append_buildkit_logs(&mut state.buildkit_logs_by_vertex_id, status);
|
|
257
261
|
update_buildkit_vertex_names(&mut state.vertex_name_by_vertex_id, status);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
262
|
+
update_export_vertex_from_logs(
|
|
263
|
+
&latest_logs,
|
|
264
|
+
&state.vertex_name_by_vertex_id,
|
|
265
|
+
&mut state.export_vertex_id,
|
|
266
|
+
&mut state.export_vertex_name,
|
|
267
|
+
);
|
|
268
|
+
let (vertex_id, vertex_name) = match select_latest_buildkit_vertex(
|
|
269
|
+
status,
|
|
270
|
+
&state.vertex_name_by_vertex_id,
|
|
271
|
+
state.export_vertex_id.as_deref(),
|
|
272
|
+
state.export_vertex_name.as_deref(),
|
|
273
|
+
) {
|
|
274
|
+
Some((vertex_id, vertex_name)) => (vertex_id, vertex_name),
|
|
275
|
+
None => {
|
|
276
|
+
let Some(log_entry) = latest_logs.last() else {
|
|
277
|
+
return;
|
|
278
|
+
};
|
|
279
|
+
let name = state
|
|
280
|
+
.vertex_name_by_vertex_id
|
|
281
|
+
.get(&log_entry.vertex_id)
|
|
282
|
+
.cloned()
|
|
283
|
+
.or_else(|| state.last_buildkit_vertex.clone())
|
|
284
|
+
.unwrap_or_else(|| format_vertex_fallback_label(&log_entry.vertex_id));
|
|
285
|
+
(log_entry.vertex_id.clone(), name)
|
|
286
|
+
}
|
|
287
|
+
};
|
|
274
288
|
record_buildkit_logs(state, &latest_logs, &vertex_id, &vertex_name);
|
|
275
|
-
state.last_buildkit_vertex_id = Some(vertex_id);
|
|
289
|
+
state.last_buildkit_vertex_id = Some(vertex_id.clone());
|
|
276
290
|
if state.last_buildkit_vertex.as_deref() != Some(&vertex_name) {
|
|
277
291
|
state.last_buildkit_vertex = Some(vertex_name.clone());
|
|
278
292
|
}
|
|
279
293
|
|
|
280
294
|
let message = if progress.is_plain_output() {
|
|
281
295
|
vertex_name
|
|
282
|
-
} else if let Some(log_entry) = latest_logs
|
|
296
|
+
} else if let Some(log_entry) = latest_logs
|
|
297
|
+
.iter()
|
|
298
|
+
.rev()
|
|
299
|
+
.find(|entry| entry.vertex_id == vertex_id)
|
|
300
|
+
{
|
|
283
301
|
format!("{vertex_name} · {}", log_entry.message)
|
|
284
302
|
} else {
|
|
285
303
|
vertex_name
|
|
@@ -360,7 +378,17 @@ fn update_buildkit_vertex_names(
|
|
|
360
378
|
fn select_latest_buildkit_vertex(
|
|
361
379
|
status: &BuildkitStatusResponse,
|
|
362
380
|
vertex_name_by_vertex_id: &HashMap<String, String>,
|
|
381
|
+
export_vertex_id: Option<&str>,
|
|
382
|
+
export_vertex_name: Option<&str>,
|
|
363
383
|
) -> Option<(String, String)> {
|
|
384
|
+
if let Some(export_vertex_id) = export_vertex_id {
|
|
385
|
+
let name = export_vertex_name
|
|
386
|
+
.map(str::to_string)
|
|
387
|
+
.or_else(|| vertex_name_by_vertex_id.get(export_vertex_id).cloned())
|
|
388
|
+
.unwrap_or_else(|| format_vertex_fallback_label(export_vertex_id));
|
|
389
|
+
return Some((export_vertex_id.to_string(), name));
|
|
390
|
+
}
|
|
391
|
+
|
|
364
392
|
let mut best_runtime: Option<(u32, String, String)> = None;
|
|
365
393
|
let mut fallback: Option<(String, String)> = None;
|
|
366
394
|
|
|
@@ -423,6 +451,24 @@ fn format_vertex_fallback_label(vertex_id: &str) -> String {
|
|
|
423
451
|
format!("vertex {short}")
|
|
424
452
|
}
|
|
425
453
|
|
|
454
|
+
fn update_export_vertex_from_logs(
|
|
455
|
+
latest_logs: &[BuildkitLogEntry],
|
|
456
|
+
vertex_name_by_vertex_id: &HashMap<String, String>,
|
|
457
|
+
export_vertex_id: &mut Option<String>,
|
|
458
|
+
export_vertex_name: &mut Option<String>,
|
|
459
|
+
) {
|
|
460
|
+
if let Some(entry) = latest_logs
|
|
461
|
+
.iter()
|
|
462
|
+
.rev()
|
|
463
|
+
.find(|log| log.message.trim_start().starts_with("exporting to image"))
|
|
464
|
+
{
|
|
465
|
+
*export_vertex_id = Some(entry.vertex_id.clone());
|
|
466
|
+
if let Some(name) = vertex_name_by_vertex_id.get(&entry.vertex_id) {
|
|
467
|
+
*export_vertex_name = Some(name.clone());
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
426
472
|
fn record_buildkit_logs(
|
|
427
473
|
state: &mut BuildLogState,
|
|
428
474
|
latest_logs: &[BuildkitLogEntry],
|
package/src/docker/mount.rs
CHANGED
|
@@ -64,9 +64,9 @@ impl ParsedMount {
|
|
|
64
64
|
/// use opencode_cloud_core::docker::ParsedMount;
|
|
65
65
|
///
|
|
66
66
|
/// // Read-write mount (default)
|
|
67
|
-
/// let mount = ParsedMount::parse("/home/user/data:/workspace/data").unwrap();
|
|
67
|
+
/// let mount = ParsedMount::parse("/home/user/data:/home/opencode/workspace/data").unwrap();
|
|
68
68
|
/// assert_eq!(mount.host_path.to_str().unwrap(), "/home/user/data");
|
|
69
|
-
/// assert_eq!(mount.container_path, "/workspace/data");
|
|
69
|
+
/// assert_eq!(mount.container_path, "/home/opencode/workspace/data");
|
|
70
70
|
/// assert!(!mount.read_only);
|
|
71
71
|
///
|
|
72
72
|
/// // Read-only mount
|
|
@@ -285,7 +285,7 @@ mod tests {
|
|
|
285
285
|
|
|
286
286
|
#[test]
|
|
287
287
|
fn non_system_path_no_warning() {
|
|
288
|
-
let warning = check_container_path_warning("/workspace/data");
|
|
288
|
+
let warning = check_container_path_warning("/home/opencode/workspace/data");
|
|
289
289
|
assert!(warning.is_none());
|
|
290
290
|
}
|
|
291
291
|
|
package/src/docker/users.rs
CHANGED
|
@@ -37,7 +37,7 @@ pub struct UserInfo {
|
|
|
37
37
|
///
|
|
38
38
|
/// # Example
|
|
39
39
|
/// ```ignore
|
|
40
|
-
/// create_user(&client, "opencode-cloud", "admin").await?;
|
|
40
|
+
/// create_user(&client, "opencode-cloud-sandbox", "admin").await?;
|
|
41
41
|
/// ```
|
|
42
42
|
pub async fn create_user(
|
|
43
43
|
client: &DockerClient,
|
|
@@ -80,7 +80,7 @@ pub async fn create_user(
|
|
|
80
80
|
///
|
|
81
81
|
/// # Example
|
|
82
82
|
/// ```ignore
|
|
83
|
-
/// set_user_password(&client, "opencode-cloud", "admin", "secret123").await?;
|
|
83
|
+
/// set_user_password(&client, "opencode-cloud-sandbox", "admin", "secret123").await?;
|
|
84
84
|
/// ```
|
|
85
85
|
pub async fn set_user_password(
|
|
86
86
|
client: &DockerClient,
|
package/src/docker/volume.rs
CHANGED
|
@@ -8,23 +8,23 @@ use bollard::volume::CreateVolumeOptions;
|
|
|
8
8
|
use std::collections::HashMap;
|
|
9
9
|
use tracing::debug;
|
|
10
10
|
|
|
11
|
-
/// Volume name for opencode
|
|
12
|
-
pub const VOLUME_SESSION: &str = "opencode-
|
|
11
|
+
/// Volume name for opencode data
|
|
12
|
+
pub const VOLUME_SESSION: &str = "opencode-data";
|
|
13
13
|
|
|
14
14
|
/// Volume name for project files
|
|
15
|
-
pub const VOLUME_PROJECTS: &str = "opencode-
|
|
15
|
+
pub const VOLUME_PROJECTS: &str = "opencode-workspace";
|
|
16
16
|
|
|
17
17
|
/// Volume name for opencode configuration
|
|
18
|
-
pub const VOLUME_CONFIG: &str = "opencode-
|
|
18
|
+
pub const VOLUME_CONFIG: &str = "opencode-config";
|
|
19
19
|
|
|
20
20
|
/// All volume names as array for iteration
|
|
21
21
|
pub const VOLUME_NAMES: [&str; 3] = [VOLUME_SESSION, VOLUME_PROJECTS, VOLUME_CONFIG];
|
|
22
22
|
|
|
23
|
-
/// Mount point for
|
|
24
|
-
pub const MOUNT_SESSION: &str = "/home/opencode/.
|
|
23
|
+
/// Mount point for opencode data inside container
|
|
24
|
+
pub const MOUNT_SESSION: &str = "/home/opencode/.local/share";
|
|
25
25
|
|
|
26
26
|
/// Mount point for project files inside container
|
|
27
|
-
pub const MOUNT_PROJECTS: &str = "/workspace";
|
|
27
|
+
pub const MOUNT_PROJECTS: &str = "/home/opencode/workspace";
|
|
28
28
|
|
|
29
29
|
/// Mount point for configuration inside container
|
|
30
30
|
pub const MOUNT_CONFIG: &str = "/home/opencode/.config";
|
|
@@ -122,9 +122,9 @@ mod tests {
|
|
|
122
122
|
|
|
123
123
|
#[test]
|
|
124
124
|
fn volume_constants_are_correct() {
|
|
125
|
-
assert_eq!(VOLUME_SESSION, "opencode-
|
|
126
|
-
assert_eq!(VOLUME_PROJECTS, "opencode-
|
|
127
|
-
assert_eq!(VOLUME_CONFIG, "opencode-
|
|
125
|
+
assert_eq!(VOLUME_SESSION, "opencode-data");
|
|
126
|
+
assert_eq!(VOLUME_PROJECTS, "opencode-workspace");
|
|
127
|
+
assert_eq!(VOLUME_CONFIG, "opencode-config");
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
#[test]
|
|
@@ -137,8 +137,8 @@ mod tests {
|
|
|
137
137
|
|
|
138
138
|
#[test]
|
|
139
139
|
fn mount_points_are_correct() {
|
|
140
|
-
assert_eq!(MOUNT_SESSION, "/home/opencode/.
|
|
141
|
-
assert_eq!(MOUNT_PROJECTS, "/workspace");
|
|
140
|
+
assert_eq!(MOUNT_SESSION, "/home/opencode/.local/share");
|
|
141
|
+
assert_eq!(MOUNT_PROJECTS, "/home/opencode/workspace");
|
|
142
142
|
assert_eq!(MOUNT_CONFIG, "/home/opencode/.config");
|
|
143
143
|
}
|
|
144
144
|
}
|