use super::GIB;
use super::MIB;
use super::strings;
use std::collections::BTreeMap;
use std::path::PathBuf;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VmResources {
pub cpus: u32,
pub memory_in_bytes: u64,
}
impl VmResources {
pub const GUEST_MEMORY_OVERHEAD: u64 = 128 * MIB;
}
impl Default for VmResources {
fn default() -> Self {
Self {
cpus: 4,
memory_in_bytes: GIB,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RuntimeOptions {
Virtioblk(Vec<String>),
Virtiofs(Vec<String>),
Any(Vec<String>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Mount {
pub r#type: String,
pub source: String,
pub destination: String,
pub options: Vec<String>,
pub runtime_options: RuntimeOptions,
}
impl Mount {
pub fn share(source: impl Into<String>, destination: impl Into<String>, options: &[&str]) -> Self {
Self {
r#type: "virtiofs".to_string(),
source: source.into(),
destination: destination.into(),
options: strings(options),
runtime_options: RuntimeOptions::Virtiofs(Vec::new()),
}
}
pub fn block(
format: impl Into<String>,
source: impl Into<String>,
destination: impl Into<String>,
options: &[&str],
) -> Self {
Self {
r#type: format.into(),
source: source.into(),
destination: destination.into(),
options: strings(options),
runtime_options: RuntimeOptions::Virtioblk(Vec::new()),
}
}
pub fn any(
r#type: impl Into<String>,
source: impl Into<String>,
destination: impl Into<String>,
options: &[&str],
) -> Self {
Self {
r#type: r#type.into(),
source: source.into(),
destination: destination.into(),
options: strings(options),
runtime_options: RuntimeOptions::Any(Vec::new()),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Direction {
#[default]
Into,
OutOf,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct UnixSocketConfiguration {
pub source: PathBuf,
pub destination: PathBuf,
pub permissions: Option<u32>,
pub direction: Direction,
}
impl UnixSocketConfiguration {
pub fn new(source: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Self {
Self {
source: source.into(),
destination: destination.into(),
permissions: None,
direction: Direction::Into,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NatInterface {
pub ipv4_address: String,
pub ipv4_gateway: Option<String>,
pub ipv6_address: Option<String>,
pub ipv6_gateway: Option<String>,
pub mac_address: Option<String>,
pub mtu: u32,
}
impl NatInterface {
pub fn new(ipv4_address: impl Into<String>, ipv4_gateway: impl Into<String>) -> Self {
Self {
ipv4_address: ipv4_address.into(),
ipv4_gateway: Some(ipv4_gateway.into()),
ipv6_address: None,
ipv6_gateway: None,
mac_address: None,
mtu: 1500,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Dns {
pub nameservers: Vec<String>,
pub domain: Option<String>,
pub search_domains: Vec<String>,
pub options: Vec<String>,
}
impl Dns {
pub fn default_nameservers() -> Vec<String> {
strings(&["1.1.1.1"])
}
}
impl Default for Dns {
fn default() -> Self {
Self {
nameservers: Self::default_nameservers(),
domain: None,
search_domains: Vec::new(),
options: Vec::new(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HostsEntry {
pub ip_address: String,
pub hostnames: Vec<String>,
pub comment: Option<String>,
}
impl HostsEntry {
pub fn new(ip_address: impl Into<String>, hostnames: &[&str]) -> Self {
Self {
ip_address: ip_address.into(),
hostnames: strings(hostnames),
comment: None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Hosts {
pub entries: Vec<HostsEntry>,
pub comment: Option<String>,
}
impl Default for Hosts {
fn default() -> Self {
Self {
entries: vec![
HostsEntry::new("127.0.0.1", &["localhost"]),
HostsEntry::new("::1", &["localhost", "ip6-localhost", "ip6-loopback"]),
HostsEntry::new("fe00::", &["ip6-localnet"]),
HostsEntry::new("ff00::", &["ip6-mcastprefix"]),
HostsEntry::new("ff02::1", &["ip6-allnodes"]),
HostsEntry::new("ff02::2", &["ip6-allrouters"]),
],
comment: None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BootLog {
pub path: PathBuf,
pub append: bool,
}
impl BootLog {
pub fn file(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
append: true,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub enum SeccompProfile {
#[default]
Unconfined,
Default,
Profile(String),
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct User {
pub uid: u32,
pub gid: u32,
pub umask: Option<u32>,
pub additional_gids: Vec<u32>,
pub username: String,
}
impl User {
pub fn named(username: impl Into<String>) -> Self {
Self {
username: username.into(),
..Self::default()
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct LinuxProcessConfiguration {
pub arguments: Option<Vec<String>>,
pub environment_variables: Vec<String>,
pub working_directory: Option<String>,
pub user: Option<User>,
}
impl LinuxProcessConfiguration {
pub fn new(arguments: &[&str]) -> Self {
Self {
arguments: Some(strings(arguments)),
..Self::default()
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LinuxContainerConfiguration {
pub process: LinuxProcessConfiguration,
pub cpus: u32,
pub memory_in_bytes: u64,
pub hostname: Option<String>,
pub sysctl: BTreeMap<String, String>,
pub interfaces: Vec<NatInterface>,
pub sockets: Vec<UnixSocketConfiguration>,
pub mounts: Vec<Mount>,
pub masked_paths: Vec<String>,
pub readonly_paths: Vec<String>,
pub dns: Option<Dns>,
pub hosts: Option<Hosts>,
pub virtualization: bool,
pub boot_log: Option<BootLog>,
pub oci_runtime_path: Option<String>,
pub seccomp_profile: SeccompProfile,
pub use_init: bool,
}
impl LinuxContainerConfiguration {
pub fn default_mounts() -> Vec<Mount> {
let defaults = ["nosuid", "noexec", "nodev"];
vec![
Mount::any("proc", "proc", "/proc", &[]),
Mount::any("sysfs", "sysfs", "/sys", &defaults),
Mount::any("devtmpfs", "none", "/dev", &["nosuid", "mode=755"]),
Mount::any("mqueue", "mqueue", "/dev/mqueue", &defaults),
Mount::any(
"tmpfs",
"tmpfs",
"/dev/shm",
&["nosuid", "noexec", "nodev", "mode=1777", "size=65536k"],
),
Mount::any("cgroup2", "none", "/sys/fs/cgroup", &defaults),
Mount::any(
"devpts",
"devpts",
"/dev/pts",
&["nosuid", "noexec", "newinstance", "gid=5", "mode=0620", "ptmxmode=0666"],
),
]
}
pub fn default_masked_paths() -> Vec<String> {
strings(&[
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware",
"/sys/devices/virtual/powercap",
])
}
pub fn default_readonly_paths() -> Vec<String> {
strings(&["/proc/bus", "/proc/fs", "/proc/irq", "/proc/sys", "/proc/sysrq-trigger"])
}
}
impl Default for LinuxContainerConfiguration {
fn default() -> Self {
Self {
process: LinuxProcessConfiguration::default(),
cpus: 4,
memory_in_bytes: GIB,
hostname: None,
sysctl: BTreeMap::new(),
interfaces: Vec::new(),
sockets: Vec::new(),
mounts: Self::default_mounts(),
masked_paths: Self::default_masked_paths(),
readonly_paths: Self::default_readonly_paths(),
dns: None,
hosts: None,
virtualization: false,
boot_log: None,
oci_runtime_path: None,
seccomp_profile: SeccompProfile::Unconfined,
use_init: false,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BootSpec {
pub id: String,
pub reference: String,
pub rootfs_size_in_bytes: u64,
pub vm: VmResources,
pub configuration: LinuxContainerConfiguration,
}
impl BootSpec {
pub const DEFAULT_ROOTFS_SIZE_IN_BYTES: u64 = 8 * GIB;
pub fn new(id: impl Into<String>, reference: impl Into<String>) -> Self {
Self {
id: id.into(),
reference: reference.into(),
rootfs_size_in_bytes: Self::DEFAULT_ROOTFS_SIZE_IN_BYTES,
vm: VmResources::default(),
configuration: LinuxContainerConfiguration::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_a_container_as_containerization_does() {
let configuration = LinuxContainerConfiguration::default();
assert_eq!(configuration.cpus, 4);
assert_eq!(configuration.memory_in_bytes, GIB);
assert_eq!(configuration.mounts.len(), 7);
assert!(
configuration
.masked_paths
.contains(&"/proc/kcore".to_string())
);
assert!(
configuration
.readonly_paths
.contains(&"/proc/sys".to_string())
);
assert_eq!(configuration.dns, None, "nothing writes resolv.conf unless asked");
assert_eq!(configuration.seccomp_profile, SeccompProfile::Unconfined);
}
#[test]
fn defaults_the_rest_as_containerization_does() {
assert_eq!(VmResources::default().memory_in_bytes, GIB);
assert_eq!(Dns::default().nameservers, ["1.1.1.1"]);
assert_eq!(Hosts::default().entries.len(), 6);
assert!(BootLog::file("/boot.log").append);
assert_eq!(UnixSocketConfiguration::new("/a", "/b").direction, Direction::Into);
assert_eq!(NatInterface::new("10.0.0.2/24", "10.0.0.1").mtu, 1500);
assert_eq!(BootSpec::new("id", "image").rootfs_size_in_bytes, 8 * GIB);
}
#[test]
fn builds_mounts_of_each_kind() {
let share = Mount::share("/Users/user/workspace", "/workspace", &["ro"]);
assert_eq!(share.r#type, "virtiofs");
assert_eq!(share.options, ["ro"]);
assert_eq!(share.runtime_options, RuntimeOptions::Virtiofs(vec![]));
assert_eq!(
Mount::block("ext4", "/images/data.ext4", "/data", &[]).runtime_options,
RuntimeOptions::Virtioblk(vec![])
);
assert_eq!(
Mount::any("tmpfs", "tmpfs", "/scratch", &[]).runtime_options,
RuntimeOptions::Any(vec![])
);
}
}