@opencode-cloud/core 1.0.8 → 1.0.10
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 +9 -2
- package/README.md +7 -0
- package/package.json +1 -1
- package/src/config/mod.rs +8 -3
- package/src/config/paths.rs +14 -0
- package/src/config/schema.rs +470 -0
- package/src/config/validation.rs +271 -0
- package/src/docker/Dockerfile +278 -153
- package/src/docker/client.rs +132 -3
- package/src/docker/container.rs +90 -33
- package/src/docker/exec.rs +278 -0
- package/src/docker/health.rs +165 -0
- package/src/docker/image.rs +2 -4
- package/src/docker/mod.rs +47 -4
- package/src/docker/progress.rs +4 -4
- package/src/docker/update.rs +156 -0
- package/src/docker/users.rs +357 -0
- package/src/docker/version.rs +95 -0
- package/src/host/error.rs +61 -0
- package/src/host/mod.rs +29 -0
- package/src/host/provision.rs +394 -0
- package/src/host/schema.rs +308 -0
- package/src/host/ssh_config.rs +282 -0
- package/src/host/storage.rs +118 -0
- package/src/host/tunnel.rs +268 -0
- package/src/lib.rs +10 -1
- package/src/platform/launchd.rs +1 -1
- package/src/platform/systemd.rs +6 -6
- package/src/singleton/mod.rs +1 -1
- package/src/version.rs +1 -6
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
//! Health check module for OpenCode service
|
|
2
|
+
//!
|
|
3
|
+
//! Provides health checking functionality by querying OpenCode's /global/health endpoint.
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
use std::time::Duration;
|
|
7
|
+
use thiserror::Error;
|
|
8
|
+
|
|
9
|
+
use super::DockerClient;
|
|
10
|
+
|
|
11
|
+
/// Response from OpenCode's /global/health endpoint
|
|
12
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
13
|
+
pub struct HealthResponse {
|
|
14
|
+
/// Whether the service is healthy
|
|
15
|
+
pub healthy: bool,
|
|
16
|
+
/// Service version string
|
|
17
|
+
pub version: String,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Extended health response including container stats
|
|
21
|
+
#[derive(Debug, Serialize)]
|
|
22
|
+
pub struct ExtendedHealthResponse {
|
|
23
|
+
/// Whether the service is healthy
|
|
24
|
+
pub healthy: bool,
|
|
25
|
+
/// Service version string
|
|
26
|
+
pub version: String,
|
|
27
|
+
/// Container state (running, stopped, etc.)
|
|
28
|
+
pub container_state: String,
|
|
29
|
+
/// Uptime in seconds
|
|
30
|
+
pub uptime_seconds: u64,
|
|
31
|
+
/// Memory usage in megabytes (if available)
|
|
32
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
33
|
+
pub memory_usage_mb: Option<u64>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Errors that can occur during health checks
|
|
37
|
+
#[derive(Debug, Error)]
|
|
38
|
+
pub enum HealthError {
|
|
39
|
+
/// HTTP request failed
|
|
40
|
+
#[error("Request failed: {0}")]
|
|
41
|
+
RequestError(#[from] reqwest::Error),
|
|
42
|
+
|
|
43
|
+
/// Service returned non-200 status
|
|
44
|
+
#[error("Service unhealthy (HTTP {0})")]
|
|
45
|
+
Unhealthy(u16),
|
|
46
|
+
|
|
47
|
+
/// Connection refused - service may not be running
|
|
48
|
+
#[error("Connection refused - service may not be running")]
|
|
49
|
+
ConnectionRefused,
|
|
50
|
+
|
|
51
|
+
/// Request timed out - service may be starting
|
|
52
|
+
#[error("Timeout - service may be starting")]
|
|
53
|
+
Timeout,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Check health by querying OpenCode's /global/health endpoint
|
|
57
|
+
///
|
|
58
|
+
/// Returns the health response on success (HTTP 200).
|
|
59
|
+
/// Returns an error for connection issues, timeouts, or non-200 responses.
|
|
60
|
+
pub async fn check_health(port: u16) -> Result<HealthResponse, HealthError> {
|
|
61
|
+
let url = format!("http://127.0.0.1:{port}/global/health");
|
|
62
|
+
|
|
63
|
+
let client = reqwest::Client::builder()
|
|
64
|
+
.timeout(Duration::from_secs(5))
|
|
65
|
+
.build()?;
|
|
66
|
+
|
|
67
|
+
let response = match client.get(&url).send().await {
|
|
68
|
+
Ok(resp) => resp,
|
|
69
|
+
Err(e) => {
|
|
70
|
+
// Check for connection refused
|
|
71
|
+
if e.is_connect() {
|
|
72
|
+
return Err(HealthError::ConnectionRefused);
|
|
73
|
+
}
|
|
74
|
+
// Check for timeout
|
|
75
|
+
if e.is_timeout() {
|
|
76
|
+
return Err(HealthError::Timeout);
|
|
77
|
+
}
|
|
78
|
+
return Err(HealthError::RequestError(e));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let status = response.status();
|
|
83
|
+
|
|
84
|
+
if status.is_success() {
|
|
85
|
+
let health_response = response.json::<HealthResponse>().await?;
|
|
86
|
+
Ok(health_response)
|
|
87
|
+
} else {
|
|
88
|
+
Err(HealthError::Unhealthy(status.as_u16()))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Check health with extended information including container stats
|
|
93
|
+
///
|
|
94
|
+
/// Combines basic health check with container statistics from Docker.
|
|
95
|
+
/// If container stats fail, still returns response with container_state = "unknown".
|
|
96
|
+
pub async fn check_health_extended(
|
|
97
|
+
client: &DockerClient,
|
|
98
|
+
port: u16,
|
|
99
|
+
) -> Result<ExtendedHealthResponse, HealthError> {
|
|
100
|
+
// Get basic health info
|
|
101
|
+
let health = check_health(port).await?;
|
|
102
|
+
|
|
103
|
+
// Get container stats
|
|
104
|
+
let container_name = super::CONTAINER_NAME;
|
|
105
|
+
|
|
106
|
+
// Try to get container info
|
|
107
|
+
let (container_state, uptime_seconds, memory_usage_mb) =
|
|
108
|
+
match client.inner().inspect_container(container_name, None).await {
|
|
109
|
+
Ok(info) => {
|
|
110
|
+
let state = info
|
|
111
|
+
.state
|
|
112
|
+
.as_ref()
|
|
113
|
+
.and_then(|s| s.status.as_ref())
|
|
114
|
+
.map(|s| s.to_string())
|
|
115
|
+
.unwrap_or_else(|| "unknown".to_string());
|
|
116
|
+
|
|
117
|
+
// Calculate uptime
|
|
118
|
+
let uptime = info
|
|
119
|
+
.state
|
|
120
|
+
.as_ref()
|
|
121
|
+
.and_then(|s| s.started_at.as_ref())
|
|
122
|
+
.and_then(|started| {
|
|
123
|
+
let timestamp = chrono::DateTime::parse_from_rfc3339(started).ok()?;
|
|
124
|
+
let now = chrono::Utc::now();
|
|
125
|
+
let started_utc = timestamp.with_timezone(&chrono::Utc);
|
|
126
|
+
if now >= started_utc {
|
|
127
|
+
Some((now - started_utc).num_seconds() as u64)
|
|
128
|
+
} else {
|
|
129
|
+
None
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.unwrap_or(0);
|
|
133
|
+
|
|
134
|
+
// Get memory usage (would require stats API call - skip for now)
|
|
135
|
+
let memory = None;
|
|
136
|
+
|
|
137
|
+
(state, uptime, memory)
|
|
138
|
+
}
|
|
139
|
+
Err(_) => ("unknown".to_string(), 0, None),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
Ok(ExtendedHealthResponse {
|
|
143
|
+
healthy: health.healthy,
|
|
144
|
+
version: health.version,
|
|
145
|
+
container_state,
|
|
146
|
+
uptime_seconds,
|
|
147
|
+
memory_usage_mb,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#[cfg(test)]
|
|
152
|
+
mod tests {
|
|
153
|
+
use super::*;
|
|
154
|
+
|
|
155
|
+
#[tokio::test]
|
|
156
|
+
async fn test_health_check_connection_refused() {
|
|
157
|
+
// Port 1 should always refuse connection
|
|
158
|
+
let result = check_health(1).await;
|
|
159
|
+
assert!(result.is_err());
|
|
160
|
+
match result.unwrap_err() {
|
|
161
|
+
HealthError::ConnectionRefused => {}
|
|
162
|
+
other => panic!("Expected ConnectionRefused, got: {other:?}"),
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/docker/image.rs
CHANGED
|
@@ -203,8 +203,7 @@ pub async fn pull_image(
|
|
|
203
203
|
Ok(full_name)
|
|
204
204
|
}
|
|
205
205
|
Err(dockerhub_err) => Err(DockerError::Pull(format!(
|
|
206
|
-
"Failed to pull from both registries. GHCR: {}. Docker Hub: {}"
|
|
207
|
-
ghcr_err, dockerhub_err
|
|
206
|
+
"Failed to pull from both registries. GHCR: {ghcr_err}. Docker Hub: {dockerhub_err}"
|
|
208
207
|
))),
|
|
209
208
|
}
|
|
210
209
|
}
|
|
@@ -246,8 +245,7 @@ async fn pull_from_registry(
|
|
|
246
245
|
|
|
247
246
|
Err(last_error.unwrap_or_else(|| {
|
|
248
247
|
DockerError::Pull(format!(
|
|
249
|
-
"Pull failed for {} after {} attempts"
|
|
250
|
-
full_name, MAX_PULL_RETRIES
|
|
248
|
+
"Pull failed for {full_name} after {MAX_PULL_RETRIES} attempts"
|
|
251
249
|
))
|
|
252
250
|
}))
|
|
253
251
|
}
|
package/src/docker/mod.rs
CHANGED
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
//! - Image build and pull operations
|
|
9
9
|
//! - Volume management for persistent storage
|
|
10
10
|
//! - Container lifecycle (create, start, stop, remove)
|
|
11
|
+
//! - Container exec for running commands inside containers
|
|
12
|
+
//! - User management operations (create, delete, lock/unlock users)
|
|
13
|
+
//! - Image update and rollback operations
|
|
11
14
|
|
|
12
15
|
mod client;
|
|
13
16
|
pub mod container;
|
|
14
17
|
mod dockerfile;
|
|
15
18
|
mod error;
|
|
19
|
+
pub mod exec;
|
|
20
|
+
mod health;
|
|
16
21
|
pub mod image;
|
|
17
22
|
pub mod progress;
|
|
23
|
+
pub mod update;
|
|
24
|
+
pub mod users;
|
|
25
|
+
mod version;
|
|
18
26
|
pub mod volume;
|
|
19
27
|
|
|
20
28
|
// Core types
|
|
@@ -22,12 +30,32 @@ pub use client::DockerClient;
|
|
|
22
30
|
pub use error::DockerError;
|
|
23
31
|
pub use progress::ProgressReporter;
|
|
24
32
|
|
|
33
|
+
// Health check operations
|
|
34
|
+
pub use health::{
|
|
35
|
+
ExtendedHealthResponse, HealthError, HealthResponse, check_health, check_health_extended,
|
|
36
|
+
};
|
|
37
|
+
|
|
25
38
|
// Dockerfile constants
|
|
26
39
|
pub use dockerfile::{DOCKERFILE, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
|
|
27
40
|
|
|
28
41
|
// Image operations
|
|
29
42
|
pub use image::{build_image, image_exists, pull_image};
|
|
30
43
|
|
|
44
|
+
// Update operations
|
|
45
|
+
pub use update::{UpdateResult, has_previous_image, rollback_image, update_image};
|
|
46
|
+
|
|
47
|
+
// Version detection
|
|
48
|
+
pub use version::{VERSION_LABEL, get_cli_version, get_image_version, versions_compatible};
|
|
49
|
+
|
|
50
|
+
// Container exec operations
|
|
51
|
+
pub use exec::{exec_command, exec_command_exit_code, exec_command_with_stdin};
|
|
52
|
+
|
|
53
|
+
// User management operations
|
|
54
|
+
pub use users::{
|
|
55
|
+
UserInfo, create_user, delete_user, list_users, lock_user, set_user_password, unlock_user,
|
|
56
|
+
user_exists,
|
|
57
|
+
};
|
|
58
|
+
|
|
31
59
|
// Volume management
|
|
32
60
|
pub use volume::{
|
|
33
61
|
MOUNT_CONFIG, MOUNT_PROJECTS, MOUNT_SESSION, VOLUME_CONFIG, VOLUME_NAMES, VOLUME_PROJECTS,
|
|
@@ -49,10 +77,16 @@ pub use container::{
|
|
|
49
77
|
/// * `client` - Docker client
|
|
50
78
|
/// * `opencode_web_port` - Port to bind on host for opencode web UI (defaults to OPENCODE_WEB_PORT)
|
|
51
79
|
/// * `env_vars` - Additional environment variables (optional)
|
|
80
|
+
/// * `bind_address` - IP address to bind on host (defaults to "127.0.0.1")
|
|
81
|
+
/// * `cockpit_port` - Port to bind on host for Cockpit (defaults to 9090)
|
|
82
|
+
/// * `cockpit_enabled` - Whether to enable Cockpit port mapping (defaults to true)
|
|
52
83
|
pub async fn setup_and_start(
|
|
53
84
|
client: &DockerClient,
|
|
54
85
|
opencode_web_port: Option<u16>,
|
|
55
86
|
env_vars: Option<Vec<String>>,
|
|
87
|
+
bind_address: Option<&str>,
|
|
88
|
+
cockpit_port: Option<u16>,
|
|
89
|
+
cockpit_enabled: Option<bool>,
|
|
56
90
|
) -> Result<String, DockerError> {
|
|
57
91
|
// Ensure volumes exist first
|
|
58
92
|
volume::ensure_volumes_exist(client).await?;
|
|
@@ -65,13 +99,23 @@ pub async fn setup_and_start(
|
|
|
65
99
|
.inspect_container(container::CONTAINER_NAME, None)
|
|
66
100
|
.await
|
|
67
101
|
.map_err(|e| {
|
|
68
|
-
DockerError::Container(format!("Failed to inspect existing container: {}"
|
|
102
|
+
DockerError::Container(format!("Failed to inspect existing container: {e}"))
|
|
69
103
|
})?;
|
|
70
104
|
info.id
|
|
71
105
|
.unwrap_or_else(|| container::CONTAINER_NAME.to_string())
|
|
72
106
|
} else {
|
|
73
107
|
// Create new container
|
|
74
|
-
container::create_container(
|
|
108
|
+
container::create_container(
|
|
109
|
+
client,
|
|
110
|
+
None,
|
|
111
|
+
None,
|
|
112
|
+
opencode_web_port,
|
|
113
|
+
env_vars,
|
|
114
|
+
bind_address,
|
|
115
|
+
cockpit_port,
|
|
116
|
+
cockpit_enabled,
|
|
117
|
+
)
|
|
118
|
+
.await?
|
|
75
119
|
};
|
|
76
120
|
|
|
77
121
|
// Start if not running
|
|
@@ -93,8 +137,7 @@ pub async fn stop_service(client: &DockerClient, remove: bool) -> Result<(), Doc
|
|
|
93
137
|
// Check if container exists
|
|
94
138
|
if !container::container_exists(client, name).await? {
|
|
95
139
|
return Err(DockerError::Container(format!(
|
|
96
|
-
"Container '{}' does not exist"
|
|
97
|
-
name
|
|
140
|
+
"Container '{name}' does not exist"
|
|
98
141
|
)));
|
|
99
142
|
}
|
|
100
143
|
|
package/src/docker/progress.rs
CHANGED
|
@@ -48,9 +48,9 @@ fn format_elapsed(duration: Duration) -> String {
|
|
|
48
48
|
let seconds = total_secs % 60;
|
|
49
49
|
|
|
50
50
|
if hours > 0 {
|
|
51
|
-
format!("{:02}:{:02}:{:02}"
|
|
51
|
+
format!("{hours:02}:{minutes:02}:{seconds:02}")
|
|
52
52
|
} else {
|
|
53
|
-
format!("{:02}:{:02}"
|
|
53
|
+
format!("{minutes:02}:{seconds:02}")
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -116,8 +116,8 @@ impl ProgressReporter {
|
|
|
116
116
|
// Format: "[elapsed] Context · message" or "[elapsed] message"
|
|
117
117
|
// Timer at the beginning for easy scanning
|
|
118
118
|
match &self.context {
|
|
119
|
-
Some(ctx) => format!("[{}] {} · {}"
|
|
120
|
-
None => format!("[{}] {}"
|
|
119
|
+
Some(ctx) => format!("[{elapsed}] {ctx} · {clean_msg}"),
|
|
120
|
+
None => format!("[{elapsed}] {clean_msg}"),
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
//! Docker image update and rollback operations
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides functionality to update the opencode image to the latest
|
|
4
|
+
//! version and rollback to a previous version if needed.
|
|
5
|
+
|
|
6
|
+
use super::image::{image_exists, pull_image};
|
|
7
|
+
use super::progress::ProgressReporter;
|
|
8
|
+
use super::{DockerClient, DockerError, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
|
|
9
|
+
use bollard::image::TagImageOptions;
|
|
10
|
+
use tracing::debug;
|
|
11
|
+
|
|
12
|
+
/// Tag for the previous image version (used for rollback)
|
|
13
|
+
pub const PREVIOUS_TAG: &str = "previous";
|
|
14
|
+
|
|
15
|
+
/// Result of an update operation
|
|
16
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
17
|
+
pub enum UpdateResult {
|
|
18
|
+
/// Update completed successfully
|
|
19
|
+
Success,
|
|
20
|
+
/// Already on the latest version
|
|
21
|
+
AlreadyLatest,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Tag the current image as "previous" for rollback support
|
|
25
|
+
///
|
|
26
|
+
/// This allows users to rollback to the version they had before updating.
|
|
27
|
+
/// If the current image doesn't exist, this is silently skipped.
|
|
28
|
+
///
|
|
29
|
+
/// # Arguments
|
|
30
|
+
/// * `client` - Docker client
|
|
31
|
+
pub async fn tag_current_as_previous(client: &DockerClient) -> Result<(), DockerError> {
|
|
32
|
+
let current_image = format!("{IMAGE_NAME_GHCR}:{IMAGE_TAG_DEFAULT}");
|
|
33
|
+
let previous_image = format!("{IMAGE_NAME_GHCR}:{PREVIOUS_TAG}");
|
|
34
|
+
|
|
35
|
+
debug!(
|
|
36
|
+
"Tagging current image {} as {}",
|
|
37
|
+
current_image, previous_image
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Check if current image exists
|
|
41
|
+
if !image_exists(client, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT).await? {
|
|
42
|
+
debug!("Current image not found, skipping backup tag");
|
|
43
|
+
return Ok(());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Tag current as previous
|
|
47
|
+
let options = TagImageOptions {
|
|
48
|
+
repo: IMAGE_NAME_GHCR,
|
|
49
|
+
tag: PREVIOUS_TAG,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
client
|
|
53
|
+
.inner()
|
|
54
|
+
.tag_image(¤t_image, Some(options))
|
|
55
|
+
.await
|
|
56
|
+
.map_err(|e| {
|
|
57
|
+
DockerError::Container(format!("Failed to tag current image as previous: {e}"))
|
|
58
|
+
})?;
|
|
59
|
+
|
|
60
|
+
debug!("Successfully tagged current image as previous");
|
|
61
|
+
Ok(())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Check if a previous image exists for rollback
|
|
65
|
+
///
|
|
66
|
+
/// Returns true if a rollback is possible, false otherwise.
|
|
67
|
+
///
|
|
68
|
+
/// # Arguments
|
|
69
|
+
/// * `client` - Docker client
|
|
70
|
+
pub async fn has_previous_image(client: &DockerClient) -> Result<bool, DockerError> {
|
|
71
|
+
image_exists(client, IMAGE_NAME_GHCR, PREVIOUS_TAG).await
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Update the opencode image to the latest version
|
|
75
|
+
///
|
|
76
|
+
/// This operation:
|
|
77
|
+
/// 1. Tags the current image as "previous" for rollback
|
|
78
|
+
/// 2. Pulls the latest image from the registry
|
|
79
|
+
///
|
|
80
|
+
/// Returns UpdateResult indicating success or if already on latest.
|
|
81
|
+
///
|
|
82
|
+
/// # Arguments
|
|
83
|
+
/// * `client` - Docker client
|
|
84
|
+
/// * `progress` - Progress reporter for user feedback
|
|
85
|
+
pub async fn update_image(
|
|
86
|
+
client: &DockerClient,
|
|
87
|
+
progress: &mut ProgressReporter,
|
|
88
|
+
) -> Result<UpdateResult, DockerError> {
|
|
89
|
+
// Step 1: Tag current image as previous for rollback
|
|
90
|
+
progress.add_spinner("backup", "Backing up current image");
|
|
91
|
+
tag_current_as_previous(client).await?;
|
|
92
|
+
progress.finish("backup", "Current image backed up");
|
|
93
|
+
|
|
94
|
+
// Step 2: Pull latest image
|
|
95
|
+
progress.add_spinner("pull", "Pulling latest image");
|
|
96
|
+
pull_image(client, Some(IMAGE_TAG_DEFAULT), progress).await?;
|
|
97
|
+
progress.finish("pull", "Latest image pulled");
|
|
98
|
+
|
|
99
|
+
Ok(UpdateResult::Success)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Rollback to the previous image version
|
|
103
|
+
///
|
|
104
|
+
/// This re-tags the "previous" image as "latest", effectively reverting
|
|
105
|
+
/// to the version that was active before the last update.
|
|
106
|
+
///
|
|
107
|
+
/// Returns an error if no previous image exists.
|
|
108
|
+
///
|
|
109
|
+
/// # Arguments
|
|
110
|
+
/// * `client` - Docker client
|
|
111
|
+
pub async fn rollback_image(client: &DockerClient) -> Result<(), DockerError> {
|
|
112
|
+
// Check if previous image exists
|
|
113
|
+
if !has_previous_image(client).await? {
|
|
114
|
+
return Err(DockerError::Container(
|
|
115
|
+
"No previous image available for rollback. Update at least once before using rollback."
|
|
116
|
+
.to_string(),
|
|
117
|
+
));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let previous_image = format!("{IMAGE_NAME_GHCR}:{PREVIOUS_TAG}");
|
|
121
|
+
let current_image = format!("{IMAGE_NAME_GHCR}:{IMAGE_TAG_DEFAULT}");
|
|
122
|
+
|
|
123
|
+
debug!("Rolling back from {} to {}", current_image, previous_image);
|
|
124
|
+
|
|
125
|
+
// Re-tag previous as latest
|
|
126
|
+
let options = TagImageOptions {
|
|
127
|
+
repo: IMAGE_NAME_GHCR,
|
|
128
|
+
tag: IMAGE_TAG_DEFAULT,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
client
|
|
132
|
+
.inner()
|
|
133
|
+
.tag_image(&previous_image, Some(options))
|
|
134
|
+
.await
|
|
135
|
+
.map_err(|e| DockerError::Container(format!("Failed to rollback image: {e}")))?;
|
|
136
|
+
|
|
137
|
+
debug!("Successfully rolled back to previous image");
|
|
138
|
+
Ok(())
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[cfg(test)]
|
|
142
|
+
mod tests {
|
|
143
|
+
use super::*;
|
|
144
|
+
|
|
145
|
+
#[test]
|
|
146
|
+
fn previous_tag_constant() {
|
|
147
|
+
assert_eq!(PREVIOUS_TAG, "previous");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn update_result_variants() {
|
|
152
|
+
assert_eq!(UpdateResult::Success, UpdateResult::Success);
|
|
153
|
+
assert_eq!(UpdateResult::AlreadyLatest, UpdateResult::AlreadyLatest);
|
|
154
|
+
assert_ne!(UpdateResult::Success, UpdateResult::AlreadyLatest);
|
|
155
|
+
}
|
|
156
|
+
}
|