Seed ingest/export moves behind ports.SeedRepository (SeedRepo in the postgres adapter; knowledge ingest absorbed from internal/knowledge, package deleted). pct_create flow (defaults, template/VMID/gateway pre-flights, pct create, graph registration) moves from httpapi's approved-execution path into app.ProvisioningService + the ssh provisioner adapter; CLI seed/export/secret become adapters over the services. EntityCreateInput gains EnrolledAt. Plan status corrected: phases 0-7 shipped, 8 + 9 gates open. VERSION 0.34.1.
126 lines
4.7 KiB
Go
126 lines
4.7 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/dtoro/oikos/internal/core/domain"
|
|
"github.com/dtoro/oikos/internal/core/ports"
|
|
"github.com/dtoro/oikos/internal/core/ports/portstest"
|
|
)
|
|
|
|
func newProvisioningTestDeps() (*portstest.FakeProvisioner, *portstest.FakeResolver, *portstest.EntityRepo, *portstest.RelRepo, *ProvisioningService) {
|
|
prov := &portstest.FakeProvisioner{LXCRes: ports.ProvisionResult{VMID: 142, Template: "debian-13-standard_13.0-1_amd64.tar.zst", Output: "create ok"}}
|
|
resolver := &portstest.FakeResolver{Addr: "10.0.0.5", User: "root"}
|
|
entities := portstest.NewEntityRepo()
|
|
rels := portstest.NewRelRepo()
|
|
entities.Create(context.Background(), ports.EntityCreateInput{
|
|
Entity: domain.Entity{ID: "host-1", Slug: "host:hubris", Type: "proxmox-host", Name: "hubris", State: "active"},
|
|
})
|
|
svc := NewProvisioningService(prov, resolver, entities, rels)
|
|
return prov, resolver, entities, rels, svc
|
|
}
|
|
|
|
func TestProvisioningCreateLXCRequiresHostname(t *testing.T) {
|
|
_, _, _, _, svc := newProvisioningTestDeps()
|
|
_, err := svc.CreateLXC(context.Background(), CreateLXCCmd{HostSlug: "host:hubris"})
|
|
if err == nil || err.Error() != "pct_create: hostname is required" {
|
|
t.Fatalf("got %v, want hostname-required error", err)
|
|
}
|
|
}
|
|
|
|
func TestProvisioningCreateLXCHappyPath(t *testing.T) {
|
|
prov, resolver, entities, rels, svc := newProvisioningTestDeps()
|
|
|
|
out, err := svc.CreateLXC(context.Background(), CreateLXCCmd{
|
|
HostSlug: "host:hubris",
|
|
Hostname: "grafana",
|
|
IP: "192.168.8.55",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateLXC: %v", err)
|
|
}
|
|
|
|
if out.Slug != "lxc:grafana" || out.VMID != 142 {
|
|
t.Errorf("outcome = %+v, want slug lxc:grafana vmid 142", out)
|
|
}
|
|
|
|
// Provisioner saw the resolved endpoint and the defaults.
|
|
if len(prov.LXCs) != 1 {
|
|
t.Fatalf("provisioner calls = %d, want 1", len(prov.LXCs))
|
|
}
|
|
in := prov.LXCs[0]
|
|
if in.HostAddr != "10.0.0.5" || in.HostUser != "root" {
|
|
t.Errorf("resolved endpoint = %s@%s, want root@10.0.0.5", in.HostUser, in.HostAddr)
|
|
}
|
|
if in.Cores != 1 || in.MemoryMB != 512 || in.DiskGB != 8 || in.Storage != "local-lvm" {
|
|
t.Errorf("defaults not applied: %+v", in)
|
|
}
|
|
if in.Bridge != "vmbr0" || in.GW != "192.168.8.2" || in.Searchdomain != "hubris.network" {
|
|
t.Errorf("network defaults not applied: %+v", in)
|
|
}
|
|
|
|
// Entity registered: provisioning state, enrolled, vmid/host attrs.
|
|
got, ok := entities.FindBySlug("lxc:grafana")
|
|
if !ok {
|
|
t.Fatal("lxc:grafana not registered")
|
|
}
|
|
if got.Type != "lxc" || got.State != "provisioning" {
|
|
t.Errorf("entity = %s/%s, want lxc/provisioning", got.Type, got.State)
|
|
}
|
|
if got.Attributes["pve_id"] != "142" || got.Attributes["host"] != "hubris" || got.Attributes["ip"] != "192.168.8.55" {
|
|
t.Errorf("attrs = %v", got.Attributes)
|
|
}
|
|
if _, enrolled := entities.Enrolled[got.ID]; !enrolled {
|
|
t.Error("entity not enrolled")
|
|
}
|
|
|
|
// Hosts edge from the Proxmox host to the guest.
|
|
edges, err := rels.ListFor(context.Background(), domain.UUID("host-1"), "outbound")
|
|
if err != nil || len(edges) != 1 {
|
|
t.Fatalf("host edges = %v err=%v, want 1", edges, err)
|
|
}
|
|
if edges[0].Type != "hosts" || edges[0].TargetID != got.ID {
|
|
t.Errorf("edge = %+v, want hosts → %s", edges[0], got.ID)
|
|
}
|
|
if edges[0].Attributes["provisioned_by"] != "nomos" {
|
|
t.Errorf("edge attrs = %v, want provisioned_by=nomos", edges[0].Attributes)
|
|
}
|
|
|
|
_ = resolver // endpoint asserted via provisioner input
|
|
}
|
|
|
|
func TestProvisioningCreateLXCProvisionerFailureRegistersNothing(t *testing.T) {
|
|
prov, _, entities, rels, svc := newProvisioningTestDeps()
|
|
prov.LXCErr = errors.New("no usable LXC template")
|
|
|
|
if _, err := svc.CreateLXC(context.Background(), CreateLXCCmd{HostSlug: "host:hubris", Hostname: "x"}); err == nil {
|
|
t.Fatal("expected provisioner error to propagate")
|
|
}
|
|
if _, ok := entities.FindBySlug("lxc:x"); ok {
|
|
t.Error("failed create must not register an entity")
|
|
}
|
|
if edges, _ := rels.ListFor(context.Background(), domain.UUID("host-1"), "outbound"); len(edges) != 0 {
|
|
t.Errorf("failed create must not create edges, got %v", edges)
|
|
}
|
|
}
|
|
|
|
func TestProvisioningCreateLXCRegistrationBestEffort(t *testing.T) {
|
|
_, _, entities, rels, svc := newProvisioningTestDeps()
|
|
entities.ErrStub = errors.New("db down")
|
|
|
|
// The container exists on the host; a graph write failure must not
|
|
// fail the provisioning outcome.
|
|
out, err := svc.CreateLXC(context.Background(), CreateLXCCmd{HostSlug: "host:hubris", Hostname: "y"})
|
|
if err != nil {
|
|
t.Fatalf("CreateLXC should survive registration failure: %v", err)
|
|
}
|
|
if out.VMID != 142 {
|
|
t.Errorf("outcome = %+v, want provisioner result", out)
|
|
}
|
|
if edges, _ := rels.ListFor(context.Background(), domain.UUID("host-1"), "outbound"); len(edges) != 0 {
|
|
t.Errorf("no edges expected when entity write failed, got %v", edges)
|
|
}
|
|
}
|