use crate::{
common::{container::Container, name::Name, non_nul_string::NonNulString, version::Version},
seccomp::Seccomp,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_with::{
rust::{maps_duplicate_key_is_error, sets_duplicate_value_is_error},
skip_serializing_none,
};
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use thiserror::Error;
use validator::{Validate, ValidationError, ValidationErrors};
use self::network::Network;
pub mod autostart;
pub mod capabilities;
pub mod cgroups;
pub mod console;
pub mod io;
pub mod mount;
pub mod network;
pub mod rlimit;
pub mod sched;
pub mod seccomp;
pub mod selinux;
pub mod socket;
#[cfg(test)]
mod test;
const MAX_SUPPL_GROUPS: usize = 64;
const MAX_SUPPL_GROUP_LENGTH: usize = 64;
const MAX_ENV_VARS: usize = 64;
const MAX_ENV_VAR_NAME_LENGTH: usize = 64;
const MAX_ENV_VAR_VALUE_LENTH: usize = 1024;
const RESERVED_ENV_VARIABLES: &[&str] = &[
"NORTHSTAR_NAME",
"NORTHSTAR_VERSION",
"NORTHSTAR_CONTAINER",
"NORTHSTAR_CONSOLE",
];
#[derive(Error, Debug)]
#[allow(missing_docs)]
pub enum Error {
#[error("invalid manifest: {0}")]
Validation(ValidationErrors),
#[error(transparent)]
Yaml(#[from] serde_yaml::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
TomlDe(#[from] toml::de::Error),
#[error(transparent)]
TomlSer(#[from] toml::ser::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[skip_serializing_none]
#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize, Validate)]
#[serde(deny_unknown_fields)]
#[validate(schema(function = "validate"))]
pub struct Manifest {
pub name: Name,
pub version: Version,
pub console: Option<console::Console>,
#[validate(length(min = 1, max = 4096))]
pub init: Option<NonNulString>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<NonNulString>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
#[validate(custom(function = "validate_env"))]
pub env: HashMap<NonNulString, NonNulString>,
#[validate(range(min = 1, message = "uid must be greater than 0"))]
pub uid: u16,
#[validate(range(min = 1, message = "gid must be greater than 0"))]
pub gid: u16,
#[validate(nested)]
pub sched: Option<sched::Sched>,
#[serde(
default,
skip_serializing_if = "HashMap::is_empty",
deserialize_with = "maps_duplicate_key_is_error::deserialize"
)]
#[validate(custom(function = "mount::validate"))]
pub mounts: HashMap<mount::MountPoint, mount::Mount>,
pub autostart: Option<autostart::Autostart>,
pub cgroups: Option<self::cgroups::CGroups>,
#[validate(custom(function = "network::validate"))]
pub network: Option<Network>,
#[validate(custom(function = "seccomp::validate"))]
pub seccomp: Option<Seccomp>,
#[validate(nested)]
pub selinux: Option<selinux::Selinux>,
#[serde(
default,
skip_serializing_if = "HashSet::is_empty",
deserialize_with = "sets_duplicate_value_is_error::deserialize"
)]
pub capabilities: HashSet<capabilities::Capability>,
#[serde(
default,
skip_serializing_if = "HashSet::is_empty",
deserialize_with = "sets_duplicate_value_is_error::deserialize"
)]
#[validate(custom(function = "validate_suppl_groups"))]
pub suppl_groups: HashSet<NonNulString>,
#[serde(
default,
skip_serializing_if = "HashMap::is_empty",
deserialize_with = "maps_duplicate_key_is_error::deserialize"
)]
pub rlimits: HashMap<rlimit::RLimitResource, rlimit::RLimitValue>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub sockets: HashMap<NonNulString, socket::Socket>,
#[serde(default)]
pub io: Option<io::Io>,
pub custom: Option<Value>,
}
impl Manifest {
pub fn container(&self) -> Container {
Container::new(self.name.clone(), self.version.clone())
}
pub fn from_reader<R: std::io::Read>(mut reader: R) -> Result<Self, Error> {
let mut buf = String::new();
reader
.read_to_string(&mut buf)
.map_err(Error::Io)
.and_then(|_| Manifest::from_str(&buf))
}
}
impl FromStr for Manifest {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
let manifest: Manifest = if let Ok(manifest) = serde_yaml::from_str(s) {
manifest
} else if let Ok(manifest) = serde_json::from_str(s) {
manifest
} else {
toml::de::from_str(s).map_err(Error::TomlDe)?
};
manifest.validate().map_err(Error::Validation)?;
Ok(manifest)
}
}
fn validate(manifest: &Manifest) -> Result<(), ValidationError> {
if manifest.init.is_none()
&& (!manifest.args.is_empty()
|| !manifest.capabilities.is_empty()
|| !manifest.env.is_empty()
|| !manifest.suppl_groups.is_empty()
|| manifest.autostart.is_some()
|| manifest.cgroups.is_some()
|| manifest.io.is_some()
|| manifest.seccomp.is_some())
{
return Err(ValidationError::new(
"resource containers must not define any of the following manifest entries:\
args, env, autostart, cgroups, seccomp, capabilities, suppl_groups, io",
));
}
Ok(())
}
fn validate_suppl_groups(groups: &HashSet<NonNulString>) -> Result<(), ValidationError> {
if groups.len() > MAX_SUPPL_GROUPS {
return Err(ValidationError::new(
"supplementary groups exceeds max length",
));
}
if groups.iter().any(|g| g.len() > MAX_SUPPL_GROUP_LENGTH) {
return Err(ValidationError::new(
"supplementary group name exceeds max length",
));
}
Ok(())
}
fn validate_env(env: &HashMap<NonNulString, NonNulString>) -> Result<(), ValidationError> {
if env.len() > MAX_ENV_VARS {
return Err(ValidationError::new("env exceeds max length"));
}
if env.keys().any(|k| k.len() > MAX_ENV_VAR_NAME_LENGTH) {
return Err(ValidationError::new("env variable name exceeds max length"));
}
if env.values().any(|v| v.len() > MAX_ENV_VAR_VALUE_LENTH) {
return Err(ValidationError::new("env value exceeds max length"));
}
if RESERVED_ENV_VARIABLES.iter().any(|key| {
env.contains_key(unsafe { &NonNulString::from_str_unchecked(key) }) }) {
Err(ValidationError::new("reserved env variable name"))
} else {
Ok(())
}
}