CI / build (push) Successful in 40s
Tiles now show the public URL and, when set, the internal URL as a second link. Label names ignore case and - versus _. Containers HODA can't fully use are listed above the tiles with the reason: labels without enable, a missing url, or an unknown label name with a suggestion (category -> group). To find labels without enable, discovery reads all containers. A "Copy labels" button copies every label as a commented template, and falls back to the copy command where the clipboard API is missing (plain http on the LAN). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
3.1 KiB
Go
94 lines
3.1 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"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
|
|
}
|
|
if r.URL.Query().Get("filters") != "" {
|
|
t.Error("containers are filtered, so labels without enable go unreported")
|
|
}
|
|
fmt.Fprint(w, `[
|
|
{"Id":"a","Names":["/gitea"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"true","sema.dashboard.url":"https://git.mariosem.es","sema.dashboard.category":"Dev"}},
|
|
{"Id":"b","Names":["/broken"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"true"}},
|
|
{"Id":"c","Names":["/forgot"],"State":"running","Status":"Up","Labels":{"sema.dashboard.url":"https://x.example"}},
|
|
{"Id":"d","Names":["/off"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"false"}},
|
|
{"Id":"e","Names":["/postgres"],"State":"running","Status":"Up","Labels":{"com.docker.compose.service":"db"}}
|
|
]`)
|
|
}))
|
|
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())
|
|
resp := store.Snapshot(nil)
|
|
if len(resp.Apps) != 1 || resp.Apps[0].Name != "gitea" || resp.Apps[0].Host != "host-1" {
|
|
t.Fatalf("apps = %+v, want only gitea", resp.Apps)
|
|
}
|
|
if len(icons.got) != 1 {
|
|
t.Error("icons were not queued for the apps")
|
|
}
|
|
var problems []string
|
|
for _, issue := range resp.Issues {
|
|
problems = append(problems, issue.Container+": "+issue.Message)
|
|
}
|
|
got := strings.Join(problems, "\n")
|
|
for _, want := range []string{"broken: missing sema.dashboard.url", "forgot: has sema.dashboard labels but no", "gitea: unknown label sema.dashboard.category"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("issues:\n%s\nwant one starting %q", got, want)
|
|
}
|
|
}
|
|
if len(resp.Issues) != 3 {
|
|
t.Errorf("%d issues, want 3 (nothing for the hidden or unlabeled containers)", len(resp.Issues))
|
|
}
|
|
if !resp.Hosts[0].OK {
|
|
t.Error("host not marked reachable")
|
|
}
|
|
|
|
up = false
|
|
d.sync(context.Background())
|
|
resp = store.Snapshot(nil)
|
|
if resp.Hosts[0].OK || len(resp.Apps) != 1 || len(resp.Issues) != 3 {
|
|
t.Errorf("Docker down: hosts %+v, %d apps, %d issues; want unreachable with both kept", resp.Hosts, len(resp.Apps), len(resp.Issues))
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|