mirror of
https://github.com/kata-containers/kata-containers.git
synced 2025-09-17 14:58:16 +00:00
runtime-rs: Introduce coco_data dir and initdata block
Implement resource storage infrastructure with initial initdata support: 1. Create dedicated `coco_data` directory for: - Centralized management of CoCo resources; - Future expansion of CoCo artifacts; 2. Atomic initdata block as foundational component in `coco_data`, it will implement creation of compressed initdata blocks with: - Gzip compression with level customization (0-9) - Sector-aligned (512B) image format with magic header - Adaptive buffering (4KB-128KB) based on payload size - Temp-file atomic writes with 0o600 permissions Signed-off-by: alex.lyn <alex.lyn@antgroup.com>
This commit is contained in:
1
src/runtime-rs/Cargo.lock
generated
1
src/runtime-rs/Cargo.lock
generated
@@ -3610,6 +3610,7 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"byte-unit",
|
||||
"cgroups-rs 0.3.5",
|
||||
"flate2",
|
||||
"futures 0.3.28",
|
||||
"hypervisor",
|
||||
"inotify",
|
||||
|
@@ -35,6 +35,8 @@ uuid = { version = "0.4", features = ["v4"] }
|
||||
oci-spec = { workspace = true }
|
||||
inotify = "0.11.0"
|
||||
walkdir = "2.5.0"
|
||||
flate2 = { version = "1.0", features = ["zlib"] }
|
||||
tempfile = "3.19.1"
|
||||
|
||||
## Dependencies from `rust-netlink`
|
||||
netlink-packet-route = "0.22"
|
||||
|
19
src/runtime-rs/crates/resource/src/coco_data/initdata.rs
Normal file
19
src/runtime-rs/crates/resource/src/coco_data/initdata.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2025 Ant Group
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use hypervisor::BlockConfig;
|
||||
|
||||
/// The path /run/kata-containers/shared/initdata, combined with the sandbox ID,
|
||||
/// will form the directory for storing the initdata image.
|
||||
/// Path::new(KATA_SHARED_INIT_DATA_PATH).join(SID)
|
||||
pub const KATA_SHARED_INIT_DATA_PATH: &str = "/run/kata-containers/shared/initdata";
|
||||
|
||||
/// kata initdata image
|
||||
pub const KATA_INIT_DATA_IMAGE: &str = "initdata.image";
|
||||
|
||||
/// InitDataConfig which is a tuple of Block Device Config and its digest of the encoded
|
||||
/// string included in the disk. And, both of them will come up at the same time.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InitDataConfig(pub BlockConfig, pub String);
|
335
src/runtime-rs/crates/resource/src/coco_data/initdata_block.rs
Normal file
335
src/runtime-rs/crates/resource/src/coco_data/initdata_block.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
// Copyright (c) 2025 Ant Group
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use flate2::{Compression, GzBuilder};
|
||||
use std::{
|
||||
fmt, fs,
|
||||
io::{self, BufWriter, Seek, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InitDataError {
|
||||
InvalidPath(PathBuf),
|
||||
IoError(String, io::Error),
|
||||
CompressionError(io::Error),
|
||||
PersistError(tempfile::PersistError),
|
||||
}
|
||||
|
||||
impl fmt::Display for InitDataError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidPath(p) => write!(f, "Invalid path: {}", p.display()),
|
||||
Self::IoError(ctx, e) => write!(f, "I/O error during {}: {}", ctx, e),
|
||||
Self::CompressionError(e) => write!(f, "Compression failed: {}", e),
|
||||
Self::PersistError(e) => write!(f, "File persistence failed: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InitDataError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::IoError(_, e) => Some(e),
|
||||
Self::CompressionError(e) => Some(e),
|
||||
Self::PersistError(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for InitDataError {
|
||||
fn from(err: io::Error) -> Self {
|
||||
InitDataError::IoError("I/O operation".into(), err)
|
||||
}
|
||||
}
|
||||
|
||||
const MAGIC_HEADER: &[u8; 8] = b"initdata";
|
||||
const SECTOR_SIZE: u64 = 512;
|
||||
|
||||
// Default buffer size, adjustable based on target storage optimization
|
||||
const DEFAULT_BUFFER_SIZE: usize = 128 * 1024;
|
||||
|
||||
/// Determines the optimal buffer size
|
||||
fn determine_buffer_size(data_size: usize) -> usize {
|
||||
// Use smaller buffers for small data to reduce memory usage
|
||||
if data_size < 4 * 1024 {
|
||||
return 4 * 1024;
|
||||
} else if data_size < 64 * 1024 {
|
||||
return 32 * 1024;
|
||||
}
|
||||
// Use larger buffers for big data to improve throughput
|
||||
DEFAULT_BUFFER_SIZE
|
||||
}
|
||||
|
||||
/// create compressed block compliant with RAW format requirements
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `initdata`: Initialization data to be compressed and stored (TOML/JSON format, etc.)
|
||||
/// - `image_path`: Target image file path
|
||||
/// - `compression_level`: Compression level (0-9, default maximum compression)
|
||||
///
|
||||
/// # Returns
|
||||
/// - `Ok(file_size)`: Total bytes written to the image file on success
|
||||
/// - `Err(InitDataError)`: Error details on failure
|
||||
///
|
||||
/// # Safety
|
||||
/// - Atomic writes ensure crash recovery
|
||||
/// - Automatic temporary file cleanup
|
||||
/// - File permissions restricted to 0o600 on Unix systems
|
||||
fn create_compressed_block(
|
||||
initdata: &str,
|
||||
image_path: &Path,
|
||||
compression_level: Option<u32>,
|
||||
) -> Result<u64, InitDataError> {
|
||||
// 1. Skip file creation if initdata is empty
|
||||
if initdata.is_empty() {
|
||||
info!(
|
||||
sl!(),
|
||||
"No initialization data provided, skipping image creation for {}",
|
||||
image_path.display()
|
||||
);
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
// Store initdata size for logging and optimization
|
||||
let initdata_size = initdata.len();
|
||||
info!(
|
||||
sl!(),
|
||||
"Processing {} bytes of initialization data", initdata_size
|
||||
);
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent_dir) = image_path.parent() {
|
||||
if !parent_dir.exists() {
|
||||
info!(sl!(), "Creating parent directory: {}", parent_dir.display());
|
||||
fs::create_dir_all(parent_dir).map_err(|e| {
|
||||
InitDataError::IoError(format!("creating directory {}", parent_dir.display()), e)
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
return Err(InitDataError::InvalidPath(image_path.to_owned()));
|
||||
}
|
||||
|
||||
// 2. Determine optimal buffer size based on data size
|
||||
let buffer_size = determine_buffer_size(initdata_size);
|
||||
info!(sl!(), "Using buffer size of {} bytes", buffer_size);
|
||||
|
||||
// 3. Create temp file in parent directory (ensures atomic rename)
|
||||
let parent_dir = image_path
|
||||
.parent()
|
||||
.ok_or_else(|| InitDataError::InvalidPath(image_path.to_owned()))?;
|
||||
|
||||
info!(
|
||||
sl!(),
|
||||
"Creating temporary file in: {}",
|
||||
parent_dir.display()
|
||||
);
|
||||
|
||||
// Using named temporary files offers crucial benefits for writing data:
|
||||
// - It ensures atomic operations by renaming the file only on successful completion;
|
||||
// - It prevents concurrent conflicts through unique naming;
|
||||
// - And it guarantees reliable atomic renames by creating the temporary file in the same directory as the target.
|
||||
let temp_file = NamedTempFile::new_in(parent_dir).map_err(|e| {
|
||||
InitDataError::IoError(format!("creating temp file in {}", parent_dir.display()), e)
|
||||
})?;
|
||||
|
||||
info!(
|
||||
sl!(),
|
||||
"Temporary file created: {}",
|
||||
temp_file.path().display()
|
||||
);
|
||||
|
||||
// 4. Set strict file permissions
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = temp_file.as_file().metadata()?.permissions();
|
||||
perms.set_mode(0o600); // User read/write permissions
|
||||
temp_file.as_file().set_permissions(perms)?;
|
||||
}
|
||||
|
||||
// 5. Create buffered writer
|
||||
let mut writer = BufWriter::with_capacity(buffer_size, temp_file);
|
||||
|
||||
// 6. Write magic header
|
||||
writer.write_all(MAGIC_HEADER)?;
|
||||
info!(sl!(), "Magic header written: {:?}", MAGIC_HEADER);
|
||||
|
||||
// 7. Configure compression level and initialize GZ writer
|
||||
let compression =
|
||||
compression_level.map_or(Compression::best(), |lvl| Compression::new(lvl.min(9)));
|
||||
|
||||
info!(
|
||||
sl!(),
|
||||
"Compressing data with compression level {}",
|
||||
compression.level()
|
||||
);
|
||||
|
||||
let mut gz = GzBuilder::new()
|
||||
.filename("initdata.toml") // Embed original filename metadata
|
||||
.comment("Generated by Confidential Containers")
|
||||
.write(writer, compression);
|
||||
|
||||
// 8. Write data in chunks to avoid large memory allocation
|
||||
let mut bytes_written = 0;
|
||||
for chunk in initdata.as_bytes().chunks(buffer_size) {
|
||||
bytes_written += gz.write(chunk)?;
|
||||
}
|
||||
info!(sl!(), "written {} bytes", bytes_written);
|
||||
|
||||
// 9. Finalize compression and retrieve writer
|
||||
let mut writer = gz.finish()?;
|
||||
let compressed_size = writer.stream_position()?;
|
||||
|
||||
info!(
|
||||
sl!(),
|
||||
"Data compressed: {} -> {} bytes (ratio: {:.2}%)",
|
||||
initdata_size,
|
||||
compressed_size,
|
||||
(compressed_size as f64 / initdata_size as f64) * 100.0
|
||||
);
|
||||
|
||||
// 10. Calculate padding for sector alignment
|
||||
let current_pos = compressed_size;
|
||||
let padding = (SECTOR_SIZE - (current_pos % SECTOR_SIZE)) % SECTOR_SIZE;
|
||||
|
||||
// 11. Zero-byte padding using small blocks
|
||||
if padding > 0 {
|
||||
info!(
|
||||
sl!(),
|
||||
"Adding {} bytes of padding for sector alignment", padding
|
||||
);
|
||||
const ZERO_BLOCK: [u8; 4096] = [0; 4096];
|
||||
let mut remaining = padding as usize;
|
||||
|
||||
while remaining > 0 {
|
||||
let write_size = std::cmp::min(remaining, ZERO_BLOCK.len());
|
||||
writer.write_all(&ZERO_BLOCK[..write_size])?;
|
||||
remaining -= write_size;
|
||||
}
|
||||
}
|
||||
|
||||
// 12. Ensure data persistence
|
||||
writer
|
||||
.flush()
|
||||
.map_err(|e| InitDataError::IoError("flush buffer".into(), e))?;
|
||||
|
||||
// This extracts the NamedTempFile from the BufWriter.
|
||||
// Essentially, it unwraps the layered writers (compression, buffering) to get back the original temporary file (temp_file),
|
||||
// allowing direct operations like syncing or renaming.
|
||||
let original_tempfile = writer
|
||||
.into_inner()
|
||||
.map_err(|e| InitDataError::IoError("retrieving inner writer".into(), e.into()))?;
|
||||
|
||||
// 13. Ensure all data is written to storage
|
||||
original_tempfile.as_file().sync_all()?;
|
||||
|
||||
// 14. Atomic commit
|
||||
let final_size = original_tempfile.as_file().metadata()?.len();
|
||||
info!(
|
||||
sl!(),
|
||||
"Final image size: {} bytes, persisting to: {}",
|
||||
final_size,
|
||||
image_path.display()
|
||||
);
|
||||
|
||||
original_tempfile
|
||||
.persist(image_path)
|
||||
.map_err(InitDataError::PersistError)?;
|
||||
|
||||
Ok(final_size)
|
||||
}
|
||||
|
||||
/// Add data to a compressed image at the specified path
|
||||
pub fn push_data(initdata_path: &Path, data: &str) -> anyhow::Result<()> {
|
||||
let _ = fs::remove_file(initdata_path);
|
||||
let size = create_compressed_block(data, initdata_path, None)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create image: {}", e))?;
|
||||
info!(
|
||||
sl!(),
|
||||
"Create compressed image successfully, size {} bytes and created at {}",
|
||||
size,
|
||||
initdata_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Unit tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::env;
|
||||
use std::io::Read;
|
||||
|
||||
fn setup_test_env() -> PathBuf {
|
||||
let dir = env::temp_dir().join("initimg_test");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_creation() {
|
||||
let dir = setup_test_env();
|
||||
let path = dir.join("test.img");
|
||||
|
||||
let data = "[config]\nkey = \"value\"\n";
|
||||
let result = create_compressed_block(data, &path, Some(6));
|
||||
|
||||
assert!(result.is_ok());
|
||||
assert!(path.exists());
|
||||
|
||||
// Verify basic structure
|
||||
let meta = fs::metadata(&path).unwrap();
|
||||
assert_eq!(meta.len() % SECTOR_SIZE, 0);
|
||||
|
||||
// Verify magic header
|
||||
let mut file = fs::File::open(&path).unwrap();
|
||||
let mut header = [0u8; 8];
|
||||
file.read_exact(&mut header).unwrap();
|
||||
assert_eq!(&header, MAGIC_HEADER);
|
||||
|
||||
// Cleanup
|
||||
fs::remove_file(path).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_input() {
|
||||
let dir = setup_test_env();
|
||||
let path = dir.join("empty.img");
|
||||
|
||||
let result = create_compressed_block("", &path, None);
|
||||
|
||||
// Should succeed but return zero size
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), 0);
|
||||
// Should not create file
|
||||
assert!(!path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_different_compression_levels() {
|
||||
let dir = setup_test_env();
|
||||
let data = "[config]\n".repeat(1000); // Generate large test data
|
||||
|
||||
let sizes = vec![0, 3, 9]
|
||||
.into_iter()
|
||||
.map(|level| {
|
||||
let path = dir.join(format!("test_comp_{}.img", level));
|
||||
let res = create_compressed_block(&data, &path, Some(level));
|
||||
let size = res.unwrap();
|
||||
fs::remove_file(&path).unwrap();
|
||||
size
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Different compression levels should produce different sizes
|
||||
// Simple check due to data and environment variability
|
||||
println!("Compression level sizes: {:?}", sizes);
|
||||
assert!(sizes[0] > 0);
|
||||
}
|
||||
}
|
7
src/runtime-rs/crates/resource/src/coco_data/mod.rs
Normal file
7
src/runtime-rs/crates/resource/src/coco_data/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2025 Ant Group
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
pub mod initdata;
|
||||
pub mod initdata_block;
|
@@ -26,6 +26,7 @@ pub mod share_fs;
|
||||
pub mod volume;
|
||||
pub use manager::ResourceManager;
|
||||
pub mod cdi_devices;
|
||||
pub mod coco_data;
|
||||
pub mod cpu_mem;
|
||||
|
||||
use kata_types::config::hypervisor::SharedFsInfo;
|
||||
|
Reference in New Issue
Block a user