Commit de2005d
feat: lease-aware scheduler gating, leader visibility, failover e2e (#82)
* feat(operator): lease-aware scheduler gating modes (ordinal|lease|auto)
Adds spec.heartbeat.schedulerGating (enum ordinal|lease|auto, default
ordinal) controlling how the heartbeat scheduler is pinned to a single
replica at replicas > 1:
- ordinal (default): ordinal-0 shell wrapper, StatefulSet only, no
failover. The HEARTBEAT_SCHEDULER_ENABLED wrapper is now additionally
guarded by the resolved mode and skipped for the Deployment workload.
- lease: no env manipulation; delegates to the app's lease-based leader
election (paperclipai/paperclip#7995).
- auto: currently resolves to ordinal; will flip to lease once the
minimum supported app version includes lease leadership.
SchedulerGatingMode in internal/resources resolves auto/empty to
ordinal. New advisory SchedulerGatingValid condition (excluded from the
Ready aggregate) reports False/OrdinalGatingRequiresStatefulSet plus a
Warning event for Deployment+ordinal at replicas > 1, True when valid,
and is removed at replicas <= 1.
Tests: wrapper presence matrix across mode/workload/replicas,
SchedulerGatingMode unit tests, condition lifecycle envtest, and the
advisory exclusion in allSubConditionsReady. Regenerated CRDs, chart
CRDs, and API docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(operator): scheduler leader visibility — role labels, status, deletion-cost
When schedulerGating resolves to lease at replicas > 1, the operator polls
each running server pod's unauthenticated /api/health for the scheduler
block, labels the lease holder paperclip.inc/role=scheduler (others web),
records status.schedulerLeader, and tags the leader with the
controller.kubernetes.io/pod-deletion-cost annotation on Deployment
workloads so ReplicaSet scale-down prefers web replicas. Per-pod poll
errors skip the pod; when every poll fails the previous leader value is
kept to avoid thrashing. Ordinal gating or replicas <= 1 strips all
markers and clears the status field. Active instances requeue every 30s.
The probe is injectable (healthProbe field) so envtest covers leader
labeling, status, deletion-cost, no-thrash, and the clear path without a
pod network.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): image-gated scheduler lease failover scenario
Adds a "Scheduler lease failover" e2e context: a 2-replica Deployment
workload Instance (database.mode external against an in-cluster
postgres:16 Deployment, persistence off, schedulerGating lease) must
elect exactly one paperclip.inc/role=scheduler pod with a matching
status.schedulerLeader, and deleting that pod must hand the lease to a
different pod (label + status follow).
The scenario needs an app image that ships lease-based scheduler
leadership (paperclipai/paperclip#7995, unreleased), so it is gated
behind PAPERCLIP_E2E_LEASE_IMAGE and SKIPPED by default; the image ref
is split into spec.image repository/tag-or-digest. objectStorage is
intentionally omitted: MultiReplicaPreconditions reports an advisory
False, but the workload reconciles regardless and the failover
assertions do not touch file state, keeping the scenario free of a
MinIO dependency. Terminating pods are excluded from the leader-label
query so the drained old leader never trips the exactly-one assertion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: scheduler gating modes, version-skew matrix, and failover guide
Adds a "Scheduler gating and failover" subsection to the README Scaling
section: the three schedulerGating modes (ordinal default, lease, auto)
with their failover behavior, the operator-vs-app version-skew matrix
(including the unsafe lease + lease-less-app combination), the
ordinal-to-lease migration recipe (upgrade the app first), and leader
observability (status.schedulerLeader, paperclip.inc/role pod labels,
pod-deletion-cost on Deployments). The Heartbeat Scheduler section now
points at it instead of stating pod-0 pinning unconditionally.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(operator): present the service FQDN as Host when polling pod health
The app enforces a Host-header allowlist (service DNS names + loopback);
polling a pod IP with the IP as Host gets 403 in authenticated mode —
found live on a real cluster; envtest's injected probe couldn't catch it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(e2e): authenticated mode for the lease-failover scenario (local_trusted cannot bind in-cluster)
The app hard-rejects local_trusted on non-loopback binds ("local_trusted
requires server.bind=loopback"), so in-cluster local_trusted Instances
crash-loop. Switch the lease-failover Instance (and the env-rendering
e2e-feat Instance, which also runs the real app image) to
deployment.mode=authenticated with a BETTER_AUTH_SECRET Secret.
auth.adminUser is deliberately omitted: the bootstrap Job proved flaky
in live verification and the failover assertions need no authenticated
principal - the operator reads scheduler leadership from the
unauthenticated redacted health endpoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(rbac): grant jobs/status to the execution ClusterRole
The kubernetes sandbox plugin polls Job completion via the status
subresource; without it every sandboxed run fails with a 403 after Job
creation. Found during live multi-replica verification.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(rbac): operator role needs jobs/status to grant it (escalation prevention)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(chart): sync jobs/status into the chart RBAC
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(api): objectStorage.forcePathStyle, networkPolicy.extraEgress, netpol Enabled pointer fix
Three live-verification findings: (1) in-cluster MinIO requires path-style
S3 addressing — new forcePathStyle field, defaulting true for provider=minio;
(2) the operator-managed NetworkPolicy had no way to allow egress to an
in-cluster object-storage endpoint — new extraEgress passthrough; (3)
networkPolicy.enabled was a plain bool with omitempty + default=true, so an
explicit false was dropped on marshal and re-defaulted by the API server —
same bug class as PersistenceSpec.Enabled, now *bool.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent a3871bf commit de2005d
21 files changed
Lines changed: 1573 additions & 28 deletions
File tree
- api/v1alpha1
- charts/paperclip-operator/templates
- crds
- config
- crd/bases
- rbac
- docs
- internal
- controller
- resources
- test/e2e
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
530 | 530 | | |
531 | 531 | | |
532 | 532 | | |
533 | | - | |
| 533 | + | |
534 | 534 | | |
535 | 535 | | |
536 | 536 | | |
537 | 537 | | |
538 | | - | |
539 | | - | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
540 | 541 | | |
541 | 542 | | |
542 | 543 | | |
| |||
637 | 638 | | |
638 | 639 | | |
639 | 640 | | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
640 | 681 | | |
641 | 682 | | |
642 | 683 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
502 | 503 | | |
503 | 504 | | |
504 | 505 | | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
505 | 514 | | |
506 | 515 | | |
507 | 516 | | |
| |||
515 | 524 | | |
516 | 525 | | |
517 | 526 | | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
518 | 542 | | |
519 | 543 | | |
520 | 544 | | |
| |||
861 | 885 | | |
862 | 886 | | |
863 | 887 | | |
| 888 | + | |
| 889 | + | |
| 890 | + | |
864 | 891 | | |
865 | 892 | | |
866 | | - | |
| 893 | + | |
867 | 894 | | |
868 | 895 | | |
869 | 896 | | |
| |||
876 | 903 | | |
877 | 904 | | |
878 | 905 | | |
| 906 | + | |
| 907 | + | |
| 908 | + | |
| 909 | + | |
| 910 | + | |
| 911 | + | |
| 912 | + | |
| 913 | + | |
879 | 914 | | |
880 | 915 | | |
881 | 916 | | |
| |||
1264 | 1299 | | |
1265 | 1300 | | |
1266 | 1301 | | |
| 1302 | + | |
| 1303 | + | |
| 1304 | + | |
| 1305 | + | |
| 1306 | + | |
| 1307 | + | |
| 1308 | + | |
| 1309 | + | |
1267 | 1310 | | |
1268 | 1311 | | |
1269 | 1312 | | |
| |||
1326 | 1369 | | |
1327 | 1370 | | |
1328 | 1371 | | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
1329 | 1376 | | |
1330 | 1377 | | |
1331 | 1378 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
0 commit comments