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
77fn volume(source: String, target: &str, read_only: bool) -> Mount {
78 Mount {
79 target: Some(target.to_owned()),
80 source: Some(source),
81 typ: Some(MountType::VOLUME),
82 read_only: Some(read_only),
83 ..Default::default()
84 }
85}
86
87pub fn runner_mounts(names: &Names, profile: &str, slot: usize) -> Vec<Mount> {
89 vec![
90 volume(names.base(profile), "/base", true),
91 volume(names.slot(profile, slot), "/state", false),
92 volume(names.pnpm(), "/shared/pnpm", false),
93 volume(names.maven(), "/shared/maven", false),
94 ]
95}
96
97pub fn mirror_body(names: &Names, mirror: &config::Mirror, slice: &str) -> ContainerCreateBody {
100 let memory = i64::try_from(mirror.memory.0).unwrap_or(i64::MAX);
101 ContainerCreateBody {
102 image: Some(MIRROR_IMAGE.to_owned()),
103 env: Some(vec![
104 "REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io".to_owned(),
105 format!("REGISTRY_PROXY_TTL={}s", mirror.ttl.as_secs()),
106 "REGISTRY_STORAGE_DELETE_ENABLED=true".to_owned(),
108 ]),
109 labels: Some(HashMap::from([(
110 "runnerd.mirror".to_owned(),
111 String::new(),
112 )])),
113 host_config: Some(HostConfig {
114 cgroup_parent: Some(slice.to_owned()),
115 memory: Some(memory),
116 memory_swap: Some(memory),
117 nano_cpus: Some(i64::from(mirror.cpus) * 1_000_000_000),
118 oom_score_adj: Some(MIRROR_OOM_SCORE_ADJ),
121 mounts: Some(vec![volume(names.mirror(), "/var/lib/registry", false)]),
122 network_mode: Some(names.network()),
123 ..Default::default()
124 }),
125 networking_config: Some(NetworkingConfig {
126 endpoints_config: Some(HashMap::from([(
127 names.network(),
128 EndpointSettings {
129 aliases: Some(vec![MIRROR_ALIAS.to_owned()]),
130 ..Default::default()
131 },
132 )])),
133 }),
134 ..Default::default()
135 }
136}
137
138pub async fn prepare(
140 docker: &impl DockerApi,
141 names: &Names,
142 profile: &str,
143 slot: usize,
144) -> Result<(), Error> {
145 docker.ensure_network(&names.network()).await?;
146 for name in [
147 names.base(profile),
148 names.slot(profile, slot),
149 names.pnpm(),
150 names.maven(),
151 ] {
152 docker.ensure_volume(&name).await?;
153 }
154 Ok(())
155}
156
157pub async fn mirror_up(
159 docker: &impl DockerApi,
160 names: &Names,
161 mirror: &config::Mirror,
162 slice: &str,
163) -> Result<(), Error> {
164 match docker.inspect(&names.mirror()).await {
165 Ok(state) if state.running => return Ok(()),
166 Ok(_) => docker.remove(&names.mirror()).await?,
168 Err(error) if is_not_found(&error) => {}
169 Err(error) => return Err(error),
170 }
171 docker.ensure_network(&names.network()).await?;
172 if !docker.image_present(MIRROR_IMAGE).await? {
173 docker.pull(MIRROR_IMAGE).await?;
174 }
175 docker
176 .run(&names.mirror(), mirror_body(names, mirror, slice))
177 .await?;
178 Ok(())
179}
180
181pub async fn mirror_down(docker: &impl DockerApi, names: &Names) -> Result<(), Error> {
183 match docker.remove(&names.mirror()).await {
184 Err(error) if is_not_found(&error) => Ok(()),
185 other => other,
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
191pub struct Trimmed {
192 pub sizes: BTreeMap<String, u64>,
194 pub emptied: Vec<Emptied>,
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct Emptied {
201 pub name: String,
203 pub size: ByteSize,
205}
206
207pub async fn trim_shared(
209 docker: &impl DockerApi,
210 names: &Names,
211 caches: &config::Caches,
212) -> Result<Trimmed, Error> {
213 let sizes = docker.volume_sizes().await?;
214 let mut emptied = Vec::new();
215 for (name, limit) in [
216 (names.pnpm(), caches.pnpm.max_size),
217 (names.maven(), caches.maven.max_size),
218 ] {
219 if let Some(&size) = sizes.get(&name)
220 && size > limit.0
221 {
222 docker.remove_volume(&name).await?;
223 emptied.push(Emptied {
224 name,
225 size: ByteSize(size),
226 });
227 }
228 }
229 Ok(Trimmed { sizes, emptied })
230}
231
232pub fn volumes(names: &Names, config: &config::Config) -> Vec<api::CacheVolume> {
234 let volume = |name: String, kind: api::CacheKind, limit: Option<u64>| api::CacheVolume {
235 name,
236 kind,
237 bytes: 0,
238 limit,
239 };
240 let mut volumes = Vec::new();
241 for (profile, settings) in &config.profiles {
242 volumes.push(volume(
243 names.base(profile),
244 api::CacheKind::Snapshot {
245 profile: profile.clone(),
246 },
247 None,
248 ));
249 for slot in 1..=settings.slots as usize {
250 volumes.push(volume(
251 names.slot(profile, slot),
252 api::CacheKind::Slot {
253 profile: profile.clone(),
254 slot,
255 },
256 None,
257 ));
258 }
259 }
260 volumes.push(volume(
261 names.pnpm(),
262 api::CacheKind::Pnpm,
263 Some(config.caches.pnpm.max_size.0),
264 ));
265 volumes.push(volume(
266 names.maven(),
267 api::CacheKind::Maven,
268 Some(config.caches.maven.max_size.0),
269 ));
270 if config.caches.mirror.enabled {
271 volumes.push(volume(names.mirror(), api::CacheKind::Mirror, None));
272 }
273 volumes.sort_by(|a, b| a.name.cmp(&b.name));
274 volumes
275}
276
277#[derive(Debug)]
279pub struct Reseed<'profile> {
280 pub names: &'profile Names,
282 pub profile: &'profile str,
284 pub image: &'profile str,
286 pub slots: usize,
288 pub slice: &'profile str,
290 pub cpus: u32,
292 pub memory: ByteSize,
294 pub min_changes: ByteSize,
296 pub isolation: &'profile dyn Isolation,
298}
299
300pub async fn reseed(docker: &impl DockerApi, reseed: &Reseed<'_>) -> Result<String, Error> {
304 let names = reseed.names;
305 let mut mounts = vec![volume(names.base(reseed.profile), "/base", false)];
306 mounts.extend((1..=reseed.slots).map(|slot| {
307 volume(
308 names.slot(reseed.profile, slot),
309 &format!("/workers/{slot}"),
310 false,
311 )
312 }));
313 for mount in &mounts {
314 if let Some(source) = &mount.source {
315 docker.ensure_volume(source).await?;
316 }
317 }
318 let name = names.seed(reseed.profile);
319 let spec = RunnerSpec {
320 name: name.clone(),
321 image: reseed.image.to_owned(),
322 profile: reseed.profile.to_owned(),
323 slice: reseed.slice.to_owned(),
324 cpus: reseed.cpus,
325 memory: reseed.memory,
326 env: Vec::new(),
327 mounts,
328 network: Some("none".to_owned()),
329 labels: BTreeMap::from([("runnerd.seed".to_owned(), String::new())]),
330 entrypoint: Some(vec![
331 "/usr/local/sbin/runnerd-seed".to_owned(),
332 reseed.min_changes.0.to_string(),
333 ]),
334 jit_config: None,
335 };
336 match docker.remove(&name).await {
338 Err(error) if !is_not_found(&error) => return Err(error),
339 _ => {}
340 }
341 let id = docker.start(&spec, reseed.isolation).await?;
342 finish_reseed(docker, &id).await
343}
344
345pub async fn finish_reseed(docker: &impl DockerApi, container: &str) -> Result<String, Error> {
348 let result = async {
349 let code = docker.wait(container).await?;
350 let output = docker.logs(container).await?;
351 if code == 0 {
352 Ok(output)
353 } else {
354 Err(Error::DockerContainerWaitError {
355 error: format!("seeding exited {code}: {}", output.trim()),
356 code,
357 })
358 }
359 }
360 .await;
361 let removed = docker.remove(container).await;
362 let output = result?;
363 removed?;
364 Ok(output)
365}