use std::collections::{BTreeMap, HashMap};
use bollard::models::{
ContainerCreateBody, EndpointSettings, HostConfig, Mount, MountType, NetworkingConfig,
};
use crate::api;
use crate::config::{self, ByteSize};
use crate::docker::{DockerApi, Error, RunnerSpec, is_not_found};
use crate::isolation::Isolation;
pub const MIRROR_IMAGE: &str =
"registry:3.1.2@sha256:01cd312e67026f06f5d89c3e133a4a9adc7cb9eeb9b1194e0b2954dfd770efd2";
const MIRROR_ALIAS: &str = "mirror";
#[derive(Debug, Clone)]
pub struct Names {
prefix: String,
}
impl Names {
pub fn new(prefix: &str) -> Names {
Names {
prefix: prefix.to_owned(),
}
}
pub fn network(&self) -> String {
self.prefix.clone()
}
pub fn base(&self, profile: &str) -> String {
format!("{}-{profile}-base", self.prefix)
}
pub fn slot(&self, profile: &str, slot: usize) -> String {
format!("{}-{profile}-slot-{slot}", self.prefix)
}
pub fn pnpm(&self) -> String {
format!("{}-pnpm", self.prefix)
}
pub fn maven(&self) -> String {
format!("{}-maven", self.prefix)
}
pub fn seed(&self, profile: &str) -> String {
format!("{}-{profile}-seed", self.prefix)
}
pub fn mirror(&self) -> String {
format!("{}-mirror", self.prefix)
}
}
fn volume(source: String, target: &str, read_only: bool) -> Mount {
Mount {
target: Some(target.to_owned()),
source: Some(source),
typ: Some(MountType::VOLUME),
read_only: Some(read_only),
..Default::default()
}
}
pub fn runner_mounts(names: &Names, profile: &str, slot: usize) -> Vec<Mount> {
vec![
volume(names.base(profile), "/base", true),
volume(names.slot(profile, slot), "/state", false),
volume(names.pnpm(), "/shared/pnpm", false),
volume(names.maven(), "/shared/maven", false),
]
}
pub fn mirror_body(names: &Names, mirror: &config::Mirror, slice: &str) -> ContainerCreateBody {
let memory = i64::try_from(mirror.memory.0).unwrap_or(i64::MAX);
ContainerCreateBody {
image: Some(MIRROR_IMAGE.to_owned()),
env: Some(vec![
"REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io".to_owned(),
format!("REGISTRY_PROXY_TTL={}s", mirror.ttl.as_secs()),
"REGISTRY_STORAGE_DELETE_ENABLED=true".to_owned(),
]),
labels: Some(HashMap::from([(
"runnerd.mirror".to_owned(),
String::new(),
)])),
host_config: Some(HostConfig {
cgroup_parent: Some(slice.to_owned()),
memory: Some(memory),
memory_swap: Some(memory),
nano_cpus: Some(i64::from(mirror.cpus) * 1_000_000_000),
mounts: Some(vec![volume(names.mirror(), "/var/lib/registry", false)]),
network_mode: Some(names.network()),
..Default::default()
}),
networking_config: Some(NetworkingConfig {
endpoints_config: Some(HashMap::from([(
names.network(),
EndpointSettings {
aliases: Some(vec![MIRROR_ALIAS.to_owned()]),
..Default::default()
},
)])),
}),
..Default::default()
}
}
pub async fn prepare(
docker: &impl DockerApi,
names: &Names,
profile: &str,
slot: usize,
) -> Result<(), Error> {
docker.ensure_network(&names.network()).await?;
for name in [
names.base(profile),
names.slot(profile, slot),
names.pnpm(),
names.maven(),
] {
docker.ensure_volume(&name).await?;
}
Ok(())
}
pub async fn mirror_up(
docker: &impl DockerApi,
names: &Names,
mirror: &config::Mirror,
slice: &str,
) -> Result<(), Error> {
match docker.inspect(&names.mirror()).await {
Ok(state) if state.running => return Ok(()),
Ok(_) => docker.remove(&names.mirror()).await?,
Err(error) if is_not_found(&error) => {}
Err(error) => return Err(error),
}
docker.ensure_network(&names.network()).await?;
if !docker.image_present(MIRROR_IMAGE).await? {
docker.pull(MIRROR_IMAGE).await?;
}
docker
.run(&names.mirror(), mirror_body(names, mirror, slice))
.await?;
Ok(())
}
pub async fn mirror_down(docker: &impl DockerApi, names: &Names) -> Result<(), Error> {
match docker.remove(&names.mirror()).await {
Err(error) if is_not_found(&error) => Ok(()),
other => other,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Trimmed {
pub sizes: BTreeMap<String, u64>,
pub emptied: Vec<Emptied>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Emptied {
pub name: String,
pub size: ByteSize,
}
pub async fn trim_shared(
docker: &impl DockerApi,
names: &Names,
caches: &config::Caches,
) -> Result<Trimmed, Error> {
let sizes = docker.volume_sizes().await?;
let mut emptied = Vec::new();
for (name, limit) in [
(names.pnpm(), caches.pnpm.max_size),
(names.maven(), caches.maven.max_size),
] {
if let Some(&size) = sizes.get(&name)
&& size > limit.0
{
docker.remove_volume(&name).await?;
emptied.push(Emptied {
name,
size: ByteSize(size),
});
}
}
Ok(Trimmed { sizes, emptied })
}
pub fn volumes(names: &Names, config: &config::Config) -> Vec<api::CacheVolume> {
let volume = |name: String, kind: api::CacheKind, limit: Option<u64>| api::CacheVolume {
name,
kind,
bytes: 0,
limit,
};
let mut volumes = Vec::new();
for (profile, settings) in &config.profiles {
volumes.push(volume(
names.base(profile),
api::CacheKind::Snapshot {
profile: profile.clone(),
},
None,
));
for slot in 1..=settings.slots as usize {
volumes.push(volume(
names.slot(profile, slot),
api::CacheKind::Slot {
profile: profile.clone(),
slot,
},
None,
));
}
}
volumes.push(volume(
names.pnpm(),
api::CacheKind::Pnpm,
Some(config.caches.pnpm.max_size.0),
));
volumes.push(volume(
names.maven(),
api::CacheKind::Maven,
Some(config.caches.maven.max_size.0),
));
if config.caches.mirror.enabled {
volumes.push(volume(names.mirror(), api::CacheKind::Mirror, None));
}
volumes.sort_by(|a, b| a.name.cmp(&b.name));
volumes
}
#[derive(Debug)]
pub struct Reseed<'profile> {
pub names: &'profile Names,
pub profile: &'profile str,
pub image: &'profile str,
pub slots: usize,
pub slice: &'profile str,
pub cpus: u32,
pub memory: ByteSize,
pub min_changes: ByteSize,
pub isolation: &'profile dyn Isolation,
}
pub async fn reseed(docker: &impl DockerApi, reseed: &Reseed<'_>) -> Result<String, Error> {
let names = reseed.names;
let mut mounts = vec![volume(names.base(reseed.profile), "/base", false)];
mounts.extend((1..=reseed.slots).map(|slot| {
volume(
names.slot(reseed.profile, slot),
&format!("/workers/{slot}"),
false,
)
}));
for mount in &mounts {
if let Some(source) = &mount.source {
docker.ensure_volume(source).await?;
}
}
let name = names.seed(reseed.profile);
let spec = RunnerSpec {
name: name.clone(),
image: reseed.image.to_owned(),
profile: reseed.profile.to_owned(),
slice: reseed.slice.to_owned(),
cpus: reseed.cpus,
memory: reseed.memory,
env: Vec::new(),
mounts,
network: Some("none".to_owned()),
labels: BTreeMap::from([("runnerd.seed".to_owned(), String::new())]),
entrypoint: Some(vec![
"/usr/local/sbin/runnerd-seed".to_owned(),
reseed.min_changes.0.to_string(),
]),
jit_config: None,
};
match docker.remove(&name).await {
Err(error) if !is_not_found(&error) => return Err(error),
_ => {}
}
let id = docker.start(&spec, reseed.isolation).await?;
finish_reseed(docker, &id).await
}
pub async fn finish_reseed(docker: &impl DockerApi, container: &str) -> Result<String, Error> {
let result = async {
let code = docker.wait(container).await?;
let output = docker.logs(container).await?;
if code == 0 {
Ok(output)
} else {
Err(Error::DockerContainerWaitError {
error: format!("seeding exited {code}: {}", output.trim()),
code,
})
}
}
.await;
let removed = docker.remove(container).await;
let output = result?;
removed?;
Ok(output)
}