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
24const MIRROR_OOM_SCORE_ADJ: i64 = -500;
26
27#[derive(Debug, Clone)]
29pub struct Names {
30 prefix: String,
31}
32
33impl Names {
34 pub fn new(prefix: &str) -> Names {
36 Names {
37 prefix: prefix.to_owned(),
38 }
39 }
40
41 pub fn network(&self) -> String {
43 self.prefix.clone()
44 }
45
46 pub fn base(&self, profile: &str) -> String {
48 format!("{}-{profile}-base", self.prefix)
49 }
50
51 pub fn slot(&self, profile: &str, slot: usize) -> String {
53 format!("{}-{profile}-slot-{slot}", self.prefix)
54 }
55
56 pub fn pnpm(&self) -> String {
58 format!("{}-pnpm", self.prefix)
59 }
60
61 pub fn maven(&self) -> String {
63 format!("{}-maven", self.prefix)
64 }
65
66 pub fn seed(&self, profile: &str) -> String {
68 format!("{}-{profile}-seed", self.prefix)
69 }
70
71 pub fn mirror(&self) -> String {
73 format!("{}-mirror", self.prefix)
74 }
75
76 pub fn build_cache(&self) -> String {
79 format!("{}-build-cache", self.prefix)
80 }
81}
82
83fn volume(source: String, target: &str, read_only: bool) -> Mount {
84 Mount {
85 target: Some(target.to_owned()),
86 source: Some(source),
87 typ: Some(MountType::VOLUME),
88 read_only: Some(read_only),
89 ..Default::default()
90 }
91}
92
93pub fn runner_mounts(names: &Names, profile: &str, slot: usize) -> Vec<Mount> {
95 vec![
96 volume(names.base(profile), "/base", true),
97 volume(names.slot(profile, slot), "/state", false),
98 volume(names.pnpm(), "/shared/pnpm", false),
99 volume(names.maven(), "/shared/maven", false),
100 ]
101}
102
103pub fn mirror_body(names: &Names, mirror: &config::Mirror, slice: &str) -> ContainerCreateBody {
106 let memory = i64::try_from(mirror.memory.0).unwrap_or(i64::MAX);
107 ContainerCreateBody {
108 image: Some(MIRROR_IMAGE.to_owned()),
109 env: Some(vec![
110 "REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io".to_owned(),
111 format!("REGISTRY_PROXY_TTL={}s", mirror.ttl.as_secs()),
112 "REGISTRY_STORAGE_DELETE_ENABLED=true".to_owned(),
114 ]),
115 labels: Some(HashMap::from([(
116 "runnerd.mirror".to_owned(),
117 String::new(),
118 )])),
119 host_config: Some(HostConfig {
120 cgroup_parent: Some(slice.to_owned()),
121 memory: Some(memory),
122 memory_swap: Some(memory),
123 nano_cpus: Some(i64::from(mirror.cpus) * 1_000_000_000),
124 oom_score_adj: Some(MIRROR_OOM_SCORE_ADJ),
127 mounts: Some(vec![volume(names.mirror(), "/var/lib/registry", false)]),
128 network_mode: Some(names.network()),
129 ..Default::default()
130 }),
131 networking_config: Some(NetworkingConfig {
132 endpoints_config: Some(HashMap::from([(
133 names.network(),
134 EndpointSettings {
135 aliases: Some(vec![MIRROR_ALIAS.to_owned()]),
136 ..Default::default()
137 },
138 )])),
139 }),
140 ..Default::default()
141 }
142}
143
144pub async fn prepare(
146 docker: &impl DockerApi,
147 names: &Names,
148 profile: &str,
149 slot: usize,
150) -> Result<(), Error> {
151 docker.ensure_network(&names.network()).await?;
152 for name in [
153 names.base(profile),
154 names.slot(profile, slot),
155 names.pnpm(),
156 names.maven(),
157 ] {
158 docker.ensure_volume(&name).await?;
159 }
160 Ok(())
161}
162
163pub async fn mirror_up(
165 docker: &impl DockerApi,
166 names: &Names,
167 mirror: &config::Mirror,
168 slice: &str,
169) -> Result<(), Error> {
170 match docker.inspect(&names.mirror()).await {
171 Ok(state) if state.running => return Ok(()),
172 Ok(_) => docker.remove(&names.mirror()).await?,
174 Err(error) if is_not_found(&error) => {}
175 Err(error) => return Err(error),
176 }
177 docker.ensure_network(&names.network()).await?;
178 if !docker.image_present(MIRROR_IMAGE).await? {
179 docker.pull(MIRROR_IMAGE).await?;
180 }
181 docker
182 .run(&names.mirror(), mirror_body(names, mirror, slice))
183 .await?;
184 Ok(())
185}
186
187pub async fn mirror_down(docker: &impl DockerApi, names: &Names) -> Result<(), Error> {
189 match docker.remove(&names.mirror()).await {
190 Err(error) if is_not_found(&error) => Ok(()),
191 other => other,
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum Mirror {
198 Up,
200 Down,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
206pub struct Trimmed {
207 pub sizes: BTreeMap<String, u64>,
209 pub emptied: Vec<Emptied>,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub struct Emptied {
216 pub name: String,
218 pub size: ByteSize,
220}
221
222pub async fn trim_shared(
225 docker: &impl DockerApi,
226 names: &Names,
227 caches: &config::Caches,
228 mirror: Mirror,
229) -> Result<Trimmed, Error> {
230 let sizes = docker.volume_sizes().await?;
231 let mut emptied = Vec::new();
232 let mut limits = vec![
233 (names.pnpm(), caches.pnpm.max_size),
234 (names.maven(), caches.maven.max_size),
235 ];
236 if mirror == Mirror::Down {
237 limits.push((names.mirror(), caches.mirror.max_size));
238 }
239 for (name, limit) in limits {
240 if let Some(&size) = sizes.get(&name)
241 && size > limit.0
242 {
243 docker.remove_volume(&name).await?;
244 emptied.push(Emptied {
245 name,
246 size: ByteSize(size),
247 });
248 }
249 }
250 Ok(Trimmed { sizes, emptied })
251}
252
253pub fn volumes(names: &Names, config: &config::Config) -> Vec<api::CacheVolume> {
255 let volume = |name: String, kind: api::CacheKind, limit: Option<u64>| api::CacheVolume {
256 name,
257 kind,
258 bytes: 0,
259 limit,
260 };
261 let mut volumes = Vec::new();
262 for (profile, settings) in &config.profiles {
263 volumes.push(volume(
264 names.base(profile),
265 api::CacheKind::Snapshot {
266 profile: profile.clone(),
267 },
268 Some(config.caches.snapshot_max.0),
269 ));
270 for slot in 1..=settings.slots as usize {
271 volumes.push(volume(
272 names.slot(profile, slot),
273 api::CacheKind::Slot {
274 profile: profile.clone(),
275 slot,
276 },
277 None,
278 ));
279 }
280 }
281 volumes.push(volume(
282 names.pnpm(),
283 api::CacheKind::Pnpm,
284 Some(config.caches.pnpm.max_size.0),
285 ));
286 volumes.push(volume(
287 names.maven(),
288 api::CacheKind::Maven,
289 Some(config.caches.maven.max_size.0),
290 ));
291 if config.caches.mirror.enabled {
292 volumes.push(volume(
293 names.mirror(),
294 api::CacheKind::Mirror,
295 Some(config.caches.mirror.max_size.0),
296 ));
297 }
298 if config.caches.build.enabled {
299 volumes.push(volume(
300 names.build_cache(),
301 api::CacheKind::Build,
302 Some(config.caches.build.max_size.0),
303 ));
304 }
305 volumes.sort_by(|a, b| a.name.cmp(&b.name));
306 volumes
307}
308
309#[derive(Debug)]
311pub struct Reseed<'profile> {
312 pub names: &'profile Names,
314 pub profile: &'profile str,
316 pub image: &'profile str,
318 pub slots: usize,
320 pub slice: &'profile str,
322 pub cpus: u32,
324 pub memory: ByteSize,
326 pub min_changes: ByteSize,
328 pub max_size: ByteSize,
330 pub isolation: &'profile dyn Isolation,
332}
333
334pub async fn reseed(docker: &impl DockerApi, reseed: &Reseed<'_>) -> Result<String, Error> {
339 let names = reseed.names;
340 let mut mounts = vec![volume(names.base(reseed.profile), "/base", false)];
341 mounts.extend((1..=reseed.slots).map(|slot| {
342 volume(
343 names.slot(reseed.profile, slot),
344 &format!("/workers/{slot}"),
345 false,
346 )
347 }));
348 for mount in &mounts {
349 if let Some(source) = &mount.source {
350 docker.ensure_volume(source).await?;
351 }
352 }
353 let name = names.seed(reseed.profile);
354 let spec = RunnerSpec {
355 name: name.clone(),
356 image: reseed.image.to_owned(),
357 profile: reseed.profile.to_owned(),
358 slice: reseed.slice.to_owned(),
359 cpus: reseed.cpus,
360 memory: reseed.memory,
361 env: Vec::new(),
362 mounts,
363 network: Some("none".to_owned()),
364 labels: BTreeMap::from([("runnerd.seed".to_owned(), String::new())]),
365 entrypoint: Some(vec![
366 "/usr/local/sbin/runnerd-seed".to_owned(),
367 reseed.min_changes.0.to_string(),
368 reseed.max_size.0.to_string(),
369 ]),
370 jit_config: None,
371 };
372 match docker.remove(&name).await {
374 Err(error) if !is_not_found(&error) => return Err(error),
375 _ => {}
376 }
377 let id = docker.start(&spec, reseed.isolation).await?;
378 finish_reseed(docker, &id).await
379}
380
381pub async fn finish_reseed(docker: &impl DockerApi, container: &str) -> Result<String, Error> {
384 let result = async {
385 let code = docker.wait(container).await?;
386 let output = docker.logs(container).await?;
387 if code == 0 {
388 Ok(output)
389 } else {
390 Err(Error::DockerContainerWaitError {
391 error: format!("seeding exited {code}: {}", output.trim()),
392 code,
393 })
394 }
395 }
396 .await;
397 let removed = docker.remove(container).await;
398 let output = result?;
399 removed?;
400 Ok(output)
401}