Files
platform/edgeconfig/reloader_kube.go
T
mats 4337110ae2 platform/edgeconfig: publish-time nginx fragment seam + switchable reloaders
Implements steps 1-2 and the RBAC half of step 4 of
docs/plan-storefront-edge-config.md (storefront auth gating, generalised
to dynamic grain routes).

- platform/edgeconfig: Fragment/Reloader interfaces, atomic Writer
  (all-or-nothing flush), HTTPReloader (dev), NullReloader,
  KubeReloader (injected PodLister keeps platform stdlib-only).
  go vet clean; tests pass under -race.
- deploy/reloader-lib.sh + nginx-reloader.sh: reload handler
  (cksum -> nginx -t -> graceful reload) + CGI endpoint on :8082
  (pod IP only) + 60s fallback poll. Include-only risk model: main
  config never changes at runtime; broken fragments fail validation
  and the previous config keeps serving.
- k8s: reloader wired into the nginx Deployment, writable cms-output
  mount, fragment include line, SA/Role/RoleBinding for backoffice
  pod listing; backoffice uses the new ServiceAccount.
- compose/dev: same handler on stock nginx:alpine, EDGE_RELOAD_PROVIDER=http
  targeting nginx:8082 from backoffice.
- docs: plan-storefront-edge-config.md (supersedes earlier gating plans)
  and the fixed-zones variant kept for drift analysis.
2026-08-24 21:59:02 +02:00

86 lines
2.5 KiB
Go

package edgeconfig
import (
"context"
"fmt"
"sync"
)
// KubeReloader fans reloads out to every pod of the nginx Deployment by
// talking to the Kubernetes API in-cluster. It exists so a change reaches
// ALL replicas (no single-pod-per-Service drift); per-pod results are
// returned for retry/alerting.
//
// The actual API calls (pod list + per-pod POST) are behind PodLister and
// reloadFunc so this file stays stdlib-only per the platform dependency
// rule — the k8s client wiring lives in the consuming module (backoffice),
// which injects them at startup.
type KubeReloader struct {
mu sync.Mutex
port string
lister PodLister
post func(ctx context.Context, target string) (HandlerResult, error)
}
// PodLister returns the addresses (ip:port or host:port) of all nginx pods
// matching the selector right now. Implemented by the consumer with
// client-go: corev1 Pods(ns).List(labelSelector) → podIP:port.
type PodLister func(ctx context.Context) ([]string, error)
// NewKubeReloader builds a KubeReloader. lister must be non-nil; port is
// the reload-handler port on each pod (default "8081").
func NewKubeReloader(lister PodLister, port string) (*KubeReloader, error) {
if lister == nil {
return nil, fmt.Errorf("edgeconfig: kube reloader needs a pod lister")
}
if port == "" {
port = "8081"
}
return &KubeReloader{
lister: lister,
port: port,
post: func(ctx context.Context, target string) (HandlerResult, error) {
return PostReload(ctx, nil, target)
},
}, nil
}
// Reload lists current nginx pods and POSTs /reload to each concurrently.
// A pod that appears after listing misses this round — bounded by the
// slow poll inside the nginx image (it loads fresh config on start anyway).
func (k *KubeReloader) Reload(ctx context.Context) Result {
k.mu.Lock()
defer k.mu.Unlock()
targets, err := k.lister(ctx)
if err != nil {
return Result{Targets: []TargetResult{{Target: "(pod-list)", Err: err}}}
}
if len(targets) == 0 {
return Result{Targets: []TargetResult{{
Target: "(pod-list)", Err: fmt.Errorf("no nginx pods found"),
}}}
}
res := Result{Targets: make([]TargetResult, len(targets))}
var wg sync.WaitGroup
for i, ip := range targets {
wg.Add(1)
go func(i int, ip string) {
defer wg.Done()
res.Targets[i] = TargetResult{
Target: ip,
Err: firstErr(k.post(ctx, joinHostPort(ip, k.port))),
}
}(i, ip)
}
wg.Wait()
return res
}
func firstErr(_ HandlerResult, err error) error { return err }
func joinHostPort(host, port string) string {
return host + ":" + port
}