1use std::collections::{BTreeMap, HashMap};
7
8use bollard::models::{
9 ContainerCreateBody, EndpointSettings, HostConfig, Mount, MountType, NetworkingConfig,
10};
11
12use crate::api;
13use crate::config::{self, ByteSize};
14use crate::docker::{DockerApi, Error, RunnerSpec, is_not_found};
15use crate::isolation::Isolation;
16
17pub const MIRROR_IMAGE: &str =
19 "registry:3.1.2@sha256:01cd312e67026f06f5d89c3e133a4a9adc7cb9eeb9b1194e0b2954dfd770efd2";
20
21const MIRROR_ALIAS: &str = "mirror";
23
24#[derive(Debug, Clone)]
26pub struct Names {
27 prefix: String,
28}
29
30impl Names {
31 pub fn new(prefix: &str) -> Names {
33 Names {
34 prefix: prefix.to_owned(),
35 }
36 }
37
38 pub fn network(&self) -> String {
40 self.prefix.clone()
41 }
42
43 pub fn base(&self, profile: &str) -> String {
45 format!("{}-{profile}-base", self.prefix)
46 }
47
48 pub fn slot(&self, profile: &str, slot: usize) -> String {
50 format!("{}-{profile}-slot-{slot}", self.prefix)
51 }
52
53 pub fn pnpm(&self) -> String {
55 format!("{}-pnpm", self.prefix)
56 }
57
58 pub fn maven(&self) -> String {
60 format!("{}-maven", self.prefix)
61 }
62
63 pub fn seed(&self, profile: &str) -> String {
65 format!("{}-{profile}-seed", self.prefix)
66 }
67
68 pub fn mirror(&self) -> String {
70 format!("{}-mirror", self.prefix)
71 }
72}
73
74fn volume(source: String, target: &str, read_only: bool) -> Mount {
75 Mount {
76 target: Some(target.to_owned()),
77 source: Some(source),
78 typ: Some(MountType::VOLUME),
79 read_only: Some(read_only),
80 ..Default::default()
81 }
82}
83
84pub fn runner_mounts(names: &Names, profile: &str, slot: usize) -> Vec<Mount> {
86 vec![
87 volume(names.base(profile), "/base", true),
88 volume(names.slot(profile, slot), "/state", false),
89 volume(names.pnpm(), "/shared/pnpm", false),
90 volume(names.maven(), "/shared/maven", false),
91 ]
92}
93
94pub fn mirror_body(names: &Names, mirror: &config::Mirror, slice: &str) -> ContainerCreateBody {
97 let memory = i64::try_from(mirror.memory.0).unwrap_or(i64::MAX);
98 ContainerCreateBody {
99 image: Some(MIRROR_IMAGE.to_owned()),
100 env: Some(vec![
101 "REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io".to_owned(),
102 format!("REGISTRY_PROXY_TTL={}s", mirror.ttl.as_secs()),
103 "REGISTRY_STORAGE_DELETE_ENABLED=true".to_owned(),
105 ]),
106 labels: Some(HashMap::from([(
107 "runnerd.mirror".to_owned(),
108 String::new(),
109 )])),
110 host_config: Some(HostConfig {
111 cgroup_parent: Some(slice.to_owned()),
112 memory: Some(memory),
113 memory_swap: Some(memory),
114 nano_cpus: Some(i64::from(mirror.cpus) * 1_000_000_000),
115 mounts: Some(vec![volume(names.mirror(), "/var/lib/registry", false)]),
116 network_mode: Some(names.network()),
117 ..Default::default()
118 }),
119 networking_config: Some(NetworkingConfig {
120 endpoints_config: Some(HashMap::from([(
121 names.network(),
122 EndpointSettings {
123 aliases: Some(vec![MIRROR_ALIAS.to_owned()]),
124 ..Default::default()
125 },
126 )])),
127 }),
128 ..Default::default()
129 }
130}
131
132pub async fn prepare(
134 docker: &impl DockerApi,
135 names: &Names,
136 profile: &str,
137 slot: usize,
138) -> Result<(), Error> {
139 docker.ensure_network(&names.network()).await?;
140 for name in [
141 names.base(profile),
142 names.slot(profile, slot),
143 names.pnpm(),
144 names.maven(),
145 ] {
146 docker.ensure_volume(&name).await?;
147 }
148 Ok(())
149}
150
151pub async fn mirror_up(
153 docker: &impl DockerApi,
154 names: &Names,
155 mirror: &config::Mirror,
156 slice: &str,
157) -> Result<(), Error> {
158 match docker.inspect(&names.mirror()).await {
159 Ok(state) if state.running => return Ok(()),
160 Ok(_) => docker.remove(&names.mirror()).await?,
162 Err(error) if is_not_found(&error) => {}
163 Err(error) => return Err(error),
164 }
165 docker.ensure_network(&names.network()).await?;
166 docker.ensure_image(MIRROR_IMAGE).await?;
167 docker
168 .run(&names.mirror(), mirror_body(names, mirror, slice))
169 .await?;
170 Ok(())
171}
172
173pub async fn mirror_down(docker: &impl DockerApi, names: &Names) -> Result<(), Error> {
175 match docker.remove(&names.mirror()).await {
176 Err(error) if is_not_found(&error) => Ok(()),
177 other => other,
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub struct Trimmed {
184 pub sizes: BTreeMap<String, u64>,
186 pub emptied: Vec<Emptied>,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct Emptied {
193 pub name: String,
195 pub size: ByteSize,
197}
198
199pub async fn trim_shared(
201 docker: &impl DockerApi,
202 names: &Names,
203 caches: &config::Caches,
204) -> Result<Trimmed, Error> {
205 let sizes = docker.volume_sizes().await?;
206 let mut emptied = Vec::new();
207 for (name, limit) in [
208 (names.pnpm(), caches.pnpm.max_size),
209 (names.maven(), caches.maven.max_size),
210 ] {
211 if let Some(&size) = sizes.get(&name)
212 && size > limit.0
213 {
214 docker.remove_volume(&name).await?;
215 emptied.push(Emptied {
216 name,
217 size: ByteSize(size),
218 });
219 }
220 }
221 Ok(Trimmed { sizes, emptied })
222}
223
224pub fn volumes(names: &Names, config: &config::Config) -> Vec<api::CacheVolume> {
226 let volume = |name: String, kind: api::CacheKind, limit: Option<u64>| api::CacheVolume {
227 name,
228 kind,
229 bytes: 0,
230 limit,
231 };
232 let mut volumes = Vec::new();
233 for (profile, settings) in &config.profiles {
234 volumes.push(volume(
235 names.base(profile),
236 api::CacheKind::Snapshot {
237 profile: profile.clone(),
238 },
239 None,
240 ));
241 for slot in 1..=settings.slots as usize {
242 volumes.push(volume(
243 names.slot(profile, slot),
244 api::CacheKind::Slot {
245 profile: profile.clone(),
246 slot,
247 },
248 None,
249 ));
250 }
251 }
252 volumes.push(volume(
253 names.pnpm(),
254 api::CacheKind::Pnpm,
255 Some(config.caches.pnpm.max_size.0),
256 ));
257 volumes.push(volume(
258 names.maven(),
259 api::CacheKind::Maven,
260 Some(config.caches.maven.max_size.0),
261 ));
262 if config.caches.mirror.enabled {
263 volumes.push(volume(names.mirror(), api::CacheKind::Mirror, None));
264 }
265 volumes.sort_by(|a, b| a.name.cmp(&b.name));
266 volumes
267}
268
269#[derive(Debug)]
271pub struct Reseed<'profile> {
272 pub names: &'profile Names,
274 pub profile: &'profile str,
276 pub image: &'profile str,
278 pub slots: usize,
280 pub slice: &'profile str,
282 pub cpus: u32,
284 pub memory: ByteSize,
286 pub min_changes: ByteSize,
288 pub isolation: &'profile dyn Isolation,
290}
291
292pub async fn reseed(docker: &impl DockerApi, reseed: &Reseed<'_>) -> Result<String, Error> {
296 let names = reseed.names;
297 let mut mounts = vec![volume(names.base(reseed.profile), "/base", false)];
298 mounts.extend((1..=reseed.slots).map(|slot| {
299 volume(
300 names.slot(reseed.profile, slot),
301 &format!("/workers/{slot}"),
302 false,
303 )
304 }));
305 for mount in &mounts {
306 if let Some(source) = &mount.source {
307 docker.ensure_volume(source).await?;
308 }
309 }
310 let name = names.seed(reseed.profile);
311 let spec = RunnerSpec {
312 name: name.clone(),
313 image: reseed.image.to_owned(),
314 profile: reseed.profile.to_owned(),
315 slice: reseed.slice.to_owned(),
316 cpus: reseed.cpus,
317 memory: reseed.memory,
318 env: Vec::new(),
319 mounts,
320 network: Some("none".to_owned()),
321 labels: BTreeMap::from([("runnerd.seed".to_owned(), String::new())]),
322 entrypoint: Some(vec![
323 "/usr/local/sbin/runnerd-seed".to_owned(),
324 reseed.min_changes.0.to_string(),
325 ]),
326 jit_config: None,
327 };
328 match docker.remove(&name).await {
330 Err(error) if !is_not_found(&error) => return Err(error),
331 _ => {}
332 }
333 let id = docker.start(&spec, reseed.isolation).await?;
334 finish_reseed(docker, &id).await
335}
336
337pub async fn finish_reseed(docker: &impl DockerApi, container: &str) -> Result<String, Error> {
340 let result = async {
341 let code = docker.wait(container).await?;
342 let output = docker.logs(container).await?;
343 if code == 0 {
344 Ok(output)
345 } else {
346 Err(Error::DockerContainerWaitError {
347 error: format!("seeding exited {code}: {}", output.trim()),
348 code,
349 })
350 }
351 }
352 .await;
353 let removed = docker.remove(container).await;
354 let output = result?;
355 removed?;
356 Ok(output)
357}