Files
sultan/pkg/agent/sultand_test.go
T
br ca6cdcca63 software steht so weit.
jetzt build + docker build und testen
2026-10-02 13:25:34 +02:00

237 lines
9.4 KiB
Go

package agent
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"sultan/pkg/traefik"
)
func parse(t *testing.T, yaml string) []ServiceSpec {
t.Helper()
specs, err := ParseServices([]byte(yaml))
if err != nil {
t.Fatalf("ParseServices: %v", err)
}
return specs
}
func TestFileWorkloads_Minimal(t *testing.T) {
specs := parse(t, `
services:
- name: shop
url: http://127.0.0.1:8080
hosts: [shop.kunde.de]
`)
ws := FileWorkloads(specs, "100.64.0.42")
want := map[string]string{
"traefik.enable": "true",
"traefik.http.routers.shop.rule": "Host(`shop.kunde.de`)",
"traefik.http.routers.shop.entrypoints": "web",
"traefik.http.routers.shop.service": "shop",
"traefik.http.services.shop.loadbalancer.server.port": "8080",
}
if len(ws) != 1 || ws[0].Name != "sultan.d/shop" || ws[0].Host != "100.64.0.42" || !reflect.DeepEqual(ws[0].Labels, want) {
t.Errorf("Workloads = %+v", ws)
}
}
func TestFileWorkloads_ComfortSwitches(t *testing.T) {
specs := parse(t, `
services:
- name: kunde-a
url: https://127.0.0.1:8443/
hosts: [Kunde-A.de]
https: true
certresolver: dns-resolver
redirect_http_to_https: true
redirect_www: to_non_www
labels:
traefik.http.middlewares.kunde-a-www.redirectregex.permanent: "false"
`)
rule := "Host(`kunde-a.de`) || Host(`www.kunde-a.de`)"
want := map[string]string{
"traefik.enable": "true",
"traefik.http.routers.kunde-a.rule": rule,
"traefik.http.routers.kunde-a.entrypoints": "websecure",
"traefik.http.routers.kunde-a.service": "kunde-a",
"traefik.http.routers.kunde-a.tls": "true",
"traefik.http.routers.kunde-a.tls.certresolver": "dns-resolver",
"traefik.http.routers.kunde-a.middlewares": "kunde-a-www",
"traefik.http.services.kunde-a.loadbalancer.server.port": "8443",
"traefik.http.services.kunde-a.loadbalancer.server.scheme": "https",
"traefik.http.routers.kunde-a-http.rule": rule,
"traefik.http.routers.kunde-a-http.entrypoints": "web",
"traefik.http.routers.kunde-a-http.service": "kunde-a",
"traefik.http.routers.kunde-a-http.middlewares": "kunde-a-https",
"traefik.http.middlewares.kunde-a-https.redirectscheme.scheme": "https",
"traefik.http.middlewares.kunde-a-https.redirectscheme.permanent": "true",
"traefik.http.middlewares.kunde-a-www.redirectregex.regex": `^https?://www\.(kunde-a\.de)(.*)`,
"traefik.http.middlewares.kunde-a-www.redirectregex.replacement": "https://${1}${2}",
"traefik.http.middlewares.kunde-a-www.redirectregex.permanent": "false", // rohes Label gewinnt
}
got := FileWorkloads(specs, "h")[0].Labels
if !reflect.DeepEqual(got, want) {
for k, v := range want {
if got[k] != v {
t.Errorf("%s = %q, erwartet %q", k, got[k], v)
}
}
for k := range got {
if _, ok := want[k]; !ok {
t.Errorf("unerwartetes Label %s = %q", k, got[k])
}
}
}
}
func TestFileWorkloads_RedirectToWWW(t *testing.T) {
specs := parse(t, `
services:
- name: web
url: http://127.0.0.1
hosts: [www.shop.de, kunde.de]
redirect_www: to_www
`)
l := FileWorkloads(specs, "h")[0].Labels
if l["traefik.http.routers.web.rule"] != "Host(`www.shop.de`) || Host(`kunde.de`) || Host(`shop.de`) || Host(`www.kunde.de`)" {
t.Errorf("Regel = %s", l["traefik.http.routers.web.rule"])
}
if l["traefik.http.middlewares.web-www.redirectregex.regex"] != `^https?://(shop\.de|kunde\.de)(.*)` ||
l["traefik.http.middlewares.web-www.redirectregex.replacement"] != "http://www.${1}${2}" {
t.Errorf("Middleware: %v", l)
}
if l["traefik.http.services.web.loadbalancer.server.port"] != "80" {
t.Errorf("Default-Port: %s", l["traefik.http.services.web.loadbalancer.server.port"])
}
}
// Die erzeugten Labels müssen dieselbe Übersetzung bestehen wie Docker-Labels.
func TestFileWorkloads_LabelsAreValidTraefik(t *testing.T) {
specs := parse(t, `
services:
- name: kunde-a
url: https://127.0.0.1:8443/
hosts: [kunde-a.de]
https: true
redirect_http_to_https: true
redirect_www: to_non_www
`)
w := FileWorkloads(specs, "100.64.0.42")[0]
cfg, err := traefik.FromLabels(w.Host, w.Labels)
if err != nil {
t.Fatalf("FromLabels: %v", err)
}
if len(cfg.HTTP.Routers) != 2 || cfg.HTTP.Services["kunde-a"].LoadBalancer.Servers[0].URL != "https://100.64.0.42:8443" {
t.Errorf("Konfiguration: %+v", cfg.HTTP)
}
}
func TestParseServices_Healthcheck(t *testing.T) {
specs := parse(t, `
services:
- name: shop
url: http://127.0.0.1:8080/start
hosts: [shop.kunde.de]
healthcheck:
interval: 10s
expect_title: "Shop - Willkommen"
expect_contains: { tag: h2, text: Impressum }
- name: blog
url: http://127.0.0.1:8081
hosts: [blog.kunde.de]
healthcheck: {}
`)
h := specs[0].Healthcheck
if h == nil || h.Interval.Duration != 10*time.Second || h.Timeout.Duration != 5*time.Second || h.Failures != 2 ||
h.ExpectStatus != 200 || h.URL != "http://127.0.0.1:8080/start" || h.ExpectTitle != "Shop - Willkommen" ||
h.ExpectContains == nil || h.ExpectContains.Tag != "h2" || h.ExpectContains.Text != "Impressum" {
t.Errorf("Healthcheck: %+v", h)
}
if d := specs[1].Healthcheck; d == nil || d.Interval.Duration != 30*time.Second || d.ExpectStatus != 200 {
t.Errorf("Healthcheck-Defaults: %+v", d)
}
}
func TestParseServices_Errors(t *testing.T) {
tests := map[string]string{
"kein YAML": "services: [",
"unbekanntes Feld": "services:\n - name: a\n url: http://x\n hosts: [a.de]\n farbe: blau\n",
"Name fehlt": "services:\n - url: http://x\n hosts: [a.de]\n",
"Name ungültig": "services:\n - name: a.b\n url: http://x\n hosts: [a.de]\n",
"URL fehlt": "services:\n - name: a\n hosts: [a.de]\n",
"URL kein http": "services:\n - name: a\n url: ftp://x\n hosts: [a.de]\n",
"Hosts fehlen": "services:\n - name: a\n url: http://x\n",
"Host mit Wildcard": "services:\n - name: a\n url: http://x\n hosts: ['*.a.de']\n",
"redirect_www ungültig": "services:\n - name: a\n url: http://x\n hosts: [a.de]\n redirect_www: manchmal\n",
"Redirect ohne https": "services:\n - name: a\n url: http://x\n hosts: [a.de]\n redirect_http_to_https: true\n",
"Intervall unlesbar": "services:\n - name: a\n url: http://x\n hosts: [a.de]\n healthcheck: { interval: oft }\n",
"Status ungültig": "services:\n - name: a\n url: http://x\n hosts: [a.de]\n healthcheck: { expect_status: 42 }\n",
"contains ohne Tag": "services:\n - name: a\n url: http://x\n hosts: [a.de]\n healthcheck: { expect_contains: { text: x } }\n",
"doppelter Name": "services:\n - name: a\n url: http://x\n hosts: [a.de]\n - name: a\n url: http://y\n hosts: [b.de]\n",
}
for name, yaml := range tests {
if _, err := ParseServices([]byte(yaml)); err == nil {
t.Errorf("%s: erwartet Fehler", name)
}
}
}
func TestLoadServiceDir(t *testing.T) {
dir := t.TempDir()
write := func(name, content string) {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
write("b.yaml", "services:\n - name: shop\n url: http://127.0.0.1:8080\n hosts: [shop.kunde.de]\n")
write("a.yml", "services:\n - name: blog\n url: http://127.0.0.1:8081\n hosts: [blog.kunde.de]\n")
write("c.yaml", "services: [") // kaputt
write("d.yaml", "services:\n - name: shop\n url: http://127.0.0.1:9\n hosts: [x.kunde.de]\n") // doppelt
write("notizen.txt", "services:\n - name: txt\n url: http://127.0.0.1:1\n hosts: [t.kunde.de]\n") // ignoriert
specs, warnings := LoadServiceDir(dir)
var names []string
for _, s := range specs {
names = append(names, s.Name)
}
if !reflect.DeepEqual(names, []string{"blog", "shop"}) {
t.Errorf("Dienste = %v (Dateien alphabetisch, Duplikat und .txt ignoriert)", names)
}
if len(warnings) != 2 || !strings.Contains(strings.Join(warnings, "\n"), "c.yaml") || !strings.Contains(strings.Join(warnings, "\n"), "d.yaml") {
t.Errorf("Warnungen = %v", warnings)
}
if specs, warnings := LoadServiceDir(filepath.Join(dir, "fehlt")); len(specs) != 0 || len(warnings) != 0 {
t.Errorf("fehlendes Verzeichnis: %v %v", specs, warnings)
}
}
func TestFileWorkloads_Maintenance(t *testing.T) {
specs := parse(t, `
services:
- name: shop
url: http://127.0.0.1:8080
hosts: [shop.kunde.de]
maintenance: true
maintenance_public_reason: "Inventur bis Montag"
- name: blog
url: http://127.0.0.1:8081
hosts: [blog.kunde.de]
`)
ws := FileWorkloads(specs, "h")
if ws[0].Labels["sultan.maintenance"] != "true" || ws[0].Labels["sultan.maintenance.public_reason"] != "Inventur bis Montag" {
t.Errorf("Wartungs-Labels fehlen: %v", ws[0].Labels)
}
if _, ok := ws[1].Labels["sultan.maintenance"]; ok {
t.Error("Dienst ohne Wartung hat Wartungs-Label")
}
if _, err := ParseServices([]byte("services:\n - name: a\n url: http://x\n hosts: [a.de]\n maintenance_public_reason: \"a\\nb\"\n")); err == nil {
t.Error("mehrzeiliger Grund akzeptiert")
}
}