Skip to content

Commit 3af0e03

Browse files
authored
Enrich ScheduledMachine status with providerID and full nodeRef (#22)
Signed-off-by: Erick Bourgeois <erick@jeb.ca>
1 parent e74bc2c commit 3af0e03

12 files changed

Lines changed: 287 additions & 111 deletions

File tree

.claude/CHANGELOG.md

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

1010
---
1111

12+
## [2026-04-16 17:00] - Phase 1: Enrich ScheduledMachine status with providerID and full nodeRef
13+
14+
**Author:** Erick Bourgeois
15+
16+
### Changed
17+
- `src/crd.rs`: Added `provider_id: Option<String>` (serialized as `providerID`) to `ScheduledMachineStatus`. Replaced the thin `LocalObjectReference { name }` node reference with a new `NodeRef { apiVersion, kind, name, uid }` struct mirroring CAPI's `Machine.status.nodeRef`. Removed the now-unused `LocalObjectReference` type.
18+
- `src/crd_tests.rs`: Added 6 TDD cases covering providerID round-trip, full `nodeRef` deserialization, optional `uid`, serialization omission, old-shape rejection, and `NodeRef` round-trip.
19+
- `src/bin/crddoc.rs`: Documented new `providerID` and `nodeRef` status fields.
20+
- `deploy/crds/scheduledmachine.yaml`: Regenerated from the updated Rust types.
21+
- `docs/reference/api.md`: Regenerated to reflect new status schema.
22+
23+
### Why
24+
Phase 1 of the event-driven watches + status enrichment roadmap. Surfacing `providerID` and a full Node reference (with UID) on `ScheduledMachine.status` lets operators correlate a scheduled machine to a specific VM and Node from `kubectl get sm -o jsonpath=...`, without manual lookups across CAPI Machines and the Node API. This is the schema foundation that Phase 2 (reconciler populates the fields) and Phases 3–4 (event-driven watches on CAPI Machine and Node) build upon.
25+
26+
### Impact
27+
- [x] Breaking change — `status.nodeRef` shape changed from `{ name }` to `{ apiVersion, kind, name, uid }`. Existing CRs with the old shape must clear `status.nodeRef` before rollout, or the controller will report deserialization errors on that field.
28+
- [x] Requires cluster rollout — CRD must be re-applied alongside the new controller image.
29+
- [ ] Config change only
30+
- [ ] Documentation only
31+
32+
---
33+
1234
## [2026-04-17] - Bump base image to cc-debian13 and fix GLIBC_2.39 crash (issue #17)
1335

1436
**Author:** Daniel Guns

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,29 @@
44

55
A cloud-native Kubernetes controller for managing time-based machine scheduling on physical nodes using Cluster API (CAPI).
66

7-
## CI/CD
7+
### CI/CD
88

99
[![Build (PR)](https://img.shields.io/github/actions/workflow/status/finos/5-spot/build.yaml?event=pull_request&label=Build%20(PR))](https://github.com/finos/5-spot/actions/workflows/build.yaml)
1010
[![Build (push)](https://img.shields.io/github/actions/workflow/status/finos/5-spot/build.yaml?event=push&label=Build%20(push))](https://github.com/finos/5-spot/actions/workflows/build.yaml)
1111
[![Build (release)](https://img.shields.io/github/actions/workflow/status/finos/5-spot/build.yaml?event=release&label=Build%20(release))](https://github.com/finos/5-spot/actions/workflows/build.yaml)
1212

13-
## Technology & Compatibility
13+
### Technology & Compatibility
1414

1515
[![Rust](https://img.shields.io/badge/rust-1.75+-orange.svg?logo=rust&logoColor=white)](https://www.rust-lang.org)
1616
[![Kubernetes](https://img.shields.io/badge/kubernetes-1.27+-326CE5.svg?logo=kubernetes&logoColor=white)](https://kubernetes.io)
1717
[![Cluster API](https://img.shields.io/badge/Cluster%20API-CAPI-326CE5.svg?logo=kubernetes&logoColor=white)](https://cluster-api.sigs.k8s.io/)
18+
[![FINOS CALM](https://img.shields.io/badge/FINOS_CALM-1.37-blue?logo=finos)](https://calm.finos.org/)
1819
[![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black)](https://www.linux.org/)
1920
[![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white)](https://www.docker.com/)
2021

21-
## Security & Compliance
22+
### Security & Compliance
2223

2324
[![SPDX](https://img.shields.io/badge/SPDX-License--Identifier-blue)](https://spdx.dev/)
2425
[![Gitleaks](https://img.shields.io/badge/Gitleaks-Secret%20Scanning-blue)](https://github.com/gitleaks/gitleaks)
2526
[![Snyk](https://img.shields.io/badge/Snyk-SAST%20Scanning-purple)](https://snyk.io/)
2627
[![Aqua](https://img.shields.io/badge/Aqua-Container%20Scanning-00ADD8)](https://www.aquasec.com/)
2728

28-
## License
29+
### License
2930

3031
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
3132

deploy/crds/scheduledmachine.yaml

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
Compiling five_spot v0.1.0 (/Users/erick/dev/5-spot)
2-
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.57s
3-
Running `target/debug/crdgen`
41
apiVersion: apiextensions.k8s.io/v1
52
kind: CustomResourceDefinition
63
metadata:
@@ -286,13 +283,27 @@ spec:
286283
nullable: true
287284
type: string
288285
nodeRef:
289-
description: Reference to the Kubernetes Node (once provisioned)
286+
description: |-
287+
Reference to the Kubernetes Node (once provisioned), mirroring the
288+
shape of CAPI's `Machine.status.nodeRef`.
290289
nullable: true
291290
properties:
291+
apiVersion:
292+
description: API version of the Node resource (typically `"v1"`)
293+
type: string
294+
kind:
295+
description: Kind of the referenced object (typically `"Node"`)
296+
type: string
292297
name:
293-
description: Name of the referenced object
298+
description: Name of the Node
299+
type: string
300+
uid:
301+
description: UID of the Node, protecting against name reuse
302+
nullable: true
294303
type: string
295304
required:
305+
- apiVersion
306+
- kind
296307
- name
297308
type: object
298309
observedGeneration:
@@ -306,6 +317,13 @@ spec:
306317
Values: Pending, Active, `ShuttingDown`, Inactive, Disabled, Terminated, Error
307318
nullable: true
308319
type: string
320+
providerID:
321+
description: |-
322+
Provider-assigned machine identifier, copied from the CAPI
323+
`Machine.spec.providerID`. Stable for the life of the machine and
324+
unique across the cluster.
325+
nullable: true
326+
type: string
309327
type: object
310328
required:
311329
- spec

docs/architecture/calm/templates/mermaid/flows.md.hbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Architecture Flows
22

3+
[![FINOS CALM](https://img.shields.io/badge/FINOS_CALM-1.37-blue?logo=finos)](https://calm.finos.org/)
4+
35
!!! note "Auto-generated"
46

57
Rendered from `docs/architecture/calm/architecture.json` by the CALM

docs/architecture/calm/templates/mermaid/system.md.hbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# System Architecture
22

3+
[![FINOS CALM](https://img.shields.io/badge/FINOS_CALM-1.37-blue?logo=finos)](https://calm.finos.org/)
4+
35
!!! note "Auto-generated"
46

57
Rendered from `docs/architecture/calm/architecture.json` by the CALM

docs/reference/api.md

Lines changed: 61 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,37 @@ automatically added to and removed from a k0smotron cluster based on a time sche
1717
apiVersion: 5spot.finos.org/v1alpha1
1818
kind: ScheduledMachine
1919
metadata:
20-
name: example-machine
20+
name: example-spot-machine
2121
namespace: default
2222
spec:
23+
clusterName: my-cluster
2324
schedule:
2425
daysOfWeek:
2526
- mon-fri
2627
hoursOfDay:
27-
- "9-17"
28+
- 9-17
2829
timezone: America/New_York
2930
enabled: true
30-
clusterName: my-cluster
3131
bootstrapSpec:
3232
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
3333
kind: K0sWorkerConfig
3434
spec:
35-
version: v1.30.0+k0s.0
35+
version: v1.32.8+k0s.0
36+
downloadURL: https://github.com/k0sproject/k0s/releases/download/v1.32.8+k0s.0/k0s-v1.32.8+k0s.0-amd64
3637
infrastructureSpec:
3738
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
3839
kind: RemoteMachine
3940
spec:
4041
address: 192.168.1.100
4142
port: 22
42-
user: admin
43+
user: root
44+
sshKeyRef:
45+
name: my-ssh-key
46+
machineTemplate:
47+
labels:
48+
node-role.kubernetes.io/worker: spot
49+
annotations:
50+
example.com/scheduled-by: 5spot
4351
priority: 50
4452
gracefulShutdownTimeout: 5m
4553
nodeDrainTimeout: 5m
@@ -52,63 +60,55 @@ spec:
5260
5361
Machine scheduling configuration.
5462
55-
- **daysOfWeek** (optional, array of strings, default: `[]`): Days when machine should be active.
63+
- **daysOfWeek** (required, array of strings): Days when machine should be active.
5664
Supports ranges (`mon-fri`) and combinations (`mon-wed,fri-sun`).
5765

58-
- **hoursOfDay** (optional, array of strings, default: `[]`): Hours when machine should be active (0-23).
66+
- **hoursOfDay** (required, array of strings): Hours when machine should be active (0-23).
5967
Supports ranges (`9-17`) and combinations (`0-9,18-23`).
6068

6169
- **timezone** (optional, string, default: `UTC`): Timezone for the schedule.
6270
Must be a valid IANA timezone (e.g., `America/New_York`, `Europe/London`).
6371

64-
- **enabled** (optional, boolean, default: `true`): Whether the schedule is active.
65-
66-
*At least one of `daysOfWeek` or `hoursOfDay` must be non-empty.*
72+
- **enabled** (optional, boolean, default: `true`): Whether the schedule is enabled.
6773

6874
#### clusterName
6975

7076
(required, string) Name of the CAPI cluster this machine belongs to.
7177

7278
#### bootstrapSpec
7379

74-
(required, object) Inline bootstrap configuration. 5-Spot creates this resource when the
75-
schedule window opens. The `apiVersion`, `kind`, and `spec` map directly to the
76-
bootstrap provider's resource type (e.g., `K0sWorkerConfig`).
80+
(required, object) Inline bootstrap configuration that will be created when the schedule is active.
81+
This is a fully unstructured object that must contain:
7782

78-
```yaml
79-
bootstrapSpec:
80-
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
81-
kind: K0sWorkerConfig
82-
spec:
83-
version: v1.30.0+k0s.0
84-
```
83+
- **apiVersion** (required, string): API version of the bootstrap resource (e.g., `bootstrap.cluster.x-k8s.io/v1beta1`)
84+
- **kind** (required, string): Kind of the bootstrap resource (e.g., `K0sWorkerConfig`, `KubeadmConfig`)
85+
- **spec** (required, object): Provider-specific configuration for the bootstrap resource
86+
87+
The controller validates that the apiVersion belongs to an allowed bootstrap API group.
8588

8689
#### infrastructureSpec
8790

88-
(required, object) Inline infrastructure configuration. 5-Spot creates this resource when
89-
the schedule window opens. The `apiVersion`, `kind`, and `spec` map directly to the
90-
infrastructure provider's resource type (e.g., `RemoteMachine`).
91+
(required, object) Inline infrastructure configuration that will be created when the schedule is active.
92+
This is a fully unstructured object that must contain:
9193

92-
```yaml
93-
infrastructureSpec:
94-
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
95-
kind: RemoteMachine
96-
spec:
97-
address: 192.168.1.100
98-
port: 22
99-
user: admin
100-
```
94+
- **apiVersion** (required, string): API version of the infrastructure resource (e.g., `infrastructure.cluster.x-k8s.io/v1beta1`)
95+
- **kind** (required, string): Kind of the infrastructure resource (e.g., `RemoteMachine`, `AWSMachine`)
96+
- **spec** (required, object): Provider-specific configuration for the infrastructure resource
97+
98+
The controller validates that the apiVersion belongs to an allowed infrastructure API group.
10199

102100
#### machineTemplate
103101

104-
(optional, object) Labels and annotations applied to the created CAPI Machine resource.
102+
(optional, object) Configuration for the created CAPI Machine resource.
103+
104+
- **labels** (optional, map of string to string): Labels to apply to the created Machine
105+
- **annotations** (optional, map of string to string): Annotations to apply to the created Machine
105106

106-
- **labels** (optional, map): Labels to apply to the created Machine.
107-
- **annotations** (optional, map): Annotations to apply to the created Machine.
107+
Note: Labels and annotations using reserved prefixes (`5spot.finos.org/`, `cluster.x-k8s.io/`) are rejected.
108108

109109
#### priority
110110

111-
(optional, integer 0-255, default: `50`) Priority for machine scheduling.
111+
(optional, integer 0-100, default: `50`) Priority for machine scheduling.
112112
Higher values indicate higher priority. Used for resource distribution across
113113
operator instances.
114114

@@ -119,36 +119,28 @@ Format: `<number><unit>` where unit is `s` (seconds), `m` (minutes), or `h` (hou
119119

120120
#### nodeDrainTimeout
121121

122-
(optional, string, default: `5m`) Timeout for draining the Kubernetes node before
123-
the CAPI Machine is deleted.
122+
(optional, string, default: `5m`) Timeout for draining the node before deletion.
123+
Format: `<number><unit>` where unit is `s` (seconds), `m` (minutes), or `h` (hours).
124124

125125
#### killSwitch
126126

127127
(optional, boolean, default: `false`) When true, immediately removes the machine
128-
from the cluster, bypassing the graceful shutdown and drain steps.
128+
from the cluster and takes it out of rotation, bypassing the grace period.
129129

130130
### Status Fields
131131

132132
#### phase
133133

134134
Current phase of the machine lifecycle. Possible values:
135135

136-
- **Pending**: Initial state, schedule not yet evaluated
136+
- **Pending**: Initial state, awaiting schedule evaluation
137137
- **Active**: Machine is running and part of the cluster
138-
- **ShuttingDown**: Machine is being drained and removed
139-
- **Inactive**: Machine has been removed and is outside the schedule window
140-
- **Disabled**: Schedule is disabled (`schedule.enabled: false`)
141-
- **Terminated**: Machine was forcibly terminated (kill switch)
138+
- **ShuttingDown**: Machine is being gracefully removed (draining, etc.)
139+
- **Inactive**: Machine is outside scheduled time window and has been removed
140+
- **Disabled**: Schedule is disabled, machine is not active
141+
- **Terminated**: Machine has been permanently removed
142142
- **Error**: An error occurred during processing
143143

144-
#### message
145-
146-
Human-readable description of the current state or error.
147-
148-
#### inSchedule
149-
150-
Boolean. Whether the machine is currently within its scheduled time window.
151-
152144
#### conditions
153145

154146
Array of condition objects with the following fields:
@@ -157,47 +149,32 @@ Array of condition objects with the following fields:
157149
- **status**: `True`, `False`, or `Unknown`
158150
- **reason**: One-word reason in CamelCase
159151
- **message**: Human-readable message
160-
- **lastTransitionTime**: Last time the condition transitioned (RFC3339)
152+
- **lastTransitionTime**: Last time the condition transitioned
161153

162-
#### machineRef
163-
164-
Reference to the created CAPI Machine resource:
154+
#### inSchedule
165155

166-
- **apiVersion**: API version of the Machine resource
167-
- **kind**: Kind of the Machine resource
168-
- **name**: Machine name
169-
- **namespace**: Machine namespace
156+
(boolean) Whether the machine is currently within its scheduled time window.
170157

171-
#### bootstrapRef
158+
#### message
172159

173-
Reference to the created bootstrap resource (e.g., `K0sWorkerConfig`):
160+
(string) Human-readable message describing the current state.
174161

175-
- **apiVersion**, **kind**, **name**, **namespace**
162+
#### observedGeneration
176163

177-
#### infrastructureRef
164+
(integer) The generation observed by the controller. Used for change detection.
178165

179-
Reference to the created infrastructure resource (e.g., `RemoteMachine`):
166+
#### providerID
180167

181-
- **apiVersion**, **kind**, **name**, **namespace**
168+
(optional, string) Provider-assigned machine identifier, copied from the CAPI Machine's
169+
`spec.providerID`. Stable for the life of the machine and unique across the cluster.
170+
Examples: `libvirt:///uuid-abc-123`, `aws:///us-east-1a/i-0abcd1234`.
182171

183172
#### nodeRef
184173

185-
Reference to the Kubernetes Node once the machine has joined the cluster:
186-
187-
- **name**: Node name
188-
189-
#### lastScheduledTime
190-
191-
Last time a machine was created and activated (RFC3339 format).
192-
193-
#### nextActivation
194-
195-
Next time the machine will be activated according to its schedule (RFC3339 format).
196-
197-
#### nextCleanup
198-
199-
Time when the machine will be cleaned up (end of current schedule window, RFC3339 format).
200-
201-
#### observedGeneration
174+
(optional, object) Reference to the Kubernetes Node once the Machine is provisioned.
175+
Mirrors the shape of CAPI's `Machine.status.nodeRef`:
202176

203-
The spec generation last processed by the controller. Used for change detection.
177+
- **apiVersion** (required, string): API version of the Node resource (typically `v1`)
178+
- **kind** (required, string): Kind of the referenced object (typically `Node`)
179+
- **name** (required, string): Name of the Node
180+
- **uid** (optional, string): UID of the Node, protecting against name reuse

docs/src/concepts/machine-lifecycle.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,11 @@ status:
163163
name: business-hours-worker-infra
164164
namespace: default
165165
nodeRef:
166+
apiVersion: v1
167+
kind: Node
166168
name: worker-node-01
169+
uid: 11111111-2222-3333-4444-555555555555
170+
providerID: libvirt:///uuid-abc-123
167171
conditions:
168172
- type: Ready
169173
status: "True"

docs/src/concepts/scheduled-machine.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ The status subresource contains the current state:
132132
| `machineRef` | `ObjectReference` | Reference to created CAPI Machine |
133133
| `bootstrapRef` | `ObjectReference` | Reference to created bootstrap resource |
134134
| `infrastructureRef` | `ObjectReference` | Reference to created infrastructure resource |
135-
| `nodeRef` | `LocalObjectReference` | Reference to the Kubernetes Node (once provisioned) |
135+
| `nodeRef` | `NodeRef` | Reference to the Kubernetes Node (apiVersion, kind, name, uid) once provisioned |
136+
| `providerID` | `string` | Provider-assigned machine identifier (copied from CAPI `Machine.spec.providerID`) |
136137
| `lastScheduledTime` | `Time` | Last time machine was created |
137138
| `nextActivation` | `Time` | Next scheduled activation time |
138139
| `nextCleanup` | `Time` | Time when machine will be cleaned up |

0 commit comments

Comments
 (0)