Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
585256c
feat: PoC failover controller
umswmayj Mar 5, 2026
c90dd80
minor
umswmayj Mar 12, 2026
f3f1d33
added some doc
umswmayj Mar 13, 2026
c9ddb47
minor
umswmayj Mar 13, 2026
0199c76
pr feedback
umswmayj Mar 13, 2026
a480514
Merge branch 'main' into failover-reservation-lifecyle
umswmayj Mar 13, 2026
baf605f
more pr feedback
umswmayj Mar 13, 2026
394c2f4
Merge remote-tracking branch 'origin/main' into failover-reservation-…
umswmayj Mar 16, 2026
8f3aead
pr feedback
umswmayj Mar 16, 2026
00ec020
pr feedback
umswmayj Mar 17, 2026
01d518d
Merge remote-tracking branch 'origin/main' into failover-reservation-…
umswmayj Mar 17, 2026
40359f3
Merge branch 'main' into failover-reservation-lifecyle
umswmayj Mar 17, 2026
1f26bdc
Merge remote-tracking branch 'origin/main' into failover-reservation-…
umswmayj Mar 17, 2026
2c1bf2f
mostly logging (via context) for debugging
umswmayj Mar 17, 2026
f35f5e8
.
umswmayj Mar 18, 2026
35a1c4b
Merge remote-tracking branch 'origin/main' into failover-reservation-…
umswmayj Mar 18, 2026
c564fe9
fix: fallback if EffectiveCapacity is not set
umswmayj Mar 18, 2026
26756bc
.
umswmayj Mar 18, 2026
197ca4e
Merge remote-tracking branch 'origin/main' into failover-reservation-…
umswmayj Mar 18, 2026
23b2869
.
umswmayj Mar 18, 2026
133fab1
Merge remote-tracking branch 'origin/main' into failover-reservation-…
umswmayj Mar 18, 2026
3a3b590
.
umswmayj Mar 18, 2026
93ec6e7
Merge remote-tracking branch 'origin/main' into failover-reservation-…
umswmayj Mar 18, 2026
95b8937
Merge remote-tracking branch 'origin/main' into failover-reservation-…
umswmayj Mar 18, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
*.dll
*.so
*.dylib
*.tgz

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good addition I'm always thinking of but immediately forgetting to commit

build/**

# Test binary, built with `go test -c`
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ GOBIN=$(shell go env GOBIN)
endif

.PHONY: all
all: crds deepcopy lint test
all: crds deepcopy lint-fix format lint test

.PHONY: help
help: ## Display this help.
Expand Down
16 changes: 14 additions & 2 deletions api/v1alpha1/reservation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const (

// LabelReservationType identifies the type of reservation.
// This label is present on all reservations to enable type-based filtering.
LabelReservationType = "reservations.cortex.sap.com/type"
LabelReservationType = "reservations.cortex.cloud/type"

// Reservation type label values
ReservationTypeLabelCommittedResource = "committed-resource"
Expand Down Expand Up @@ -152,6 +152,18 @@ type FailoverReservationStatus struct {
// Key: VM/instance UUID, Value: Host name where the VM is currently running.
// +kubebuilder:validation:Optional
Allocations map[string]string `json:"allocations,omitempty"`

// LastChanged tracks when the reservation was last modified.
// This is used to track pending changes that need acknowledgment.
// +kubebuilder:validation:Optional
LastChanged *metav1.Time `json:"lastChanged,omitempty"`

// AcknowledgedAt is the timestamp when the last change was acknowledged.
// When nil, the reservation is in a pending state awaiting acknowledgment.
// This does not affect the Ready condition - reservations are still considered
// ready even when not yet acknowledged.
// +kubebuilder:validation:Optional
AcknowledgedAt *metav1.Time `json:"acknowledgedAt,omitempty"`
}

// ReservationStatus defines the observed state of Reservation.
Expand Down Expand Up @@ -189,7 +201,7 @@ type ReservationStatus struct {
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".metadata.labels['reservations\\.cortex\\.sap\\.com/type']"
// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".metadata.labels['reservations\\.cortex\\.cloud/type']"
// +kubebuilder:printcolumn:name="Host",type="string",JSONPath=".status.host"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"

Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

106 changes: 106 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,16 @@ import (
"github.com/cobaltcore-dev/cortex/internal/knowledge/kpis"
"github.com/cobaltcore-dev/cortex/internal/scheduling/cinder"
"github.com/cobaltcore-dev/cortex/internal/scheduling/explanation"
"github.com/cobaltcore-dev/cortex/internal/scheduling/external"
schedulinglib "github.com/cobaltcore-dev/cortex/internal/scheduling/lib"
"github.com/cobaltcore-dev/cortex/internal/scheduling/machines"
"github.com/cobaltcore-dev/cortex/internal/scheduling/manila"
"github.com/cobaltcore-dev/cortex/internal/scheduling/nova"
"github.com/cobaltcore-dev/cortex/internal/scheduling/pods"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/commitments"
reservationscontroller "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/controller"
"github.com/cobaltcore-dev/cortex/internal/scheduling/reservations/failover"
"github.com/cobaltcore-dev/cortex/pkg/conf"
"github.com/cobaltcore-dev/cortex/pkg/monitoring"
"github.com/cobaltcore-dev/cortex/pkg/multicluster"
Expand Down Expand Up @@ -142,6 +145,12 @@ func main() {

ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

// Log the main configuration
setupLog.Info("loaded main configuration",
"enabledControllers", mainConfig.EnabledControllers,
"enabledTasks", mainConfig.EnabledTasks,
"leaderElectionID", mainConfig.LeaderElectionID)

// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
Expand Down Expand Up @@ -350,6 +359,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "nova-deschedulings-executor") {
setupLog.Info("enabling controller", "controller", "nova-deschedulings-executor")
executorConfig := conf.GetConfigOrDie[nova.DeschedulingsExecutorConfig]()
novaClient := nova.NewNovaClient()
novaClientConfig := conf.GetConfigOrDie[nova.NovaClientConfig]()
Expand Down Expand Up @@ -379,6 +389,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "manila-decisions-pipeline-controller") {
setupLog.Info("enabling controller", "controller", "manila-decisions-pipeline-controller")
controller := &manila.FilterWeigherPipelineController{
Monitor: filterWeigherPipelineMonitor,
}
Expand All @@ -398,6 +409,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "cinder-decisions-pipeline-controller") {
setupLog.Info("enabling controller", "controller", "cinder-decisions-pipeline-controller")
controller := &cinder.FilterWeigherPipelineController{
Monitor: filterWeigherPipelineMonitor,
}
Expand All @@ -417,6 +429,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "ironcore-decisions-pipeline-controller") {
setupLog.Info("enabling controller", "controller", "ironcore-decisions-pipeline-controller")
controller := &machines.FilterWeigherPipelineController{
Monitor: filterWeigherPipelineMonitor,
}
Expand All @@ -435,6 +448,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "pods-decisions-pipeline-controller") {
setupLog.Info("enabling controller", "controller", "pods-decisions-pipeline-controller")
controller := &pods.FilterWeigherPipelineController{
Monitor: filterWeigherPipelineMonitor,
}
Expand All @@ -453,6 +467,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "explanation-controller") {
setupLog.Info("enabling controller", "controller", "explanation-controller")
// Setup a controller which will reconcile the history and explanation for
// decision resources.
explanationControllerConfig := conf.GetConfigOrDie[explanation.ControllerConfig]()
Expand All @@ -466,6 +481,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "reservations-controller") {
setupLog.Info("enabling controller", "controller", "reservations-controller")
monitor := reservationscontroller.NewControllerMonitor(multiclusterClient)
metrics.Registry.MustRegister(&monitor)
reservationsControllerConfig := conf.GetConfigOrDie[reservationscontroller.Config]()
Expand All @@ -480,6 +496,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "datasource-controllers") {
setupLog.Info("enabling controller", "controller", "datasource-controllers")
monitor := datasources.NewMonitor()
metrics.Registry.MustRegister(&monitor)
if err := (&openstack.OpenStackDatasourceReconciler{
Expand All @@ -502,6 +519,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "knowledge-controllers") {
setupLog.Info("enabling controller", "controller", "knowledge-controllers")
monitor := extractor.NewMonitor()
metrics.Registry.MustRegister(&monitor)
if err := (&extractor.KnowledgeReconciler{
Expand All @@ -523,6 +541,7 @@ func main() {
}
}
if slices.Contains(mainConfig.EnabledControllers, "kpis-controller") {
setupLog.Info("enabling controller", "controller", "kpis-controller")
kpisControllerConfig := conf.GetConfigOrDie[kpis.ControllerConfig]()
if err := (&kpis.Controller{
Client: multiclusterClient,
Expand All @@ -532,6 +551,93 @@ func main() {
os.Exit(1)
}
}
if slices.Contains(mainConfig.EnabledControllers, "failover-reservations-controller") {
setupLog.Info("enabling controller", "controller", "failover-reservations-controller")
failoverConfig := conf.GetConfigOrDie[failover.FailoverConfig]()

// Apply defaults for unset values
defaults := failover.DefaultConfig()
if failoverConfig.DatasourceName == "" {
failoverConfig.DatasourceName = defaults.DatasourceName
}
if failoverConfig.SchedulerURL == "" {
failoverConfig.SchedulerURL = defaults.SchedulerURL
}
if failoverConfig.ReconcileInterval == 0 {
failoverConfig.ReconcileInterval = defaults.ReconcileInterval
}
if failoverConfig.Creator == "" {
failoverConfig.Creator = defaults.Creator
}
if failoverConfig.FlavorFailoverRequirements == nil {
failoverConfig.FlavorFailoverRequirements = defaults.FlavorFailoverRequirements
}
if failoverConfig.RevalidationInterval == 0 {
failoverConfig.RevalidationInterval = defaults.RevalidationInterval
}

// DatasourceName is still required - check after applying defaults
if failoverConfig.DatasourceName == "" {
setupLog.Error(nil, "failover-reservations-controller requires datasourceName to be configured")
os.Exit(1)
}

// The scheduler client calls the nova external scheduler API to get placement decisions
schedulerClient := reservations.NewSchedulerClient(failoverConfig.SchedulerURL)

// Defer the initialization of PostgresReader until the manager starts
// because the cache is not ready during setup
if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
// Create PostgresReader from the configured Datasource CRD
// This runs after the cache is started
postgresReader, err := external.NewPostgresReader(ctx, multiclusterClient, failoverConfig.DatasourceName)
if err != nil {
setupLog.Error(err, "unable to create postgres reader for failover controller",
"datasourceName", failoverConfig.DatasourceName)
return err
}

// Create NovaReader and DBVMSource
novaReader := external.NewNovaReader(postgresReader)
vmSource := failover.NewDBVMSource(novaReader)

// Create the unified failover controller
// It handles both:
// 1. Watch-based per-reservation reconciliation (acknowledgment, validation)
// 2. Periodic bulk VM processing (creating/assigning reservations)
failoverController := failover.NewFailoverReservationController(
multiclusterClient,
vmSource,
failoverConfig,
schedulerClient,
)

// Set up the watch-based reconciler for per-reservation reconciliation
if err := failoverController.SetupWithManager(mgr, multiclusterClient); err != nil {
setupLog.Error(err, "unable to set up failover reservation controller")
return err
}

setupLog.Info("failover-reservations-controller starting",
"datasourceName", failoverConfig.DatasourceName,
"schedulerURL", failoverConfig.SchedulerURL,
"reconcileInterval", failoverConfig.ReconcileInterval,
"revalidationInterval", failoverConfig.RevalidationInterval)

// Start the controller's periodic reconciliation loop
return failoverController.Start(ctx)
})); err != nil {
setupLog.Error(err, "unable to add failover controller to manager")
os.Exit(1)
}
setupLog.Info("failover-reservations-controller registered",
"datasourceName", failoverConfig.DatasourceName,
"schedulerURL", failoverConfig.SchedulerURL,
"reconcileInterval", failoverConfig.ReconcileInterval,
"revalidationInterval", failoverConfig.RevalidationInterval,
"trustHypervisorLocation", failoverConfig.TrustHypervisorLocation,
"maxVMsToProcess", failoverConfig.MaxVMsToProcess)
}

// +kubebuilder:scaffold:builder

Expand Down
Loading
Loading