Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion dragonfly-client-config/src/dfinit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ pub struct Containerd {
#[serde(default = "default_container_runtime_containerd_config_path")]
pub config_path: PathBuf,

/// The CRI plugin id owning the registry configuration, e.g. "io.containerd.grpc.v1.cri"
/// or "io.containerd.cri.v1.images". If not set, the plugin table present in the
/// containerd configuration is used, preferring "io.containerd.grpc.v1.cri" for
/// version 2 configurations and "io.containerd.cri.v1.images" for version 3.
#[serde(skip_serializing_if = "Option::is_none")]
pub cri_plugin_id: Option<String>,

/// The list of containerd registries.
pub registries: Vec<ContainerdRegistry>,

Expand All @@ -157,6 +164,7 @@ 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(),
}
Expand Down Expand Up @@ -493,16 +501,21 @@ containerRuntime:
assert_eq!("hello".to_string(), cfg.proxy.addr);

let raw_data = r#"
proxy:
proxy:
addr: "hello"
containerRuntime:
containerd:
configPath: "test_path"
criPluginId: "io.containerd.cri.v1.images"
"#;
let cfg: Config = serde_yaml::from_str(raw_data).expect("failed to deserialize");
assert_eq!("hello".to_string(), cfg.proxy.addr);
if let Some(ContainerRuntimeConfig::Containerd(c)) = cfg.container_runtime.config {
assert_eq!(PathBuf::from("test_path"), c.config_path);
assert_eq!(
Some("io.containerd.cri.v1.images".to_string()),
c.cri_plugin_id
);
} else {
panic!("failed to deserialize");
}
Expand Down
143 changes: 132 additions & 11 deletions dragonfly-client-init/src/container_runtime/containerd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,12 @@ impl Containerd {
.unwrap_or(2);
info!("containerd version: {}", version);

let plugin_id = if version == 3 {
"io.containerd.cri.v1.images"
} else {
"io.containerd.grpc.v1.cri"
};
let plugin_id = self
.config
.cri_plugin_id
.as_deref()
.unwrap_or_else(|| Self::get_cri_plugin_id(&containerd_config, version));
info!("containerd CRI plugin: {}", plugin_id);

// If containerd supports config_path mode and config_path is not empty,
// add registries to the certs.d directory.
Expand Down Expand Up @@ -142,6 +143,28 @@ impl Containerd {
Ok(())
}

/// Gets the CRI plugin id owning the registry configuration: the first plugin table
/// present in the config, checked in version-preferred order. Version 2 configurations
/// default to "io.containerd.grpc.v1.cri" and version 3 to "io.containerd.cri.v1.images",
/// but e.g. AKS ships version 2 configurations with the containerd 2.x images plugin.
#[instrument(skip_all)]
fn get_cri_plugin_id(containerd_config: &DocumentMut, version: i64) -> &'static str {
let candidates = match version {
..=2 => ["io.containerd.grpc.v1.cri", "io.containerd.cri.v1.images"],
_ => ["io.containerd.cri.v1.images", "io.containerd.grpc.v1.cri"],
};

candidates
.into_iter()
.find(|&plugin_id| {
containerd_config
.get("plugins")
.and_then(|plugins| plugins.get(plugin_id))
.is_some()
})
.unwrap_or(candidates[0])
}

/// Adds registries to the containerd configuration, when containerd supports
/// config_path mode and config_path is not empty.
#[instrument(skip_all)]
Expand Down Expand Up @@ -278,6 +301,7 @@ mod tests {
let containerd = Containerd::new(
dfinit::Containerd {
config_path: config_path.clone(),
cri_plugin_id: None,
registries: vec![ContainerdRegistry {
host_namespace: "docker.io".into(),
server_addr: "https://registry.example.com".into(),
Expand Down Expand Up @@ -320,6 +344,106 @@ X-Dragonfly-Registry = "https://registry.example.com"
assert_eq!(contents.trim(), expected_contents.trim());
}

#[tokio::test]
async fn test_containerd_config_with_v2_images_plugin_config_path() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
let certs_dir = temp_dir.path().join("certs.d");
let certs_dir_str = certs_dir.to_str().unwrap();

let initial_config = format!(
r#"
version = 2

[plugins]
[plugins."io.containerd.cri.v1.images"]
[plugins."io.containerd.cri.v1.images".registry]
config_path = "{certs_dir_str}"
"#
);
fs::write(&config_path, initial_config).await.unwrap();

let containerd = Containerd::new(
dfinit::Containerd {
config_path: config_path.clone(),
cri_plugin_id: None,
registries: vec![ContainerdRegistry {
host_namespace: "docker.io".into(),
server_addr: "https://registry.example.com".into(),
skip_verify: None,
ca: None,
capabilities: vec!["pull".into(), "resolve".into()],
}],
proxy_all_registries: false,
},
dfinit::Proxy {
addr: "http://127.0.0.1:65001".into(),
},
);

assert!(containerd.run().await.is_ok());

let contents = fs::read_to_string(certs_dir.join("docker.io").join("hosts.toml"))
.await
.unwrap();
assert!(contents.contains("X-Dragonfly-Registry = \"https://registry.example.com\""));

let config_contents = fs::read_to_string(&config_path).await.unwrap();
assert!(!config_contents.contains("io.containerd.grpc.v1.cri"));
}

#[tokio::test]
async fn test_containerd_config_with_cri_plugin_id() {
let temp_dir = TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
let certs_dir = temp_dir.path().join("certs.d");
let certs_dir_str = certs_dir.to_str().unwrap();

let initial_config = format!(
r#"
version = 2

[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.cri.v1.images"]
[plugins."io.containerd.cri.v1.images".registry]
config_path = "{certs_dir_str}"
"#
);
fs::write(&config_path, initial_config.as_bytes())
.await
.unwrap();

let containerd = Containerd::new(
dfinit::Containerd {
config_path: config_path.clone(),
cri_plugin_id: Some("io.containerd.cri.v1.images".into()),
registries: vec![ContainerdRegistry {
host_namespace: "docker.io".into(),
server_addr: "https://registry.example.com".into(),
skip_verify: None,
ca: None,
capabilities: vec!["pull".into(), "resolve".into()],
}],
proxy_all_registries: false,
},
dfinit::Proxy {
addr: "http://127.0.0.1:65001".into(),
},
);

assert!(containerd.run().await.is_ok());

let contents = fs::read_to_string(certs_dir.join("docker.io").join("hosts.toml"))
.await
.unwrap();
assert!(contents.contains("X-Dragonfly-Registry = \"https://registry.example.com\""));
assert_eq!(
fs::read_to_string(&config_path).await.unwrap(),
initial_config
);
}

#[tokio::test]
async fn test_containerd_config_with_proxy_all_registries() {
let temp_dir = TempDir::new().unwrap();
Expand All @@ -340,6 +464,7 @@ X-Dragonfly-Registry = "https://registry.example.com"
let containerd = Containerd::new(
dfinit::Containerd {
config_path: config_path.clone(),
cri_plugin_id: None,
registries: vec![ContainerdRegistry {
host_namespace: "docker.io".into(),
server_addr: "https://registry.example.com".into(),
Expand All @@ -357,14 +482,11 @@ X-Dragonfly-Registry = "https://registry.example.com"
let result = containerd.run().await;
assert!(result.is_ok(), "containerd.run() failed: {result:?}");

// Explicitly configured registry still gets its own hosts.toml with the registry header.
let explicit_hosts = fs::read_to_string(certs_dir.join("docker.io").join("hosts.toml"))
.await
.unwrap();
assert!(explicit_hosts.contains("X-Dragonfly-Registry = \"https://registry.example.com\""));

// _default catch-all is written, without a top-level `server` or X-Dragonfly-Registry
// header — dfdaemon infers the upstream from the containerd `ns=` query parameter.
let default_hosts = fs::read_to_string(certs_dir.join("_default").join("hosts.toml"))
.await
.unwrap();
Expand Down Expand Up @@ -394,6 +516,7 @@ capabilities = ["pull", "resolve"]
let containerd = Containerd::new(
dfinit::Containerd {
config_path: config_path.clone(),
cri_plugin_id: None,
registries: vec![],
proxy_all_registries: false,
},
Expand All @@ -413,7 +536,6 @@ capabilities = ["pull", "resolve"]
let certs_dir = temp_dir.path().join("certs.d");
let certs_dir_str = certs_dir.to_str().unwrap();

// Create initial containerd config with version = 3 and config_path
let initial_config = format!(
r#"
version = 3
Expand All @@ -430,6 +552,7 @@ version = 3
let containerd = Containerd::new(
dfinit::Containerd {
config_path: config_path.clone(),
cri_plugin_id: None,
registries: vec![ContainerdRegistry {
host_namespace: "docker.io".into(),
server_addr: "https://registry.example.com".into(),
Expand All @@ -444,7 +567,6 @@ version = 3
},
);

// Run containerd configuration
let result = containerd.run().await;
if let Err(e) = &result {
println!("Error: {e:?}");
Expand All @@ -454,7 +576,6 @@ version = 3
}
assert!(result.is_ok());

// Verify the hosts.toml file content
let hosts_file_path = certs_dir.join("docker.io").join("hosts.toml");
let contents = fs::read_to_string(&hosts_file_path).await.unwrap();

Expand Down
Loading