Skip to content

Commit 9896a98

Browse files
committed
Move to using providers instead of inline time based schdules, this will solidify the provider contract as a first class API
Signed-off-by: Erick Bourgeois <erick@jeb.ca>
1 parent 2add878 commit 9896a98

74 files changed

Lines changed: 2769 additions & 2265 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/CHANGELOG.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,152 @@ The format is based on the regulated environment requirements:
99

1010
---
1111

12+
## [2026-06-17 09:00] - Pin deploy manifest image tags to the release version; crdgen back to stdout
13+
14+
**Author:** Erick Bourgeois
15+
16+
### Changed
17+
- `Makefile`: NEW `set-image-version VERSION=…` target — rewrites every
18+
`ghcr.io/finos/5-spot*` image tag under `deploy/` (controller, both
19+
spot-schedule providers, reclaim agent, kata-config agent) to the given
20+
version. Tag-suffix-only regex, so it is independent of the tag committed in
21+
git. Added to `.PHONY`.
22+
- `.github/workflows/build.yaml` (`package-deploy-manifests`, release-only):
23+
added a "Pin deployment image versions" step running
24+
`make set-image-version VERSION=${{ needs.extract-version.outputs.tag_name }}`
25+
before tarring, so the released `deploy-manifests.tar.gz` references the image
26+
variant just built/pushed. Its "Regenerate CRDs" step now runs `make crds`.
27+
- `src/bin/crdgen.rs`: reverted to **stdout** output (the binary no longer writes
28+
files or assumes `deploy/crds/` exists — matches `crddoc`). No-arg prints all
29+
CRDs as a `---`-separated multi-doc stream (pipe-to-`kubectl`-friendly); a
30+
positional selector (`scheduledmachine` / `timebasedspotschedule` /
31+
`capitalmarketsschedule`) prints just one. The file-writing approach was only
32+
introduced in the Phase-1 multi-version commit to host the schedule/spotSchedule
33+
CEL injection, which ADR 0009 removed — so crdgen is pure serialization again.
34+
- `Makefile` `crds` target: now owns the file layout, redirecting each selector
35+
to `deploy/crds/<selector>.yaml`.
36+
- `deploy/crds/capitalmarketsschedule.yaml`: regenerated (picks up the
37+
`TimeBasedSpotScheduleSpec` doc-comment fix in its description).
38+
39+
### Why
40+
The deployment manifests hardcoded `:v0.1.0`; the release pipeline built/pushed
41+
`ghcr.io/finos/5-spot:<tag>` but shipped manifests still pointed at the stale
42+
tag. Pinning at release time keeps the shipped manifests in lockstep with the
43+
image that was actually built. crdgen returning to stdout keeps path/layout
44+
decisions with the caller (Makefile/CI), not baked into the binary.
45+
46+
### Impact
47+
- [ ] Breaking change
48+
- [x] Requires cluster rollout
49+
- [ ] Config change only
50+
- [ ] Documentation only
51+
52+
---
53+
54+
## [2026-06-16 13:00] - ADR 0009: examples, time-based provider deploy manifests, and prose docs
55+
56+
**Author:** Erick Bourgeois
57+
58+
### Changed
59+
- `src/crd.rs`: `ScheduledMachineSpec.schedule` is now a **required**
60+
`SpotScheduleRef` (was `Option<ScheduleSpec>`); removed `spec.spotSchedule`;
61+
added top-level `spec.enabled` master switch; removed `ScheduleSpec` and the
62+
`v1alpha1` `ScheduledMachine` module; added the `TimeBasedSpotSchedule` CRD
63+
(spec + status) in `spotschedules.5spot.finos.org`. `is_enabled()` now reads
64+
`spec.enabled`.
65+
- `src/providers/time_based.rs` (+tests) and `src/bin/spot_schedule_time_based.rs`:
66+
NEW core/default provider controller — `is_active_at`/`next_transition`/
67+
`reconcile`, mirroring `capital_markets`; publishes `status.active`.
68+
- `src/reconcilers/{scheduled_machine,helpers,spot_schedule_watch}.rs`: drive
69+
activation solely from the single `spec.schedule` provider verdict; removed
70+
`evaluate_schedule`; simplified `compose_should_be_active` (verdict + hold-last
71+
-state); replaced `validate_activation_source` with `validate_schedule_ref`;
72+
emergency-reclaim loop-breaker now patches `spec.enabled=false` (never the
73+
shared provider object); watch reverse-index keys off `spec.schedule`.
74+
- `src/metrics.rs` (+tests): added `fivespot_time_based_active` /
75+
`fivespot_time_based_transitions_total` + record fns.
76+
- `src/bin/{crdgen,crddoc}.rs`: emit the `TimeBasedSpotSchedule` CRD; single
77+
`v1beta1` `ScheduledMachine` (dropped the multi-version merge + schedule/
78+
spotSchedule XOR CEL); api.md regenerated.
79+
- `deploy/admission/validatingadmissionpolicy.yaml`: removed the inline-schedule
80+
rules (cron/day/hour) and the "at least one" rule; the remaining schedule rule
81+
pins `spec.schedule.apiVersion` to the provider group; `v1beta1`-only.
82+
- `docs/adr/0009-*.md` (+ index; amended 0006 §1/§3 and 0007);
83+
`docs/architecture/calm/architecture.json` (provider model + flows; validated,
84+
diagrams regenerated).
85+
- `examples/scheduledmachine-*.yaml`: migrated all to `5spot.finos.org/v1beta1`,
86+
added `spec.enabled: true`, and replaced every inline `schedule:` / removed
87+
`spotSchedule:` block with a single `spec.schedule:` provider reference. Basic
88+
/ weekend / tainted / bad-taint / kata / child-cluster now reference a
89+
`TimeBasedSpotSchedule`; `scheduledmachine-spot-schedule.yaml` references
90+
`CapitalMarketsSchedule` (`nyse-equities`) alone (no more AND-composition).
91+
- `examples/timebasedspotschedule.yaml`: NEW companion file defining the
92+
`business-hours` / `weekend-only` (ns `default`) and `business-hours-toronto`
93+
(ns `hosted-cluster-alpha`) provider objects the SM examples reference.
94+
- `examples/capitalmarketsschedule.yaml`, `examples/workshop/*`: updated
95+
comments / shape to `spec.schedule` reference + `spec.enabled`; workshop now
96+
installs the time-based provider and toggles `spec.enabled` / `tbss`.
97+
- `deploy/spot-schedule-providers/time-based/`: NEW provider deploy bundle
98+
(serviceaccount / clusterrole / clusterrolebinding / deployment /
99+
kustomization), mirroring the capital-markets bundle — least-privilege RBAC
100+
(get/list/watch `timebasedspotschedules`, update/patch its `/status`),
101+
hardened `Deployment` running `spot-schedule-time-based`.
102+
- `docs/src/**` and `docs/mkdocs.yml`: rewrote all spot-schedule prose to the
103+
ADR 0009 shape (required `spec.schedule` provider ref; `TimeBasedSpotSchedule`
104+
as the default first-party provider; `spec.enabled` master switch; emergency
105+
reclaim sets `spec.enabled`; VAP single group-pin rule 4 / `v1beta1`-only
106+
matchConstraints). Added a new `docs/src/guides/time-based-schedule.md` guide
107+
and listed it in the nav.
108+
109+
### Why
110+
ADR 0009 reified the former inline `ScheduledMachine.spec.schedule` window into
111+
the first-party `TimeBasedSpotSchedule` provider, made `spec.schedule` a required
112+
provider reference, dropped the SM `v1alpha1` version and the `spotSchedule`
113+
field, and introduced `spec.enabled`. Examples, deploy manifests, and docs had
114+
to match the new contract.
115+
116+
### Impact
117+
- [x] Breaking change (pre-release CRD contract: `spec.schedule` is now a
118+
required provider reference, `spec.spotSchedule` removed, SM `v1alpha1` dropped)
119+
- [x] Requires cluster rollout (new CRD + provider controller + RBAC; re-apply
120+
`deploy/crds/` and the time-based provider bundle)
121+
- [ ] Config change only
122+
- [ ] Documentation only
123+
124+
---
125+
126+
**Author:** Erick Bourgeois
127+
128+
### Changed
129+
- `docs/src/guides/create-your-own-provider.md`: NEW standalone guide — a
130+
complete, copy-pasteable walkthrough building a minimal `ManualSchedule`
131+
provider end to end: CRD (namespaced, `status` subresource), controller
132+
(status-patch reconcile, `Ready` semantics, event-driven not polling),
133+
least-privilege RBAC, deployment, referencing from a `ScheduledMachine`,
134+
verification, and additive versioning. Language-agnostic.
135+
- `docs/mkdocs.yml`: grouped both provider guides under a **Spot Schedule
136+
Providers** sub-section in Guides (CapitalMarketsSchedule + Create Your Own
137+
Provider) — makes the existing capital-markets guide more discoverable.
138+
- `docs/src/reference/spot-schedule-contract.md`: replaced the inline
139+
"implement your own" walkthrough with a pointer to the new guide (reference =
140+
spec, guide = how-to); added both guides to See-also.
141+
- `docs/src/guides/capital-markets-schedule.md`: added a See-also section
142+
cross-linking the concept, contract, and the new provider guide.
143+
144+
### Why
145+
The "create your own provider" how-to was buried inside the contract *reference*
146+
page; operators asked for it (and the CapitalMarketsSchedule guide) as
147+
first-class entries under **Guides**. Splitting how-to from spec also keeps the
148+
reference page focused.
149+
150+
### Impact
151+
- [ ] Breaking change
152+
- [ ] Requires cluster rollout
153+
- [ ] Config change only
154+
- [x] Documentation only
155+
156+
---
157+
12158
## [2026-06-15 10:30] - Restore dropped spot-schedule metric functions (build fix)
13159

14160
**Author:** Erick Bourgeois

.github/workflows/build.yaml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1288,8 +1288,17 @@ jobs:
12881288
- name: Cache cargo dependencies
12891289
uses: firestoned/github-actions/rust/cache-cargo@53b483254bc648903c364ee3c73a546d0936a91e # v1.3.6
12901290

1291+
# `make crds` writes one CRD per file under deploy/crds/ (crdgen itself
1292+
# only prints to stdout — the Makefile owns the file layout).
12911293
- name: Regenerate CRDs
1292-
run: cargo run --quiet --bin crdgen > deploy/crds/scheduledmachine.yaml
1294+
run: make crds
1295+
1296+
# Pin every deploy/ image tag to the release tag so the packaged manifests
1297+
# reference the image variant just built and pushed (ghcr.io/finos/5-spot
1298+
# and the 5-spot-* agent images all share this tag). See `make
1299+
# set-image-version`.
1300+
- name: Pin deployment image versions to the release tag
1301+
run: make set-image-version VERSION=${{ needs.extract-version.outputs.tag_name }}
12931302

12941303
- name: Package deploy manifests
12951304
run: tar czf deploy-manifests.tar.gz deploy/

Makefile

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Copyright (c) 2025 Erick Bourgeois, RBC Capital Markets
22
# SPDX-License-Identifier: Apache-2.0
33

4-
.PHONY: help install build build-debug build-linux-amd64 build-linux-arm64 build-macos-arm64 prepare-binaries-linux-amd64 prepare-binaries-linux-arm64 test test-lib lint format clean crds crddoc docs docs-serve docs-clean docs-rustdoc calm-diagrams calm-validate run-local docker-build docker-build-amd64 docker-build-arm64 docker-build-chainguard docker-push docker-buildx docker-buildx-chainguard gitleaks gitleaks-install install-git-hooks security-scan-local sbom audit vexctl-install vex-validate vex-assemble vex-auto-presence vex-auto-reachability vex-auto vex-auto-check kind-install kind-create kind-delete kind-load kind-deploy kind-example kind-setup kind-status
4+
.PHONY: help install build build-debug build-linux-amd64 build-linux-arm64 build-macos-arm64 prepare-binaries-linux-amd64 prepare-binaries-linux-arm64 test test-lib lint format clean crds crddoc docs docs-serve docs-clean docs-rustdoc calm-diagrams calm-validate run-local docker-build docker-build-amd64 docker-build-arm64 docker-build-chainguard docker-push docker-buildx docker-buildx-chainguard gitleaks gitleaks-install install-git-hooks security-scan-local sbom audit vexctl-install vex-validate vex-assemble vex-auto-presence vex-auto-reachability vex-auto vex-auto-check set-image-version kind-install kind-create kind-delete kind-load kind-deploy kind-example kind-setup kind-status
55

66
# CALM (FINOS Common Architecture Language Model) configuration
77
CALM_CLI_VERSION ?= 1.37.0
@@ -172,16 +172,40 @@ run-local: ## Run operator locally
172172
# Code Generation
173173
# ============================================================
174174

175+
# crdgen prints one CRD to stdout per selector; this target owns the file layout
176+
# (the binary makes no path assumptions). Keep this list in sync with crdgen's
177+
# SELECTORS and the committed deploy/crds/*.yaml filenames.
178+
CRD_SELECTORS ?= scheduledmachine timebasedspotschedule capitalmarketsschedule
179+
175180
crds: ## Generate CRD YAML files from Rust types
176181
@echo "Generating CRD YAML files from src/crd.rs..."
177-
@cargo run --quiet --bin crdgen
178-
@echo "✓ CRD YAML files generated under deploy/crds/"
182+
@for sel in $(CRD_SELECTORS); do \
183+
cargo run --quiet --bin crdgen -- "$$sel" > "deploy/crds/$$sel.yaml"; \
184+
echo "✓ wrote deploy/crds/$$sel.yaml"; \
185+
done
179186

180187
crddoc: ## Generate API documentation from CRD types
181188
@echo "Generating API documentation..."
182189
@cargo run --quiet --bin crddoc > docs/src/reference/api.md
183190
@echo "✓ API documentation generated: docs/src/reference/api.md"
184191

192+
# Pin every 5-Spot image tag under deploy/ to VERSION so the shipped manifests
193+
# match the image actually built/pushed at release. The controller and both
194+
# spot-schedule providers share the main `ghcr.io/finos/5-spot` image; the
195+
# reclaim- and kata-config agents use their own `ghcr.io/finos/5-spot-*` repos —
196+
# all carry the same release tag. The regex replaces only the tag suffix, so it
197+
# is independent of whatever tag is committed in git. Used by the release
198+
# `package-deploy-manifests` CI job; safe to run locally to preview the diff.
199+
set-image-version: ## Pin deploy/ image tags to VERSION (e.g. make set-image-version VERSION=v0.2.0)
200+
@test -n "$(VERSION)" || { \
201+
echo "ERROR: VERSION is required, e.g. make set-image-version VERSION=v0.2.0"; exit 1; }
202+
@files=$$(grep -rl "image:[[:space:]]*ghcr.io/finos/5-spot" deploy/); \
203+
if [ -z "$$files" ]; then echo "ERROR: no 5-Spot image references found under deploy/"; exit 1; fi; \
204+
for f in $$files; do \
205+
perl -i -pe 's{(image:\s*ghcr\.io/finos/5-spot[\w./-]*):\S+}{$${1}:$(VERSION)}g' "$$f"; \
206+
echo "✓ pinned 5-Spot images in $$f to $(VERSION)"; \
207+
done
208+
185209
# ============================================================
186210
# Documentation
187211
# ============================================================

deploy/admission/validatingadmissionpolicy.yaml

Lines changed: 17 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,11 @@ spec:
2727

2828
matchConstraints:
2929
resourceRules:
30-
# Both served versions must be validated (ADR 0007). v1beta1 is the
31-
# storage/current version (adds spec.spotSchedule and an optional
32-
# spec.schedule); v1alpha1 is the frozen, deprecated legacy version. A
33-
# version omitted here would BYPASS every validation below.
30+
# Single served/storage version since ADR 0009 (the pre-release v1alpha1
31+
# ScheduledMachine was dropped). spec.schedule is a required provider
32+
# reference. A version omitted here would BYPASS every validation below.
3433
- apiGroups: ["5spot.finos.org"]
35-
apiVersions: ["v1alpha1", "v1beta1"]
34+
apiVersions: ["v1beta1"]
3635
resources: ["scheduledmachines"]
3736
operations: ["CREATE", "UPDATE"]
3837

@@ -121,84 +120,23 @@ spec:
121120
(format: <positive integer> followed by s, m, or h)
122121
reason: Invalid
123122

124-
# NOTE on the schedule rules below: `spec.schedule` is OPTIONAL since
125-
# v1beta1 (ADR 0006) — a machine may instead delegate to a spotSchedule
126-
# provider. Each schedule rule is therefore guarded with
127-
# `!has(object.spec.schedule) || …` so a valid spotSchedule-only machine is
128-
# not rejected. The "at least one of schedule/spotSchedule" invariant is
129-
# enforced by rule 7a below (and by the CRD's own x-kubernetes-validations).
123+
# NOTE on the schedule rule below: since ADR 0009, `spec.schedule` is a
124+
# REQUIRED reference to a spot-schedule provider object (apiVersion/kind/
125+
# name in the spotschedules.5spot.finos.org group) — the former inline time
126+
# window moved to the TimeBasedSpotSchedule provider CRD, which carries its
127+
# own day/hour/timezone schema validation. The only ScheduledMachine-side
128+
# invariant left for this VAP is the provider group pin (rule 4). The
129+
# "required" and structural shape are enforced by the CRD schema itself.
130130

131-
# ── 4. Cron XOR explicit day/hour windows ────────────────────────────────
132-
# Using cron together with daysOfWeek or hoursOfDay is ambiguous.
133-
# The controller silently ignores daysOfWeek/hoursOfDay when cron is set;
134-
# we reject the spec at admission to prevent silent data loss.
135-
- expression: |
136-
!has(object.spec.schedule) ||
137-
!has(object.spec.schedule.cron) ||
138-
(object.spec.schedule.daysOfWeek.size() == 0 &&
139-
object.spec.schedule.hoursOfDay.size() == 0)
140-
message: >-
141-
spec.schedule: cron is mutually exclusive with daysOfWeek and hoursOfDay —
142-
set one or the other, not both
143-
reason: Invalid
144-
145-
# ── 5. Without cron, both day and hour windows must be provided ───────────
146-
- expression: |
147-
!has(object.spec.schedule) ||
148-
has(object.spec.schedule.cron) ||
149-
(object.spec.schedule.daysOfWeek.size() > 0 &&
150-
object.spec.schedule.hoursOfDay.size() > 0)
151-
message: >-
152-
spec.schedule: when cron is not set, both daysOfWeek and hoursOfDay must be
153-
non-empty to define an active window
154-
reason: Invalid
155-
156-
# ── 6. daysOfWeek item format ─────────────────────────────────────────────
157-
# Each element must be a day name (mon…sun), an inclusive range (mon-fri),
158-
# or a comma-separated combination (mon-wed,fri-sun).
159-
# Trivially true for empty arrays (cron path).
160-
- expression: |
161-
!has(object.spec.schedule) ||
162-
object.spec.schedule.daysOfWeek.all(d,
163-
d.matches('^(mon|tue|wed|thu|fri|sat|sun)(-(mon|tue|wed|thu|fri|sat|sun))?(,(mon|tue|wed|thu|fri|sat|sun)(-(mon|tue|wed|thu|fri|sat|sun))?)*$'))
164-
message: >-
165-
spec.schedule.daysOfWeek items must be day names or ranges
166-
(e.g. 'mon', 'mon-fri', 'mon-wed,fri-sun')
167-
reason: Invalid
168-
169-
# ── 7. hoursOfDay item format ─────────────────────────────────────────────
170-
# Each element must be a 1-2 digit hour, a range (9-17), or a comma-
171-
# separated combination (0-9,18-23). Numeric bounds (0-23) are enforced
172-
# at runtime by the schedule evaluator.
173-
# Trivially true for empty arrays (cron path).
174-
- expression: |
175-
!has(object.spec.schedule) ||
176-
object.spec.schedule.hoursOfDay.all(h,
177-
h.matches('^[0-9]{1,2}(-[0-9]{1,2})?(,[0-9]{1,2}(-[0-9]{1,2})?)*$'))
178-
message: >-
179-
spec.schedule.hoursOfDay items must be hours or ranges
180-
(e.g. '9', '9-17', '0-9,18-23')
181-
reason: Invalid
182-
183-
# ── 7a. At least one activation source ────────────────────────────────────
184-
# A machine must declare an inline schedule, a spotSchedule provider, or
185-
# both (ADR 0006). Mirrors the CRD x-kubernetes-validations and the runtime
186-
# validate_activation_source() in src/reconcilers/helpers.rs.
187-
- expression: "has(object.spec.schedule) || has(object.spec.spotSchedule)"
188-
message: >-
189-
at least one of spec.schedule or spec.spotSchedule must be set
190-
reason: Invalid
191-
192-
# ── 7b. spotSchedule.apiVersion group pin ─────────────────────────────────
131+
# ── 4. schedule.apiVersion group pin ──────────────────────────────────────
193132
# The provider reference group MUST be spotschedules.5spot.finos.org
194-
# (ADR 0006 §2 / §6) — providers are untrusted inputs and the controller's
195-
# read-only RBAC is scoped to exactly this group. Mirrors the CRD field CEL
196-
# and the runtime group check.
133+
# (ADR 0006 §2 / §6, ADR 0009) — providers are untrusted inputs and the
134+
# controller's read-only RBAC is scoped to exactly this group. Mirrors the
135+
# CRD field CEL and the runtime validate_schedule_ref() check.
197136
- expression: |
198-
!has(object.spec.spotSchedule) ||
199-
object.spec.spotSchedule.apiVersion.startsWith('spotschedules.5spot.finos.org/')
137+
object.spec.schedule.apiVersion.startsWith('spotschedules.5spot.finos.org/')
200138
message: >-
201-
spec.spotSchedule.apiVersion group must be spotschedules.5spot.finos.org
139+
spec.schedule.apiVersion group must be spotschedules.5spot.finos.org
202140
reason: Invalid
203141
204142
# ── 8. bootstrapSpec.apiVersion must use a namespaced API group ───────────

0 commit comments

Comments
 (0)