Skip to main content

xianyun_deploy/registrycache/
config.rs

1//! Zot config templating.
2//!
3//! Ported from `internal/registrycache/config.go`.
4
5use serde::Serialize;
6use thiserror::Error;
7
8/// Errors from rendering a zot config.
9#[derive(Debug, Error)]
10pub enum ConfigError {
11    /// Returned when called with an empty upstream list. Callers should
12    /// treat this as a feature-off signal, not a configuration error.
13    #[error("no upstreams configured")]
14    NoUpstreams,
15    /// The config document failed to serialize to JSON.
16    #[error("marshalling zot config: {0}")]
17    Serialize(#[from] serde_json::Error),
18}
19
20/// Maps a registry hostname (as it appears in image refs) to the URL
21/// zot needs to dial. Only `docker.io` has a non-obvious mapping;
22/// everything else passes through with an `https://` prefix.
23fn upstream_url(host: &str) -> String {
24    if host == "docker.io" {
25        "https://registry-1.docker.io".to_string()
26    } else {
27        format!("https://{host}")
28    }
29}
30
31#[derive(Serialize)]
32struct ContentEntry {
33    prefix: &'static str,
34}
35
36#[derive(Serialize)]
37struct SyncRegistry {
38    urls: Vec<String>,
39    #[serde(rename = "onDemand")]
40    on_demand: bool,
41    #[serde(rename = "tlsVerify")]
42    tls_verify: bool,
43    content: Vec<ContentEntry>,
44}
45
46#[derive(Serialize)]
47struct Storage<'a> {
48    #[serde(rename = "rootDirectory")]
49    root_directory: &'a str,
50    gc: bool,
51    dedupe: bool,
52}
53
54#[derive(Serialize)]
55struct Http<'a> {
56    address: &'static str,
57    port: &'a str,
58}
59
60#[derive(Serialize)]
61struct Log {
62    level: &'static str,
63}
64
65#[derive(Serialize)]
66struct Sync {
67    enable: bool,
68    registries: Vec<SyncRegistry>,
69}
70
71#[derive(Serialize)]
72struct Extensions {
73    sync: Sync,
74}
75
76#[derive(Serialize)]
77struct Document<'a> {
78    #[serde(rename = "distSpecVersion")]
79    dist_spec_version: &'static str,
80    storage: Storage<'a>,
81    http: Http<'a>,
82    log: Log,
83    extensions: Extensions,
84}
85
86/// Builds a zot config JSON body from the given parameters. `data_dir`
87/// is where zot stores cached blobs/manifests; `port` is the listen
88/// port; `upstreams` is the list of registry hostnames to proxy (as
89/// they appear in image references -- e.g. `"docker.io"`, not the
90/// registry's host URL).
91///
92/// Returns [`ConfigError::NoUpstreams`] when `upstreams` is empty.
93pub fn render_config(
94    data_dir: &str,
95    port: &str,
96    upstreams: &[String],
97) -> Result<Vec<u8>, ConfigError> {
98    if upstreams.is_empty() {
99        return Err(ConfigError::NoUpstreams);
100    }
101
102    let registries = upstreams
103        .iter()
104        .map(|host| SyncRegistry {
105            urls: vec![upstream_url(host)],
106            on_demand: true,
107            tls_verify: true,
108            content: vec![ContentEntry { prefix: "**" }],
109        })
110        .collect();
111
112    let doc = Document {
113        dist_spec_version: "1.1.1",
114        storage: Storage {
115            root_directory: data_dir,
116            gc: true,
117            dedupe: true,
118        },
119        http: Http {
120            address: "0.0.0.0",
121            port,
122        },
123        log: Log { level: "info" },
124        extensions: Extensions {
125            sync: Sync {
126                enable: true,
127                registries,
128            },
129        },
130    };
131
132    Ok(serde_json::to_vec_pretty(&doc)?)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use serde_json::Value;
139
140    // Ported from internal/registrycache/config_test.go:10 (TestUpstreamURL)
141    #[test]
142    fn test_upstream_url() {
143        let cases = [
144            ("docker.io", "https://registry-1.docker.io"),
145            ("quay.io", "https://quay.io"),
146            ("ghcr.io", "https://ghcr.io"),
147            ("registry.gitlab.com", "https://registry.gitlab.com"),
148            (
149                "private.example.com:5000",
150                "https://private.example.com:5000",
151            ),
152        ];
153
154        for (host, want) in cases {
155            assert_eq!(upstream_url(host), want, "host={host}");
156        }
157    }
158
159    // Ported from internal/registrycache/config_test.go:32 (TestRenderConfig)
160    #[test]
161    fn test_render_config() {
162        let data_dir = "/tmp/registry-data";
163        let upstreams = vec!["docker.io".to_string(), "quay.io".to_string()];
164        let port = "5555";
165
166        let body = render_config(data_dir, port, &upstreams).unwrap();
167        let got: Value = serde_json::from_slice(&body).unwrap();
168
169        assert_eq!(got["distSpecVersion"], "1.1.1");
170        assert_eq!(got["storage"]["rootDirectory"], data_dir);
171        assert_eq!(got["storage"]["gc"], true);
172        assert_eq!(got["storage"]["dedupe"], true);
173        assert_eq!(got["http"]["address"], "0.0.0.0");
174        assert_eq!(got["http"]["port"], port);
175        assert_eq!(got["log"]["level"], "info");
176        assert_eq!(got["extensions"]["sync"]["enable"], true);
177
178        let registries = got["extensions"]["sync"]["registries"].as_array().unwrap();
179        assert_eq!(registries.len(), 2);
180        assert_eq!(
181            registries[0]["urls"],
182            Value::Array(vec![Value::String("https://registry-1.docker.io".into())])
183        );
184        assert_eq!(
185            registries[1]["urls"],
186            Value::Array(vec![Value::String("https://quay.io".into())])
187        );
188        assert_eq!(registries[0]["onDemand"], true);
189        assert_eq!(registries[0]["tlsVerify"], true);
190        let content = registries[0]["content"].as_array().unwrap();
191        assert_eq!(content.len(), 1);
192        assert_eq!(content[0]["prefix"], "**");
193    }
194
195    // Ported from internal/registrycache/config_test.go:90 (TestRenderConfig_EmptyUpstreams)
196    #[test]
197    fn test_render_config_empty_upstreams() {
198        let err = render_config("/tmp", "5555", &[]).unwrap_err();
199        assert!(matches!(err, ConfigError::NoUpstreams));
200    }
201}