refactor: Migrate container management to heropods module
- Remove `herorun` module and related scripts - Introduce `heropods` module for container management - Enhance `tmux` module with pane clearing and creation - Update `Container` methods to use `osal.Command` result - Improve `ContainerFactory` for image & container handling
This commit is contained in:
@@ -9,10 +9,10 @@ import json
|
||||
|
||||
pub struct Container {
|
||||
pub mut:
|
||||
name string
|
||||
node ?&builder.Node
|
||||
name string
|
||||
node ?&builder.Node
|
||||
tmux_pane ?&tmux.Pane
|
||||
factory &ContainerFactory
|
||||
factory &ContainerFactory
|
||||
}
|
||||
|
||||
pub fn (mut self Container) start() ! {
|
||||
@@ -21,7 +21,7 @@ pub fn (mut self Container) start() ! {
|
||||
console.print_debug('Container ${self.name} is already running')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
osal.exec(cmd: 'crun start ${self.name}', stdout: true)!
|
||||
console.print_green('Container ${self.name} started')
|
||||
}
|
||||
@@ -32,10 +32,10 @@ pub fn (mut self Container) stop() ! {
|
||||
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 {}
|
||||
@@ -50,29 +50,25 @@ pub fn (mut self Container) delete() ! {
|
||||
}
|
||||
|
||||
// Execute command inside the container
|
||||
pub fn (mut self Container) exec(args osal.ExecArgs) !string {
|
||||
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()!
|
||||
return node.exec(cmd: args.cmd, stdout: args.stdout)
|
||||
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
|
||||
}
|
||||
|
||||
result := osal.exec(cmd: 'crun state ${self.name}', stdout: false) or { return .unknown }
|
||||
|
||||
// Parse JSON output from crun state
|
||||
state := json.decode(map[string]json.Any, result) or {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
status_str := state['status'] or { json.Any('') }.str()
|
||||
|
||||
state := json.decode(map[string]json.Any, result.output) or { return .unknown }
|
||||
|
||||
status_str := state['status'].str()
|
||||
|
||||
return match status_str {
|
||||
'running' { .running }
|
||||
'stopped' { .stopped }
|
||||
@@ -91,13 +87,14 @@ pub enum ContainerStatus {
|
||||
// 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
|
||||
}
|
||||
|
||||
result := osal.exec(
|
||||
cmd: 'cat /sys/fs/cgroup/system.slice/crun-${self.name}.scope/cpu.stat'
|
||||
stdout: false
|
||||
) or { return 0.0 }
|
||||
|
||||
// Parse cpu.stat file and calculate usage percentage
|
||||
// This is a simplified implementation
|
||||
for line in result.split_into_lines() {
|
||||
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
|
||||
@@ -108,11 +105,12 @@ pub fn (self Container) cpu_usage() !f64 {
|
||||
|
||||
// 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.trim_space().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
|
||||
}
|
||||
|
||||
@@ -120,69 +118,61 @@ 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
|
||||
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 tmux_session := self.factory.tmux_session
|
||||
if tmux_session == '' {
|
||||
tmux_session = 'herorun'
|
||||
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 tmux session
|
||||
mut session := tmux.session_get(name: tmux_session) or {
|
||||
tmux.session_new(name: tmux_session)!
|
||||
}
|
||||
|
||||
|
||||
// Get or create window
|
||||
mut window := session.window_get(name: args.window_name) or {
|
||||
session.window_new(name: args.window_name)!
|
||||
}
|
||||
|
||||
// Get or create pane
|
||||
mut pane := window.pane_get(nr: args.pane_nr) or {
|
||||
window.pane_new()!
|
||||
}
|
||||
|
||||
|
||||
// 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 != '' {
|
||||
// First enter the container namespace
|
||||
pane.send_keys('crun exec ${self.name} ${args.cmd}')!
|
||||
}
|
||||
|
||||
self.tmux_pane = &pane
|
||||
return &pane
|
||||
|
||||
self.tmux_pane = pane
|
||||
return pane
|
||||
}
|
||||
|
||||
pub fn (mut self Container) node() !&builder.Node {
|
||||
if node := self.node {
|
||||
return node
|
||||
}
|
||||
|
||||
// Create a new ExecutorCrun for this container
|
||||
mut executor := builder.ExecutorCrun{
|
||||
container_id: self.name
|
||||
}
|
||||
|
||||
|
||||
// // Create a new ExecutorCrun for this container
|
||||
// mut executor := builder.ExecutorCrun{
|
||||
// container_id: self.name
|
||||
// }
|
||||
|
||||
mut b := builder.new()!
|
||||
mut node := &builder.Node{
|
||||
name: 'container_${self.name}'
|
||||
executor: executor
|
||||
factory: &b
|
||||
}
|
||||
|
||||
mut node := b.node_new(name: 'container_${self.name}')!
|
||||
|
||||
self.node = node
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,22 +17,22 @@ pub enum ContainerImageType {
|
||||
@[params]
|
||||
pub struct ContainerNewArgs {
|
||||
pub:
|
||||
name string @[required]
|
||||
image ContainerImageType = .alpine_3_20
|
||||
name string @[required]
|
||||
image ContainerImageType = .alpine_3_20
|
||||
custom_image_name string // Used when image = .custom
|
||||
docker_url string // Docker image URL for new images
|
||||
reset bool
|
||||
docker_url string // Docker image URL for new images
|
||||
reset bool
|
||||
}
|
||||
|
||||
pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
|
||||
if args.name in self.containers && !args.reset {
|
||||
return self.containers[args.name]
|
||||
}
|
||||
|
||||
|
||||
// Determine image to use
|
||||
mut image_name := ''
|
||||
mut rootfs_path := ''
|
||||
|
||||
|
||||
match args.image {
|
||||
.alpine_3_20 {
|
||||
image_name = 'alpine'
|
||||
@@ -52,35 +52,38 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
|
||||
}
|
||||
image_name = args.custom_image_name
|
||||
rootfs_path = '/containers/images/${image_name}/rootfs'
|
||||
|
||||
|
||||
// Check if image exists, if not and docker_url provided, create it
|
||||
if !os.is_dir(rootfs_path) && args.docker_url != '' {
|
||||
console.print_debug('Creating new image ${image_name} from ${args.docker_url}')
|
||||
_ = self.image_new(
|
||||
image_name: image_name
|
||||
docker_url: args.docker_url
|
||||
reset: args.reset
|
||||
reset: args.reset
|
||||
)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Verify rootfs exists
|
||||
if !os.is_dir(rootfs_path) {
|
||||
return error('Image rootfs not found: ${rootfs_path}. Please ensure the image is available.')
|
||||
}
|
||||
|
||||
|
||||
// Create container config
|
||||
self.create_container_config(args.name, rootfs_path)!
|
||||
|
||||
|
||||
// Create container using crun
|
||||
osal.exec(cmd: 'crun create --bundle /containers/configs/${args.name} ${args.name}', stdout: true)!
|
||||
|
||||
osal.exec(
|
||||
cmd: 'crun create --bundle /containers/configs/${args.name} ${args.name}'
|
||||
stdout: true
|
||||
)!
|
||||
|
||||
mut container := &Container{
|
||||
name: args.name
|
||||
name: args.name
|
||||
factory: &self
|
||||
}
|
||||
|
||||
|
||||
self.containers[args.name] = container
|
||||
return container
|
||||
}
|
||||
@@ -88,11 +91,11 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
|
||||
fn (self ContainerFactory) create_container_config(container_name string, rootfs_path string) ! {
|
||||
config_dir := '/containers/configs/${container_name}'
|
||||
osal.exec(cmd: 'mkdir -p ${config_dir}', stdout: false)!
|
||||
|
||||
|
||||
// Generate OCI config.json using template
|
||||
config_content := $tmpl('config_template.json')
|
||||
config_path := '${config_dir}/config.json'
|
||||
|
||||
|
||||
mut p := pathlib.get_file(path: config_path, create: true)!
|
||||
p.write(config_content)!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
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 freeflowuniverse.herolib.core.pathlib
|
||||
import os
|
||||
|
||||
@[heap]
|
||||
pub struct ContainerFactory {
|
||||
pub mut:
|
||||
tmux_session string // tmux session name if used
|
||||
tmux_session string
|
||||
containers map[string]&Container
|
||||
images map[string]&ContainerImage // Added images map
|
||||
images map[string]&ContainerImage
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct FactoryInitArgs {
|
||||
pub:
|
||||
reset bool
|
||||
use_podman bool = true // Use podman for image management
|
||||
reset bool
|
||||
use_podman bool = true
|
||||
}
|
||||
|
||||
pub fn new(args FactoryInitArgs) !ContainerFactory {
|
||||
@@ -30,24 +28,43 @@ pub fn new(args FactoryInitArgs) !ContainerFactory {
|
||||
|
||||
fn (mut self ContainerFactory) init(args FactoryInitArgs) ! {
|
||||
// Ensure base directories exist
|
||||
osal.exec(cmd: 'mkdir -p /containers/images /containers/configs /containers/runtime', stdout: false)!
|
||||
|
||||
osal.exec(
|
||||
cmd: 'mkdir -p /containers/images /containers/configs /containers/runtime'
|
||||
stdout: false
|
||||
)!
|
||||
|
||||
if args.use_podman {
|
||||
// Check if podman is installed
|
||||
if !osal.cmd_exists('podman') {
|
||||
console.print_stderr('Warning: podman not found. Installing podman is recommended for better image management.')
|
||||
console.print_debug('You can install podman with: apt install podman (Ubuntu) or brew install podman (macOS)')
|
||||
console.print_stderr('Warning: podman not found. Install podman for better image management.')
|
||||
console.print_debug('Install with: apt install podman (Ubuntu) or brew install podman (macOS)')
|
||||
} else {
|
||||
console.print_debug('Using podman for image management')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Load existing images into cache
|
||||
self.load_existing_images()!
|
||||
|
||||
// Setup default images if they don't exist
|
||||
|
||||
// Setup default images if not using podman
|
||||
if !args.use_podman {
|
||||
self.setup_default_images_legacy(args.reset)!
|
||||
self.setup_default_images(args.reset)!
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut self ContainerFactory) setup_default_images(reset bool) ! {
|
||||
console.print_header('Setting up default images...')
|
||||
|
||||
default_images := [ContainerImageType.alpine_3_20, .ubuntu_24_04, .ubuntu_25_04]
|
||||
|
||||
for img in default_images {
|
||||
mut args := ContainerImageArgs{
|
||||
image_name: img.str()
|
||||
reset: reset
|
||||
}
|
||||
if img.str() !in self.images || reset {
|
||||
console.print_debug('Preparing default image: ${img.str()}')
|
||||
_ = self.image_new(args)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +74,7 @@ fn (mut self ContainerFactory) load_existing_images() ! {
|
||||
if !os.is_dir(images_base_dir) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
dirs := os.ls(images_base_dir) or { return }
|
||||
for dir in dirs {
|
||||
full_path := '${images_base_dir}/${dir}'
|
||||
@@ -65,12 +82,12 @@ fn (mut self ContainerFactory) load_existing_images() ! {
|
||||
rootfs_path := '${full_path}/rootfs'
|
||||
if os.is_dir(rootfs_path) {
|
||||
mut image := &ContainerImage{
|
||||
image_name: dir
|
||||
image_name: dir
|
||||
rootfs_path: rootfs_path
|
||||
factory: &self
|
||||
factory: &self
|
||||
}
|
||||
image.update_metadata() or {
|
||||
console.print_stderr('Failed to load image metadata for ${dir}')
|
||||
console.print_stderr('⚠️ Failed to update metadata for image ${dir}: ${err}')
|
||||
continue
|
||||
}
|
||||
self.images[dir] = image
|
||||
@@ -80,82 +97,9 @@ fn (mut self ContainerFactory) load_existing_images() ! {
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy method for downloading images directly (fallback if no podman)
|
||||
fn (mut self ContainerFactory) setup_default_images_legacy(reset bool) ! {
|
||||
// Setup for all supported images
|
||||
images := [ContainerImage.alpine_3_20, .ubuntu_24_04, .ubuntu_25_04]
|
||||
|
||||
for image in images {
|
||||
match image {
|
||||
.alpine_3_20 {
|
||||
alpine_ver := '3.20.3'
|
||||
alpine_file := 'alpine-minirootfs-${alpine_ver}-x86_64.tar.gz'
|
||||
alpine_url := 'https://dl-cdn.alpinelinux.org/alpine/v${alpine_ver[..4]}/releases/x86_64/${alpine_file}'
|
||||
alpine_dest := '/containers/images/alpine/${alpine_file}'
|
||||
alpine_rootfs := '/containers/images/alpine/rootfs'
|
||||
|
||||
if reset || !os.exists(alpine_rootfs) {
|
||||
osal.download(
|
||||
url: alpine_url
|
||||
dest: alpine_dest
|
||||
minsize_kb: 1024
|
||||
)!
|
||||
|
||||
// Extract alpine rootfs
|
||||
osal.exec(cmd: 'mkdir -p ${alpine_rootfs}', stdout: false)!
|
||||
osal.exec(cmd: 'tar -xzf ${alpine_dest} -C ${alpine_rootfs}', stdout: false)!
|
||||
}
|
||||
console.print_green('Alpine ${alpine_ver} rootfs prepared at ${alpine_rootfs}')
|
||||
}
|
||||
.ubuntu_24_04 {
|
||||
ver := '24.04'
|
||||
codename := 'noble'
|
||||
file := 'ubuntu-${ver}-minimal-cloudimg-amd64-root.tar.xz'
|
||||
url := 'https://cloud-images.ubuntu.com/minimal/releases/${codename}/release/${file}'
|
||||
dest := '/containers/images/ubuntu/${ver}/${file}'
|
||||
rootfs := '/containers/images/ubuntu/${ver}/rootfs'
|
||||
|
||||
if reset || !os.exists(rootfs) {
|
||||
osal.download(
|
||||
url: url
|
||||
dest: dest
|
||||
minsize_kb: 10240
|
||||
)!
|
||||
|
||||
// Extract ubuntu rootfs
|
||||
osal.exec(cmd: 'mkdir -p ${rootfs}', stdout: false)!
|
||||
osal.exec(cmd: 'tar -xf ${dest} -C ${rootfs}', stdout: false)!
|
||||
}
|
||||
console.print_green('Ubuntu ${ver} (${codename}) rootfs prepared at ${rootfs}')
|
||||
}
|
||||
.ubuntu_25_04 {
|
||||
ver := '25.04'
|
||||
codename := 'plucky'
|
||||
file := 'ubuntu-${ver}-minimal-cloudimg-amd64-root.tar.xz'
|
||||
url := 'https://cloud-images.ubuntu.com/daily/minimal/releases/${codename}/release/${file}'
|
||||
dest := '/containers/images/ubuntu/${ver}/${file}'
|
||||
rootfs := '/containers/images/ubuntu/${ver}/rootfs'
|
||||
|
||||
if reset || !os.exists(rootfs) {
|
||||
osal.download(
|
||||
url: url
|
||||
dest: dest
|
||||
minsize_kb: 10240
|
||||
)!
|
||||
|
||||
// Extract ubuntu rootfs
|
||||
osal.exec(cmd: 'mkdir -p ${rootfs}', stdout: false)!
|
||||
osal.exec(cmd: 'tar -xf ${dest} -C ${rootfs}', stdout: false)!
|
||||
}
|
||||
console.print_green('Ubuntu ${ver} (${codename}) rootfs prepared at ${rootfs}')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut self ContainerFactory) get(args ContainerNewArgs) !&Container {
|
||||
if args.name !in self.containers {
|
||||
return error('Container ${args.name} does not exist')
|
||||
return error('Container "${args.name}" does not exist. Use factory.new() to create it first.')
|
||||
}
|
||||
return self.containers[args.name]
|
||||
}
|
||||
@@ -163,18 +107,18 @@ pub fn (mut self ContainerFactory) get(args ContainerNewArgs) !&Container {
|
||||
// Get image by name
|
||||
pub fn (mut self ContainerFactory) image_get(name string) !&ContainerImage {
|
||||
if name !in self.images {
|
||||
return error('Image ${name} does not exist')
|
||||
return error('Image "${name}" not found in cache. Try importing or downloading it.')
|
||||
}
|
||||
return self.images[name]
|
||||
}
|
||||
|
||||
// List all containers currently managed by crun
|
||||
pub fn (self ContainerFactory) list() ![]Container {
|
||||
mut containers := []Container{}
|
||||
result := osal.exec(cmd: 'crun list --format json', stdout: false) or { '[]' }
|
||||
|
||||
// Parse crun list output and populate containers
|
||||
// The output format from crun list is typically tab-separated
|
||||
lines := result.split_into_lines()
|
||||
result := osal.exec(cmd: 'crun list --format json', stdout: false)!
|
||||
|
||||
// Parse crun list output (tab-separated)
|
||||
lines := result.output.split_into_lines()
|
||||
for line in lines {
|
||||
if line.trim_space() == '' || line.starts_with('ID') {
|
||||
continue
|
||||
@@ -182,10 +126,10 @@ pub fn (self ContainerFactory) list() ![]Container {
|
||||
parts := line.split('\t')
|
||||
if parts.len > 0 {
|
||||
containers << Container{
|
||||
name: parts[0]
|
||||
name: parts[0]
|
||||
factory: &self
|
||||
}
|
||||
}
|
||||
}
|
||||
return containers
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user