use crate::dfdaemon::default_proxy_server_port;
use dragonfly_client_core::error::{ErrorType, OrErr};
use dragonfly_client_core::Result;
use serde::{ser::SerializeStruct, Deserialize, Serialize};
use std::fs;
use std::net::Ipv4Addr;
use std::path::PathBuf;
use tracing::{info, instrument};
use validator::Validate;
pub const NAME: &str = "dfinit";
#[inline]
pub fn default_dfinit_config_path() -> PathBuf {
crate::default_config_dir().join("dfinit.yaml")
}
#[inline]
fn default_container_runtime_containerd_config_path() -> PathBuf {
PathBuf::from("/etc/containerd/config.toml")
}
#[inline]
fn default_container_runtime_docker_config_path() -> PathBuf {
PathBuf::from("/etc/docker/daemon.json")
}
#[inline]
fn default_container_runtime_crio_config_path() -> PathBuf {
PathBuf::from("/etc/containers/registries.conf")
}
#[inline]
fn default_container_runtime_podman_config_path() -> PathBuf {
PathBuf::from("/etc/containers/registries.conf")
}
#[inline]
fn default_container_runtime_crio_unqualified_search_registries() -> Vec<String> {
vec![
"registry.fedoraproject.org".to_string(),
"registry.access.redhat.com".to_string(),
"docker.io".to_string(),
]
}
#[inline]
fn default_container_runtime_podman_unqualified_search_registries() -> Vec<String> {
vec![
"registry.fedoraproject.org".to_string(),
"registry.access.redhat.com".to_string(),
"docker.io".to_string(),
]
}
#[inline]
fn default_proxy_addr() -> String {
format!(
"http://{}:{}",
Ipv4Addr::LOCALHOST,
default_proxy_server_port()
)
}
#[inline]
pub fn default_container_runtime_containerd_registry_capabilities() -> Vec<String> {
vec!["pull".to_string(), "resolve".to_string()]
}
#[inline]
fn default_container_runtime_containerd_proxy_all_registries() -> bool {
true
}
#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ContainerdRegistry {
pub host_namespace: String,
pub server_addr: String,
#[serde(default = "default_container_runtime_containerd_registry_capabilities")]
pub capabilities: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub skip_verify: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ca: Option<Vec<String>>,
}
#[derive(Debug, Clone, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Containerd {
#[serde(default = "default_container_runtime_containerd_config_path")]
pub config_path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub cri_plugin_id: Option<String>,
pub registries: Vec<ContainerdRegistry>,
#[serde(default = "default_container_runtime_containerd_proxy_all_registries")]
pub proxy_all_registries: bool,
}
impl Default for Containerd {
fn default() -> Self {
Self {
config_path: PathBuf::default(),
cri_plugin_id: None,
registries: Vec::default(),
proxy_all_registries: default_container_runtime_containerd_proxy_all_registries(),
}
}
}
#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize, PartialEq, Eq)]
#[serde(default, rename_all = "camelCase")]
pub struct CRIORegistry {
pub prefix: String,
pub location: String,
}
#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct CRIO {
#[serde(default = "default_container_runtime_crio_config_path")]
pub config_path: PathBuf,
#[serde(default = "default_container_runtime_crio_unqualified_search_registries")]
pub unqualified_search_registries: Vec<String>,
pub registries: Vec<CRIORegistry>,
}
#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize, PartialEq, Eq)]
#[serde(default, rename_all = "camelCase")]
pub struct PodmanRegistry {
pub prefix: String,
pub location: String,
}
#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Podman {
#[serde(default = "default_container_runtime_podman_config_path")]
pub config_path: PathBuf,
#[serde(default = "default_container_runtime_podman_unqualified_search_registries")]
pub unqualified_search_registries: Vec<String>,
pub registries: Vec<PodmanRegistry>,
}
#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Docker {
#[serde(default = "default_container_runtime_docker_config_path")]
pub config_path: PathBuf,
}
#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ContainerRuntime {
#[serde(flatten)]
pub config: Option<ContainerRuntimeConfig>,
}
#[derive(Debug, Clone)]
pub enum ContainerRuntimeConfig {
Containerd(Containerd),
Docker(Docker),
CRIO(CRIO),
Podman(Podman),
}
impl Serialize for ContainerRuntimeConfig {
fn serialize<S>(&self, serializer: S) -> std::prelude::v1::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match *self {
ContainerRuntimeConfig::Containerd(ref cfg) => {
let mut state = serializer.serialize_struct("containerd", 1)?;
state.serialize_field("containerd", &cfg)?;
state.end()
}
ContainerRuntimeConfig::Docker(ref cfg) => {
let mut state = serializer.serialize_struct("docker", 1)?;
state.serialize_field("docker", &cfg)?;
state.end()
}
ContainerRuntimeConfig::CRIO(ref cfg) => {
let mut state = serializer.serialize_struct("crio", 1)?;
state.serialize_field("crio", &cfg)?;
state.end()
}
ContainerRuntimeConfig::Podman(ref cfg) => {
let mut state = serializer.serialize_struct("podman", 1)?;
state.serialize_field("podman", &cfg)?;
state.end()
}
}
}
}
impl<'de> Deserialize<'de> for ContainerRuntimeConfig {
fn deserialize<D>(deserializer: D) -> std::prelude::v1::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct ContainerRuntimeHelper {
containerd: Option<Containerd>,
docker: Option<Docker>,
crio: Option<CRIO>,
podman: Option<Podman>,
}
let helper = ContainerRuntimeHelper::deserialize(deserializer)?;
match helper {
ContainerRuntimeHelper {
containerd: Some(containerd),
..
} => Ok(ContainerRuntimeConfig::Containerd(containerd)),
ContainerRuntimeHelper {
docker: Some(docker),
..
} => Ok(ContainerRuntimeConfig::Docker(docker)),
ContainerRuntimeHelper {
crio: Some(crio), ..
} => Ok(ContainerRuntimeConfig::CRIO(crio)),
ContainerRuntimeHelper {
podman: Some(podman),
..
} => Ok(ContainerRuntimeConfig::Podman(podman)),
_ => {
use serde::de::Error;
Err(D::Error::custom(
"expected containerd or docker or crio or podman",
))
}
}
}
}
#[derive(Debug, Clone, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Proxy {
#[serde(default = "default_proxy_addr")]
pub addr: String,
}
impl Default for Proxy {
fn default() -> Self {
Self {
addr: default_proxy_addr(),
}
}
}
#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Config {
#[validate]
pub proxy: Proxy,
#[validate]
pub container_runtime: ContainerRuntime,
}
impl Config {
#[instrument(skip_all)]
pub fn load(path: &PathBuf) -> Result<Config> {
let content = fs::read_to_string(path)?;
let config: Config = serde_yaml::from_str(&content).or_err(ErrorType::ConfigError)?;
info!("load config from {}", path.display());
config.validate().or_err(ErrorType::ValidationError)?;
Ok(config)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::type_complexity)]
use super::*;
use dragonfly_client_core::Error;
use std::path::Path;
#[test]
fn default_dfinit_config_path_joins_config_dir() {
assert_eq!(
default_dfinit_config_path(),
crate::default_config_dir().join("dfinit.yaml")
);
}
#[test]
fn config_serializes_container_runtime_under_runtime_key() {
let test_cases = vec![
(
None,
r#"
proxy:
addr: hello
containerRuntime: {}"#,
),
(
Some(ContainerRuntimeConfig::Containerd(Containerd::default())),
r#"
proxy:
addr: hello
containerRuntime:
containerd:
configPath: ''
registries: []
proxyAllRegistries: true"#,
),
(
Some(ContainerRuntimeConfig::Containerd(Containerd {
config_path: PathBuf::from("/root/.dragonfly/config/dfinit/yaml"),
..Default::default()
})),
r#"
proxy:
addr: hello
containerRuntime:
containerd:
configPath: /root/.dragonfly/config/dfinit/yaml
registries: []
proxyAllRegistries: true"#,
),
(
Some(ContainerRuntimeConfig::Docker(Docker {
config_path: PathBuf::from("/root/.dragonfly/config/dfinit/yaml"),
})),
r#"
proxy:
addr: hello
containerRuntime:
docker:
configPath: /root/.dragonfly/config/dfinit/yaml"#,
),
(
Some(ContainerRuntimeConfig::CRIO(CRIO::default())),
r#"
proxy:
addr: hello
containerRuntime:
crio:
configPath: ''
unqualifiedSearchRegistries: []
registries: []"#,
),
(
Some(ContainerRuntimeConfig::Podman(Podman::default())),
r#"
proxy:
addr: hello
containerRuntime:
podman:
configPath: ''
unqualifiedSearchRegistries: []
registries: []"#,
),
];
for (runtime_config, expected) in test_cases {
let config = Config {
proxy: Proxy {
addr: "hello".to_string(),
},
container_runtime: ContainerRuntime {
config: runtime_config,
},
};
let yaml = serde_yaml::to_string(&config).unwrap();
assert_eq!(yaml.trim(), expected.trim());
}
}
#[test]
fn config_deserializes_container_runtime_variant() {
let test_cases: Vec<(&str, fn(&Config))> = vec![
("{}", |config| {
assert_eq!(config.proxy.addr, "http://127.0.0.1:4001");
assert!(config.container_runtime.config.is_none());
}),
("proxy:\n addr: hello\n", |config| {
assert_eq!(config.proxy.addr, "hello");
assert!(config.container_runtime.config.is_none());
}),
("containerRuntime:\n unknown: {}\n", |config| {
assert!(config.container_runtime.config.is_none());
}),
(
r#"
proxy:
addr: hello
containerRuntime:
containerd:
configPath: test_path
criPluginId: io.containerd.cri.v1.images
proxyAllRegistries: false
registries:
- hostNamespace: docker.io
serverAddr: https://index.docker.io
skipVerify: true
ca:
- /etc/ssl/certs/ca.crt
- hostNamespace: ghcr.io
serverAddr: https://ghcr.io
capabilities:
- pull
"#,
|config| {
assert_eq!(config.proxy.addr, "hello");
let Some(ContainerRuntimeConfig::Containerd(containerd)) =
&config.container_runtime.config
else {
unreachable!()
};
assert_eq!(containerd.config_path, PathBuf::from("test_path"));
assert_eq!(
containerd.cri_plugin_id,
Some("io.containerd.cri.v1.images".to_string())
);
assert!(!containerd.proxy_all_registries);
assert_eq!(containerd.registries.len(), 2);
assert_eq!(containerd.registries[0].host_namespace, "docker.io");
assert_eq!(
containerd.registries[0].server_addr,
"https://index.docker.io"
);
assert_eq!(
containerd.registries[0].capabilities,
vec!["pull", "resolve"]
);
assert_eq!(containerd.registries[0].skip_verify, Some(true));
assert_eq!(
containerd.registries[0].ca,
Some(vec!["/etc/ssl/certs/ca.crt".to_string()])
);
assert_eq!(containerd.registries[1].capabilities, vec!["pull"]);
assert!(containerd.registries[1].skip_verify.is_none());
assert!(containerd.registries[1].ca.is_none());
},
),
("containerRuntime:\n containerd: {}\n", |config| {
let Some(ContainerRuntimeConfig::Containerd(containerd)) =
&config.container_runtime.config
else {
unreachable!()
};
assert_eq!(
containerd.config_path,
Path::new("/etc/containerd/config.toml")
);
assert!(containerd.cri_plugin_id.is_none());
assert!(containerd.registries.is_empty());
assert!(containerd.proxy_all_registries);
}),
(
"containerRuntime:\n docker:\n configPath: test_path\n",
|config| {
let Some(ContainerRuntimeConfig::Docker(docker)) =
&config.container_runtime.config
else {
unreachable!()
};
assert_eq!(docker.config_path, PathBuf::from("test_path"));
},
),
("containerRuntime:\n docker: {}\n", |config| {
let Some(ContainerRuntimeConfig::Docker(docker)) = &config.container_runtime.config
else {
unreachable!()
};
assert_eq!(docker.config_path, Path::new("/etc/docker/daemon.json"));
}),
(
r#"
containerRuntime:
crio:
configPath: test_path
unqualifiedSearchRegistries:
- reg1
- reg2
registries:
- prefix: prefix1
location: location1
- prefix: prefix2
location: location2
"#,
|config| {
let Some(ContainerRuntimeConfig::CRIO(crio)) = &config.container_runtime.config
else {
unreachable!()
};
assert_eq!(crio.config_path, PathBuf::from("test_path"));
assert_eq!(crio.unqualified_search_registries, vec!["reg1", "reg2"]);
assert_eq!(
crio.registries,
vec![
CRIORegistry {
prefix: "prefix1".to_string(),
location: "location1".to_string(),
},
CRIORegistry {
prefix: "prefix2".to_string(),
location: "location2".to_string(),
},
]
);
},
),
("containerRuntime:\n crio: {}\n", |config| {
let Some(ContainerRuntimeConfig::CRIO(crio)) = &config.container_runtime.config
else {
unreachable!()
};
assert_eq!(
crio.config_path,
Path::new("/etc/containers/registries.conf")
);
assert_eq!(
crio.unqualified_search_registries,
vec![
"registry.fedoraproject.org",
"registry.access.redhat.com",
"docker.io"
]
);
assert!(crio.registries.is_empty());
}),
(
r#"
containerRuntime:
podman:
configPath: test_path
unqualifiedSearchRegistries:
- reg1
- reg2
registries:
- prefix: prefix1
location: location1
- prefix: prefix2
location: location2
"#,
|config| {
let Some(ContainerRuntimeConfig::Podman(podman)) =
&config.container_runtime.config
else {
unreachable!()
};
assert_eq!(podman.config_path, PathBuf::from("test_path"));
assert_eq!(podman.unqualified_search_registries, vec!["reg1", "reg2"]);
assert_eq!(
podman.registries,
vec![
PodmanRegistry {
prefix: "prefix1".to_string(),
location: "location1".to_string(),
},
PodmanRegistry {
prefix: "prefix2".to_string(),
location: "location2".to_string(),
},
]
);
},
),
("containerRuntime:\n podman: {}\n", |config| {
let Some(ContainerRuntimeConfig::Podman(podman)) = &config.container_runtime.config
else {
unreachable!()
};
assert_eq!(
podman.config_path,
Path::new("/etc/containers/registries.conf")
);
assert_eq!(
podman.unqualified_search_registries,
vec![
"registry.fedoraproject.org",
"registry.access.redhat.com",
"docker.io"
]
);
assert!(podman.registries.is_empty());
}),
];
for (yaml, expect) in test_cases {
let config: Config = serde_yaml::from_str(yaml).unwrap();
expect(&config);
}
}
#[test]
fn load_reads_file_and_wraps_parse_errors() {
let test_cases: Vec<(&str, fn(Result<Config>))> = vec![
(
"containerRuntime:\n docker:\n configPath: test_path\n",
|result| {
let config = result.unwrap();
let Some(ContainerRuntimeConfig::Docker(docker)) =
&config.container_runtime.config
else {
unreachable!()
};
assert_eq!(docker.config_path, PathBuf::from("test_path"));
},
),
("containerRuntime: [", |result| {
assert!(
matches!(result, Err(Error::ExternalError(ref err)) if err.etype == ErrorType::ConfigError)
);
}),
];
for (content, expect) in test_cases {
let file = tempfile::NamedTempFile::new().unwrap();
fs::write(file.path(), content).unwrap();
expect(Config::load(&file.path().to_path_buf()));
}
}
}