opencode_cloud_core/docker/
update.rs1use 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
13pub const PREVIOUS_TAG: &str = "previous";
15
16#[derive(Debug, Clone, PartialEq)]
18pub enum UpdateResult {
19 Success,
21 AlreadyLatest,
23}
24
25pub 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 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 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(¤t_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
66pub 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
77pub async fn update_image(
89 client: &DockerClient,
90 progress: &mut ProgressReporter,
91) -> Result<UpdateResult, DockerError> {
92 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 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
105pub async fn rollback_image(client: &DockerClient) -> Result<(), DockerError> {
115 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 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}