hub_tool/
tags.rs

1use anyhow::Context;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::{fetch, DockerHubClient};
6
7#[derive(Serialize, Deserialize, Debug)]
8pub struct Image {
9    architecture: String,
10    features: String,
11    variant: Option<String>,
12    digest: String,
13    os: Option<String>,
14    os_features: String,
15    os_version: Option<String>,
16    size: u64,
17    status: String,
18    last_pulled: DateTime<Utc>,
19    last_pushed: DateTime<Utc>,
20}
21
22#[derive(Serialize, Deserialize, Debug)]
23pub struct Tag {
24    /// The Docker ID of the creator of the current tag
25    creator: u64,
26
27    /// The ID of the current tag on the Docker Hub
28    id: u64,
29
30    images: Vec<Image>,
31    last_updated: DateTime<Utc>,
32    last_updater: u64,
33    last_updater_username: String,
34
35    /// The name of the tag for a given repository in the Docker Hub
36    name: String,
37
38    repository: u64,
39    full_size: u64,
40    v2: bool,
41    tag_status: String,
42    tag_last_pulled: DateTime<Utc>,
43    tag_last_pushed: DateTime<Utc>,
44    media_type: String,
45    content_type: String,
46    digest: String,
47}
48
49impl DockerHubClient {
50    /// List all the tags for a given repository on the Docker Hub
51    ///
52    /// This method expects both the organization or username via the `org`
53    /// argument plus the `repository` name for the repository that the tags
54    /// will be listed for.
55    pub async fn list_tags(&self, org: &str, repository: &str) -> anyhow::Result<Vec<Tag>> {
56        let url = self
57            .url
58            .join(&format!(
59                "v2/namespaces/{}/repositories/{}/tags", // For some reason the endpoint `v2/repositories/{}/{}/tags` works seamlessly
60                org, repository
61            ))
62            .context("failed formatting the url with the provided org and repository")?;
63
64        fetch::<Tag>(&self.client, &url)
65            .await
66            .context("fetching the provided url failed")
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use serde_json::json;
74
75    #[test]
76    fn test_tag_serde() {
77        let value = json!({
78          "creator": 14304909,
79          "id": 529481097,
80          "images": [
81            {
82              "architecture": "amd64",
83              "features": "",
84              "variant": null,
85              "digest": "sha256:96b6a4e66250499a9d87a4adf259ced7cd213e2320fb475914217f4d69abe98d",
86              "os": "linux",
87              "os_features": "",
88              "os_version": null,
89              "size": 755930694,
90              "status": "active",
91              "last_pulled": "2025-03-05T07:52:00.613197154Z",
92              "last_pushed": "2024-01-16T20:54:52Z"
93            }
94          ],
95          "last_updated": "2024-01-16T20:54:55.914808Z",
96          "last_updater": 14304909,
97          "last_updater_username": "mxyng",
98          "name": "gguf",
99          "repository": 22180121,
100          "full_size": 755930694,
101          "v2": true,
102          "tag_status": "active",
103          "tag_last_pulled": "2025-03-05T07:52:00.613197154Z",
104          "tag_last_pushed": "2024-01-16T20:54:55.914808Z",
105          "media_type": "application/vnd.oci.image.index.v1+json",
106          "content_type": "image",
107          "digest": "sha256:7c49490a9e4a7ca4326e09c4b47bc525aa0a9dfc8ea0b3a30d62af23a60db712"
108        });
109
110        let tag = serde_json::from_value::<Tag>(value)
111            .context("failed to deserialize the tag payload")
112            .unwrap();
113
114        println!("{tag:#?}");
115    }
116
117    #[tokio::test]
118    async fn test_list_tags() -> anyhow::Result<()> {
119        let pat =
120            std::env::var("DOCKER_PAT").context("environment variable `DOCKER_PAT` is not set")?;
121        let dh =
122            DockerHubClient::new(&pat).context("the docker hub client couldn't be instantiated")?;
123
124        println!("{:#?}", dh.list_tags("ollama", "ollama").await);
125
126        Ok(())
127    }
128}