Files
homelab/ci/images/kuma-sync/internal/app/manager.go
T
claude-botandClaude Opus 5.5 37c83ed6c2 feat(kuma-sync): declare the public status page as a KumaStatusPage
New namespaced CRD KumaStatusPage (uptime.renner.uno/v1alpha1) and a
reconciler that owns the Kuma status page completely: config, domain
names and the full group/monitor list. Sections select KumaMonitors in
any namespace by label; monitors are ordered by displayName, then
namespace/name, a monitor goes to the first matching section only, and
monitors without a Kuma ID are left out and counted in status.

The reconciler adopts an existing page with the slug, writes only when
the managed content differs (no write per resync), reuses Kuma group IDs
by position so saves do not churn them, reverts UI edits on resync,
deletes the page on resource deletion, and reports Ready=False/Conflict
for a younger resource claiming the same slug. Every KumaMonitor change
to labels, displayName, monitor ID or deletion enqueues all pages.

kumaclient gains a StatusPages interface (Breml + Fake). The socket API
returns no group list, so groups are read from Kuma's public
GET /api/status-page/<slug>, as the bootstrap did. Fields kuma-sync does
not manage (logo, custom CSS, analytics, RSS title, auto refresh
interval) are written back unchanged. autoRefreshInterval is not in the
spec: go-uptime-kuma-client v0.6.2 (latest) never sends it on save.

The Ansible bootstrap no longer creates or edits status pages; the
existing page is left in place for kuma-sync to adopt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:03:50 +02:00

107 lines
3.4 KiB
Go

// Package app wires the controller-runtime manager shared by the binary and
// the end-to-end tests.
package app
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/config"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/manager"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
v1 "gitea.renner.uno/konairius/homelab/kuma-sync/api/v1alpha1"
"gitea.renner.uno/konairius/homelab/kuma-sync/internal/controller"
"gitea.renner.uno/konairius/homelab/kuma-sync/internal/kumaclient"
)
// Options configures NewManager.
type Options struct {
Config *rest.Config
Kuma kumaclient.Client
// MetricsAddr and ProbeAddr are bind addresses; "0" and "" disable them.
MetricsAddr string
ProbeAddr string
// GCInterval is the orphan collection period; 0 means 5 minutes.
GCInterval time.Duration
// OrphanGrace is how long a monitor must be seen without its resource
// before the GC deletes it. Production uses 1h; 0 only requires two runs.
OrphanGrace time.Duration
// Resync is how often a healthy resource is reconciled again, which is
// what repairs drift made in Kuma; 0 means 5 minutes.
Resync time.Duration
// SkipNameValidation allows several managers in one process, as in tests
// that restart the manager.
SkipNameValidation bool
}
// NewManager builds the manager with the reconcilers and the orphan GC.
// There is no leader election: a single replica runs.
func NewManager(o Options) (manager.Manager, error) {
scheme := runtime.NewScheme()
// Core types: the GC reads Namespaces.
if err := clientgoscheme.AddToScheme(scheme); err != nil {
return nil, err
}
if err := v1.AddToScheme(scheme); err != nil {
return nil, err
}
mopts := ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{BindAddress: o.MetricsAddr},
HealthProbeBindAddress: o.ProbeAddr,
LeaderElection: false,
}
if o.SkipNameValidation {
mopts.Controller = config.Controller{SkipNameValidation: ptr.To(true)}
}
mgr, err := ctrl.NewManager(o.Config, mopts)
if err != nil {
return nil, fmt.Errorf("create manager: %w", err)
}
if err := (&controller.Reconciler{Client: mgr.GetClient(), Kuma: o.Kuma, Resync: o.Resync}).SetupWithManager(mgr); err != nil {
return nil, fmt.Errorf("set up reconciler: %w", err)
}
if err := (&controller.StatusPageReconciler{Client: mgr.GetClient(), Kuma: o.Kuma, Resync: o.Resync}).SetupWithManager(mgr); err != nil {
return nil, fmt.Errorf("set up status page reconciler: %w", err)
}
if err := mgr.Add(&controller.GC{
Client: mgr.GetClient(),
Kuma: o.Kuma,
Interval: o.GCInterval,
OrphanGrace: o.OrphanGrace,
Cache: mgr.GetCache(),
APIReader: mgr.GetAPIReader(),
}); err != nil {
return nil, fmt.Errorf("add GC: %w", err)
}
if err := mgr.AddHealthzCheck("ping", healthz.Ping); err != nil {
return nil, err
}
if err := mgr.AddReadyzCheck("cache-synced", func(req *http.Request) error {
ctx, cancel := context.WithTimeout(req.Context(), time.Second)
defer cancel()
if !mgr.GetCache().WaitForCacheSync(ctx) {
return errors.New("cache not synced")
}
return nil
}); err != nil {
return nil, err
}
return mgr, nil
}