mirror of
https://github.com/kata-containers/kata-containers.git
synced 2026-07-01 14:38:33 +00:00
kata-deploy: implement selective tarball extraction in installer
Add zstd and tar as Rust dependencies and rewrite the artifact installation logic to extract only the component tarballs required by the enabled runtime classes. extract_component_tarballs reads shim-components.json to determine which kata-static-<name>.tar.zst files are needed for the selected shims and current architecture. Shared components (e.g. kernel, shim-v2-go) are listed by multiple shims and must only be unpacked once per install run. Deduplication is handled with an in-memory set passed through the call, avoiding any risk of stale on-disk state surviving across pod restarts. Within each tarball, opt/kata path prefixes are stripped and absolute symlink / hard-link targets are rewritten to point at the resolved installation directory, correctly handling MULTI_INSTALL_SUFFIX. Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
This commit is contained in:
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -3570,10 +3570,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_yaml 0.9.34+deprecated",
|
||||
"serial_test 0.10.0",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"toml_edit 0.22.27",
|
||||
"walkdir",
|
||||
"zstd 0.13.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4576,7 +4578,7 @@ dependencies = [
|
||||
"sha2 0.10.9",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"zstd",
|
||||
"zstd 0.11.2+zstd.1.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9611,7 +9613,16 @@ version = "0.11.2+zstd.1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
"zstd-safe 5.0.2+zstd.1.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe 7.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9624,6 +9635,15 @@ dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
|
||||
@@ -34,6 +34,7 @@ log.workspace = true
|
||||
regex.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_yaml = "0.9"
|
||||
tar = "0.4.45"
|
||||
tokio = { workspace = true, features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
@@ -45,6 +46,7 @@ tokio = { workspace = true, features = [
|
||||
] }
|
||||
toml_edit = "0.22"
|
||||
walkdir = "2"
|
||||
zstd = "0.13.3"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
|
||||
@@ -10,9 +10,11 @@ use crate::utils;
|
||||
use crate::utils::toml as toml_utils;
|
||||
use anyhow::{Context, Result};
|
||||
use log::info;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
#[cfg(test)]
|
||||
use walkdir::WalkDir;
|
||||
|
||||
/// All valid shims
|
||||
@@ -100,7 +102,8 @@ pub async fn install_artifacts(config: &Config, container_runtime: &str) -> Resu
|
||||
));
|
||||
}
|
||||
|
||||
copy_artifacts("/opt/kata-artifacts/opt/kata", &config.host_install_dir)?;
|
||||
let mut extracted: HashSet<String> = HashSet::new();
|
||||
extract_component_tarballs(config, &mut extracted)?;
|
||||
|
||||
set_executable_permissions(&config.host_install_dir)?;
|
||||
|
||||
@@ -318,12 +321,11 @@ fn remove_custom_runtime_configs(config: &Config) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Note: The src parameter is kept to allow for unit testing with temporary directories,
|
||||
/// even though in production it always uses /opt/kata-artifacts/opt/kata
|
||||
/// Copy an extracted artifact tree from `src` into `dst`.
|
||||
///
|
||||
/// Symlinks in the source tree are preserved at the destination (recreated as symlinks
|
||||
/// instead of copying the target file). Absolute targets under the source root are
|
||||
/// rewritten to the destination root so they remain valid.
|
||||
/// Used only by unit tests; production code now uses `extract_component_tarballs`.
|
||||
/// Symlinks are preserved; absolute targets under the source root are rewritten to dst.
|
||||
#[cfg(test)]
|
||||
fn copy_artifacts(src: &str, dst: &str) -> Result<()> {
|
||||
let src_path = Path::new(src);
|
||||
for entry in WalkDir::new(src).follow_links(false) {
|
||||
@@ -386,6 +388,281 @@ fn copy_artifacts(src: &str, dst: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Path to the shim-components.json manifest inside the kata-deploy container image.
|
||||
const SHIM_COMPONENTS_PATH: &str = "/opt/kata-artifacts/shim-components.json";
|
||||
|
||||
/// Directory inside the container image where individual component tarballs are stored.
|
||||
const TARBALLS_DIR: &str = "/opt/kata-artifacts/tarballs";
|
||||
|
||||
/// Common prefix stripped from tarball entries to get the install-relative path.
|
||||
const TAR_PREFIX: &str = "opt/kata";
|
||||
|
||||
/// Absolute install path embedded in the tarballs (used to rewrite absolute symlink targets).
|
||||
const TARBALL_ABS_PREFIX: &str = "/opt/kata";
|
||||
|
||||
/// Return the current architecture string as used in shim-components.json.
|
||||
fn current_arch() -> &'static str {
|
||||
match std::env::consts::ARCH {
|
||||
"x86_64" => "x86_64",
|
||||
"aarch64" => "aarch64",
|
||||
"s390x" => "s390x",
|
||||
"powerpc64" => "ppc64le",
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse shim-components.json and return the union of component tarball names
|
||||
/// required by all shims listed in `config.shims_for_arch` for the current arch.
|
||||
fn collect_required_tarballs(config: &Config) -> Result<HashSet<String>> {
|
||||
let arch = current_arch();
|
||||
let json_str = fs::read_to_string(SHIM_COMPONENTS_PATH)
|
||||
.with_context(|| format!("Failed to read {SHIM_COMPONENTS_PATH}"))?;
|
||||
let doc: serde_json::Value =
|
||||
serde_json::from_str(&json_str).context("Failed to parse shim-components.json")?;
|
||||
let shims_map = doc["shims"]
|
||||
.as_object()
|
||||
.ok_or_else(|| anyhow::anyhow!("shim-components.json is missing the 'shims' object"))?;
|
||||
|
||||
let mut required: HashSet<String> = HashSet::new();
|
||||
for shim in &config.shims_for_arch {
|
||||
match shims_map
|
||||
.get(shim.as_str())
|
||||
.and_then(|v| v.get(arch))
|
||||
.and_then(|v| v.as_array())
|
||||
{
|
||||
Some(tarballs) => {
|
||||
for t in tarballs {
|
||||
if let Some(name) = t.as_str() {
|
||||
required.insert(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
log::warn!(
|
||||
"Shim '{}' has no entry for architecture '{}' in shim-components.json; \
|
||||
no tarballs will be extracted for it",
|
||||
shim,
|
||||
arch
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(required)
|
||||
}
|
||||
|
||||
/// Extract a `.tar.zst` tarball into `dest_dir`, stripping the leading `opt/kata/` prefix
|
||||
/// from every entry so files land directly under `dest_dir`.
|
||||
///
|
||||
/// Absolute symlink targets that point into `/opt/kata` are rewritten to point into
|
||||
/// `dest_dir` instead, keeping symlinks valid for non-default installation prefixes.
|
||||
fn extract_tarball(tarball_path: &Path, dest_dir: &str) -> Result<()> {
|
||||
use std::path::Component;
|
||||
|
||||
let file = fs::File::open(tarball_path)
|
||||
.with_context(|| format!("Failed to open tarball: {}", tarball_path.display()))?;
|
||||
let decoder = zstd::Decoder::new(file).with_context(|| {
|
||||
format!(
|
||||
"Failed to create zstd decoder for: {}",
|
||||
tarball_path.display()
|
||||
)
|
||||
})?;
|
||||
let mut archive = tar::Archive::new(decoder);
|
||||
let dest_path = Path::new(dest_dir);
|
||||
|
||||
for entry_result in archive.entries()? {
|
||||
let mut entry = entry_result.context("Failed to read tar entry")?;
|
||||
let raw_path = entry
|
||||
.path()
|
||||
.context("Failed to get tar entry path")?
|
||||
.into_owned();
|
||||
|
||||
// Strip the "opt/kata" or "./opt/kata" prefix; skip anything else.
|
||||
let dot_slash_prefix = Path::new("./opt/kata");
|
||||
let stripped = if let Ok(p) = raw_path.strip_prefix(TAR_PREFIX) {
|
||||
p.to_path_buf()
|
||||
} else if let Ok(p) = raw_path.strip_prefix(dot_slash_prefix) {
|
||||
p.to_path_buf()
|
||||
} else {
|
||||
log::debug!(
|
||||
"Skipping entry without expected prefix: {}",
|
||||
raw_path.display()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
// The root "opt/kata/" directory itself → just ensure dest_dir exists.
|
||||
if stripped.as_os_str().is_empty() {
|
||||
fs::create_dir_all(dest_path)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reject path traversal attempts.
|
||||
for component in stripped.components() {
|
||||
if component == Component::ParentDir {
|
||||
anyhow::bail!(
|
||||
"Tarball {} contains path traversal in entry: {}",
|
||||
tarball_path.display(),
|
||||
raw_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let dest_entry = dest_path.join(&stripped);
|
||||
let entry_type = entry.header().entry_type();
|
||||
|
||||
if entry_type.is_dir() {
|
||||
fs::create_dir_all(&dest_entry)
|
||||
.with_context(|| format!("Failed to create directory: {}", dest_entry.display()))?;
|
||||
} else if entry_type.is_symlink() {
|
||||
let link_target = entry
|
||||
.header()
|
||||
.link_name()?
|
||||
.ok_or_else(|| anyhow::anyhow!("Symlink has no link name: {}", raw_path.display()))?
|
||||
.into_owned();
|
||||
|
||||
// Rewrite absolute symlinks that pointed into /opt/kata so they point into dest_dir.
|
||||
let final_target: std::path::PathBuf = if link_target.is_absolute() {
|
||||
if let Ok(rel) = link_target.strip_prefix(TARBALL_ABS_PREFIX) {
|
||||
dest_path.join(rel)
|
||||
} else {
|
||||
link_target
|
||||
}
|
||||
} else {
|
||||
link_target
|
||||
};
|
||||
|
||||
if let Some(parent) = dest_entry.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
match fs::remove_file(&dest_entry) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
std::os::unix::fs::symlink(&final_target, &dest_entry).with_context(|| {
|
||||
format!(
|
||||
"Failed to create symlink {} -> {}",
|
||||
dest_entry.display(),
|
||||
final_target.display()
|
||||
)
|
||||
})?;
|
||||
} else if entry_type.is_hard_link() {
|
||||
let link_target = entry
|
||||
.header()
|
||||
.link_name()?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Hard link has no link name: {}", raw_path.display())
|
||||
})?
|
||||
.into_owned();
|
||||
|
||||
// Strip the prefix from the hard link target as well.
|
||||
let link_stripped = if let Ok(p) = link_target.strip_prefix(TAR_PREFIX) {
|
||||
dest_path.join(p)
|
||||
} else if let Ok(p) = link_target.strip_prefix(dot_slash_prefix) {
|
||||
dest_path.join(p)
|
||||
} else {
|
||||
dest_path.join(&link_target)
|
||||
};
|
||||
|
||||
if let Some(parent) = dest_entry.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
match fs::remove_file(&dest_entry) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
fs::hard_link(&link_stripped, &dest_entry).with_context(|| {
|
||||
format!(
|
||||
"Failed to create hard link {} -> {}",
|
||||
dest_entry.display(),
|
||||
link_stripped.display()
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
// Regular file
|
||||
if let Some(parent) = dest_entry.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
match fs::remove_file(&dest_entry) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
entry
|
||||
.unpack(&dest_entry)
|
||||
.with_context(|| format!("Failed to unpack entry to: {}", dest_entry.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select and extract the component tarballs required for the configured shims.
|
||||
///
|
||||
/// Shared components (e.g. kernel, shim-v2-go) are listed by multiple shims.
|
||||
/// The `extracted` set tracks which components have already been unpacked in
|
||||
/// this install run so that shared components are only extracted once.
|
||||
/// Using an in-memory set (rather than on-disk markers) avoids any risk of
|
||||
/// stale state surviving across pod restarts.
|
||||
fn extract_component_tarballs(config: &Config, extracted: &mut HashSet<String>) -> Result<()> {
|
||||
let required = collect_required_tarballs(config)?;
|
||||
|
||||
if required.is_empty() {
|
||||
log::warn!(
|
||||
"No component tarballs required for the configured shims on '{}'; \
|
||||
check shim-components.json",
|
||||
current_arch()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"Component tarballs required for shims [{}]: {:?}",
|
||||
config.shims_for_arch.join(", "),
|
||||
{
|
||||
let mut sorted: Vec<_> = required.iter().collect();
|
||||
sorted.sort();
|
||||
sorted
|
||||
}
|
||||
);
|
||||
|
||||
let mut sorted_components: Vec<_> = required.iter().collect();
|
||||
sorted_components.sort();
|
||||
|
||||
for component in sorted_components {
|
||||
if extracted.contains(component.as_str()) {
|
||||
info!("Component '{}' already extracted, skipping", component);
|
||||
continue;
|
||||
}
|
||||
|
||||
let tarball_name = format!("kata-static-{}.tar.zst", component);
|
||||
let tarball_path = Path::new(TARBALLS_DIR).join(&tarball_name);
|
||||
|
||||
if !tarball_path.exists() {
|
||||
anyhow::bail!(
|
||||
"Required component tarball not found: {}. \
|
||||
Ensure the kata-deploy image was built with the '{}' component.",
|
||||
tarball_path.display(),
|
||||
component
|
||||
);
|
||||
}
|
||||
|
||||
info!("Extracting component '{}'", component);
|
||||
extract_tarball(&tarball_path, &config.host_install_dir).with_context(|| {
|
||||
format!(
|
||||
"Failed to extract component '{}' from {}",
|
||||
component,
|
||||
tarball_path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
extracted.insert(component.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_executable_permissions(dir: &str) -> Result<()> {
|
||||
let bin_paths = ["bin", "runtime-rs/bin"];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user