Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
This commit is contained in:
347
pkg/backend/proot/proot_test.go
Normal file
347
pkg/backend/proot/proot_test.go
Normal file
@@ -0,0 +1,347 @@
|
||||
package proot
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/armoredgate/volt/pkg/backend"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestName(t *testing.T) {
|
||||
b := New()
|
||||
if b.Name() != "proot" {
|
||||
t.Errorf("expected name 'proot', got %q", b.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
b := New()
|
||||
if b.SupportsVMs() {
|
||||
t.Error("proot should not support VMs")
|
||||
}
|
||||
if b.SupportsServices() {
|
||||
t.Error("proot should not support services")
|
||||
}
|
||||
if !b.SupportsNetworking() {
|
||||
t.Error("proot should support basic networking")
|
||||
}
|
||||
if b.SupportsTuning() {
|
||||
t.Error("proot should not support tuning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
b := New()
|
||||
|
||||
if err := b.Init(tmpDir); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory structure
|
||||
for _, sub := range []string{"containers", "images", "tmp"} {
|
||||
path := filepath.Join(tmpDir, sub)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Errorf("expected directory %s to exist: %v", sub, err)
|
||||
continue
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Errorf("expected %s to be a directory", sub)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify tmp has 0777 permissions
|
||||
info, _ := os.Stat(filepath.Join(tmpDir, "tmp"))
|
||||
if info.Mode().Perm() != 0777 {
|
||||
t.Errorf("expected tmp perms 0777, got %o", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAndDelete(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
b := New()
|
||||
b.Init(tmpDir)
|
||||
|
||||
// Create a container
|
||||
opts := backend.CreateOptions{
|
||||
Name: "test-container",
|
||||
Memory: "512M",
|
||||
CPU: 1,
|
||||
Env: []string{"FOO=bar"},
|
||||
Ports: []backend.PortMapping{{HostPort: 8080, ContainerPort: 80, Protocol: "tcp"}},
|
||||
}
|
||||
|
||||
if err := b.Create(opts); err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify container directory structure
|
||||
cDir := filepath.Join(tmpDir, "containers", "test-container")
|
||||
for _, sub := range []string{"rootfs", "logs"} {
|
||||
path := filepath.Join(cDir, sub)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Errorf("expected %s to exist: %v", sub, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify state.json
|
||||
stateData, err := os.ReadFile(filepath.Join(cDir, "state.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read state.json: %v", err)
|
||||
}
|
||||
var state containerState
|
||||
if err := json.Unmarshal(stateData, &state); err != nil {
|
||||
t.Fatalf("failed to parse state.json: %v", err)
|
||||
}
|
||||
if state.Name != "test-container" {
|
||||
t.Errorf("expected name 'test-container', got %q", state.Name)
|
||||
}
|
||||
if state.Status != "created" {
|
||||
t.Errorf("expected status 'created', got %q", state.Status)
|
||||
}
|
||||
|
||||
// Verify config.yaml
|
||||
cfgData, err := os.ReadFile(filepath.Join(cDir, "config.yaml"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config.yaml: %v", err)
|
||||
}
|
||||
var cfg containerConfig
|
||||
if err := yaml.Unmarshal(cfgData, &cfg); err != nil {
|
||||
t.Fatalf("failed to parse config.yaml: %v", err)
|
||||
}
|
||||
if cfg.Memory != "512M" {
|
||||
t.Errorf("expected memory '512M', got %q", cfg.Memory)
|
||||
}
|
||||
if len(cfg.Ports) != 1 || cfg.Ports[0].HostPort != 8080 {
|
||||
t.Errorf("expected port mapping 8080:80, got %+v", cfg.Ports)
|
||||
}
|
||||
|
||||
// Verify duplicate create fails
|
||||
if err := b.Create(opts); err == nil {
|
||||
t.Error("expected duplicate create to fail")
|
||||
}
|
||||
|
||||
// List should return one container
|
||||
containers, err := b.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
if len(containers) != 1 {
|
||||
t.Errorf("expected 1 container, got %d", len(containers))
|
||||
}
|
||||
|
||||
// Inspect should work
|
||||
info, err := b.Inspect("test-container")
|
||||
if err != nil {
|
||||
t.Fatalf("Inspect failed: %v", err)
|
||||
}
|
||||
if info.Status != "created" {
|
||||
t.Errorf("expected status 'created', got %q", info.Status)
|
||||
}
|
||||
|
||||
// Delete should work
|
||||
if err := b.Delete("test-container", false); err != nil {
|
||||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify directory removed
|
||||
if _, err := os.Stat(cDir); !os.IsNotExist(err) {
|
||||
t.Error("expected container directory to be removed")
|
||||
}
|
||||
|
||||
// List should be empty now
|
||||
containers, err = b.List()
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
if len(containers) != 0 {
|
||||
t.Errorf("expected 0 containers, got %d", len(containers))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyOperations(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
b := New()
|
||||
b.Init(tmpDir)
|
||||
|
||||
// Create a container
|
||||
opts := backend.CreateOptions{Name: "copy-test"}
|
||||
if err := b.Create(opts); err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
// Create a source file on "host"
|
||||
srcFile := filepath.Join(tmpDir, "host-file.txt")
|
||||
os.WriteFile(srcFile, []byte("hello from host"), 0644)
|
||||
|
||||
// Copy to container
|
||||
if err := b.CopyToContainer("copy-test", srcFile, "/etc/test.txt"); err != nil {
|
||||
t.Fatalf("CopyToContainer failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists in rootfs
|
||||
containerFile := filepath.Join(tmpDir, "containers", "copy-test", "rootfs", "etc", "test.txt")
|
||||
data, err := os.ReadFile(containerFile)
|
||||
if err != nil {
|
||||
t.Fatalf("file not found in container: %v", err)
|
||||
}
|
||||
if string(data) != "hello from host" {
|
||||
t.Errorf("expected 'hello from host', got %q", string(data))
|
||||
}
|
||||
|
||||
// Copy from container
|
||||
dstFile := filepath.Join(tmpDir, "from-container.txt")
|
||||
if err := b.CopyFromContainer("copy-test", "/etc/test.txt", dstFile); err != nil {
|
||||
t.Fatalf("CopyFromContainer failed: %v", err)
|
||||
}
|
||||
|
||||
data, err = os.ReadFile(dstFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read copied file: %v", err)
|
||||
}
|
||||
if string(data) != "hello from host" {
|
||||
t.Errorf("expected 'hello from host', got %q", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogs(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
b := New()
|
||||
b.Init(tmpDir)
|
||||
|
||||
// Create a container
|
||||
opts := backend.CreateOptions{Name: "log-test"}
|
||||
b.Create(opts)
|
||||
|
||||
// Write some log lines
|
||||
logDir := filepath.Join(tmpDir, "containers", "log-test", "logs")
|
||||
logFile := filepath.Join(logDir, "current.log")
|
||||
lines := "line1\nline2\nline3\nline4\nline5\n"
|
||||
os.WriteFile(logFile, []byte(lines), 0644)
|
||||
|
||||
// Full logs
|
||||
content, err := b.Logs("log-test", backend.LogOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("Logs failed: %v", err)
|
||||
}
|
||||
if content != lines {
|
||||
t.Errorf("expected full log content, got %q", content)
|
||||
}
|
||||
|
||||
// Tail 2 lines
|
||||
content, err = b.Logs("log-test", backend.LogOptions{Tail: 2})
|
||||
if err != nil {
|
||||
t.Fatalf("Logs tail failed: %v", err)
|
||||
}
|
||||
// Last 2 lines of "line1\nline2\nline3\nline4\nline5\n" split gives 6 elements
|
||||
// (last is empty after trailing \n), so tail 2 gives "line5\n"
|
||||
if content == "" {
|
||||
t.Error("expected some tail output")
|
||||
}
|
||||
|
||||
// No logs available
|
||||
content, err = b.Logs("nonexistent", backend.LogOptions{})
|
||||
if err == nil {
|
||||
// Container doesn't exist, should get error from readState
|
||||
// but Logs reads file directly, so check
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvailable(t *testing.T) {
|
||||
b := New()
|
||||
// Just verify it doesn't panic
|
||||
_ = b.Available()
|
||||
}
|
||||
|
||||
func TestProcessAlive(t *testing.T) {
|
||||
// PID 1 (init) should be alive
|
||||
if !processAlive(1) {
|
||||
t.Error("expected PID 1 to be alive")
|
||||
}
|
||||
|
||||
// PID 0 should not be alive
|
||||
if processAlive(0) {
|
||||
t.Error("expected PID 0 to not be alive")
|
||||
}
|
||||
|
||||
// Very large PID should not be alive
|
||||
if processAlive(999999999) {
|
||||
t.Error("expected PID 999999999 to not be alive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectOS(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// No os-release file
|
||||
result := detectOS(tmpDir)
|
||||
if result != "-" {
|
||||
t.Errorf("expected '-' for missing os-release, got %q", result)
|
||||
}
|
||||
|
||||
// Create os-release
|
||||
etcDir := filepath.Join(tmpDir, "etc")
|
||||
os.MkdirAll(etcDir, 0755)
|
||||
osRelease := `NAME="Ubuntu"
|
||||
VERSION="24.04 LTS (Noble Numbat)"
|
||||
ID=ubuntu
|
||||
PRETTY_NAME="Ubuntu 24.04 LTS"
|
||||
VERSION_ID="24.04"
|
||||
`
|
||||
os.WriteFile(filepath.Join(etcDir, "os-release"), []byte(osRelease), 0644)
|
||||
|
||||
result = detectOS(tmpDir)
|
||||
if result != "Ubuntu 24.04 LTS" {
|
||||
t.Errorf("expected 'Ubuntu 24.04 LTS', got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntrypointDetection(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
b := New()
|
||||
|
||||
cfg := &containerConfig{Name: "test"}
|
||||
|
||||
// Empty rootfs — should fallback to /bin/sh
|
||||
ep, args := b.detectEntrypoint(tmpDir, cfg)
|
||||
if ep != "/bin/sh" {
|
||||
t.Errorf("expected /bin/sh fallback, got %q", ep)
|
||||
}
|
||||
if len(args) != 0 {
|
||||
t.Errorf("expected no args for /bin/sh, got %v", args)
|
||||
}
|
||||
|
||||
// Create /init
|
||||
initPath := filepath.Join(tmpDir, "init")
|
||||
os.WriteFile(initPath, []byte("#!/bin/sh\nexec /bin/sh"), 0755)
|
||||
|
||||
ep, _ = b.detectEntrypoint(tmpDir, cfg)
|
||||
if ep != "/init" {
|
||||
t.Errorf("expected /init, got %q", ep)
|
||||
}
|
||||
|
||||
// Remove /init, create nginx
|
||||
os.Remove(initPath)
|
||||
nginxDir := filepath.Join(tmpDir, "usr", "sbin")
|
||||
os.MkdirAll(nginxDir, 0755)
|
||||
os.WriteFile(filepath.Join(nginxDir, "nginx"), []byte(""), 0755)
|
||||
|
||||
ep, args = b.detectEntrypoint(tmpDir, cfg)
|
||||
if ep != "/usr/sbin/nginx" {
|
||||
t.Errorf("expected /usr/sbin/nginx, got %q", ep)
|
||||
}
|
||||
|
||||
// With port mapping, should use shell wrapper
|
||||
cfg.Ports = []backend.PortMapping{{HostPort: 8080, ContainerPort: 80}}
|
||||
ep, args = b.detectEntrypoint(tmpDir, cfg)
|
||||
if ep != "/bin/sh" {
|
||||
t.Errorf("expected /bin/sh wrapper for nginx with ports, got %q", ep)
|
||||
}
|
||||
if len(args) != 2 || args[0] != "-c" {
|
||||
t.Errorf("expected [-c <shellcmd>] for nginx wrapper, got %v", args)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user