- 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
245 lines
6.5 KiB
V
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
|
|
}
|