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
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 rand
import freeflowuniverse.herolib.osal.core as osal
import freeflowuniverse.herolib.osal.rsync
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.data.ipaddress
import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.core.texttools

View File

@@ -7,7 +7,7 @@ import time
import os
@[heap]
struct Pane {
pub struct Pane {
pub mut:
window &Window @[str: skip]
id int // pane id (e.g., %1, %2)
@@ -696,3 +696,22 @@ pub fn (p Pane) logging_status() string {
}
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)')
}
// 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 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
}
}

View File

@@ -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)!
}
}

View File

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