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

+56 -3
View File
@@ -21,6 +21,7 @@ package policy
import (
"errors"
"fmt"
"net/url"
"regexp"
"slices"
"strings"
@@ -61,8 +62,23 @@ const (
HeaderStage = "X-Sultan-Stage"
HeaderTenant = "X-Sultan-Tenant"
HeaderTier = "X-Sultan-Tier" // z. B. für den Pro-Hinweis auf Community-Wartungsseiten
// HeaderUntil: voraussichtliches Wartungsende (RFC 3339), nur bei befristeter Wartung.
HeaderUntil = "X-Sultan-Until"
// HeaderPublicReason: öffentlicher Wartungsgrund, URL-kodiert (Header sind
// einzeilig; Umlaute kommen so unverändert beim Lander an).
HeaderPublicReason = "X-Sultan-Public-Reason"
)
// Maintenance ist eine angemeldete Wartung (Knoten oder Dienst).
type Maintenance struct {
Until time.Time // Nullwert = unbefristet
PublicReason string
}
func (m *Maintenance) active(now time.Time) bool {
return m != nil && (m.Until.IsZero() || now.Before(m.Until))
}
// AckCookie setzt der Lander, nachdem er den Mahnhinweis gezeigt hat; der
// Wert ist die Stufe. Ein bestätigter Hinweis gilt nicht für eine höhere
// Stufe. Das Cookie ist bewusst kein Sicherheitsmerkmal – wer es selbst
@@ -124,6 +140,14 @@ type Input struct {
// DownServices: Services (Originalnamen), die der Healthcheck des
// Agenten als ausgefallen meldet.
DownServices map[string]bool
// Maintenance: Wartung des ganzen Knotens (nil = keine).
Maintenance *Maintenance
// ServiceMaintenance: Wartung einzelner Services (Originalnamen), z. B.
// per Label sultan.maintenance=true.
ServiceMaintenance map[string]Maintenance
// Disconnected: Der Knoten ist ungeplant getrennt, seine Lease gilt aber
// noch – Besucher sehen die Wartungsseite (ohne Ende und Grund).
Disconnected bool
}
// ViolationKind klassifiziert einen Verstoß.
@@ -202,7 +226,8 @@ func StageFor(t Tenant, l license.License, now time.Time) Stage {
switch {
case t.Status == store.TenantQuarantined:
return StageBlocked
case t.Status == store.TenantExpired || l.Expired(now):
// Community wird automatisch neu lizenziert: nur der explizite Status zählt.
case t.Status == store.TenantExpired || (l.Tier != license.TierCommunity && l.Expired(now)):
return StageLanding
case l.Tier == license.TierCommunity:
return StageNormal // kein Mahnwesen im Community-Betrieb
@@ -226,7 +251,8 @@ func (e *Engine) Apply(in Input) (Result, error) {
if e.now != nil {
now = e.now
}
stage := StageFor(in.Tenant, in.License, now())
at := now()
stage := StageFor(in.Tenant, in.License, at)
res := Result{Config: &traefik.Configuration{}, Stage: stage}
if stage == StageBlocked || in.NodeDrained || in.Config == nil || in.Config.HTTP == nil {
return res, nil
@@ -289,8 +315,11 @@ func (e *Engine) Apply(in Input) (Result, error) {
out.Services[nr.Service] = copyService(src.Services[r.Service])
}
// Vorrang: Ablauf/Paywall > Wartung (Knoten, Dienst, Healthcheck) >
// Mahnhinweis > normal. (Sperre und drain liefern oben gar keine Routen.)
routerStage := stage
if routerStage == StageNormal && in.DownServices[r.Service] {
maint, inMaint := maintenanceFor(in, r.Service, at)
if inMaint && (stage == StageNormal || slices.Contains(ackStages, stage)) {
routerStage = StageMaintenance
}
if routerStage != StageNormal {
@@ -319,6 +348,14 @@ func (e *Engine) Apply(in Input) (Result, error) {
HeaderTenant: in.Tenant.ID,
HeaderTier: string(in.License.Tier),
}
if routerStage == StageMaintenance {
if !maint.Until.IsZero() {
headers[HeaderUntil] = maint.Until.UTC().Format(time.RFC3339)
}
if maint.PublicReason != "" {
headers[HeaderPublicReason] = url.QueryEscape(maint.PublicReason)
}
}
mwName := "sultan__" + prefix + name
out.Middlewares[mwName] = traefik.Middleware{"headers": map[string]any{"customRequestHeaders": headers}}
nr.Middlewares = append(slices.Clone(nr.Middlewares), mwName)
@@ -336,6 +373,22 @@ func (e *Engine) Apply(in Input) (Result, error) {
return res, nil
}
// maintenanceFor liefert die geltende Wartung eines Services: zuerst die des
// Knotens, dann die des Services, dann ein Healthcheck-Ausfall oder ein
// getrennter Knoten (beides ohne Grund).
func maintenanceFor(in Input, service string, now time.Time) (Maintenance, bool) {
if in.Maintenance.active(now) {
return *in.Maintenance, true
}
if m, ok := in.ServiceMaintenance[service]; ok && m.active(now) {
return m, true
}
if in.DownServices[service] || in.Disconnected {
return Maintenance{}, true
}
return Maintenance{}, false
}
func validateInput(in Input) error {
switch {
case !ValidID(in.Tenant.ID) || in.Tenant.ID == "sultan":