Maintenance-Window Control Plane — declarative change windows reconciled by Fleet
Status
draft — opened 2026-05-31 from a live operational gap (a stalled Flow backup that should have alerted but didn't, and an upcoming CIMMG maintenance window with no coordination mechanism). Design only; not yet scoped into implementation tickets.
Summary
Today a planned change / maintenance window in the Koder Stack has no single declarative owner. The four audiences that must react to "service X is down / changing from T0 to T1" are coordinated by four unrelated mechanisms:
| Audience | Today (fragmented) |
|---|---|
| AI sessions (don't deploy / heavy-work the target) | file locknotice convention (`metacontextnotices`) — manual, read at prompt time |
| End users hitting the vhost | Koder Jet serves a maintenance/503 page — set manually |
| On-call / alerting (don't page on expected downtime) | nothing — koder-notify has no "silence", so planned downtime either pages falsely or the alert is never wired (see Motivation) |
| Scheduled jobs (pause the nightly during the window) | systemd timers — no window awareness |
This RFC proposes modeling a maintenance window as a declarative resource reconciled by the Stack's existing global control plane (Koder Fleet, infra-RFC-004) — NOT a new bespoke orchestrator — fanning out to the existing edges. One source of truth, auditable, GitOps-able.
Motivation
Two concrete incidents on 2026-05-31 exposed the gap:
- Stalled prod backup, no alert.
koder-flow-backup.service(the Flow nightlykzip → Drive) hung for >1h30 in Google Drive per-user rate-limit backoff (0 KiB/s, both kzip+rclone idle). The backup-failure notifier (
koder-flow-backup-alert.service, FLOW-215) wasinactive/dead, so nothing surfaced it. Conversely, had it been wired, a planned migration window would have paged falsely — there is no way to say "this downtime is expected, suppress." - CIMMG maintenance window (infra-RFC-001 Phase 0/2). Migrating production
healthcare LXCs needs a coordinated window: warn AI sessions off
flow/CIMMG components, show end users a maintenance page, suppress alerts, pause backups. Each is hand-coordinated today.
As autonomous operation scales (dozens of concurrent AI sessions + a growing service fleet), ad-hoc coordination develops seams: false pages, missed suppressions, deploy-into-downtime races, manual maintenance pages forgotten.
Decision
Model the maintenance window as a Fleet CRD + controller (reuse-first), fanning out to existing edges. Do NOT build a parallel maintenance orchestrator.
infra-RFC-004 already ratifies Koder Fleet (infra/net/fleet) as the Stack's declarative global control plane — it has crd/, controller/, admission/, alert/ subsystems. A maintenance window is just another declarative resource it reconciles. Building a separate maintenance daemon would duplicate Fleet's reconciliation loop and koder-notify's alerting — the exact "parallel control plane / duplicated plumbing" anti-pattern rejected in infra-RFC-001 §Axis C and governed by reuse-first.kmd.
The resource
apiVersion: fleet.koder.dev/v1
kind: MaintenanceWindow
metadata: { name: cimmg-phase0-cutover }
spec:
start: 2026-06-02T02:00:00Z
duration: 4h
reason: "infra-RFC-001 Phase 0 — Docker → kbox cutover (CIMMG PoC)"
targets:
services: [koder-flow] # logical services
vhosts: [flow.koder.dev] # Jet edge
components: [products/dev/flow, infra/net/box] # AI-session scope
pauseTimers: [koder-flow-backup.service]
silenceAlerts: true # koder-notify silence for the targetsThe controller fan-out (reconcile on window active/expired)
| Effect | Existing edge it drives | New capability needed |
|---|---|---|
| Suppress expected alerts | koder-notify (infra/observe) |
silence primitive (Alertmanager-class: a time-bounded matcher that drops/holds alerts). New. |
| End-user maintenance page | Koder Jet (infra/net/jet) |
a per-vhost maintenance flag honoured by Jet's routing (Jet already has auth-gate / slot-routing hooks — extend, don't add a server). |
| Pause scheduled jobs | systemd timers | controller systemctl stop/start <unit> (or a --maintenance env the unit reads). |
| Warn AI sessions | notice convention (meta/context/notices/) |
controller emits/retracts a notice — the convention becomes a controller output, not a manual edit. |
On window expiry the controller reverses every effect (un-silence, un-maintenance, resume timers, retract notice). Idempotent, reconciled — a missed tick self-heals.
Why not the alternatives
- Standalone maintenance daemon (rejected). Duplicates Fleet's reconciliation +
notify's silencing →
reuse-first.kmdviolation; a second control plane to operate. Same trap avoided in infra-RFC-001 (kbox not a god-object). - Status quo (rejected long-term). Works at today's scale but the seams above
don't survive growing autonomous ops; not auditable, not declarative.
- Put it all in Jet (rejected). Jet is the HTTP data-plane edge (reverse
proxy/TLS), not a control plane — it serves the maintenance page but must not own job pausing, alert silencing, or session coordination (
web-server.kmdscopes Jet to web-serving).
Trade-off (Quality > Speed, stack-principles §2): extend the existing control plane (Fleet CRD) + add a silence primitive to notify, rather than a new orchestrator — more correct long-term (reuse-first + declarative/reconciled) at the cost of depending on Fleet's CRD/controller foundation maturing.
Phasing
The CRD/controller waits on Fleet's foundation (mid-build per infra-RFC-004 + active FLEET tickets). The first two slices are independently useful and become the controller's reconciliation targets later:
| Phase | Scope | Independent value |
|---|---|---|
| 1 — notify silence | Add time-bounded alert silences to koder-notify (matcher + API). | Fixes "planned downtime shouldn't page" immediately (the FLOW-215 incident). Useful with or without Fleet. |
| 2 — Jet maintenance flag | Per-vhost maintenance mode in Jet (503 page + stop upstream routing) via config/API. |
Manual maintenance pages become a one-liner; reused by the controller later. |
| 3 — Fleet MaintenanceWindow CRD + controller | The declarative resource + reconciliation fanning out to Phase 1/2 + timer pause + notice emission. | The unifying layer; lands when Fleet CRD/controller is ready. |
| 4 — Session-notice integration | Controller emitsretracts `noticesactive/` entries; AI sessions already read them. | Closes the AI-coordination loop declaratively. |
Implementation status (2026-06-20): Phase 3 started under FLEET-052 slice 5 — the
MaintenanceWindowkind + controller (Pending→Active→Expired) landed ininfra/net/fleet, with the first reconciliation effect: a window freezes ServiceDeployment rollouts to its targeted services/namespaces while active. The remaining fan-out (Phase 1 notify-silence, Phase 2 Jet maintenance flag, Phase 4 notice emission) is not yet wired. This RFC stays draft until the owner ratifies; the implemented subset is additive and reversible.
Out of scope
- Replacing the
koder-lock/notice convention for ad-hoc per-component work locks— those stay (they're about who's editing what, not planned downtime). This RFC only adds declared maintenance windows as a controller-managed overlay.
- Approval workflows / change-management bureaucracy. A window is a fact to fan out,
not a ticket to approve.
Open questions
- Q1 — silence model in notify: label-matcher (Alertmanager-style) vs
service-id list. Lean matcher for composability.
- Q2 — timer pause mechanism: controller
systemctlover SSH/incus vs aper-unit
ConditionPathExists=!/run/koder-maintenance/<svc>drop-in (no remote exec; the controller just touches a file). Lean the drop-in (declarative, no SSH). - Q3 — does the window auto-pause AI deploys, or only warn? Warn first (notice);
a hard gate (block deploy hooks during a window) is a later tightening.
Cross-references
- infra-RFC-004 (Fleet control plane — the home for the CRD)
- infra-RFC-001 §Phasing (CIMMG window is the first real consumer)
- FLOW-215 (backup-failure alerting — the silence's first beneficiary)
policies/observability-first.kmd(silences are an observability primitive)