Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .claude/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,28 @@ The format is based on the regulated environment requirements:

---

## [2026-04-16 17:00] - Phase 1: Enrich ScheduledMachine status with providerID and full nodeRef

**Author:** Erick Bourgeois

### Changed
- `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.
- `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.
- `src/bin/crddoc.rs`: Documented new `providerID` and `nodeRef` status fields.
- `deploy/crds/scheduledmachine.yaml`: Regenerated from the updated Rust types.
- `docs/reference/api.md`: Regenerated to reflect new status schema.

### Why
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.

### Impact
- [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.
- [x] Requires cluster rollout — CRD must be re-applied alongside the new controller image.
- [ ] Config change only
- [ ] Documentation only

---

## [2026-04-17] - Bump base image to cc-debian13 and fix GLIBC_2.39 crash (issue #17)

**Author:** Daniel Guns
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,29 @@

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

## CI/CD
### CI/CD

[![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)
[![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)
[![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)

## Technology & Compatibility
### Technology & Compatibility

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

## Security & Compliance
### Security & Compliance

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

## License
### License

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

Expand Down
28 changes: 23 additions & 5 deletions deploy/crds/scheduledmachine.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
Compiling five_spot v0.1.0 (/Users/erick/dev/5-spot)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.57s
Running `target/debug/crdgen`
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
Expand Down Expand Up @@ -286,13 +283,27 @@ spec:
nullable: true
type: string
nodeRef:
description: Reference to the Kubernetes Node (once provisioned)
description: |-
Reference to the Kubernetes Node (once provisioned), mirroring the
shape of CAPI's `Machine.status.nodeRef`.
nullable: true
properties:
apiVersion:
description: API version of the Node resource (typically `"v1"`)
type: string
kind:
description: Kind of the referenced object (typically `"Node"`)
type: string
name:
description: Name of the referenced object
description: Name of the Node
type: string
uid:
description: UID of the Node, protecting against name reuse
nullable: true
type: string
required:
- apiVersion
- kind
- name
type: object
observedGeneration:
Expand All @@ -306,6 +317,13 @@ spec:
Values: Pending, Active, `ShuttingDown`, Inactive, Disabled, Terminated, Error
nullable: true
type: string
providerID:
description: |-
Provider-assigned machine identifier, copied from the CAPI
`Machine.spec.providerID`. Stable for the life of the machine and
unique across the cluster.
nullable: true
type: string
type: object
required:
- spec
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/calm/templates/mermaid/flows.md.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Architecture Flows

[![FINOS CALM](https://img.shields.io/badge/FINOS_CALM-1.37-blue?logo=finos)](https://calm.finos.org/)

!!! note "Auto-generated"

Rendered from `docs/architecture/calm/architecture.json` by the CALM
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/calm/templates/mermaid/system.md.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# System Architecture

[![FINOS CALM](https://img.shields.io/badge/FINOS_CALM-1.37-blue?logo=finos)](https://calm.finos.org/)

!!! note "Auto-generated"

Rendered from `docs/architecture/calm/architecture.json` by the CALM
Expand Down
145 changes: 61 additions & 84 deletions docs/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,37 @@ automatically added to and removed from a k0smotron cluster based on a time sche
apiVersion: 5spot.finos.org/v1alpha1
kind: ScheduledMachine
metadata:
name: example-machine
name: example-spot-machine
namespace: default
spec:
clusterName: my-cluster
schedule:
daysOfWeek:
- mon-fri
hoursOfDay:
- "9-17"
- 9-17
timezone: America/New_York
enabled: true
clusterName: my-cluster
bootstrapSpec:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: K0sWorkerConfig
spec:
version: v1.30.0+k0s.0
version: v1.32.8+k0s.0
downloadURL: https://github.com/k0sproject/k0s/releases/download/v1.32.8+k0s.0/k0s-v1.32.8+k0s.0-amd64
infrastructureSpec:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: RemoteMachine
spec:
address: 192.168.1.100
port: 22
user: admin
user: root
sshKeyRef:
name: my-ssh-key
machineTemplate:
labels:
node-role.kubernetes.io/worker: spot
annotations:
example.com/scheduled-by: 5spot
priority: 50
gracefulShutdownTimeout: 5m
nodeDrainTimeout: 5m
Expand All @@ -52,63 +60,55 @@ spec:

Machine scheduling configuration.

- **daysOfWeek** (optional, array of strings, default: `[]`): Days when machine should be active.
- **daysOfWeek** (required, array of strings): Days when machine should be active.
Supports ranges (`mon-fri`) and combinations (`mon-wed,fri-sun`).

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

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

- **enabled** (optional, boolean, default: `true`): Whether the schedule is active.

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

#### clusterName

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

#### bootstrapSpec

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

```yaml
bootstrapSpec:
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: K0sWorkerConfig
spec:
version: v1.30.0+k0s.0
```
- **apiVersion** (required, string): API version of the bootstrap resource (e.g., `bootstrap.cluster.x-k8s.io/v1beta1`)
- **kind** (required, string): Kind of the bootstrap resource (e.g., `K0sWorkerConfig`, `KubeadmConfig`)
- **spec** (required, object): Provider-specific configuration for the bootstrap resource

The controller validates that the apiVersion belongs to an allowed bootstrap API group.

#### infrastructureSpec

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

```yaml
infrastructureSpec:
apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: RemoteMachine
spec:
address: 192.168.1.100
port: 22
user: admin
```
- **apiVersion** (required, string): API version of the infrastructure resource (e.g., `infrastructure.cluster.x-k8s.io/v1beta1`)
- **kind** (required, string): Kind of the infrastructure resource (e.g., `RemoteMachine`, `AWSMachine`)
- **spec** (required, object): Provider-specific configuration for the infrastructure resource

The controller validates that the apiVersion belongs to an allowed infrastructure API group.

#### machineTemplate

(optional, object) Labels and annotations applied to the created CAPI Machine resource.
(optional, object) Configuration for the created CAPI Machine resource.

- **labels** (optional, map of string to string): Labels to apply to the created Machine
- **annotations** (optional, map of string to string): Annotations to apply to the created Machine

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

#### priority

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

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

#### nodeDrainTimeout

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

#### killSwitch

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

### Status Fields

#### phase

Current phase of the machine lifecycle. Possible values:

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

#### message

Human-readable description of the current state or error.

#### inSchedule

Boolean. Whether the machine is currently within its scheduled time window.

#### conditions

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

#### machineRef

Reference to the created CAPI Machine resource:
#### inSchedule

- **apiVersion**: API version of the Machine resource
- **kind**: Kind of the Machine resource
- **name**: Machine name
- **namespace**: Machine namespace
(boolean) Whether the machine is currently within its scheduled time window.

#### bootstrapRef
#### message

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

- **apiVersion**, **kind**, **name**, **namespace**
#### observedGeneration

#### infrastructureRef
(integer) The generation observed by the controller. Used for change detection.

Reference to the created infrastructure resource (e.g., `RemoteMachine`):
#### providerID

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

#### nodeRef

Reference to the Kubernetes Node once the machine has joined the cluster:

- **name**: Node name

#### lastScheduledTime

Last time a machine was created and activated (RFC3339 format).

#### nextActivation

Next time the machine will be activated according to its schedule (RFC3339 format).

#### nextCleanup

Time when the machine will be cleaned up (end of current schedule window, RFC3339 format).

#### observedGeneration
(optional, object) Reference to the Kubernetes Node once the Machine is provisioned.
Mirrors the shape of CAPI's `Machine.status.nodeRef`:

The spec generation last processed by the controller. Used for change detection.
- **apiVersion** (required, string): API version of the Node resource (typically `v1`)
- **kind** (required, string): Kind of the referenced object (typically `Node`)
- **name** (required, string): Name of the Node
- **uid** (optional, string): UID of the Node, protecting against name reuse
4 changes: 4 additions & 0 deletions docs/src/concepts/machine-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,11 @@ status:
name: business-hours-worker-infra
namespace: default
nodeRef:
apiVersion: v1
kind: Node
name: worker-node-01
uid: 11111111-2222-3333-4444-555555555555
providerID: libvirt:///uuid-abc-123
conditions:
- type: Ready
status: "True"
Expand Down
3 changes: 2 additions & 1 deletion docs/src/concepts/scheduled-machine.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ The status subresource contains the current state:
| `machineRef` | `ObjectReference` | Reference to created CAPI Machine |
| `bootstrapRef` | `ObjectReference` | Reference to created bootstrap resource |
| `infrastructureRef` | `ObjectReference` | Reference to created infrastructure resource |
| `nodeRef` | `LocalObjectReference` | Reference to the Kubernetes Node (once provisioned) |
| `nodeRef` | `NodeRef` | Reference to the Kubernetes Node (apiVersion, kind, name, uid) once provisioned |
| `providerID` | `string` | Provider-assigned machine identifier (copied from CAPI `Machine.spec.providerID`) |
| `lastScheduledTime` | `Time` | Last time machine was created |
| `nextActivation` | `Time` | Next scheduled activation time |
| `nextCleanup` | `Time` | Time when machine will be cleaned up |
Expand Down
Loading
Loading