software steht so weit.

jetzt build + docker build und testen
This commit is contained in:
br committed 2026-10-02 13:25:34 +02:00
1 parent 42f0c0accb
commit ca6cdcca63
90 files changed
+10298 -480

No files matched your search

+47 -9
View File
@@ -16,6 +16,8 @@ import (
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"sync"
"sultan/pkg/license"
@@ -54,11 +56,22 @@ type NodeState struct {
// NodeContext ist, was die Policy über einen Knoten wissen muss.
type NodeContext struct {
Tenant policy.Tenant
License license.License
Drained bool
Tenant policy.Tenant
License license.License
Drained bool
Maintenance *policy.Maintenance // Wartung des Knotens (nil = keine)
// Disconnected: Knoten getrennt, Routen werden (noch) gehalten – sie
// zeigen die Wartungsseite.
Disconnected bool
}
// Labels, mit denen ein Workload (Container, sultan.d-Dienst) seine Services
// in Wartung schickt.
const (
LabelMaintenance = "sultan.maintenance"
LabelMaintenancePublicReason = "sultan.maintenance.public_reason"
)
// Directory liefert den NodeContext; für gelöschte/unbekannte Knoten
// ErrUnknownNode (ggf. verpackt).
type Directory interface {
@@ -207,6 +220,7 @@ func (r *Registry) evaluate(ctx context.Context, nodeID string, state NodeState)
workloads := slices.Clone(state.Workloads)
slices.SortStableFunc(workloads, func(a, b Workload) int { return cmp.Compare(a.Name, b.Name) })
merged := &traefik.Configuration{}
serviceMaint := map[string]policy.Maintenance{}
seen := map[string]bool{}
for _, w := range workloads {
if seen[w.Name] {
@@ -225,6 +239,11 @@ func (r *Registry) evaluate(ctx context.Context, nodeID string, state NodeState)
continue
}
merged = m
if maint, ok := workloadMaintenance(w.Labels); ok && c.HTTP != nil {
for svc := range c.HTTP.Services {
serviceMaint[svc] = maint
}
}
}
down := map[string]bool{}
@@ -232,12 +251,15 @@ func (r *Registry) evaluate(ctx context.Context, nodeID string, state NodeState)
down[s] = true
}
res, err := r.Policy.Apply(policy.Input{
Tenant: nc.Tenant,
License: nc.License,
NodeID: nodeID,
NodeDrained: nc.Drained,
Config: merged,
DownServices: down,
Tenant: nc.Tenant,
License: nc.License,
NodeID: nodeID,
NodeDrained: nc.Drained,
Config: merged,
DownServices: down,
Maintenance: nc.Maintenance,
ServiceMaintenance: serviceMaint,
Disconnected: nc.Disconnected,
})
if err != nil {
return nil, err
@@ -249,6 +271,22 @@ func (r *Registry) evaluate(ctx context.Context, nodeID string, state NodeState)
return entry, nil
}
// workloadMaintenance liest sultan.maintenance(=true) und den optionalen
// öffentlichen Grund; Schlüssel ohne Beachtung der Groß-/Kleinschreibung.
func workloadMaintenance(labels map[string]string) (policy.Maintenance, bool) {
var on bool
var m policy.Maintenance
for k, v := range labels {
switch strings.ToLower(k) {
case LabelMaintenance:
on, _ = strconv.ParseBool(strings.TrimSpace(v))
case LabelMaintenancePublicReason:
m.PublicReason = strings.TrimSpace(v)
}
}
return m, on
}
// store übernimmt entry und meldet nur Verstöße, die vorher nicht bestanden.
func (r *Registry) store(nodeID string, entry *nodeEntry) {
if r.nodes == nil {
+43
View File
@@ -398,3 +398,46 @@ func TestRegistry_Concurrent(t *testing.T) {
t.Errorf("%d Router, erwartet 20", n)
}
}
// Container/Dienst in Wartung per Label: nur dessen Router gehen zum Lander.
func TestRegistry_ServiceMaintenanceLabel(t *testing.T) {
r, _, _ := newRegistry()
shop := wp("shop", "100.64.0.1", "shop.kunde.de")
shop.Labels["traefik.http.routers.wp.rule"] = "Host(`shop.kunde.de`)"
shop.Labels = map[string]string{
"traefik.enable": "true",
"traefik.http.routers.shop.rule": "Host(`shop.kunde.de`)",
"traefik.http.services.shop.loadbalancer.server.port": "8080",
"sultan.maintenance": "true",
"sultan.maintenance.public_reason": "Inventur",
}
r.Update(ctx, "n1", NodeState{Workloads: []Workload{wp("wordpress", "100.64.0.1", "kunde.de"), shop}})
cfg := r.Config().HTTP
if cfg.Routers["kunde__n1__shop"].Service != policy.InterceptorService {
t.Error("Dienst mit sultan.maintenance=true nicht umgeleitet")
}
if cfg.Routers["kunde__n1__wp"].Service == policy.InterceptorService {
t.Error("anderer Dienst wurde umgeleitet")
}
hdr := cfg.Middlewares["sultan__kunde__n1__shop"]["headers"].(map[string]any)["customRequestHeaders"].(map[string]any)
if hdr[policy.HeaderPublicReason] != "Inventur" {
t.Errorf("öffentlicher Grund: %v", hdr)
}
shop.Labels["sultan.maintenance"] = "false"
r.Update(ctx, "n1", NodeState{Workloads: []Workload{shop}})
if r.Config().HTTP.Routers["kunde__n1__shop"].Service == policy.InterceptorService {
t.Error("sultan.maintenance=false leitet um")
}
}
func TestRegistry_NodeMaintenanceFromDirectory(t *testing.T) {
r, dir, _ := newRegistry()
r.Update(ctx, "n1", NodeState{Workloads: []Workload{wp("wordpress", "100.64.0.1", "kunde.de")}})
dir.set("n1", func(nc *NodeContext) { nc.Maintenance = &policy.Maintenance{Until: time.Now().Add(time.Hour)} })
r.Refresh(ctx)
if r.Config().HTTP.Routers["kunde__n1__wp"].Service != policy.InterceptorService {
t.Error("Knotenwartung aus dem Directory nicht angewendet")
}
}