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") } }