Accountant: Inventar, where, Checkmk, Abrechnung
This commit is contained in:
1 parent
9f16148cde
commit
dd93277750
59 files changed
+7522
-113
No files matched your search
+18
-8
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -42,6 +43,7 @@ type Config struct {
|
||||
Token string // Einladung, nur beim ersten Start
|
||||
NodeID string // Default aus dem Hostnamen
|
||||
Hostname string // Default os.Hostname()
|
||||
FQDN string // Default automatisch (detectFQDN); nur Information für das Inventar
|
||||
|
||||
DockerSocket string // Default /var/run/docker.sock; "none" schaltet Docker ab
|
||||
BackendHost string // Adresse, unter der der Ingress die Dienste erreicht
|
||||
@@ -65,6 +67,7 @@ type Agent struct {
|
||||
signer *auth.Signer
|
||||
id Identity
|
||||
serverURL string // tatsächlich genutzte Adresse (Override oder node.json)
|
||||
fqdn string // in Run ermittelt
|
||||
|
||||
mu sync.Mutex
|
||||
fileSpecs []ServiceSpec
|
||||
@@ -144,6 +147,7 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.fqdn = detectFQDN(ctx, net.DefaultResolver, a.cfg.FQDN, a.cfg.Hostname, host)
|
||||
hc := a.httpClient()
|
||||
a.runCtx = ctx
|
||||
// Abgebrochener Schlüsselwechsel? Vor dem Verbinden abschließen.
|
||||
@@ -153,7 +157,7 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
a.log.Info("Agent gestartet", "node", a.id.NodeID, "server", a.serverURL, "backend", host,
|
||||
"docker", a.cfg.DockerSocket, "config_dir", a.cfg.ConfigDir)
|
||||
"fqdn", a.fqdn, "docker", a.cfg.DockerSocket, "config_dir", a.cfg.ConfigDir)
|
||||
|
||||
trigger := make(chan struct{}, 1)
|
||||
kick := func() {
|
||||
@@ -235,23 +239,28 @@ func (a *Agent) sendLoop(ctx context.Context, hc *http.Client, host string, trig
|
||||
// gesendet (sonst würden alle Container-Routen entfernt).
|
||||
func (a *Agent) collect(ctx context.Context, host string) (registry.NodeState, error) {
|
||||
state := registry.NodeState{Workloads: []registry.Workload{}}
|
||||
var client *docker.Client
|
||||
if a.dockerAvailable() {
|
||||
client := &docker.Client{Socket: a.cfg.DockerSocket}
|
||||
client = &docker.Client{Socket: a.cfg.DockerSocket}
|
||||
containers, err := client.List(ctx)
|
||||
if err != nil {
|
||||
return state, err
|
||||
}
|
||||
// Gestoppte Container nennen in der Liste keine Ports.
|
||||
// Startzeit und Health stehen nur in den Details; gestoppte Container
|
||||
// nennen ihre Ports ebenfalls nur dort.
|
||||
for i, c := range containers {
|
||||
if c.State == "running" || !enabled(c.Labels) {
|
||||
if !enabled(c.Labels) {
|
||||
continue
|
||||
}
|
||||
ports, err := client.PortBindings(ctx, c.ID)
|
||||
d, err := client.Inspect(ctx, c.ID)
|
||||
if err != nil {
|
||||
a.log.Debug("Portzuordnung nicht lesbar", "container", c.Name(), "err", err)
|
||||
continue // inzwischen entfernt o. Ä. → Workloads warnt
|
||||
a.log.Debug("Container-Details nicht lesbar", "container", c.Name(), "err", err)
|
||||
continue // inzwischen entfernt o. Ä. → Workloads warnt ggf.
|
||||
}
|
||||
containers[i].StartedAt, containers[i].Health = d.StartedAt, d.Health
|
||||
if c.State != "running" {
|
||||
containers[i].Ports = d.Ports
|
||||
}
|
||||
containers[i].Ports = ports
|
||||
}
|
||||
ws, down, warnings := Workloads(containers, host)
|
||||
for _, w := range warnings {
|
||||
@@ -265,6 +274,7 @@ func (a *Agent) collect(ctx context.Context, host string) (registry.NodeState, e
|
||||
a.mu.Unlock()
|
||||
state.Workloads = append(state.Workloads, FileWorkloads(specs, host)...)
|
||||
state.DownServices = append(state.DownServices, a.health.down()...)
|
||||
state.Host = a.hostInfo(ctx, client)
|
||||
return state, nil
|
||||
}
|
||||
|
||||
|
||||
+53
-1
@@ -24,6 +24,7 @@ import (
|
||||
"sultan/pkg/crypto"
|
||||
"sultan/pkg/docker"
|
||||
"sultan/pkg/license"
|
||||
"sultan/pkg/registry"
|
||||
"sultan/pkg/server"
|
||||
"sultan/pkg/store"
|
||||
"sultan/pkg/stream"
|
||||
@@ -179,11 +180,18 @@ func newFakeDocker(t *testing.T, containers ...docker.Container) *fakeDocker {
|
||||
key := fmt.Sprintf("%d/%s", p.PrivatePort, p.Type)
|
||||
bindings[key] = append(bindings[key], map[string]string{"HostIp": p.IP, "HostPort": fmt.Sprint(p.PublicPort)})
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"Id": c.ID, "HostConfig": map[string]any{"PortBindings": bindings}})
|
||||
state := map[string]any{"Status": c.State, "StartedAt": c.StartedAt}
|
||||
if c.Health != "" {
|
||||
state["Health"] = map[string]string{"Status": c.Health}
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"Id": c.ID, "State": state, "HostConfig": map[string]any{"PortBindings": bindings}})
|
||||
return
|
||||
}
|
||||
http.Error(w, `{"message":"No such container"}`, http.StatusNotFound)
|
||||
})
|
||||
mux.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{"Name":"webdocker11","OperatingSystem":"Debian GNU/Linux 13 (trixie)","ServerVersion":"29.8.2"}`))
|
||||
})
|
||||
mux.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
ch := make(chan docker.Event, 8)
|
||||
f.mu.Lock()
|
||||
@@ -872,3 +880,47 @@ func TestServerCandidates(t *testing.T) {
|
||||
t.Errorf("ohne Override: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Zusatzdaten für Inventar und Störfall kommen automatisch beim Server an:
|
||||
// Compose-Projekt/-Verzeichnis, Startzeit, Health je Container; FQDN,
|
||||
// Betriebssystem (des Hosts) und Docker-Version je Knoten. Ein Wechsel des
|
||||
// Health-Status wird nachgemeldet.
|
||||
func TestRun_ReportsMetadata(t *testing.T) {
|
||||
s := startServer(t, "", "")
|
||||
started := time.Date(2026, 10, 16, 8, 15, 30, 0, time.UTC)
|
||||
wp := wordpress()
|
||||
wp.Labels["com.docker.compose.project"] = "kunde-a"
|
||||
wp.Labels["com.docker.compose.service"] = "wordpress"
|
||||
wp.Labels["com.docker.compose.project.working_dir"] = "/srv/kunde-a"
|
||||
wp.StartedAt = started
|
||||
wp.Health = "healthy"
|
||||
dock := newFakeDocker(t, wp)
|
||||
cfg := testConfig(t, filepath.Join(t.TempDir(), "suldata"), s, s.invite(t, "kunde"), dock.socket)
|
||||
cfg.FQDN = "webdocker11.lan.r25dc.de"
|
||||
runAgent(t, cfg)
|
||||
|
||||
var st registry.NodeState
|
||||
eventually(t, "Zustand mit Zusatzdaten", func() bool {
|
||||
var ok bool
|
||||
st, ok = s.NodeState("node-1")
|
||||
return ok && len(st.Workloads) == 1 && st.Host != nil
|
||||
})
|
||||
wantMeta := ®istry.WorkloadMeta{
|
||||
ComposeProject: "kunde-a", ComposeService: "wordpress", ComposeDir: "/srv/kunde-a",
|
||||
State: "running", StartedAt: started, Health: "healthy",
|
||||
}
|
||||
if !reflect.DeepEqual(st.Workloads[0].Meta, wantMeta) {
|
||||
t.Errorf("Meta = %+v, erwartet %+v", st.Workloads[0].Meta, wantMeta)
|
||||
}
|
||||
if h := st.Host; h.FQDN != "webdocker11.lan.r25dc.de" || h.OS != "Debian GNU/Linux 13 (trixie)" || h.DockerVersion != "29.8.2" {
|
||||
t.Errorf("Host = %+v", h)
|
||||
}
|
||||
|
||||
sick := wp
|
||||
sick.Health = "unhealthy"
|
||||
dock.set("health_status: unhealthy", sick)
|
||||
eventually(t, "Health-Wechsel nachgemeldet", func() bool {
|
||||
st, ok := s.NodeState("node-1")
|
||||
return ok && len(st.Workloads) == 1 && st.Workloads[0].Meta != nil && st.Workloads[0].Meta.Health == "unhealthy"
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/docker"
|
||||
"sultan/pkg/registry"
|
||||
)
|
||||
|
||||
// osReleasePath: Quelle des Betriebssystems ohne Docker (systemd-Agent).
|
||||
var osReleasePath = "/etc/os-release"
|
||||
|
||||
// resolver ist der Teil von *net.Resolver, den detectFQDN braucht.
|
||||
type resolver interface {
|
||||
LookupCNAME(ctx context.Context, host string) (string, error)
|
||||
LookupAddr(ctx context.Context, addr string) ([]string, error)
|
||||
}
|
||||
|
||||
// detectFQDN ermittelt den vollständigen Namen des Knotens für Inventar und
|
||||
// Störfall (Spec 5.3): explizit (--fqdn) > Hostname mit Punkt > Hostname
|
||||
// über die Suchdomain aufgelöst > Reverse-Lookup der Backend-Adresse >
|
||||
// kurzer Hostname. Jede Abfrage ist kurz begrenzt; scheitert alles, bleibt
|
||||
// der Hostname.
|
||||
func detectFQDN(ctx context.Context, res resolver, explicit, hostname, backend string) string {
|
||||
if n := normalizeName(explicit); n != "" {
|
||||
return n
|
||||
}
|
||||
short := normalizeName(hostname)
|
||||
if strings.Contains(short, ".") {
|
||||
return short
|
||||
}
|
||||
lookup := func(f func(context.Context) string) string {
|
||||
lctx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
return f(lctx)
|
||||
}
|
||||
if short != "" {
|
||||
if n := lookup(func(c context.Context) string {
|
||||
cname, err := res.LookupCNAME(c, short)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return normalizeName(cname)
|
||||
}); strings.Contains(n, ".") {
|
||||
return n
|
||||
}
|
||||
}
|
||||
if net.ParseIP(backend) != nil {
|
||||
if n := lookup(func(c context.Context) string {
|
||||
names, err := res.LookupAddr(c, backend)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, name := range names {
|
||||
if n := normalizeName(name); strings.Contains(n, ".") {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}); n != "" {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return short
|
||||
}
|
||||
|
||||
func normalizeName(s string) string {
|
||||
return strings.ToLower(strings.TrimSuffix(strings.TrimSpace(s), "."))
|
||||
}
|
||||
|
||||
// cgnat ist 100.64.0.0/10 – der Bereich, aus dem NetBird Adressen vergibt.
|
||||
var cgnat = &net.IPNet{IP: net.IPv4(100, 64, 0, 0).To4(), Mask: net.CIDRMask(10, 32)}
|
||||
|
||||
// netbirdIP wählt die NetBird-Adresse: IPv4 auf wt0, sonst die erste
|
||||
// IPv4-Adresse aus 100.64.0.0/10 (Interfaces alphabetisch).
|
||||
func netbirdIP(addrs map[string][]net.IP) string {
|
||||
for _, ip := range addrs["wt0"] {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4.String()
|
||||
}
|
||||
}
|
||||
names := make([]string, 0, len(addrs))
|
||||
for name := range addrs {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
for _, name := range names {
|
||||
for _, ip := range addrs[name] {
|
||||
if ip4 := ip.To4(); ip4 != nil && cgnat.Contains(ip4) {
|
||||
return ip4.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// interfaceAddrs liest die Adressen aller Interfaces.
|
||||
func interfaceAddrs() map[string][]net.IP {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := map[string][]net.IP{}
|
||||
for _, ifc := range ifaces {
|
||||
addrs, err := ifc.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
out[ifc.Name] = append(out[ifc.Name], ipnet.IP)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// osPrettyName liest PRETTY_NAME (ersatzweise NAME) aus einer os-release-Datei.
|
||||
func osPrettyName(path string) string {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
var name string
|
||||
sc := bufio.NewScanner(f)
|
||||
for sc.Scan() {
|
||||
key, value, ok := strings.Cut(strings.TrimSpace(sc.Text()), "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value = strings.Trim(value, `"'`)
|
||||
switch key {
|
||||
case "PRETTY_NAME":
|
||||
return value
|
||||
case "NAME":
|
||||
name = value
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// hostInfo sammelt die Angaben zum Knoten. Mit Docker kommt das
|
||||
// Betriebssystem aus der Docker-API (das des Hosts, auch wenn der Agent in
|
||||
// einem Container läuft); Fehler lassen Felder nur leer.
|
||||
func (a *Agent) hostInfo(ctx context.Context, client *docker.Client) *registry.HostInfo {
|
||||
h := ®istry.HostInfo{FQDN: a.fqdn, NetBirdIP: netbirdIP(interfaceAddrs())}
|
||||
if client != nil {
|
||||
info, err := client.Info(ctx)
|
||||
if err != nil {
|
||||
a.log.Debug("Docker-Info nicht lesbar", "err", err)
|
||||
}
|
||||
h.OS, h.DockerVersion = info.OperatingSystem, info.ServerVersion
|
||||
}
|
||||
if h.OS == "" {
|
||||
h.OS = osPrettyName(osReleasePath)
|
||||
}
|
||||
return h
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeResolver beantwortet CNAME- und PTR-Anfragen aus Tabellen.
|
||||
type fakeResolver struct {
|
||||
cname map[string]string
|
||||
ptr map[string][]string
|
||||
}
|
||||
|
||||
func (f fakeResolver) LookupCNAME(_ context.Context, host string) (string, error) {
|
||||
if c, ok := f.cname[host]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return "", errors.New("no such host")
|
||||
}
|
||||
|
||||
func (f fakeResolver) LookupAddr(_ context.Context, addr string) ([]string, error) {
|
||||
if p, ok := f.ptr[addr]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return nil, errors.New("no such host")
|
||||
}
|
||||
|
||||
// FQDN: explizit > Hostname mit Punkt > aufgelöster Hostname (Suchdomain) >
|
||||
// Reverse-Lookup der Backend-Adresse > kurzer Hostname.
|
||||
func TestDetectFQDN(t *testing.T) {
|
||||
res := fakeResolver{
|
||||
cname: map[string]string{"webdocker11": "webdocker11.lan.r25dc.de.", "kurz": "kurz"},
|
||||
ptr: map[string][]string{"10.0.0.11": {"backend11.lan.r25dc.de."}},
|
||||
}
|
||||
tests := []struct {
|
||||
name, explicit, hostname, backend, want string
|
||||
}{
|
||||
{"explizit gewinnt", "web.example.de", "webdocker11", "10.0.0.11", "web.example.de"},
|
||||
{"Hostname ist schon FQDN", "", "pi.home.arpa", "10.0.0.11", "pi.home.arpa"},
|
||||
{"über Suchdomain aufgelöst", "", "webdocker11", "10.0.0.11", "webdocker11.lan.r25dc.de"},
|
||||
{"Reverse-Lookup der Backend-Adresse", "", "kurz", "10.0.0.11", "backend11.lan.r25dc.de"},
|
||||
{"nichts auflösbar: kurzer Hostname", "", "solo", "10.9.9.9", "solo"},
|
||||
{"Groß/Klein und Leerraum", " Web.Example.DE ", "x", "", "web.example.de"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := detectFQDN(context.Background(), res, tt.explicit, tt.hostname, tt.backend); got != tt.want {
|
||||
t.Errorf("detectFQDN = %q, erwartet %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// NetBird: Interface wt0, sonst eine Adresse aus 100.64.0.0/10 (CGNAT, wie
|
||||
// NetBird sie vergibt).
|
||||
func TestNetBirdIP(t *testing.T) {
|
||||
ip := net.ParseIP
|
||||
tests := []struct {
|
||||
name string
|
||||
addrs map[string][]net.IP
|
||||
want string
|
||||
}{
|
||||
{"wt0", map[string][]net.IP{"eth0": {ip("10.0.0.11")}, "wt0": {ip("fe80::1"), ip("100.92.10.4")}}, "100.92.10.4"},
|
||||
{"CGNAT auf anderem Interface", map[string][]net.IP{"eth0": {ip("10.0.0.11")}, "nb0": {ip("100.64.3.2")}}, "100.64.3.2"},
|
||||
{"kein Overlay", map[string][]net.IP{"eth0": {ip("10.0.0.11")}, "lo": {ip("127.0.0.1")}}, ""},
|
||||
{"100.128.x ist kein CGNAT", map[string][]net.IP{"eth0": {ip("100.128.0.1")}}, ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := netbirdIP(tt.addrs); got != tt.want {
|
||||
t.Errorf("netbirdIP = %q, erwartet %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ohne Docker (systemd-Agent) kommt das Betriebssystem aus /etc/os-release.
|
||||
func TestOSPrettyName(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "os-release")
|
||||
os.WriteFile(p, []byte("NAME=\"Debian GNU/Linux\"\nPRETTY_NAME=\"Debian GNU/Linux 13 (trixie)\"\nID=debian\n"), 0o644)
|
||||
if got := osPrettyName(p); got != "Debian GNU/Linux 13 (trixie)" {
|
||||
t.Errorf("osPrettyName = %q", got)
|
||||
}
|
||||
os.WriteFile(p, []byte("NAME=Alpine\nID=alpine\n"), 0o644)
|
||||
if got := osPrettyName(p); got != "Alpine" {
|
||||
t.Errorf("ohne PRETTY_NAME: %q, erwartet NAME", got)
|
||||
}
|
||||
if got := osPrettyName(filepath.Join(dir, "fehlt")); got != "" {
|
||||
t.Errorf("fehlende Datei: %q", got)
|
||||
}
|
||||
}
|
||||
+21
-1
@@ -57,7 +57,7 @@ func Workloads(containers []docker.Container, host string) (workloads []registry
|
||||
continue
|
||||
}
|
||||
cand := candidate{
|
||||
workload: registry.Workload{Name: c.Name(), Host: host, Labels: labels},
|
||||
workload: registry.Workload{Name: c.Name(), Host: host, Labels: labels, Meta: meta(c)},
|
||||
running: c.State == "running",
|
||||
}
|
||||
for _, proto := range protocols {
|
||||
@@ -108,6 +108,26 @@ func Workloads(containers []docker.Container, host string) (workloads []registry
|
||||
return workloads, slices.Compact(down), warnings
|
||||
}
|
||||
|
||||
// Labels, die Docker Compose an jeden Container setzt.
|
||||
const (
|
||||
labelComposeProject = "com.docker.compose.project"
|
||||
labelComposeService = "com.docker.compose.service"
|
||||
labelComposeDir = "com.docker.compose.project.working_dir"
|
||||
)
|
||||
|
||||
// meta liefert die Zusatzdaten eines Containers (Spec 5.3) – nur aus dem,
|
||||
// was Docker ohnehin kennt, ohne zusätzliche Labels.
|
||||
func meta(c docker.Container) *registry.WorkloadMeta {
|
||||
return ®istry.WorkloadMeta{
|
||||
ComposeProject: c.Labels[labelComposeProject],
|
||||
ComposeService: c.Labels[labelComposeService],
|
||||
ComposeDir: c.Labels[labelComposeDir],
|
||||
State: c.State,
|
||||
StartedAt: c.StartedAt,
|
||||
Health: c.Health,
|
||||
}
|
||||
}
|
||||
|
||||
// labelNames liefert die Namen (Schreibweise aus dem Label) unter prefix,
|
||||
// z. B. die Router aus traefik.http.routers.<name>.…
|
||||
func labelNames(labels map[string]string, prefix string) []string {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/docker"
|
||||
"sultan/pkg/registry"
|
||||
@@ -38,6 +39,7 @@ func TestWorkloads_TranslatesPublishedPort(t *testing.T) {
|
||||
"traefik.http.routers.wp.rule": "Host(`kunde.de`)",
|
||||
"traefik.http.services.wp.loadbalancer.server.port": "8081",
|
||||
},
|
||||
Meta: ®istry.WorkloadMeta{State: "running"},
|
||||
}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Workloads =\n%+v\nerwartet\n%+v", got, want)
|
||||
@@ -220,3 +222,39 @@ func TestWorkloads_RunningWinsOverStopped(t *testing.T) {
|
||||
t.Errorf("ohne Überschneidung: got=%+v down=%v", got, down)
|
||||
}
|
||||
}
|
||||
|
||||
// Compose-Projekt, -Verzeichnis, Zustand, Startzeit und Health kommen
|
||||
// automatisch aus Docker – ohne zusätzliche Labels.
|
||||
func TestWorkloads_Meta(t *testing.T) {
|
||||
started := time.Date(2026, 10, 16, 8, 15, 30, 0, time.UTC)
|
||||
labels := wpLabels()
|
||||
labels["com.docker.compose.project"] = "kunde-a"
|
||||
labels["com.docker.compose.service"] = "wordpress"
|
||||
labels["com.docker.compose.project.working_dir"] = "/srv/kunde-a"
|
||||
compose := ctr("kunde-a-wordpress-1", labels, pub(80, 8081))
|
||||
compose.StartedAt = started
|
||||
compose.Health = "healthy"
|
||||
|
||||
plain := ctr("solo", map[string]string{
|
||||
"traefik.enable": "true",
|
||||
"traefik.http.routers.so.rule": "Host(`solo.kunde.de`)",
|
||||
}, pub(80, 8082))
|
||||
plain.State = "exited"
|
||||
|
||||
got, _, warnings := Workloads([]docker.Container{compose, plain}, "100.64.0.42")
|
||||
if len(warnings) != 0 || len(got) != 2 {
|
||||
t.Fatalf("Workloads = %+v, Warnungen %v", got, warnings)
|
||||
}
|
||||
want := map[string]*registry.WorkloadMeta{
|
||||
"kunde-a-wordpress-1": {
|
||||
ComposeProject: "kunde-a", ComposeService: "wordpress", ComposeDir: "/srv/kunde-a",
|
||||
State: "running", StartedAt: started, Health: "healthy",
|
||||
},
|
||||
"solo": {State: "exited"},
|
||||
}
|
||||
for _, w := range got {
|
||||
if !reflect.DeepEqual(w.Meta, want[w.Name]) {
|
||||
t.Errorf("%s: Meta = %+v, erwartet %+v", w.Name, w.Meta, want[w.Name])
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user