Files
sultan/pkg/agent/workloads_test.go
T

261 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import (
"reflect"
"strings"
"testing"
"time"
"sultan/pkg/docker"
"sultan/pkg/registry"
)
func ctr(name string, labels map[string]string, ports ...docker.Port) docker.Container {
return docker.Container{ID: name + "-id", Names: []string{"/" + name}, State: "running", Labels: labels, Ports: ports}
}
func pub(private, public uint16) docker.Port {
return docker.Port{IP: "0.0.0.0", PrivatePort: private, PublicPort: public, Type: "tcp"}
}
func wpLabels() map[string]string {
return map[string]string{
"traefik.enable": "true",
"traefik.http.routers.wp.rule": "Host(`kunde.de`)",
"traefik.http.services.wp.loadbalancer.server.port": "80",
}
}
func TestWorkloads_TranslatesPublishedPort(t *testing.T) {
in := ctr("wordpress", wpLabels(), pub(80, 8081), pub(443, 8443))
got, _, warnings := Workloads([]docker.Container{in}, "100.64.0.42")
if len(warnings) != 0 {
t.Errorf("Warnungen: %v", warnings)
}
want := []registry.Workload{{
Name: "wordpress", Host: "100.64.0.42",
Labels: map[string]string{
"traefik.enable": "true",
"traefik.http.routers.wp.rule": "Host(`kunde.de`)",
"traefik.http.services.wp.loadbalancer.server.port": "8081",
},
Meta: &registry.WorkloadMeta{State: "running"},
}}
if !reflect.DeepEqual(got, want) {
t.Errorf("Workloads =\n%+v\nerwartet\n%+v", got, want)
}
if in.Labels["traefik.http.services.wp.loadbalancer.server.port"] != "80" {
t.Error("Eingabe-Labels wurden verändert")
}
}
func TestWorkloads_SkipsDisabled(t *testing.T) {
off := wpLabels()
off["traefik.enable"] = "false"
none := wpLabels()
delete(none, "traefik.enable")
got, _, warnings := Workloads([]docker.Container{
ctr("aus", off, pub(80, 8081)),
ctr("ohne", none, pub(80, 8082)),
ctr("db", map[string]string{}, pub(5432, 5432)),
}, "h")
if len(got) != 0 || len(warnings) != 0 {
t.Errorf("got=%v warnings=%v", got, warnings)
}
}
func TestWorkloads_UnreachablePorts(t *testing.T) {
tests := map[string][]docker.Port{
"nicht veröffentlicht": {{PrivatePort: 80, Type: "tcp"}},
"nur IPv4-Loopback": {{IP: "127.0.0.1", PrivatePort: 80, PublicPort: 8081, Type: "tcp"}},
"nur IPv6-Loopback": {{IP: "::1", PrivatePort: 80, PublicPort: 8081, Type: "tcp"}},
"nur UDP": {{IP: "0.0.0.0", PrivatePort: 80, PublicPort: 8081, Type: "udp"}},
"anderer Port": {pub(8080, 8081)},
}
for name, ports := range tests {
got, _, warnings := Workloads([]docker.Container{ctr("wordpress", wpLabels(), ports...)}, "h")
if len(got) != 0 {
t.Errorf("%s: Workload trotz unerreichbarem Port: %+v", name, got)
}
if len(warnings) != 1 || !strings.Contains(warnings[0], "wordpress") || !strings.Contains(warnings[0], "80") {
t.Errorf("%s: Warnungen = %v", name, warnings)
}
}
}
func TestWorkloads_IPv6AnyIsReachable(t *testing.T) {
got, _, _ := Workloads([]docker.Container{ctr("wordpress", wpLabels(),
docker.Port{IP: "::", PrivatePort: 80, PublicPort: 8081, Type: "tcp"})}, "h")
if len(got) != 1 || got[0].Labels["traefik.http.services.wp.loadbalancer.server.port"] != "8081" {
t.Errorf("got = %+v", got)
}
}
// Wie Traefiks Docker-Provider: ohne Service-Labels gibt es einen Service
// mit dem Containernamen und dem einzigen veröffentlichten Port.
func TestWorkloads_DefaultService(t *testing.T) {
labels := map[string]string{"traefik.enable": "true", "traefik.http.routers.app.rule": "Host(`app.kunde.de`)"}
got, _, warnings := Workloads([]docker.Container{ctr("my.app", labels, pub(3000, 13000))}, "h")
if len(warnings) != 0 || len(got) != 1 {
t.Fatalf("got=%+v warnings=%v", got, warnings)
}
if got[0].Labels["traefik.http.services.my-app.loadbalancer.server.port"] != "13000" {
t.Errorf("Default-Service fehlt: %v", got[0].Labels)
}
_, _, warnings = Workloads([]docker.Container{ctr("multi", labels, pub(3000, 13000), pub(3001, 13001))}, "h")
if len(warnings) != 1 || !strings.Contains(warnings[0], "multi") {
t.Errorf("mehrere Ports ohne Port-Label: Warnungen = %v", warnings)
}
}
func TestWorkloads_ServiceWithoutPortLabel(t *testing.T) {
labels := map[string]string{
"traefik.enable": "true",
"traefik.http.routers.wp.rule": "Host(`kunde.de`)",
"traefik.http.services.wp.loadbalancer.passhostheader": "true",
}
got, _, warnings := Workloads([]docker.Container{ctr("wordpress", labels, pub(80, 8081))}, "h")
if len(warnings) != 0 || len(got) != 1 || got[0].Labels["traefik.http.services.wp.loadbalancer.server.port"] != "8081" {
t.Errorf("got=%+v warnings=%v", got, warnings)
}
}
func TestWorkloads_MultipleServicesAndCaseInsensitiveKeys(t *testing.T) {
labels := map[string]string{
"Traefik.Enable": "TRUE",
"traefik.http.routers.web.rule": "Host(`kunde.de`)",
"traefik.http.routers.web.service": "web",
"traefik.http.routers.api.rule": "Host(`api.kunde.de`)",
"traefik.http.routers.api.service": "api",
"traefik.http.services.web.loadbalancer.server.port": "80",
"Traefik.HTTP.Services.Api.LoadBalancer.Server.Port": "9000",
}
got, _, warnings := Workloads([]docker.Container{ctr("app", labels, pub(80, 8080), pub(9000, 19000))}, "h")
if len(warnings) != 0 || len(got) != 1 {
t.Fatalf("got=%+v warnings=%v", got, warnings)
}
l := got[0].Labels
if l["traefik.http.services.web.loadbalancer.server.port"] != "8080" || l["Traefik.HTTP.Services.Api.LoadBalancer.Server.Port"] != "19000" {
t.Errorf("Labels = %v", l)
}
}
func TestWorkloads_SortedByName(t *testing.T) {
got, _, _ := Workloads([]docker.Container{
ctr("b", wpLabels(), pub(80, 8082)),
ctr("a", wpLabels(), pub(80, 8081)),
}, "h")
if len(got) != 2 || got[0].Name != "a" || got[1].Name != "b" {
t.Errorf("Reihenfolge: %+v", got)
}
}
func stopped(c docker.Container, state string) docker.Container {
c.State = state
return c
}
// Gestoppt (nicht entfernt): Route bleibt, Services gelten als down → die
// Policy leitet zur Wartungsseite statt 404. Die Ports kommen bei gestoppten
// Containern aus PortBindings (der Agent setzt sie vorher in Ports ein).
func TestWorkloads_StoppedContainerIsDown(t *testing.T) {
for _, state := range []string{"exited", "created", "restarting", "paused", "dead"} {
got, down, warnings := Workloads([]docker.Container{stopped(ctr("nginx", wpLabels(), pub(80, 9080)), state)}, "h")
if len(warnings) != 0 || len(got) != 1 {
t.Fatalf("%s: got=%+v warnings=%v", state, got, warnings)
}
if got[0].Labels["traefik.http.services.wp.loadbalancer.server.port"] != "9080" {
t.Errorf("%s: Labels = %v", state, got[0].Labels)
}
if !reflect.DeepEqual(down, []string{"wp"}) {
t.Errorf("%s: down = %v, erwartet [wp]", state, down)
}
}
// Default-Service und mehrere Services, alle down, sortiert.
labels := map[string]string{"traefik.enable": "true", "traefik.http.routers.app.rule": "Host(`app.kunde.de`)"}
_, down, _ := Workloads([]docker.Container{stopped(ctr("my.app", labels, pub(3000, 13000)), "exited")}, "h")
if !reflect.DeepEqual(down, []string{"my-app"}) {
t.Errorf("Default-Service: down = %v", down)
}
multi := map[string]string{
"traefik.enable": "true",
"traefik.http.routers.web.rule": "Host(`kunde.de`)",
"traefik.http.routers.web.service": "web",
"traefik.http.services.web.loadbalancer.server.port": "80",
"traefik.http.services.Api.loadbalancer.server.port": "9000",
}
_, down, _ = Workloads([]docker.Container{stopped(ctr("app", multi, pub(80, 8080), pub(9000, 19000)), "exited")}, "h")
if !reflect.DeepEqual(down, []string{"Api", "web"}) {
t.Errorf("mehrere Services: down = %v", down)
}
// Laufend: nichts down.
if _, down, _ := Workloads([]docker.Container{ctr("nginx", wpLabels(), pub(80, 9080))}, "h"); len(down) != 0 {
t.Errorf("laufend: down = %v", down)
}
}
// Ein gestoppter Container mit denselben Router- oder Service-Namen wie ein
// laufender (z. B. alte Kopie) darf diesen nicht verdrängen.
func TestWorkloads_RunningWinsOverStopped(t *testing.T) {
routerOnly := map[string]string{"traefik.enable": "true", "traefik.http.routers.WP.rule": "Host(`alt.kunde.de`)",
"traefik.http.services.other.loadbalancer.server.port": "80"}
serviceOnly := map[string]string{"traefik.enable": "true", "traefik.http.routers.x.rule": "Host(`x.kunde.de`)",
"traefik.http.routers.x.service": "wp", "traefik.http.services.wp.loadbalancer.server.port": "80"}
for name, labels := range map[string]map[string]string{"gleiche Labels": wpLabels(), "gleicher Router": routerOnly, "gleicher Service": serviceOnly} {
got, down, warnings := Workloads([]docker.Container{
stopped(ctr("a-alt", labels, pub(80, 9081)), "exited"),
ctr("b-neu", wpLabels(), pub(80, 9080)),
}, "h")
if len(got) != 1 || got[0].Name != "b-neu" || len(down) != 0 || len(warnings) != 0 {
t.Errorf("%s: got=%+v down=%v warnings=%v", name, got, down, warnings)
}
}
// Ohne Überschneidung bleiben beide.
other := map[string]string{"traefik.enable": "true", "traefik.http.routers.alt.rule": "Host(`alt.kunde.de`)",
"traefik.http.services.alt.loadbalancer.server.port": "80"}
got, down, _ := Workloads([]docker.Container{stopped(ctr("a-alt", other, pub(80, 9081)), "exited"), ctr("b-neu", wpLabels(), pub(80, 9080))}, "h")
if len(got) != 2 || !reflect.DeepEqual(down, []string{"alt"}) {
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])
}
}
}