diff --git a/.gitignore b/.gitignore index 3b146830..869ae5c5 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ Temporary Items # Ignore unuseful files scripts/certs + +# Ignore .vscode folder +.vscode diff --git a/Cargo.lock b/Cargo.lock index 352eb8c1..7faed6f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1007,6 +1007,7 @@ dependencies = [ "dragonfly-client", "dragonfly-client-config", "dragonfly-client-core", + "tempfile", "tokio", "toml", "toml_edit", diff --git a/dragonfly-client-config/src/dfinit.rs b/dragonfly-client-config/src/dfinit.rs index d66931bc..6c6ff410 100644 --- a/dragonfly-client-config/src/dfinit.rs +++ b/dragonfly-client-config/src/dfinit.rs @@ -56,6 +56,12 @@ fn default_container_runtime_crio_config_path() -> PathBuf { PathBuf::from("/etc/containers/registries.conf") } +/// default_container_runtime_podman_config_path is the default podman configuration path. +#[inline] +fn default_container_runtime_podman_config_path() -> PathBuf { + PathBuf::from("/etc/containers/registries.conf") +} + /// default_container_runtime_crio_unqualified_search_registries is the default unqualified search registries of cri-o, /// refer to https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md#global-settings. #[inline] @@ -67,6 +73,17 @@ fn default_container_runtime_crio_unqualified_search_registries() -> Vec ] } +/// default_container_runtime_podman_unqualified_search_registries is the default unqualified search registries of cri-o, +/// refer to https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md#global-settings. +#[inline] +fn default_container_runtime_podman_unqualified_search_registries() -> Vec { + vec![ + "registry.fedoraproject.org".to_string(), + "registry.access.redhat.com".to_string(), + "docker.io".to_string(), + ] +} + /// default_proxy_addr is the default proxy address of dfdaemon. #[inline] fn default_proxy_addr() -> String { @@ -156,6 +173,37 @@ pub struct CRIO { pub registries: Vec, } +/// CRIORegistry is the registry configuration for cri-o. +#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize, PartialEq, Eq)] +#[serde(default, rename_all = "camelCase")] +pub struct PodmanRegistry { + /// prefix is the prefix of the user-specified image name, refer to + /// https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md#choosing-a-registry-toml-table. + pub prefix: String, + + /// location accepts the same format as the prefix field, and specifies the physical location of the prefix-rooted namespace, + /// refer to https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md#remapping-and-mirroring-registries. + pub location: String, +} + +/// Podman is the podman configuration for dfinit. +#[derive(Debug, Clone, Default, Validate, Deserialize, Serialize)] +#[serde(default, rename_all = "camelCase")] +pub struct Podman { + /// config_path is the path of cri-o registries's configuration file. + #[serde(default = "default_container_runtime_podman_config_path")] + pub config_path: PathBuf, + + /// unqualified_search_registries is an array of host[:port] registries to try when pulling an unqualified image, in order. + /// Refer to https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md#global-settings. + #[serde(default = "default_container_runtime_podman_unqualified_search_registries")] + pub unqualified_search_registries: Vec, + + /// registries is the list of cri-o registries, refer to + /// https://github.com/containers/image/blob/main/docs/containers-registries.conf.5.md#namespaced-registry-settings. + pub registries: Vec, +} + /// Docker is the docker configuration for dfinit. #[derive(Debug, Clone, Default, Validate, Deserialize, Serialize)] #[serde(default, rename_all = "camelCase")] @@ -179,6 +227,7 @@ pub enum ContainerRuntimeConfig { Containerd(Containerd), Docker(Docker), CRIO(CRIO), + Podman(Podman), } /// Serialize is the implementation of the Serialize trait for ContainerRuntimeConfig. @@ -203,6 +252,11 @@ impl Serialize for ContainerRuntimeConfig { 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() + } } } } @@ -218,6 +272,7 @@ impl<'de> Deserialize<'de> for ContainerRuntimeConfig { containerd: Option, docker: Option, crio: Option, + podman: Option, } let helper = ContainerRuntimeHelper::deserialize(deserializer)?; @@ -233,9 +288,15 @@ impl<'de> Deserialize<'de> for ContainerRuntimeConfig { 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")) + Err(D::Error::custom( + "expected containerd or docker or crio or podman", + )) } } } diff --git a/dragonfly-client-init/Cargo.toml b/dragonfly-client-init/Cargo.toml index 646f35d6..31cefc69 100644 --- a/dragonfly-client-init/Cargo.toml +++ b/dragonfly-client-init/Cargo.toml @@ -25,3 +25,4 @@ tracing.workspace = true toml_edit.workspace = true toml.workspace = true url.workspace = true +tempfile.workspace = true diff --git a/dragonfly-client-init/src/container_runtime/mod.rs b/dragonfly-client-init/src/container_runtime/mod.rs index d81cad68..2ba5cbd3 100644 --- a/dragonfly-client-init/src/container_runtime/mod.rs +++ b/dragonfly-client-init/src/container_runtime/mod.rs @@ -21,6 +21,7 @@ use tracing::{info, instrument}; pub mod containerd; pub mod crio; pub mod docker; +pub mod podman; /// Engine represents config of the container runtime engine. #[derive(Debug, Clone)] @@ -28,6 +29,7 @@ enum Engine { Containerd(containerd::Containerd), Docker(docker::Docker), Crio(crio::CRIO), + Podman(podman::Podman), } /// ContainerRuntime represents the container runtime manager. @@ -55,6 +57,7 @@ impl ContainerRuntime { Some(Engine::Containerd(containerd)) => containerd.run().await, Some(Engine::Docker(docker)) => docker.run().await, Some(Engine::Crio(crio)) => crio.run().await, + Some(Engine::Podman(podman)) => podman.run().await, } } @@ -72,6 +75,9 @@ impl ContainerRuntime { ContainerRuntimeConfig::CRIO(crio) => { Engine::Crio(crio::CRIO::new(crio.clone(), config.proxy.clone())) } + ContainerRuntimeConfig::Podman(podman) => { + Engine::Podman(podman::Podman::new(podman.clone(), config.proxy.clone())) + } }; info!("container runtime engine is {:?}", engine); diff --git a/dragonfly-client-init/src/container_runtime/podman.rs b/dragonfly-client-init/src/container_runtime/podman.rs new file mode 100644 index 00000000..fc6b6194 --- /dev/null +++ b/dragonfly-client-init/src/container_runtime/podman.rs @@ -0,0 +1,163 @@ +/* + * Copyright 2024 The Dragonfly Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use dragonfly_client_config::dfinit; +use dragonfly_client_core::{ + error::{ErrorType, OrErr}, + Error, Result, +}; +use tokio::{self, fs}; +use toml_edit::{value, Array, ArrayOfTables, Item, Table, Value}; +use tracing::{info, instrument}; +use url::Url; + +/// Podman represents the podman runtime manager. +#[derive(Debug, Clone)] +pub struct Podman { + /// config is the configuration for initializing + /// runtime environment for the dfdaemon. + config: dfinit::Podman, + + /// proxy_config is the configuration for the dfdaemon's proxy server. + proxy_config: dfinit::Proxy, +} + +/// Podman implements the podman runtime manager. +impl Podman { + /// new creates a new podman runtime manager. + #[instrument(skip_all)] + pub fn new(config: dfinit::Podman, proxy_config: dfinit::Proxy) -> Self { + Self { + config, + proxy_config, + } + } + + /// run runs the podman runtime to initialize + /// runtime environment for the dfdaemon. + #[instrument(skip_all)] + pub async fn run(&self) -> Result<()> { + let mut registries_config_table = toml_edit::DocumentMut::new(); + registries_config_table.set_implicit(true); + + // Add unqualified-search-registries to registries config. + let mut unqualified_search_registries = Array::default(); + for unqualified_search_registry in self.config.unqualified_search_registries.clone() { + unqualified_search_registries.push(Value::from(unqualified_search_registry)); + } + registries_config_table.insert( + "unqualified-search-registries", + value(unqualified_search_registries), + ); + + // Parse proxy address to get host and port. + let proxy_url = + Url::parse(self.proxy_config.addr.as_str()).or_err(ErrorType::ParseError)?; + let proxy_host = proxy_url + .host_str() + .ok_or(Error::Unknown("host not found".to_string()))?; + let proxy_port = proxy_url + .port_or_known_default() + .ok_or(Error::Unknown("port not found".to_string()))?; + let proxy_location = format!("{}:{}", proxy_host, proxy_port); + + // Add registries to the registries config. + let mut registries_table = ArrayOfTables::new(); + for registry in self.config.registries.clone() { + info!("add registry: {:?}", registry); + let mut registry_mirror_table = Table::new(); + registry_mirror_table.set_implicit(true); + registry_mirror_table.insert("insecure", value(true)); + registry_mirror_table.insert("location", value(proxy_location.as_str())); + + let mut registry_mirrors_table = ArrayOfTables::new(); + registry_mirrors_table.push(registry_mirror_table); + + let mut registry_table = Table::new(); + registry_table.set_implicit(true); + registry_table.insert("prefix", value(registry.prefix)); + registry_table.insert("location", value(registry.location)); + registry_table.insert("mirror", Item::ArrayOfTables(registry_mirrors_table)); + + registries_table.push(registry_table); + } + registries_config_table.insert("registry", Item::ArrayOfTables(registries_table)); + + let registries_config_dir = self + .config + .config_path + .parent() + .ok_or(Error::Unknown("invalid config path".to_string()))?; + fs::create_dir_all(registries_config_dir.as_os_str()).await?; + fs::write( + self.config.config_path.as_os_str(), + registries_config_table.to_string().as_bytes(), + ) + .await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_podman_config() { + use tempfile::NamedTempFile; + + let podman_config_file = NamedTempFile::new().unwrap(); + let podman = Podman::new( + dfinit::Podman { + config_path: podman_config_file.path().to_path_buf(), + registries: vec![dfinit::PodmanRegistry { + prefix: "registry.example.com".into(), + location: "registry.example.com".into(), + }], + unqualified_search_registries: vec!["registry.example.com".into()], + }, + dfinit::Proxy { + addr: "http://127.0.0.1:5000".into(), + }, + ); + let result = podman.run().await; + + assert!(result.is_ok()); + + // get the contents of the file + let contents = fs::read_to_string(podman_config_file.path().to_path_buf()) + .await + .unwrap(); + let expected_contents = r#"unqualified-search-registries = ["registry.example.com"] + +[[registry]] +prefix = "registry.example.com" +location = "registry.example.com" + +[[registry.mirror]] +insecure = true +location = "127.0.0.1:5000" +"#; + // assert that the contents of the file are as expected + assert_eq!(contents, expected_contents); + + // clean up + fs::remove_file(podman_config_file.path().to_path_buf()) + .await + .unwrap(); + } +}