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
348 lines
8.7 KiB
Go
348 lines
8.7 KiB
Go
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)
|
|
}
|
|
}
|