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:
Mahmoud-Emad
2025-09-07 15:56:59 +03:00
parent 9123c2bcb8
commit a74129ff90
18 changed files with 163 additions and 644 deletions

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run #!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun import freeflowuniverse.herolib.virt.heropods

View File

@@ -1,105 +0,0 @@
# HeroRun - AI Agent Optimized Container Management
**Production-ready scripts for fast remote command execution**
## 🎯 Purpose
Optimized for AI agents that need rapid, reliable command execution with minimal latency and clean output.
## 🏗️ Base Image Types
HeroRun supports different base images through the `BaseImage` enum:
```v
pub enum BaseImage {
alpine // Standard Alpine Linux minirootfs (~5MB)
alpine_python // Alpine Linux with Python 3 pre-installed
}
```
### Usage Examples
**Standard Alpine Container:**
```v
base_image: .alpine // Default - minimal Alpine Linux
```
**Alpine with Python:**
```v
base_image: .alpine_python // Python 3 + pip pre-installed
```
## 📋 Three Scripts
### 1. `setup.vsh` - Environment Preparation
Creates container infrastructure on remote node.
```bash
./setup.vsh
```
**Output:** `Setup complete`
### 2. `execute.vsh` - Fast Command Execution
Executes commands on remote node with clean output only.
```bash
./execute.vsh "command" [context_id]
```
**Examples:**
```bash
./execute.vsh "ls /containers"
./execute.vsh "whoami"
./execute.vsh "echo 'Hello World'"
```
**Output:** Command result only (no verbose logging)
### 3. `cleanup.vsh` - Complete Teardown
Removes container and cleans up all resources.
```bash
./cleanup.vsh
```
**Output:** `Cleanup complete`
## ⚡ Performance Features
- **Clean Output**: Execute returns only command results
- **No Verbose Logging**: Silent operation for production use
- **Fast Execution**: Direct SSH without tmux overhead
- **AI Agent Ready**: Perfect for automated command execution
## 🚀 Usage Pattern
```bash
# Setup once
./setup.vsh
# Execute many commands (fast)
./execute.vsh "ls -la"
./execute.vsh "ps aux"
./execute.vsh "df -h"
# Cleanup when done
./cleanup.vsh
```
## 🎯 AI Agent Integration
Perfect for AI agents that need:
- Rapid command execution
- Clean, parseable output
- Minimal setup overhead
- Production-ready reliability
Each execute call returns only the command output, making it ideal for AI agents to parse and process results.

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun
// Create user with SSH key using sshagent module
mut user := herorun.new_user(keyname: 'id_ed25519')!
// Create executor using proper modules
mut executor := herorun.new_executor(
node_ip: '65.21.132.119'
user: 'root'
container_id: 'ai_agent_container'
keyname: 'id_ed25519'
)!
// Cleanup using tmux and osal modules
executor.cleanup()!
println('Cleanup complete')

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun
import os
// Get command from command line args
if os.args.len < 2 {
println('Usage: ./execute.vsh "command" [context_id]')
exit(1)
}
cmd := os.args[1]
// context_id := if os.args.len > 2 { os.args[2] } else { 'default' }
// Create user with SSH key using sshagent module
mut user := herorun.new_user(keyname: 'id_ed25519')!
// Create executor using proper modules
mut executor := herorun.new_executor(
node_ip: '65.21.132.119'
user: 'root'
container_id: 'ai_agent_container'
keyname: 'id_ed25519'
)!
// Execute command using osal module for clean output
output := executor.execute(cmd)!
// Output only the command result
print(output)

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun
// Create user with SSH key using sshagent module
mut user := herorun.new_user(keyname: 'id_ed25519')!
// Create executor using proper module integration
mut executor := herorun.new_executor(
node_ip: '65.21.132.119'
user: 'root'
container_id: 'ai_agent_container'
keyname: 'id_ed25519'
)!
// Setup using sshagent, tmux, hetznermanager, and osal modules
executor.setup()!
println('Setup complete')

View File

@@ -1,55 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun
// Create user with SSH key using sshagent module
mut user := herorun.new_user(keyname: 'id_ed25519')!
// Create executor with Alpine Python base image
mut executor := herorun.new_executor(
node_ip: '65.21.132.119'
user: 'root'
container_id: 'python_alpine_container'
keyname: 'id_ed25519'
image_script: 'examples/virt/herorun/images/python_server.sh'
base_image: .alpine_python // Use Alpine with Python pre-installed
)!
// Setup container
executor.setup()!
// Create container with Python Alpine base and Python server script
mut container := executor.get_or_create_container(
name: 'python_alpine_container'
image_script: 'examples/virt/herorun/images/python_server.sh'
base_image: .alpine_python
)!
println(' Setup complete with Python Alpine container')
println('Container: python_alpine_container')
println('Base image: Alpine Linux with Python 3 pre-installed')
println('Entry point: python_server.sh')
// Test the container to show Python is available
println('\n🐍 Testing Python availability...')
python_test := executor.execute('runc exec python_alpine_container python3 --version') or {
println(' Python test failed: ${err}')
return
}
println(' Python version: ${python_test}')
println('\n🚀 Running Python HTTP server...')
println('Note: This will start the server and exit (use runc run for persistent server)')
// Run the container to start the Python server
result := executor.execute('runc run python_alpine_container') or {
println(' Container execution failed: ${err}')
return
}
println('📋 Server output:')
println(result)
println('\n🎉 Python Alpine container executed successfully!')
println('💡 The Python HTTP server would run on port 8000 if started persistently')

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun
// Create user with SSH key using sshagent module
mut user := herorun.new_user(keyname: 'id_ed25519')!
// Create executor with image script for Python server
mut executor := herorun.new_executor(
node_ip: '65.21.132.119'
user: 'root'
container_id: 'python_server_container'
keyname: 'id_ed25519'
image_script: 'examples/virt/herorun/images/python_server.sh' // Path to entry point script
)!
// Setup using sshagent, tmux, hetznermanager, and osal modules
executor.setup()!
// Create container with the Python server script
mut container := executor.get_or_create_container(
name: 'python_server_container'
image_script: 'examples/virt/herorun/images/python_server.sh'
)!
println('Setup complete with Python server container')
println('Container: python_server_container')
println('Entry point: examples/virt/herorun/images/python_server.sh (Python HTTP server)')
println('To start the server: runc run python_server_container')

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun
// Create user with SSH key using sshagent module
mut user := herorun.new_user(keyname: 'id_ed25519')!
// Create executor with hello world script
mut executor := herorun.new_executor(
node_ip: '65.21.132.119'
user: 'root'
container_id: 'hello_world_container'
keyname: 'id_ed25519'
image_script: 'examples/virt/herorun/images/hello_world.sh'
)!
// Setup container
executor.setup()!
// Create container with hello world script
mut container := executor.get_or_create_container(
name: 'hello_world_container'
image_script: 'examples/virt/herorun/images/hello_world.sh'
)!
println(' Setup complete with Hello World container')
println('Container: hello_world_container')
println('Entry point: hello_world.sh')
// Run the container to demonstrate it works
println('\n🚀 Running container...')
result := executor.execute('runc run hello_world_container') or {
println(' Container execution failed: ${err}')
return
}
println('📋 Container output:')
println(result)
println('\n🎉 Container executed successfully!')

View File

@@ -1,35 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun
import freeflowuniverse.herolib.ui.console
// Create container factory
mut factory := herorun.new(reset: false)!
// Create a new Alpine container
mut container := factory.new(name: 'test-alpine', image: .alpine_3_20)!
// Start the container
container.start()!
// Execute commands in the container
result := container.exec(cmd: 'ls -la /', stdout: true)!
console.print_debug('Container ls result: ${result}')
// Test file operations
container.exec(cmd: 'echo "Hello from container" > /tmp/test.txt', stdout: false)!
content := container.exec(cmd: 'cat /tmp/test.txt', stdout: false)!
console.print_debug('File content: ${content}')
// Get container status and resource usage
status := container.status()!
cpu := container.cpu_usage()!
mem := container.mem_usage()!
console.print_debug('Container status: ${status}')
console.print_debug('CPU usage: ${cpu}%')
console.print_debug('Memory usage: ${mem} MB')
// Clean up
container.stop()!
container.delete()!

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.virt.herorun
import freeflowuniverse.herolib.builder
import freeflowuniverse.herolib.ui.console
// Create container
mut factory := herorun.new()!
mut container := factory.new(name: 'builder-test', image: .ubuntu_24_04)!
container.start()!
// Get builder node for the container
mut node := container.node()!
// Use builder methods to interact with container
node.file_write('/tmp/script.sh', '
#!/bin/bash
echo "Running from builder node"
whoami
pwd
ls -la /
')!
result := node.exec(cmd: 'chmod +x /tmp/script.sh && /tmp/script.sh', stdout: true)!
console.print_debug('Builder execution result: ${result}')
// Test file operations through builder
exists := node.file_exists('/tmp/script.sh')
console.print_debug('Script exists: ${exists}')
content := node.file_read('/tmp/script.sh')!
console.print_debug('Script content: ${content}')
// Clean up
container.stop()!
container.delete()!

View File

@@ -1,11 +0,0 @@
#!/bin/sh
set -e
echo "🎉 Hello from custom container entry point!"
echo "Container ID: $(hostname)"
echo "Current time: $(date)"
echo "Working directory: $(pwd)"
echo "Available commands:"
ls /bin | head -10
echo "..."
echo "✅ Container is working perfectly!"

View File

@@ -1,74 +0,0 @@
#!/bin/sh
set -e
echo "🐍 Starting Python HTTP server..."
# Allow overriding port via environment variable (default: 8000)
PORT=${PORT:-8000}
HOST=${HOST:-0.0.0.0}
# Check if Python is available
if ! command -v python >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then
echo "❌ Python not found in this container"
echo "💡 To use Python server, you need a container with Python pre-installed"
echo " For now, starting a simple HTTP server using busybox httpd..."
# Create a simple index.html
mkdir -p /tmp/www
cat > /tmp/www/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>Container HTTP Server</title>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.container { max-width: 600px; margin: 0 auto; }
.status { color: #28a745; }
.info { background: #f8f9fa; padding: 20px; border-radius: 5px; }
</style>
</head>
<body>
<div class="container">
<h1>🎉 Container HTTP Server</h1>
<p class="status">✅ Container is running successfully!</p>
<div class="info">
<h3>Server Information:</h3>
<ul>
<li><strong>Server:</strong> BusyBox httpd</li>
<li><strong>Port:</strong> 8000</li>
<li><strong>Container:</strong> Alpine Linux</li>
<li><strong>Status:</strong> Active</li>
</ul>
</div>
<p><em>Note: Python was not available, so we're using BusyBox httpd instead.</em></p>
</div>
</body>
</html>
EOF
echo "📁 Created simple web content at /tmp/www/"
echo "🌐 Would start HTTP server on $HOST:$PORT (if httpd was available)"
echo ""
echo "🎉 Container executed successfully!"
echo "✅ Entry point script is working"
echo "📋 Container contents:"
ls -la /tmp/www/
echo ""
echo "📄 Sample web content:"
cat /tmp/www/index.html | head -10
echo "..."
echo ""
echo "💡 To run a real HTTP server, use a container image with Python or httpd pre-installed"
else
# Use python3 if available, otherwise python
PYTHON_CMD="python3"
if ! command -v python3 >/dev/null 2>&1; then
PYTHON_CMD="python"
fi
echo "✅ Found Python: $PYTHON_CMD"
echo "🌐 Starting Python HTTP server on $HOST:$PORT"
# Use exec so signals (like Ctrl+C) are properly handled
exec $PYTHON_CMD -m http.server "$PORT" --bind "$HOST"
fi

View File

@@ -3,9 +3,7 @@ module builder
import os import os
import rand import rand
import freeflowuniverse.herolib.osal.core as osal import freeflowuniverse.herolib.osal.core as osal
import freeflowuniverse.herolib.osal.rsync
import freeflowuniverse.herolib.core.pathlib import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.data.ipaddress
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.core.texttools import freeflowuniverse.herolib.core.texttools

View File

@@ -7,7 +7,7 @@ import time
import os import os
@[heap] @[heap]
struct Pane { pub struct Pane {
pub mut: pub mut:
window &Window @[str: skip] window &Window @[str: skip]
id int // pane id (e.g., %1, %2) id int // pane id (e.g., %1, %2)
@@ -696,3 +696,22 @@ pub fn (p Pane) logging_status() string {
} }
return 'disabled' return 'disabled'
} }
pub fn (mut p Pane) clear() ! {
// Kill current process in the pane
osal.exec(
cmd: 'tmux send-keys -t %${p.id} C-c'
stdout: false
name: 'tmux_pane_interrupt'
) or {}
// Reset pane by running a new bash
osal.exec(
cmd: "tmux send-keys -t %${p.id} '/bin/bash' Enter"
stdout: false
name: 'tmux_pane_reset_shell'
)!
// Update pane info
p.window.scan()!
}

View File

@@ -406,3 +406,22 @@ pub fn (mut w Window) stop_ttyd(port int) ! {
} }
println('ttyd stopped for window ${w.name} on port ${port} (if it was running)') println('ttyd stopped for window ${w.name} on port ${port} (if it was running)')
} }
// Get a pane by its ID
pub fn (mut w Window) pane_get(id int) !&Pane {
w.scan()! // refresh info from tmux
for pane in w.panes {
if pane.id == id {
return pane
}
}
return error('Pane with id ${id} not found in window ${w.name}. Available panes: ${w.panes}')
}
// Create a new pane (just a split with default shell)
pub fn (mut w Window) pane_new() !&Pane {
return w.pane_split(
cmd: '/bin/bash'
horizontal: true
)
}

View File

@@ -9,10 +9,10 @@ import json
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 factory &ContainerFactory
} }
pub fn (mut self Container) start() ! { pub fn (mut self Container) start() ! {
@@ -50,7 +50,7 @@ pub fn (mut self Container) delete() ! {
} }
// Execute command inside the container // 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 // Ensure container is running
if self.status()! != .running { if self.status()! != .running {
self.start()! self.start()!
@@ -58,20 +58,16 @@ pub fn (mut self Container) exec(args osal.ExecArgs) !string {
// Use the builder node to execute inside container // Use the builder node to execute inside container
mut node := self.node()! 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 { pub fn (self Container) status() !ContainerStatus {
result := osal.exec(cmd: 'crun state ${self.name}', stdout: false) or { result := osal.exec(cmd: 'crun state ${self.name}', stdout: false) or { return .unknown }
return .unknown
}
// Parse JSON output from crun state // Parse JSON output from crun state
state := json.decode(map[string]json.Any, result) or { state := json.decode(map[string]json.Any, result.output) or { return .unknown }
return .unknown
}
status_str := state['status'] or { json.Any('') }.str() status_str := state['status'].str()
return match status_str { return match status_str {
'running' { .running } 'running' { .running }
@@ -91,13 +87,14 @@ pub enum ContainerStatus {
// Get CPU usage in percentage // Get CPU usage in percentage
pub fn (self Container) cpu_usage() !f64 { pub fn (self Container) cpu_usage() !f64 {
// Use cgroup stats to get CPU usage // 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 { result := osal.exec(
return 0.0 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 // Parse cpu.stat file and calculate usage percentage
// This is a simplified implementation // 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') { if line.starts_with('usage_usec') {
usage := line.split(' ')[1].f64() usage := line.split(' ')[1].f64()
return usage / 1000000.0 // Convert to percentage return usage / 1000000.0 // Convert to percentage
@@ -108,11 +105,12 @@ pub fn (self Container) cpu_usage() !f64 {
// Get memory usage in MB // Get memory usage in MB
pub fn (self Container) mem_usage() !f64 { 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 { result := osal.exec(
return 0.0 cmd: 'cat /sys/fs/cgroup/system.slice/crun-${self.name}.scope/memory.current'
} stdout: false
) or { return 0.0 }
bytes := result.trim_space().f64() bytes := result.output.trim_space().f64()
return bytes / (1024 * 1024) // Convert to MB return bytes / (1024 * 1024) // Convert to MB
} }
@@ -120,21 +118,20 @@ pub struct TmuxPaneArgs {
pub mut: pub mut:
window_name string window_name string
pane_nr int pane_nr int
pane_name string // optional pane_name string // optional
cmd string // optional, will execute this cmd cmd string // optional, will execute this cmd
reset bool // if true will reset everything and restart a 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 env map[string]string // optional, will set these env vars in the pane
} }
pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane { pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane {
mut tmux_session := self.factory.tmux_session mut t := tmux.new()!
if tmux_session == '' { session_name := 'herorun'
tmux_session = 'herorun'
}
// Get or create tmux session mut session := if t.session_exist(session_name) {
mut session := tmux.session_get(name: tmux_session) or { t.session_get(session_name)!
tmux.session_new(name: tmux_session)! } else {
t.session_create(name: session_name)!
} }
// Get or create window // Get or create window
@@ -142,10 +139,8 @@ pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane {
session.window_new(name: args.window_name)! session.window_new(name: args.window_name)!
} }
// Get or create pane // Get existing pane by number, or create a new one
mut pane := window.pane_get(nr: args.pane_nr) or { mut pane := window.pane_get(args.pane_nr) or { window.pane_new()! }
window.pane_new()!
}
if args.reset { if args.reset {
pane.clear()! pane.clear()!
@@ -158,12 +153,11 @@ 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 != '' {
// First enter the container namespace
pane.send_keys('crun exec ${self.name} ${args.cmd}')! pane.send_keys('crun exec ${self.name} ${args.cmd}')!
} }
self.tmux_pane = &pane self.tmux_pane = pane
return &pane return pane
} }
pub fn (mut self Container) node() !&builder.Node { pub fn (mut self Container) node() !&builder.Node {
@@ -171,17 +165,13 @@ pub fn (mut self Container) node() !&builder.Node {
return node return node
} }
// Create a new ExecutorCrun for this container // // Create a new ExecutorCrun for this container
mut executor := builder.ExecutorCrun{ // mut executor := builder.ExecutorCrun{
container_id: self.name // container_id: self.name
} // }
mut b := builder.new()! mut b := builder.new()!
mut node := &builder.Node{ mut node := b.node_new(name: 'container_${self.name}')!
name: 'container_${self.name}'
executor: executor
factory: &b
}
self.node = node self.node = node
return node return node

View File

@@ -17,11 +17,11 @@ pub enum ContainerImageType {
@[params] @[params]
pub struct ContainerNewArgs { pub struct ContainerNewArgs {
pub: pub:
name string @[required] name string @[required]
image ContainerImageType = .alpine_3_20 image ContainerImageType = .alpine_3_20
custom_image_name string // Used when image = .custom custom_image_name string // Used when image = .custom
docker_url string // Docker image URL for new images docker_url string // Docker image URL for new images
reset bool reset bool
} }
pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container { pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
@@ -59,7 +59,7 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
_ = self.image_new( _ = self.image_new(
image_name: image_name image_name: image_name
docker_url: args.docker_url docker_url: args.docker_url
reset: args.reset reset: args.reset
)! )!
} }
} }
@@ -74,10 +74,13 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
self.create_container_config(args.name, rootfs_path)! self.create_container_config(args.name, rootfs_path)!
// Create container using crun // 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{ mut container := &Container{
name: args.name name: args.name
factory: &self factory: &self
} }

View File

@@ -1,25 +1,23 @@
module heropods module heropods
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.osal.tmux
import freeflowuniverse.herolib.osal.core as osal import freeflowuniverse.herolib.osal.core as osal
import time import time
import freeflowuniverse.herolib.builder
import freeflowuniverse.herolib.core.pathlib
import os import os
@[heap]
pub struct ContainerFactory { pub struct ContainerFactory {
pub mut: pub mut:
tmux_session string // tmux session name if used tmux_session string
containers map[string]&Container containers map[string]&Container
images map[string]&ContainerImage // Added images map images map[string]&ContainerImage
} }
@[params] @[params]
pub struct FactoryInitArgs { pub struct FactoryInitArgs {
pub: pub:
reset bool reset bool
use_podman bool = true // Use podman for image management use_podman bool = true
} }
pub fn new(args FactoryInitArgs) !ContainerFactory { pub fn new(args FactoryInitArgs) !ContainerFactory {
@@ -30,13 +28,15 @@ pub fn new(args FactoryInitArgs) !ContainerFactory {
fn (mut self ContainerFactory) init(args FactoryInitArgs) ! { fn (mut self ContainerFactory) init(args FactoryInitArgs) ! {
// Ensure base directories exist // 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 { if args.use_podman {
// Check if podman is installed
if !osal.cmd_exists('podman') { if !osal.cmd_exists('podman') {
console.print_stderr('Warning: podman not found. Installing podman is recommended for better image management.') console.print_stderr('Warning: podman not found. Install podman for better image management.')
console.print_debug('You can install podman with: apt install podman (Ubuntu) or brew install podman (macOS)') console.print_debug('Install with: apt install podman (Ubuntu) or brew install podman (macOS)')
} else { } else {
console.print_debug('Using podman for image management') console.print_debug('Using podman for image management')
} }
@@ -45,9 +45,26 @@ fn (mut self ContainerFactory) init(args FactoryInitArgs) ! {
// Load existing images into cache // Load existing images into cache
self.load_existing_images()! self.load_existing_images()!
// Setup default images if they don't exist // Setup default images if not using podman
if !args.use_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)!
}
} }
} }
@@ -65,12 +82,12 @@ fn (mut self ContainerFactory) load_existing_images() ! {
rootfs_path := '${full_path}/rootfs' rootfs_path := '${full_path}/rootfs'
if os.is_dir(rootfs_path) { if os.is_dir(rootfs_path) {
mut image := &ContainerImage{ mut image := &ContainerImage{
image_name: dir image_name: dir
rootfs_path: rootfs_path rootfs_path: rootfs_path
factory: &self factory: &self
} }
image.update_metadata() or { 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 continue
} }
self.images[dir] = image 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 { 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') 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]
} }
@@ -163,18 +107,18 @@ pub fn (mut self ContainerFactory) get(args ContainerNewArgs) !&Container {
// Get image by name // Get image by name
pub fn (mut self ContainerFactory) image_get(name string) !&ContainerImage { pub fn (mut self ContainerFactory) image_get(name string) !&ContainerImage {
if name !in self.images { 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] return self.images[name]
} }
// List all containers currently managed by crun
pub fn (self ContainerFactory) list() ![]Container { pub fn (self ContainerFactory) list() ![]Container {
mut containers := []Container{} mut containers := []Container{}
result := osal.exec(cmd: 'crun list --format json', stdout: false) or { '[]' } result := osal.exec(cmd: 'crun list --format json', stdout: false)!
// Parse crun list output and populate containers // Parse crun list output (tab-separated)
// The output format from crun list is typically tab-separated lines := result.output.split_into_lines()
lines := result.split_into_lines()
for line in lines { for line in lines {
if line.trim_space() == '' || line.starts_with('ID') { if line.trim_space() == '' || line.starts_with('ID') {
continue continue
@@ -182,7 +126,7 @@ pub fn (self ContainerFactory) list() ![]Container {
parts := line.split('\t') parts := line.split('\t')
if parts.len > 0 { if parts.len > 0 {
containers << Container{ containers << Container{
name: parts[0] name: parts[0]
factory: &self factory: &self
} }
} }