f54a6ecee3
Introduces the data layer for the Workload refactor (see docs/plans/workload-refactor.md): three new tables and store methods, no behavior changes elsewhere yet. - workloads: unifying primitive over Project/Stack/StaticSite, paired via UNIQUE(kind, ref_id). Notification + webhook config hosted here so it lives in one place across kinds. - containers: normalized index of every Tinyforge-managed container with first-class subdomain/proxy_route_id/npm_proxy_id columns (heavily queried by ListProxyRoutes / stale detection). - apps: optional grouping of workloads; schema only, no UI in v1. Foundation only — deployer surgery, reconciler, and consumer switchover land in the next commit.
232 lines
6.5 KiB
Go
232 lines
6.5 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestCreateAndGetContainer(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
c, err := s.CreateContainer(Container{
|
|
WorkloadID: "wl-1", WorkloadKind: "project", Role: "prod",
|
|
ContainerID: "abc123", ImageRef: "nginx:1", ImageTag: "1",
|
|
State: "running", Port: 80, Subdomain: "prod-app",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateContainer: %v", err)
|
|
}
|
|
if c.ID == "" {
|
|
t.Fatal("container ID should be set")
|
|
}
|
|
if c.Host != "local" {
|
|
t.Fatalf("default host should be 'local', got %q", c.Host)
|
|
}
|
|
if c.ExtraJSON != "{}" {
|
|
t.Fatalf("default extra_json should be '{}', got %q", c.ExtraJSON)
|
|
}
|
|
|
|
got, err := s.GetContainerByID(c.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetContainerByID: %v", err)
|
|
}
|
|
if got.ContainerID != "abc123" || got.Subdomain != "prod-app" {
|
|
t.Fatalf("got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestUpsertContainerInsert(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
if err := s.UpsertContainer(Container{
|
|
ID: "fixed-id", WorkloadID: "wl-1", WorkloadKind: "project",
|
|
Role: "dev", State: "running",
|
|
}); err != nil {
|
|
t.Fatalf("UpsertContainer insert: %v", err)
|
|
}
|
|
|
|
got, err := s.GetContainerByID("fixed-id")
|
|
if err != nil {
|
|
t.Fatalf("GetContainerByID: %v", err)
|
|
}
|
|
if got.State != "running" {
|
|
t.Fatalf("got state %q", got.State)
|
|
}
|
|
}
|
|
|
|
func TestUpsertContainerUpdate(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
_ = s.UpsertContainer(Container{
|
|
ID: "fixed-id", WorkloadID: "wl-1", WorkloadKind: "project",
|
|
Role: "dev", State: "starting",
|
|
})
|
|
|
|
if err := s.UpsertContainer(Container{
|
|
ID: "fixed-id", WorkloadID: "wl-1", WorkloadKind: "project",
|
|
Role: "dev", State: "running", Port: 8080,
|
|
}); err != nil {
|
|
t.Fatalf("UpsertContainer update: %v", err)
|
|
}
|
|
|
|
got, _ := s.GetContainerByID("fixed-id")
|
|
if got.State != "running" || got.Port != 8080 {
|
|
t.Fatalf("upsert did not update fields: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestUpsertContainerRequiresID(t *testing.T) {
|
|
s := newTestStore(t)
|
|
if err := s.UpsertContainer(Container{WorkloadID: "wl-1"}); err == nil {
|
|
t.Fatal("UpsertContainer without ID should fail")
|
|
}
|
|
}
|
|
|
|
func TestGetContainerByDockerID(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
c, _ := s.CreateContainer(Container{
|
|
WorkloadID: "wl-1", WorkloadKind: "project", Role: "prod",
|
|
ContainerID: "docker-xyz", State: "running",
|
|
})
|
|
|
|
got, err := s.GetContainerByDockerID("docker-xyz")
|
|
if err != nil {
|
|
t.Fatalf("GetContainerByDockerID: %v", err)
|
|
}
|
|
if got.ID != c.ID {
|
|
t.Fatalf("got container %s, want %s", got.ID, c.ID)
|
|
}
|
|
|
|
if _, err := s.GetContainerByDockerID(""); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("empty docker id should be NotFound, got %v", err)
|
|
}
|
|
if _, err := s.GetContainerByDockerID("ghost"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("unknown docker id should be NotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListContainersByWorkload(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
s.CreateContainer(Container{WorkloadID: "wl-1", WorkloadKind: "project", Role: "a"})
|
|
s.CreateContainer(Container{WorkloadID: "wl-1", WorkloadKind: "project", Role: "b"})
|
|
s.CreateContainer(Container{WorkloadID: "wl-2", WorkloadKind: "project", Role: "c"})
|
|
|
|
out, err := s.ListContainersByWorkload("wl-1")
|
|
if err != nil {
|
|
t.Fatalf("ListContainersByWorkload: %v", err)
|
|
}
|
|
if len(out) != 2 {
|
|
t.Fatalf("expected 2 containers, got %d", len(out))
|
|
}
|
|
}
|
|
|
|
func TestListContainersWithFilter(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
s.CreateContainer(Container{WorkloadID: "wl-1", WorkloadKind: "project", State: "running"})
|
|
s.CreateContainer(Container{WorkloadID: "wl-2", WorkloadKind: "stack", State: "running"})
|
|
s.CreateContainer(Container{WorkloadID: "wl-3", WorkloadKind: "site", State: "stopped"})
|
|
|
|
out, err := s.ListContainers(ContainerFilter{WorkloadKind: "project"})
|
|
if err != nil {
|
|
t.Fatalf("ListContainers kind filter: %v", err)
|
|
}
|
|
if len(out) != 1 || out[0].WorkloadKind != "project" {
|
|
t.Fatalf("kind filter wrong: %+v", out)
|
|
}
|
|
|
|
out, err = s.ListContainers(ContainerFilter{State: "running"})
|
|
if err != nil {
|
|
t.Fatalf("ListContainers state filter: %v", err)
|
|
}
|
|
if len(out) != 2 {
|
|
t.Fatalf("expected 2 running, got %d", len(out))
|
|
}
|
|
|
|
out, err = s.ListContainers(ContainerFilter{})
|
|
if err != nil {
|
|
t.Fatalf("ListContainers no filter: %v", err)
|
|
}
|
|
if len(out) != 3 {
|
|
t.Fatalf("expected 3 with no filter, got %d", len(out))
|
|
}
|
|
}
|
|
|
|
func TestListContainersByApp(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
app, _ := s.CreateApp(App{Name: "my-saas"})
|
|
w1, _ := s.CreateWorkload(Workload{Kind: "project", RefID: "p1", Name: "web", AppID: app.ID})
|
|
w2, _ := s.CreateWorkload(Workload{Kind: "stack", RefID: "s1", Name: "worker", AppID: app.ID})
|
|
wOther, _ := s.CreateWorkload(Workload{Kind: "project", RefID: "p2", Name: "other"})
|
|
|
|
s.CreateContainer(Container{WorkloadID: w1.ID, WorkloadKind: "project"})
|
|
s.CreateContainer(Container{WorkloadID: w2.ID, WorkloadKind: "stack"})
|
|
s.CreateContainer(Container{WorkloadID: wOther.ID, WorkloadKind: "project"})
|
|
|
|
out, err := s.ListContainers(ContainerFilter{AppID: app.ID})
|
|
if err != nil {
|
|
t.Fatalf("ListContainers AppID: %v", err)
|
|
}
|
|
if len(out) != 2 {
|
|
t.Fatalf("expected 2 containers in app, got %d", len(out))
|
|
}
|
|
}
|
|
|
|
func TestUpdateContainerState(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
c, _ := s.CreateContainer(Container{
|
|
WorkloadID: "wl-1", WorkloadKind: "project", State: "starting",
|
|
})
|
|
|
|
if err := s.UpdateContainerState(c.ID, "running"); err != nil {
|
|
t.Fatalf("UpdateContainerState: %v", err)
|
|
}
|
|
got, _ := s.GetContainerByID(c.ID)
|
|
if got.State != "running" {
|
|
t.Fatalf("got state %q", got.State)
|
|
}
|
|
if got.LastSeenAt == "" {
|
|
t.Fatal("last_seen_at should have been bumped")
|
|
}
|
|
}
|
|
|
|
func TestMarkContainerMissing(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
c, _ := s.CreateContainer(Container{
|
|
WorkloadID: "wl-1", WorkloadKind: "project", State: "running",
|
|
})
|
|
if err := s.MarkContainerMissing(c.ID); err != nil {
|
|
t.Fatalf("MarkContainerMissing: %v", err)
|
|
}
|
|
got, _ := s.GetContainerByID(c.ID)
|
|
if got.State != "missing" {
|
|
t.Fatalf("got state %q, want missing", got.State)
|
|
}
|
|
}
|
|
|
|
func TestDeleteContainersByWorkload(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
s.CreateContainer(Container{WorkloadID: "wl-1", WorkloadKind: "project"})
|
|
s.CreateContainer(Container{WorkloadID: "wl-1", WorkloadKind: "project"})
|
|
s.CreateContainer(Container{WorkloadID: "wl-2", WorkloadKind: "project"})
|
|
|
|
if err := s.DeleteContainersByWorkload("wl-1"); err != nil {
|
|
t.Fatalf("DeleteContainersByWorkload: %v", err)
|
|
}
|
|
|
|
out, _ := s.ListContainersByWorkload("wl-1")
|
|
if len(out) != 0 {
|
|
t.Fatalf("expected 0 after delete, got %d", len(out))
|
|
}
|
|
out, _ = s.ListContainersByWorkload("wl-2")
|
|
if len(out) != 1 {
|
|
t.Fatalf("untouched workload should still have 1, got %d", len(out))
|
|
}
|
|
}
|