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::progress::ProgressReporter;
8use super::{DockerClient, DockerError, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
9use bollard::image::TagImageOptions;
10use tracing::debug;
11
12/// Tag for the previous image version (used for rollback)
13pub const PREVIOUS_TAG: &str = "previous";
14
15/// Result of an update operation
16#[derive(Debug, Clone, PartialEq)]
17pub 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
31pub 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(&current_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
70pub 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
85pub 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
111pub 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)]
142mod 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}