Skip to main content

opencode_cloud_core/docker/
update.rs

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
6use super::image::{image_exists, pull_image};
7use super::profile::active_resource_names;
8use super::progress::ProgressReporter;
9use super::{DockerClient, DockerError, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
10use bollard::query_parameters::TagImageOptions;
11use tracing::debug;
12
13/// Tag for the previous image version (used for rollback)
14pub const PREVIOUS_TAG: &str = "previous";
15
16/// Result of an update operation
17#[derive(Debug, Clone, PartialEq)]
18pub enum UpdateResult {
19    /// Update completed successfully
20    Success,
21    /// Already on the latest version
22    AlreadyLatest,
23}
24
25/// Tag the current image as "previous" for rollback support
26///
27/// This allows users to rollback to the version they had before updating.
28/// If the current image doesn't exist, this is silently skipped.
29///
30/// # Arguments
31/// * `client` - Docker client
32pub async fn tag_current_as_previous(client: &DockerClient) -> Result<(), DockerError> {
33    let names = active_resource_names();
34    let current_image = format!("{IMAGE_NAME_GHCR}:{}", names.image_tag);
35    let previous_image = format!("{IMAGE_NAME_GHCR}:{}", names.previous_image_tag);
36
37    debug!(
38        "Tagging current image {} as {}",
39        current_image, previous_image
40    );
41
42    // Check if current image exists
43    if !image_exists(client, IMAGE_NAME_GHCR, &names.image_tag).await? {
44        debug!("Current image not found, skipping backup tag");
45        return Ok(());
46    }
47
48    // Tag current as previous
49    let options = TagImageOptions {
50        repo: Some(IMAGE_NAME_GHCR.to_string()),
51        tag: Some(names.previous_image_tag),
52    };
53
54    client
55        .inner()
56        .tag_image(&current_image, Some(options))
57        .await
58        .map_err(|e| {
59            DockerError::Container(format!("Failed to tag current image as previous: {e}"))
60        })?;
61
62    debug!("Successfully tagged current image as previous");
63    Ok(())
64}
65
66/// Check if a previous image exists for rollback
67///
68/// Returns true if a rollback is possible, false otherwise.
69///
70/// # Arguments
71/// * `client` - Docker client
72pub async fn has_previous_image(client: &DockerClient) -> Result<bool, DockerError> {
73    let names = active_resource_names();
74    image_exists(client, IMAGE_NAME_GHCR, &names.previous_image_tag).await
75}
76
77/// Update the opencode image to the latest version
78///
79/// This operation:
80/// 1. Tags the current image as "previous" for rollback
81/// 2. Pulls the latest image from the registry
82///
83/// Returns UpdateResult indicating success or if already on latest.
84///
85/// # Arguments
86/// * `client` - Docker client
87/// * `progress` - Progress reporter for user feedback
88pub async fn update_image(
89    client: &DockerClient,
90    progress: &mut ProgressReporter,
91) -> Result<UpdateResult, DockerError> {
92    // Step 1: Tag current image as previous for rollback
93    progress.add_spinner("backup", "Backing up current image");
94    tag_current_as_previous(client).await?;
95    progress.finish("backup", "Current image backed up");
96
97    // Step 2: Pull latest image
98    progress.add_spinner("pull", "Pulling latest image");
99    pull_image(client, Some(IMAGE_TAG_DEFAULT), progress).await?;
100    progress.finish("pull", "Latest image pulled");
101
102    Ok(UpdateResult::Success)
103}
104
105/// Rollback to the previous image version
106///
107/// This re-tags the "previous" image as "latest", effectively reverting
108/// to the version that was active before the last update.
109///
110/// Returns an error if no previous image exists.
111///
112/// # Arguments
113/// * `client` - Docker client
114pub async fn rollback_image(client: &DockerClient) -> Result<(), DockerError> {
115    // Check if previous image exists
116    if !has_previous_image(client).await? {
117        return Err(DockerError::Container(
118            "No previous image available for rollback. Update at least once before using rollback."
119                .to_string(),
120        ));
121    }
122
123    let names = active_resource_names();
124    let previous_image = format!("{IMAGE_NAME_GHCR}:{}", names.previous_image_tag);
125    let current_image = format!("{IMAGE_NAME_GHCR}:{}", names.image_tag);
126
127    debug!("Rolling back from {} to {}", current_image, previous_image);
128
129    // Re-tag previous as latest
130    let options = TagImageOptions {
131        repo: Some(IMAGE_NAME_GHCR.to_string()),
132        tag: Some(names.image_tag),
133    };
134
135    client
136        .inner()
137        .tag_image(&previous_image, Some(options))
138        .await
139        .map_err(|e| DockerError::Container(format!("Failed to rollback image: {e}")))?;
140
141    debug!("Successfully rolled back to previous image");
142    Ok(())
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn previous_tag_constant() {
151        assert_eq!(PREVIOUS_TAG, "previous");
152    }
153
154    #[test]
155    fn update_result_variants() {
156        assert_eq!(UpdateResult::Success, UpdateResult::Success);
157        assert_eq!(UpdateResult::AlreadyLatest, UpdateResult::AlreadyLatest);
158        assert_ne!(UpdateResult::Success, UpdateResult::AlreadyLatest);
159    }
160}