CI / build (push) Successful in 30s
Apps opt in with sema.dashboard.* labels in their own compose file. The primary reads its host's containers through a read-only socket proxy, follows Docker events, and merges in what the agent on the second host reports. Icons come from the icon label, the dashboard-icons set or the app's favicon, with a letter as the fallback. Go with only the standard library, a distroless image, and a Gitea Actions workflow copied from FiMa's: checks in the Dockerfile's check stage, a Trivy scan, and a push to git.mariosem.es/msemes/hoda. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
2.2 KiB
Go
78 lines
2.2 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.mariosem.es/msemes/hoda/internal/apps"
|
|
"git.mariosem.es/msemes/hoda/internal/docker"
|
|
)
|
|
|
|
type recorder struct{ got []apps.App }
|
|
|
|
func (r *recorder) Ensure(list []apps.App) { r.got = list }
|
|
|
|
func TestSync(t *testing.T) {
|
|
up := true
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if !up {
|
|
http.Error(w, "down", http.StatusBadGateway)
|
|
return
|
|
}
|
|
fmt.Fprint(w, `[
|
|
{"Id":"a","Names":["/gitea"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"true","sema.dashboard.url":"https://git.mariosem.es"}},
|
|
{"Id":"b","Names":["/broken"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"true"}},
|
|
{"Id":"c","Names":["/off"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"false"}}
|
|
]`)
|
|
}))
|
|
defer srv.Close()
|
|
dc, err := docker.New(srv.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := apps.NewStore()
|
|
icons := &recorder{}
|
|
d := New(dc, store, icons, "sema.dashboard", "host-1", time.Minute, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
|
|
d.sync(context.Background())
|
|
hosts, list := store.Snapshot(nil)
|
|
if len(list) != 1 || list[0].Name != "gitea" || list[0].Host != "host-1" {
|
|
t.Fatalf("apps = %+v, want only gitea", list)
|
|
}
|
|
if len(icons.got) != 1 {
|
|
t.Error("icons were not queued for the apps")
|
|
}
|
|
if _, warned := d.warned["broken"]; !warned {
|
|
t.Error("the container without a url label was not reported")
|
|
}
|
|
if !hosts[0].OK {
|
|
t.Error("host not marked reachable")
|
|
}
|
|
|
|
up = false
|
|
d.sync(context.Background())
|
|
hosts, list = store.Snapshot(nil)
|
|
if hosts[0].OK || len(list) != 1 {
|
|
t.Errorf("Docker down: hosts %+v, %d apps; want unreachable with apps kept", hosts, len(list))
|
|
}
|
|
}
|
|
|
|
func TestRelevant(t *testing.T) {
|
|
for _, action := range []string{"start", "stop", "die", "destroy", "create", "health_status: unhealthy", "pause", "rename"} {
|
|
if !relevant(action) {
|
|
t.Errorf("%q is ignored", action)
|
|
}
|
|
}
|
|
for _, action := range []string{"exec_create: sh -c curl", "exec_start: sh", "exec_die", "attach", "resize", "top", "archive-path"} {
|
|
if relevant(action) {
|
|
t.Errorf("%q triggers a re-list", action)
|
|
}
|
|
}
|
|
}
|