Merge pull request #147 from Incubaid/development_crun

Configure Crun module with Heropods module to work with configs
This commit is contained in:
Omdanii
2025-09-14 14:14:00 +03:00
committed by GitHub
10 changed files with 381 additions and 311 deletions

View File

@@ -13,7 +13,7 @@ println('=== HeroPods Refactored API Demo ===')
// Step 1: factory.new() now only creates a container definition/handle // Step 1: factory.new() now only creates a container definition/handle
// It does NOT create the actual container in the backend yet // It does NOT create the actual container in the backend yet
mut container := factory.new( mut container := factory.new(
name: 'myalpine' name: 'demo_alpine'
image: .custom image: .custom
custom_image_name: 'alpine_3_20' custom_image_name: 'alpine_3_20'
docker_url: 'docker.io/library/alpine:3.20' docker_url: 'docker.io/library/alpine:3.20'

View File

@@ -8,7 +8,7 @@ mut factory := heropods.new(
) or { panic('Failed to init ContainerFactory: ${err}') } ) or { panic('Failed to init ContainerFactory: ${err}') }
mut container := factory.new( mut container := factory.new(
name: 'myalpine' name: 'alpine_demo'
image: .custom image: .custom
custom_image_name: 'alpine_3_20' custom_image_name: 'alpine_3_20'
docker_url: 'docker.io/library/alpine:3.20' docker_url: 'docker.io/library/alpine:3.20'

View File

@@ -11,13 +11,22 @@ import freeflowuniverse.herolib.core.texttools
pub struct ExecutorCrun { pub struct ExecutorCrun {
pub mut: pub mut:
container_id string // container ID for crun container_id string // container ID for crun
crun_root string // custom crun root directory
retry int = 1 retry int = 1
debug bool = true 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() ! { pub fn (mut executor ExecutorCrun) init() ! {
// Verify container exists and is running // 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') 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}') 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') { if args.cmd.contains('\n') {
// For multiline commands, write to temp file first // For multiline commands, write to temp file first
temp_script := '/tmp/crun_script_${rand.uuid_v4()}.sh' 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 // Copy script into container and execute
executor.file_write('/tmp/exec_script.sh', script_content)! 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)! 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' 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) console.print_debug(cmd)
osal.execute_interactive(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 {} } defer { os.rm(temp_file) or {} }
// Use crun exec to copy file content // 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)! osal.exec(cmd: cmd, stdout: false)!
} }

View File

@@ -1,31 +1,31 @@
module crun module crun
pub fn example_heropods_compatible() ! { pub fn example_heropods_compatible() ! {
mut configs := map[string]&CrunConfig{} mut configs := map[string]&CrunConfig{}
// Create a container configuration compatible with heropods template // Create a container configuration compatible with heropods template
mut config := new(mut configs, name: 'heropods-example')! 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']) config.set_command(['/bin/sh'])
.set_working_dir('/') config.set_working_dir('/')
.set_user(0, 0, []) config.set_user(0, 0, [])
.add_env('PATH', '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin') config.add_env('PATH', '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin')
.add_env('TERM', 'xterm') config.add_env('TERM', 'xterm')
.set_rootfs('${rootfs_path}', false) // This will be replaced by the actual path config.set_rootfs('/tmp/rootfs', false) // This will be replaced by the actual path
.set_hostname('container') config.set_hostname('container')
.set_no_new_privileges(true) config.set_no_new_privileges(true)
// Add the specific rlimit from template // Add the specific rlimit from template
config.add_rlimit(.rlimit_nofile, 1024, 1024) config.add_rlimit(.rlimit_nofile, 1024, 1024)
// Validate the configuration // Validate the configuration
config.validate()! config.validate()!
// Generate and print JSON // Generate and print JSON
json_output := config.to_json()! json_output := config.to_json()!
println(json_output) println(json_output)
// Save to file // Save to file
config.save_to_file('/tmp/heropods_config.json')! config.save_to_file('/tmp/heropods_config.json')!
println('Heropods-compatible configuration saved to /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{} mut configs := map[string]&CrunConfig{}
// Create a more complex container configuration // Create a more complex container configuration
mut config := new(mut configs, name: 'custom-container')! mut config := new(mut configs, name: 'custom-container')!
config.set_command(['/usr/bin/my-app', '--config', '/etc/myapp/config.yaml']) config.set_command(['/usr/bin/my-app', '--config', '/etc/myapp/config.yaml'])
.set_working_dir('/app') config.set_working_dir('/app')
.set_user(1000, 1000, [1001, 1002]) config.set_user(1000, 1000, [1001, 1002])
.add_env('MY_VAR', 'my_value') config.add_env('MY_VAR', 'my_value')
.add_env('ANOTHER_VAR', 'another_value') config.add_env('ANOTHER_VAR', 'another_value')
.set_rootfs('/path/to/rootfs', false) config.set_rootfs('/path/to/rootfs', false)
.set_hostname('my-custom-container') config.set_hostname('my-custom-container')
.set_memory_limit(1024 * 1024 * 1024) // 1GB config.set_memory_limit(1024 * 1024 * 1024) // 1GB
.set_cpu_limits(100000, 50000, 1024) // period, quota, shares config.set_cpu_limits(100000, 50000, 1024) // period, quota, shares
.set_pids_limit(500) config.set_pids_limit(500)
.add_mount('/host/path', '/container/path', .bind, [.rw]) config.add_mount('/host/path', '/container/path', .bind, [.rw])
.add_mount('/tmp/cache', '/app/cache', .tmpfs, [.rw, .noexec]) config.add_mount('/tmp/cache', '/app/cache', .tmpfs, [.rw, .noexec])
.add_capability(.cap_sys_admin) config.add_capability(.cap_sys_admin)
.remove_capability(.cap_net_raw) config.remove_capability(.cap_net_raw)
.add_rlimit(.rlimit_nproc, 100, 50) config.add_rlimit(.rlimit_nproc, 100, 50)
.set_no_new_privileges(true) config.set_no_new_privileges(true)
// Add some additional security hardening // Add some additional security hardening
config.add_masked_path('/proc/kcore') config.add_masked_path('/proc/kcore')
.add_readonly_path('/proc/sys') config.add_readonly_path('/proc/sys')
// Validate before use // Validate before use
config.validate()! config.validate()!
// Get the JSON // Get the JSON
json_str := config.to_json()! json_str := config.to_json()!
println('Custom container config:') println('Custom container config:')
println(json_str) println(json_str)
} }

View File

@@ -2,11 +2,10 @@ module crun
import freeflowuniverse.herolib.core.texttools import freeflowuniverse.herolib.core.texttools
@[params] @[params]
pub struct FactoryArgs { pub struct FactoryArgs {
pub mut: pub mut:
name string = "default" name string = 'default'
} }
pub struct CrunConfig { pub struct CrunConfig {
@@ -23,6 +22,8 @@ pub fn (mount_type MountType) to_string() string {
.proc { 'proc' } .proc { 'proc' }
.sysfs { 'sysfs' } .sysfs { 'sysfs' }
.devpts { 'devpts' } .devpts { 'devpts' }
.mqueue { 'mqueue' }
.cgroup { 'cgroup' }
.nfs { 'nfs' } .nfs { 'nfs' }
.overlay { 'overlay' } .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 { pub fn (mut config CrunConfig) set_user(uid u32, gid u32, additional_gids []u32) &CrunConfig {
config.spec.process.user = User{ config.spec.process.user = User{
uid: uid uid: uid
gid: gid gid: gid
additional_gids: additional_gids.clone() additional_gids: additional_gids.clone()
} }
return config return config
} }
pub fn (mut config CrunConfig) add_env(key string, value string) &CrunConfig { 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}' config.spec.process.env << '${key}=${value}'
return config return config
} }
pub fn (mut config CrunConfig) set_rootfs(path string, readonly bool) &CrunConfig { pub fn (mut config CrunConfig) set_rootfs(path string, readonly bool) &CrunConfig {
config.spec.root = Root{ config.spec.root = Root{
path: path path: path
readonly: readonly readonly: readonly
} }
return config 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 { pub fn (mut config CrunConfig) add_mount(destination string, source string, typ MountType, options []MountOption) &CrunConfig {
config.spec.mounts << Mount{ config.spec.mounts << Mount{
destination: destination destination: destination
typ: typ.to_string() typ: typ.to_string()
source: source source: source
options: options.map(it.to_string()) options: options.map(it.to_string())
} }
return config return config
} }
pub fn (mut config CrunConfig) add_capability(cap Capability) &CrunConfig { pub fn (mut config CrunConfig) add_capability(cap Capability) &CrunConfig {
cap_str := cap.to_string() cap_str := cap.to_string()
if cap_str !in config.spec.process.capabilities.bounding { if cap_str !in config.spec.process.capabilities.bounding {
config.spec.process.capabilities.bounding << cap_str 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 { pub fn (mut config CrunConfig) remove_capability(cap Capability) &CrunConfig {
cap_str := cap.to_string() cap_str := cap.to_string()
config.spec.process.capabilities.bounding = config.spec.process.capabilities.bounding.filter(it != cap_str) 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.effective = config.spec.process.capabilities.effective.filter(it != cap_str)
config.spec.process.capabilities.permitted = config.spec.process.capabilities.permitted.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 { 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{ config.spec.process.rlimits << Rlimit{
typ: typ.to_string() typ: typ_str
hard: hard hard: hard
soft: soft soft: soft
} }
@@ -210,6 +216,11 @@ pub fn (mut config CrunConfig) set_no_new_privileges(value bool) &CrunConfig {
return config 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 { pub fn (mut config CrunConfig) add_masked_path(path string) &CrunConfig {
if path !in config.spec.linux.masked_paths { if path !in config.spec.linux.masked_paths {
config.spec.linux.masked_paths << path 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 { pub fn new(mut configs map[string]&CrunConfig, args FactoryArgs) !&CrunConfig {
name := texttools.name_fix(args.name) name := texttools.name_fix(args.name)
mut config := &CrunConfig{ mut config := &CrunConfig{
name: name name: name
spec: create_default_spec() spec: create_default_spec()
} }
configs[name] = config configs[name] = config
return config return config
} }
pub fn get(configs map[string]&CrunConfig, args FactoryArgs) !&CrunConfig { pub fn get(configs map[string]&CrunConfig, args FactoryArgs) !&CrunConfig {
name := texttools.name_fix(args.name) name := texttools.name_fix(args.name)
return configs[name] or { return configs[name] or { return error('crun config with name "${name}" does not exist') }
return error('crun config with name "${name}" does not exist')
}
} }
fn create_default_spec() Spec { fn create_default_spec() Spec {
// Create default spec that matches the heropods template // Create default spec that matches the heropods template
mut spec := Spec{ mut spec := Spec{
oci_version: '1.0.2' // Set default here oci_version: '1.0.2' // Set default here
platform: Platform{ platform: Platform{
os: 'linux' os: 'linux'
arch: 'amd64' arch: 'amd64'
} }
process: Process{ process: Process{
terminal: true terminal: true
user: User{ user: User{
uid: 0 uid: 0
gid: 0 gid: 0
} }
args: ['/bin/sh'] args: ['/bin/sh']
env: [ env: [
'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
'TERM=xterm' 'TERM=xterm',
] ]
cwd: '/' cwd: '/'
capabilities: Capabilities{ capabilities: Capabilities{
bounding: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE'] bounding: ['CAP_AUDIT_WRITE', 'CAP_KILL', 'CAP_NET_BIND_SERVICE']
effective: ['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'] 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{ Rlimit{
typ: 'RLIMIT_NOFILE' typ: 'RLIMIT_NOFILE'
hard: 1024 hard: 1024
soft: 1024 soft: 1024
} },
] ]
no_new_privileges: true // No JSON annotation needed here no_new_privileges: true // No JSON annotation needed here
} }
root: Root{ root: Root{
path: 'rootfs' path: 'rootfs'
readonly: false readonly: false
} }
hostname: 'container' hostname: 'container'
mounts: create_default_mounts() mounts: create_default_mounts()
linux: Linux{ linux: Linux{
namespaces: create_default_namespaces() namespaces: create_default_namespaces()
masked_paths: [ masked_paths: [
'/proc/acpi', '/proc/acpi',
'/proc/kcore', '/proc/kcore',
'/proc/keys', '/proc/keys',
@@ -295,7 +304,7 @@ fn create_default_spec() Spec {
'/proc/timer_stats', '/proc/timer_stats',
'/proc/sched_debug', '/proc/sched_debug',
'/proc/scsi', '/proc/scsi',
'/sys/firmware' '/sys/firmware',
] ]
readonly_paths: [ readonly_paths: [
'/proc/asound', '/proc/asound',
@@ -303,21 +312,34 @@ fn create_default_spec() Spec {
'/proc/fs', '/proc/fs',
'/proc/irq', '/proc/irq',
'/proc/sys', '/proc/sys',
'/proc/sysrq-trigger' '/proc/sysrq-trigger',
] ]
} }
} }
return spec return spec
} }
fn create_default_namespaces() []LinuxNamespace { fn create_default_namespaces() []LinuxNamespace {
return [ return [
LinuxNamespace{typ: 'pid'}, LinuxNamespace{
LinuxNamespace{typ: 'network'}, typ: 'pid'
LinuxNamespace{typ: 'ipc'}, },
LinuxNamespace{typ: 'uts'}, LinuxNamespace{
LinuxNamespace{typ: 'mount'}, typ: 'network'
},
LinuxNamespace{
typ: 'ipc'
},
LinuxNamespace{
typ: 'uts'
},
LinuxNamespace{
typ: 'cgroup'
},
LinuxNamespace{
typ: 'mount'
},
] ]
} }
@@ -325,20 +347,44 @@ fn create_default_mounts() []Mount {
return [ return [
Mount{ Mount{
destination: '/proc' destination: '/proc'
typ: 'proc' typ: 'proc'
source: 'proc' source: 'proc'
}, },
Mount{ Mount{
destination: '/dev' destination: '/dev'
typ: 'tmpfs' typ: 'tmpfs'
source: 'tmpfs' source: 'tmpfs'
options: ['nosuid', 'strictatime', 'mode=755', 'size=65536k'] 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{ Mount{
destination: '/sys' destination: '/sys'
typ: 'sysfs' typ: 'sysfs'
source: 'sysfs' source: 'sysfs'
options: ['nosuid', 'noexec', 'nodev', 'ro'] options: ['nosuid', 'noexec', 'nodev', 'ro']
},
Mount{
destination: '/sys/fs/cgroup'
typ: 'cgroup'
source: 'cgroup'
options: ['nosuid', 'noexec', 'nodev', 'relatime', 'ro']
}, },
] ]
} }

View File

@@ -3,7 +3,7 @@ module crun
// OCI Runtime Spec structures that can be directly encoded to JSON // OCI Runtime Spec structures that can be directly encoded to JSON
pub struct Spec { pub struct Spec {
pub mut: pub mut:
oci_version string oci_version string @[json: 'ociVersion']
platform Platform platform Platform
process Process process Process
root Root root Root
@@ -21,21 +21,21 @@ pub mut:
pub struct Process { pub struct Process {
pub mut: pub mut:
terminal bool = true terminal bool = true
user User user User
args []string args []string
env []string env []string
cwd string = '/' cwd string = '/'
capabilities Capabilities capabilities Capabilities
rlimits []Rlimit rlimits []Rlimit
no_new_privileges bool no_new_privileges bool @[json: 'noNewPrivileges']
} }
pub struct User { pub struct User {
pub mut: pub mut:
uid u32 uid u32
gid u32 gid u32
additional_gids []u32 additional_gids []u32 @[json: 'additionalGids']
} }
pub struct Capabilities { pub struct Capabilities {
@@ -49,7 +49,7 @@ pub mut:
pub struct Rlimit { pub struct Rlimit {
pub mut: pub mut:
typ string typ string @[json: 'type']
hard u64 hard u64
soft u64 soft u64
} }
@@ -63,26 +63,26 @@ pub mut:
pub struct Mount { pub struct Mount {
pub mut: pub mut:
destination string destination string
typ string typ string @[json: 'type']
source string source string
options []string options []string
} }
pub struct Linux { pub struct Linux {
pub mut: pub mut:
namespaces []LinuxNamespace namespaces []LinuxNamespace
resources LinuxResources resources LinuxResources
devices []LinuxDevice devices []LinuxDevice
masked_paths []string masked_paths []string @[json: 'maskedPaths']
readonly_paths []string readonly_paths []string @[json: 'readonlyPaths']
uid_mappings []LinuxIDMapping uid_mappings []LinuxIDMapping @[json: 'uidMappings']
gid_mappings []LinuxIDMapping gid_mappings []LinuxIDMapping @[json: 'gidMappings']
} }
pub struct LinuxNamespace { pub struct LinuxNamespace {
pub mut: pub mut:
typ string typ string @[json: 'type']
path string path string @[omitempty]
} }
pub struct LinuxResources { pub struct LinuxResources {
@@ -95,47 +95,47 @@ pub mut:
pub struct Memory { pub struct Memory {
pub mut: pub mut:
limit u64 limit u64 @[omitempty]
reservation u64 reservation u64 @[omitempty]
swap u64 swap u64 @[omitempty]
kernel u64 kernel u64 @[omitempty]
swappiness i64 swappiness i64 @[omitempty]
} }
pub struct CPU { pub struct CPU {
pub mut: pub mut:
shares u64 shares u64 @[omitempty]
quota i64 quota i64 @[omitempty]
period u64 period u64 @[omitempty]
cpus string cpus string @[omitempty]
mems string mems string @[omitempty]
} }
pub struct Pids { pub struct Pids {
pub mut: pub mut:
limit i64 limit i64 @[omitempty]
} }
pub struct BlockIO { pub struct BlockIO {
pub mut: pub mut:
weight u16 weight u16 @[omitempty]
} }
pub struct LinuxDevice { pub struct LinuxDevice {
pub mut: pub mut:
path string path string
typ string typ string @[json: 'type']
major i64 major i64
minor i64 minor i64
file_mode u32 file_mode u32 @[json: 'fileMode']
uid u32 uid u32
gid u32 gid u32
} }
pub struct LinuxIDMapping { pub struct LinuxIDMapping {
pub mut: pub mut:
container_id u32 container_id u32 @[json: 'containerID']
host_id u32 host_id u32 @[json: 'hostID']
size u32 size u32
} }
@@ -160,6 +160,8 @@ pub enum MountType {
proc proc
sysfs sysfs
devpts devpts
mqueue
cgroup
nfs nfs
overlay overlay
} }
@@ -235,4 +237,4 @@ pub enum RlimitType {
rlimit_nice rlimit_nice
rlimit_rtprio rlimit_rtprio
rlimit_rttime rlimit_rttime
} }

View File

@@ -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"
]
}
}

View File

@@ -3,16 +3,19 @@ module heropods
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.osal.tmux import freeflowuniverse.herolib.osal.tmux
import freeflowuniverse.herolib.osal.core as osal import freeflowuniverse.herolib.osal.core as osal
import freeflowuniverse.herolib.virt.crun
import time import time
import freeflowuniverse.herolib.builder import freeflowuniverse.herolib.builder
import json import json
@[heap]
pub struct Container { pub struct Container {
pub mut: pub mut:
name string name string
node ?&builder.Node node ?&builder.Node
tmux_pane ?&tmux.Pane tmux_pane ?&tmux.Pane
factory &ContainerFactory crun_config ?&crun.CrunConfig
factory &ContainerFactory
} }
// Struct to parse JSON output of `crun state` // Struct to parse JSON output of `crun state`
@@ -31,10 +34,32 @@ pub fn (mut self Container) start() ! {
if !container_exists { if !container_exists {
// Container doesn't exist, create it first // Container doesn't exist, create it first
console.print_debug('Container ${self.name} does not exist, creating it...') console.print_debug('Container ${self.name} does not exist, creating it...')
osal.exec( // Try to create the container, if it fails with "File exists" error,
cmd: 'crun create --bundle ${self.factory.base_dir}/configs/${self.name} ${self.name}' // 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 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') 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 // because crun doesn't allow restarting a stopped container
if container_exists && status != .running { if container_exists && status != .running {
console.print_debug('Container ${self.name} exists but is stopped, recreating...') 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( 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 stdout: true
)! )!
console.print_debug('Container ${self.name} recreated') console.print_debug('Container ${self.name} recreated')
} }
// start the container (crun start doesn't have --detach flag) // 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') console.print_green('Container ${self.name} started')
} }
@@ -68,12 +95,13 @@ pub fn (mut self Container) stop() ! {
return 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) time.sleep(2 * time.second)
// Force kill if still running // Force kill if still running
if self.status()! == .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') console.print_green('Container ${self.name} stopped')
} }
@@ -86,7 +114,8 @@ pub fn (mut self Container) delete() ! {
} }
self.stop()! 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 // Remove from factory's container cache
if self.name in self.factory.containers { 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 { 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 // Parse JSON output from crun state
state := json.decode(CrunState, result.output) or { return .unknown } 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) // Check if container exists in crun (regardless of its state)
fn (self Container) container_exists_in_crun() !bool { fn (self Container) container_exists_in_crun() !bool {
// Try to get container state - if it fails, container doesn't exist // 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) // If we get here, the container exists (even if stopped/paused)
return result.exit_code == 0 return result.exit_code == 0
@@ -206,7 +241,8 @@ pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane {
// Execute command if provided // Execute command if provided
if args.cmd != '' { 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 self.tmux_pane = pane
@@ -223,6 +259,7 @@ pub fn (mut self Container) node() !&builder.Node {
mut exec := builder.ExecutorCrun{ mut exec := builder.ExecutorCrun{
container_id: self.name container_id: self.name
crun_root: '${self.factory.base_dir}/runtime'
debug: false debug: false
} }
@@ -242,3 +279,58 @@ pub fn (mut self Container) node() !&builder.Node {
self.node = node self.node = node
return 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)!
}

View File

@@ -2,10 +2,9 @@ module heropods
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.osal.core as osal 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 freeflowuniverse.herolib.installers.virt.herorunner as herorunner_installer
import os import os
import x.json2
// Updated enum to be more flexible // Updated enum to be more flexible
pub enum ContainerImageType { pub enum ContainerImageType {
@@ -27,7 +26,7 @@ pub:
pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container { pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
if args.name in self.containers && !args.reset { 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 // 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.') 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 // Create crun configuration using the crun module
self.create_container_config(args.name, rootfs_path)! mut crun_config := self.create_crun_config(args.name, rootfs_path)!
// Ensure crun is installed on host // Ensure crun is installed on host
if !osal.cmd_exists('crun') { 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 // Create container struct but don't create the actual container in crun yet
// The actual container creation will happen in container.start() // The actual container creation will happen in container.start()
mut container := &Container{ mut container := &Container{
name: args.name name: args.name
factory: &self crun_config: crun_config
factory: &self
} }
self.containers[args.name] = container self.containers[args.name] = container
return container return container
} }
// Create OCI config.json from template // Create crun configuration using the crun module
fn (self ContainerFactory) create_container_config(container_name string, rootfs_path string) ! { 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}' config_dir := '${self.base_dir}/configs/${container_name}'
osal.exec(cmd: 'mkdir -p ${config_dir}', stdout: false)! 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' config_path := '${config_dir}/config.json'
mut p := pathlib.get_file(path: config_path, create: true)! config.save_to_file(config_path)!
p.write(json2.encode_pretty(json2.Any(config)))!
return config
} }
// Use podman to pull image and extract rootfs // Use podman to pull image and extract rootfs

View File

@@ -2,7 +2,7 @@ module heropods
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.osal.core as osal import freeflowuniverse.herolib.osal.core as osal
import time import freeflowuniverse.herolib.virt.crun
import os import os
@[heap] @[heap]
@@ -11,6 +11,7 @@ pub mut:
tmux_session string tmux_session string
containers map[string]&Container containers map[string]&Container
images map[string]&ContainerImage images map[string]&ContainerImage
crun_configs map[string]&crun.CrunConfig
base_dir string 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 // Load existing images into cache
self.load_existing_images()! self.load_existing_images()!
@@ -104,7 +110,7 @@ pub fn (mut self ContainerFactory) get(args ContainerNewArgs) !&Container {
if args.name !in self.containers { if args.name !in self.containers {
return error('Container "${args.name}" does not exist. Use factory.new() to create it first.') 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 // Get image by name
@@ -112,7 +118,7 @@ pub fn (mut self ContainerFactory) image_get(name string) !&ContainerImage {
if name !in self.images { if name !in self.images {
return error('Image "${name}" not found in cache. Try importing or downloading it.') 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 // List all containers currently managed by crun
@@ -136,3 +142,34 @@ pub fn (self ContainerFactory) list() ![]Container {
} }
return containers 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 {}
}