@opencode-cloud/core 10.0.0 → 10.1.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 +3 -0
- package/package.json +1 -1
- package/src/docker/Dockerfile +55 -46
- package/src/docker/mod.rs +5 -1
- package/src/docker/registry.rs +214 -0
- package/src/docker/version.rs +60 -1
package/Cargo.toml
CHANGED
package/README.md
CHANGED
package/package.json
CHANGED
package/src/docker/Dockerfile
CHANGED
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
# -----------------------------------------------------------------------------
|
|
31
31
|
|
|
32
32
|
# -----------------------------------------------------------------------------
|
|
33
|
-
# Stage 1:
|
|
33
|
+
# Stage 1: Base
|
|
34
34
|
# -----------------------------------------------------------------------------
|
|
35
|
-
FROM ubuntu:24.04 AS
|
|
35
|
+
FROM ubuntu:24.04 AS base
|
|
36
36
|
|
|
37
37
|
# OCI Labels for image metadata
|
|
38
38
|
LABEL org.opencontainers.image.title="opencode-cloud"
|
|
@@ -236,6 +236,11 @@ ENV PNPM_HOME="/home/opencode/.local/share/pnpm"
|
|
|
236
236
|
ENV PATH="${PNPM_HOME}:${PATH}"
|
|
237
237
|
RUN mkdir -p "${PNPM_HOME}"
|
|
238
238
|
|
|
239
|
+
# bun - self-managing installer, trusted to handle versions
|
|
240
|
+
RUN curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \
|
|
241
|
+
&& rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun
|
|
242
|
+
|
|
243
|
+
ENV PATH="/home/opencode/.bun/bin:${PATH}"
|
|
239
244
|
|
|
240
245
|
# uv - self-managing installer, trusted to handle versions (fast Python package manager)
|
|
241
246
|
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
@@ -495,6 +500,11 @@ RUN printf '%s\n' \
|
|
|
495
500
|
# Set up pipx path
|
|
496
501
|
RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshrc
|
|
497
502
|
|
|
503
|
+
# -----------------------------------------------------------------------------
|
|
504
|
+
# Stage 2: opencode build
|
|
505
|
+
# -----------------------------------------------------------------------------
|
|
506
|
+
FROM base AS opencode-build
|
|
507
|
+
|
|
498
508
|
# -----------------------------------------------------------------------------
|
|
499
509
|
# opencode Setup (Fork + Broker + Proxy)
|
|
500
510
|
# -----------------------------------------------------------------------------
|
|
@@ -504,11 +514,9 @@ RUN echo 'export PATH="/home/opencode/.local/bin:$PATH"' >> /home/opencode/.zshr
|
|
|
504
514
|
# This block includes:
|
|
505
515
|
# - opencode build (backend binary) + app build (frontend dist)
|
|
506
516
|
# - opencode-broker build
|
|
507
|
-
# - opencode web build + runtime
|
|
508
|
-
# - PAM configuration + systemd services
|
|
509
|
-
# - opencode config file
|
|
510
517
|
#
|
|
511
|
-
# NOTE: This
|
|
518
|
+
# NOTE: This stage uses opencode user + sudo for privileged installs.
|
|
519
|
+
USER opencode
|
|
512
520
|
|
|
513
521
|
# -----------------------------------------------------------------------------
|
|
514
522
|
# opencode Installation (Fork from pRizz/opencode)
|
|
@@ -522,12 +530,9 @@ RUN OPENCODE_COMMIT="8a63844a3d3166273a83d5e94d8820d62ad82510" \
|
|
|
522
530
|
&& cd /tmp/opencode-repo \
|
|
523
531
|
&& git fetch --depth 1 origin "${OPENCODE_COMMIT}" \
|
|
524
532
|
&& git checkout "${OPENCODE_COMMIT}" \
|
|
525
|
-
&& curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.5" \
|
|
526
|
-
&& export PATH="/home/opencode/.bun/bin:${PATH}" \
|
|
527
533
|
&& bun install --frozen-lockfile \
|
|
528
534
|
&& cd packages/opencode \
|
|
529
535
|
&& bun run build-single-ui \
|
|
530
|
-
&& rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun \
|
|
531
536
|
&& cd /tmp/opencode-repo \
|
|
532
537
|
&& sudo mkdir -p /opt/opencode/bin /opt/opencode/ui \
|
|
533
538
|
&& echo "${OPENCODE_COMMIT}" | sudo tee /opt/opencode/COMMIT >/dev/null \
|
|
@@ -536,30 +541,21 @@ RUN OPENCODE_COMMIT="8a63844a3d3166273a83d5e94d8820d62ad82510" \
|
|
|
536
541
|
&& sudo cp -R /tmp/opencode-repo/packages/opencode/dist/opencode-*/ui/. /opt/opencode/ui/ \
|
|
537
542
|
&& sudo chown -R opencode:opencode /opt/opencode \
|
|
538
543
|
&& sudo chmod +x /opt/opencode/bin/opencode \
|
|
539
|
-
&& /
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
544
|
+
&& cd /tmp/opencode-repo/packages/opencode-broker \
|
|
545
|
+
&& cargo build --release \
|
|
546
|
+
&& sudo mkdir -p /usr/local/bin \
|
|
547
|
+
&& sudo cp target/release/opencode-broker /usr/local/bin/opencode-broker \
|
|
548
|
+
&& sudo chmod 4755 /usr/local/bin/opencode-broker \
|
|
549
|
+
&& rm -rf /tmp/opencode-repo \
|
|
550
|
+
&& rm -rf /home/opencode/.bun/install/cache /home/opencode/.bun/cache /home/opencode/.cache/bun
|
|
543
551
|
|
|
544
552
|
# -----------------------------------------------------------------------------
|
|
545
|
-
#
|
|
553
|
+
# Stage 3: Runtime
|
|
546
554
|
# -----------------------------------------------------------------------------
|
|
547
|
-
|
|
548
|
-
# The broker handles PAM authentication and user process spawning
|
|
549
|
-
# NOTE: Requires root privileges for setuid bit (chmod 4755) to allow broker
|
|
550
|
-
# to run with elevated privileges for PAM authentication
|
|
551
|
-
USER root
|
|
552
|
-
RUN cd /tmp/opencode-repo/packages/opencode-broker \
|
|
553
|
-
&& runuser -u opencode -- bash -c '. /home/opencode/.cargo/env && cargo build --release' \
|
|
554
|
-
&& mkdir -p /usr/local/bin \
|
|
555
|
-
&& cp target/release/opencode-broker /usr/local/bin/opencode-broker \
|
|
556
|
-
&& chmod 4755 /usr/local/bin/opencode-broker \
|
|
557
|
-
&& rm -rf /tmp/opencode-repo
|
|
555
|
+
FROM base AS runtime
|
|
558
556
|
|
|
559
|
-
#
|
|
560
|
-
|
|
561
|
-
&& test -x /usr/local/bin/opencode-broker \
|
|
562
|
-
&& echo "Broker installed"
|
|
557
|
+
# Add opencode to PATH
|
|
558
|
+
ENV PATH="/opt/opencode/bin:${PATH}"
|
|
563
559
|
|
|
564
560
|
# -----------------------------------------------------------------------------
|
|
565
561
|
# PAM Configuration
|
|
@@ -567,6 +563,7 @@ RUN ls -la /usr/local/bin/opencode-broker \
|
|
|
567
563
|
# Install PAM configuration for opencode authentication
|
|
568
564
|
# This allows opencode to authenticate users via PAM (same users as Cockpit)
|
|
569
565
|
# NOTE: Requires root privileges to write to /etc/pam.d/
|
|
566
|
+
USER root
|
|
570
567
|
RUN printf '%s\n' \
|
|
571
568
|
'# PAM configuration for OpenCode authentication' \
|
|
572
569
|
'# Install to /etc/pam.d/opencode' \
|
|
@@ -625,24 +622,11 @@ RUN printf '%s\n' \
|
|
|
625
622
|
RUN mkdir -p /etc/systemd/system/multi-user.target.wants \
|
|
626
623
|
&& ln -sf /etc/systemd/system/opencode-broker.service /etc/systemd/system/multi-user.target.wants/opencode-broker.service
|
|
627
624
|
|
|
628
|
-
USER opencode
|
|
629
|
-
|
|
630
|
-
# -----------------------------------------------------------------------------
|
|
631
|
-
# GSD Plugin Installation
|
|
632
|
-
# -----------------------------------------------------------------------------
|
|
633
|
-
# Install the GSD (Get Shit Done) plugin for opencode
|
|
634
|
-
# Note: If this fails in container builds due to "~" path resolution, retry with
|
|
635
|
-
# OPENCODE_CONFIG_DIR=/home/opencode/.config/opencode set explicitly.
|
|
636
|
-
RUN mkdir -p /home/opencode/.npm \
|
|
637
|
-
&& npx --yes get-shit-done-cc --opencode --global \
|
|
638
|
-
&& rm -rf /home/opencode/.npm/_cacache /home/opencode/.npm/_npx
|
|
639
|
-
|
|
640
625
|
# -----------------------------------------------------------------------------
|
|
641
626
|
# opencode systemd Service (2026-01-22)
|
|
642
627
|
# -----------------------------------------------------------------------------
|
|
643
628
|
# Create opencode as a systemd service for Cockpit integration
|
|
644
629
|
# NOTE: Requires root privileges to write to /etc/systemd/system/
|
|
645
|
-
USER root
|
|
646
630
|
RUN printf '%s\n' \
|
|
647
631
|
'[Unit]' \
|
|
648
632
|
'Description=opencode Web Interface' \
|
|
@@ -655,7 +639,7 @@ RUN printf '%s\n' \
|
|
|
655
639
|
'ExecStart=/opt/opencode/bin/opencode web --port 3000 --hostname 0.0.0.0' \
|
|
656
640
|
'Restart=always' \
|
|
657
641
|
'RestartSec=5' \
|
|
658
|
-
'Environment=PATH=/opt/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' \
|
|
642
|
+
'Environment=PATH=/opt/opencode/bin:/home/opencode/.local/bin:/home/opencode/.cargo/bin:/home/opencode/.local/share/mise/shims:/home/opencode/.bun/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' \
|
|
659
643
|
'' \
|
|
660
644
|
'[Install]' \
|
|
661
645
|
'WantedBy=multi-user.target' \
|
|
@@ -683,15 +667,12 @@ RUN mkdir -p /home/opencode/.config/opencode \
|
|
|
683
667
|
# Verify config file exists
|
|
684
668
|
RUN ls -la /home/opencode/.config/opencode/opencode.jsonc && cat /home/opencode/.config/opencode/opencode.jsonc
|
|
685
669
|
|
|
686
|
-
USER opencode
|
|
687
|
-
|
|
688
670
|
# -----------------------------------------------------------------------------
|
|
689
671
|
# Entrypoint Script (Hybrid Init Support)
|
|
690
672
|
# -----------------------------------------------------------------------------
|
|
691
673
|
# Supports both tini (default, works everywhere) and systemd (for Cockpit on Linux)
|
|
692
674
|
# Set USE_SYSTEMD=1 environment variable to use systemd init
|
|
693
675
|
# Note: Entrypoint runs as root to support both modes; tini mode drops to opencode user
|
|
694
|
-
USER root
|
|
695
676
|
RUN printf '%s\n' \
|
|
696
677
|
'#!/bin/bash' \
|
|
697
678
|
'if [ "${USE_SYSTEMD}" = "1" ]; then' \
|
|
@@ -716,6 +697,34 @@ RUN printf '%s\n' \
|
|
|
716
697
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
|
717
698
|
CMD curl -f -H "Accept: text/html" http://localhost:3000/ || exit 1
|
|
718
699
|
|
|
700
|
+
# -----------------------------------------------------------------------------
|
|
701
|
+
# opencode Artifacts
|
|
702
|
+
# -----------------------------------------------------------------------------
|
|
703
|
+
COPY --from=opencode-build /opt/opencode /opt/opencode
|
|
704
|
+
COPY --from=opencode-build /usr/local/bin/opencode-broker /usr/local/bin/opencode-broker
|
|
705
|
+
|
|
706
|
+
RUN chown -R opencode:opencode /opt/opencode \
|
|
707
|
+
&& chmod +x /opt/opencode/bin/opencode \
|
|
708
|
+
&& chmod 4755 /usr/local/bin/opencode-broker
|
|
709
|
+
|
|
710
|
+
# Verify broker binary exists and is executable
|
|
711
|
+
RUN ls -la /usr/local/bin/opencode-broker \
|
|
712
|
+
&& test -x /usr/local/bin/opencode-broker \
|
|
713
|
+
&& echo "Broker installed"
|
|
714
|
+
|
|
715
|
+
USER opencode
|
|
716
|
+
RUN /opt/opencode/bin/opencode --version
|
|
717
|
+
|
|
718
|
+
# -----------------------------------------------------------------------------
|
|
719
|
+
# GSD Plugin Installation
|
|
720
|
+
# -----------------------------------------------------------------------------
|
|
721
|
+
# Install the GSD (Get Shit Done) plugin for opencode
|
|
722
|
+
# Note: If this fails in container builds due to "~" path resolution, retry with
|
|
723
|
+
# OPENCODE_CONFIG_DIR=/home/opencode/.config/opencode set explicitly.
|
|
724
|
+
RUN mkdir -p /home/opencode/.npm \
|
|
725
|
+
&& npx --yes get-shit-done-cc --opencode --global \
|
|
726
|
+
&& rm -rf /home/opencode/.npm/_cacache /home/opencode/.npm/_npx
|
|
727
|
+
|
|
719
728
|
# -----------------------------------------------------------------------------
|
|
720
729
|
# Version File
|
|
721
730
|
# -----------------------------------------------------------------------------
|
package/src/docker/mod.rs
CHANGED
|
@@ -21,6 +21,7 @@ mod health;
|
|
|
21
21
|
pub mod image;
|
|
22
22
|
pub mod mount;
|
|
23
23
|
pub mod progress;
|
|
24
|
+
mod registry;
|
|
24
25
|
pub mod state;
|
|
25
26
|
pub mod update;
|
|
26
27
|
pub mod users;
|
|
@@ -47,7 +48,10 @@ pub use image::{build_image, image_exists, pull_image};
|
|
|
47
48
|
pub use update::{UpdateResult, has_previous_image, rollback_image, update_image};
|
|
48
49
|
|
|
49
50
|
// Version detection
|
|
50
|
-
pub use version::{
|
|
51
|
+
pub use version::{
|
|
52
|
+
VERSION_LABEL, get_cli_version, get_image_version, get_registry_latest_version,
|
|
53
|
+
versions_compatible,
|
|
54
|
+
};
|
|
51
55
|
|
|
52
56
|
// Container exec operations
|
|
53
57
|
pub use exec::{
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
//! Docker registry API helpers.
|
|
2
|
+
//!
|
|
3
|
+
//! Provides lightweight helpers for querying registry manifests and configs
|
|
4
|
+
//! without pulling images.
|
|
5
|
+
|
|
6
|
+
use super::DockerError;
|
|
7
|
+
use reqwest::header::ACCEPT;
|
|
8
|
+
use serde::Deserialize;
|
|
9
|
+
use std::collections::HashMap;
|
|
10
|
+
|
|
11
|
+
#[derive(Deserialize)]
|
|
12
|
+
struct TokenResponse {
|
|
13
|
+
token: Option<String>,
|
|
14
|
+
access_token: Option<String>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[derive(Deserialize)]
|
|
18
|
+
struct ManifestConfig {
|
|
19
|
+
digest: String,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[derive(Deserialize)]
|
|
23
|
+
struct Manifest {
|
|
24
|
+
config: ManifestConfig,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[derive(Deserialize)]
|
|
28
|
+
struct ManifestList {
|
|
29
|
+
manifests: Vec<ManifestDescriptor>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Deserialize)]
|
|
33
|
+
struct ManifestDescriptor {
|
|
34
|
+
digest: String,
|
|
35
|
+
platform: Option<ManifestPlatform>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[derive(Deserialize)]
|
|
39
|
+
struct ManifestPlatform {
|
|
40
|
+
architecture: Option<String>,
|
|
41
|
+
os: Option<String>,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[derive(Deserialize)]
|
|
45
|
+
#[serde(untagged)]
|
|
46
|
+
enum ManifestResponse {
|
|
47
|
+
Single(Manifest),
|
|
48
|
+
List(ManifestList),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[derive(Deserialize)]
|
|
52
|
+
struct ImageConfig {
|
|
53
|
+
config: Option<ImageConfigDetails>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[derive(Deserialize)]
|
|
57
|
+
struct ImageConfigDetails {
|
|
58
|
+
#[serde(rename = "Labels")]
|
|
59
|
+
labels: Option<HashMap<String, String>>,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub async fn fetch_registry_version(
|
|
63
|
+
registry_base: &str,
|
|
64
|
+
token_url: &str,
|
|
65
|
+
repo: &str,
|
|
66
|
+
tag: &str,
|
|
67
|
+
maybe_manifest_digest: Option<&str>,
|
|
68
|
+
label_key: &str,
|
|
69
|
+
) -> Result<Option<String>, DockerError> {
|
|
70
|
+
let client = reqwest::Client::new();
|
|
71
|
+
let token = fetch_registry_token(&client, token_url).await?;
|
|
72
|
+
let manifest = if let Some(digest) = maybe_manifest_digest {
|
|
73
|
+
match fetch_registry_manifest(&client, registry_base, repo, digest, &token).await {
|
|
74
|
+
Ok(manifest) => manifest,
|
|
75
|
+
Err(digest_err) => fetch_registry_manifest(&client, registry_base, repo, tag, &token)
|
|
76
|
+
.await
|
|
77
|
+
.map_err(|tag_err| {
|
|
78
|
+
DockerError::Connection(format!(
|
|
79
|
+
"Failed to fetch registry manifest. Digest error: {digest_err}. Tag error: {tag_err}"
|
|
80
|
+
))
|
|
81
|
+
})?,
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
fetch_registry_manifest(&client, registry_base, repo, tag, &token).await?
|
|
85
|
+
};
|
|
86
|
+
let image_config = fetch_registry_image_config(
|
|
87
|
+
&client,
|
|
88
|
+
registry_base,
|
|
89
|
+
repo,
|
|
90
|
+
&manifest.config.digest,
|
|
91
|
+
&token,
|
|
92
|
+
)
|
|
93
|
+
.await?;
|
|
94
|
+
|
|
95
|
+
let maybe_version = image_config
|
|
96
|
+
.config
|
|
97
|
+
.and_then(|details| details.labels)
|
|
98
|
+
.and_then(|labels| labels.get(label_key).cloned());
|
|
99
|
+
|
|
100
|
+
Ok(maybe_version)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async fn fetch_registry_token(
|
|
104
|
+
client: &reqwest::Client,
|
|
105
|
+
token_url: &str,
|
|
106
|
+
) -> Result<String, DockerError> {
|
|
107
|
+
let response = client
|
|
108
|
+
.get(token_url)
|
|
109
|
+
.send()
|
|
110
|
+
.await
|
|
111
|
+
.map_err(|e| DockerError::Connection(format!("Failed to fetch registry token: {e}")))?;
|
|
112
|
+
|
|
113
|
+
let token_response: TokenResponse = response
|
|
114
|
+
.json()
|
|
115
|
+
.await
|
|
116
|
+
.map_err(|e| DockerError::Connection(format!("Failed to decode registry token: {e}")))?;
|
|
117
|
+
let token = token_response
|
|
118
|
+
.token
|
|
119
|
+
.or(token_response.access_token)
|
|
120
|
+
.ok_or_else(|| DockerError::Connection("Registry token missing".to_string()))?;
|
|
121
|
+
|
|
122
|
+
Ok(token)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async fn fetch_registry_manifest(
|
|
126
|
+
client: &reqwest::Client,
|
|
127
|
+
registry_base: &str,
|
|
128
|
+
repo: &str,
|
|
129
|
+
reference: &str,
|
|
130
|
+
token: &str,
|
|
131
|
+
) -> Result<Manifest, DockerError> {
|
|
132
|
+
let mut current_reference = reference.to_string();
|
|
133
|
+
loop {
|
|
134
|
+
let manifest_url = format!("{registry_base}/v2/{repo}/manifests/{current_reference}");
|
|
135
|
+
let response = client
|
|
136
|
+
.get(&manifest_url)
|
|
137
|
+
.header(
|
|
138
|
+
ACCEPT,
|
|
139
|
+
"application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json",
|
|
140
|
+
)
|
|
141
|
+
.bearer_auth(token)
|
|
142
|
+
.send()
|
|
143
|
+
.await
|
|
144
|
+
.map_err(|e| {
|
|
145
|
+
DockerError::Connection(format!("Failed to fetch registry manifest: {e}"))
|
|
146
|
+
})?;
|
|
147
|
+
|
|
148
|
+
if response.status() == reqwest::StatusCode::NOT_FOUND {
|
|
149
|
+
return Err(DockerError::Connection(format!(
|
|
150
|
+
"Manifest not found for {repo}:{current_reference}"
|
|
151
|
+
)));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let manifest_response: ManifestResponse = response
|
|
155
|
+
.json()
|
|
156
|
+
.await
|
|
157
|
+
.map_err(|e| DockerError::Connection(format!("Failed to decode manifest: {e}")))?;
|
|
158
|
+
|
|
159
|
+
match manifest_response {
|
|
160
|
+
ManifestResponse::Single(manifest) => return Ok(manifest),
|
|
161
|
+
ManifestResponse::List(list) => {
|
|
162
|
+
let digest = select_manifest_digest(&list).ok_or_else(|| {
|
|
163
|
+
DockerError::Connection(format!(
|
|
164
|
+
"No manifests available for {repo}:{current_reference}"
|
|
165
|
+
))
|
|
166
|
+
})?;
|
|
167
|
+
current_reference = digest;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn select_manifest_digest(list: &ManifestList) -> Option<String> {
|
|
174
|
+
let preferred = list.manifests.iter().find(|manifest| {
|
|
175
|
+
let Some(platform) = &manifest.platform else {
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
platform.os.as_deref() == Some("linux") && platform.architecture.as_deref() == Some("amd64")
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
preferred
|
|
182
|
+
.or_else(|| list.manifests.first())
|
|
183
|
+
.map(|manifest| manifest.digest.clone())
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async fn fetch_registry_image_config(
|
|
187
|
+
client: &reqwest::Client,
|
|
188
|
+
registry_base: &str,
|
|
189
|
+
repo: &str,
|
|
190
|
+
digest: &str,
|
|
191
|
+
token: &str,
|
|
192
|
+
) -> Result<ImageConfig, DockerError> {
|
|
193
|
+
let config_url = format!("{registry_base}/v2/{repo}/blobs/{digest}");
|
|
194
|
+
let response = client
|
|
195
|
+
.get(&config_url)
|
|
196
|
+
.bearer_auth(token)
|
|
197
|
+
.send()
|
|
198
|
+
.await
|
|
199
|
+
.map_err(|e| DockerError::Connection(format!("Failed to fetch image config: {e}")))?;
|
|
200
|
+
|
|
201
|
+
if !response.status().is_success() {
|
|
202
|
+
return Err(DockerError::Connection(format!(
|
|
203
|
+
"Failed to fetch image config: HTTP {}",
|
|
204
|
+
response.status()
|
|
205
|
+
)));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let config = response
|
|
209
|
+
.json()
|
|
210
|
+
.await
|
|
211
|
+
.map_err(|e| DockerError::Connection(format!("Failed to decode image config: {e}")))?;
|
|
212
|
+
|
|
213
|
+
Ok(config)
|
|
214
|
+
}
|
package/src/docker/version.rs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! Reads version information from Docker image labels.
|
|
4
4
|
|
|
5
|
-
use super::
|
|
5
|
+
use super::registry::fetch_registry_version;
|
|
6
|
+
use super::{DockerClient, DockerError, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
|
|
6
7
|
|
|
7
8
|
/// Version label key in Docker image
|
|
8
9
|
pub const VERSION_LABEL: &str = "org.opencode-cloud.version";
|
|
@@ -38,6 +39,64 @@ pub async fn get_image_version(
|
|
|
38
39
|
Ok(version)
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
pub async fn get_registry_latest_version(
|
|
43
|
+
client: &DockerClient,
|
|
44
|
+
) -> Result<Option<String>, DockerError> {
|
|
45
|
+
match fetch_ghcr_registry_version(client).await {
|
|
46
|
+
Ok(version) => Ok(version),
|
|
47
|
+
Err(ghcr_err) => fetch_dockerhub_registry_version(client).await.map_err(|dockerhub_err| {
|
|
48
|
+
DockerError::Connection(format!(
|
|
49
|
+
"Failed to fetch registry version. GHCR: {ghcr_err}. Docker Hub: {dockerhub_err}"
|
|
50
|
+
))
|
|
51
|
+
}),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async fn fetch_ghcr_registry_version(client: &DockerClient) -> Result<Option<String>, DockerError> {
|
|
56
|
+
let repo = IMAGE_NAME_GHCR
|
|
57
|
+
.strip_prefix("ghcr.io/")
|
|
58
|
+
.unwrap_or(IMAGE_NAME_GHCR);
|
|
59
|
+
let reference = format!("{IMAGE_NAME_GHCR}:{IMAGE_TAG_DEFAULT}");
|
|
60
|
+
let digest = fetch_registry_digest(client, &reference).await;
|
|
61
|
+
fetch_registry_version(
|
|
62
|
+
"https://ghcr.io",
|
|
63
|
+
&format!("https://ghcr.io/token?scope=repository:{repo}:pull"),
|
|
64
|
+
repo,
|
|
65
|
+
IMAGE_TAG_DEFAULT,
|
|
66
|
+
digest.as_deref(),
|
|
67
|
+
VERSION_LABEL,
|
|
68
|
+
)
|
|
69
|
+
.await
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async fn fetch_dockerhub_registry_version(
|
|
73
|
+
client: &DockerClient,
|
|
74
|
+
) -> Result<Option<String>, DockerError> {
|
|
75
|
+
let repo = IMAGE_NAME_DOCKERHUB;
|
|
76
|
+
let reference = format!("{IMAGE_NAME_DOCKERHUB}:{IMAGE_TAG_DEFAULT}");
|
|
77
|
+
let digest = fetch_registry_digest(client, &reference).await;
|
|
78
|
+
fetch_registry_version(
|
|
79
|
+
"https://registry-1.docker.io",
|
|
80
|
+
&format!(
|
|
81
|
+
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:{repo}:pull"
|
|
82
|
+
),
|
|
83
|
+
repo,
|
|
84
|
+
IMAGE_TAG_DEFAULT,
|
|
85
|
+
digest.as_deref(),
|
|
86
|
+
VERSION_LABEL,
|
|
87
|
+
)
|
|
88
|
+
.await
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async fn fetch_registry_digest(client: &DockerClient, reference: &str) -> Option<String> {
|
|
92
|
+
client
|
|
93
|
+
.inner()
|
|
94
|
+
.inspect_registry_image(reference, None)
|
|
95
|
+
.await
|
|
96
|
+
.ok()
|
|
97
|
+
.and_then(|info| info.descriptor.digest)
|
|
98
|
+
}
|
|
99
|
+
|
|
41
100
|
/// CLI version from Cargo.toml
|
|
42
101
|
pub fn get_cli_version() -> &'static str {
|
|
43
102
|
env!("CARGO_PKG_VERSION")
|