Merge branch 'development' into development_heroprompt_v2
This commit is contained in:
62
examples/virt/heropods/heropods.vsh
Executable file
62
examples/virt/heropods/heropods.vsh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.virt.heropods
|
||||
|
||||
// Initialize factory
|
||||
mut factory := heropods.new(
|
||||
reset: false
|
||||
use_podman: true
|
||||
) or { panic('Failed to init ContainerFactory: ${err}') }
|
||||
|
||||
println('=== HeroPods Refactored API Demo ===')
|
||||
|
||||
// Step 1: factory.new() now only creates a container definition/handle
|
||||
// It does NOT create the actual container in the backend yet
|
||||
mut container := factory.new(
|
||||
name: 'myalpine'
|
||||
image: .custom
|
||||
custom_image_name: 'alpine_3_20'
|
||||
docker_url: 'docker.io/library/alpine:3.20'
|
||||
)!
|
||||
|
||||
println('✓ Container definition created: ${container.name}')
|
||||
println(' (No actual container created in backend yet)')
|
||||
|
||||
// Step 2: container.start() handles creation and starting
|
||||
// - Checks if container exists in backend
|
||||
// - Creates it if it doesn't exist
|
||||
// - Starts it if it exists but is stopped
|
||||
println('\n--- First start() call ---')
|
||||
container.start()!
|
||||
println('✓ Container started successfully')
|
||||
|
||||
// Step 3: Multiple start() calls are now idempotent
|
||||
println('\n--- Second start() call (should be idempotent) ---')
|
||||
container.start()!
|
||||
println('✓ Second start() call successful - no errors!')
|
||||
|
||||
// Step 4: Execute commands in the container and save results
|
||||
println('\n--- Executing commands in container ---')
|
||||
result1 := container.exec(cmd: 'ls -la /')!
|
||||
println('✓ Command executed: ls -la /')
|
||||
println('Result: ${result1}')
|
||||
|
||||
result2 := container.exec(cmd: 'echo "Hello from container!"')!
|
||||
println('✓ Command executed: echo "Hello from container!"')
|
||||
println('Result: ${result2}')
|
||||
|
||||
result3 := container.exec(cmd: 'uname -a')!
|
||||
println('✓ Command executed: uname -a')
|
||||
println('Result: ${result3}')
|
||||
|
||||
// Step 5: container.delete() works naturally on the instance
|
||||
println('\n--- Deleting container ---')
|
||||
container.delete()!
|
||||
println('✓ Container deleted successfully')
|
||||
|
||||
println('\n=== Demo completed! ===')
|
||||
println('The refactored API now works as expected:')
|
||||
println('- factory.new() creates definition only')
|
||||
println('- container.start() is idempotent')
|
||||
println('- container.exec() works and returns results')
|
||||
println('- container.delete() works on instances')
|
||||
19
examples/virt/heropods/runcommands.vsh
Normal file
19
examples/virt/heropods/runcommands.vsh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import freeflowuniverse.herolib.virt.heropods
|
||||
|
||||
mut factory := heropods.new(
|
||||
reset: false
|
||||
use_podman: true
|
||||
) or { panic('Failed to init ContainerFactory: ${err}') }
|
||||
|
||||
mut container := factory.new(
|
||||
name: 'myalpine'
|
||||
image: .custom
|
||||
custom_image_name: 'alpine_3_20'
|
||||
docker_url: 'docker.io/library/alpine:3.20'
|
||||
)!
|
||||
|
||||
container.start()!
|
||||
container.exec(cmd: 'ls')!
|
||||
container.stop()!
|
||||
@@ -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.
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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!')
|
||||
@@ -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()!
|
||||
@@ -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()!
|
||||
@@ -1,4 +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
|
||||
|
||||
@@ -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!"
|
||||
@@ -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
|
||||
@@ -2,7 +2,7 @@ module builder
|
||||
|
||||
import freeflowuniverse.herolib.data.ipaddress
|
||||
|
||||
type Executor = ExecutorLocal | ExecutorSSH
|
||||
type Executor = ExecutorLocal | ExecutorSSH | ExecutorCrun
|
||||
|
||||
pub struct ExecutorNewArguments {
|
||||
pub mut:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,14 +15,14 @@ pub mut:
|
||||
debug bool = true
|
||||
}
|
||||
|
||||
fn (mut executor ExecutorCrun) init() ! {
|
||||
pub fn (mut executor ExecutorCrun) init() ! {
|
||||
// Verify container exists and is running
|
||||
result := osal.exec(cmd: 'crun state ${executor.container_id}', stdout: false) or {
|
||||
return error('Container ${executor.container_id} not found or not accessible')
|
||||
}
|
||||
|
||||
// Parse state to ensure container is running
|
||||
if !result.output.contains('"status":"running"') {
|
||||
if !result.output.contains('"status": "running"') {
|
||||
return error('Container ${executor.container_id} is not running')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ pub fn (mut node Node) exec(args ExecArgs) !string {
|
||||
return node.executor.exec(cmd: args.cmd, stdout: args.stdout)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.exec(cmd: args.cmd, stdout: args.stdout)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.exec(cmd: args.cmd, stdout: args.stdout)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -80,6 +82,8 @@ pub fn (mut node Node) exec_silent(cmd string) !string {
|
||||
return node.executor.exec(cmd: cmd, stdout: false)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.exec(cmd: cmd, stdout: false)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.exec(cmd: cmd, stdout: false)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -89,8 +93,11 @@ pub fn (mut node Node) exec_interactive(cmd_ string) ! {
|
||||
node.executor.exec_interactive(cmd: cmd_)!
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
node.executor.exec_interactive(cmd: cmd_)!
|
||||
}
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
node.executor.exec_interactive(cmd: cmd_)!
|
||||
} else {
|
||||
panic('did not find right executor')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut node Node) file_write(path string, text string) ! {
|
||||
@@ -98,6 +105,8 @@ pub fn (mut node Node) file_write(path string, text string) ! {
|
||||
return node.executor.file_write(path, text)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.file_write(path, text)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.file_write(path, text)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -107,6 +116,8 @@ pub fn (mut node Node) file_read(path string) !string {
|
||||
return node.executor.file_read(path)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.file_read(path)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.file_read(path)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -116,6 +127,8 @@ pub fn (mut node Node) file_exists(path string) bool {
|
||||
return node.executor.file_exists(path)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.file_exists(path)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.file_exists(path)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -137,6 +150,8 @@ pub fn (mut node Node) delete(path string) ! {
|
||||
return node.executor.delete(path)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.delete(path)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.delete(path)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -179,6 +194,8 @@ pub fn (mut node Node) download(args_ SyncArgs) ! {
|
||||
return node.executor.download(args)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.download(args)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.download(args)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -208,6 +225,8 @@ pub fn (mut node Node) upload(args_ SyncArgs) ! {
|
||||
return node.executor.upload(args)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.upload(args)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.upload(args)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -224,6 +243,8 @@ pub fn (mut node Node) environ_get(args EnvGetParams) !map[string]string {
|
||||
return node.executor.environ_get()
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.environ_get()
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.environ_get()
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -235,6 +256,8 @@ pub fn (mut node Node) info() map[string]string {
|
||||
return node.executor.info()
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.info()
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.info()
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -244,6 +267,8 @@ pub fn (mut node Node) shell(cmd string) ! {
|
||||
return node.executor.shell(cmd)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.shell(cmd)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.shell(cmd)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -257,6 +282,8 @@ pub fn (mut node Node) list(path string) ![]string {
|
||||
return node.executor.list(path)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.list(path)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.list(path)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -266,6 +293,8 @@ pub fn (mut node Node) dir_exists(path string) bool {
|
||||
return node.executor.dir_exists(path)
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
return node.executor.dir_exists(path)
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
return node.executor.dir_exists(path)
|
||||
}
|
||||
panic('did not find right executor')
|
||||
}
|
||||
@@ -275,8 +304,11 @@ pub fn (mut node Node) debug_off() {
|
||||
node.executor.debug_off()
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
node.executor.debug_off()
|
||||
}
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
node.executor.debug_off()
|
||||
} else {
|
||||
panic('did not find right executor')
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut node Node) debug_on() {
|
||||
@@ -284,6 +316,9 @@ pub fn (mut node Node) debug_on() {
|
||||
node.executor.debug_on()
|
||||
} else if mut node.executor is ExecutorSSH {
|
||||
node.executor.debug_on()
|
||||
}
|
||||
} else if mut node.executor is ExecutorCrun {
|
||||
node.executor.debug_on()
|
||||
} else {
|
||||
panic('did not find right executor')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ fn upload() ! {
|
||||
|
||||
fn install() ! {
|
||||
console.print_header('install herorunner')
|
||||
osal.package_install('
|
||||
xz-utils
|
||||
crun')!
|
||||
osal.package_install('crun')!
|
||||
|
||||
// osal.exec(
|
||||
// cmd: '
|
||||
|
||||
@@ -18,9 +18,6 @@ pub mut:
|
||||
// your checking & initialization code if needed
|
||||
fn obj_init(mycfg_ HeroRunner) !HeroRunner {
|
||||
mut mycfg := mycfg_
|
||||
if mycfg.password == '' && mycfg.secret == '' {
|
||||
return error('password or secret needs to be filled in for ${mycfg.name}')
|
||||
}
|
||||
return mycfg
|
||||
}
|
||||
|
||||
|
||||
@@ -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()!
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
"gid": 0
|
||||
},
|
||||
"args": [
|
||||
"/bin/sh"
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"while true; do sleep 30; done"
|
||||
],
|
||||
"env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
|
||||
@@ -15,13 +15,48 @@ pub mut:
|
||||
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')
|
||||
}
|
||||
@@ -44,13 +79,25 @@ pub fn (mut self Container) stop() ! {
|
||||
}
|
||||
|
||||
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(args osal.ExecArgs) !string {
|
||||
pub fn (mut self Container) exec(cmd_ osal.Command) !string {
|
||||
// Ensure container is running
|
||||
if self.status()! != .running {
|
||||
self.start()!
|
||||
@@ -58,22 +105,17 @@ pub fn (mut self Container) exec(args osal.ExecArgs) !string {
|
||||
|
||||
// Use the builder node to execute inside container
|
||||
mut node := self.node()!
|
||||
return node.exec(cmd: args.cmd, stdout: args.stdout)
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
state := json.decode(CrunState, result.output) or { return .unknown }
|
||||
|
||||
status_str := state['status'] or { json.Any('') }.str()
|
||||
|
||||
return match status_str {
|
||||
return match state.status {
|
||||
'running' { .running }
|
||||
'stopped' { .stopped }
|
||||
'paused' { .paused }
|
||||
@@ -81,6 +123,15 @@ pub fn (self Container) status() !ContainerStatus {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -91,13 +142,12 @@ 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 +158,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
|
||||
}
|
||||
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()
|
||||
bytes := result.output.trim_space().f64()
|
||||
return bytes / (1024 * 1024) // Convert to MB
|
||||
}
|
||||
|
||||
@@ -127,14 +178,13 @@ pub mut:
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
// Get or create tmux session
|
||||
mut session := tmux.session_get(name: tmux_session) or {
|
||||
tmux.session_new(name: tmux_session)!
|
||||
mut session := if t.session_exist(session_name) {
|
||||
t.session_get(session_name)!
|
||||
} else {
|
||||
t.session_create(name: session_name)!
|
||||
}
|
||||
|
||||
// Get or create window
|
||||
@@ -142,10 +192,8 @@ pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane {
|
||||
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()!
|
||||
@@ -158,31 +206,39 @@ pub fn (mut self Container) tmux_pane(args TmuxPaneArgs) !&tmux.Pane {
|
||||
|
||||
// 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
|
||||
// If node already initialized, return it
|
||||
if self.node != none {
|
||||
return self.node
|
||||
}
|
||||
|
||||
mut b := builder.new()!
|
||||
mut node := &builder.Node{
|
||||
name: 'container_${self.name}'
|
||||
executor: executor
|
||||
factory: &b
|
||||
|
||||
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
|
||||
}
|
||||
@@ -3,8 +3,9 @@ module heropods
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.osal.core as osal
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.installers.virt.herorunner as herorunner_installer
|
||||
import os
|
||||
import x.json2
|
||||
|
||||
// Updated enum to be more flexible
|
||||
pub enum ContainerImageType {
|
||||
@@ -36,31 +37,27 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
|
||||
match args.image {
|
||||
.alpine_3_20 {
|
||||
image_name = 'alpine'
|
||||
rootfs_path = '/containers/images/alpine/rootfs'
|
||||
rootfs_path = '${self.base_dir}/images/alpine/rootfs'
|
||||
}
|
||||
.ubuntu_24_04 {
|
||||
image_name = 'ubuntu_24_04'
|
||||
rootfs_path = '/containers/images/ubuntu/24.04/rootfs'
|
||||
rootfs_path = '${self.base_dir}/images/ubuntu/24.04/rootfs'
|
||||
}
|
||||
.ubuntu_25_04 {
|
||||
image_name = 'ubuntu_25_04'
|
||||
rootfs_path = '/containers/images/ubuntu/25.04/rootfs'
|
||||
rootfs_path = '${self.base_dir}/images/ubuntu/25.04/rootfs'
|
||||
}
|
||||
.custom {
|
||||
if args.custom_image_name == '' {
|
||||
return error('custom_image_name is required when using custom image type')
|
||||
}
|
||||
image_name = args.custom_image_name
|
||||
rootfs_path = '/containers/images/${image_name}/rootfs'
|
||||
rootfs_path = '${self.base_dir}/images/${image_name}/rootfs'
|
||||
|
||||
// Check if image exists, if not and docker_url provided, create it
|
||||
// If image not yet extracted, pull and unpack 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
|
||||
)!
|
||||
console.print_debug('Pulling image ${args.docker_url} with podman...')
|
||||
self.podman_pull_and_export(args.docker_url, image_name, rootfs_path)!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,12 +67,17 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
|
||||
return error('Image rootfs not found: ${rootfs_path}. Please ensure the image is available.')
|
||||
}
|
||||
|
||||
// Create container config
|
||||
// Create container config (with terminal disabled) but don't create the container yet
|
||||
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)!
|
||||
// Ensure crun is installed on host
|
||||
if !osal.cmd_exists('crun') {
|
||||
mut herorunner := herorunner_installer.new()!
|
||||
herorunner.install()!
|
||||
}
|
||||
|
||||
// Create container struct but don't create the actual container in crun yet
|
||||
// The actual container creation will happen in container.start()
|
||||
mut container := &Container{
|
||||
name: args.name
|
||||
factory: &self
|
||||
@@ -85,14 +87,63 @@ pub fn (mut self ContainerFactory) new(args ContainerNewArgs) !&Container {
|
||||
return container
|
||||
}
|
||||
|
||||
// Create OCI config.json from template
|
||||
fn (self ContainerFactory) create_container_config(container_name string, rootfs_path string) ! {
|
||||
config_dir := '/containers/configs/${container_name}'
|
||||
config_dir := '${self.base_dir}/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'
|
||||
// Load template
|
||||
mut config_content := $tmpl('config_template.json')
|
||||
|
||||
// Parse JSON with json2
|
||||
mut root := json2.raw_decode(config_content)!
|
||||
mut config := root.as_map()
|
||||
|
||||
// Get or create process map
|
||||
mut process := if 'process' in config {
|
||||
config['process'].as_map()
|
||||
} else {
|
||||
map[string]json2.Any{}
|
||||
}
|
||||
|
||||
// Force disable terminal
|
||||
process['terminal'] = json2.Any(false)
|
||||
config['process'] = json2.Any(process)
|
||||
|
||||
// Write back to config.json
|
||||
config_path := '${config_dir}/config.json'
|
||||
mut p := pathlib.get_file(path: config_path, create: true)!
|
||||
p.write(config_content)!
|
||||
p.write(json2.encode_pretty(json2.Any(config)))!
|
||||
}
|
||||
|
||||
// Use podman to pull image and extract rootfs
|
||||
fn (self ContainerFactory) podman_pull_and_export(docker_url string, image_name string, rootfs_path string) ! {
|
||||
// Pull image
|
||||
osal.exec(
|
||||
cmd: 'podman pull ${docker_url}'
|
||||
stdout: true
|
||||
)!
|
||||
|
||||
// Create temp container
|
||||
temp_name := 'tmp_${image_name}_${os.getpid()}'
|
||||
osal.exec(
|
||||
cmd: 'podman create --name ${temp_name} ${docker_url}'
|
||||
stdout: true
|
||||
)!
|
||||
|
||||
// Export container filesystem
|
||||
osal.exec(
|
||||
cmd: 'mkdir -p ${rootfs_path}'
|
||||
stdout: false
|
||||
)!
|
||||
osal.exec(
|
||||
cmd: 'podman export ${temp_name} | tar -C ${rootfs_path} -xf -'
|
||||
stdout: true
|
||||
)!
|
||||
|
||||
// Cleanup temp container
|
||||
osal.exec(
|
||||
cmd: 'podman rm ${temp_name}'
|
||||
stdout: false
|
||||
)!
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import json
|
||||
@[heap]
|
||||
pub struct ContainerImage {
|
||||
pub mut:
|
||||
image_name string @[required] // image is located in /containers/images/<image_name>/rootfs
|
||||
image_name string @[required] // image is located in ${self.factory.base_dir}/images/<image_name>/rootfs
|
||||
docker_url string // optional docker image URL
|
||||
rootfs_path string // path to the extracted rootfs
|
||||
size_mb f64 // size in MB
|
||||
@@ -21,7 +21,7 @@ pub mut:
|
||||
@[params]
|
||||
pub struct ContainerImageArgs {
|
||||
pub mut:
|
||||
image_name string @[required] // image is located in /containers/images/<image_name>/rootfs
|
||||
image_name string @[required] // image is located in ${self.factory.base_dir}/images/<image_name>/rootfs
|
||||
docker_url string // docker image URL like "alpine:3.20" or "ubuntu:24.04"
|
||||
reset bool
|
||||
}
|
||||
@@ -43,7 +43,7 @@ pub mut:
|
||||
// Create new image or get existing
|
||||
pub fn (mut self ContainerFactory) image_new(args ContainerImageArgs) !&ContainerImage {
|
||||
mut image_name := texttools.name_fix(args.image_name)
|
||||
rootfs_path := '/containers/images/${image_name}/rootfs'
|
||||
rootfs_path := '${self.base_dir}/images/${image_name}/rootfs'
|
||||
|
||||
// Check if image already exists
|
||||
if image_name in self.images && !args.reset {
|
||||
@@ -84,7 +84,7 @@ fn (mut self ContainerImage) download_from_docker(docker_url string, reset bool)
|
||||
console.print_header('Downloading image: ${docker_url}')
|
||||
|
||||
// Clean image name for local storage
|
||||
image_dir := '/containers/images/${self.image_name}'
|
||||
image_dir := '${self.factory.base_dir}/images/${self.image_name}'
|
||||
|
||||
// Remove existing if reset is true
|
||||
if reset && os.is_dir(image_dir) {
|
||||
@@ -133,15 +133,15 @@ fn (mut self ContainerImage) update_metadata() ! {
|
||||
self.size_mb = size_str.f64()
|
||||
|
||||
// Get creation time
|
||||
stat_result := osal.exec(cmd: 'stat -c "%Y" ${self.rootfs_path}', stdout: false)!
|
||||
self.created_at = stat_result.output.trim_space() // TODO: should this be ourtime?
|
||||
info := os.stat(self.rootfs_path) or { return error('stat failed: ${err}') }
|
||||
self.created_at = info.ctime.str() // or mtime.str(), depending on what you want
|
||||
}
|
||||
|
||||
// List all available images
|
||||
pub fn (mut self ContainerFactory) images_list() ![]&ContainerImage {
|
||||
mut images := []&ContainerImage{}
|
||||
|
||||
images_base_dir := '/containers/images'
|
||||
images_base_dir := '${self.base_dir}/images'
|
||||
if !os.is_dir(images_base_dir) {
|
||||
return images
|
||||
}
|
||||
@@ -206,7 +206,7 @@ pub fn (mut self ContainerFactory) image_import(args ImageImportArgs) !&Containe
|
||||
|
||||
console.print_header('Importing image from ${args.source_path}')
|
||||
|
||||
image_dir := '/containers/images/${image_name_clean}'
|
||||
image_dir := '${self.base_dir}/images/${image_name_clean}'
|
||||
rootfs_path := '${image_dir}/rootfs'
|
||||
|
||||
// Check if image already exists
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
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
|
||||
base_dir string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct FactoryInitArgs {
|
||||
pub:
|
||||
reset bool
|
||||
use_podman bool = true // Use podman for image management
|
||||
use_podman bool = true
|
||||
}
|
||||
|
||||
pub fn new(args FactoryInitArgs) !ContainerFactory {
|
||||
@@ -30,13 +29,17 @@ 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)!
|
||||
self.base_dir = os.getenv_opt('CONTAINERS_DIR') or { os.home_dir() + '/.containers' }
|
||||
|
||||
osal.exec(
|
||||
cmd: 'mkdir -p ${self.base_dir}/images ${self.base_dir}/configs ${self.base_dir}/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')
|
||||
}
|
||||
@@ -45,15 +48,32 @@ fn (mut self ContainerFactory) init(args FactoryInitArgs) ! {
|
||||
// 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)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load existing images from filesystem into cache
|
||||
fn (mut self ContainerFactory) load_existing_images() ! {
|
||||
images_base_dir := '/containers/images'
|
||||
images_base_dir := '${self.base_dir}/containers/images'
|
||||
if !os.is_dir(images_base_dir) {
|
||||
return
|
||||
}
|
||||
@@ -70,7 +90,7 @@ fn (mut self ContainerFactory) load_existing_images() ! {
|
||||
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 +100,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 +110,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 { '[]' }
|
||||
result := osal.exec(cmd: 'crun list --format json', stdout: false)!
|
||||
|
||||
// Parse crun list output and populate containers
|
||||
// The output format from crun list is typically tab-separated
|
||||
lines := result.split_into_lines()
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user