use std::{fmt, str::FromStr};
use maybe_multiple::MaybeMultiple;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use thiserror::Error;
static SECURITY_OPT_REGEX_STR: &str = r".+:.+";
static SECURITY_OPT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(&format!("^{SECURITY_OPT_REGEX_STR}$"))
.expect("instantiating SECURITY_OPT_REGEX from given static string must not fail")
});
macro_rules! stringvec {
($($x:expr),*) => (vec![$($x.to_string()),*]);
}
#[derive(Debug, Serialize)]
pub struct Docker {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_images: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_privileged_images: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_pull_policies: Option<Vec<PullPolicy>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_services: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub allowed_privileged_services: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_dir: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cap_add: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cap_drop: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cpuset_cpus: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cpuset_mems: Option<String>,
pub cpu_shares: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub cpus: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub devices: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub device_cgroup_rules: Vec<String>,
pub disable_cache: bool,
pub disable_entrypoint_overwrite: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub dns: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub dns_search: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extra_hosts: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gpus: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub group_add: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub helper_image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub helper_image_flavor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub helper_image_autoset_arch_and_os: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
pub image: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub links: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_swap: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_reservation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub network_mode: Option<String>,
pub network_mtu: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub mac_address: Option<String>,
pub oom_kill_disable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub oom_score_adjust: Option<i32>,
pub privileged: bool,
#[serde(skip_serializing_if = "MaybeMultiple::is_none")]
pub pull_policy: MaybeMultiple<PullPolicy>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runtime: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub isolation: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub security_opt: Vec<SecurityOpt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shm_size: Option<u32>,
pub smg_size: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub sysctls: Option<Sysctls>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tls_cert_path: Option<String>,
pub tls_verify: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub userns_mode: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub volumes: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub volumes_from: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume_driver: Option<String>,
pub wait_for_service_timeout: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub container_labels: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub services: Vec<Service>,
}
impl Default for Docker {
fn default() -> Self {
Self {
allowed_images: Vec::new(),
allowed_privileged_images: Vec::new(),
allowed_pull_policies: Vec::new().into(),
allowed_services: Vec::new(),
allowed_privileged_services: Vec::new(),
cache_dir: None,
cap_add: Vec::new(),
cap_drop: Vec::new(),
cpuset_cpus: None,
cpuset_mems: None,
cpu_shares: 1024,
cpus: None,
devices: Vec::new(),
device_cgroup_rules: Vec::new(),
disable_cache: false,
disable_entrypoint_overwrite: false,
dns: Vec::new(),
dns_search: Vec::new(),
extra_hosts: Vec::new(),
gpus: None,
group_add: Vec::new(),
helper_image: None,
helper_image_flavor: None,
helper_image_autoset_arch_and_os: None,
host: None,
hostname: None,
image: "alpine:latest".to_string(),
links: Vec::new(),
memory: None,
memory_swap: None,
memory_reservation: None,
network_mode: None,
network_mtu: 0,
mac_address: None,
oom_kill_disable: false,
oom_score_adjust: None,
privileged: false,
pull_policy: MaybeMultiple::Some(PullPolicy::Always),
runtime: None,
isolation: None,
security_opt: Vec::new(),
shm_size: None,
smg_size: 0,
sysctls: None,
tls_cert_path: None,
tls_verify: false,
user: None,
userns_mode: None,
volumes: stringvec!["/cache"],
volumes_from: Vec::new(),
volume_driver: None,
wait_for_service_timeout: 30,
container_labels: Vec::new(),
services: Vec::new(),
}
}
}
#[derive(Debug, Serialize)]
pub struct Sysctls {}
#[derive(Debug, Serialize)]
pub struct Service {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub alias: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub entrypoint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub environment: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all(serialize = "kebab-case"))]
pub enum PullPolicy {
Always, IfNotPresent, Never, }
#[derive(Debug, PartialEq, Eq, Error)]
#[error("invalid security option; must be a key:value pair")]
pub struct SecurityOptParseError;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SecurityOpt(String);
impl SecurityOpt {
pub fn parse<S>(opt: S) -> Result<Self, SecurityOptParseError>
where
S: Into<String>,
{
let opt = opt.into();
if !SECURITY_OPT_REGEX.is_match(&opt) {
#[cfg(feature = "tracing")]
tracing::error!("invalid security option: {opt}");
return Err(SecurityOptParseError);
}
Ok(Self(opt))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for SecurityOpt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl FromStr for SecurityOpt {
type Err = SecurityOptParseError;
fn from_str(opt: &str) -> Result<Self, Self::Err> {
Self::parse(opt)
}
}
impl<'a> Deserialize<'a> for SecurityOpt {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'a>,
{
let opt = String::deserialize(deserializer)?;
Self::parse(opt).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "sqlx")]
impl<DB> sqlx::Type<DB> for SecurityOpt
where
DB: sqlx::Database,
String: sqlx::Type<DB>,
{
fn type_info() -> DB::TypeInfo {
<String as sqlx::Type<DB>>::type_info()
}
fn compatible(ty: &DB::TypeInfo) -> bool {
<String as sqlx::Type<DB>>::compatible(ty)
}
}
#[cfg(feature = "sqlx")]
impl<'a, DB> sqlx::Encode<'a, DB> for SecurityOpt
where
DB: sqlx::Database,
String: sqlx::Encode<'a, DB>,
{
fn encode_by_ref(
&self,
buf: &mut <DB as sqlx::database::HasArguments<'a>>::ArgumentBuffer,
) -> sqlx::encode::IsNull {
self.0.encode_by_ref(buf)
}
}
#[cfg(feature = "sqlx")]
impl<'a, DB> sqlx::Decode<'a, DB> for SecurityOpt
where
DB: sqlx::Database,
String: sqlx::Decode<'a, DB>,
{
fn decode(
value: <DB as sqlx::database::HasValueRef<'a>>::ValueRef,
) -> Result<Self, Box<dyn std::error::Error + 'static + Send + Sync>> {
let value = <String as sqlx::Decode<DB>>::decode(value)?;
Ok(SecurityOpt::parse(value)?)
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use test_strategy::proptest;
use super::{
MaybeMultiple, PullPolicy, SecurityOpt, SECURITY_OPT_REGEX, SECURITY_OPT_REGEX_STR,
};
#[proptest]
fn parse_valid_security_options(#[strategy(SECURITY_OPT_REGEX_STR)] opt: String) {
assert_eq!(opt, SecurityOpt::parse(&opt).unwrap().as_str());
}
#[proptest]
fn parse_invalid_security_options(#[filter(|o| !SECURITY_OPT_REGEX.is_match(o))] opt: String) {
assert!(SecurityOpt::parse(opt).is_err());
}
#[test]
fn pull_policy_serialization() {
let policy = PullPolicy::Always;
let serialized = serde_json::to_string(&policy).unwrap();
assert_eq!(serialized, r#""always""#);
let policy = MaybeMultiple::from_vec(vec![PullPolicy::Always, PullPolicy::IfNotPresent]);
let serialized = serde_json::to_string(&policy).unwrap();
assert_eq!(serialized, r#"["always","if-not-present"]"#);
}
}