xianyun_deploy/registrycache/
config.rs1use serde::Serialize;
6use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum ConfigError {
11 #[error("no upstreams configured")]
14 NoUpstreams,
15 #[error("marshalling zot config: {0}")]
17 Serialize(#[from] serde_json::Error),
18}
19
20fn 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
86pub 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 #[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 #[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 #[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}