Files
herolib/lib/virt/heropods/container.v
Mahmoud-Emad 41c8f7cf6d feat: Add basic heropods container example
- Initialize `heropods` factory using Podman
- Create, start, and stop a custom `alpine` container
- Execute a command within the container
- Add debug log for container command execution
2025-09-08 14:29:26 +03:00

245 lines
6.5 KiB
V

module heropods
import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.osal.tmux
import freeflowuniverse.herolib.osal.core as osal
import time
import freeflowuniverse.herolib.builder
import json
pub struct Container {
pub mut:
name string
node ?&builder.Node
tmux_pane ?&tmux.Pane
factory &ContainerFactory
}
// Struct to parse JSON output of `crun state`
struct CrunState {
id string
status string
pid int
bundle string
created string
}
pub fn (mut self Container) start() ! {
// Check if container exists in crun
container_exists := self.container_exists_in_crun()!
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}'
stdout: true
)!
console.print_debug('Container ${self.name} created')
}
status := self.status()!
if status == .running {
console.print_debug('Container ${self.name} is already running')
return
}
// If container exists but is stopped, we need to delete and recreate it
// 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 {}
osal.exec(
cmd: 'crun 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)!
console.print_green('Container ${self.name} started')
}
pub fn (mut self Container) stop() ! {
status := self.status()!
if status == .stopped {
console.print_debug('Container ${self.name} is already stopped')
return
}
osal.exec(cmd: 'crun 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 {}
}
console.print_green('Container ${self.name} stopped')
}
pub fn (mut self Container) delete() ! {
// Check if container exists before trying to delete
if !self.container_exists_in_crun()! {
console.print_debug('Container ${self.name} does not exist, nothing to delete')
return
}
self.stop()!
osal.exec(cmd: 'crun delete ${self.name}', stdout: false) or {}
// Remove from factory's container cache
if self.name in self.factory.containers {
self.factory.containers.delete(self.name)
}
console.print_green('Container ${self.name} deleted')
}
// Execute command inside the container
pub fn (mut self Container) exec(cmd_ osal.Command) !string {
// Ensure container is running
if self.status()! != .running {
self.start()!
}
// Use the builder node to execute inside container
mut node := self.node()!
console.print_debug('Executing command in container ${self.name}: ${cmd_.cmd}')
return node.exec(cmd: cmd_.cmd, stdout: cmd_.stdout)
}
pub fn (self Container) status() !ContainerStatus {
result := osal.exec(cmd: 'crun state ${self.name}', stdout: false) or { return .unknown }
// Parse JSON output from crun state
state := json.decode(CrunState, result.output) or { return .unknown }
return match state.status {
'running' { .running }
'stopped' { .stopped }
'paused' { .paused }
else { .unknown }
}
}
// 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 }
// If we get here, the container exists (even if stopped/paused)
return result.exit_code == 0
}
pub enum ContainerStatus {
running
stopped
paused
unknown
}
// Get CPU usage in percentage
pub fn (self Container) cpu_usage() !f64 {
// Use cgroup stats to get CPU usage
result := osal.exec(
cmd: 'cat /sys/fs/cgroup/system.slice/crun-${self.name}.scope/cpu.stat'
stdout: false
) or { return 0.0 }
for line in result.output.split_into_lines() {
if line.starts_with('usage_usec') {
usage := line.split(' ')[1].f64()
return usage / 1000000.0 // Convert to percentage
}
}
return 0.0
}
// Get memory usage in MB
pub fn (self Container) mem_usage() !f64 {
result := osal.exec(
cmd: 'cat /sys/fs/cgroup/system.slice/crun-${self.name}.scope/memory.current'
stdout: false
) or { return 0.0 }
bytes := result.output.trim_space().f64()
return bytes / (1024 * 1024) // Convert to MB
}
pub struct TmuxPaneArgs {
pub mut:
window_name string
pane_nr int
pane_name string // optional
cmd string // optional, will execute this cmd
reset bool // if true will reset everything and restart a cmd
env map[string]string // optional, will set these env vars in the pane
}
pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane {
mut t := tmux.new()!
session_name := 'herorun'
mut session := if t.session_exist(session_name) {
t.session_get(session_name)!
} else {
t.session_create(name: session_name)!
}
// Get or create window
mut window := session.window_get(name: args.window_name) or {
session.window_new(name: args.window_name)!
}
// Get existing pane by number, or create a new one
mut pane := window.pane_get(args.pane_nr) or { window.pane_new()! }
if args.reset {
pane.clear()!
}
// Set environment variables if provided
for key, value in args.env {
pane.send_keys('export ${key}="${value}"')!
}
// Execute command if provided
if args.cmd != '' {
pane.send_keys('crun exec ${self.name} ${args.cmd}')!
}
self.tmux_pane = pane
return pane
}
pub fn (mut self Container) node() !&builder.Node {
// If node already initialized, return it
if self.node != none {
return self.node
}
mut b := builder.new()!
mut exec := builder.ExecutorCrun{
container_id: self.name
debug: false
}
exec.init() or {
return error('Failed to init ExecutorCrun for container ${self.name}: ${err}')
}
// Create node using the factory method, then override the executor
mut node := b.node_new(name: 'container_${self.name}', ipaddr: 'localhost')!
node.executor = exec
node.platform = .alpine
node.cputype = .intel
node.done = map[string]string{}
node.environment = map[string]string{}
node.hostname = self.name
self.node = node
return node
}