142 lines
5.4 KiB
Go
142 lines
5.4 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"git.misaka.ren/m1saka/dsh-web/internal/store"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestReconcileIdentityAndMissing(t *testing.T) {
|
|
t.Setenv("PANEL_CONTAINER_ID", "")
|
|
a, _ := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Fatalf("unexpected destructive request %s", r.Method)
|
|
}
|
|
w.Write([]byte(`[{"Id":"c1","State":"running","Labels":{"dsh-mgr.instance":"one","dsh-mgr.owner":"owner"}},{"Id":"c3","State":"running","Labels":{"dsh-mgr.instance":"orphan","dsh-mgr.owner":"owner"}}]`))
|
|
})
|
|
for _, i := range []store.Instance{{ID: "one", ContainerID: "c1", Status: "exited"}, {ID: "missing", ContainerID: "c2", Status: "running"}, {ID: "locked", ContainerID: "c4", Status: "error"}} {
|
|
i.UID = "owner"
|
|
i.Created = time.Now()
|
|
if err := a.s.AddInstance(i); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := a.Reconcile(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for id, want := range map[string]string{"one": "running", "missing": "error", "locked": "error"} {
|
|
i, err := a.s.GetInstance(id)
|
|
if err != nil || i.Status != want {
|
|
t.Fatalf("%s: %+v %v", id, i, err)
|
|
}
|
|
}
|
|
var count int
|
|
if err := a.s.DB.QueryRow("SELECT count(*) FROM audit WHERE action='reconcile.orphan'").Scan(&count); err != nil || count != 1 {
|
|
t.Fatalf("orphan audit: %d %v", count, err)
|
|
}
|
|
}
|
|
|
|
func TestReconcilePanelNetwork(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
connected, race, connectFail, missing, foreign, wrongDriver, panelFail bool
|
|
wantConnect int
|
|
wantError bool
|
|
}{
|
|
{name: "already connected", connected: true},
|
|
{name: "reconnect", wantConnect: 1},
|
|
{name: "concurrent connection", race: true, wantConnect: 1},
|
|
{name: "connect failure", connectFail: true, wantConnect: 1, wantError: true},
|
|
{name: "missing network", missing: true, wantError: true},
|
|
{name: "foreign network", foreign: true, wantError: true},
|
|
{name: "not bridge", wrongDriver: true, wantError: true},
|
|
{name: "panel inspect failure", panelFail: true, wantError: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Setenv("PANEL_CONTAINER_ID", "configured-panel")
|
|
connects := 0
|
|
connected := tc.connected
|
|
a, _ := runtimeTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method + " " + r.URL.Path {
|
|
case "GET /v1.47/containers/json":
|
|
fmt.Fprint(w, `[{"Id":"c1","State":"running","Labels":{"dsh-mgr.instance":"one","dsh-mgr.owner":"owner"}},{"Id":"foreign","State":"running","Labels":{"dsh-mgr.instance":"foreign","dsh-mgr.owner":"other"}},{"Id":"unknown","State":"running","Labels":{"dsh-mgr.instance":"unknown","dsh-mgr.owner":"owner"}}]`)
|
|
case "GET /v1.47/networks/" + runtimeNetwork("one"):
|
|
if tc.missing {
|
|
http.Error(w, "network not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
owner, driver := "one", "bridge"
|
|
if tc.foreign {
|
|
owner = "other"
|
|
}
|
|
if tc.wrongDriver {
|
|
driver = "host"
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]any{"Id": "network-id", "Name": runtimeNetwork("one"), "Driver": driver, "Labels": map[string]string{"dsh-mgr.instance": owner}})
|
|
case "GET /v1.47/containers/configured-panel/json", "GET /v1.47/containers/panel-full-id/json":
|
|
if tc.panelFail {
|
|
http.Error(w, "panel missing", http.StatusNotFound)
|
|
return
|
|
}
|
|
networkID := "unrelated-network"
|
|
if connected {
|
|
networkID = "network-id"
|
|
}
|
|
fmt.Fprintf(w, `{"Id":"panel-full-id","NetworkSettings":{"Networks":{"bridge":{"NetworkID":%q}}}}`, networkID)
|
|
case "POST /v1.47/networks/network-id/connect":
|
|
connects++
|
|
var body map[string]string
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body) != 1 || body["Container"] != "panel-full-id" {
|
|
t.Errorf("unsafe connection body: %v, %v", body, err)
|
|
}
|
|
if tc.connectFail || tc.race {
|
|
connected = tc.race
|
|
http.Error(w, "connection failed", http.StatusForbidden)
|
|
return
|
|
}
|
|
connected = true
|
|
w.WriteHeader(http.StatusOK)
|
|
default:
|
|
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
http.Error(w, "unexpected request", http.StatusBadRequest)
|
|
}
|
|
})
|
|
for _, i := range []store.Instance{
|
|
{ID: "one", UID: "owner", ContainerID: "c1", Status: "exited", Created: time.Now()},
|
|
{ID: "foreign", UID: "owner", ContainerID: "foreign", Status: "running", Created: time.Now()},
|
|
} {
|
|
if err := a.s.AddInstance(i); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := a.Reconcile(context.Background()); (err != nil) != tc.wantError {
|
|
t.Fatalf("Reconcile error = %v, want error %v", err, tc.wantError)
|
|
}
|
|
if !tc.wantError {
|
|
if err := a.Reconcile(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if connects != tc.wantConnect {
|
|
t.Fatalf("connections = %d, want %d", connects, tc.wantConnect)
|
|
}
|
|
wantStatus, wantAudits := "running", 0
|
|
if tc.wantError {
|
|
wantStatus, wantAudits = "error", 1
|
|
}
|
|
i, err := a.s.GetInstance("one")
|
|
if err != nil || i.Status != wantStatus {
|
|
t.Fatalf("instance: %+v %v", i, err)
|
|
}
|
|
var audits int
|
|
if err := a.s.DB.QueryRow("SELECT count(*) FROM audit WHERE action='reconcile.network_error'").Scan(&audits); err != nil || audits != wantAudits {
|
|
t.Fatalf("network audits: %d %v", audits, err)
|
|
}
|
|
})
|
|
}
|
|
}
|