opencode_cloud_core/docker/
version.rs1use super::registry::fetch_registry_version;
6use super::{DockerClient, DockerError, IMAGE_NAME_DOCKERHUB, IMAGE_NAME_GHCR, IMAGE_TAG_DEFAULT};
7
8pub const VERSION_LABEL: &str = "org.opencode-cloud.version";
10
11pub async fn get_image_version(
16 client: &DockerClient,
17 image_name: &str,
18) -> Result<Option<String>, DockerError> {
19 let inspect = match client.inner().inspect_image(image_name).await {
20 Ok(info) => info,
21 Err(bollard::errors::Error::DockerResponseServerError {
22 status_code: 404, ..
23 }) => {
24 return Ok(None);
25 }
26 Err(e) => {
27 return Err(DockerError::Connection(format!(
28 "Failed to inspect image: {e}"
29 )));
30 }
31 };
32
33 let version = inspect
35 .config
36 .and_then(|c| c.labels)
37 .and_then(|labels| labels.get(VERSION_LABEL).cloned());
38
39 Ok(version)
40}
41
42pub 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
55async 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
72async 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
91async 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
100pub fn get_cli_version() -> &'static str {
102 env!("CARGO_PKG_VERSION")
103}
104
105pub fn versions_compatible(cli_version: &str, image_version: Option<&str>) -> bool {
110 match image_version {
111 None => true, Some("dev") => true, Some(img_ver) => cli_version == img_ver,
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_versions_compatible_none() {
123 assert!(versions_compatible("1.0.8", None));
124 }
125
126 #[test]
127 fn test_versions_compatible_dev() {
128 assert!(versions_compatible("1.0.8", Some("dev")));
129 }
130
131 #[test]
132 fn test_versions_compatible_same() {
133 assert!(versions_compatible("1.0.8", Some("1.0.8")));
134 }
135
136 #[test]
137 fn test_versions_compatible_different() {
138 assert!(!versions_compatible("1.0.8", Some("1.0.7")));
139 }
140
141 #[test]
142 fn test_get_cli_version_format() {
143 let version = get_cli_version();
144 assert!(version.contains('.'));
146 let parts: Vec<&str> = version.split('.').collect();
147 assert_eq!(parts.len(), 3);
148 }
149
150 #[test]
151 fn test_version_label_constant() {
152 assert_eq!(VERSION_LABEL, "org.opencode-cloud.version");
153 }
154}