Files
herolib/lib/virt/heropods/container_image.v
Mahmoud-Emad ad7e1980a5 refactor: Integrate logger and refactor network operations
- Replace console logging with logger.log calls
- Improve network bridge creation robustness
- Enhance network IP allocation and cleanup logic
- Refactor network cleanup for better concurrency handling
2025-11-12 11:28:56 +02:00

370 lines
10 KiB
V

module heropods
import incubaid.herolib.osal.core as osal
import incubaid.herolib.core.texttools
import os
// ContainerImage represents a container base image with its rootfs
//
// Thread Safety:
// Image operations are filesystem-based and don't interact with network_config,
// so no special thread safety considerations are needed.
@[heap]
pub struct ContainerImage {
pub mut:
image_name string @[required] // Image name (located in ${self.factory.base_dir}/images/<image_name>/rootfs)
docker_url string // Optional Docker registry URL
rootfs_path string // Path to the extracted rootfs
size_mb f64 // Size in MB
created_at string // Creation timestamp
factory &HeroPods @[skip; str: skip] // Reference to parent HeroPods instance
}
// ContainerImageArgs defines parameters for creating/managing container images
@[params]
pub struct ContainerImageArgs {
pub mut:
image_name string @[required] // Unique image name (located in ${self.factory.base_dir}/images/<image_name>/rootfs)
docker_url string // Docker image URL like "alpine:3.20" or "ubuntu:24.04"
reset bool // Reset if image already exists
}
// ImageExportArgs defines parameters for exporting an image
@[params]
pub struct ImageExportArgs {
pub mut:
dest_path string @[required] // Destination .tgz file path
compress_level int = 6 // Compression level 1-9
}
// ImageImportArgs defines parameters for importing an image
@[params]
pub struct ImageImportArgs {
pub mut:
source_path string @[required] // Source .tgz file path
reset bool // Overwrite if exists
}
// Create a new image or get existing image
//
// This method:
// 1. Normalizes the image name
// 2. Returns existing image if found (unless reset=true)
// 3. Downloads image from Docker registry if docker_url provided
// 4. Creates image metadata and stores in cache
//
// Thread Safety:
// Image operations are filesystem-based and don't interact with network_config.
pub fn (mut self HeroPods) image_new(args ContainerImageArgs) !&ContainerImage {
mut image_name := texttools.name_fix(args.image_name)
rootfs_path := '${self.base_dir}/images/${image_name}/rootfs'
// Check if image already exists
if image_name in self.images && !args.reset {
return self.images[image_name] or { panic('bug') }
}
// Ensure podman is installed
if !osal.cmd_exists('podman') {
return error('Podman is required for image management. Please install podman first.')
}
mut image := &ContainerImage{
image_name: image_name
docker_url: args.docker_url
rootfs_path: rootfs_path
factory: &self
}
// If docker_url is provided, download and extract the image
if args.docker_url != '' {
image.download_from_docker(args.docker_url, args.reset)!
} else {
// Check if rootfs directory exists
if !os.is_dir(rootfs_path) {
return error('Image rootfs not found at ${rootfs_path} and no docker_url provided')
}
}
// Update image metadata
image.update_metadata()!
self.images[image_name] = image
return image
}
// Download image from Docker registry using podman
//
// This method:
// 1. Pulls the image from Docker registry
// 2. Creates a temporary container
// 3. Exports the rootfs to the images directory
// 4. Cleans up the temporary container
fn (mut self ContainerImage) download_from_docker(docker_url string, reset bool) ! {
self.factory.logger.log(
cat: 'images'
log: 'Downloading image: ${docker_url}'
logtype: .stdout
) or {}
// Clean image name for local storage
image_dir := '${self.factory.base_dir}/images/${self.image_name}'
// Remove existing if reset is true
if reset && os.is_dir(image_dir) {
osal.exec(cmd: 'rm -rf ${image_dir}', stdout: false)!
}
// Create image directory
osal.exec(cmd: 'mkdir -p ${image_dir}', stdout: false)!
// Pull image using podman
self.factory.logger.log(
cat: 'images'
log: 'Pulling image: ${docker_url}'
logtype: .stdout
) or {}
osal.exec(cmd: 'podman pull ${docker_url}', stdout: true)!
// Create container from image (without running it)
temp_container := 'temp_${self.image_name}_extract'
osal.exec(cmd: 'podman create --name ${temp_container} ${docker_url}', stdout: false)!
// Export container filesystem
tar_file := '${image_dir}/rootfs.tar'
osal.exec(cmd: 'podman export ${temp_container} -o ${tar_file}', stdout: true)!
// Extract to rootfs directory
osal.exec(cmd: 'mkdir -p ${self.rootfs_path}', stdout: false)!
osal.exec(cmd: 'tar -xf ${tar_file} -C ${self.rootfs_path}', stdout: true)!
// Clean up temporary container and tar file
osal.exec(cmd: 'podman rm ${temp_container}', stdout: false) or {}
osal.exec(cmd: 'rm -f ${tar_file}', stdout: false) or {}
// Remove the pulled image from podman to save space (optional)
osal.exec(cmd: 'podman rmi ${docker_url}', stdout: false) or {}
self.factory.logger.log(
cat: 'images'
log: 'Image ${docker_url} extracted to ${self.rootfs_path}'
logtype: .stdout
) or {}
}
// Update image metadata (size, creation time, etc.)
//
// Calculates the rootfs size and records creation timestamp
fn (mut self ContainerImage) update_metadata() ! {
if !os.is_dir(self.rootfs_path) {
return error('Rootfs path does not exist: ${self.rootfs_path}')
}
// Calculate size in MB
result := osal.exec(cmd: 'du -sm ${self.rootfs_path}', stdout: false)!
result_parts := result.output.split_by_space()[0] or { panic('bug') }
size_str := result_parts.trim_space()
self.size_mb = size_str.f64()
// Get creation time
info := os.stat(self.rootfs_path) or { return error('stat failed: ${err}') }
self.created_at = info.ctime.str()
}
// List all available images
//
// Scans the images directory and returns all found images with metadata
pub fn (mut self HeroPods) images_list() ![]&ContainerImage {
mut images := []&ContainerImage{}
images_base_dir := '${self.base_dir}/images'
if !os.is_dir(images_base_dir) {
return images
}
// Scan for image directories
dirs := os.ls(images_base_dir)!
for dir in dirs {
full_path := '${images_base_dir}/${dir}'
if os.is_dir(full_path) {
rootfs_path := '${full_path}/rootfs'
if os.is_dir(rootfs_path) {
// Create image object if not in cache
if dir !in self.images {
mut image := &ContainerImage{
image_name: dir
rootfs_path: rootfs_path
factory: &self
}
image.update_metadata() or {
self.logger.log(
cat: 'images'
log: 'Failed to update metadata for image ${dir}: ${err}'
logtype: .error
) or {}
continue
}
self.images[dir] = image
}
images << self.images[dir] or { panic('bug') }
}
}
}
return images
}
// Export image to .tgz file
//
// Creates a compressed tarball of the image rootfs
pub fn (mut self ContainerImage) export(args ImageExportArgs) ! {
if !os.is_dir(self.rootfs_path) {
return error('Image rootfs not found: ${self.rootfs_path}')
}
self.factory.logger.log(
cat: 'images'
log: 'Exporting image ${self.image_name} to ${args.dest_path}'
logtype: .stdout
) or {}
// Ensure destination directory exists
dest_dir := os.dir(args.dest_path)
osal.exec(cmd: 'mkdir -p ${dest_dir}', stdout: false)!
// Create compressed archive
cmd := 'tar -czf ${args.dest_path} -C ${os.dir(self.rootfs_path)} ${os.base(self.rootfs_path)}'
osal.exec(cmd: cmd, stdout: true)!
self.factory.logger.log(
cat: 'images'
log: 'Image exported successfully to ${args.dest_path}'
logtype: .stdout
) or {}
}
// Import image from .tgz file
//
// Extracts a compressed tarball into the images directory and creates image metadata
pub fn (mut self HeroPods) image_import(args ImageImportArgs) !&ContainerImage {
if !os.exists(args.source_path) {
return error('Source file not found: ${args.source_path}')
}
// Extract image name from filename
filename := os.base(args.source_path)
image_name := filename.replace('.tgz', '').replace('.tar.gz', '')
image_name_clean := texttools.name_fix(image_name)
self.logger.log(
cat: 'images'
log: 'Importing image from ${args.source_path}'
logtype: .stdout
) or {}
image_dir := '${self.base_dir}/images/${image_name_clean}'
rootfs_path := '${image_dir}/rootfs'
// Check if image already exists
if os.is_dir(rootfs_path) && !args.reset {
return error('Image ${image_name_clean} already exists. Use reset=true to overwrite.')
}
// Remove existing if reset
if args.reset && os.is_dir(image_dir) {
osal.exec(cmd: 'rm -rf ${image_dir}', stdout: false)!
}
// Create directories
osal.exec(cmd: 'mkdir -p ${image_dir}', stdout: false)!
// Extract archive
osal.exec(cmd: 'tar -xzf ${args.source_path} -C ${image_dir}', stdout: true)!
// Create image object
mut image := &ContainerImage{
image_name: image_name_clean
rootfs_path: rootfs_path
factory: &self
}
image.update_metadata()!
self.images[image_name_clean] = image
self.logger.log(
cat: 'images'
log: 'Image imported successfully: ${image_name_clean}'
logtype: .stdout
) or {}
return image
}
// Delete image
//
// Removes the image directory and removes from factory cache
pub fn (mut self ContainerImage) delete() ! {
self.factory.logger.log(
cat: 'images'
log: 'Deleting image: ${self.image_name}'
logtype: .stdout
) or {}
image_dir := os.dir(self.rootfs_path)
if os.is_dir(image_dir) {
osal.exec(cmd: 'rm -rf ${image_dir}', stdout: true)!
}
// Remove from factory cache
if self.image_name in self.factory.images {
self.factory.images.delete(self.image_name)
}
self.factory.logger.log(
cat: 'images'
log: 'Image ${self.image_name} deleted successfully'
logtype: .stdout
) or {}
}
// Get image info as map
//
// Returns image metadata as a string map for display/serialization
pub fn (self ContainerImage) info() map[string]string {
return {
'name': self.image_name
'docker_url': self.docker_url
'rootfs_path': self.rootfs_path
'size_mb': self.size_mb.str()
'created_at': self.created_at
}
}
// List available Docker images that can be downloaded
//
// Returns a curated list of commonly used Docker images
pub fn list_available_docker_images() []string {
return [
'alpine:3.20',
'alpine:3.19',
'alpine:latest',
'ubuntu:24.04',
'ubuntu:22.04',
'ubuntu:20.04',
'ubuntu:latest',
'debian:12',
'debian:11',
'debian:latest',
'fedora:39',
'fedora:38',
'fedora:latest',
'archlinux:latest',
'centos:stream9',
'rockylinux:9',
'nginx:alpine',
'redis:alpine',
'postgres:15-alpine',
'node:20-alpine',
'python:3.12-alpine',
]
}