Implements the hypervisor design's Phase 1: a second runtime.Runtime backend (QEMU) that runs each service component as a Nanos unikernel VM instead of a podman container, selected per-component via a new runtime = "unikernel" service-def field. - internal/runtime/qemu.go: QEMURuntime. Pull extracts the ELF from the OCI image; Run does `ops build` + boots qemu-system-x86_64 with KVM, user-mode net port-forwards, QMP control socket and serial console log; Stop/Remove/Inspect/List/Logs map onto VM lifecycle + state dir. - proto/registry/servicedef: add runtime, memory_mb, vcpus fields (registry migration 5). - agent: holds both runtimes; runtimeFor() selects per component; listAllContainers() merges containers + VMs so drift/status see both. Unikernel runtime auto-enables on nodes with /dev/kvm + ops. Validated end-to-end on straylight: a test service deploys via `mcp deploy --direct`, boots as a Nanos unikernel, serves HTTP through the agent port-forward, and reports running via `mcp status`/`mcp logs`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
406 lines
11 KiB
Go
406 lines
11 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1"
|
|
"git.wntrmute.dev/mc/mcp/internal/registry"
|
|
)
|
|
|
|
func TestPurgeComponentRemoved(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
// Set up a service with a stale component.
|
|
if err := registry.CreateService(a.DB, "mcns", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "coredns",
|
|
Service: "mcns",
|
|
Image: "coredns:latest",
|
|
DesiredState: "running",
|
|
ObservedState: "removed",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
// Insert an event for this component.
|
|
if err := registry.InsertEvent(a.DB, "mcns", "coredns", "running", "removed"); err != nil {
|
|
t.Fatalf("insert event: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{
|
|
DefinedComponents: []string{"mcns/mcns"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if len(resp.Results) != 1 {
|
|
t.Fatalf("expected 1 result, got %d", len(resp.Results))
|
|
}
|
|
|
|
r := resp.Results[0]
|
|
if !r.Purged {
|
|
t.Fatalf("expected purged=true, got reason: %s", r.Reason)
|
|
}
|
|
if r.Service != "mcns" || r.Component != "coredns" {
|
|
t.Fatalf("unexpected result: %s/%s", r.Service, r.Component)
|
|
}
|
|
|
|
// Verify component was deleted.
|
|
_, err = registry.GetComponent(a.DB, "mcns", "coredns")
|
|
if err == nil {
|
|
t.Fatal("component should have been deleted")
|
|
}
|
|
|
|
// Service should also be deleted since it has no remaining components.
|
|
_, err = registry.GetService(a.DB, "mcns")
|
|
if err == nil {
|
|
t.Fatal("service should have been deleted (no remaining components)")
|
|
}
|
|
}
|
|
|
|
func TestPurgeRefusesRunning(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
if err := registry.CreateService(a.DB, "mcr", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "api",
|
|
Service: "mcr",
|
|
Image: "mcr:latest",
|
|
DesiredState: "running",
|
|
ObservedState: "running",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{
|
|
Service: "mcr",
|
|
Component: "api",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if len(resp.Results) != 1 {
|
|
t.Fatalf("expected 1 result, got %d", len(resp.Results))
|
|
}
|
|
if resp.Results[0].Purged {
|
|
t.Fatal("should not purge a running component")
|
|
}
|
|
|
|
// Verify component still exists.
|
|
_, err = registry.GetComponent(a.DB, "mcr", "api")
|
|
if err != nil {
|
|
t.Fatalf("component should still exist: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPurgeRefusesStopped(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
if err := registry.CreateService(a.DB, "mcr", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "api",
|
|
Service: "mcr",
|
|
Image: "mcr:latest",
|
|
DesiredState: "stopped",
|
|
ObservedState: "stopped",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{
|
|
Service: "mcr",
|
|
Component: "api",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if resp.Results[0].Purged {
|
|
t.Fatal("should not purge a stopped component")
|
|
}
|
|
}
|
|
|
|
func TestPurgeSkipsDefinedComponent(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
if err := registry.CreateService(a.DB, "mcns", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "mcns",
|
|
Service: "mcns",
|
|
Image: "mcns:latest",
|
|
DesiredState: "running",
|
|
ObservedState: "exited",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{
|
|
DefinedComponents: []string{"mcns/mcns"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if len(resp.Results) != 1 {
|
|
t.Fatalf("expected 1 result, got %d", len(resp.Results))
|
|
}
|
|
if resp.Results[0].Purged {
|
|
t.Fatal("should not purge a component that is still in service definitions")
|
|
}
|
|
if resp.Results[0].Reason != "still in service definitions" {
|
|
t.Fatalf("unexpected reason: %s", resp.Results[0].Reason)
|
|
}
|
|
}
|
|
|
|
func TestPurgeDryRun(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
if err := registry.CreateService(a.DB, "mcns", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "coredns",
|
|
Service: "mcns",
|
|
Image: "coredns:latest",
|
|
DesiredState: "running",
|
|
ObservedState: "removed",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{
|
|
DryRun: true,
|
|
DefinedComponents: []string{"mcns/mcns"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if len(resp.Results) != 1 {
|
|
t.Fatalf("expected 1 result, got %d", len(resp.Results))
|
|
}
|
|
if !resp.Results[0].Purged {
|
|
t.Fatal("dry run should report purged=true for eligible components")
|
|
}
|
|
|
|
// Verify component was NOT deleted (dry run).
|
|
_, err = registry.GetComponent(a.DB, "mcns", "coredns")
|
|
if err != nil {
|
|
t.Fatalf("component should still exist after dry run: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPurgeServiceFilter(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
// Create two services.
|
|
if err := registry.CreateService(a.DB, "mcns", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "coredns", Service: "mcns", Image: "coredns:latest",
|
|
DesiredState: "running", ObservedState: "removed",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
if err := registry.CreateService(a.DB, "mcr", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "old", Service: "mcr", Image: "old:latest",
|
|
DesiredState: "running", ObservedState: "removed",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
// Purge only mcns.
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{
|
|
Service: "mcns",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if len(resp.Results) != 1 {
|
|
t.Fatalf("expected 1 result, got %d", len(resp.Results))
|
|
}
|
|
if resp.Results[0].Service != "mcns" {
|
|
t.Fatalf("expected mcns, got %s", resp.Results[0].Service)
|
|
}
|
|
|
|
// mcr/old should still exist.
|
|
_, err = registry.GetComponent(a.DB, "mcr", "old")
|
|
if err != nil {
|
|
t.Fatalf("mcr/old should still exist: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPurgeServiceDeletedWhenEmpty(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
if err := registry.CreateService(a.DB, "mcns", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "coredns", Service: "mcns", Image: "coredns:latest",
|
|
DesiredState: "running", ObservedState: "removed",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "old-thing", Service: "mcns", Image: "old:latest",
|
|
DesiredState: "stopped", ObservedState: "unknown",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
// Both components should be purged.
|
|
if len(resp.Results) != 2 {
|
|
t.Fatalf("expected 2 results, got %d", len(resp.Results))
|
|
}
|
|
for _, r := range resp.Results {
|
|
if !r.Purged {
|
|
t.Fatalf("expected purged=true for %s/%s: %s", r.Service, r.Component, r.Reason)
|
|
}
|
|
}
|
|
|
|
// Service should be deleted.
|
|
_, err = registry.GetService(a.DB, "mcns")
|
|
if err == nil {
|
|
t.Fatal("service should have been deleted")
|
|
}
|
|
}
|
|
|
|
func TestPurgeServiceKeptWhenComponentsRemain(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
if err := registry.CreateService(a.DB, "mcns", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
// Stale component (will be purged).
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "coredns", Service: "mcns", Image: "coredns:latest",
|
|
DesiredState: "running", ObservedState: "removed",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
// Live component (will not be purged).
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "mcns", Service: "mcns", Image: "mcns:latest",
|
|
DesiredState: "running", ObservedState: "running",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if len(resp.Results) != 2 {
|
|
t.Fatalf("expected 2 results, got %d", len(resp.Results))
|
|
}
|
|
|
|
// coredns should be purged, mcns should not.
|
|
purged := 0
|
|
for _, r := range resp.Results {
|
|
if r.Purged {
|
|
purged++
|
|
if r.Component != "coredns" {
|
|
t.Fatalf("expected coredns to be purged, got %s", r.Component)
|
|
}
|
|
}
|
|
}
|
|
if purged != 1 {
|
|
t.Fatalf("expected 1 purged, got %d", purged)
|
|
}
|
|
|
|
// Service should still exist.
|
|
_, err = registry.GetService(a.DB, "mcns")
|
|
if err != nil {
|
|
t.Fatalf("service should still exist: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPurgeExitedState(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
if err := registry.CreateService(a.DB, "test", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "old", Service: "test", Image: "old:latest",
|
|
DesiredState: "stopped", ObservedState: "exited",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if len(resp.Results) != 1 || !resp.Results[0].Purged {
|
|
t.Fatalf("exited component should be purgeable")
|
|
}
|
|
}
|
|
|
|
func TestPurgeUnknownState(t *testing.T) {
|
|
rt := &fakeRuntime{}
|
|
a := newTestAgent(t, rt)
|
|
ctx := context.Background()
|
|
|
|
if err := registry.CreateService(a.DB, "test", true, ""); err != nil {
|
|
t.Fatalf("create service: %v", err)
|
|
}
|
|
if err := registry.CreateComponent(a.DB, ®istry.Component{
|
|
Name: "ghost", Service: "test", Image: "ghost:latest",
|
|
DesiredState: "running", ObservedState: "unknown",
|
|
}); err != nil {
|
|
t.Fatalf("create component: %v", err)
|
|
}
|
|
|
|
resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{})
|
|
if err != nil {
|
|
t.Fatalf("PurgeComponent: %v", err)
|
|
}
|
|
|
|
if len(resp.Results) != 1 || !resp.Results[0].Purged {
|
|
t.Fatalf("unknown component should be purgeable")
|
|
}
|
|
}
|