diff --git a/examples/virt/heropods/heropods.vsh b/examples/virt/heropods/heropods.vsh index 7fc57890..aafbdd12 100755 --- a/examples/virt/heropods/heropods.vsh +++ b/examples/virt/heropods/heropods.vsh @@ -13,7 +13,7 @@ println('=== HeroPods Refactored API Demo ===') // Step 1: factory.new() now only creates a container definition/handle // It does NOT create the actual container in the backend yet mut container := factory.new( - name: 'myalpine' + name: 'demo_alpine' image: .custom custom_image_name: 'alpine_3_20' docker_url: 'docker.io/library/alpine:3.20' diff --git a/examples/virt/heropods/runcommands.vsh b/examples/virt/heropods/runcommands.vsh index f54fc43b..9352cc80 100644 --- a/examples/virt/heropods/runcommands.vsh +++ b/examples/virt/heropods/runcommands.vsh @@ -8,7 +8,7 @@ mut factory := heropods.new( ) or { panic('Failed to init ContainerFactory: ${err}') } mut container := factory.new( - name: 'myalpine' + name: 'alpine_demo' image: .custom custom_image_name: 'alpine_3_20' docker_url: 'docker.io/library/alpine:3.20' diff --git a/lib/builder/executor_crun.v b/lib/builder/executor_crun.v index 990c5d0a..7d38504b 100644 --- a/lib/builder/executor_crun.v +++ b/lib/builder/executor_crun.v @@ -11,13 +11,22 @@ import freeflowuniverse.herolib.core.texttools pub struct ExecutorCrun { pub mut: container_id string // container ID for crun + crun_root string // custom crun root directory retry int = 1 debug bool = true } +// Helper method to get crun command with custom root +fn (executor ExecutorCrun) crun_cmd(cmd string) string { + if executor.crun_root != '' { + return 'crun --root ${executor.crun_root} ${cmd}' + } + return 'crun ${cmd}' +} + pub fn (mut executor ExecutorCrun) init() ! { // Verify container exists and is running - result := osal.exec(cmd: 'crun state ${executor.container_id}', stdout: false) or { + result := osal.exec(cmd: executor.crun_cmd('state ${executor.container_id}'), stdout: false) or { return error('Container ${executor.container_id} not found or not accessible') } @@ -41,7 +50,7 @@ pub fn (mut executor ExecutorCrun) exec(args_ ExecArgs) !string { console.print_debug('execute in container ${executor.container_id}: ${args.cmd}') } - mut cmd := 'crun exec ${executor.container_id} ${args.cmd}' + mut cmd := executor.crun_cmd('exec ${executor.container_id} ${args.cmd}') if args.cmd.contains('\n') { // For multiline commands, write to temp file first temp_script := '/tmp/crun_script_${rand.uuid_v4()}.sh' @@ -50,7 +59,7 @@ pub fn (mut executor ExecutorCrun) exec(args_ ExecArgs) !string { // Copy script into container and execute executor.file_write('/tmp/exec_script.sh', script_content)! - cmd = 'crun exec ${executor.container_id} bash /tmp/exec_script.sh' + cmd = executor.crun_cmd('exec ${executor.container_id} bash /tmp/exec_script.sh') } res := osal.exec(cmd: cmd, stdout: args.stdout, debug: executor.debug)! @@ -66,7 +75,7 @@ pub fn (mut executor ExecutorCrun) exec_interactive(args_ ExecArgs) ! { args.cmd = 'bash /tmp/interactive_script.sh' } - cmd := 'crun exec -t ${executor.container_id} ${args.cmd}' + cmd := executor.crun_cmd('exec -t ${executor.container_id} ${args.cmd}') console.print_debug(cmd) osal.execute_interactive(cmd)! } @@ -82,7 +91,8 @@ pub fn (mut executor ExecutorCrun) file_write(path string, text string) ! { defer { os.rm(temp_file) or {} } // Use crun exec to copy file content - cmd := 'cat ${temp_file} | crun exec -i ${executor.container_id} tee ${path} > /dev/null' + sbcmd := executor.crun_cmd('exec -i ${executor.container_id} tee ${path}') + cmd := 'cat ${temp_file} | ${sbcmd} > /dev/null' osal.exec(cmd: cmd, stdout: false)! } diff --git a/lib/osal/core/package_test.v b/lib/osal/core/package_test.v index 3ad6935f..a968df0c 100644 --- a/lib/osal/core/package_test.v +++ b/lib/osal/core/package_test.v @@ -3,33 +3,33 @@ module core import freeflowuniverse.herolib.core fn test_package_management() { - // platform_ := core.platform()! + platform_ := core.platform()! - // if platform_ == .osx { - // // Check if brew is installed - // if !cmd_exists('brew') { - // eprintln('WARNING: Homebrew is not installed. Please install it to run package management tests on OSX.') - // return - // } - // } + if platform_ == .osx { + // Check if brew is installed + if !cmd_exists('brew') { + eprintln('WARNING: Homebrew is not installed. Please install it to run package management tests on OSX.') + return + } + } - // is_wget_installed := cmd_exists('wget') + is_wget_installed := cmd_exists('wget') - // if is_wget_installed { - // // Clean up - remove wget - // package_remove('wget') or { assert false, 'Failed to remove wget: ${err}' } - // assert !cmd_exists('wget') - // // Reinstalling wget as it was previously installed - // package_install('wget') or { assert false, 'Failed to install wget: ${err}' } - // assert cmd_exists('wget') - // return - // } + if is_wget_installed { + // Clean up - remove wget + package_remove('wget') or { assert false, 'Failed to remove wget: ${err}' } + assert !cmd_exists('wget') + // Reinstalling wget as it was previously installed + package_install('wget') or { assert false, 'Failed to install wget: ${err}' } + assert cmd_exists('wget') + return + } - // // Intstall wget and verify it is installed - // package_install('wget') or { assert false, 'Failed to install wget: ${err}' } - // assert cmd_exists('wget') + // Intstall wget and verify it is installed + package_install('wget') or { assert false, 'Failed to install wget: ${err}' } + assert cmd_exists('wget') - // // Clean up - remove wget - // package_remove('wget') or { assert false, 'Failed to remove wget: ${err}' } - // assert !cmd_exists('wget') + // Clean up - remove wget + package_remove('wget') or { assert false, 'Failed to remove wget: ${err}' } + assert !cmd_exists('wget') } diff --git a/lib/virt/crun/example.v b/lib/virt/crun/example.v index 8be266a9..e6ab984a 100644 --- a/lib/virt/crun/example.v +++ b/lib/virt/crun/example.v @@ -1,31 +1,31 @@ module crun - pub fn example_heropods_compatible() ! { mut configs := map[string]&CrunConfig{} // Create a container configuration compatible with heropods template mut config := new(mut configs, name: 'heropods-example')! - - // Configure to match the template + + // Configure to match the template - disable terminal for background containers + config.set_terminal(false) config.set_command(['/bin/sh']) - .set_working_dir('/') - .set_user(0, 0, []) - .add_env('PATH', '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') - .add_env('TERM', 'xterm') - .set_rootfs('${rootfs_path}', false) // This will be replaced by the actual path - .set_hostname('container') - .set_no_new_privileges(true) - + config.set_working_dir('/') + config.set_user(0, 0, []) + config.add_env('PATH', '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') + config.add_env('TERM', 'xterm') + config.set_rootfs('/tmp/rootfs', false) // This will be replaced by the actual path + config.set_hostname('container') + config.set_no_new_privileges(true) + // Add the specific rlimit from template config.add_rlimit(.rlimit_nofile, 1024, 1024) - + // Validate the configuration config.validate()! - + // Generate and print JSON json_output := config.to_json()! println(json_output) - + // Save to file config.save_to_file('/tmp/heropods_config.json')! println('Heropods-compatible configuration saved to /tmp/heropods_config.json') @@ -35,33 +35,34 @@ pub fn example_custom() ! { mut configs := map[string]&CrunConfig{} // Create a more complex container configuration mut config := new(mut configs, name: 'custom-container')! - + config.set_command(['/usr/bin/my-app', '--config', '/etc/myapp/config.yaml']) - .set_working_dir('/app') - .set_user(1000, 1000, [1001, 1002]) - .add_env('MY_VAR', 'my_value') - .add_env('ANOTHER_VAR', 'another_value') - .set_rootfs('/path/to/rootfs', false) - .set_hostname('my-custom-container') - .set_memory_limit(1024 * 1024 * 1024) // 1GB - .set_cpu_limits(100000, 50000, 1024) // period, quota, shares - .set_pids_limit(500) - .add_mount('/host/path', '/container/path', .bind, [.rw]) - .add_mount('/tmp/cache', '/app/cache', .tmpfs, [.rw, .noexec]) - .add_capability(.cap_sys_admin) - .remove_capability(.cap_net_raw) - .add_rlimit(.rlimit_nproc, 100, 50) - .set_no_new_privileges(true) - + config.set_working_dir('/app') + config.set_user(1000, 1000, [1001, 1002]) + config.add_env('MY_VAR', 'my_value') + config.add_env('ANOTHER_VAR', 'another_value') + config.set_rootfs('/path/to/rootfs', false) + config.set_hostname('my-custom-container') + config.set_memory_limit(1024 * 1024 * 1024) // 1GB + config.set_cpu_limits(100000, 50000, 1024) // period, quota, shares + config.set_pids_limit(500) + config.add_mount('/host/path', '/container/path', .bind, [.rw]) + config.add_mount('/tmp/cache', '/app/cache', .tmpfs, [.rw, .noexec]) + config.add_capability(.cap_sys_admin) + config.remove_capability(.cap_net_raw) + config.add_rlimit(.rlimit_nproc, 100, 50) + config.set_no_new_privileges(true) + // Add some additional security hardening + config.add_masked_path('/proc/kcore') - .add_readonly_path('/proc/sys') - + config.add_readonly_path('/proc/sys') + // Validate before use config.validate()! - + // Get the JSON json_str := config.to_json()! println('Custom container config:') println(json_str) -} \ No newline at end of file +} diff --git a/lib/virt/crun/factory.v b/lib/virt/crun/factory.v index e93b3fde..8fdc99b3 100644 --- a/lib/virt/crun/factory.v +++ b/lib/virt/crun/factory.v @@ -2,11 +2,10 @@ module crun import freeflowuniverse.herolib.core.texttools - @[params] pub struct FactoryArgs { pub mut: - name string = "default" + name string = 'default' } pub struct CrunConfig { @@ -23,6 +22,8 @@ pub fn (mount_type MountType) to_string() string { .proc { 'proc' } .sysfs { 'sysfs' } .devpts { 'devpts' } + .mqueue { 'mqueue' } + .cgroup { 'cgroup' } .nfs { 'nfs' } .overlay { 'overlay' } } @@ -120,21 +121,23 @@ pub fn (mut config CrunConfig) set_working_dir(cwd string) &CrunConfig { pub fn (mut config CrunConfig) set_user(uid u32, gid u32, additional_gids []u32) &CrunConfig { config.spec.process.user = User{ - uid: uid - gid: gid + uid: uid + gid: gid additional_gids: additional_gids.clone() } return config } pub fn (mut config CrunConfig) add_env(key string, value string) &CrunConfig { + // Remove existing env var with same key to avoid duplicates + config.spec.process.env = config.spec.process.env.filter(!it.starts_with('${key}=')) config.spec.process.env << '${key}=${value}' return config } pub fn (mut config CrunConfig) set_rootfs(path string, readonly bool) &CrunConfig { config.spec.root = Root{ - path: path + path: path readonly: readonly } return config @@ -165,16 +168,16 @@ pub fn (mut config CrunConfig) set_pids_limit(limit i64) &CrunConfig { pub fn (mut config CrunConfig) add_mount(destination string, source string, typ MountType, options []MountOption) &CrunConfig { config.spec.mounts << Mount{ destination: destination - typ: typ.to_string() - source: source - options: options.map(it.to_string()) + typ: typ.to_string() + source: source + options: options.map(it.to_string()) } return config } pub fn (mut config CrunConfig) add_capability(cap Capability) &CrunConfig { cap_str := cap.to_string() - + if cap_str !in config.spec.process.capabilities.bounding { config.spec.process.capabilities.bounding << cap_str } @@ -189,7 +192,7 @@ pub fn (mut config CrunConfig) add_capability(cap Capability) &CrunConfig { pub fn (mut config CrunConfig) remove_capability(cap Capability) &CrunConfig { cap_str := cap.to_string() - + config.spec.process.capabilities.bounding = config.spec.process.capabilities.bounding.filter(it != cap_str) config.spec.process.capabilities.effective = config.spec.process.capabilities.effective.filter(it != cap_str) config.spec.process.capabilities.permitted = config.spec.process.capabilities.permitted.filter(it != cap_str) @@ -197,8 +200,11 @@ pub fn (mut config CrunConfig) remove_capability(cap Capability) &CrunConfig { } pub fn (mut config CrunConfig) add_rlimit(typ RlimitType, hard u64, soft u64) &CrunConfig { + // Remove existing rlimit with same type to avoid duplicates + typ_str := typ.to_string() + config.spec.process.rlimits = config.spec.process.rlimits.filter(it.typ != typ_str) config.spec.process.rlimits << Rlimit{ - typ: typ.to_string() + typ: typ_str hard: hard soft: soft } @@ -210,6 +216,11 @@ pub fn (mut config CrunConfig) set_no_new_privileges(value bool) &CrunConfig { return config } +pub fn (mut config CrunConfig) set_terminal(value bool) &CrunConfig { + config.spec.process.terminal = value + return config +} + pub fn (mut config CrunConfig) add_masked_path(path string) &CrunConfig { if path !in config.spec.linux.masked_paths { config.spec.linux.masked_paths << path @@ -226,67 +237,65 @@ pub fn (mut config CrunConfig) add_readonly_path(path string) &CrunConfig { pub fn new(mut configs map[string]&CrunConfig, args FactoryArgs) !&CrunConfig { name := texttools.name_fix(args.name) - + mut config := &CrunConfig{ name: name spec: create_default_spec() } - + configs[name] = config return config } pub fn get(configs map[string]&CrunConfig, args FactoryArgs) !&CrunConfig { name := texttools.name_fix(args.name) - return configs[name] or { - return error('crun config with name "${name}" does not exist') - } + return configs[name] or { return error('crun config with name "${name}" does not exist') } } fn create_default_spec() Spec { // Create default spec that matches the heropods template mut spec := Spec{ oci_version: '1.0.2' // Set default here - platform: Platform{ - os: 'linux' + platform: Platform{ + os: 'linux' arch: 'amd64' } - process: Process{ - terminal: true - user: User{ + process: Process{ + terminal: true + user: User{ uid: 0 gid: 0 } - args: ['/bin/sh'] - env: [ + args: ['/bin/sh'] + env: [ 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', - 'TERM=xterm' + 'TERM=xterm', ] - cwd: '/' - capabilities: Capabilities{ - bounding: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE'] - effective: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE'] + cwd: '/' + capabilities: Capabilities{ + bounding: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE'] + effective: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE'] inheritable: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE'] - permitted: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE'] + permitted: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE'] } - rlimits: [ + rlimits: [ Rlimit{ - typ: 'RLIMIT_NOFILE' + typ: 'RLIMIT_NOFILE' hard: 1024 soft: 1024 - } + }, ] no_new_privileges: true // No JSON annotation needed here } - root: Root{ - path: 'rootfs' + root: Root{ + path: 'rootfs' readonly: false } - hostname: 'container' - mounts: create_default_mounts() - linux: Linux{ - namespaces: create_default_namespaces() - masked_paths: [ + hostname: 'container' + mounts: create_default_mounts() + linux: Linux{ + namespaces: create_default_namespaces() + masked_paths: [ '/proc/acpi', '/proc/kcore', '/proc/keys', @@ -295,7 +304,7 @@ fn create_default_spec() Spec { '/proc/timer_stats', '/proc/sched_debug', '/proc/scsi', - '/sys/firmware' + '/sys/firmware', ] readonly_paths: [ '/proc/asound', @@ -303,21 +312,34 @@ fn create_default_spec() Spec { '/proc/fs', '/proc/irq', '/proc/sys', - '/proc/sysrq-trigger' + '/proc/sysrq-trigger', ] } } - + return spec } fn create_default_namespaces() []LinuxNamespace { return [ - LinuxNamespace{typ: 'pid'}, - LinuxNamespace{typ: 'network'}, - LinuxNamespace{typ: 'ipc'}, - LinuxNamespace{typ: 'uts'}, - LinuxNamespace{typ: 'mount'}, + LinuxNamespace{ + typ: 'pid' + }, + LinuxNamespace{ + typ: 'network' + }, + LinuxNamespace{ + typ: 'ipc' + }, + LinuxNamespace{ + typ: 'uts' + }, + LinuxNamespace{ + typ: 'cgroup' + }, + LinuxNamespace{ + typ: 'mount' + }, ] } @@ -325,20 +347,44 @@ fn create_default_mounts() []Mount { return [ Mount{ destination: '/proc' - typ: 'proc' - source: 'proc' + typ: 'proc' + source: 'proc' }, Mount{ destination: '/dev' - typ: 'tmpfs' - source: 'tmpfs' - options: ['nosuid', 'strictatime', 'mode=755', 'size=65536k'] + typ: 'tmpfs' + source: 'tmpfs' + options: ['nosuid', 'strictatime', 'mode=755', 'size=65536k'] + }, + Mount{ + destination: '/dev/pts' + typ: 'devpts' + source: 'devpts' + options: ['nosuid', 'noexec', 'newinstance', 'ptmxmode=0666', 'mode=0620', 'gid=5'] + }, + Mount{ + destination: '/dev/shm' + typ: 'tmpfs' + source: 'shm' + options: ['nosuid', 'noexec', 'nodev', 'mode=1777', 'size=65536k'] + }, + Mount{ + destination: '/dev/mqueue' + typ: 'mqueue' + source: 'mqueue' + options: ['nosuid', 'noexec', 'nodev'] }, Mount{ destination: '/sys' - typ: 'sysfs' - source: 'sysfs' - options: ['nosuid', 'noexec', 'nodev', 'ro'] + typ: 'sysfs' + source: 'sysfs' + options: ['nosuid', 'noexec', 'nodev', 'ro'] + }, + Mount{ + destination: '/sys/fs/cgroup' + typ: 'cgroup' + source: 'cgroup' + options: ['nosuid', 'noexec', 'nodev', 'relatime', 'ro'] }, ] -} \ No newline at end of file +} diff --git a/lib/virt/crun/model.v b/lib/virt/crun/model.v index f099a408..2eb7400c 100644 --- a/lib/virt/crun/model.v +++ b/lib/virt/crun/model.v @@ -3,7 +3,7 @@ module crun // OCI Runtime Spec structures that can be directly encoded to JSON pub struct Spec { pub mut: - oci_version string + oci_version string @[json: 'ociVersion'] platform Platform process Process root Root @@ -21,21 +21,21 @@ pub mut: pub struct Process { pub mut: - terminal bool = true - user User - args []string - env []string - cwd string = '/' - capabilities Capabilities - rlimits []Rlimit - no_new_privileges bool + terminal bool = true + user User + args []string + env []string + cwd string = '/' + capabilities Capabilities + rlimits []Rlimit + no_new_privileges bool @[json: 'noNewPrivileges'] } pub struct User { pub mut: uid u32 gid u32 - additional_gids []u32 + additional_gids []u32 @[json: 'additionalGids'] } pub struct Capabilities { @@ -49,7 +49,7 @@ pub mut: pub struct Rlimit { pub mut: - typ string + typ string @[json: 'type'] hard u64 soft u64 } @@ -63,26 +63,26 @@ pub mut: pub struct Mount { pub mut: destination string - typ string + typ string @[json: 'type'] source string options []string } pub struct Linux { pub mut: - namespaces []LinuxNamespace - resources LinuxResources - devices []LinuxDevice - masked_paths []string - readonly_paths []string - uid_mappings []LinuxIDMapping - gid_mappings []LinuxIDMapping + namespaces []LinuxNamespace + resources LinuxResources + devices []LinuxDevice + masked_paths []string @[json: 'maskedPaths'] + readonly_paths []string @[json: 'readonlyPaths'] + uid_mappings []LinuxIDMapping @[json: 'uidMappings'] + gid_mappings []LinuxIDMapping @[json: 'gidMappings'] } pub struct LinuxNamespace { pub mut: - typ string - path string + typ string @[json: 'type'] + path string @[omitempty] } pub struct LinuxResources { @@ -95,47 +95,47 @@ pub mut: pub struct Memory { pub mut: - limit u64 - reservation u64 - swap u64 - kernel u64 - swappiness i64 + limit u64 @[omitempty] + reservation u64 @[omitempty] + swap u64 @[omitempty] + kernel u64 @[omitempty] + swappiness i64 @[omitempty] } pub struct CPU { pub mut: - shares u64 - quota i64 - period u64 - cpus string - mems string + shares u64 @[omitempty] + quota i64 @[omitempty] + period u64 @[omitempty] + cpus string @[omitempty] + mems string @[omitempty] } pub struct Pids { pub mut: - limit i64 + limit i64 @[omitempty] } pub struct BlockIO { pub mut: - weight u16 + weight u16 @[omitempty] } pub struct LinuxDevice { pub mut: - path string - typ string - major i64 - minor i64 - file_mode u32 - uid u32 - gid u32 + path string + typ string @[json: 'type'] + major i64 + minor i64 + file_mode u32 @[json: 'fileMode'] + uid u32 + gid u32 } pub struct LinuxIDMapping { pub mut: - container_id u32 - host_id u32 + container_id u32 @[json: 'containerID'] + host_id u32 @[json: 'hostID'] size u32 } @@ -160,6 +160,8 @@ pub enum MountType { proc sysfs devpts + mqueue + cgroup nfs overlay } @@ -235,4 +237,4 @@ pub enum RlimitType { rlimit_nice rlimit_rtprio rlimit_rttime -} \ No newline at end of file +} diff --git a/lib/virt/heropods/config_template.json b/lib/virt/heropods/config_template.json deleted file mode 100644 index 51cd699a..00000000 --- a/lib/virt/heropods/config_template.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "ociVersion": "1.0.2", - "process": { - "terminal": true, - "user": { - "uid": 0, - "gid": 0 - }, - "args": [ - "/bin/sh", - "-c", - "while true; do sleep 30; done" - ], - "env": [ - "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", - "TERM=xterm" - ], - "cwd": "/", - "capabilities": { - "bounding": [ - "CAP_AUDIT_WRITE", - "CAP_KILL", - "CAP_NET_BIND_SERVICE" - ], - "effective": [ - "CAP_AUDIT_WRITE", - "CAP_KILL", - "CAP_NET_BIND_SERVICE" - ], - "inheritable": [ - "CAP_AUDIT_WRITE", - "CAP_KILL", - "CAP_NET_BIND_SERVICE" - ], - "permitted": [ - "CAP_AUDIT_WRITE", - "CAP_KILL", - "CAP_NET_BIND_SERVICE" - ] - }, - "rlimits": [ - { - "type": "RLIMIT_NOFILE", - "hard": 1024, - "soft": 1024 - } - ], - "noNewPrivileges": true - }, - "root": { - "path": "${rootfs_path}", - "readonly": false - }, - "mounts": [ - { - "destination": "/proc", - "type": "proc", - "source": "proc" - }, - { - "destination": "/dev", - "type": "tmpfs", - "source": "tmpfs", - "options": [ - "nosuid", - "strictatime", - "mode=755", - "size=65536k" - ] - }, - { - "destination": "/sys", - "type": "sysfs", - "source": "sysfs", - "options": [ - "nosuid", - "noexec", - "nodev", - "ro" - ] - } - ], - "linux": { - "namespaces": [ - { - "type": "pid" - }, - { - "type": "network" - }, - { - "type": "ipc" - }, - { - "type": "uts" - }, - { - "type": "mount" - } - ], - "maskedPaths": [ - "/proc/acpi", - "/proc/kcore", - "/proc/keys", - "/proc/latency_stats", - "/proc/timer_list", - "/proc/timer_stats", - "/proc/sched_debug", - "/proc/scsi", - "/sys/firmware" - ], - "readonlyPaths": [ - "/proc/asound", - "/proc/bus", - "/proc/fs", - "/proc/irq", - "/proc/sys", - "/proc/sysrq-trigger" - ] - } -} \ No newline at end of file diff --git a/lib/virt/heropods/container.v b/lib/virt/heropods/container.v index bb2e1289..a01009c3 100644 --- a/lib/virt/heropods/container.v +++ b/lib/virt/heropods/container.v @@ -3,16 +3,19 @@ module heropods import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.osal.tmux import freeflowuniverse.herolib.osal.core as osal +import freeflowuniverse.herolib.virt.crun import time import freeflowuniverse.herolib.builder import json +@[heap] pub struct Container { pub mut: - name string - node ?&builder.Node - tmux_pane ?&tmux.Pane - factory &ContainerFactory + name string + node ?&builder.Node + tmux_pane ?&tmux.Pane + crun_config ?&crun.CrunConfig + factory &ContainerFactory } // Struct to parse JSON output of `crun state` @@ -31,10 +34,32 @@ pub fn (mut self Container) start() ! { if !container_exists { // Container doesn't exist, create it first console.print_debug('Container ${self.name} does not exist, creating it...') - osal.exec( - cmd: 'crun create --bundle ${self.factory.base_dir}/configs/${self.name} ${self.name}' + // Try to create the container, if it fails with "File exists" error, + // try to force delete any leftover state and retry + crun_root := '${self.factory.base_dir}/runtime' + create_result := osal.exec( + cmd: 'crun --root ${crun_root} create --bundle ${self.factory.base_dir}/configs/${self.name} ${self.name}' stdout: true - )! + ) or { + if err.msg().contains('File exists') { + console.print_debug('Container creation failed with "File exists", attempting to clean up leftover state...') + // Force delete any leftover state - try multiple cleanup approaches + osal.exec(cmd: 'crun --root ${crun_root} delete ${self.name}', stdout: false) or {} + osal.exec(cmd: 'crun delete ${self.name}', stdout: false) or {} // Also try default root + // Clean up any leftover runtime directories + osal.exec(cmd: 'rm -rf ${crun_root}/${self.name}', stdout: false) or {} + osal.exec(cmd: 'rm -rf /run/crun/${self.name}', stdout: false) or {} + // Wait a moment for cleanup to complete + time.sleep(500 * time.millisecond) + // Retry creation + osal.exec( + cmd: 'crun --root ${crun_root} create --bundle ${self.factory.base_dir}/configs/${self.name} ${self.name}' + stdout: true + )! + } else { + return err + } + } console.print_debug('Container ${self.name} created') } @@ -48,16 +73,18 @@ pub fn (mut self Container) start() ! { // because crun doesn't allow restarting a stopped container if container_exists && status != .running { console.print_debug('Container ${self.name} exists but is stopped, recreating...') - osal.exec(cmd: 'crun delete ${self.name}', stdout: false) or {} + crun_root := '${self.factory.base_dir}/runtime' + osal.exec(cmd: 'crun --root ${crun_root} delete ${self.name}', stdout: false) or {} osal.exec( - cmd: 'crun create --bundle ${self.factory.base_dir}/configs/${self.name} ${self.name}' + cmd: 'crun --root ${crun_root} create --bundle ${self.factory.base_dir}/configs/${self.name} ${self.name}' stdout: true )! console.print_debug('Container ${self.name} recreated') } // start the container (crun start doesn't have --detach flag) - osal.exec(cmd: 'crun start ${self.name}', stdout: true)! + crun_root := '${self.factory.base_dir}/runtime' + osal.exec(cmd: 'crun --root ${crun_root} start ${self.name}', stdout: true)! console.print_green('Container ${self.name} started') } @@ -68,12 +95,13 @@ pub fn (mut self Container) stop() ! { return } - osal.exec(cmd: 'crun kill ${self.name} SIGTERM', stdout: false) or {} + crun_root := '${self.factory.base_dir}/runtime' + osal.exec(cmd: 'crun --root ${crun_root} kill ${self.name} SIGTERM', stdout: false) or {} time.sleep(2 * time.second) // Force kill if still running if self.status()! == .running { - osal.exec(cmd: 'crun kill ${self.name} SIGKILL', stdout: false) or {} + osal.exec(cmd: 'crun --root ${crun_root} kill ${self.name} SIGKILL', stdout: false) or {} } console.print_green('Container ${self.name} stopped') } @@ -86,7 +114,8 @@ pub fn (mut self Container) delete() ! { } self.stop()! - osal.exec(cmd: 'crun delete ${self.name}', stdout: false) or {} + crun_root := '${self.factory.base_dir}/runtime' + osal.exec(cmd: 'crun --root ${crun_root} delete ${self.name}', stdout: false) or {} // Remove from factory's container cache if self.name in self.factory.containers { @@ -110,7 +139,10 @@ pub fn (mut self Container) exec(cmd_ osal.Command) !string { } pub fn (self Container) status() !ContainerStatus { - result := osal.exec(cmd: 'crun state ${self.name}', stdout: false) or { return .unknown } + crun_root := '${self.factory.base_dir}/runtime' + result := osal.exec(cmd: 'crun --root ${crun_root} state ${self.name}', stdout: false) or { + return .unknown + } // Parse JSON output from crun state state := json.decode(CrunState, result.output) or { return .unknown } @@ -126,7 +158,10 @@ pub fn (self Container) status() !ContainerStatus { // Check if container exists in crun (regardless of its state) fn (self Container) container_exists_in_crun() !bool { // Try to get container state - if it fails, container doesn't exist - result := osal.exec(cmd: 'crun state ${self.name}', stdout: false) or { return false } + crun_root := '${self.factory.base_dir}/runtime' + result := osal.exec(cmd: 'crun --root ${crun_root} state ${self.name}', stdout: false) or { + return false + } // If we get here, the container exists (even if stopped/paused) return result.exit_code == 0 @@ -206,7 +241,8 @@ pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane { // Execute command if provided if args.cmd != '' { - pane.send_keys('crun exec ${self.name} ${args.cmd}')! + crun_root := '${self.factory.base_dir}/runtime' + pane.send_keys('crun --root ${crun_root} exec ${self.name} ${args.cmd}')! } self.tmux_pane = pane @@ -223,6 +259,7 @@ pub fn (mut self Container) node() !&builder.Node { mut exec := builder.ExecutorCrun{ container_id: self.name + crun_root: '${self.factory.base_dir}/runtime' debug: false } @@ -242,3 +279,58 @@ pub fn (mut self Container) node() !&builder.Node { self.node = node return node } + +// Get the crun configuration for this container +pub fn (self Container) config() !&crun.CrunConfig { + return self.crun_config or { return error('Container ${self.name} has no crun configuration') } +} + +// Container configuration customization methods +pub fn (mut self Container) set_memory_limit(limit_mb u64) !&Container { + mut config := self.config()! + config.set_memory_limit(limit_mb * 1024 * 1024) // Convert MB to bytes + return &self +} + +pub fn (mut self Container) set_cpu_limits(period u64, quota i64, shares u64) !&Container { + mut config := self.config()! + config.set_cpu_limits(period, quota, shares) + return &self +} + +pub fn (mut self Container) add_mount(source string, destination string, mount_type crun.MountType, options []crun.MountOption) !&Container { + mut config := self.config()! + config.add_mount(source, destination, mount_type, options) + return &self +} + +pub fn (mut self Container) add_capability(cap crun.Capability) !&Container { + mut config := self.config()! + config.add_capability(cap) + return &self +} + +pub fn (mut self Container) remove_capability(cap crun.Capability) !&Container { + mut config := self.config()! + config.remove_capability(cap) + return &self +} + +pub fn (mut self Container) add_env(key string, value string) !&Container { + mut config := self.config()! + config.add_env(key, value) + return &self +} + +pub fn (mut self Container) set_working_dir(dir string) !&Container { + mut config := self.config()! + config.set_working_dir(dir) + return &self +} + +// Save the current configuration to disk +pub fn (self Container) save_config() ! { + config := self.config()! + config_path := '${self.factory.base_dir}/configs/${self.name}/config.json' + config.save_to_file(config_path)! +} diff --git a/lib/virt/heropods/container_create.v b/lib/virt/heropods/container_create.v index 80ae4d80..66fec4fc 100644 --- a/lib/virt/heropods/container_create.v +++ b/lib/virt/heropods/container_create.v @@ -2,10 +2,9 @@ module heropods import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.osal.core as osal -import freeflowuniverse.herolib.core.pathlib +import freeflowuniverse.herolib.virt.crun import freeflowuniverse.herolib.installers.virt.herorunner as herorunner_installer import os -import x.json2 // Updated enum to be more flexible pub enum ContainerImageType { @@ -27,7 +26,7 @@ pub: pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container { if args.name in self.containers && !args.reset { - return self.containers[args.name] + return self.containers[args.name] or { panic('bug: container should exist') } } // Determine image to use @@ -67,8 +66,8 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container { return error('Image rootfs not found: ${rootfs_path}. Please ensure the image is available.') } - // Create container config (with terminal disabled) but don't create the container yet - self.create_container_config(args.name, rootfs_path)! + // Create crun configuration using the crun module + mut crun_config := self.create_crun_config(args.name, rootfs_path)! // Ensure crun is installed on host if !osal.cmd_exists('crun') { @@ -79,41 +78,45 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container { // Create container struct but don't create the actual container in crun yet // The actual container creation will happen in container.start() mut container := &Container{ - name: args.name - factory: &self + name: args.name + crun_config: crun_config + factory: &self } self.containers[args.name] = container return container } -// Create OCI config.json from template -fn (self ContainerFactory) create_container_config(container_name string, rootfs_path string) ! { +// Create crun configuration using the crun module +fn (mut self ContainerFactory) create_crun_config(container_name string, rootfs_path string) !&crun.CrunConfig { + // Create crun configuration using the factory pattern + mut config := crun.new(mut self.crun_configs, name: container_name)! + + // Configure for heropods use case - disable terminal for background containers + config.set_terminal(false) + config.set_command(['/bin/sh', '-c', 'while true; do sleep 30; done']) + config.set_working_dir('/') + config.set_user(0, 0, []) + config.add_env('PATH', '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') + config.add_env('TERM', 'xterm') + config.set_rootfs(rootfs_path, false) + config.set_hostname('container') + config.set_no_new_privileges(true) + + // Add the specific rlimit for file descriptors + config.add_rlimit(.rlimit_nofile, 1024, 1024) + + // Validate the configuration + config.validate()! + + // Create config directory and save JSON config_dir := '${self.base_dir}/configs/${container_name}' osal.exec(cmd: 'mkdir -p ${config_dir}', stdout: false)! - // Load template - mut config_content := $tmpl('config_template.json') - - // Parse JSON with json2 - mut root := json2.raw_decode(config_content)! - mut config := root.as_map() - - // Get or create process map - mut process := if 'process' in config { - config['process'].as_map() - } else { - map[string]json2.Any{} - } - - // Force disable terminal - process['terminal'] = json2.Any(false) - config['process'] = json2.Any(process) - - // Write back to config.json config_path := '${config_dir}/config.json' - mut p := pathlib.get_file(path: config_path, create: true)! - p.write(json2.encode_pretty(json2.Any(config)))! + config.save_to_file(config_path)! + + return config } // Use podman to pull image and extract rootfs diff --git a/lib/virt/heropods/factory.v b/lib/virt/heropods/factory.v index ef6516c1..bfd21019 100644 --- a/lib/virt/heropods/factory.v +++ b/lib/virt/heropods/factory.v @@ -2,7 +2,7 @@ module heropods import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.osal.core as osal -import time +import freeflowuniverse.herolib.virt.crun import os @[heap] @@ -11,6 +11,7 @@ pub mut: tmux_session string containers map[string]&Container images map[string]&ContainerImage + crun_configs map[string]&crun.CrunConfig base_dir string } @@ -45,6 +46,11 @@ fn (mut self ContainerFactory) init(args FactoryInitArgs) ! { } } + // Clean up any leftover crun state if reset is requested + if args.reset { + self.cleanup_crun_state()! + } + // Load existing images into cache self.load_existing_images()! @@ -104,7 +110,7 @@ pub fn (mut self ContainerFactory) get(args ContainerNewArgs) !&Container { if args.name !in self.containers { return error('Container "${args.name}" does not exist. Use factory.new() to create it first.') } - return self.containers[args.name] + return self.containers[args.name] or { panic('bug: container should exist') } } // Get image by name @@ -112,7 +118,7 @@ pub fn (mut self ContainerFactory) image_get(name string) !&ContainerImage { if name !in self.images { return error('Image "${name}" not found in cache. Try importing or downloading it.') } - return self.images[name] + return self.images[name] or { panic('bug: image should exist') } } // List all containers currently managed by crun @@ -136,3 +142,34 @@ pub fn (self ContainerFactory) list() ![]Container { } return containers } + +// Clean up any leftover crun state +fn (mut self ContainerFactory) cleanup_crun_state() ! { + console.print_debug('Cleaning up leftover crun state...') + crun_root := '${self.base_dir}/runtime' + + // Stop and delete all containers in our custom root + result := osal.exec(cmd: 'crun --root ${crun_root} list -q', stdout: false) or { return } + + for container_name in result.output.split_into_lines() { + if container_name.trim_space() != '' { + console.print_debug('Cleaning up container: ${container_name}') + osal.exec(cmd: 'crun --root ${crun_root} kill ${container_name} SIGKILL', stdout: false) or {} + osal.exec(cmd: 'crun --root ${crun_root} delete ${container_name}', stdout: false) or {} + } + } + + // Also clean up any containers in the default root that might be ours + result2 := osal.exec(cmd: 'crun list -q', stdout: false) or { return } + for container_name in result2.output.split_into_lines() { + if container_name.trim_space() != '' && container_name in self.containers { + console.print_debug('Cleaning up container from default root: ${container_name}') + osal.exec(cmd: 'crun kill ${container_name} SIGKILL', stdout: false) or {} + osal.exec(cmd: 'crun delete ${container_name}', stdout: false) or {} + } + } + + // Clean up runtime directories + osal.exec(cmd: 'rm -rf ${crun_root}/*', stdout: false) or {} + osal.exec(cmd: 'find /run/crun -name "*" -type d -exec rm -rf {} + 2>/dev/null', stdout: false) or {} +} diff --git a/test_basic.vsh b/test_basic.vsh index 89c443df..ece1fbec 100755 --- a/test_basic.vsh +++ b/test_basic.vsh @@ -117,6 +117,8 @@ fn dotest(path string, base_dir string, mut cache TestCache) ! { return } + println('Running test (not cached or expired): ${path} (key: ${test_key})') + cmd := 'v -stats -enable-globals -n -w -gc none test ${norm_path}' println(cmd) result := os.execute(cmd) @@ -128,7 +130,8 @@ fn dotest(path string, base_dir string, mut cache TestCache) ! { } // Update cache with successful test run - update_test_cache(mut cache, test_key) + cache.tests[test_key] = time.now().unix() + save_test_cache(cache) println('Test passed: ${path}') } @@ -225,11 +228,23 @@ test_files_error := tests_error.split('\n').filter(it.trim_space() != '') // Load test cache mut cache := load_test_cache() println('Test cache loaded from ${cache_file}') +println('Cache contains ${cache.tests.len} entries') +if cache.tests.len > 0 { + println('Sample cache entries:') + mut count := 0 + for key, timestamp in cache.tests { + if count < 3 { + println(' ${key}: ${timestamp}') + count++ + } + } +} println('tests to ignore') println(tests_ignore) // Run each test with proper v command flags +println('Starting main test loop with ${test_files.len} test entries') for test in test_files { if test.trim_space() == '' || test.trim_space().starts_with('//') || test.trim_space().starts_with('#') { @@ -246,14 +261,20 @@ for test in test_files { if os.is_dir(full_path) { // If directory, run tests for each .v file in it recursively files := os.walk_ext(full_path, '.v') + println('Found ${files.len} .v files in directory: ${full_path}') for file in files { + println('Processing file: ${file}') process_test_file(file, norm_dir_of_script, test_files_ignore, test_files_error, mut cache)! } + println('Completed directory: ${full_path}') } else if os.is_file(full_path) { + println('Processing single file: ${full_path}') process_test_file(full_path, norm_dir_of_script, test_files_ignore, test_files_error, mut cache)! } } println('All (non skipped) tests ok') +println('Test runner completed successfully. Exiting.') +exit(0)