github_workflow_update/updater/
docker.rs

1// Copyright (C) 2022 Leandro Lisboa Penz <lpenz@lpenz.org>
2// This file is subject to the terms and conditions defined in
3// file 'LICENSE', which is part of this source code package.
4
5use async_trait::async_trait;
6use tracing::instrument;
7
8use crate::entity::Entity;
9use crate::error::Error;
10use crate::error::Result;
11use crate::updater;
12use crate::version::Version;
13
14#[derive(Debug, Default)]
15pub struct Docker {}
16
17#[async_trait]
18impl updater::Updater for Docker {
19    fn url(&self, resource: &str) -> Option<String> {
20        url(resource)
21    }
22
23    async fn get_versions(&self, url: &str) -> Result<Vec<Version>> {
24        get_versions(url).await
25    }
26
27    fn updated_line(&self, entity: &Entity) -> Option<String> {
28        entity
29            .latest
30            .as_ref()
31            .map(|v| format!("{}:{}", entity.resource, v))
32    }
33}
34
35#[instrument(level = "debug")]
36pub fn url(resource: &str) -> Option<String> {
37    resource.strip_prefix("docker://").map(|path| {
38        format!(
39            "https://registry.hub.docker.com/v1/repositories/{}/tags",
40            path
41        )
42    })
43}
44
45#[instrument(level = "debug")]
46async fn get_json(url: &str) -> Result<serde_json::Value> {
47    let response = reqwest::get(url).await?;
48    if !response.status().is_success() {
49        return Err(Error::HttpError(url.into(), response.status()));
50    }
51    Ok(response.json::<serde_json::Value>().await?)
52}
53
54#[instrument(level = "debug")]
55fn parse_versions(data: serde_json::Value) -> Result<Vec<Version>> {
56    data.as_array()
57        .ok_or_else(|| Error::JsonParsing("invalid type for layer object list".into()))?
58        .iter()
59        .map(|layer| {
60            layer
61                .as_object()
62                .ok_or_else(|| Error::JsonParsing("invalid type for layer object".into()))?
63                .get("name")
64                .ok_or_else(|| {
65                    Error::JsonParsing("\"name\" field not found in layer object".into())
66                })
67                .map(|version_value| {
68                    let version_str = version_value.as_str().ok_or_else(|| {
69                        Error::JsonParsing("invalid type for \"name\" field in layer object".into())
70                    })?;
71                    Version::new(version_str)
72                        .ok_or_else(|| Error::VersionParsing(version_str.into()))
73                })?
74        })
75        .collect::<Result<Vec<Version>>>()
76}
77
78#[instrument(level = "debug")]
79pub async fn get_versions(url: &str) -> Result<Vec<Version>> {
80    let data = get_json(url).await?;
81    let versions = parse_versions(data)?;
82    Ok(versions)
83}
84
85#[test]
86fn test_docker_parse_versions() -> Result<()> {
87    let json_str = r#"[{"layer": "", "name": "latest"}, {"layer": "", "name": "0.2"}, {"layer": "", "name": "0.3"}, {"layer": "", "name": "0.4"}, {"layer": "", "name": "0.6"}, {"layer": "", "name": "0.7"}, {"layer": "", "name": "0.8.0"}, {"layer": "", "name": "0.9.0"}]"#;
88    let json_value: serde_json::Value = serde_json::from_str(json_str)?;
89    let versions = parse_versions(json_value)?
90        .into_iter()
91        .map(|v| format!("{}", v))
92        .collect::<Vec<_>>();
93    assert_eq!(
94        versions,
95        ["latest", "0.2", "0.3", "0.4", "0.6", "0.7", "0.8.0", "0.9.0"]
96    );
97    Ok(())
98}