From 36b7d9468c5428622f1919a7bf76e4e7788c7483 Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 5 May 2026 10:25:05 +0200 Subject: [PATCH 01/19] feat: adding pipeline options --- .../filter_weigher_pipeline_controller.go | 2 +- internal/scheduling/lib/filter_monitor.go | 4 +-- .../scheduling/lib/filter_monitor_test.go | 2 +- internal/scheduling/lib/filter_test.go | 2 +- internal/scheduling/lib/filter_validation.go | 4 +-- .../scheduling/lib/filter_validation_test.go | 2 +- .../scheduling/lib/filter_weigher_pipeline.go | 16 ++++++---- .../lib/filter_weigher_pipeline_step.go | 4 ++- .../filter_weigher_pipeline_step_monitor.go | 3 +- ...lter_weigher_pipeline_step_monitor_test.go | 2 +- .../lib/filter_weigher_pipeline_test.go | 4 +-- internal/scheduling/lib/options.go | 32 +++++++++++++++++++ internal/scheduling/lib/weigher_monitor.go | 4 +-- .../scheduling/lib/weigher_monitor_test.go | 2 +- internal/scheduling/lib/weigher_test.go | 2 +- internal/scheduling/lib/weigher_validation.go | 4 +-- .../scheduling/lib/weigher_validation_test.go | 4 +-- .../filter_weigher_pipeline_controller.go | 2 +- ...filter_weigher_pipeline_controller_test.go | 2 +- .../machines/plugins/filters/filter_noop.go | 2 +- .../plugins/filters/filter_noop_test.go | 3 +- .../filter_weigher_pipeline_controller.go | 2 +- .../weighers/netapp_cpu_usage_balancing.go | 2 +- .../netapp_cpu_usage_balancing_test.go | 3 +- .../filter_weigher_pipeline_controller.go | 20 ++++++------ ...filter_weigher_pipeline_controller_test.go | 8 ++--- .../filters/filter_aggregate_metadata.go | 2 +- .../filters/filter_aggregate_metadata_test.go | 5 +-- .../filters/filter_allowed_projects.go | 2 +- .../filters/filter_allowed_projects_test.go | 3 +- .../plugins/filters/filter_capabilities.go | 2 +- .../filters/filter_capabilities_test.go | 5 +-- .../nova/plugins/filters/filter_correct_az.go | 2 +- .../plugins/filters/filter_correct_az_test.go | 3 +- .../plugins/filters/filter_exclude_hosts.go | 1 + .../filters/filter_exclude_hosts_test.go | 2 +- .../filters/filter_external_customer.go | 2 +- .../filters/filter_external_customer_test.go | 3 +- .../filters/filter_has_accelerators.go | 2 +- .../filters/filter_has_accelerators_test.go | 3 +- .../filters/filter_has_enough_capacity.go | 2 +- .../filter_has_enough_capacity_test.go | 13 ++++---- .../filters/filter_has_requested_traits.go | 2 +- .../filter_has_requested_traits_test.go | 3 +- .../filters/filter_host_instructions.go | 2 +- .../filters/filter_host_instructions_test.go | 3 +- .../filters/filter_instance_group_affinity.go | 1 + .../filter_instance_group_affinity_test.go | 3 +- .../filter_instance_group_anti_affinity.go | 1 + ...ilter_instance_group_anti_affinity_test.go | 3 +- .../plugins/filters/filter_live_migratable.go | 1 + .../filters/filter_live_migratable_test.go | 6 ++-- .../filters/filter_requested_destination.go | 2 +- .../filter_requested_destination_test.go | 4 +-- .../filters/filter_status_conditions.go | 2 +- .../filters/filter_status_conditions_test.go | 3 +- .../nova/plugins/weighers/kvm_binpack.go | 2 +- .../nova/plugins/weighers/kvm_binpack_test.go | 3 +- .../weighers/kvm_failover_evacuation.go | 2 +- .../weighers/kvm_failover_evacuation_test.go | 3 +- .../kvm_failover_reservation_consolidation.go | 2 +- ...failover_reservation_consolidation_test.go | 3 +- .../kvm_instance_group_soft_affinity.go | 2 +- .../kvm_instance_group_soft_affinity_test.go | 3 +- .../weighers/kvm_prefer_smaller_hosts.go | 2 +- .../weighers/kvm_prefer_smaller_hosts_test.go | 3 +- .../vmware_anti_affinity_noisy_projects.go | 2 +- ...mware_anti_affinity_noisy_projects_test.go | 3 +- .../vmware_avoid_long_term_contended_hosts.go | 2 +- ...re_avoid_long_term_contended_hosts_test.go | 3 +- ...vmware_avoid_short_term_contended_hosts.go | 2 +- ...e_avoid_short_term_contended_hosts_test.go | 3 +- .../nova/plugins/weighers/vmware_binpack.go | 2 +- .../plugins/weighers/vmware_binpack_test.go | 3 +- .../filter_weigher_pipeline_controller.go | 2 +- ...filter_weigher_pipeline_controller_test.go | 2 +- .../plugins/filters/filter_node_affinity.go | 2 +- .../filters/filter_node_affinity_test.go | 3 +- .../plugins/filters/filter_node_available.go | 2 +- .../filters/filter_node_available_test.go | 3 +- .../plugins/filters/filter_node_capacity.go | 2 +- .../filters/filter_node_capacity_test.go | 3 +- .../pods/plugins/filters/filter_noop.go | 2 +- .../pods/plugins/filters/filter_noop_test.go | 3 +- .../pods/plugins/filters/filter_taint.go | 2 +- .../pods/plugins/filters/filter_taint_test.go | 3 +- .../pods/plugins/weighers/binpack.go | 2 +- .../pods/plugins/weighers/binpack_test.go | 3 +- 88 files changed, 188 insertions(+), 116 deletions(-) create mode 100644 internal/scheduling/lib/options.go diff --git a/internal/scheduling/cinder/filter_weigher_pipeline_controller.go b/internal/scheduling/cinder/filter_weigher_pipeline_controller.go index 52ec37306..5a4081784 100644 --- a/internal/scheduling/cinder/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/cinder/filter_weigher_pipeline_controller.go @@ -121,7 +121,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision return err } - result, err := pipeline.Run(request) + result, err := pipeline.Run(request, lib.Options{}) if err != nil { log.Error(err, "failed to run pipeline") return err diff --git a/internal/scheduling/lib/filter_monitor.go b/internal/scheduling/lib/filter_monitor.go index d0afd9282..97e9d661d 100644 --- a/internal/scheduling/lib/filter_monitor.go +++ b/internal/scheduling/lib/filter_monitor.go @@ -43,6 +43,6 @@ func (fm *FilterMonitor[RequestType]) Validate(ctx context.Context, params v1alp } // Run the filter and observe its execution. -func (fm *FilterMonitor[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { - return fm.monitor.RunWrapped(traceLog, request, fm.filter) +func (fm *FilterMonitor[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { + return fm.monitor.RunWrapped(traceLog, request, opts, fm.filter) } diff --git a/internal/scheduling/lib/filter_monitor_test.go b/internal/scheduling/lib/filter_monitor_test.go index f709d88aa..b27f811b1 100644 --- a/internal/scheduling/lib/filter_monitor_test.go +++ b/internal/scheduling/lib/filter_monitor_test.go @@ -100,7 +100,7 @@ func TestFilterMonitor_Run(t *testing.T) { Weights: map[string]float64{"host1": 0.1, "host2": 0.2, "host3": 0.3}, } - result, err := fm.Run(slog.Default(), request) + result, err := fm.Run(slog.Default(), request, Options{}) if err != nil { t.Errorf("expected no error, got %v", err) } diff --git a/internal/scheduling/lib/filter_test.go b/internal/scheduling/lib/filter_test.go index 652211163..14fe73997 100644 --- a/internal/scheduling/lib/filter_test.go +++ b/internal/scheduling/lib/filter_test.go @@ -31,7 +31,7 @@ func (m *mockFilter[RequestType]) Validate(ctx context.Context, params v1alpha1. } return m.ValidateFunc(ctx, params) } -func (m *mockFilter[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { +func (m *mockFilter[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { if m.RunFunc == nil { return &FilterWeigherPipelineStepResult{}, nil } diff --git a/internal/scheduling/lib/filter_validation.go b/internal/scheduling/lib/filter_validation.go index 9ad43311d..2054a9ad9 100644 --- a/internal/scheduling/lib/filter_validation.go +++ b/internal/scheduling/lib/filter_validation.go @@ -35,8 +35,8 @@ func validateFilter[RequestType FilterWeigherPipelineRequest](filter Filter[Requ } // Run the filter and validate what happens. -func (s *FilterValidator[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { - result, err := s.Filter.Run(traceLog, request) +func (s *FilterValidator[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { + result, err := s.Filter.Run(traceLog, request, opts) if err != nil { return nil, err } diff --git a/internal/scheduling/lib/filter_validation_test.go b/internal/scheduling/lib/filter_validation_test.go index dc35c2f6a..deb064dfc 100644 --- a/internal/scheduling/lib/filter_validation_test.go +++ b/internal/scheduling/lib/filter_validation_test.go @@ -156,7 +156,7 @@ func TestFilterValidator_Run(t *testing.T) { } traceLog := slog.Default() - result, err := validator.Run(traceLog, request) + result, err := validator.Run(traceLog, request, Options{}) if tt.expectError && err == nil { t.Error("expected error but got nil") diff --git a/internal/scheduling/lib/filter_weigher_pipeline.go b/internal/scheduling/lib/filter_weigher_pipeline.go index ee769433d..871aa771f 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline.go +++ b/internal/scheduling/lib/filter_weigher_pipeline.go @@ -18,8 +18,8 @@ import ( ) type FilterWeigherPipeline[RequestType FilterWeigherPipelineRequest] interface { - // Run the scheduling pipeline with the given request. - Run(request RequestType) (v1alpha1.DecisionResult, error) + // Run the scheduling pipeline with the given request and call-time options. + Run(request RequestType, opts Options) (v1alpha1.DecisionResult, error) } // Pipeline of scheduler steps. @@ -138,6 +138,7 @@ func InitNewFilterWeigherPipeline[RequestType FilterWeigherPipelineRequest]( func (p *filterWeigherPipeline[RequestType]) runFilters( log *slog.Logger, request RequestType, + opts Options, ) (filteredRequest RequestType, stepResults []v1alpha1.StepResult) { filteredRequest = request @@ -145,7 +146,7 @@ func (p *filterWeigherPipeline[RequestType]) runFilters( filter := p.filters[filterName] stepLog := log.With("filter", filterName) stepLog.Info("scheduler: running filter") - result, err := filter.Run(stepLog, filteredRequest) + result, err := filter.Run(stepLog, filteredRequest, opts) if errors.Is(err, ErrStepSkipped) { stepLog.Info("scheduler: filter skipped") continue @@ -170,6 +171,7 @@ func (p *filterWeigherPipeline[RequestType]) runFilters( func (p *filterWeigherPipeline[RequestType]) runWeighers( log *slog.Logger, filteredRequest RequestType, + opts Options, ) map[string]map[string]float64 { activationsByStep := map[string]map[string]float64{} @@ -181,7 +183,7 @@ func (p *filterWeigherPipeline[RequestType]) runWeighers( wg.Go(func() { stepLog := log.With("weigher", weigherName) stepLog.Info("scheduler: running weigher") - result, err := weigher.Run(stepLog, filteredRequest) + result, err := weigher.Run(stepLog, filteredRequest, opts) if errors.Is(err, ErrStepSkipped) { stepLog.Info("scheduler: weigher skipped") return @@ -262,7 +264,7 @@ func (s *filterWeigherPipeline[RequestType]) sortHostsByWeights(weights map[stri } // Evaluate the pipeline and return a list of hosts in order of preference. -func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1.DecisionResult, error) { +func (p *filterWeigherPipeline[RequestType]) Run(request RequestType, opts Options) (v1alpha1.DecisionResult, error) { slogArgs := request.GetTraceLogArgs() slogArgsAny := make([]any, 0, len(slogArgs)) for _, arg := range slogArgs { @@ -279,7 +281,7 @@ func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1. // Run filters first to reduce the number of hosts. // Any weights assigned to filtered out hosts are ignored. - filteredRequest, filterStepResults := p.runFilters(traceLog, request) + filteredRequest, filterStepResults := p.runFilters(traceLog, request, opts) traceLog.Info( "scheduler: finished filters", "remainingHosts", filteredRequest.GetHosts(), @@ -290,7 +292,7 @@ func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1. for _, host := range filteredRequest.GetHosts() { remainingWeights[host] = inWeights[host] } - stepWeights := p.runWeighers(traceLog, filteredRequest) + stepWeights := p.runWeighers(traceLog, filteredRequest, opts) outWeights := p.applyWeights(traceLog, stepWeights, remainingWeights) traceLog.Info("scheduler: output weights", "weights", outWeights) diff --git a/internal/scheduling/lib/filter_weigher_pipeline_step.go b/internal/scheduling/lib/filter_weigher_pipeline_step.go index 26dc5de40..cf6b3f207 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_step.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_step.go @@ -30,7 +30,9 @@ type FilterWeigherPipelineStep[RequestType FilterWeigherPipelineRequest] interfa // // A traceLog is provided that contains the global request id and should // be used to log the step's execution. - Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) + // + // opts carries per-call behavioral options set by the pipeline caller. + Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) } // Common base for all steps that provides some functionality diff --git a/internal/scheduling/lib/filter_weigher_pipeline_step_monitor.go b/internal/scheduling/lib/filter_weigher_pipeline_step_monitor.go index 3e64fa6ee..e54651ec5 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_step_monitor.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_step_monitor.go @@ -65,6 +65,7 @@ func monitorStep[RequestType FilterWeigherPipelineRequest](stepName string, m Fi func (s *FilterWeigherPipelineStepMonitor[RequestType]) RunWrapped( traceLog *slog.Logger, request RequestType, + opts Options, step FilterWeigherPipelineStep[RequestType], ) (*FilterWeigherPipelineStepResult, error) { @@ -74,7 +75,7 @@ func (s *FilterWeigherPipelineStepMonitor[RequestType]) RunWrapped( } inWeights := request.GetWeights() - stepResult, err := step.Run(traceLog, request) + stepResult, err := step.Run(traceLog, request, opts) if err != nil { return nil, err } diff --git a/internal/scheduling/lib/filter_weigher_pipeline_step_monitor_test.go b/internal/scheduling/lib/filter_weigher_pipeline_step_monitor_test.go index 7d7817abd..af4bf74ec 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_step_monitor_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_step_monitor_test.go @@ -38,7 +38,7 @@ func TestStepMonitorRun(t *testing.T) { Hosts: []string{"host1", "host2", "host3"}, Weights: map[string]float64{"host1": 0.2, "host2": 0.1, "host3": 0.0}, } - if _, err := monitor.RunWrapped(slog.Default(), request, step); err != nil { + if _, err := monitor.RunWrapped(slog.Default(), request, Options{}, step); err != nil { t.Fatalf("Run() error = %v, want nil", err) } if len(removedHostsObserver.Observations) != 1 { diff --git a/internal/scheduling/lib/filter_weigher_pipeline_test.go b/internal/scheduling/lib/filter_weigher_pipeline_test.go index 0e2775944..9636084f3 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_test.go @@ -72,7 +72,7 @@ func TestPipeline_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := pipeline.Run(tt.request) + result, err := pipeline.Run(tt.request, Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -221,7 +221,7 @@ func TestPipeline_RunFilters(t *testing.T) { Weights: map[string]float64{"host1": 0.0, "host2": 0.0, "host3": 0.0}, } - req, _ := p.runFilters(slog.Default(), request) + req, _ := p.runFilters(slog.Default(), request, Options{}) if len(req.Hosts) != 2 { t.Fatalf("expected 2 step results, got %d", len(req.Hosts)) } diff --git a/internal/scheduling/lib/options.go b/internal/scheduling/lib/options.go new file mode 100644 index 000000000..28819415d --- /dev/null +++ b/internal/scheduling/lib/options.go @@ -0,0 +1,32 @@ +// Copyright SAP SE +// SPDX-License-Identifier: Apache-2.0 + +package lib + +import "github.com/cobaltcore-dev/cortex/api/v1alpha1" + +// Options configure the behavior of a single pipeline run at call time. +// These are distinct from per-step YAML options (FilterWeigherPipelineStepOpts), +// which are static and set when the pipeline is initialized. +// +// Consumed by steps: ReadOnly, LockReservations, AssumeEmptyHosts, IgnoredReservationTypes. +// Consumed by the controller after pipeline.Run(): RecordHistory, CreateInflight. +type Options struct { + // ReadOnly means the pipeline could run without using the mutex, i.e. concurrent runs are ok. + ReadOnly bool + // LockReservations prevents reservation unlocking, e.g. in the capacity filter. + // Set when finding hosts for new reservations (failover, CR) to see true available capacity. + LockReservations bool + // AssumeEmptyHosts treats all hosts as having no running VMs. + AssumeEmptyHosts bool + // IgnoredReservationTypes lists reservation types the capacity filter skips entirely. + IgnoredReservationTypes []v1alpha1.ReservationType + // MaxCandidates limits the number of hosts returned after weighing. 0 means no limit. + MaxCandidates int + + // RecordHistory records the placement decision in placement history. + // Replaces pipeline.Spec.CreateHistory once pipelines consolidate. + RecordHistory bool + // CreateInflight creates pessimistic blocking reservations for all returned candidates. + CreateInflight bool +} diff --git a/internal/scheduling/lib/weigher_monitor.go b/internal/scheduling/lib/weigher_monitor.go index df855d067..56a9ebb6e 100644 --- a/internal/scheduling/lib/weigher_monitor.go +++ b/internal/scheduling/lib/weigher_monitor.go @@ -43,6 +43,6 @@ func (wm *WeigherMonitor[RequestType]) Validate(ctx context.Context, params v1al } // Run the weigher and observe its execution. -func (wm *WeigherMonitor[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { - return wm.monitor.RunWrapped(traceLog, request, wm.weigher) +func (wm *WeigherMonitor[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { + return wm.monitor.RunWrapped(traceLog, request, opts, wm.weigher) } diff --git a/internal/scheduling/lib/weigher_monitor_test.go b/internal/scheduling/lib/weigher_monitor_test.go index 6f8f906e3..c84435234 100644 --- a/internal/scheduling/lib/weigher_monitor_test.go +++ b/internal/scheduling/lib/weigher_monitor_test.go @@ -100,7 +100,7 @@ func TestWeigherMonitor_Run(t *testing.T) { Weights: map[string]float64{"host1": 0.1, "host2": 0.2, "host3": 0.3}, } - result, err := wm.Run(slog.Default(), request) + result, err := wm.Run(slog.Default(), request, Options{}) if err != nil { t.Errorf("expected no error, got %v", err) } diff --git a/internal/scheduling/lib/weigher_test.go b/internal/scheduling/lib/weigher_test.go index 4660207c4..488704ef4 100644 --- a/internal/scheduling/lib/weigher_test.go +++ b/internal/scheduling/lib/weigher_test.go @@ -34,7 +34,7 @@ func (m *mockWeigher[RequestType]) Validate(ctx context.Context, params v1alpha1 } return m.ValidateFunc(ctx, params) } -func (m *mockWeigher[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { +func (m *mockWeigher[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { if m.RunFunc == nil { return &FilterWeigherPipelineStepResult{}, nil } diff --git a/internal/scheduling/lib/weigher_validation.go b/internal/scheduling/lib/weigher_validation.go index c454d171e..bb4c6b823 100644 --- a/internal/scheduling/lib/weigher_validation.go +++ b/internal/scheduling/lib/weigher_validation.go @@ -35,8 +35,8 @@ func validateWeigher[RequestType FilterWeigherPipelineRequest](weigher Weigher[R } // Run the weigher and validate what happens. -func (s *WeigherValidator[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { - result, err := s.Weigher.Run(traceLog, request) +func (s *WeigherValidator[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { + result, err := s.Weigher.Run(traceLog, request, opts) if err != nil { return nil, err } diff --git a/internal/scheduling/lib/weigher_validation_test.go b/internal/scheduling/lib/weigher_validation_test.go index 852448a88..af7efb163 100644 --- a/internal/scheduling/lib/weigher_validation_test.go +++ b/internal/scheduling/lib/weigher_validation_test.go @@ -96,7 +96,7 @@ func TestWeigherValidator_Run_ValidHosts(t *testing.T) { Weigher: mockStep, } - result, err := validator.Run(slog.Default(), request) + result, err := validator.Run(slog.Default(), request, Options{}) if err != nil { t.Errorf("Run() error = %v, want nil", err) } @@ -130,7 +130,7 @@ func TestWeigherValidator_Run_HostNumberMismatch(t *testing.T) { Weigher: mockStep, } - result, err := validator.Run(slog.Default(), request) + result, err := validator.Run(slog.Default(), request, Options{}) if err == nil { t.Errorf("Run() error = nil, want error") } diff --git a/internal/scheduling/machines/filter_weigher_pipeline_controller.go b/internal/scheduling/machines/filter_weigher_pipeline_controller.go index 35d51708a..93e0f5e41 100644 --- a/internal/scheduling/machines/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/machines/filter_weigher_pipeline_controller.go @@ -144,7 +144,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision // Execute the scheduling pipeline. request := ironcore.MachinePipelineRequest{Pools: pools.Items} - result, err := pipeline.Run(request) + result, err := pipeline.Run(request, lib.Options{}) if err != nil { log.V(1).Error(err, "failed to run scheduler pipeline") return errors.New("failed to run scheduler pipeline") diff --git a/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go b/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go index bc2e0722a..28fe49ed7 100644 --- a/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go @@ -516,7 +516,7 @@ func createMockPipeline() lib.FilterWeigherPipeline[ironcore.MachinePipelineRequ type mockMachinePipeline struct{} -func (m *mockMachinePipeline) Run(request ironcore.MachinePipelineRequest) (v1alpha1.DecisionResult, error) { +func (m *mockMachinePipeline) Run(request ironcore.MachinePipelineRequest, opts lib.Options) (v1alpha1.DecisionResult, error) { if len(request.Pools) == 0 { return v1alpha1.DecisionResult{}, nil } diff --git a/internal/scheduling/machines/plugins/filters/filter_noop.go b/internal/scheduling/machines/plugins/filters/filter_noop.go index da901e5c0..56fb55dfe 100644 --- a/internal/scheduling/machines/plugins/filters/filter_noop.go +++ b/internal/scheduling/machines/plugins/filters/filter_noop.go @@ -31,7 +31,7 @@ func (f *NoopFilter) Validate(ctx context.Context, params v1alpha1.Parameters) e // not in the map are considered as filtered out. // Provide a traceLog that contains the global request id and should // be used to log the step's execution. -func (NoopFilter) Run(traceLog *slog.Logger, request ironcore.MachinePipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (NoopFilter) Run(traceLog *slog.Logger, request ironcore.MachinePipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64, len(request.Pools)) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) // Usually you would do some filtering here, or adjust the weights. diff --git a/internal/scheduling/machines/plugins/filters/filter_noop_test.go b/internal/scheduling/machines/plugins/filters/filter_noop_test.go index 2fa369a4f..06d80c771 100644 --- a/internal/scheduling/machines/plugins/filters/filter_noop_test.go +++ b/internal/scheduling/machines/plugins/filters/filter_noop_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -64,7 +65,7 @@ func TestNoopFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NoopFilter{} - result, err := filter.Run(slog.Default(), tt.request) + result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/manila/filter_weigher_pipeline_controller.go b/internal/scheduling/manila/filter_weigher_pipeline_controller.go index 128b7d719..18ed212f5 100644 --- a/internal/scheduling/manila/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/manila/filter_weigher_pipeline_controller.go @@ -121,7 +121,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision return err } - result, err := pipeline.Run(request) + result, err := pipeline.Run(request, lib.Options{}) if err != nil { log.Error(err, "failed to run pipeline") return err diff --git a/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing.go b/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing.go index ce3e30ebe..01d31a55a 100644 --- a/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing.go +++ b/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing.go @@ -61,7 +61,7 @@ func (s *NetappCPUUsageBalancingStep) Init(ctx context.Context, client client.Cl } // Downvote hosts that are highly contended. -func (s *NetappCPUUsageBalancingStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *NetappCPUUsageBalancingStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["avg cpu contention"] = s.PrepareStats(request, "%") result.Statistics["max cpu contention"] = s.PrepareStats(request, "%") diff --git a/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing_test.go b/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing_test.go index f3e9c66ea..eeb0cdea2 100644 --- a/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing_test.go +++ b/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -164,7 +165,7 @@ func TestNetappCPUUsageBalancingStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller.go b/internal/scheduling/nova/filter_weigher_pipeline_controller.go index 279ac1c3e..0c8b97e02 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "sync" "time" @@ -77,10 +76,6 @@ func (c *FilterWeigherPipelineController) ProcessNewDecisionFromAPI(ctx context. c.processMu.Lock() defer c.processMu.Unlock() - pipelineConf, ok := c.PipelineConfigs[decision.Spec.PipelineRef.Name] - if !ok { - return fmt.Errorf("pipeline %s not configured", decision.Spec.PipelineRef.Name) - } err := c.process(ctx, decision) if err != nil { meta.SetStatusCondition(&decision.Status.Conditions, metav1.Condition{ @@ -97,9 +92,6 @@ func (c *FilterWeigherPipelineController) ProcessNewDecisionFromAPI(ctx context. Message: "pipeline run succeeded", }) } - if pipelineConf.Spec.CreateHistory { - c.upsertHistory(ctx, decision, err) - } return err } @@ -166,7 +158,11 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision log.Info("gathered all placement candidates", "numHosts", len(request.Hosts)) } - result, err := pipeline.Run(request) + opts := c.buildOptions(pipelineConf) + result, err := pipeline.Run(request, opts) + if opts.RecordHistory { + c.upsertHistory(ctx, decision, err) + } if err != nil { log.Error(err, "failed to run pipeline") return err @@ -183,6 +179,12 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision } // The base controller will delegate the pipeline creation down to this method. +func (c *FilterWeigherPipelineController) buildOptions(pipelineConf v1alpha1.Pipeline) lib.Options { + return lib.Options{ + RecordHistory: pipelineConf.Spec.CreateHistory, + } +} + func (c *FilterWeigherPipelineController) InitPipeline( ctx context.Context, p v1alpha1.Pipeline, diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go index 752725df8..ed15145d4 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go @@ -528,7 +528,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) expectResult: false, expectHistoryCreated: false, expectUpdatedStatus: false, - errorContains: "pipeline nonexistent-pipeline not configured", + errorContains: "pipeline not found or not ready", }, { name: "decision without novaRaw spec", @@ -573,7 +573,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) createHistory: true, expectError: true, expectResult: false, - expectHistoryCreated: true, + expectHistoryCreated: false, expectUpdatedStatus: false, errorContains: "no novaRaw spec defined", }, @@ -611,7 +611,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) createHistory: true, expectError: true, expectResult: false, - expectHistoryCreated: true, + expectHistoryCreated: false, expectUpdatedStatus: false, errorContains: "pipeline not found or not ready", }, @@ -649,7 +649,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) createHistory: true, expectError: true, expectResult: false, - expectHistoryCreated: true, + expectHistoryCreated: false, expectUpdatedStatus: false, errorContains: "pipeline not found or not ready", }, diff --git a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata.go b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata.go index 157a80521..82a50ce1c 100644 --- a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata.go +++ b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata.go @@ -19,7 +19,7 @@ type FilterAggregateMetadata struct { // Restrict hosts to specific projects if they are in an aggregate that has // the "filter_tenant_id" metadata key set. -func (s *FilterAggregateMetadata) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterAggregateMetadata) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) hvs := &hv1.HypervisorList{} diff --git a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go index d1ff9cd2d..f7093a003 100644 --- a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "context" "log/slog" "testing" @@ -336,7 +337,7 @@ func TestFilterAggregateMetadata_Run(t *testing.T) { step := &FilterAggregateMetadata{} step.Client = fakeClient - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -388,7 +389,7 @@ func TestFilterAggregateMetadata_Run_ClientError(t *testing.T) { step := &FilterAggregateMetadata{} step.Client = fakeClient - _, err := step.Run(slog.Default(), request) + _, err := step.Run(slog.Default(), request, lib.Options{}) if err == nil { t.Errorf("expected error when client fails, got none") } diff --git a/internal/scheduling/nova/plugins/filters/filter_allowed_projects.go b/internal/scheduling/nova/plugins/filters/filter_allowed_projects.go index a0a486f3d..21d6c6dd8 100644 --- a/internal/scheduling/nova/plugins/filters/filter_allowed_projects.go +++ b/internal/scheduling/nova/plugins/filters/filter_allowed_projects.go @@ -19,7 +19,7 @@ type FilterAllowedProjectsStep struct { // Lock certain hosts for certain projects, based on the hypervisor spec. // Note that hosts without specified projects are still accessible. -func (s *FilterAllowedProjectsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterAllowedProjectsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) if request.Spec.Data.ProjectID == "" { traceLog.Info("no project ID in request, skipping filter") diff --git a/internal/scheduling/nova/plugins/filters/filter_allowed_projects_test.go b/internal/scheduling/nova/plugins/filters/filter_allowed_projects_test.go index 070160e2e..9dc95da59 100644 --- a/internal/scheduling/nova/plugins/filters/filter_allowed_projects_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_allowed_projects_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -295,7 +296,7 @@ func TestFilterAllowedProjectsStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_capabilities.go b/internal/scheduling/nova/plugins/filters/filter_capabilities.go index cda9a9a20..0fd1781ea 100644 --- a/internal/scheduling/nova/plugins/filters/filter_capabilities.go +++ b/internal/scheduling/nova/plugins/filters/filter_capabilities.go @@ -45,7 +45,7 @@ func hvToNovaCapabilities(hv hv1.Hypervisor) (map[string]string, error) { // Check the capabilities of each host and if they match the extra spec provided // in the request spec flavor. -func (s *FilterCapabilitiesStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterCapabilitiesStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) extraSpecs := request.Spec.Data.Flavor.Data.ExtraSpecs if len(extraSpecs) == 0 { diff --git a/internal/scheduling/nova/plugins/filters/filter_capabilities_test.go b/internal/scheduling/nova/plugins/filters/filter_capabilities_test.go index 9b5f111dc..2aa6d2ba7 100644 --- a/internal/scheduling/nova/plugins/filters/filter_capabilities_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_capabilities_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -553,7 +554,7 @@ func TestFilterCapabilitiesStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -628,7 +629,7 @@ func TestFilterCapabilitiesStep_DoesNotMutateExtraSpecs(t *testing.T) { WithObjects(hvs...). Build() - _, err = step.Run(slog.Default(), request) + _, err = step.Run(slog.Default(), request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_correct_az.go b/internal/scheduling/nova/plugins/filters/filter_correct_az.go index ed7f68188..94311bd82 100644 --- a/internal/scheduling/nova/plugins/filters/filter_correct_az.go +++ b/internal/scheduling/nova/plugins/filters/filter_correct_az.go @@ -18,7 +18,7 @@ type FilterCorrectAZStep struct { } // Only get hosts in the requested az. -func (s *FilterCorrectAZStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterCorrectAZStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) if request.Spec.Data.AvailabilityZone == "" { traceLog.Info("no availability zone requested, skipping filter_correct_az step") diff --git a/internal/scheduling/nova/plugins/filters/filter_correct_az_test.go b/internal/scheduling/nova/plugins/filters/filter_correct_az_test.go index d8389de9e..4ac16cfa3 100644 --- a/internal/scheduling/nova/plugins/filters/filter_correct_az_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_correct_az_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -169,7 +170,7 @@ func TestFilterCorrectAZStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_exclude_hosts.go b/internal/scheduling/nova/plugins/filters/filter_exclude_hosts.go index 231efa9aa..1a68602a2 100644 --- a/internal/scheduling/nova/plugins/filters/filter_exclude_hosts.go +++ b/internal/scheduling/nova/plugins/filters/filter_exclude_hosts.go @@ -30,6 +30,7 @@ func (opts FilterExcludeHostsStepOpts) Validate() error { return nil } func (s *FilterExcludeHostsStep) Run( traceLog *slog.Logger, request api.ExternalSchedulerRequest, + opts lib.Options, ) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) diff --git a/internal/scheduling/nova/plugins/filters/filter_exclude_hosts_test.go b/internal/scheduling/nova/plugins/filters/filter_exclude_hosts_test.go index 0c9e35c59..42c0ab200 100644 --- a/internal/scheduling/nova/plugins/filters/filter_exclude_hosts_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_exclude_hosts_test.go @@ -218,7 +218,7 @@ func TestFilterExcludeHostsStep_Run(t *testing.T) { ExcludedHosts: tt.excludedHosts, } - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_external_customer.go b/internal/scheduling/nova/plugins/filters/filter_external_customer.go index 827712a84..b96f76b6b 100644 --- a/internal/scheduling/nova/plugins/filters/filter_external_customer.go +++ b/internal/scheduling/nova/plugins/filters/filter_external_customer.go @@ -33,7 +33,7 @@ type FilterExternalCustomerStep struct { // Prefix-match the domain name for external customer domains and filter out hosts // that are not intended for external customers. -func (s *FilterExternalCustomerStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterExternalCustomerStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) domainName, err := request.Spec.Data.GetSchedulerHintStr("domain_name") if err != nil { diff --git a/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go b/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go index 05bdbc6f6..9a46f31ee 100644 --- a/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -358,7 +359,7 @@ func TestFilterExternalCustomerStep_Run(t *testing.T) { Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if tt.expectError { if err == nil { t.Errorf("expected error but got none") diff --git a/internal/scheduling/nova/plugins/filters/filter_has_accelerators.go b/internal/scheduling/nova/plugins/filters/filter_has_accelerators.go index dcccdc010..2c61ad588 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_accelerators.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_accelerators.go @@ -18,7 +18,7 @@ type FilterHasAcceleratorsStep struct { } // If requested, only get hosts with accelerators. -func (s *FilterHasAcceleratorsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterHasAcceleratorsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) extraSpecs := request.Spec.Data.Flavor.Data.ExtraSpecs if _, ok := extraSpecs["accel:device_profile"]; !ok { diff --git a/internal/scheduling/nova/plugins/filters/filter_has_accelerators_test.go b/internal/scheduling/nova/plugins/filters/filter_has_accelerators_test.go index 1d1a06764..1008ae17f 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_accelerators_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_accelerators_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -347,7 +348,7 @@ func TestFilterHasAcceleratorsStep_Run(t *testing.T) { WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go index 88e2f07d5..811371d4b 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go @@ -57,7 +57,7 @@ type FilterHasEnoughCapacity struct { // known at this point. // // Please also note that disk space is currently not considered by this filter. -func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) // This map holds the free resources per host. diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go index 5b026408f..85141864b 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -583,7 +584,7 @@ func TestFilterHasEnoughCapacity_ReservationTypes(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -798,7 +799,7 @@ func TestFilterHasEnoughCapacity_IgnoredReservationTypes(t *testing.T) { IgnoredReservationTypes: tt.ignoredReservationTypes, } - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -961,7 +962,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -1011,7 +1012,7 @@ func TestFilterHasEnoughCapacity_PlannedCRDoesNotBlock(t *testing.T) { step.Options = FilterHasEnoughCapacityOpts{LockReserved: false} request := newNovaRequest("instance-123", "project-A", "m1.large", "gp-1", 4, "8Gi", false, []string{"host1"}) - result, err := step.Run(slog.Default(), request) + result, err := step.Run(slog.Default(), request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -1084,7 +1085,7 @@ func TestFilterHasEnoughCapacity_NilEffectiveCapacityFallback(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = FilterHasEnoughCapacityOpts{LockReserved: false} - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -1283,7 +1284,7 @@ func TestFilterHasEnoughCapacity_VMInterReservationMigration(t *testing.T) { step.Options = FilterHasEnoughCapacityOpts{LockReserved: false} request := newNovaRequest("instance-new", thirdParty, "m1.small", flavorGroup, 3, "6Gi", false, []string{"hv-a", "hv-b"}) - result, err := step.Run(slog.Default(), request) + result, err := step.Run(slog.Default(), request, lib.Options{}) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_has_requested_traits.go b/internal/scheduling/nova/plugins/filters/filter_has_requested_traits.go index aa35d2fc9..53f050db5 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_requested_traits.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_requested_traits.go @@ -21,7 +21,7 @@ type FilterHasRequestedTraits struct { // Filter hosts that do not have the requested traits given by the extra spec: // - "trait:": "forbidden" means the host must not have the specified trait. // - "trait:": "required" means the host must have the specified trait. -func (s *FilterHasRequestedTraits) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterHasRequestedTraits) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) var requiredTraits, forbiddenTraits []string for key, value := range request.Spec.Data.Flavor.Data.ExtraSpecs { diff --git a/internal/scheduling/nova/plugins/filters/filter_has_requested_traits_test.go b/internal/scheduling/nova/plugins/filters/filter_has_requested_traits_test.go index 10a7c94aa..edd7bb9a8 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_requested_traits_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_requested_traits_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -461,7 +462,7 @@ func TestFilterHasRequestedTraits_Run(t *testing.T) { WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_host_instructions.go b/internal/scheduling/nova/plugins/filters/filter_host_instructions.go index dafb6675f..6e6a13da8 100644 --- a/internal/scheduling/nova/plugins/filters/filter_host_instructions.go +++ b/internal/scheduling/nova/plugins/filters/filter_host_instructions.go @@ -18,7 +18,7 @@ type FilterHostInstructionsStep struct { // Filter hosts based on instructions given in the request spec. Supported are: // - spec.ignore_hosts: Filter out all hosts in this list. // - spec.force_hosts: Include only hosts in this list. -func (s *FilterHostInstructionsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterHostInstructionsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) if request.Spec.Data.IgnoreHosts != nil { for _, host := range *request.Spec.Data.IgnoreHosts { diff --git a/internal/scheduling/nova/plugins/filters/filter_host_instructions_test.go b/internal/scheduling/nova/plugins/filters/filter_host_instructions_test.go index 10bcb60c9..a09e12a2e 100644 --- a/internal/scheduling/nova/plugins/filters/filter_host_instructions_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_host_instructions_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -331,7 +332,7 @@ func TestFilterHostInstructionsStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { step := &FilterHostInstructionsStep{} - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity.go b/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity.go index 326864b9d..41eb5181e 100644 --- a/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity.go +++ b/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity.go @@ -19,6 +19,7 @@ type FilterInstanceGroupAffinityStep struct { func (s *FilterInstanceGroupAffinityStep) Run( traceLog *slog.Logger, request api.ExternalSchedulerRequest, + opts lib.Options, ) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) diff --git a/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity_test.go b/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity_test.go index 7321747e3..25e0c224d 100644 --- a/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -326,7 +327,7 @@ func TestFilterInstanceGroupAffinityStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { step := &FilterInstanceGroupAffinityStep{} - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity.go b/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity.go index 0dee29d9e..17a9a8735 100644 --- a/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity.go +++ b/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity.go @@ -22,6 +22,7 @@ type FilterInstanceGroupAntiAffinityStep struct { func (s *FilterInstanceGroupAntiAffinityStep) Run( traceLog *slog.Logger, request api.ExternalSchedulerRequest, + opts lib.Options, ) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) diff --git a/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity_test.go b/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity_test.go index 6eea6bc7f..e70ada792 100644 --- a/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -519,7 +520,7 @@ func TestFilterInstanceGroupAntiAffinityStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_live_migratable.go b/internal/scheduling/nova/plugins/filters/filter_live_migratable.go index a19238721..32c467fe8 100644 --- a/internal/scheduling/nova/plugins/filters/filter_live_migratable.go +++ b/internal/scheduling/nova/plugins/filters/filter_live_migratable.go @@ -51,6 +51,7 @@ func (s *FilterLiveMigratableStep) checkHasSufficientFeatures( func (s *FilterLiveMigratableStep) Run( traceLog *slog.Logger, request api.ExternalSchedulerRequest, + opts lib.Options, ) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) diff --git a/internal/scheduling/nova/plugins/filters/filter_live_migratable_test.go b/internal/scheduling/nova/plugins/filters/filter_live_migratable_test.go index c5651b025..c4cb9df9c 100644 --- a/internal/scheduling/nova/plugins/filters/filter_live_migratable_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_live_migratable_test.go @@ -641,7 +641,7 @@ func TestFilterLiveMigratableStep_Run(t *testing.T) { }, } - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if tt.expectErr { if err == nil { @@ -728,7 +728,7 @@ func TestFilterLiveMigratableStep_Run_SourceHostNotFound(t *testing.T) { }, } - _, err := step.Run(slog.Default(), request) + _, err := step.Run(slog.Default(), request, lib.Options{}) if err == nil { t.Errorf("expected error when source host not found, got none") } @@ -774,7 +774,7 @@ func TestFilterLiveMigratableStep_Run_ClientError(t *testing.T) { }, } - _, err := step.Run(slog.Default(), request) + _, err := step.Run(slog.Default(), request, lib.Options{}) if err == nil { t.Errorf("expected error when client fails, got none") } diff --git a/internal/scheduling/nova/plugins/filters/filter_requested_destination.go b/internal/scheduling/nova/plugins/filters/filter_requested_destination.go index 8922ab8c4..83c0d5521 100644 --- a/internal/scheduling/nova/plugins/filters/filter_requested_destination.go +++ b/internal/scheduling/nova/plugins/filters/filter_requested_destination.go @@ -100,7 +100,7 @@ func (s *FilterRequestedDestinationStep) processRequestedHost( // The requested destination can include a specific host, aggregates, or both. // When both are specified, aggregate filtering is applied first, followed by // host filtering. -func (s *FilterRequestedDestinationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterRequestedDestinationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) rd := request.Spec.Data.RequestedDestination if rd == nil { diff --git a/internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go b/internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go index 5a752160e..e85d38a42 100644 --- a/internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go @@ -578,7 +578,7 @@ func TestFilterRequestedDestinationStep_Run(t *testing.T) { }, } - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if tt.expectErr { if err == nil { @@ -777,7 +777,7 @@ func TestFilterRequestedDestinationStep_Run_ClientError(t *testing.T) { }, } - _, err := step.Run(slog.Default(), request) + _, err := step.Run(slog.Default(), request, lib.Options{}) if err == nil { t.Errorf("expected error when client fails, got none") } diff --git a/internal/scheduling/nova/plugins/filters/filter_status_conditions.go b/internal/scheduling/nova/plugins/filters/filter_status_conditions.go index 3d7f2aae6..05bbbdcc4 100644 --- a/internal/scheduling/nova/plugins/filters/filter_status_conditions.go +++ b/internal/scheduling/nova/plugins/filters/filter_status_conditions.go @@ -20,7 +20,7 @@ type FilterStatusConditionsStep struct { // Check that all status conditions meet the expected values, for example, // that the hypervisor is ready and not disabled. -func (s *FilterStatusConditionsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterStatusConditionsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) hvs := &hv1.HypervisorList{} diff --git a/internal/scheduling/nova/plugins/filters/filter_status_conditions_test.go b/internal/scheduling/nova/plugins/filters/filter_status_conditions_test.go index adbfc8c65..91cc67489 100644 --- a/internal/scheduling/nova/plugins/filters/filter_status_conditions_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_status_conditions_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -341,7 +342,7 @@ func TestFilterStatusConditionsStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/kvm_binpack.go b/internal/scheduling/nova/plugins/weighers/kvm_binpack.go index e1509a4cc..717f3e667 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_binpack.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_binpack.go @@ -69,7 +69,7 @@ type KVMBinpackStep struct { } // Run this weigher in the pipeline after filters have been executed. -func (s *KVMBinpackStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMBinpackStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["binpack score"] = s.PrepareStats(request, "float") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go b/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go index 69e1aa9f6..ae7ff0a6d 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" @@ -448,7 +449,7 @@ func TestKVMBinpackStep_Run(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation.go index dcbcbf8bd..8e463ddc9 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation.go @@ -50,7 +50,7 @@ type KVMFailoverEvacuationStep struct { // Run the weigher step. // For evacuation requests, hosts matching a failover reservation where the VM is in Allocations get a higher weight. // For non-evacuation requests (e.g., live migration, rebuild), this weigher has no effect. -func (s *KVMFailoverEvacuationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMFailoverEvacuationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) intent, err := request.GetIntent() diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go index 0664e55d4..eb7925c71 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -264,7 +265,7 @@ func TestKVMFailoverEvacuationStep_Run(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation.go index 727afce33..754db1be4 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation.go @@ -78,7 +78,7 @@ type KVMFailoverReservationConsolidationStep struct { // Run the weigher step. // For reserve_for_failover requests, hosts are scored based on existing failover reservation density // and same-spec diversity. For all other request types, this weigher has no effect. -func (s *KVMFailoverReservationConsolidationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMFailoverReservationConsolidationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) intent, err := request.GetIntent() diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go index 62d69d319..a065d1ef8 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "math" "testing" @@ -256,7 +257,7 @@ func TestKVMFailoverReservationConsolidationStep_Run(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity.go b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity.go index 5f13897f0..95c35fe92 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity.go @@ -26,7 +26,7 @@ type KVMInstanceGroupSoftAffinityStep struct { lib.BaseWeigher[api.ExternalSchedulerRequest, lib.EmptyFilterWeigherPipelineStepOpts] } -func (s *KVMInstanceGroupSoftAffinityStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMInstanceGroupSoftAffinityStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["affinity"] = s.PrepareStats(request, "float") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go index fcf13f86b..b0817b737 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -346,7 +347,7 @@ func TestKVMInstanceGroupSoftAffinityStep_Run(t *testing.T) { step := &KVMInstanceGroupSoftAffinityStep{} step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts.go b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts.go index b65a5f75f..88fbd2ca9 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts.go @@ -57,7 +57,7 @@ type KVMPreferSmallerHostsStep struct { } // Run this weigher in the pipeline after filters have been executed. -func (s *KVMPreferSmallerHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMPreferSmallerHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["small host score"] = s.PrepareStats(request, "float") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go index 2ab2deb89..6f306b414 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" @@ -599,7 +600,7 @@ func TestKVMPreferSmallerHostsStep_Run(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects.go b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects.go index b2b886d49..59a485d07 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects.go @@ -52,7 +52,7 @@ func (s *VMwareAntiAffinityNoisyProjectsStep) Init(ctx context.Context, client c } // Downvote the hosts a project is currently running on if it's noisy. -func (s *VMwareAntiAffinityNoisyProjectsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *VMwareAntiAffinityNoisyProjectsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["avg cpu usage of this project"] = s.PrepareStats(request, "%") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go index 304ab0612..6c33ab721 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "context" "log/slog" "strings" @@ -273,7 +274,7 @@ func TestVMwareAntiAffinityNoisyProjectsStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts.go index 14905cf00..8f46dc5d8 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts.go @@ -61,7 +61,7 @@ func (s *VMwareAvoidLongTermContendedHostsStep) Init(ctx context.Context, client } // Downvote hosts that are highly contended. -func (s *VMwareAvoidLongTermContendedHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *VMwareAvoidLongTermContendedHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["avg cpu contention"] = s.PrepareStats(request, "%") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go index 5a69cdaf7..fd0bd99d8 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "context" "log/slog" "strings" @@ -256,7 +257,7 @@ func TestVMwareAvoidLongTermContendedHostsStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts.go index fd9e81335..f2d7896f0 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts.go @@ -61,7 +61,7 @@ func (s *VMwareAvoidShortTermContendedHostsStep) Init(ctx context.Context, clien } // Downvote hosts that are highly contended. -func (s *VMwareAvoidShortTermContendedHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *VMwareAvoidShortTermContendedHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["avg cpu contention"] = s.PrepareStats(request, "%") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go index 0dfe280d0..8c0f8a941 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "context" "log/slog" "strings" @@ -256,7 +257,7 @@ func TestVMwareAvoidShortTermContendedHostsStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_binpack.go b/internal/scheduling/nova/plugins/weighers/vmware_binpack.go index 217dc7d7f..adb6f86bc 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_binpack.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_binpack.go @@ -85,7 +85,7 @@ func (s *VMwareBinpackStep) Init(ctx context.Context, client client.Client, weig } // Run this weigher in the pipeline after filters have been executed. -func (s *VMwareBinpackStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *VMwareBinpackStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["binpack score"] = s.PrepareStats(request, "float") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go b/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go index cdca6c569..d274ef40d 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -211,7 +212,7 @@ func TestVMwareBinpackStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request) + result, err := step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/pods/filter_weigher_pipeline_controller.go b/internal/scheduling/pods/filter_weigher_pipeline_controller.go index 0ceee6485..7f76adede 100644 --- a/internal/scheduling/pods/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/pods/filter_weigher_pipeline_controller.go @@ -158,7 +158,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision // Execute the scheduling pipeline. request := pods.PodPipelineRequest{Nodes: nodes.Items, Pod: *pod} - result, err := pipeline.Run(request) + result, err := pipeline.Run(request, lib.Options{}) if err != nil { log.V(1).Error(err, "failed to run scheduler pipeline") return errors.New("failed to run scheduler pipeline") diff --git a/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go b/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go index 143ed9f83..f1e429da3 100644 --- a/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go @@ -492,7 +492,7 @@ func createMockPodPipeline() lib.FilterWeigherPipeline[pods.PodPipelineRequest] type mockPodPipeline struct{} -func (m *mockPodPipeline) Run(request pods.PodPipelineRequest) (v1alpha1.DecisionResult, error) { +func (m *mockPodPipeline) Run(request pods.PodPipelineRequest, opts lib.Options) (v1alpha1.DecisionResult, error) { if len(request.Nodes) == 0 { return v1alpha1.DecisionResult{}, nil } diff --git a/internal/scheduling/pods/plugins/filters/filter_node_affinity.go b/internal/scheduling/pods/plugins/filters/filter_node_affinity.go index 996897bdb..c8953ec1b 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_affinity.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_affinity.go @@ -27,7 +27,7 @@ func (f *NodeAffinityFilter) Validate(ctx context.Context, params v1alpha1.Param return nil } -func (NodeAffinityFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (NodeAffinityFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_affinity_test.go b/internal/scheduling/pods/plugins/filters/filter_node_affinity_test.go index 0172d60d7..1442994c2 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_affinity_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_affinity_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -399,7 +400,7 @@ func TestNodeAffinityFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NodeAffinityFilter{} - result, err := filter.Run(slog.Default(), tt.request) + result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_available.go b/internal/scheduling/pods/plugins/filters/filter_node_available.go index 2d6e11d22..cfbccfa28 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_available.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_available.go @@ -26,7 +26,7 @@ func (f *NodeAvailableFilter) Validate(ctx context.Context, params v1alpha1.Para return nil } -func (NodeAvailableFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (NodeAvailableFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_available_test.go b/internal/scheduling/pods/plugins/filters/filter_node_available_test.go index 3eac7873c..e9966d594 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_available_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_available_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -309,7 +310,7 @@ func TestNodeAvailableFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NodeAvailableFilter{} - result, err := filter.Run(slog.Default(), tt.request) + result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_capacity.go b/internal/scheduling/pods/plugins/filters/filter_node_capacity.go index cfceb8835..6d2f0eae7 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_capacity.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_capacity.go @@ -27,7 +27,7 @@ func (f *NodeCapacityFilter) Validate(ctx context.Context, params v1alpha1.Param return nil } -func (NodeCapacityFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (NodeCapacityFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_capacity_test.go b/internal/scheduling/pods/plugins/filters/filter_node_capacity_test.go index 543b4561d..950adace7 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_capacity_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_capacity_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -351,7 +352,7 @@ func TestNodeCapacityFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NodeCapacityFilter{} - result, err := filter.Run(slog.Default(), tt.request) + result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/filters/filter_noop.go b/internal/scheduling/pods/plugins/filters/filter_noop.go index e0666537b..fda0510f6 100644 --- a/internal/scheduling/pods/plugins/filters/filter_noop.go +++ b/internal/scheduling/pods/plugins/filters/filter_noop.go @@ -31,7 +31,7 @@ func (f *NoopFilter) Validate(ctx context.Context, params v1alpha1.Parameters) e // not in the map are considered as filtered out. // Provide a traceLog that contains the global request id and should // be used to log the step's execution. -func (NoopFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (NoopFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64, len(request.Nodes)) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) // Usually you would do some filtering here, or adjust the weights. diff --git a/internal/scheduling/pods/plugins/filters/filter_noop_test.go b/internal/scheduling/pods/plugins/filters/filter_noop_test.go index dcdd90a69..71e22f72f 100644 --- a/internal/scheduling/pods/plugins/filters/filter_noop_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_noop_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -85,7 +86,7 @@ func TestNoopFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NoopFilter{} - result, err := filter.Run(slog.Default(), tt.request) + result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/filters/filter_taint.go b/internal/scheduling/pods/plugins/filters/filter_taint.go index 5a54d1cb6..f349c5213 100644 --- a/internal/scheduling/pods/plugins/filters/filter_taint.go +++ b/internal/scheduling/pods/plugins/filters/filter_taint.go @@ -26,7 +26,7 @@ func (f *TaintFilter) Validate(ctx context.Context, params v1alpha1.Parameters) return nil } -func (TaintFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (TaintFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) diff --git a/internal/scheduling/pods/plugins/filters/filter_taint_test.go b/internal/scheduling/pods/plugins/filters/filter_taint_test.go index 1d248b685..258f78ef0 100644 --- a/internal/scheduling/pods/plugins/filters/filter_taint_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_taint_test.go @@ -4,6 +4,7 @@ package filters import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -252,7 +253,7 @@ func TestTaintFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &TaintFilter{} - result, err := filter.Run(slog.Default(), tt.request) + result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/weighers/binpack.go b/internal/scheduling/pods/plugins/weighers/binpack.go index 07d310fb8..d0c8e2cf9 100644 --- a/internal/scheduling/pods/plugins/weighers/binpack.go +++ b/internal/scheduling/pods/plugins/weighers/binpack.go @@ -31,7 +31,7 @@ type BinpackingStep struct { lib.BaseWeigher[api.PodPipelineRequest, BinpackingStepOpts] } -func (s *BinpackingStep) Run(traceLog *slog.Logger, request api.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *BinpackingStep) Run(traceLog *slog.Logger, request api.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) podResources := helpers.GetPodResourceRequests(request.Pod) diff --git a/internal/scheduling/pods/plugins/weighers/binpack_test.go b/internal/scheduling/pods/plugins/weighers/binpack_test.go index 198e110c1..82838909f 100644 --- a/internal/scheduling/pods/plugins/weighers/binpack_test.go +++ b/internal/scheduling/pods/plugins/weighers/binpack_test.go @@ -4,6 +4,7 @@ package weighers import ( + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "math" "testing" @@ -257,7 +258,7 @@ func TestBinpackingStep_Run(t *testing.T) { }, } - result, err := tt.step.Run(slog.Default(), tt.request) + result, err := tt.step.Run(slog.Default(), tt.request, lib.Options{}) if err != nil { t.Fatalf("expected no error, got %v", err) } From ad6611254d711318b4516f37e1e03ff3895a65ae Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 5 May 2026 10:30:46 +0200 Subject: [PATCH 02/19] example usage --- .../nova/filter_weigher_pipeline_controller.go | 14 +++++++++++--- .../filters/filter_aggregate_metadata_test.go | 2 +- .../plugins/filters/filter_has_enough_capacity.go | 14 +++++--------- .../filters/filter_has_enough_capacity_test.go | 13 +++++++++---- .../vmware_anti_affinity_noisy_projects_test.go | 2 +- .../vmware_avoid_long_term_contended_hosts_test.go | 2 +- ...vmware_avoid_short_term_contended_hosts_test.go | 2 +- .../commitments/reservation_controller.go | 10 ++++++++++ .../scheduling/reservations/scheduler_client.go | 5 +++++ 9 files changed, 44 insertions(+), 20 deletions(-) diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller.go b/internal/scheduling/nova/filter_weigher_pipeline_controller.go index 0c8b97e02..3ebf3216a 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller.go @@ -158,7 +158,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision log.Info("gathered all placement candidates", "numHosts", len(request.Hosts)) } - opts := c.buildOptions(pipelineConf) + opts := c.buildOptions(request, pipelineConf) result, err := pipeline.Run(request, opts) if opts.RecordHistory { c.upsertHistory(ctx, decision, err) @@ -179,10 +179,18 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision } // The base controller will delegate the pipeline creation down to this method. -func (c *FilterWeigherPipelineController) buildOptions(pipelineConf v1alpha1.Pipeline) lib.Options { - return lib.Options{ +func (c *FilterWeigherPipelineController) buildOptions(request api.ExternalSchedulerRequest, pipelineConf v1alpha1.Pipeline) lib.Options { + opts := lib.Options{ RecordHistory: pipelineConf.Spec.CreateHistory, } + intent, err := request.GetIntent() + if err == nil { + switch intent { + case api.ReserveForCommittedResourceIntent, api.ReserveForFailoverIntent: + opts.LockReservations = true + } + } + return opts } func (c *FilterWeigherPipelineController) InitPipeline( diff --git a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go index f7093a003..45814d50f 100644 --- a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go @@ -4,8 +4,8 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "context" + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go index 811371d4b..2b627e19c 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go @@ -122,18 +122,14 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa // Check if this is a CR reservation scheduling request. // If so, we should NOT unlock any CR reservations to prevent overbooking. // CR capacity should only be unlocked for actual VM scheduling. - intent, err := request.GetIntent() switch { - case err == nil && intent == api.ReserveForCommittedResourceIntent: - traceLog.Debug("keeping CR reservation locked for CR reservation scheduling", + case opts.LockReservations || s.Options.LockReserved: + traceLog.Debug("keeping CR reservation locked", "reservation", reservation.Name, - "intent", intent) + "lockReservations", opts.LockReservations, + "lockReserved", s.Options.LockReserved) // Don't continue - fall through to block the resources - case !s.Options.LockReserved && - // For committed resource reservations: unlock resources only if: - // 1. Project ID matches - // 2. ResourceGroup matches the flavor's hw_version - reservation.Spec.CommittedResourceReservation.ProjectID == request.Spec.Data.ProjectID && + case reservation.Spec.CommittedResourceReservation.ProjectID == request.Spec.Data.ProjectID && reservation.Spec.CommittedResourceReservation.ResourceGroup == request.Spec.Data.Flavor.Data.ExtraSpecs["hw_version"]: traceLog.Info("unlocking resources reserved by matching committed resource reservation with allocation", "reservation", reservation.Name, diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go index 85141864b..56941bbd3 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go @@ -820,6 +820,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) reservations []*v1alpha1.Reservation request api.ExternalSchedulerRequest opts FilterHasEnoughCapacityOpts + pipelineOpts lib.Options expectedHosts []string filteredHosts []string }{ @@ -835,8 +836,9 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) }, // Request with reserve_for_committed_resource intent (scheduling a new CR reservation) request: newNovaRequestWithIntent("new-reservation-uuid", "project-A", "m1.large", "gp-1", 4, "8Gi", "reserve_for_committed_resource", false, []string{"host1", "host2"}), - opts: FilterHasEnoughCapacityOpts{LockReserved: false}, // Note: LockReserved is false, but intent overrides - expectedHosts: []string{"host2"}, // host1 blocked because existing-cr stays locked + opts: FilterHasEnoughCapacityOpts{LockReserved: false}, + pipelineOpts: lib.Options{LockReservations: true}, + expectedHosts: []string{"host2"}, // host1 blocked because existing-cr stays locked filteredHosts: []string{"host1"}, }, { @@ -868,6 +870,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) // Request with reserve_for_committed_resource intent request: newNovaRequestWithIntent("new-reservation-uuid", "project-A", "m1.large", "gp-1", 4, "8Gi", "reserve_for_committed_resource", false, []string{"host1", "host2"}), opts: FilterHasEnoughCapacityOpts{LockReserved: false}, + pipelineOpts: lib.Options{LockReservations: true}, expectedHosts: []string{"host2"}, filteredHosts: []string{"host1"}, // host1 blocked by other project's reservation (would be blocked anyway) }, @@ -886,6 +889,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) // After blocking all 3 reservations (24 CPU), only 8 CPU free -> should fail request: newNovaRequestWithIntent("new-reservation-uuid", "project-A", "m1.large", "gp-1", 10, "20Gi", "reserve_for_committed_resource", false, []string{"host1"}), opts: FilterHasEnoughCapacityOpts{LockReserved: false}, + pipelineOpts: lib.Options{LockReservations: true}, expectedHosts: []string{}, filteredHosts: []string{"host1"}, // All reservations stay locked, not enough capacity }, @@ -917,13 +921,14 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) newCommittedReservation("existing-cr", "host1", "project-A", "m1.large", "gp-1", "8", "16Gi", nil, nil), }, // Request with reserve_for_committed_resource intent - // IgnoredReservationTypes is a safety flag that overrides everything, including intent + // IgnoredReservationTypes is a safety flag that overrides everything, including LockReservations request: newNovaRequestWithIntent("new-reservation-uuid", "project-A", "m1.large", "gp-1", 4, "8Gi", "reserve_for_committed_resource", false, []string{"host1"}), opts: FilterHasEnoughCapacityOpts{ LockReserved: false, // IgnoredReservationTypes is a safety override - ignores CR even for CR scheduling IgnoredReservationTypes: []v1alpha1.ReservationType{v1alpha1.ReservationTypeCommittedResource}, }, + pipelineOpts: lib.Options{LockReservations: true}, expectedHosts: []string{"host1"}, // CR reservation is ignored via IgnoredReservationTypes (safety override) filteredHosts: []string{}, }, @@ -962,7 +967,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request, tt.pipelineOpts) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go index 6c33ab721..887982049 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go @@ -4,8 +4,8 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "context" + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go index fd0bd99d8..e3a62423c 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go @@ -4,8 +4,8 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "context" + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go index 8c0f8a941..9de020c95 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go @@ -4,8 +4,8 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "context" + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" diff --git a/internal/scheduling/reservations/commitments/reservation_controller.go b/internal/scheduling/reservations/commitments/reservation_controller.go index b65842c60..09cca220d 100644 --- a/internal/scheduling/reservations/commitments/reservation_controller.go +++ b/internal/scheduling/reservations/commitments/reservation_controller.go @@ -24,6 +24,7 @@ import ( schedulerdelegationapi "github.com/cobaltcore-dev/cortex/api/external/nova" "github.com/cobaltcore-dev/cortex/api/v1alpha1" + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations" "github.com/cobaltcore-dev/cortex/pkg/multicluster" hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" @@ -285,6 +286,15 @@ func (r *CommitmentReservationController) Reconcile(ctx context.Context, req ctr SchedulerHints: map[string]any{ "_nova_check_type": string(schedulerdelegationapi.ReserveForCommittedResourceIntent), }, + Options: lib.Options{ + ReadOnly: false, // mutates state (reservation placement) + LockReservations: true, // don't unlock CR reservations; finding a slot, not placing a VM + AssumeEmptyHosts: false, + IgnoredReservationTypes: nil, + MaxCandidates: 1, + RecordHistory: false, + CreateInflight: false, + }, } scheduleResp, err := r.SchedulerClient.ScheduleReservation(ctx, scheduleReq) diff --git a/internal/scheduling/reservations/scheduler_client.go b/internal/scheduling/reservations/scheduler_client.go index a42172ce2..e250d170d 100644 --- a/internal/scheduling/reservations/scheduler_client.go +++ b/internal/scheduling/reservations/scheduler_client.go @@ -12,6 +12,7 @@ import ( "time" api "github.com/cobaltcore-dev/cortex/api/external/nova" + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "github.com/go-logr/logr" logf "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -78,6 +79,10 @@ type ScheduleReservationRequest struct { // SchedulerHints are hints passed to the scheduler pipeline. // Used to set _nova_check_type for evacuation intent detection. SchedulerHints map[string]any + // Options configures the pipeline behavior for this scheduling call. + // These are derived from intent in buildOptions for the current HTTP path; + // will be passed directly once the scheduler client is a direct Go call. + Options lib.Options } // ScheduleReservationResponse contains the result of scheduling a reservation. From 4a7bc9e16918efd6dbd9e031acb5d774928753dd Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 5 May 2026 15:36:45 +0200 Subject: [PATCH 03/19] adding: MaxCandidates, IgnoredReservationTypes, ReadOnly --- .../scheduling/lib/filter_weigher_pipeline.go | 10 +++ .../lib/filter_weigher_pipeline_test.go | 56 ++++++++++++++ .../filter_weigher_pipeline_controller.go | 47 ++++++++++-- ...filter_weigher_pipeline_controller_test.go | 74 +++++++++++++++++++ .../filters/filter_has_enough_capacity.go | 3 +- .../filter_has_enough_capacity_test.go | 37 ++++++++++ 6 files changed, 219 insertions(+), 8 deletions(-) diff --git a/internal/scheduling/lib/filter_weigher_pipeline.go b/internal/scheduling/lib/filter_weigher_pipeline.go index 871aa771f..232c64313 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline.go +++ b/internal/scheduling/lib/filter_weigher_pipeline.go @@ -299,6 +299,16 @@ func (p *filterWeigherPipeline[RequestType]) Run(request RequestType, opts Optio hosts := p.sortHostsByWeights(outWeights) traceLog.Info("scheduler: sorted hosts", "hosts", hosts) + if opts.MaxCandidates > 0 && len(hosts) > opts.MaxCandidates { + hosts = hosts[:opts.MaxCandidates] + // Drop trimmed hosts from outWeights so AggregatedOutWeights stays consistent. + for host := range outWeights { + if !slices.Contains(hosts, host) { + delete(outWeights, host) + } + } + } + // Collect some metrics about the pipeline execution. go p.monitor.observePipelineResult(request, hosts) diff --git a/internal/scheduling/lib/filter_weigher_pipeline_test.go b/internal/scheduling/lib/filter_weigher_pipeline_test.go index 9636084f3..9b89a2592 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_test.go @@ -372,3 +372,59 @@ func TestFilterWeigherPipelineMonitor_SubPipeline(t *testing.T) { t.Error("original monitor should not be modified") } } + +func TestPipeline_MaxCandidates(t *testing.T) { + // Pipeline that passes all 4 hosts with descending weights. + pipeline := &filterWeigherPipeline[mockFilterWeigherPipelineRequest]{ + filters: map[string]Filter[mockFilterWeigherPipelineRequest]{}, + filtersOrder: []string{}, + weighersOrder: []string{}, + weighers: map[string]Weigher[mockFilterWeigherPipelineRequest]{}, + } + request := mockFilterWeigherPipelineRequest{ + Hosts: []string{"host1", "host2", "host3", "host4"}, + Weights: map[string]float64{"host1": 4.0, "host2": 3.0, "host3": 2.0, "host4": 1.0}, + } + + tests := []struct { + name string + maxCandidates int + wantLen int + wantFirst string + }{ + {"no limit", 0, 4, "host1"}, + {"limit to 2", 2, 2, "host1"}, + {"limit to 1", 1, 1, "host1"}, + {"limit larger than hosts", 10, 4, "host1"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := pipeline.Run(request, Options{MaxCandidates: tt.maxCandidates}) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(result.OrderedHosts) != tt.wantLen { + t.Errorf("expected %d hosts, got %d: %v", tt.wantLen, len(result.OrderedHosts), result.OrderedHosts) + } + if len(result.OrderedHosts) > 0 && result.OrderedHosts[0] != tt.wantFirst { + t.Errorf("expected first host %s, got %s", tt.wantFirst, result.OrderedHosts[0]) + } + if tt.maxCandidates > 0 && len(result.OrderedHosts) <= tt.maxCandidates { + // AggregatedOutWeights must only contain returned hosts. + for host := range result.AggregatedOutWeights { + found := false + for _, h := range result.OrderedHosts { + if h == host { + found = true + break + } + } + if !found { + t.Errorf("AggregatedOutWeights contains trimmed host %s", host) + } + } + } + }) + } +} diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller.go b/internal/scheduling/nova/filter_weigher_pipeline_controller.go index 3ebf3216a..53d4555f1 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller.go @@ -37,8 +37,9 @@ type FilterWeigherPipelineController struct { // Toolbox shared between all pipeline controllers. lib.BasePipelineController[lib.FilterWeigherPipeline[api.ExternalSchedulerRequest]] - // Mutex to only allow one process at a time - processMu sync.Mutex + // Mutex to only allow one process at a time. + // Read-only runs (opts.ReadOnly == true) acquire a read lock; write runs acquire the full lock. + processMu sync.RWMutex // Monitor to pass down to all pipelines. Monitor lib.FilterWeigherPipelineMonitor @@ -53,13 +54,23 @@ func (c *FilterWeigherPipelineController) PipelineType() v1alpha1.PipelineType { // Callback executed when kubernetes asks to reconcile a decision resource. func (c *FilterWeigherPipelineController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - c.processMu.Lock() - defer c.processMu.Unlock() - + // Peek at the decision before acquiring the lock so we can choose the right lock type. + // Read-only runs can proceed concurrently; write runs need the exclusive lock. decision := &v1alpha1.Decision{} if err := c.Get(ctx, req.NamespacedName, decision); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } + if c.peekReadOnly(decision) { + c.processMu.RLock() + defer c.processMu.RUnlock() + } else { + c.processMu.Lock() + defer c.processMu.Unlock() + // Re-fetch after acquiring the exclusive lock to see consistent state. + if err := c.Get(ctx, req.NamespacedName, decision); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + } old := decision.DeepCopy() if err := c.process(ctx, decision); err != nil { return ctrl.Result{}, err @@ -73,8 +84,13 @@ func (c *FilterWeigherPipelineController) Reconcile(ctx context.Context, req ctr // Process the decision from the API. Should create and return the updated decision. func (c *FilterWeigherPipelineController) ProcessNewDecisionFromAPI(ctx context.Context, decision *v1alpha1.Decision) error { - c.processMu.Lock() - defer c.processMu.Unlock() + if c.peekReadOnly(decision) { + c.processMu.RLock() + defer c.processMu.RUnlock() + } else { + c.processMu.Lock() + defer c.processMu.Unlock() + } err := c.process(ctx, decision) if err != nil { @@ -178,6 +194,23 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision return nil } +// peekReadOnly determines whether a decision should use a read lock instead of +// the exclusive write lock. Defaults to false (exclusive) on any parse error. +func (c *FilterWeigherPipelineController) peekReadOnly(decision *v1alpha1.Decision) bool { + if decision.Spec.NovaRaw == nil { + return false + } + var request api.ExternalSchedulerRequest + if err := json.Unmarshal(decision.Spec.NovaRaw.Raw, &request); err != nil { + return false + } + pipelineConf, ok := c.PipelineConfigs[decision.Spec.PipelineRef.Name] + if !ok { + return false + } + return c.buildOptions(request, pipelineConf).ReadOnly +} + // The base controller will delegate the pipeline creation down to this method. func (c *FilterWeigherPipelineController) buildOptions(request api.ExternalSchedulerRequest, pipelineConf v1alpha1.Pipeline) lib.Options { opts := lib.Options{ diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go index ed15145d4..781e259bd 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go @@ -928,3 +928,77 @@ func TestFilterWeigherPipelineController_IgnorePreselection(t *testing.T) { // Error variable for testing var errGathererFailed = errors.New("gatherer failed") + +func TestFilterWeigherPipelineController_PeekReadOnly(t *testing.T) { + validRequest := api.ExternalSchedulerRequest{ + Spec: api.NovaObject[api.NovaSpec]{ + Data: api.NovaSpec{NumInstances: 1}, + }, + } + validRaw, err := json.Marshal(validRequest) + if err != nil { + t.Fatalf("failed to marshal test request: %v", err) + } + + c := &FilterWeigherPipelineController{} + c.PipelineConfigs = map[string]v1alpha1.Pipeline{ + "test-pipeline": { + Spec: v1alpha1.PipelineSpec{CreateHistory: false}, + }, + } + + tests := []struct { + name string + decision *v1alpha1.Decision + want bool + }{ + { + name: "nil NovaRaw defaults to exclusive lock", + decision: &v1alpha1.Decision{ + Spec: v1alpha1.DecisionSpec{ + PipelineRef: corev1.ObjectReference{Name: "test-pipeline"}, + }, + }, + want: false, + }, + { + name: "invalid JSON defaults to exclusive lock", + decision: &v1alpha1.Decision{ + Spec: v1alpha1.DecisionSpec{ + PipelineRef: corev1.ObjectReference{Name: "test-pipeline"}, + NovaRaw: &runtime.RawExtension{Raw: []byte("not-json")}, + }, + }, + want: false, + }, + { + name: "unknown pipeline defaults to exclusive lock", + decision: &v1alpha1.Decision{ + Spec: v1alpha1.DecisionSpec{ + PipelineRef: corev1.ObjectReference{Name: "unknown-pipeline"}, + NovaRaw: &runtime.RawExtension{Raw: validRaw}, + }, + }, + want: false, + }, + { + name: "valid request with non-ReadOnly intent uses exclusive lock", + decision: &v1alpha1.Decision{ + Spec: v1alpha1.DecisionSpec{ + PipelineRef: corev1.ObjectReference{Name: "test-pipeline"}, + NovaRaw: &runtime.RawExtension{Raw: validRaw}, + }, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := c.peekReadOnly(tt.decision) + if got != tt.want { + t.Errorf("expected peekReadOnly = %v, got %v", tt.want, got) + } + }) + } +} diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go index 2b627e19c..343554d82 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go @@ -106,7 +106,8 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa } // Check if this reservation type should be ignored - if slices.Contains(s.Options.IgnoredReservationTypes, reservation.Spec.Type) { + if slices.Contains(s.Options.IgnoredReservationTypes, reservation.Spec.Type) || + slices.Contains(opts.IgnoredReservationTypes, reservation.Spec.Type) { traceLog.Debug("ignoring reservation type", "type", reservation.Spec.Type, "reservation", reservation.Name) continue } diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go index 56941bbd3..fbbd21587 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go @@ -808,6 +808,43 @@ func TestFilterHasEnoughCapacity_IgnoredReservationTypes(t *testing.T) { } } +func TestFilterHasEnoughCapacity_IgnoredReservationTypes_CallTime(t *testing.T) { + scheme := buildTestScheme(t) + + // Same two-host setup as the YAML-path test: CR on host1, Failover on host2. + // Each blocks 4 CPU, leaving 4 free; request needs 8 CPU so both hosts fail without ignoring. + hypervisors := []*hv1.Hypervisor{ + newHypervisor("host1", "16", "8", "32Gi", "16Gi"), + newHypervisor("host2", "16", "8", "32Gi", "16Gi"), + } + reservations := []*v1alpha1.Reservation{ + newCommittedReservation("cr-res", "host1", "project-X", "m1.large", "gp-1", "4", "8Gi", nil, nil), + newFailoverReservation("failover-res", "host2", "4", "8Gi", map[string]string{"other-vm": "host3"}), + } + request := newNovaRequest("instance-123", "project-A", "m1.large", "gp-1", 8, "16Gi", false, []string{"host1", "host2"}) + + objects := make([]client.Object, 0, len(hypervisors)+len(reservations)) + for _, h := range hypervisors { + objects = append(objects, h.DeepCopy()) + } + for _, r := range reservations { + objects = append(objects, r.DeepCopy()) + } + + step := &FilterHasEnoughCapacity{} + step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() + step.Options = FilterHasEnoughCapacityOpts{LockReserved: true} // no YAML-level ignores + + // Call-time: ignore CR reservations → host1 passes, host2 still blocked by failover. + result, err := step.Run(slog.Default(), request, lib.Options{ + IgnoredReservationTypes: []v1alpha1.ReservationType{v1alpha1.ReservationTypeCommittedResource}, + }) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + assertActivations(t, result.Activations, []string{"host1"}, []string{"host2"}) +} + func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) { scheme := buildTestScheme(t) From d298e75c81337d2e4791846d0fea03d324ed314c Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 5 May 2026 15:41:06 +0200 Subject: [PATCH 04/19] validate options --- .../scheduling/lib/filter_weigher_pipeline.go | 3 +++ internal/scheduling/lib/options.go | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/scheduling/lib/filter_weigher_pipeline.go b/internal/scheduling/lib/filter_weigher_pipeline.go index 232c64313..a67fa9b96 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline.go +++ b/internal/scheduling/lib/filter_weigher_pipeline.go @@ -265,6 +265,9 @@ func (s *filterWeigherPipeline[RequestType]) sortHostsByWeights(weights map[stri // Evaluate the pipeline and return a list of hosts in order of preference. func (p *filterWeigherPipeline[RequestType]) Run(request RequestType, opts Options) (v1alpha1.DecisionResult, error) { + if err := opts.Validate(); err != nil { + return v1alpha1.DecisionResult{}, err + } slogArgs := request.GetTraceLogArgs() slogArgsAny := make([]any, 0, len(slogArgs)) for _, arg := range slogArgs { diff --git a/internal/scheduling/lib/options.go b/internal/scheduling/lib/options.go index 28819415d..c516957f0 100644 --- a/internal/scheduling/lib/options.go +++ b/internal/scheduling/lib/options.go @@ -3,7 +3,11 @@ package lib -import "github.com/cobaltcore-dev/cortex/api/v1alpha1" +import ( + "errors" + + "github.com/cobaltcore-dev/cortex/api/v1alpha1" +) // Options configure the behavior of a single pipeline run at call time. // These are distinct from per-step YAML options (FilterWeigherPipelineStepOpts), @@ -30,3 +34,14 @@ type Options struct { // CreateInflight creates pessimistic blocking reservations for all returned candidates. CreateInflight bool } + +// Validate checks for mutually exclusive or inconsistent option combinations. +func (o Options) Validate() error { + if o.ReadOnly && o.RecordHistory { + return errors.New("ReadOnly and RecordHistory are mutually exclusive: read-only runs must not mutate state") + } + if o.ReadOnly && o.CreateInflight { + return errors.New("ReadOnly and CreateInflight are mutually exclusive: read-only runs must not mutate state") + } + return nil +} From 54639d144a11bae08e7b7cab097e8bb2a54b3880 Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 5 May 2026 16:52:53 +0200 Subject: [PATCH 05/19] refactor --- api/external/cinder/messages.go | 3 ++ api/external/ironcore/messages.go | 3 ++ api/external/manila/messages.go | 3 ++ api/external/nova/messages.go | 6 ++++ api/external/pods/messages.go | 3 ++ .../filter_weigher_pipeline_controller.go | 2 +- .../scheduling/lib/filter_weigher_pipeline.go | 8 +++-- .../lib/filter_weigher_pipeline_request.go | 2 ++ .../filter_weigher_pipeline_request_test.go | 2 ++ .../lib/filter_weigher_pipeline_test.go | 6 ++-- internal/scheduling/lib/options_test.go | 34 ++++++++++++++++++ .../filter_weigher_pipeline_controller.go | 2 +- ...filter_weigher_pipeline_controller_test.go | 2 +- .../filter_weigher_pipeline_controller.go | 2 +- .../filter_weigher_pipeline_controller.go | 30 ++++------------ ...filter_weigher_pipeline_controller_test.go | 35 +++++++++---------- .../filter_weigher_pipeline_controller.go | 2 +- ...filter_weigher_pipeline_controller_test.go | 2 +- .../commitments/reservation_controller.go | 20 +++++------ .../failover/reservation_scheduling.go | 5 +-- .../reservations/scheduler_client.go | 7 ++-- 21 files changed, 109 insertions(+), 70 deletions(-) create mode 100644 internal/scheduling/lib/options_test.go diff --git a/api/external/cinder/messages.go b/api/external/cinder/messages.go index 260e93815..08fe6edee 100644 --- a/api/external/cinder/messages.go +++ b/api/external/cinder/messages.go @@ -30,8 +30,11 @@ type ExternalSchedulerRequest struct { Weights map[string]float64 `json:"weights"` // The name of the pipeline to execute. Pipeline string `json:"pipeline"` + // Options configure the pipeline behavior for this scheduling call. + Options lib.Options `json:"options,omitempty"` } +func (r ExternalSchedulerRequest) GetOptions() lib.Options { return r.Options } func (r ExternalSchedulerRequest) GetHosts() []string { hosts := make([]string, len(r.Hosts)) for i, host := range r.Hosts { diff --git a/api/external/ironcore/messages.go b/api/external/ironcore/messages.go index ac517f61a..29da83b9b 100644 --- a/api/external/ironcore/messages.go +++ b/api/external/ironcore/messages.go @@ -13,8 +13,11 @@ import ( type MachinePipelineRequest struct { // The available machine pools. Pools []ironcorev1alpha1.MachinePool `json:"pools"` + // Options configure the pipeline behavior for this scheduling call. + Options lib.Options } +func (r MachinePipelineRequest) GetOptions() lib.Options { return r.Options } func (r MachinePipelineRequest) GetHosts() []string { hosts := make([]string, len(r.Pools)) for i, host := range r.Pools { diff --git a/api/external/manila/messages.go b/api/external/manila/messages.go index 5255a0d4f..013fa70fb 100644 --- a/api/external/manila/messages.go +++ b/api/external/manila/messages.go @@ -30,8 +30,11 @@ type ExternalSchedulerRequest struct { Weights map[string]float64 `json:"weights"` // The name of the pipeline to execute. Pipeline string `json:"pipeline"` + // Options configure the pipeline behavior for this scheduling call. + Options lib.Options `json:"options,omitempty"` } +func (r ExternalSchedulerRequest) GetOptions() lib.Options { return r.Options } func (r ExternalSchedulerRequest) GetHosts() []string { hosts := make([]string, len(r.Hosts)) for i, host := range r.Hosts { diff --git a/api/external/nova/messages.go b/api/external/nova/messages.go index e82568941..202f85cf1 100644 --- a/api/external/nova/messages.go +++ b/api/external/nova/messages.go @@ -37,8 +37,14 @@ type ExternalSchedulerRequest struct { // The name of the pipeline to execute. Pipeline string `json:"pipeline"` + + // Options configure the pipeline behavior for this scheduling call. + // Set by the caller (CR controller, failover controller, Nova). + // Nova does not set these; Cortex fills in config-derived defaults server-side. + Options lib.Options `json:"options,omitempty"` } +func (r ExternalSchedulerRequest) GetOptions() lib.Options { return r.Options } func (r ExternalSchedulerRequest) GetHosts() []string { hosts := make([]string, len(r.Hosts)) for i, host := range r.Hosts { diff --git a/api/external/pods/messages.go b/api/external/pods/messages.go index 3ec329b39..0a801ac22 100644 --- a/api/external/pods/messages.go +++ b/api/external/pods/messages.go @@ -15,8 +15,11 @@ type PodPipelineRequest struct { Nodes []corev1.Node `json:"nodes"` // The pod to be scheduled. Pod corev1.Pod `json:"pod"` + // Options configure the pipeline behavior for this scheduling call. + Options lib.Options } +func (r PodPipelineRequest) GetOptions() lib.Options { return r.Options } func (r PodPipelineRequest) GetHosts() []string { hosts := make([]string, len(r.Nodes)) for i, host := range r.Nodes { diff --git a/internal/scheduling/cinder/filter_weigher_pipeline_controller.go b/internal/scheduling/cinder/filter_weigher_pipeline_controller.go index 5a4081784..52ec37306 100644 --- a/internal/scheduling/cinder/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/cinder/filter_weigher_pipeline_controller.go @@ -121,7 +121,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision return err } - result, err := pipeline.Run(request, lib.Options{}) + result, err := pipeline.Run(request) if err != nil { log.Error(err, "failed to run pipeline") return err diff --git a/internal/scheduling/lib/filter_weigher_pipeline.go b/internal/scheduling/lib/filter_weigher_pipeline.go index a67fa9b96..7930d1434 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline.go +++ b/internal/scheduling/lib/filter_weigher_pipeline.go @@ -18,8 +18,9 @@ import ( ) type FilterWeigherPipeline[RequestType FilterWeigherPipelineRequest] interface { - // Run the scheduling pipeline with the given request and call-time options. - Run(request RequestType, opts Options) (v1alpha1.DecisionResult, error) + // Run the scheduling pipeline with the given request. + // Call-time options are read from request.GetOptions(). + Run(request RequestType) (v1alpha1.DecisionResult, error) } // Pipeline of scheduler steps. @@ -264,7 +265,8 @@ func (s *filterWeigherPipeline[RequestType]) sortHostsByWeights(weights map[stri } // Evaluate the pipeline and return a list of hosts in order of preference. -func (p *filterWeigherPipeline[RequestType]) Run(request RequestType, opts Options) (v1alpha1.DecisionResult, error) { +func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1.DecisionResult, error) { + opts := request.GetOptions() if err := opts.Validate(); err != nil { return v1alpha1.DecisionResult{}, err } diff --git a/internal/scheduling/lib/filter_weigher_pipeline_request.go b/internal/scheduling/lib/filter_weigher_pipeline_request.go index 26688c358..69a9522e5 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_request.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_request.go @@ -21,4 +21,6 @@ type FilterWeigherPipelineRequest interface { // Get logging args to be used in the step's trace log. // Usually, this will be the request context including the request ID. GetTraceLogArgs() []slog.Attr + // Get the call-time options for this pipeline run. + GetOptions() Options } diff --git a/internal/scheduling/lib/filter_weigher_pipeline_request_test.go b/internal/scheduling/lib/filter_weigher_pipeline_request_test.go index 87ab0d786..765752a45 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_request_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_request_test.go @@ -11,6 +11,7 @@ type mockFilterWeigherPipelineRequest struct { Hosts []string Weights map[string]float64 Pipeline string + Options Options } func (m mockFilterWeigherPipelineRequest) GetWeightKeys() []string { return m.WeightKeys } @@ -18,6 +19,7 @@ func (m mockFilterWeigherPipelineRequest) GetTraceLogArgs() []slog.Attr { retu func (m mockFilterWeigherPipelineRequest) GetHosts() []string { return m.Hosts } func (m mockFilterWeigherPipelineRequest) GetWeights() map[string]float64 { return m.Weights } func (m mockFilterWeigherPipelineRequest) GetPipeline() string { return m.Pipeline } +func (m mockFilterWeigherPipelineRequest) GetOptions() Options { return m.Options } func (m mockFilterWeigherPipelineRequest) Filter(hosts map[string]float64) FilterWeigherPipelineRequest { filteredHosts := make([]string, 0, len(hosts)) diff --git a/internal/scheduling/lib/filter_weigher_pipeline_test.go b/internal/scheduling/lib/filter_weigher_pipeline_test.go index 9b89a2592..d22349a81 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_test.go @@ -72,7 +72,7 @@ func TestPipeline_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := pipeline.Run(tt.request, Options{}) + result, err := pipeline.Run(tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -400,7 +400,9 @@ func TestPipeline_MaxCandidates(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := pipeline.Run(request, Options{MaxCandidates: tt.maxCandidates}) + req := request + req.Options = Options{MaxCandidates: tt.maxCandidates} + result, err := pipeline.Run(req) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/lib/options_test.go b/internal/scheduling/lib/options_test.go new file mode 100644 index 000000000..6eb366b0c --- /dev/null +++ b/internal/scheduling/lib/options_test.go @@ -0,0 +1,34 @@ +// Copyright SAP SE +// SPDX-License-Identifier: Apache-2.0 + +package lib + +import "testing" + +func TestOptions_Validate(t *testing.T) { + tests := []struct { + name string + opts Options + wantErr bool + }{ + {"zero value is valid", Options{}, false}, + {"write run with history", Options{RecordHistory: true}, false}, + {"write run with inflight", Options{CreateInflight: true}, false}, + {"read-only run, no side effects", Options{ReadOnly: true}, false}, + {"ReadOnly + RecordHistory is invalid", Options{ReadOnly: true, RecordHistory: true}, true}, + {"ReadOnly + CreateInflight is invalid", Options{ReadOnly: true, CreateInflight: true}, true}, + {"ReadOnly + both invalid", Options{ReadOnly: true, RecordHistory: true, CreateInflight: true}, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.opts.Validate() + if tt.wantErr && err == nil { + t.Error("expected error, got nil") + } + if !tt.wantErr && err != nil { + t.Errorf("expected no error, got %v", err) + } + }) + } +} diff --git a/internal/scheduling/machines/filter_weigher_pipeline_controller.go b/internal/scheduling/machines/filter_weigher_pipeline_controller.go index 93e0f5e41..35d51708a 100644 --- a/internal/scheduling/machines/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/machines/filter_weigher_pipeline_controller.go @@ -144,7 +144,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision // Execute the scheduling pipeline. request := ironcore.MachinePipelineRequest{Pools: pools.Items} - result, err := pipeline.Run(request, lib.Options{}) + result, err := pipeline.Run(request) if err != nil { log.V(1).Error(err, "failed to run scheduler pipeline") return errors.New("failed to run scheduler pipeline") diff --git a/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go b/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go index 28fe49ed7..bc2e0722a 100644 --- a/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go @@ -516,7 +516,7 @@ func createMockPipeline() lib.FilterWeigherPipeline[ironcore.MachinePipelineRequ type mockMachinePipeline struct{} -func (m *mockMachinePipeline) Run(request ironcore.MachinePipelineRequest, opts lib.Options) (v1alpha1.DecisionResult, error) { +func (m *mockMachinePipeline) Run(request ironcore.MachinePipelineRequest) (v1alpha1.DecisionResult, error) { if len(request.Pools) == 0 { return v1alpha1.DecisionResult{}, nil } diff --git a/internal/scheduling/manila/filter_weigher_pipeline_controller.go b/internal/scheduling/manila/filter_weigher_pipeline_controller.go index 18ed212f5..128b7d719 100644 --- a/internal/scheduling/manila/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/manila/filter_weigher_pipeline_controller.go @@ -121,7 +121,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision return err } - result, err := pipeline.Run(request, lib.Options{}) + result, err := pipeline.Run(request) if err != nil { log.Error(err, "failed to run pipeline") return err diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller.go b/internal/scheduling/nova/filter_weigher_pipeline_controller.go index 53d4555f1..703f6d23f 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller.go @@ -174,9 +174,12 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision log.Info("gathered all placement candidates", "numHosts", len(request.Hosts)) } - opts := c.buildOptions(request, pipelineConf) - result, err := pipeline.Run(request, opts) - if opts.RecordHistory { + // Fill RecordHistory from config if the caller didn't set it. + if !request.Options.RecordHistory { + request.Options.RecordHistory = pipelineConf.Spec.CreateHistory + } + result, err := pipeline.Run(request) + if request.Options.RecordHistory { c.upsertHistory(ctx, decision, err) } if err != nil { @@ -204,26 +207,7 @@ func (c *FilterWeigherPipelineController) peekReadOnly(decision *v1alpha1.Decisi if err := json.Unmarshal(decision.Spec.NovaRaw.Raw, &request); err != nil { return false } - pipelineConf, ok := c.PipelineConfigs[decision.Spec.PipelineRef.Name] - if !ok { - return false - } - return c.buildOptions(request, pipelineConf).ReadOnly -} - -// The base controller will delegate the pipeline creation down to this method. -func (c *FilterWeigherPipelineController) buildOptions(request api.ExternalSchedulerRequest, pipelineConf v1alpha1.Pipeline) lib.Options { - opts := lib.Options{ - RecordHistory: pipelineConf.Spec.CreateHistory, - } - intent, err := request.GetIntent() - if err == nil { - switch intent { - case api.ReserveForCommittedResourceIntent, api.ReserveForFailoverIntent: - opts.LockReservations = true - } - } - return opts + return request.Options.ReadOnly } func (c *FilterWeigherPipelineController) InitPipeline( diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go index 781e259bd..031527b84 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go @@ -930,22 +930,19 @@ func TestFilterWeigherPipelineController_IgnorePreselection(t *testing.T) { var errGathererFailed = errors.New("gatherer failed") func TestFilterWeigherPipelineController_PeekReadOnly(t *testing.T) { - validRequest := api.ExternalSchedulerRequest{ - Spec: api.NovaObject[api.NovaSpec]{ - Data: api.NovaSpec{NumInstances: 1}, - }, - } - validRaw, err := json.Marshal(validRequest) - if err != nil { - t.Fatalf("failed to marshal test request: %v", err) + makeRaw := func(readOnly bool) []byte { + r := api.ExternalSchedulerRequest{ + Spec: api.NovaObject[api.NovaSpec]{Data: api.NovaSpec{NumInstances: 1}}, + Options: lib.Options{ReadOnly: readOnly}, + } + raw, err := json.Marshal(r) + if err != nil { + panic(err) + } + return raw } c := &FilterWeigherPipelineController{} - c.PipelineConfigs = map[string]v1alpha1.Pipeline{ - "test-pipeline": { - Spec: v1alpha1.PipelineSpec{CreateHistory: false}, - }, - } tests := []struct { name string @@ -972,24 +969,24 @@ func TestFilterWeigherPipelineController_PeekReadOnly(t *testing.T) { want: false, }, { - name: "unknown pipeline defaults to exclusive lock", + name: "ReadOnly=false uses exclusive lock", decision: &v1alpha1.Decision{ Spec: v1alpha1.DecisionSpec{ - PipelineRef: corev1.ObjectReference{Name: "unknown-pipeline"}, - NovaRaw: &runtime.RawExtension{Raw: validRaw}, + PipelineRef: corev1.ObjectReference{Name: "test-pipeline"}, + NovaRaw: &runtime.RawExtension{Raw: makeRaw(false)}, }, }, want: false, }, { - name: "valid request with non-ReadOnly intent uses exclusive lock", + name: "ReadOnly=true uses read lock", decision: &v1alpha1.Decision{ Spec: v1alpha1.DecisionSpec{ PipelineRef: corev1.ObjectReference{Name: "test-pipeline"}, - NovaRaw: &runtime.RawExtension{Raw: validRaw}, + NovaRaw: &runtime.RawExtension{Raw: makeRaw(true)}, }, }, - want: false, + want: true, }, } diff --git a/internal/scheduling/pods/filter_weigher_pipeline_controller.go b/internal/scheduling/pods/filter_weigher_pipeline_controller.go index 7f76adede..0ceee6485 100644 --- a/internal/scheduling/pods/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/pods/filter_weigher_pipeline_controller.go @@ -158,7 +158,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision // Execute the scheduling pipeline. request := pods.PodPipelineRequest{Nodes: nodes.Items, Pod: *pod} - result, err := pipeline.Run(request, lib.Options{}) + result, err := pipeline.Run(request) if err != nil { log.V(1).Error(err, "failed to run scheduler pipeline") return errors.New("failed to run scheduler pipeline") diff --git a/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go b/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go index f1e429da3..143ed9f83 100644 --- a/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go @@ -492,7 +492,7 @@ func createMockPodPipeline() lib.FilterWeigherPipeline[pods.PodPipelineRequest] type mockPodPipeline struct{} -func (m *mockPodPipeline) Run(request pods.PodPipelineRequest, opts lib.Options) (v1alpha1.DecisionResult, error) { +func (m *mockPodPipeline) Run(request pods.PodPipelineRequest) (v1alpha1.DecisionResult, error) { if len(request.Nodes) == 0 { return v1alpha1.DecisionResult{}, nil } diff --git a/internal/scheduling/reservations/commitments/reservation_controller.go b/internal/scheduling/reservations/commitments/reservation_controller.go index 09cca220d..5d9ab7483 100644 --- a/internal/scheduling/reservations/commitments/reservation_controller.go +++ b/internal/scheduling/reservations/commitments/reservation_controller.go @@ -286,18 +286,18 @@ func (r *CommitmentReservationController) Reconcile(ctx context.Context, req ctr SchedulerHints: map[string]any{ "_nova_check_type": string(schedulerdelegationapi.ReserveForCommittedResourceIntent), }, - Options: lib.Options{ - ReadOnly: false, // mutates state (reservation placement) - LockReservations: true, // don't unlock CR reservations; finding a slot, not placing a VM - AssumeEmptyHosts: false, - IgnoredReservationTypes: nil, - MaxCandidates: 1, - RecordHistory: false, - CreateInflight: false, - }, + } + scheduleOpts := lib.Options{ + ReadOnly: false, // mutates state (reservation placement) + LockReservations: true, // don't unlock CR reservations; finding a slot, not placing a VM + AssumeEmptyHosts: false, + IgnoredReservationTypes: nil, + MaxCandidates: 1, + RecordHistory: false, + CreateInflight: false, // not a VM placement; no pessimistic blocking needed } - scheduleResp, err := r.SchedulerClient.ScheduleReservation(ctx, scheduleReq) + scheduleResp, err := r.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduleOpts) if err != nil { logger.Error(err, "failed to schedule reservation") return ctrl.Result{}, err diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index f482f3393..5f8c93767 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -11,6 +11,7 @@ import ( api "github.com/cobaltcore-dev/cortex/api/external/nova" "github.com/cobaltcore-dev/cortex/api/v1alpha1" + "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations" ) @@ -91,7 +92,7 @@ func (c *FailoverReservationController) queryHypervisorsFromScheduler(ctx contex "eligibleHypervisors", len(eligibleHypervisors), "ignoreHypervisors", ignoreHypervisors) - scheduleResp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq) + scheduleResp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, lib.Options{LockReservations: true}) if err != nil { logger.Error(err, "failed to schedule failover reservation", "vmUUID", vm.UUID, "pipeline", pipeline) return nil, fmt.Errorf("failed to schedule failover reservation: %w", err) @@ -222,7 +223,7 @@ func (c *FailoverReservationController) validateVMViaSchedulerEvacuation( "vmCurrentHost", vm.CurrentHypervisor, "pipeline", PipelineAcknowledgeFailoverReservation) - resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq) + resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, lib.Options{ReadOnly: true}) if err != nil { logger.Error(err, "failed to validate VM for reservation host", "vmUUID", vm.UUID, "reservationHost", reservationHost) return false, fmt.Errorf("failed to validate VM for reservation host: %w", err) diff --git a/internal/scheduling/reservations/scheduler_client.go b/internal/scheduling/reservations/scheduler_client.go index e250d170d..f10ad21d0 100644 --- a/internal/scheduling/reservations/scheduler_client.go +++ b/internal/scheduling/reservations/scheduler_client.go @@ -79,10 +79,6 @@ type ScheduleReservationRequest struct { // SchedulerHints are hints passed to the scheduler pipeline. // Used to set _nova_check_type for evacuation intent detection. SchedulerHints map[string]any - // Options configures the pipeline behavior for this scheduling call. - // These are derived from intent in buildOptions for the current HTTP path; - // will be passed directly once the scheduler client is a direct Go call. - Options lib.Options } // ScheduleReservationResponse contains the result of scheduling a reservation. @@ -94,7 +90,7 @@ type ScheduleReservationResponse struct { // ScheduleReservation calls the external scheduler API to find a host for a reservation. // The context should contain GlobalRequestID and RequestID for logging (use WithGlobalRequestID/WithRequestID). -func (c *SchedulerClient) ScheduleReservation(ctx context.Context, req ScheduleReservationRequest) (*ScheduleReservationResponse, error) { +func (c *SchedulerClient) ScheduleReservation(ctx context.Context, req ScheduleReservationRequest, opts lib.Options) (*ScheduleReservationResponse, error) { logger := loggerFromContext(ctx) // Build weights map (all zero for reservations) @@ -120,6 +116,7 @@ func (c *SchedulerClient) ScheduleReservation(ctx context.Context, req ScheduleR Pipeline: req.Pipeline, Hosts: req.EligibleHosts, Weights: weights, + Options: opts, Context: api.NovaRequestContext{ RequestID: RequestIDFromContext(ctx), GlobalRequestID: globalReqID, From c2d0fc255402989f60b6d78e112de2792355eeb6 Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 5 May 2026 17:01:53 +0200 Subject: [PATCH 06/19] . --- api/external/ironcore/messages.go | 2 +- api/external/pods/messages.go | 2 +- internal/scheduling/lib/options.go | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/api/external/ironcore/messages.go b/api/external/ironcore/messages.go index 29da83b9b..05797e15a 100644 --- a/api/external/ironcore/messages.go +++ b/api/external/ironcore/messages.go @@ -14,7 +14,7 @@ type MachinePipelineRequest struct { // The available machine pools. Pools []ironcorev1alpha1.MachinePool `json:"pools"` // Options configure the pipeline behavior for this scheduling call. - Options lib.Options + Options lib.Options `json:"options,omitempty"` } func (r MachinePipelineRequest) GetOptions() lib.Options { return r.Options } diff --git a/api/external/pods/messages.go b/api/external/pods/messages.go index 0a801ac22..0b5466415 100644 --- a/api/external/pods/messages.go +++ b/api/external/pods/messages.go @@ -16,7 +16,7 @@ type PodPipelineRequest struct { // The pod to be scheduled. Pod corev1.Pod `json:"pod"` // Options configure the pipeline behavior for this scheduling call. - Options lib.Options + Options lib.Options `json:"options,omitempty"` } func (r PodPipelineRequest) GetOptions() lib.Options { return r.Options } diff --git a/internal/scheduling/lib/options.go b/internal/scheduling/lib/options.go index c516957f0..44445cf6d 100644 --- a/internal/scheduling/lib/options.go +++ b/internal/scheduling/lib/options.go @@ -16,7 +16,9 @@ import ( // Consumed by steps: ReadOnly, LockReservations, AssumeEmptyHosts, IgnoredReservationTypes. // Consumed by the controller after pipeline.Run(): RecordHistory, CreateInflight. type Options struct { - // ReadOnly means the pipeline could run without using the mutex, i.e. concurrent runs are ok. + // ReadOnly means the pipeline run does not modify shared scheduling state (reservations, + // history, inflight records). Concurrent read-only runs are safe under a shared read lock. + // Note: the controller may still write the Decision status after Run() regardless of this flag. ReadOnly bool // LockReservations prevents reservation unlocking, e.g. in the capacity filter. // Set when finding hosts for new reservations (failover, CR) to see true available capacity. @@ -38,10 +40,10 @@ type Options struct { // Validate checks for mutually exclusive or inconsistent option combinations. func (o Options) Validate() error { if o.ReadOnly && o.RecordHistory { - return errors.New("ReadOnly and RecordHistory are mutually exclusive: read-only runs must not mutate state") + return errors.New("ReadOnly and RecordHistory are mutually exclusive: read-only runs must not write scheduling history") } if o.ReadOnly && o.CreateInflight { - return errors.New("ReadOnly and CreateInflight are mutually exclusive: read-only runs must not mutate state") + return errors.New("ReadOnly and CreateInflight are mutually exclusive: read-only runs must not create inflight reservations") } return nil } From 4de31f9c4ce717aef0739cbc6567723a9d604285 Mon Sep 17 00:00:00 2001 From: mblos Date: Wed, 6 May 2026 11:31:28 +0200 Subject: [PATCH 07/19] refactor --- internal/scheduling/lib/filter_monitor.go | 4 ++-- internal/scheduling/lib/filter_monitor_test.go | 2 +- internal/scheduling/lib/filter_test.go | 2 +- internal/scheduling/lib/filter_validation.go | 4 ++-- .../scheduling/lib/filter_validation_test.go | 2 +- .../scheduling/lib/filter_weigher_pipeline.go | 11 +++++------ .../lib/filter_weigher_pipeline_step.go | 4 ++-- .../filter_weigher_pipeline_step_monitor.go | 3 +-- ...ilter_weigher_pipeline_step_monitor_test.go | 2 +- .../lib/filter_weigher_pipeline_test.go | 2 +- internal/scheduling/lib/weigher_monitor.go | 4 ++-- .../scheduling/lib/weigher_monitor_test.go | 2 +- internal/scheduling/lib/weigher_test.go | 2 +- internal/scheduling/lib/weigher_validation.go | 4 ++-- .../scheduling/lib/weigher_validation_test.go | 4 ++-- .../machines/plugins/filters/filter_noop.go | 2 +- .../plugins/filters/filter_noop_test.go | 3 +-- .../weighers/netapp_cpu_usage_balancing.go | 2 +- .../netapp_cpu_usage_balancing_test.go | 3 +-- .../filters/filter_aggregate_metadata.go | 2 +- .../filters/filter_aggregate_metadata_test.go | 5 ++--- .../plugins/filters/filter_allowed_projects.go | 2 +- .../filters/filter_allowed_projects_test.go | 3 +-- .../plugins/filters/filter_capabilities.go | 2 +- .../filters/filter_capabilities_test.go | 5 ++--- .../nova/plugins/filters/filter_correct_az.go | 2 +- .../plugins/filters/filter_correct_az_test.go | 3 +-- .../plugins/filters/filter_exclude_hosts.go | 1 - .../filters/filter_exclude_hosts_test.go | 2 +- .../filters/filter_external_customer.go | 2 +- .../filters/filter_external_customer_test.go | 3 +-- .../plugins/filters/filter_has_accelerators.go | 2 +- .../filters/filter_has_accelerators_test.go | 3 +-- .../filters/filter_has_enough_capacity.go | 3 ++- .../filters/filter_has_enough_capacity_test.go | 18 ++++++++++-------- .../filters/filter_has_requested_traits.go | 2 +- .../filter_has_requested_traits_test.go | 3 +-- .../filters/filter_host_instructions.go | 2 +- .../filters/filter_host_instructions_test.go | 3 +-- .../filters/filter_instance_group_affinity.go | 1 - .../filter_instance_group_affinity_test.go | 3 +-- .../filter_instance_group_anti_affinity.go | 1 - ...filter_instance_group_anti_affinity_test.go | 3 +-- .../plugins/filters/filter_live_migratable.go | 1 - .../filters/filter_live_migratable_test.go | 6 +++--- .../filters/filter_requested_destination.go | 2 +- .../filter_requested_destination_test.go | 4 ++-- .../filters/filter_status_conditions.go | 2 +- .../filters/filter_status_conditions_test.go | 3 +-- .../nova/plugins/weighers/kvm_binpack.go | 2 +- .../nova/plugins/weighers/kvm_binpack_test.go | 3 +-- .../weighers/kvm_failover_evacuation.go | 2 +- .../weighers/kvm_failover_evacuation_test.go | 3 +-- .../kvm_failover_reservation_consolidation.go | 2 +- ..._failover_reservation_consolidation_test.go | 3 +-- .../kvm_instance_group_soft_affinity.go | 2 +- .../kvm_instance_group_soft_affinity_test.go | 3 +-- .../weighers/kvm_prefer_smaller_hosts.go | 2 +- .../weighers/kvm_prefer_smaller_hosts_test.go | 3 +-- .../vmware_anti_affinity_noisy_projects.go | 2 +- ...vmware_anti_affinity_noisy_projects_test.go | 3 +-- .../vmware_avoid_long_term_contended_hosts.go | 2 +- ...are_avoid_long_term_contended_hosts_test.go | 3 +-- .../vmware_avoid_short_term_contended_hosts.go | 2 +- ...re_avoid_short_term_contended_hosts_test.go | 3 +-- .../nova/plugins/weighers/vmware_binpack.go | 2 +- .../plugins/weighers/vmware_binpack_test.go | 3 +-- .../plugins/filters/filter_node_affinity.go | 2 +- .../filters/filter_node_affinity_test.go | 3 +-- .../plugins/filters/filter_node_available.go | 2 +- .../filters/filter_node_available_test.go | 3 +-- .../plugins/filters/filter_node_capacity.go | 2 +- .../filters/filter_node_capacity_test.go | 3 +-- .../pods/plugins/filters/filter_noop.go | 2 +- .../pods/plugins/filters/filter_noop_test.go | 3 +-- .../pods/plugins/filters/filter_taint.go | 2 +- .../pods/plugins/filters/filter_taint_test.go | 3 +-- .../pods/plugins/weighers/binpack.go | 2 +- .../pods/plugins/weighers/binpack_test.go | 3 +-- 79 files changed, 100 insertions(+), 131 deletions(-) diff --git a/internal/scheduling/lib/filter_monitor.go b/internal/scheduling/lib/filter_monitor.go index 97e9d661d..d0afd9282 100644 --- a/internal/scheduling/lib/filter_monitor.go +++ b/internal/scheduling/lib/filter_monitor.go @@ -43,6 +43,6 @@ func (fm *FilterMonitor[RequestType]) Validate(ctx context.Context, params v1alp } // Run the filter and observe its execution. -func (fm *FilterMonitor[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { - return fm.monitor.RunWrapped(traceLog, request, opts, fm.filter) +func (fm *FilterMonitor[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { + return fm.monitor.RunWrapped(traceLog, request, fm.filter) } diff --git a/internal/scheduling/lib/filter_monitor_test.go b/internal/scheduling/lib/filter_monitor_test.go index b27f811b1..f709d88aa 100644 --- a/internal/scheduling/lib/filter_monitor_test.go +++ b/internal/scheduling/lib/filter_monitor_test.go @@ -100,7 +100,7 @@ func TestFilterMonitor_Run(t *testing.T) { Weights: map[string]float64{"host1": 0.1, "host2": 0.2, "host3": 0.3}, } - result, err := fm.Run(slog.Default(), request, Options{}) + result, err := fm.Run(slog.Default(), request) if err != nil { t.Errorf("expected no error, got %v", err) } diff --git a/internal/scheduling/lib/filter_test.go b/internal/scheduling/lib/filter_test.go index 14fe73997..652211163 100644 --- a/internal/scheduling/lib/filter_test.go +++ b/internal/scheduling/lib/filter_test.go @@ -31,7 +31,7 @@ func (m *mockFilter[RequestType]) Validate(ctx context.Context, params v1alpha1. } return m.ValidateFunc(ctx, params) } -func (m *mockFilter[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { +func (m *mockFilter[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { if m.RunFunc == nil { return &FilterWeigherPipelineStepResult{}, nil } diff --git a/internal/scheduling/lib/filter_validation.go b/internal/scheduling/lib/filter_validation.go index 2054a9ad9..9ad43311d 100644 --- a/internal/scheduling/lib/filter_validation.go +++ b/internal/scheduling/lib/filter_validation.go @@ -35,8 +35,8 @@ func validateFilter[RequestType FilterWeigherPipelineRequest](filter Filter[Requ } // Run the filter and validate what happens. -func (s *FilterValidator[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { - result, err := s.Filter.Run(traceLog, request, opts) +func (s *FilterValidator[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { + result, err := s.Filter.Run(traceLog, request) if err != nil { return nil, err } diff --git a/internal/scheduling/lib/filter_validation_test.go b/internal/scheduling/lib/filter_validation_test.go index deb064dfc..dc35c2f6a 100644 --- a/internal/scheduling/lib/filter_validation_test.go +++ b/internal/scheduling/lib/filter_validation_test.go @@ -156,7 +156,7 @@ func TestFilterValidator_Run(t *testing.T) { } traceLog := slog.Default() - result, err := validator.Run(traceLog, request, Options{}) + result, err := validator.Run(traceLog, request) if tt.expectError && err == nil { t.Error("expected error but got nil") diff --git a/internal/scheduling/lib/filter_weigher_pipeline.go b/internal/scheduling/lib/filter_weigher_pipeline.go index 7930d1434..e2b9ce468 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline.go +++ b/internal/scheduling/lib/filter_weigher_pipeline.go @@ -139,7 +139,6 @@ func InitNewFilterWeigherPipeline[RequestType FilterWeigherPipelineRequest]( func (p *filterWeigherPipeline[RequestType]) runFilters( log *slog.Logger, request RequestType, - opts Options, ) (filteredRequest RequestType, stepResults []v1alpha1.StepResult) { filteredRequest = request @@ -147,7 +146,7 @@ func (p *filterWeigherPipeline[RequestType]) runFilters( filter := p.filters[filterName] stepLog := log.With("filter", filterName) stepLog.Info("scheduler: running filter") - result, err := filter.Run(stepLog, filteredRequest, opts) + result, err := filter.Run(stepLog, filteredRequest) if errors.Is(err, ErrStepSkipped) { stepLog.Info("scheduler: filter skipped") continue @@ -172,7 +171,6 @@ func (p *filterWeigherPipeline[RequestType]) runFilters( func (p *filterWeigherPipeline[RequestType]) runWeighers( log *slog.Logger, filteredRequest RequestType, - opts Options, ) map[string]map[string]float64 { activationsByStep := map[string]map[string]float64{} @@ -184,7 +182,7 @@ func (p *filterWeigherPipeline[RequestType]) runWeighers( wg.Go(func() { stepLog := log.With("weigher", weigherName) stepLog.Info("scheduler: running weigher") - result, err := weigher.Run(stepLog, filteredRequest, opts) + result, err := weigher.Run(stepLog, filteredRequest) if errors.Is(err, ErrStepSkipped) { stepLog.Info("scheduler: weigher skipped") return @@ -286,7 +284,7 @@ func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1. // Run filters first to reduce the number of hosts. // Any weights assigned to filtered out hosts are ignored. - filteredRequest, filterStepResults := p.runFilters(traceLog, request, opts) + filteredRequest, filterStepResults := p.runFilters(traceLog, request) traceLog.Info( "scheduler: finished filters", "remainingHosts", filteredRequest.GetHosts(), @@ -297,7 +295,7 @@ func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1. for _, host := range filteredRequest.GetHosts() { remainingWeights[host] = inWeights[host] } - stepWeights := p.runWeighers(traceLog, filteredRequest, opts) + stepWeights := p.runWeighers(traceLog, filteredRequest) outWeights := p.applyWeights(traceLog, stepWeights, remainingWeights) traceLog.Info("scheduler: output weights", "weights", outWeights) @@ -305,6 +303,7 @@ func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1. traceLog.Info("scheduler: sorted hosts", "hosts", hosts) if opts.MaxCandidates > 0 && len(hosts) > opts.MaxCandidates { + traceLog.Info("scheduler: trimming candidate list", "maxCandidates", opts.MaxCandidates, "before", len(hosts)) hosts = hosts[:opts.MaxCandidates] // Drop trimmed hosts from outWeights so AggregatedOutWeights stays consistent. for host := range outWeights { diff --git a/internal/scheduling/lib/filter_weigher_pipeline_step.go b/internal/scheduling/lib/filter_weigher_pipeline_step.go index cf6b3f207..54816519c 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_step.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_step.go @@ -31,8 +31,8 @@ type FilterWeigherPipelineStep[RequestType FilterWeigherPipelineRequest] interfa // A traceLog is provided that contains the global request id and should // be used to log the step's execution. // - // opts carries per-call behavioral options set by the pipeline caller. - Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) + // Per-call options are available via request.GetOptions(). + Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) } // Common base for all steps that provides some functionality diff --git a/internal/scheduling/lib/filter_weigher_pipeline_step_monitor.go b/internal/scheduling/lib/filter_weigher_pipeline_step_monitor.go index e54651ec5..3e64fa6ee 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_step_monitor.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_step_monitor.go @@ -65,7 +65,6 @@ func monitorStep[RequestType FilterWeigherPipelineRequest](stepName string, m Fi func (s *FilterWeigherPipelineStepMonitor[RequestType]) RunWrapped( traceLog *slog.Logger, request RequestType, - opts Options, step FilterWeigherPipelineStep[RequestType], ) (*FilterWeigherPipelineStepResult, error) { @@ -75,7 +74,7 @@ func (s *FilterWeigherPipelineStepMonitor[RequestType]) RunWrapped( } inWeights := request.GetWeights() - stepResult, err := step.Run(traceLog, request, opts) + stepResult, err := step.Run(traceLog, request) if err != nil { return nil, err } diff --git a/internal/scheduling/lib/filter_weigher_pipeline_step_monitor_test.go b/internal/scheduling/lib/filter_weigher_pipeline_step_monitor_test.go index af4bf74ec..7d7817abd 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_step_monitor_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_step_monitor_test.go @@ -38,7 +38,7 @@ func TestStepMonitorRun(t *testing.T) { Hosts: []string{"host1", "host2", "host3"}, Weights: map[string]float64{"host1": 0.2, "host2": 0.1, "host3": 0.0}, } - if _, err := monitor.RunWrapped(slog.Default(), request, Options{}, step); err != nil { + if _, err := monitor.RunWrapped(slog.Default(), request, step); err != nil { t.Fatalf("Run() error = %v, want nil", err) } if len(removedHostsObserver.Observations) != 1 { diff --git a/internal/scheduling/lib/filter_weigher_pipeline_test.go b/internal/scheduling/lib/filter_weigher_pipeline_test.go index d22349a81..54251731c 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_test.go @@ -221,7 +221,7 @@ func TestPipeline_RunFilters(t *testing.T) { Weights: map[string]float64{"host1": 0.0, "host2": 0.0, "host3": 0.0}, } - req, _ := p.runFilters(slog.Default(), request, Options{}) + req, _ := p.runFilters(slog.Default(), request) if len(req.Hosts) != 2 { t.Fatalf("expected 2 step results, got %d", len(req.Hosts)) } diff --git a/internal/scheduling/lib/weigher_monitor.go b/internal/scheduling/lib/weigher_monitor.go index 56a9ebb6e..df855d067 100644 --- a/internal/scheduling/lib/weigher_monitor.go +++ b/internal/scheduling/lib/weigher_monitor.go @@ -43,6 +43,6 @@ func (wm *WeigherMonitor[RequestType]) Validate(ctx context.Context, params v1al } // Run the weigher and observe its execution. -func (wm *WeigherMonitor[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { - return wm.monitor.RunWrapped(traceLog, request, opts, wm.weigher) +func (wm *WeigherMonitor[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { + return wm.monitor.RunWrapped(traceLog, request, wm.weigher) } diff --git a/internal/scheduling/lib/weigher_monitor_test.go b/internal/scheduling/lib/weigher_monitor_test.go index c84435234..6f8f906e3 100644 --- a/internal/scheduling/lib/weigher_monitor_test.go +++ b/internal/scheduling/lib/weigher_monitor_test.go @@ -100,7 +100,7 @@ func TestWeigherMonitor_Run(t *testing.T) { Weights: map[string]float64{"host1": 0.1, "host2": 0.2, "host3": 0.3}, } - result, err := wm.Run(slog.Default(), request, Options{}) + result, err := wm.Run(slog.Default(), request) if err != nil { t.Errorf("expected no error, got %v", err) } diff --git a/internal/scheduling/lib/weigher_test.go b/internal/scheduling/lib/weigher_test.go index 488704ef4..4660207c4 100644 --- a/internal/scheduling/lib/weigher_test.go +++ b/internal/scheduling/lib/weigher_test.go @@ -34,7 +34,7 @@ func (m *mockWeigher[RequestType]) Validate(ctx context.Context, params v1alpha1 } return m.ValidateFunc(ctx, params) } -func (m *mockWeigher[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { +func (m *mockWeigher[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { if m.RunFunc == nil { return &FilterWeigherPipelineStepResult{}, nil } diff --git a/internal/scheduling/lib/weigher_validation.go b/internal/scheduling/lib/weigher_validation.go index bb4c6b823..c454d171e 100644 --- a/internal/scheduling/lib/weigher_validation.go +++ b/internal/scheduling/lib/weigher_validation.go @@ -35,8 +35,8 @@ func validateWeigher[RequestType FilterWeigherPipelineRequest](weigher Weigher[R } // Run the weigher and validate what happens. -func (s *WeigherValidator[RequestType]) Run(traceLog *slog.Logger, request RequestType, opts Options) (*FilterWeigherPipelineStepResult, error) { - result, err := s.Weigher.Run(traceLog, request, opts) +func (s *WeigherValidator[RequestType]) Run(traceLog *slog.Logger, request RequestType) (*FilterWeigherPipelineStepResult, error) { + result, err := s.Weigher.Run(traceLog, request) if err != nil { return nil, err } diff --git a/internal/scheduling/lib/weigher_validation_test.go b/internal/scheduling/lib/weigher_validation_test.go index af7efb163..852448a88 100644 --- a/internal/scheduling/lib/weigher_validation_test.go +++ b/internal/scheduling/lib/weigher_validation_test.go @@ -96,7 +96,7 @@ func TestWeigherValidator_Run_ValidHosts(t *testing.T) { Weigher: mockStep, } - result, err := validator.Run(slog.Default(), request, Options{}) + result, err := validator.Run(slog.Default(), request) if err != nil { t.Errorf("Run() error = %v, want nil", err) } @@ -130,7 +130,7 @@ func TestWeigherValidator_Run_HostNumberMismatch(t *testing.T) { Weigher: mockStep, } - result, err := validator.Run(slog.Default(), request, Options{}) + result, err := validator.Run(slog.Default(), request) if err == nil { t.Errorf("Run() error = nil, want error") } diff --git a/internal/scheduling/machines/plugins/filters/filter_noop.go b/internal/scheduling/machines/plugins/filters/filter_noop.go index 56fb55dfe..da901e5c0 100644 --- a/internal/scheduling/machines/plugins/filters/filter_noop.go +++ b/internal/scheduling/machines/plugins/filters/filter_noop.go @@ -31,7 +31,7 @@ func (f *NoopFilter) Validate(ctx context.Context, params v1alpha1.Parameters) e // not in the map are considered as filtered out. // Provide a traceLog that contains the global request id and should // be used to log the step's execution. -func (NoopFilter) Run(traceLog *slog.Logger, request ironcore.MachinePipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (NoopFilter) Run(traceLog *slog.Logger, request ironcore.MachinePipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64, len(request.Pools)) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) // Usually you would do some filtering here, or adjust the weights. diff --git a/internal/scheduling/machines/plugins/filters/filter_noop_test.go b/internal/scheduling/machines/plugins/filters/filter_noop_test.go index 06d80c771..2fa369a4f 100644 --- a/internal/scheduling/machines/plugins/filters/filter_noop_test.go +++ b/internal/scheduling/machines/plugins/filters/filter_noop_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -65,7 +64,7 @@ func TestNoopFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NoopFilter{} - result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) + result, err := filter.Run(slog.Default(), tt.request) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing.go b/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing.go index 01d31a55a..ce3e30ebe 100644 --- a/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing.go +++ b/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing.go @@ -61,7 +61,7 @@ func (s *NetappCPUUsageBalancingStep) Init(ctx context.Context, client client.Cl } // Downvote hosts that are highly contended. -func (s *NetappCPUUsageBalancingStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *NetappCPUUsageBalancingStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["avg cpu contention"] = s.PrepareStats(request, "%") result.Statistics["max cpu contention"] = s.PrepareStats(request, "%") diff --git a/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing_test.go b/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing_test.go index eeb0cdea2..f3e9c66ea 100644 --- a/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing_test.go +++ b/internal/scheduling/manila/plugins/weighers/netapp_cpu_usage_balancing_test.go @@ -4,7 +4,6 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -165,7 +164,7 @@ func TestNetappCPUUsageBalancingStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata.go b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata.go index 82a50ce1c..157a80521 100644 --- a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata.go +++ b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata.go @@ -19,7 +19,7 @@ type FilterAggregateMetadata struct { // Restrict hosts to specific projects if they are in an aggregate that has // the "filter_tenant_id" metadata key set. -func (s *FilterAggregateMetadata) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterAggregateMetadata) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) hvs := &hv1.HypervisorList{} diff --git a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go index 45814d50f..d1ff9cd2d 100644 --- a/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_aggregate_metadata_test.go @@ -5,7 +5,6 @@ package filters import ( "context" - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -337,7 +336,7 @@ func TestFilterAggregateMetadata_Run(t *testing.T) { step := &FilterAggregateMetadata{} step.Client = fakeClient - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -389,7 +388,7 @@ func TestFilterAggregateMetadata_Run_ClientError(t *testing.T) { step := &FilterAggregateMetadata{} step.Client = fakeClient - _, err := step.Run(slog.Default(), request, lib.Options{}) + _, err := step.Run(slog.Default(), request) if err == nil { t.Errorf("expected error when client fails, got none") } diff --git a/internal/scheduling/nova/plugins/filters/filter_allowed_projects.go b/internal/scheduling/nova/plugins/filters/filter_allowed_projects.go index 21d6c6dd8..a0a486f3d 100644 --- a/internal/scheduling/nova/plugins/filters/filter_allowed_projects.go +++ b/internal/scheduling/nova/plugins/filters/filter_allowed_projects.go @@ -19,7 +19,7 @@ type FilterAllowedProjectsStep struct { // Lock certain hosts for certain projects, based on the hypervisor spec. // Note that hosts without specified projects are still accessible. -func (s *FilterAllowedProjectsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterAllowedProjectsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) if request.Spec.Data.ProjectID == "" { traceLog.Info("no project ID in request, skipping filter") diff --git a/internal/scheduling/nova/plugins/filters/filter_allowed_projects_test.go b/internal/scheduling/nova/plugins/filters/filter_allowed_projects_test.go index 9dc95da59..070160e2e 100644 --- a/internal/scheduling/nova/plugins/filters/filter_allowed_projects_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_allowed_projects_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -296,7 +295,7 @@ func TestFilterAllowedProjectsStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_capabilities.go b/internal/scheduling/nova/plugins/filters/filter_capabilities.go index 0fd1781ea..cda9a9a20 100644 --- a/internal/scheduling/nova/plugins/filters/filter_capabilities.go +++ b/internal/scheduling/nova/plugins/filters/filter_capabilities.go @@ -45,7 +45,7 @@ func hvToNovaCapabilities(hv hv1.Hypervisor) (map[string]string, error) { // Check the capabilities of each host and if they match the extra spec provided // in the request spec flavor. -func (s *FilterCapabilitiesStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterCapabilitiesStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) extraSpecs := request.Spec.Data.Flavor.Data.ExtraSpecs if len(extraSpecs) == 0 { diff --git a/internal/scheduling/nova/plugins/filters/filter_capabilities_test.go b/internal/scheduling/nova/plugins/filters/filter_capabilities_test.go index 2aa6d2ba7..9b5f111dc 100644 --- a/internal/scheduling/nova/plugins/filters/filter_capabilities_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_capabilities_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -554,7 +553,7 @@ func TestFilterCapabilitiesStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -629,7 +628,7 @@ func TestFilterCapabilitiesStep_DoesNotMutateExtraSpecs(t *testing.T) { WithObjects(hvs...). Build() - _, err = step.Run(slog.Default(), request, lib.Options{}) + _, err = step.Run(slog.Default(), request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_correct_az.go b/internal/scheduling/nova/plugins/filters/filter_correct_az.go index 94311bd82..ed7f68188 100644 --- a/internal/scheduling/nova/plugins/filters/filter_correct_az.go +++ b/internal/scheduling/nova/plugins/filters/filter_correct_az.go @@ -18,7 +18,7 @@ type FilterCorrectAZStep struct { } // Only get hosts in the requested az. -func (s *FilterCorrectAZStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterCorrectAZStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) if request.Spec.Data.AvailabilityZone == "" { traceLog.Info("no availability zone requested, skipping filter_correct_az step") diff --git a/internal/scheduling/nova/plugins/filters/filter_correct_az_test.go b/internal/scheduling/nova/plugins/filters/filter_correct_az_test.go index 4ac16cfa3..d8389de9e 100644 --- a/internal/scheduling/nova/plugins/filters/filter_correct_az_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_correct_az_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -170,7 +169,7 @@ func TestFilterCorrectAZStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_exclude_hosts.go b/internal/scheduling/nova/plugins/filters/filter_exclude_hosts.go index 1a68602a2..231efa9aa 100644 --- a/internal/scheduling/nova/plugins/filters/filter_exclude_hosts.go +++ b/internal/scheduling/nova/plugins/filters/filter_exclude_hosts.go @@ -30,7 +30,6 @@ func (opts FilterExcludeHostsStepOpts) Validate() error { return nil } func (s *FilterExcludeHostsStep) Run( traceLog *slog.Logger, request api.ExternalSchedulerRequest, - opts lib.Options, ) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) diff --git a/internal/scheduling/nova/plugins/filters/filter_exclude_hosts_test.go b/internal/scheduling/nova/plugins/filters/filter_exclude_hosts_test.go index 42c0ab200..0c9e35c59 100644 --- a/internal/scheduling/nova/plugins/filters/filter_exclude_hosts_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_exclude_hosts_test.go @@ -218,7 +218,7 @@ func TestFilterExcludeHostsStep_Run(t *testing.T) { ExcludedHosts: tt.excludedHosts, } - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_external_customer.go b/internal/scheduling/nova/plugins/filters/filter_external_customer.go index b96f76b6b..827712a84 100644 --- a/internal/scheduling/nova/plugins/filters/filter_external_customer.go +++ b/internal/scheduling/nova/plugins/filters/filter_external_customer.go @@ -33,7 +33,7 @@ type FilterExternalCustomerStep struct { // Prefix-match the domain name for external customer domains and filter out hosts // that are not intended for external customers. -func (s *FilterExternalCustomerStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterExternalCustomerStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) domainName, err := request.Spec.Data.GetSchedulerHintStr("domain_name") if err != nil { diff --git a/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go b/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go index 9a46f31ee..05bdbc6f6 100644 --- a/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_external_customer_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -359,7 +358,7 @@ func TestFilterExternalCustomerStep_Run(t *testing.T) { Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if tt.expectError { if err == nil { t.Errorf("expected error but got none") diff --git a/internal/scheduling/nova/plugins/filters/filter_has_accelerators.go b/internal/scheduling/nova/plugins/filters/filter_has_accelerators.go index 2c61ad588..dcccdc010 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_accelerators.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_accelerators.go @@ -18,7 +18,7 @@ type FilterHasAcceleratorsStep struct { } // If requested, only get hosts with accelerators. -func (s *FilterHasAcceleratorsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterHasAcceleratorsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) extraSpecs := request.Spec.Data.Flavor.Data.ExtraSpecs if _, ok := extraSpecs["accel:device_profile"]; !ok { diff --git a/internal/scheduling/nova/plugins/filters/filter_has_accelerators_test.go b/internal/scheduling/nova/plugins/filters/filter_has_accelerators_test.go index 1008ae17f..1d1a06764 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_accelerators_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_accelerators_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -348,7 +347,7 @@ func TestFilterHasAcceleratorsStep_Run(t *testing.T) { WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go index 343554d82..b01976a93 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go @@ -57,7 +57,8 @@ type FilterHasEnoughCapacity struct { // known at this point. // // Please also note that disk space is currently not considered by this filter. -func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { + opts := request.GetOptions() result := s.IncludeAllHostsFromRequest(request) // This map holds the free resources per host. diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go index fbbd21587..f6f3689b9 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go @@ -584,7 +584,7 @@ func TestFilterHasEnoughCapacity_ReservationTypes(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -799,7 +799,7 @@ func TestFilterHasEnoughCapacity_IgnoredReservationTypes(t *testing.T) { IgnoredReservationTypes: tt.ignoredReservationTypes, } - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -836,9 +836,10 @@ func TestFilterHasEnoughCapacity_IgnoredReservationTypes_CallTime(t *testing.T) step.Options = FilterHasEnoughCapacityOpts{LockReserved: true} // no YAML-level ignores // Call-time: ignore CR reservations → host1 passes, host2 still blocked by failover. - result, err := step.Run(slog.Default(), request, lib.Options{ + request.Options = lib.Options{ IgnoredReservationTypes: []v1alpha1.ReservationType{v1alpha1.ReservationTypeCommittedResource}, - }) + } + result, err := step.Run(slog.Default(), request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -1003,8 +1004,9 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) step := &FilterHasEnoughCapacity{} step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts + tt.request.Options = tt.pipelineOpts - result, err := step.Run(slog.Default(), tt.request, tt.pipelineOpts) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -1054,7 +1056,7 @@ func TestFilterHasEnoughCapacity_PlannedCRDoesNotBlock(t *testing.T) { step.Options = FilterHasEnoughCapacityOpts{LockReserved: false} request := newNovaRequest("instance-123", "project-A", "m1.large", "gp-1", 4, "8Gi", false, []string{"host1"}) - result, err := step.Run(slog.Default(), request, lib.Options{}) + result, err := step.Run(slog.Default(), request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -1127,7 +1129,7 @@ func TestFilterHasEnoughCapacity_NilEffectiveCapacityFallback(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = FilterHasEnoughCapacityOpts{LockReserved: false} - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } @@ -1326,7 +1328,7 @@ func TestFilterHasEnoughCapacity_VMInterReservationMigration(t *testing.T) { step.Options = FilterHasEnoughCapacityOpts{LockReserved: false} request := newNovaRequest("instance-new", thirdParty, "m1.small", flavorGroup, 3, "6Gi", false, []string{"hv-a", "hv-b"}) - result, err := step.Run(slog.Default(), request, lib.Options{}) + result, err := step.Run(slog.Default(), request) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_has_requested_traits.go b/internal/scheduling/nova/plugins/filters/filter_has_requested_traits.go index 53f050db5..aa35d2fc9 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_requested_traits.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_requested_traits.go @@ -21,7 +21,7 @@ type FilterHasRequestedTraits struct { // Filter hosts that do not have the requested traits given by the extra spec: // - "trait:": "forbidden" means the host must not have the specified trait. // - "trait:": "required" means the host must have the specified trait. -func (s *FilterHasRequestedTraits) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterHasRequestedTraits) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) var requiredTraits, forbiddenTraits []string for key, value := range request.Spec.Data.Flavor.Data.ExtraSpecs { diff --git a/internal/scheduling/nova/plugins/filters/filter_has_requested_traits_test.go b/internal/scheduling/nova/plugins/filters/filter_has_requested_traits_test.go index edd7bb9a8..10a7c94aa 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_requested_traits_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_requested_traits_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -462,7 +461,7 @@ func TestFilterHasRequestedTraits_Run(t *testing.T) { WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_host_instructions.go b/internal/scheduling/nova/plugins/filters/filter_host_instructions.go index 6e6a13da8..dafb6675f 100644 --- a/internal/scheduling/nova/plugins/filters/filter_host_instructions.go +++ b/internal/scheduling/nova/plugins/filters/filter_host_instructions.go @@ -18,7 +18,7 @@ type FilterHostInstructionsStep struct { // Filter hosts based on instructions given in the request spec. Supported are: // - spec.ignore_hosts: Filter out all hosts in this list. // - spec.force_hosts: Include only hosts in this list. -func (s *FilterHostInstructionsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterHostInstructionsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) if request.Spec.Data.IgnoreHosts != nil { for _, host := range *request.Spec.Data.IgnoreHosts { diff --git a/internal/scheduling/nova/plugins/filters/filter_host_instructions_test.go b/internal/scheduling/nova/plugins/filters/filter_host_instructions_test.go index a09e12a2e..10bcb60c9 100644 --- a/internal/scheduling/nova/plugins/filters/filter_host_instructions_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_host_instructions_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -332,7 +331,7 @@ func TestFilterHostInstructionsStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { step := &FilterHostInstructionsStep{} - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity.go b/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity.go index 41eb5181e..326864b9d 100644 --- a/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity.go +++ b/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity.go @@ -19,7 +19,6 @@ type FilterInstanceGroupAffinityStep struct { func (s *FilterInstanceGroupAffinityStep) Run( traceLog *slog.Logger, request api.ExternalSchedulerRequest, - opts lib.Options, ) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) diff --git a/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity_test.go b/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity_test.go index 25e0c224d..7321747e3 100644 --- a/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_instance_group_affinity_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -327,7 +326,7 @@ func TestFilterInstanceGroupAffinityStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { step := &FilterInstanceGroupAffinityStep{} - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity.go b/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity.go index 17a9a8735..0dee29d9e 100644 --- a/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity.go +++ b/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity.go @@ -22,7 +22,6 @@ type FilterInstanceGroupAntiAffinityStep struct { func (s *FilterInstanceGroupAntiAffinityStep) Run( traceLog *slog.Logger, request api.ExternalSchedulerRequest, - opts lib.Options, ) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) diff --git a/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity_test.go b/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity_test.go index e70ada792..6eea6bc7f 100644 --- a/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_instance_group_anti_affinity_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -520,7 +519,7 @@ func TestFilterInstanceGroupAntiAffinityStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/filters/filter_live_migratable.go b/internal/scheduling/nova/plugins/filters/filter_live_migratable.go index 32c467fe8..a19238721 100644 --- a/internal/scheduling/nova/plugins/filters/filter_live_migratable.go +++ b/internal/scheduling/nova/plugins/filters/filter_live_migratable.go @@ -51,7 +51,6 @@ func (s *FilterLiveMigratableStep) checkHasSufficientFeatures( func (s *FilterLiveMigratableStep) Run( traceLog *slog.Logger, request api.ExternalSchedulerRequest, - opts lib.Options, ) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) diff --git a/internal/scheduling/nova/plugins/filters/filter_live_migratable_test.go b/internal/scheduling/nova/plugins/filters/filter_live_migratable_test.go index c4cb9df9c..c5651b025 100644 --- a/internal/scheduling/nova/plugins/filters/filter_live_migratable_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_live_migratable_test.go @@ -641,7 +641,7 @@ func TestFilterLiveMigratableStep_Run(t *testing.T) { }, } - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if tt.expectErr { if err == nil { @@ -728,7 +728,7 @@ func TestFilterLiveMigratableStep_Run_SourceHostNotFound(t *testing.T) { }, } - _, err := step.Run(slog.Default(), request, lib.Options{}) + _, err := step.Run(slog.Default(), request) if err == nil { t.Errorf("expected error when source host not found, got none") } @@ -774,7 +774,7 @@ func TestFilterLiveMigratableStep_Run_ClientError(t *testing.T) { }, } - _, err := step.Run(slog.Default(), request, lib.Options{}) + _, err := step.Run(slog.Default(), request) if err == nil { t.Errorf("expected error when client fails, got none") } diff --git a/internal/scheduling/nova/plugins/filters/filter_requested_destination.go b/internal/scheduling/nova/plugins/filters/filter_requested_destination.go index 83c0d5521..8922ab8c4 100644 --- a/internal/scheduling/nova/plugins/filters/filter_requested_destination.go +++ b/internal/scheduling/nova/plugins/filters/filter_requested_destination.go @@ -100,7 +100,7 @@ func (s *FilterRequestedDestinationStep) processRequestedHost( // The requested destination can include a specific host, aggregates, or both. // When both are specified, aggregate filtering is applied first, followed by // host filtering. -func (s *FilterRequestedDestinationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterRequestedDestinationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) rd := request.Spec.Data.RequestedDestination if rd == nil { diff --git a/internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go b/internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go index e85d38a42..5a752160e 100644 --- a/internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go @@ -578,7 +578,7 @@ func TestFilterRequestedDestinationStep_Run(t *testing.T) { }, } - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if tt.expectErr { if err == nil { @@ -777,7 +777,7 @@ func TestFilterRequestedDestinationStep_Run_ClientError(t *testing.T) { }, } - _, err := step.Run(slog.Default(), request, lib.Options{}) + _, err := step.Run(slog.Default(), request) if err == nil { t.Errorf("expected error when client fails, got none") } diff --git a/internal/scheduling/nova/plugins/filters/filter_status_conditions.go b/internal/scheduling/nova/plugins/filters/filter_status_conditions.go index 05bbbdcc4..3d7f2aae6 100644 --- a/internal/scheduling/nova/plugins/filters/filter_status_conditions.go +++ b/internal/scheduling/nova/plugins/filters/filter_status_conditions.go @@ -20,7 +20,7 @@ type FilterStatusConditionsStep struct { // Check that all status conditions meet the expected values, for example, // that the hypervisor is ready and not disabled. -func (s *FilterStatusConditionsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *FilterStatusConditionsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) hvs := &hv1.HypervisorList{} diff --git a/internal/scheduling/nova/plugins/filters/filter_status_conditions_test.go b/internal/scheduling/nova/plugins/filters/filter_status_conditions_test.go index 91cc67489..adbfc8c65 100644 --- a/internal/scheduling/nova/plugins/filters/filter_status_conditions_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_status_conditions_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -342,7 +341,7 @@ func TestFilterStatusConditionsStep_Run(t *testing.T) { WithScheme(scheme). WithObjects(hvs...). Build() - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/kvm_binpack.go b/internal/scheduling/nova/plugins/weighers/kvm_binpack.go index 717f3e667..e1509a4cc 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_binpack.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_binpack.go @@ -69,7 +69,7 @@ type KVMBinpackStep struct { } // Run this weigher in the pipeline after filters have been executed. -func (s *KVMBinpackStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMBinpackStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["binpack score"] = s.PrepareStats(request, "float") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go b/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go index ae7ff0a6d..69e1aa9f6 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_binpack_test.go @@ -4,7 +4,6 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" @@ -449,7 +448,7 @@ func TestKVMBinpackStep_Run(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation.go index 8e463ddc9..dcbcbf8bd 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation.go @@ -50,7 +50,7 @@ type KVMFailoverEvacuationStep struct { // Run the weigher step. // For evacuation requests, hosts matching a failover reservation where the VM is in Allocations get a higher weight. // For non-evacuation requests (e.g., live migration, rebuild), this weigher has no effect. -func (s *KVMFailoverEvacuationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMFailoverEvacuationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) intent, err := request.GetIntent() diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go index eb7925c71..0664e55d4 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_evacuation_test.go @@ -4,7 +4,6 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -265,7 +264,7 @@ func TestKVMFailoverEvacuationStep_Run(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation.go index 754db1be4..727afce33 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation.go @@ -78,7 +78,7 @@ type KVMFailoverReservationConsolidationStep struct { // Run the weigher step. // For reserve_for_failover requests, hosts are scored based on existing failover reservation density // and same-spec diversity. For all other request types, this weigher has no effect. -func (s *KVMFailoverReservationConsolidationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMFailoverReservationConsolidationStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) intent, err := request.GetIntent() diff --git a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go index a065d1ef8..62d69d319 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_failover_reservation_consolidation_test.go @@ -4,7 +4,6 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "math" "testing" @@ -257,7 +256,7 @@ func TestKVMFailoverReservationConsolidationStep_Run(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity.go b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity.go index 95c35fe92..5f13897f0 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity.go @@ -26,7 +26,7 @@ type KVMInstanceGroupSoftAffinityStep struct { lib.BaseWeigher[api.ExternalSchedulerRequest, lib.EmptyFilterWeigherPipelineStepOpts] } -func (s *KVMInstanceGroupSoftAffinityStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMInstanceGroupSoftAffinityStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["affinity"] = s.PrepareStats(request, "float") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go index b0817b737..fcf13f86b 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_instance_group_soft_affinity_test.go @@ -4,7 +4,6 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -347,7 +346,7 @@ func TestKVMInstanceGroupSoftAffinityStep_Run(t *testing.T) { step := &KVMInstanceGroupSoftAffinityStep{} step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts.go b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts.go index 88fbd2ca9..b65a5f75f 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts.go @@ -57,7 +57,7 @@ type KVMPreferSmallerHostsStep struct { } // Run this weigher in the pipeline after filters have been executed. -func (s *KVMPreferSmallerHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *KVMPreferSmallerHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["small host score"] = s.PrepareStats(request, "float") diff --git a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go index 6f306b414..2ab2deb89 100644 --- a/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/kvm_prefer_smaller_hosts_test.go @@ -4,7 +4,6 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" @@ -600,7 +599,7 @@ func TestKVMPreferSmallerHostsStep_Run(t *testing.T) { step.Client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(objects...).Build() step.Options = tt.opts - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if tt.wantErr { if err == nil { t.Fatalf("expected error, got nil") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects.go b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects.go index 59a485d07..b2b886d49 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects.go @@ -52,7 +52,7 @@ func (s *VMwareAntiAffinityNoisyProjectsStep) Init(ctx context.Context, client c } // Downvote the hosts a project is currently running on if it's noisy. -func (s *VMwareAntiAffinityNoisyProjectsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *VMwareAntiAffinityNoisyProjectsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["avg cpu usage of this project"] = s.PrepareStats(request, "%") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go index 887982049..304ab0612 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_anti_affinity_noisy_projects_test.go @@ -5,7 +5,6 @@ package weighers import ( "context" - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" @@ -274,7 +273,7 @@ func TestVMwareAntiAffinityNoisyProjectsStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts.go index 8f46dc5d8..14905cf00 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts.go @@ -61,7 +61,7 @@ func (s *VMwareAvoidLongTermContendedHostsStep) Init(ctx context.Context, client } // Downvote hosts that are highly contended. -func (s *VMwareAvoidLongTermContendedHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *VMwareAvoidLongTermContendedHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["avg cpu contention"] = s.PrepareStats(request, "%") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go index e3a62423c..5a69cdaf7 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_long_term_contended_hosts_test.go @@ -5,7 +5,6 @@ package weighers import ( "context" - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" @@ -257,7 +256,7 @@ func TestVMwareAvoidLongTermContendedHostsStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts.go index f2d7896f0..fd9e81335 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts.go @@ -61,7 +61,7 @@ func (s *VMwareAvoidShortTermContendedHostsStep) Init(ctx context.Context, clien } // Downvote hosts that are highly contended. -func (s *VMwareAvoidShortTermContendedHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *VMwareAvoidShortTermContendedHostsStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["avg cpu contention"] = s.PrepareStats(request, "%") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go index 9de020c95..0dfe280d0 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_avoid_short_term_contended_hosts_test.go @@ -5,7 +5,6 @@ package weighers import ( "context" - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "strings" "testing" @@ -257,7 +256,7 @@ func TestVMwareAvoidShortTermContendedHostsStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/nova/plugins/weighers/vmware_binpack.go b/internal/scheduling/nova/plugins/weighers/vmware_binpack.go index adb6f86bc..217dc7d7f 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_binpack.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_binpack.go @@ -85,7 +85,7 @@ func (s *VMwareBinpackStep) Init(ctx context.Context, client client.Client, weig } // Run this weigher in the pipeline after filters have been executed. -func (s *VMwareBinpackStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *VMwareBinpackStep) Run(traceLog *slog.Logger, request api.ExternalSchedulerRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) result.Statistics["binpack score"] = s.PrepareStats(request, "float") diff --git a/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go b/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go index d274ef40d..cdca6c569 100644 --- a/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go +++ b/internal/scheduling/nova/plugins/weighers/vmware_binpack_test.go @@ -4,7 +4,6 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -212,7 +211,7 @@ func TestVMwareBinpackStep_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result, err := step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } diff --git a/internal/scheduling/pods/plugins/filters/filter_node_affinity.go b/internal/scheduling/pods/plugins/filters/filter_node_affinity.go index c8953ec1b..996897bdb 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_affinity.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_affinity.go @@ -27,7 +27,7 @@ func (f *NodeAffinityFilter) Validate(ctx context.Context, params v1alpha1.Param return nil } -func (NodeAffinityFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (NodeAffinityFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_affinity_test.go b/internal/scheduling/pods/plugins/filters/filter_node_affinity_test.go index 1442994c2..0172d60d7 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_affinity_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_affinity_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -400,7 +399,7 @@ func TestNodeAffinityFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NodeAffinityFilter{} - result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) + result, err := filter.Run(slog.Default(), tt.request) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_available.go b/internal/scheduling/pods/plugins/filters/filter_node_available.go index cfbccfa28..2d6e11d22 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_available.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_available.go @@ -26,7 +26,7 @@ func (f *NodeAvailableFilter) Validate(ctx context.Context, params v1alpha1.Para return nil } -func (NodeAvailableFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (NodeAvailableFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_available_test.go b/internal/scheduling/pods/plugins/filters/filter_node_available_test.go index e9966d594..3eac7873c 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_available_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_available_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -310,7 +309,7 @@ func TestNodeAvailableFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NodeAvailableFilter{} - result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) + result, err := filter.Run(slog.Default(), tt.request) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_capacity.go b/internal/scheduling/pods/plugins/filters/filter_node_capacity.go index 6d2f0eae7..cfceb8835 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_capacity.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_capacity.go @@ -27,7 +27,7 @@ func (f *NodeCapacityFilter) Validate(ctx context.Context, params v1alpha1.Param return nil } -func (NodeCapacityFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (NodeCapacityFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) diff --git a/internal/scheduling/pods/plugins/filters/filter_node_capacity_test.go b/internal/scheduling/pods/plugins/filters/filter_node_capacity_test.go index 950adace7..543b4561d 100644 --- a/internal/scheduling/pods/plugins/filters/filter_node_capacity_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_node_capacity_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -352,7 +351,7 @@ func TestNodeCapacityFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NodeCapacityFilter{} - result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) + result, err := filter.Run(slog.Default(), tt.request) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/filters/filter_noop.go b/internal/scheduling/pods/plugins/filters/filter_noop.go index fda0510f6..e0666537b 100644 --- a/internal/scheduling/pods/plugins/filters/filter_noop.go +++ b/internal/scheduling/pods/plugins/filters/filter_noop.go @@ -31,7 +31,7 @@ func (f *NoopFilter) Validate(ctx context.Context, params v1alpha1.Parameters) e // not in the map are considered as filtered out. // Provide a traceLog that contains the global request id and should // be used to log the step's execution. -func (NoopFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (NoopFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64, len(request.Nodes)) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) // Usually you would do some filtering here, or adjust the weights. diff --git a/internal/scheduling/pods/plugins/filters/filter_noop_test.go b/internal/scheduling/pods/plugins/filters/filter_noop_test.go index 71e22f72f..dcdd90a69 100644 --- a/internal/scheduling/pods/plugins/filters/filter_noop_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_noop_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -86,7 +85,7 @@ func TestNoopFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &NoopFilter{} - result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) + result, err := filter.Run(slog.Default(), tt.request) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/filters/filter_taint.go b/internal/scheduling/pods/plugins/filters/filter_taint.go index f349c5213..5a54d1cb6 100644 --- a/internal/scheduling/pods/plugins/filters/filter_taint.go +++ b/internal/scheduling/pods/plugins/filters/filter_taint.go @@ -26,7 +26,7 @@ func (f *TaintFilter) Validate(ctx context.Context, params v1alpha1.Parameters) return nil } -func (TaintFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (TaintFilter) Run(traceLog *slog.Logger, request pods.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { activations := make(map[string]float64) stats := make(map[string]lib.FilterWeigherPipelineStepStatistics) diff --git a/internal/scheduling/pods/plugins/filters/filter_taint_test.go b/internal/scheduling/pods/plugins/filters/filter_taint_test.go index 258f78ef0..1d248b685 100644 --- a/internal/scheduling/pods/plugins/filters/filter_taint_test.go +++ b/internal/scheduling/pods/plugins/filters/filter_taint_test.go @@ -4,7 +4,6 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "testing" @@ -253,7 +252,7 @@ func TestTaintFilter_Run(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { filter := &TaintFilter{} - result, err := filter.Run(slog.Default(), tt.request, lib.Options{}) + result, err := filter.Run(slog.Default(), tt.request) if err != nil { t.Errorf("expected Run() to succeed, got error: %v", err) diff --git a/internal/scheduling/pods/plugins/weighers/binpack.go b/internal/scheduling/pods/plugins/weighers/binpack.go index d0c8e2cf9..07d310fb8 100644 --- a/internal/scheduling/pods/plugins/weighers/binpack.go +++ b/internal/scheduling/pods/plugins/weighers/binpack.go @@ -31,7 +31,7 @@ type BinpackingStep struct { lib.BaseWeigher[api.PodPipelineRequest, BinpackingStepOpts] } -func (s *BinpackingStep) Run(traceLog *slog.Logger, request api.PodPipelineRequest, opts lib.Options) (*lib.FilterWeigherPipelineStepResult, error) { +func (s *BinpackingStep) Run(traceLog *slog.Logger, request api.PodPipelineRequest) (*lib.FilterWeigherPipelineStepResult, error) { result := s.IncludeAllHostsFromRequest(request) podResources := helpers.GetPodResourceRequests(request.Pod) diff --git a/internal/scheduling/pods/plugins/weighers/binpack_test.go b/internal/scheduling/pods/plugins/weighers/binpack_test.go index 82838909f..198e110c1 100644 --- a/internal/scheduling/pods/plugins/weighers/binpack_test.go +++ b/internal/scheduling/pods/plugins/weighers/binpack_test.go @@ -4,7 +4,6 @@ package weighers import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "log/slog" "math" "testing" @@ -258,7 +257,7 @@ func TestBinpackingStep_Run(t *testing.T) { }, } - result, err := tt.step.Run(slog.Default(), tt.request, lib.Options{}) + result, err := tt.step.Run(slog.Default(), tt.request) if err != nil { t.Fatalf("expected no error, got %v", err) } From c2f0b5698e58c420efeff49fea986219465dcd1c Mon Sep 17 00:00:00 2001 From: mblos Date: Wed, 6 May 2026 13:04:36 +0200 Subject: [PATCH 08/19] comments --- internal/scheduling/lib/filter_weigher_pipeline.go | 6 +++++- .../scheduling/lib/filter_weigher_pipeline_test.go | 10 ++-------- internal/scheduling/lib/options.go | 3 --- .../nova/filter_weigher_pipeline_controller.go | 2 ++ 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/internal/scheduling/lib/filter_weigher_pipeline.go b/internal/scheduling/lib/filter_weigher_pipeline.go index e2b9ce468..d12b566ad 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline.go +++ b/internal/scheduling/lib/filter_weigher_pipeline.go @@ -306,8 +306,12 @@ func (p *filterWeigherPipeline[RequestType]) Run(request RequestType) (v1alpha1. traceLog.Info("scheduler: trimming candidate list", "maxCandidates", opts.MaxCandidates, "before", len(hosts)) hosts = hosts[:opts.MaxCandidates] // Drop trimmed hosts from outWeights so AggregatedOutWeights stays consistent. + kept := make(map[string]struct{}, len(hosts)) + for _, h := range hosts { + kept[h] = struct{}{} + } for host := range outWeights { - if !slices.Contains(hosts, host) { + if _, ok := kept[host]; !ok { delete(outWeights, host) } } diff --git a/internal/scheduling/lib/filter_weigher_pipeline_test.go b/internal/scheduling/lib/filter_weigher_pipeline_test.go index 54251731c..a110aeec3 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_test.go @@ -7,6 +7,7 @@ import ( "context" "log/slog" "math" + "slices" "testing" "github.com/cobaltcore-dev/cortex/api/v1alpha1" @@ -415,14 +416,7 @@ func TestPipeline_MaxCandidates(t *testing.T) { if tt.maxCandidates > 0 && len(result.OrderedHosts) <= tt.maxCandidates { // AggregatedOutWeights must only contain returned hosts. for host := range result.AggregatedOutWeights { - found := false - for _, h := range result.OrderedHosts { - if h == host { - found = true - break - } - } - if !found { + if !slices.Contains(result.OrderedHosts, host) { t.Errorf("AggregatedOutWeights contains trimmed host %s", host) } } diff --git a/internal/scheduling/lib/options.go b/internal/scheduling/lib/options.go index 44445cf6d..c4e43080b 100644 --- a/internal/scheduling/lib/options.go +++ b/internal/scheduling/lib/options.go @@ -12,9 +12,6 @@ import ( // Options configure the behavior of a single pipeline run at call time. // These are distinct from per-step YAML options (FilterWeigherPipelineStepOpts), // which are static and set when the pipeline is initialized. -// -// Consumed by steps: ReadOnly, LockReservations, AssumeEmptyHosts, IgnoredReservationTypes. -// Consumed by the controller after pipeline.Run(): RecordHistory, CreateInflight. type Options struct { // ReadOnly means the pipeline run does not modify shared scheduling state (reservations, // history, inflight records). Concurrent read-only runs are safe under a shared read lock. diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller.go b/internal/scheduling/nova/filter_weigher_pipeline_controller.go index 703f6d23f..40252fdc2 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller.go @@ -84,6 +84,8 @@ func (c *FilterWeigherPipelineController) Reconcile(ctx context.Context, req ctr // Process the decision from the API. Should create and return the updated decision. func (c *FilterWeigherPipelineController) ProcessNewDecisionFromAPI(ctx context.Context, decision *v1alpha1.Decision) error { + // Read-only runs share the cached decision state; no re-fetch needed because they + // don't observe writes from concurrent exclusive-lock runs. if c.peekReadOnly(decision) { c.processMu.RLock() defer c.processMu.RUnlock() From 7a014b2fa47dbab07d1f1fa381d5ad2c8d7079e5 Mon Sep 17 00:00:00 2001 From: mblos Date: Wed, 6 May 2026 13:40:20 +0200 Subject: [PATCH 09/19] . --- internal/scheduling/lib/options.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/scheduling/lib/options.go b/internal/scheduling/lib/options.go index c4e43080b..613a9ca2a 100644 --- a/internal/scheduling/lib/options.go +++ b/internal/scheduling/lib/options.go @@ -16,22 +16,22 @@ type Options struct { // ReadOnly means the pipeline run does not modify shared scheduling state (reservations, // history, inflight records). Concurrent read-only runs are safe under a shared read lock. // Note: the controller may still write the Decision status after Run() regardless of this flag. - ReadOnly bool + ReadOnly bool `json:"read_only,omitempty"` // LockReservations prevents reservation unlocking, e.g. in the capacity filter. // Set when finding hosts for new reservations (failover, CR) to see true available capacity. - LockReservations bool + LockReservations bool `json:"lock_reservations,omitempty"` // AssumeEmptyHosts treats all hosts as having no running VMs. - AssumeEmptyHosts bool + AssumeEmptyHosts bool `json:"assume_empty_hosts,omitempty"` // IgnoredReservationTypes lists reservation types the capacity filter skips entirely. - IgnoredReservationTypes []v1alpha1.ReservationType + IgnoredReservationTypes []v1alpha1.ReservationType `json:"ignored_reservation_types,omitempty"` // MaxCandidates limits the number of hosts returned after weighing. 0 means no limit. - MaxCandidates int + MaxCandidates int `json:"max_candidates,omitempty"` // RecordHistory records the placement decision in placement history. // Replaces pipeline.Spec.CreateHistory once pipelines consolidate. - RecordHistory bool + RecordHistory bool `json:"record_history,omitempty"` // CreateInflight creates pessimistic blocking reservations for all returned candidates. - CreateInflight bool + CreateInflight bool `json:"create_inflight,omitempty"` } // Validate checks for mutually exclusive or inconsistent option combinations. From cc9b6e6aa5b387faa8eac29a887f9682328c51f4 Mon Sep 17 00:00:00 2001 From: mblos Date: Thu, 7 May 2026 09:35:54 +0200 Subject: [PATCH 10/19] refactor CreateHistory --- api/v1alpha1/pipeline_types.go | 5 ----- .../cinder/filter_weigher_pipeline_controller.go | 15 +++++---------- .../filter_weigher_pipeline_controller_test.go | 3 --- internal/scheduling/lib/options.go | 10 +++++----- internal/scheduling/lib/options_test.go | 8 ++++---- .../filter_weigher_pipeline_controller.go | 15 +++++---------- .../filter_weigher_pipeline_controller_test.go | 3 --- .../manila/filter_weigher_pipeline_controller.go | 15 +++++---------- .../filter_weigher_pipeline_controller_test.go | 3 --- .../nova/filter_weigher_pipeline_controller.go | 6 +----- .../filter_weigher_pipeline_controller_test.go | 9 --------- .../pods/filter_weigher_pipeline_controller.go | 15 +++++---------- .../filter_weigher_pipeline_controller_test.go | 3 --- .../commitments/reservation_controller.go | 2 +- 14 files changed, 31 insertions(+), 81 deletions(-) diff --git a/api/v1alpha1/pipeline_types.go b/api/v1alpha1/pipeline_types.go index 180db85e5..1e5cfdca3 100644 --- a/api/v1alpha1/pipeline_types.go +++ b/api/v1alpha1/pipeline_types.go @@ -78,11 +78,6 @@ type PipelineSpec struct { // +kubebuilder:validation:Optional Description string `json:"description,omitempty"` - // If this pipeline should create history objects. - // When this is false, the pipeline will still process requests. - // +kubebuilder:default=false - CreateHistory bool `json:"createHistory,omitempty"` - // If this pipeline should ignore host preselection and gather all // available placement candidates before applying filters, instead of // relying on a pre-filtered set and weights. diff --git a/internal/scheduling/cinder/filter_weigher_pipeline_controller.go b/internal/scheduling/cinder/filter_weigher_pipeline_controller.go index 52ec37306..99abfc1ba 100644 --- a/internal/scheduling/cinder/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/cinder/filter_weigher_pipeline_controller.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "sync" "time" @@ -74,10 +73,6 @@ func (c *FilterWeigherPipelineController) ProcessNewDecisionFromAPI(ctx context. c.processMu.Lock() defer c.processMu.Unlock() - pipelineConf, ok := c.PipelineConfigs[decision.Spec.PipelineRef.Name] - if !ok { - return fmt.Errorf("pipeline %s not configured", decision.Spec.PipelineRef.Name) - } err := c.process(ctx, decision) if err != nil { meta.SetStatusCondition(&decision.Status.Conditions, metav1.Condition{ @@ -94,11 +89,6 @@ func (c *FilterWeigherPipelineController) ProcessNewDecisionFromAPI(ctx context. Message: "pipeline run succeeded", }) } - if pipelineConf.Spec.CreateHistory { - if upsertErr := c.HistoryManager.CreateOrUpdateHistory(ctx, decision, nil, err); upsertErr != nil { - ctrl.LoggerFrom(ctx).Error(upsertErr, "failed to create/update history") - } - } return err } @@ -122,6 +112,11 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision } result, err := pipeline.Run(request) + if !request.Options.SkipHistory { + if upsertErr := c.HistoryManager.CreateOrUpdateHistory(ctx, decision, nil, err); upsertErr != nil { + log.Error(upsertErr, "failed to create/update history") + } + } if err != nil { log.Error(err, "failed to run pipeline") return err diff --git a/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go b/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go index e51dfec87..11798a11c 100644 --- a/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go @@ -281,7 +281,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainCinder, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -315,7 +314,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainCinder, - CreateHistory: false, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -369,7 +367,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainCinder, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, diff --git a/internal/scheduling/lib/options.go b/internal/scheduling/lib/options.go index 613a9ca2a..84c4ecde0 100644 --- a/internal/scheduling/lib/options.go +++ b/internal/scheduling/lib/options.go @@ -27,17 +27,17 @@ type Options struct { // MaxCandidates limits the number of hosts returned after weighing. 0 means no limit. MaxCandidates int `json:"max_candidates,omitempty"` - // RecordHistory records the placement decision in placement history. - // Replaces pipeline.Spec.CreateHistory once pipelines consolidate. - RecordHistory bool `json:"record_history,omitempty"` + // SkipHistory skips recording the placement decision in placement history. + // Defaults to false so Nova requests record history without needing to set this explicitly. + SkipHistory bool `json:"skip_history,omitempty"` // CreateInflight creates pessimistic blocking reservations for all returned candidates. CreateInflight bool `json:"create_inflight,omitempty"` } // Validate checks for mutually exclusive or inconsistent option combinations. func (o Options) Validate() error { - if o.ReadOnly && o.RecordHistory { - return errors.New("ReadOnly and RecordHistory are mutually exclusive: read-only runs must not write scheduling history") + if o.ReadOnly && !o.SkipHistory { + return errors.New("read-only runs must not write scheduling history: set SkipHistory=true") } if o.ReadOnly && o.CreateInflight { return errors.New("ReadOnly and CreateInflight are mutually exclusive: read-only runs must not create inflight reservations") diff --git a/internal/scheduling/lib/options_test.go b/internal/scheduling/lib/options_test.go index 6eb366b0c..cba9304aa 100644 --- a/internal/scheduling/lib/options_test.go +++ b/internal/scheduling/lib/options_test.go @@ -12,12 +12,12 @@ func TestOptions_Validate(t *testing.T) { wantErr bool }{ {"zero value is valid", Options{}, false}, - {"write run with history", Options{RecordHistory: true}, false}, + {"write run, history recorded by default", Options{}, false}, {"write run with inflight", Options{CreateInflight: true}, false}, - {"read-only run, no side effects", Options{ReadOnly: true}, false}, - {"ReadOnly + RecordHistory is invalid", Options{ReadOnly: true, RecordHistory: true}, true}, + {"read-only run, skipping history", Options{ReadOnly: true, SkipHistory: true}, false}, + {"ReadOnly without SkipHistory is invalid", Options{ReadOnly: true}, true}, {"ReadOnly + CreateInflight is invalid", Options{ReadOnly: true, CreateInflight: true}, true}, - {"ReadOnly + both invalid", Options{ReadOnly: true, RecordHistory: true, CreateInflight: true}, true}, + {"ReadOnly + both invalid", Options{ReadOnly: true, CreateInflight: true}, true}, } for _, tt := range tests { diff --git a/internal/scheduling/machines/filter_weigher_pipeline_controller.go b/internal/scheduling/machines/filter_weigher_pipeline_controller.go index 35d51708a..29627814f 100644 --- a/internal/scheduling/machines/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/machines/filter_weigher_pipeline_controller.go @@ -6,7 +6,6 @@ package machines import ( "context" "errors" - "fmt" "sync" "time" @@ -95,10 +94,6 @@ func (c *FilterWeigherPipelineController) ProcessNewMachine(ctx context.Context, }, } - pipelineConf, ok := c.PipelineConfigs[decision.Spec.PipelineRef.Name] - if !ok { - return fmt.Errorf("pipeline %s not configured", decision.Spec.PipelineRef.Name) - } err := c.process(ctx, decision) if err != nil { meta.SetStatusCondition(&decision.Status.Conditions, metav1.Condition{ @@ -115,11 +110,6 @@ func (c *FilterWeigherPipelineController) ProcessNewMachine(ctx context.Context, Message: "pipeline run succeeded", }) } - if pipelineConf.Spec.CreateHistory { - if upsertErr := c.HistoryManager.CreateOrUpdateHistory(ctx, decision, nil, err); upsertErr != nil { - ctrl.LoggerFrom(ctx).Error(upsertErr, "failed to create/update history") - } - } return err } @@ -145,6 +135,11 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision // Execute the scheduling pipeline. request := ironcore.MachinePipelineRequest{Pools: pools.Items} result, err := pipeline.Run(request) + if !request.Options.SkipHistory { + if upsertErr := c.HistoryManager.CreateOrUpdateHistory(ctx, decision, nil, err); upsertErr != nil { + log.Error(upsertErr, "failed to create/update history") + } + } if err != nil { log.V(1).Error(err, "failed to run scheduler pipeline") return errors.New("failed to run scheduler pipeline") diff --git a/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go b/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go index bc2e0722a..c0daae2ec 100644 --- a/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go @@ -322,7 +322,6 @@ func TestFilterWeigherPipelineController_ProcessNewMachine(t *testing.T) { Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainMachines, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -356,7 +355,6 @@ func TestFilterWeigherPipelineController_ProcessNewMachine(t *testing.T) { Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainMachines, - CreateHistory: false, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -403,7 +401,6 @@ func TestFilterWeigherPipelineController_ProcessNewMachine(t *testing.T) { Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainMachines, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, diff --git a/internal/scheduling/manila/filter_weigher_pipeline_controller.go b/internal/scheduling/manila/filter_weigher_pipeline_controller.go index 128b7d719..6e00593a9 100644 --- a/internal/scheduling/manila/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/manila/filter_weigher_pipeline_controller.go @@ -7,7 +7,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "sync" "time" @@ -74,10 +73,6 @@ func (c *FilterWeigherPipelineController) ProcessNewDecisionFromAPI(ctx context. c.processMu.Lock() defer c.processMu.Unlock() - pipelineConf, ok := c.PipelineConfigs[decision.Spec.PipelineRef.Name] - if !ok { - return fmt.Errorf("pipeline %s not configured", decision.Spec.PipelineRef.Name) - } err := c.process(ctx, decision) if err != nil { meta.SetStatusCondition(&decision.Status.Conditions, metav1.Condition{ @@ -94,11 +89,6 @@ func (c *FilterWeigherPipelineController) ProcessNewDecisionFromAPI(ctx context. Message: "pipeline run succeeded", }) } - if pipelineConf.Spec.CreateHistory { - if upsertErr := c.HistoryManager.CreateOrUpdateHistory(ctx, decision, nil, err); upsertErr != nil { - ctrl.LoggerFrom(ctx).Error(upsertErr, "failed to create/update history") - } - } return err } @@ -122,6 +112,11 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision } result, err := pipeline.Run(request) + if !request.Options.SkipHistory { + if upsertErr := c.HistoryManager.CreateOrUpdateHistory(ctx, decision, nil, err); upsertErr != nil { + log.Error(upsertErr, "failed to create/update history") + } + } if err != nil { log.Error(err, "failed to run pipeline") return err diff --git a/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go b/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go index e16c55c27..bfdcbf358 100644 --- a/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go @@ -279,7 +279,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainManila, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -313,7 +312,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainManila, - CreateHistory: false, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -367,7 +365,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainManila, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller.go b/internal/scheduling/nova/filter_weigher_pipeline_controller.go index 40252fdc2..7be2b5dee 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller.go @@ -176,12 +176,8 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision log.Info("gathered all placement candidates", "numHosts", len(request.Hosts)) } - // Fill RecordHistory from config if the caller didn't set it. - if !request.Options.RecordHistory { - request.Options.RecordHistory = pipelineConf.Spec.CreateHistory - } result, err := pipeline.Run(request) - if request.Options.RecordHistory { + if !request.Options.SkipHistory { c.upsertHistory(ctx, decision, err) } if err != nil { diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go index 031527b84..30bc59d3c 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go @@ -431,7 +431,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -443,7 +442,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -480,7 +478,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: false, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -492,7 +489,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: false, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -552,7 +548,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -564,7 +559,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -602,7 +596,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -640,7 +633,6 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -864,7 +856,6 @@ func TestFilterWeigherPipelineController_IgnorePreselection(t *testing.T) { Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainNova, - CreateHistory: false, IgnorePreselection: tt.ignorePreselection, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, diff --git a/internal/scheduling/pods/filter_weigher_pipeline_controller.go b/internal/scheduling/pods/filter_weigher_pipeline_controller.go index 0ceee6485..aa28d988f 100644 --- a/internal/scheduling/pods/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/pods/filter_weigher_pipeline_controller.go @@ -6,7 +6,6 @@ package pods import ( "context" "errors" - "fmt" "sync" "time" @@ -95,10 +94,6 @@ func (c *FilterWeigherPipelineController) ProcessNewPod(ctx context.Context, pod }, } - pipelineConf, ok := c.PipelineConfigs[decision.Spec.PipelineRef.Name] - if !ok { - return fmt.Errorf("pipeline %s not configured", decision.Spec.PipelineRef.Name) - } err := c.process(ctx, decision) if err != nil { meta.SetStatusCondition(&decision.Status.Conditions, metav1.Condition{ @@ -115,11 +110,6 @@ func (c *FilterWeigherPipelineController) ProcessNewPod(ctx context.Context, pod Message: "pipeline run succeeded", }) } - if pipelineConf.Spec.CreateHistory { - if upsertErr := c.HistoryManager.CreateOrUpdateHistory(ctx, decision, nil, err); upsertErr != nil { - ctrl.LoggerFrom(ctx).Error(upsertErr, "failed to create/update history") - } - } return err } @@ -159,6 +149,11 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision // Execute the scheduling pipeline. request := pods.PodPipelineRequest{Nodes: nodes.Items, Pod: *pod} result, err := pipeline.Run(request) + if !request.Options.SkipHistory { + if upsertErr := c.HistoryManager.CreateOrUpdateHistory(ctx, decision, nil, err); upsertErr != nil { + log.Error(upsertErr, "failed to create/update history") + } + } if err != nil { log.V(1).Error(err, "failed to run scheduler pipeline") return errors.New("failed to run scheduler pipeline") diff --git a/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go b/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go index 143ed9f83..696f83c95 100644 --- a/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go @@ -300,7 +300,6 @@ func TestFilterWeigherPipelineController_ProcessNewPod(t *testing.T) { Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainPods, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -334,7 +333,6 @@ func TestFilterWeigherPipelineController_ProcessNewPod(t *testing.T) { Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainPods, - CreateHistory: false, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, @@ -381,7 +379,6 @@ func TestFilterWeigherPipelineController_ProcessNewPod(t *testing.T) { Spec: v1alpha1.PipelineSpec{ Type: v1alpha1.PipelineTypeFilterWeigher, SchedulingDomain: v1alpha1.SchedulingDomainPods, - CreateHistory: true, Filters: []v1alpha1.FilterSpec{}, Weighers: []v1alpha1.WeigherSpec{}, }, diff --git a/internal/scheduling/reservations/commitments/reservation_controller.go b/internal/scheduling/reservations/commitments/reservation_controller.go index 5d9ab7483..e18bf7ed5 100644 --- a/internal/scheduling/reservations/commitments/reservation_controller.go +++ b/internal/scheduling/reservations/commitments/reservation_controller.go @@ -293,7 +293,7 @@ func (r *CommitmentReservationController) Reconcile(ctx context.Context, req ctr AssumeEmptyHosts: false, IgnoredReservationTypes: nil, MaxCandidates: 1, - RecordHistory: false, + SkipHistory: true, CreateInflight: false, // not a VM placement; no pessimistic blocking needed } From 4886d377907cff129252638691d0befee2bf260d Mon Sep 17 00:00:00 2001 From: mblos Date: Fri, 8 May 2026 15:44:39 +0200 Subject: [PATCH 11/19] . --- .../files/crds/cortex.cloud_pipelines.yaml | 6 ------ .../filter_weigher_pipeline_controller_test.go | 13 ++++++++++++- .../filter_weigher_pipeline_controller_test.go | 5 +++-- .../filter_weigher_pipeline_controller_test.go | 17 ++++++++++++----- .../filter_weigher_pipeline_controller_test.go | 12 ++++++++++++ internal/scheduling/nova/integration_test.go | 1 + .../filter_weigher_pipeline_controller_test.go | 5 +++-- .../reservations/failover/integration_test.go | 5 ++--- .../failover/reservation_scheduling.go | 2 +- 9 files changed, 46 insertions(+), 20 deletions(-) diff --git a/helm/library/cortex/files/crds/cortex.cloud_pipelines.yaml b/helm/library/cortex/files/crds/cortex.cloud_pipelines.yaml index a79532d4b..ccdb89b47 100644 --- a/helm/library/cortex/files/crds/cortex.cloud_pipelines.yaml +++ b/helm/library/cortex/files/crds/cortex.cloud_pipelines.yaml @@ -58,12 +58,6 @@ spec: spec: description: spec defines the desired state of Pipeline properties: - createHistory: - default: false - description: |- - If this pipeline should create history objects. - When this is false, the pipeline will still process requests. - type: boolean description: description: An optional description of the pipeline, helping understand its purpose. diff --git a/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go b/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go index 11798a11c..e2939bd6e 100644 --- a/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go @@ -46,6 +46,7 @@ func TestFilterWeigherPipelineController_Reconcile(t *testing.T) { }, Weights: map[string]float64{"cinder-volume-1": 1.0, "cinder-volume-2": 0.5}, Pipeline: "test-pipeline", + Options: lib.Options{SkipHistory: true}, } cinderRaw, err := json.Marshal(cinderRequest) @@ -373,7 +374,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) }, createHistory: true, expectError: true, - expectHistoryCreated: true, + expectHistoryCreated: false, expectResult: false, }, } @@ -410,6 +411,16 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) controller.Pipelines[tt.pipelineConfig.Name] = initResult.Pipeline } + if tt.decision.Spec.CinderRaw != nil { + req := cinderRequest + req.Options = lib.Options{SkipHistory: !tt.createHistory} + raw, marshalErr := json.Marshal(req) + if marshalErr != nil { + t.Fatalf("Failed to marshal request with options: %v", marshalErr) + } + tt.decision.Spec.CinderRaw = &runtime.RawExtension{Raw: raw} + } + err := controller.ProcessNewDecisionFromAPI(context.Background(), tt.decision) if tt.expectError && err == nil { diff --git a/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go b/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go index c0daae2ec..9431f79ed 100644 --- a/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/machines/filter_weigher_pipeline_controller_test.go @@ -125,6 +125,7 @@ func TestFilterWeigherPipelineController_Reconcile(t *testing.T) { Pipelines: map[string]lib.FilterWeigherPipeline[ironcore.MachinePipelineRequest]{ "machines-scheduler": createMockPipeline(), }, + HistoryManager: lib.HistoryClient{Client: client}, }, Monitor: lib.FilterWeigherPipelineMonitor{}, } @@ -361,7 +362,7 @@ func TestFilterWeigherPipelineController_ProcessNewMachine(t *testing.T) { }, createHistory: false, expectError: false, - expectHistoryCreated: false, + expectHistoryCreated: true, expectMachinePoolAssigned: true, expectTargetHost: "pool1", }, @@ -407,7 +408,7 @@ func TestFilterWeigherPipelineController_ProcessNewMachine(t *testing.T) { }, createHistory: true, expectError: true, - expectHistoryCreated: true, // Decision is created but processing fails + expectHistoryCreated: false, expectMachinePoolAssigned: false, }, } diff --git a/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go b/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go index bfdcbf358..d79fd8c1e 100644 --- a/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go @@ -49,6 +49,7 @@ func TestFilterWeigherPipelineController_Reconcile(t *testing.T) { }, Weights: map[string]float64{"manila-share-1@backend1": 1.0, "manila-share-2@backend2": 0.5}, Pipeline: "test-pipeline", + Options: lib.Options{SkipHistory: true}, } manilaRaw, err := json.Marshal(manilaRequest) @@ -371,7 +372,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) }, createHistory: true, expectError: true, - expectHistoryCreated: true, + expectHistoryCreated: false, expectResult: false, }, } @@ -408,11 +409,17 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) controller.Pipelines[tt.pipelineConfig.Name] = initResult.Pipeline } - err := controller.ProcessNewDecisionFromAPI(context.Background(), tt.decision) - - if tt.expectError && err == nil { - t.Error("Expected error but got none") + if tt.decision.Spec.ManilaRaw != nil { + req := manilaRequest + req.Options = lib.Options{SkipHistory: !tt.createHistory} + raw, marshalErr := json.Marshal(req) + if marshalErr != nil { + t.Fatalf("Failed to marshal request with options: %v", marshalErr) + } + tt.decision.Spec.ManilaRaw = &runtime.RawExtension{Raw: raw} } + + err := controller.ProcessNewDecisionFromAPI(context.Background(), tt.decision) if !tt.expectError && err != nil { t.Errorf("Expected no error but got: %v", err) } diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go index 30bc59d3c..7d63aa296 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go @@ -77,6 +77,7 @@ func TestFilterWeigherPipelineController_Reconcile(t *testing.T) { }, Weights: map[string]float64{"compute-1": 1.0, "compute-2": 0.5}, Pipeline: "test-pipeline", + Options: lib.Options{SkipHistory: true}, } novaRaw, err := json.Marshal(novaRequest) @@ -689,6 +690,16 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) controller.Pipelines[tt.pipeline.Name] = initResult.Pipeline } + if tt.decision.Spec.NovaRaw != nil { + req := novaRequest + req.Options = lib.Options{SkipHistory: !tt.createHistory} + raw, marshalErr := json.Marshal(req) + if marshalErr != nil { + t.Fatalf("Failed to marshal request with options: %v", marshalErr) + } + tt.decision.Spec.NovaRaw = &runtime.RawExtension{Raw: raw} + } + // Call the method under test err := controller.ProcessNewDecisionFromAPI(context.Background(), tt.decision) @@ -771,6 +782,7 @@ func TestFilterWeigherPipelineController_IgnorePreselection(t *testing.T) { }, Weights: map[string]float64{"original-host-1": 1.0, "original-host-2": 0.5}, Pipeline: "test-pipeline", + Options: lib.Options{SkipHistory: true}, } novaRaw, err := json.Marshal(novaRequest) diff --git a/internal/scheduling/nova/integration_test.go b/internal/scheduling/nova/integration_test.go index f4bb38a79..1b84601a7 100644 --- a/internal/scheduling/nova/integration_test.go +++ b/internal/scheduling/nova/integration_test.go @@ -252,6 +252,7 @@ func NewIntegrationTestServer(t *testing.T, pipelineConfig PipelineConfig, objec Client: k8sClient, Pipelines: make(map[string]lib.FilterWeigherPipeline[novaapi.ExternalSchedulerRequest]), PipelineConfigs: make(map[string]v1alpha1.Pipeline), + HistoryManager: lib.HistoryClient{Client: k8sClient}, }, Monitor: getSharedMonitor(), } diff --git a/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go b/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go index 696f83c95..7a8159be6 100644 --- a/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/pods/filter_weigher_pipeline_controller_test.go @@ -122,6 +122,7 @@ func TestFilterWeigherPipelineController_Reconcile(t *testing.T) { Pipelines: map[string]lib.FilterWeigherPipeline[pods.PodPipelineRequest]{ "pods-scheduler": createMockPodPipeline(), }, + HistoryManager: lib.HistoryClient{Client: client}, }, Monitor: lib.FilterWeigherPipelineMonitor{}, } @@ -339,7 +340,7 @@ func TestFilterWeigherPipelineController_ProcessNewPod(t *testing.T) { }, createHistory: false, expectError: false, - expectHistoryCreated: false, + expectHistoryCreated: true, expectNodeAssigned: true, expectTargetHost: "node1", }, @@ -385,7 +386,7 @@ func TestFilterWeigherPipelineController_ProcessNewPod(t *testing.T) { }, createHistory: true, expectError: true, - expectHistoryCreated: true, // Decision is created but processing fails + expectHistoryCreated: false, expectNodeAssigned: false, }, } diff --git a/internal/scheduling/reservations/failover/integration_test.go b/internal/scheduling/reservations/failover/integration_test.go index 66d5733bb..d60c6cb2d 100644 --- a/internal/scheduling/reservations/failover/integration_test.go +++ b/internal/scheduling/reservations/failover/integration_test.go @@ -1104,10 +1104,10 @@ func newIntegrationTestEnv(t *testing.T, vms []VM, hypervisors []*hv1.Hypervisor Client: k8sClient, Pipelines: make(map[string]lib.FilterWeigherPipeline[novaapi.ExternalSchedulerRequest]), PipelineConfigs: make(map[string]v1alpha1.Pipeline), + HistoryManager: lib.HistoryClient{Client: k8sClient}, }, Monitor: getSharedMonitor(), } - // Register all pipelines needed for testing pipelines := []v1alpha1.Pipeline{ { @@ -1294,11 +1294,10 @@ func newIntegrationTestEnvWithTraitsFilter(t *testing.T, vms []VM, hypervisors [ Client: k8sClient, Pipelines: make(map[string]lib.FilterWeigherPipeline[novaapi.ExternalSchedulerRequest]), PipelineConfigs: make(map[string]v1alpha1.Pipeline), + HistoryManager: lib.HistoryClient{Client: k8sClient}, }, Monitor: getSharedMonitor(), } - - // Register all pipelines needed for testing (with traits filter enabled) pipelines := []v1alpha1.Pipeline{ { ObjectMeta: metav1.ObjectMeta{ diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index 5f8c93767..be63bcf4e 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -223,7 +223,7 @@ func (c *FailoverReservationController) validateVMViaSchedulerEvacuation( "vmCurrentHost", vm.CurrentHypervisor, "pipeline", PipelineAcknowledgeFailoverReservation) - resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, lib.Options{ReadOnly: true}) + resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, lib.Options{ReadOnly: true, SkipHistory: true}) if err != nil { logger.Error(err, "failed to validate VM for reservation host", "vmUUID", vm.UUID, "reservationHost", reservationHost) return false, fmt.Errorf("failed to validate VM for reservation host: %w", err) From 3b8cf87ac7efd85505104ed1d47cc8f9455d9246 Mon Sep 17 00:00:00 2001 From: mblos Date: Fri, 8 May 2026 15:57:38 +0200 Subject: [PATCH 12/19] moving to api package --- api/external/cinder/messages.go | 5 +++-- api/external/ironcore/messages.go | 5 +++-- api/external/manila/messages.go | 5 +++-- api/external/nova/messages.go | 5 +++-- api/external/pods/messages.go | 5 +++-- .../scheduling/lib => api/scheduling}/options.go | 2 +- .../lib => api/scheduling}/options_test.go | 2 +- .../filter_weigher_pipeline_controller_test.go | 5 +++-- .../lib/filter_weigher_pipeline_request.go | 8 ++++++-- .../lib/filter_weigher_pipeline_request_test.go | 10 +++++++--- .../scheduling/lib/filter_weigher_pipeline_test.go | 3 ++- .../filter_weigher_pipeline_controller_test.go | 5 +++-- .../filter_weigher_pipeline_controller_test.go | 9 +++++---- .../filters/filter_has_enough_capacity_test.go | 14 +++++++------- .../commitments/reservation_controller.go | 4 ++-- .../failover/reservation_scheduling.go | 6 +++--- .../scheduling/reservations/scheduler_client.go | 4 ++-- 17 files changed, 57 insertions(+), 40 deletions(-) rename {internal/scheduling/lib => api/scheduling}/options.go (99%) rename {internal/scheduling/lib => api/scheduling}/options_test.go (98%) diff --git a/api/external/cinder/messages.go b/api/external/cinder/messages.go index 08fe6edee..632189629 100644 --- a/api/external/cinder/messages.go +++ b/api/external/cinder/messages.go @@ -6,6 +6,7 @@ package api import ( "log/slog" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" ) @@ -31,10 +32,10 @@ type ExternalSchedulerRequest struct { // The name of the pipeline to execute. Pipeline string `json:"pipeline"` // Options configure the pipeline behavior for this scheduling call. - Options lib.Options `json:"options,omitempty"` + Options scheduling.Options `json:"options,omitempty"` } -func (r ExternalSchedulerRequest) GetOptions() lib.Options { return r.Options } +func (r ExternalSchedulerRequest) GetOptions() scheduling.Options { return r.Options } func (r ExternalSchedulerRequest) GetHosts() []string { hosts := make([]string, len(r.Hosts)) for i, host := range r.Hosts { diff --git a/api/external/ironcore/messages.go b/api/external/ironcore/messages.go index 05797e15a..25c71c0a5 100644 --- a/api/external/ironcore/messages.go +++ b/api/external/ironcore/messages.go @@ -7,6 +7,7 @@ import ( "log/slog" ironcorev1alpha1 "github.com/cobaltcore-dev/cortex/api/external/ironcore/v1alpha1" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" ) @@ -14,10 +15,10 @@ type MachinePipelineRequest struct { // The available machine pools. Pools []ironcorev1alpha1.MachinePool `json:"pools"` // Options configure the pipeline behavior for this scheduling call. - Options lib.Options `json:"options,omitempty"` + Options scheduling.Options `json:"options,omitempty"` } -func (r MachinePipelineRequest) GetOptions() lib.Options { return r.Options } +func (r MachinePipelineRequest) GetOptions() scheduling.Options { return r.Options } func (r MachinePipelineRequest) GetHosts() []string { hosts := make([]string, len(r.Pools)) for i, host := range r.Pools { diff --git a/api/external/manila/messages.go b/api/external/manila/messages.go index 013fa70fb..ad0aef2f1 100644 --- a/api/external/manila/messages.go +++ b/api/external/manila/messages.go @@ -6,6 +6,7 @@ package api import ( "log/slog" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" ) @@ -31,10 +32,10 @@ type ExternalSchedulerRequest struct { // The name of the pipeline to execute. Pipeline string `json:"pipeline"` // Options configure the pipeline behavior for this scheduling call. - Options lib.Options `json:"options,omitempty"` + Options scheduling.Options `json:"options,omitempty"` } -func (r ExternalSchedulerRequest) GetOptions() lib.Options { return r.Options } +func (r ExternalSchedulerRequest) GetOptions() scheduling.Options { return r.Options } func (r ExternalSchedulerRequest) GetHosts() []string { hosts := make([]string, len(r.Hosts)) for i, host := range r.Hosts { diff --git a/api/external/nova/messages.go b/api/external/nova/messages.go index 202f85cf1..e83d37ced 100644 --- a/api/external/nova/messages.go +++ b/api/external/nova/messages.go @@ -9,6 +9,7 @@ import ( "log/slog" "strings" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" ) @@ -41,10 +42,10 @@ type ExternalSchedulerRequest struct { // Options configure the pipeline behavior for this scheduling call. // Set by the caller (CR controller, failover controller, Nova). // Nova does not set these; Cortex fills in config-derived defaults server-side. - Options lib.Options `json:"options,omitempty"` + Options scheduling.Options `json:"options,omitempty"` } -func (r ExternalSchedulerRequest) GetOptions() lib.Options { return r.Options } +func (r ExternalSchedulerRequest) GetOptions() scheduling.Options { return r.Options } func (r ExternalSchedulerRequest) GetHosts() []string { hosts := make([]string, len(r.Hosts)) for i, host := range r.Hosts { diff --git a/api/external/pods/messages.go b/api/external/pods/messages.go index 0b5466415..3d0930ef5 100644 --- a/api/external/pods/messages.go +++ b/api/external/pods/messages.go @@ -6,6 +6,7 @@ package pods import ( "log/slog" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" corev1 "k8s.io/api/core/v1" ) @@ -16,10 +17,10 @@ type PodPipelineRequest struct { // The pod to be scheduled. Pod corev1.Pod `json:"pod"` // Options configure the pipeline behavior for this scheduling call. - Options lib.Options `json:"options,omitempty"` + Options scheduling.Options `json:"options,omitempty"` } -func (r PodPipelineRequest) GetOptions() lib.Options { return r.Options } +func (r PodPipelineRequest) GetOptions() scheduling.Options { return r.Options } func (r PodPipelineRequest) GetHosts() []string { hosts := make([]string, len(r.Nodes)) for i, host := range r.Nodes { diff --git a/internal/scheduling/lib/options.go b/api/scheduling/options.go similarity index 99% rename from internal/scheduling/lib/options.go rename to api/scheduling/options.go index 84c4ecde0..c182fbdd4 100644 --- a/internal/scheduling/lib/options.go +++ b/api/scheduling/options.go @@ -1,7 +1,7 @@ // Copyright SAP SE // SPDX-License-Identifier: Apache-2.0 -package lib +package scheduling import ( "errors" diff --git a/internal/scheduling/lib/options_test.go b/api/scheduling/options_test.go similarity index 98% rename from internal/scheduling/lib/options_test.go rename to api/scheduling/options_test.go index cba9304aa..870609281 100644 --- a/internal/scheduling/lib/options_test.go +++ b/api/scheduling/options_test.go @@ -1,7 +1,7 @@ // Copyright SAP SE // SPDX-License-Identifier: Apache-2.0 -package lib +package scheduling import "testing" diff --git a/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go b/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go index e2939bd6e..05d474b60 100644 --- a/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/cinder/filter_weigher_pipeline_controller_test.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" api "github.com/cobaltcore-dev/cortex/api/external/cinder" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" @@ -46,7 +47,7 @@ func TestFilterWeigherPipelineController_Reconcile(t *testing.T) { }, Weights: map[string]float64{"cinder-volume-1": 1.0, "cinder-volume-2": 0.5}, Pipeline: "test-pipeline", - Options: lib.Options{SkipHistory: true}, + Options: scheduling.Options{SkipHistory: true}, } cinderRaw, err := json.Marshal(cinderRequest) @@ -413,7 +414,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) if tt.decision.Spec.CinderRaw != nil { req := cinderRequest - req.Options = lib.Options{SkipHistory: !tt.createHistory} + req.Options = scheduling.Options{SkipHistory: !tt.createHistory} raw, marshalErr := json.Marshal(req) if marshalErr != nil { t.Fatalf("Failed to marshal request with options: %v", marshalErr) diff --git a/internal/scheduling/lib/filter_weigher_pipeline_request.go b/internal/scheduling/lib/filter_weigher_pipeline_request.go index 69a9522e5..f66431545 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_request.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_request.go @@ -3,7 +3,11 @@ package lib -import "log/slog" +import ( + "log/slog" + + "github.com/cobaltcore-dev/cortex/api/scheduling" +) type FilterWeigherPipelineRequest interface { // Get the hosts that went in the pipeline. @@ -22,5 +26,5 @@ type FilterWeigherPipelineRequest interface { // Usually, this will be the request context including the request ID. GetTraceLogArgs() []slog.Attr // Get the call-time options for this pipeline run. - GetOptions() Options + GetOptions() scheduling.Options } diff --git a/internal/scheduling/lib/filter_weigher_pipeline_request_test.go b/internal/scheduling/lib/filter_weigher_pipeline_request_test.go index 765752a45..3b2b6d246 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_request_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_request_test.go @@ -3,7 +3,11 @@ package lib -import "log/slog" +import ( + "log/slog" + + "github.com/cobaltcore-dev/cortex/api/scheduling" +) type mockFilterWeigherPipelineRequest struct { WeightKeys []string @@ -11,7 +15,7 @@ type mockFilterWeigherPipelineRequest struct { Hosts []string Weights map[string]float64 Pipeline string - Options Options + Options scheduling.Options } func (m mockFilterWeigherPipelineRequest) GetWeightKeys() []string { return m.WeightKeys } @@ -19,7 +23,7 @@ func (m mockFilterWeigherPipelineRequest) GetTraceLogArgs() []slog.Attr { retu func (m mockFilterWeigherPipelineRequest) GetHosts() []string { return m.Hosts } func (m mockFilterWeigherPipelineRequest) GetWeights() map[string]float64 { return m.Weights } func (m mockFilterWeigherPipelineRequest) GetPipeline() string { return m.Pipeline } -func (m mockFilterWeigherPipelineRequest) GetOptions() Options { return m.Options } +func (m mockFilterWeigherPipelineRequest) GetOptions() scheduling.Options { return m.Options } func (m mockFilterWeigherPipelineRequest) Filter(hosts map[string]float64) FilterWeigherPipelineRequest { filteredHosts := make([]string, 0, len(hosts)) diff --git a/internal/scheduling/lib/filter_weigher_pipeline_test.go b/internal/scheduling/lib/filter_weigher_pipeline_test.go index a110aeec3..3a2012db7 100644 --- a/internal/scheduling/lib/filter_weigher_pipeline_test.go +++ b/internal/scheduling/lib/filter_weigher_pipeline_test.go @@ -10,6 +10,7 @@ import ( "slices" "testing" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -402,7 +403,7 @@ func TestPipeline_MaxCandidates(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := request - req.Options = Options{MaxCandidates: tt.maxCandidates} + req.Options = scheduling.Options{MaxCandidates: tt.maxCandidates} result, err := pipeline.Run(req) if err != nil { t.Fatalf("expected no error, got %v", err) diff --git a/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go b/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go index d79fd8c1e..3193fb3e6 100644 --- a/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/manila/filter_weigher_pipeline_controller_test.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" api "github.com/cobaltcore-dev/cortex/api/external/manila" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" testlib "github.com/cobaltcore-dev/cortex/pkg/testing" "github.com/sapcc/go-bits/must" @@ -49,7 +50,7 @@ func TestFilterWeigherPipelineController_Reconcile(t *testing.T) { }, Weights: map[string]float64{"manila-share-1@backend1": 1.0, "manila-share-2@backend2": 0.5}, Pipeline: "test-pipeline", - Options: lib.Options{SkipHistory: true}, + Options: scheduling.Options{SkipHistory: true}, } manilaRaw, err := json.Marshal(manilaRequest) @@ -411,7 +412,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) if tt.decision.Spec.ManilaRaw != nil { req := manilaRequest - req.Options = lib.Options{SkipHistory: !tt.createHistory} + req.Options = scheduling.Options{SkipHistory: !tt.createHistory} raw, marshalErr := json.Marshal(req) if marshalErr != nil { t.Fatalf("Failed to marshal request with options: %v", marshalErr) diff --git a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go index 7d63aa296..52064d0c3 100644 --- a/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go +++ b/internal/scheduling/nova/filter_weigher_pipeline_controller_test.go @@ -20,6 +20,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" api "github.com/cobaltcore-dev/cortex/api/external/nova" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" @@ -77,7 +78,7 @@ func TestFilterWeigherPipelineController_Reconcile(t *testing.T) { }, Weights: map[string]float64{"compute-1": 1.0, "compute-2": 0.5}, Pipeline: "test-pipeline", - Options: lib.Options{SkipHistory: true}, + Options: scheduling.Options{SkipHistory: true}, } novaRaw, err := json.Marshal(novaRequest) @@ -692,7 +693,7 @@ func TestFilterWeigherPipelineController_ProcessNewDecisionFromAPI(t *testing.T) if tt.decision.Spec.NovaRaw != nil { req := novaRequest - req.Options = lib.Options{SkipHistory: !tt.createHistory} + req.Options = scheduling.Options{SkipHistory: !tt.createHistory} raw, marshalErr := json.Marshal(req) if marshalErr != nil { t.Fatalf("Failed to marshal request with options: %v", marshalErr) @@ -782,7 +783,7 @@ func TestFilterWeigherPipelineController_IgnorePreselection(t *testing.T) { }, Weights: map[string]float64{"original-host-1": 1.0, "original-host-2": 0.5}, Pipeline: "test-pipeline", - Options: lib.Options{SkipHistory: true}, + Options: scheduling.Options{SkipHistory: true}, } novaRaw, err := json.Marshal(novaRequest) @@ -936,7 +937,7 @@ func TestFilterWeigherPipelineController_PeekReadOnly(t *testing.T) { makeRaw := func(readOnly bool) []byte { r := api.ExternalSchedulerRequest{ Spec: api.NovaObject[api.NovaSpec]{Data: api.NovaSpec{NumInstances: 1}}, - Options: lib.Options{ReadOnly: readOnly}, + Options: scheduling.Options{ReadOnly: readOnly}, } raw, err := json.Marshal(r) if err != nil { diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go index f6f3689b9..1cdc800f0 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity_test.go @@ -4,7 +4,7 @@ package filters import ( - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" + "github.com/cobaltcore-dev/cortex/api/scheduling" "log/slog" "testing" @@ -836,7 +836,7 @@ func TestFilterHasEnoughCapacity_IgnoredReservationTypes_CallTime(t *testing.T) step.Options = FilterHasEnoughCapacityOpts{LockReserved: true} // no YAML-level ignores // Call-time: ignore CR reservations → host1 passes, host2 still blocked by failover. - request.Options = lib.Options{ + request.Options = scheduling.Options{ IgnoredReservationTypes: []v1alpha1.ReservationType{v1alpha1.ReservationTypeCommittedResource}, } result, err := step.Run(slog.Default(), request) @@ -858,7 +858,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) reservations []*v1alpha1.Reservation request api.ExternalSchedulerRequest opts FilterHasEnoughCapacityOpts - pipelineOpts lib.Options + pipelineOpts scheduling.Options expectedHosts []string filteredHosts []string }{ @@ -875,7 +875,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) // Request with reserve_for_committed_resource intent (scheduling a new CR reservation) request: newNovaRequestWithIntent("new-reservation-uuid", "project-A", "m1.large", "gp-1", 4, "8Gi", "reserve_for_committed_resource", false, []string{"host1", "host2"}), opts: FilterHasEnoughCapacityOpts{LockReserved: false}, - pipelineOpts: lib.Options{LockReservations: true}, + pipelineOpts: scheduling.Options{LockReservations: true}, expectedHosts: []string{"host2"}, // host1 blocked because existing-cr stays locked filteredHosts: []string{"host1"}, }, @@ -908,7 +908,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) // Request with reserve_for_committed_resource intent request: newNovaRequestWithIntent("new-reservation-uuid", "project-A", "m1.large", "gp-1", 4, "8Gi", "reserve_for_committed_resource", false, []string{"host1", "host2"}), opts: FilterHasEnoughCapacityOpts{LockReserved: false}, - pipelineOpts: lib.Options{LockReservations: true}, + pipelineOpts: scheduling.Options{LockReservations: true}, expectedHosts: []string{"host2"}, filteredHosts: []string{"host1"}, // host1 blocked by other project's reservation (would be blocked anyway) }, @@ -927,7 +927,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) // After blocking all 3 reservations (24 CPU), only 8 CPU free -> should fail request: newNovaRequestWithIntent("new-reservation-uuid", "project-A", "m1.large", "gp-1", 10, "20Gi", "reserve_for_committed_resource", false, []string{"host1"}), opts: FilterHasEnoughCapacityOpts{LockReserved: false}, - pipelineOpts: lib.Options{LockReservations: true}, + pipelineOpts: scheduling.Options{LockReservations: true}, expectedHosts: []string{}, filteredHosts: []string{"host1"}, // All reservations stay locked, not enough capacity }, @@ -966,7 +966,7 @@ func TestFilterHasEnoughCapacity_ReserveForCommittedResourceIntent(t *testing.T) // IgnoredReservationTypes is a safety override - ignores CR even for CR scheduling IgnoredReservationTypes: []v1alpha1.ReservationType{v1alpha1.ReservationTypeCommittedResource}, }, - pipelineOpts: lib.Options{LockReservations: true}, + pipelineOpts: scheduling.Options{LockReservations: true}, expectedHosts: []string{"host1"}, // CR reservation is ignored via IgnoredReservationTypes (safety override) filteredHosts: []string{}, }, diff --git a/internal/scheduling/reservations/commitments/reservation_controller.go b/internal/scheduling/reservations/commitments/reservation_controller.go index e18bf7ed5..4cc545a81 100644 --- a/internal/scheduling/reservations/commitments/reservation_controller.go +++ b/internal/scheduling/reservations/commitments/reservation_controller.go @@ -23,8 +23,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" schedulerdelegationapi "github.com/cobaltcore-dev/cortex/api/external/nova" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations" "github.com/cobaltcore-dev/cortex/pkg/multicluster" hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1" @@ -287,7 +287,7 @@ func (r *CommitmentReservationController) Reconcile(ctx context.Context, req ctr "_nova_check_type": string(schedulerdelegationapi.ReserveForCommittedResourceIntent), }, } - scheduleOpts := lib.Options{ + scheduleOpts := scheduling.Options{ ReadOnly: false, // mutates state (reservation placement) LockReservations: true, // don't unlock CR reservations; finding a slot, not placing a VM AssumeEmptyHosts: false, diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index be63bcf4e..ed45a5cf3 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -10,8 +10,8 @@ import ( "sort" api "github.com/cobaltcore-dev/cortex/api/external/nova" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations" ) @@ -92,7 +92,7 @@ func (c *FailoverReservationController) queryHypervisorsFromScheduler(ctx contex "eligibleHypervisors", len(eligibleHypervisors), "ignoreHypervisors", ignoreHypervisors) - scheduleResp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, lib.Options{LockReservations: true}) + scheduleResp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{LockReservations: true}) if err != nil { logger.Error(err, "failed to schedule failover reservation", "vmUUID", vm.UUID, "pipeline", pipeline) return nil, fmt.Errorf("failed to schedule failover reservation: %w", err) @@ -223,7 +223,7 @@ func (c *FailoverReservationController) validateVMViaSchedulerEvacuation( "vmCurrentHost", vm.CurrentHypervisor, "pipeline", PipelineAcknowledgeFailoverReservation) - resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, lib.Options{ReadOnly: true, SkipHistory: true}) + resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{ReadOnly: true, SkipHistory: true}) if err != nil { logger.Error(err, "failed to validate VM for reservation host", "vmUUID", vm.UUID, "reservationHost", reservationHost) return false, fmt.Errorf("failed to validate VM for reservation host: %w", err) diff --git a/internal/scheduling/reservations/scheduler_client.go b/internal/scheduling/reservations/scheduler_client.go index f10ad21d0..c23e66354 100644 --- a/internal/scheduling/reservations/scheduler_client.go +++ b/internal/scheduling/reservations/scheduler_client.go @@ -12,7 +12,7 @@ import ( "time" api "github.com/cobaltcore-dev/cortex/api/external/nova" - "github.com/cobaltcore-dev/cortex/internal/scheduling/lib" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/go-logr/logr" logf "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -90,7 +90,7 @@ type ScheduleReservationResponse struct { // ScheduleReservation calls the external scheduler API to find a host for a reservation. // The context should contain GlobalRequestID and RequestID for logging (use WithGlobalRequestID/WithRequestID). -func (c *SchedulerClient) ScheduleReservation(ctx context.Context, req ScheduleReservationRequest, opts lib.Options) (*ScheduleReservationResponse, error) { +func (c *SchedulerClient) ScheduleReservation(ctx context.Context, req ScheduleReservationRequest, opts scheduling.Options) (*ScheduleReservationResponse, error) { logger := loggerFromContext(ctx) // Build weights map (all zero for reservations) From 037f74c0dde32f94214a13e1c9f5a524ffaccc7b Mon Sep 17 00:00:00 2001 From: mblos Date: Fri, 8 May 2026 16:38:02 +0200 Subject: [PATCH 13/19] fix merge main --- internal/scheduling/reservations/capacity/controller.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/scheduling/reservations/capacity/controller.go b/internal/scheduling/reservations/capacity/controller.go index 37b6da4ef..100a0ffa9 100644 --- a/internal/scheduling/reservations/capacity/controller.go +++ b/internal/scheduling/reservations/capacity/controller.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" schedulerapi "github.com/cobaltcore-dev/cortex/api/external/nova" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" "github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute" "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations" @@ -264,7 +265,7 @@ func (c *Controller) probeScheduler( AvailabilityZone: az, Pipeline: pipeline, EligibleHosts: eligibleHosts, - }) + }, scheduling.Options{}) if err != nil { return 0, 0, fmt.Errorf("scheduler call failed (pipeline=%s): %w", pipeline, err) } From 91dfe3e2cf1f7898752421cbf47733a1472592c5 Mon Sep 17 00:00:00 2001 From: mblos Date: Mon, 11 May 2026 09:09:53 +0200 Subject: [PATCH 14/19] refactor --- .../cortex-ironcore/templates/pipelines.yaml | 1 - .../cortex-nova/templates/pipelines.yaml | 2 - .../cortex-nova/templates/pipelines_kvm.yaml | 128 ------------------ .../cortex-pods/templates/pipelines.yaml | 1 - .../reservations/capacity/controller.go | 2 +- .../failover/reservation_scheduling.go | 5 +- 6 files changed, 4 insertions(+), 135 deletions(-) diff --git a/helm/bundles/cortex-ironcore/templates/pipelines.yaml b/helm/bundles/cortex-ironcore/templates/pipelines.yaml index a77991b00..768a3912f 100644 --- a/helm/bundles/cortex-ironcore/templates/pipelines.yaml +++ b/helm/bundles/cortex-ironcore/templates/pipelines.yaml @@ -8,7 +8,6 @@ spec: description: | This pipeline is used to schedule ironcore machines onto machinepools. type: filter-weigher - createHistory: false filters: [] weighers: - name: noop diff --git a/helm/bundles/cortex-nova/templates/pipelines.yaml b/helm/bundles/cortex-nova/templates/pipelines.yaml index e1abb1969..09d8961d0 100644 --- a/helm/bundles/cortex-nova/templates/pipelines.yaml +++ b/helm/bundles/cortex-nova/templates/pipelines.yaml @@ -14,7 +14,6 @@ spec: Specifically, this pipeline is used for general purpose workloads. type: filter-weigher - createHistory: false filters: [] weighers: - name: vmware_binpack @@ -73,7 +72,6 @@ spec: Specifically, this pipeline is used for HANA workloads. type: filter-weigher - createHistory: false filters: [] weighers: - name: vmware_binpack diff --git a/helm/bundles/cortex-nova/templates/pipelines_kvm.yaml b/helm/bundles/cortex-nova/templates/pipelines_kvm.yaml index 143c0488a..e11edb281 100644 --- a/helm/bundles/cortex-nova/templates/pipelines_kvm.yaml +++ b/helm/bundles/cortex-nova/templates/pipelines_kvm.yaml @@ -16,7 +16,6 @@ spec: type: filter-weigher # Fetch all placement candidates, ignoring nova's preselection. ignorePreselection: true - createHistory: true filters: - name: filter_correct_az description: | @@ -155,7 +154,6 @@ spec: type: filter-weigher # Fetch all placement candidates, ignoring nova's preselection. ignorePreselection: true - createHistory: true filters: - name: filter_correct_az description: | @@ -277,128 +275,6 @@ spec: --- apiVersion: cortex.cloud/v1alpha1 kind: Pipeline -metadata: - name: kvm-new-failover-reservation -spec: - schedulingDomain: nova - description: | - This pipeline is used by the failover reservation controller to find a host - for creating a new failover reservation. It validates host compatibility AND - checks capacity. - - Note: Domain filtering (filter_external_customer) is not applied for failover - reservations because domains are currently not considered in failover scheduling. - - This is the pipeline used for KVM hypervisors (qemu and cloud-hypervisor). - type: filter-weigher - createHistory: false - # Fetch all placement candidates, ignoring nova's preselection. - ignorePreselection: true - filters: - - name: filter_host_instructions - description: | - This step will consider the `ignore_hosts` and `force_hosts` instructions - from the nova scheduler request spec to filter out or exclusively allow - certain hosts. - - name: filter_has_enough_capacity - description: | - This step will filter out hosts that do not have enough available capacity - to host the requested flavor. If enabled, this step will subtract the - current reservations residing on this host from the available capacity. - params: - # If reserved space should be locked even for matching requests. - # For the reservations pipeline, we don't want to unlock - # reserved space, to avoid reservations for the same project - # and flavor to overlap. - - {key: lockReserved, boolValue: true} - - name: filter_has_requested_traits - description: | - This step filters hosts that do not have the requested traits given by the - nova flavor extra spec: "trait:": "forbidden" means the host must - not have the specified trait. "trait:": "required" means the host - must have the specified trait. - - name: filter_has_accelerators - description: | - This step will filter out hosts without the trait `COMPUTE_ACCELERATORS` if - the nova flavor extra specs request accelerators via "accel:device_profile". - - name: filter_correct_az - description: | - This step will filter out hosts whose aggregate information indicates they - are not placed in the requested availability zone. - - name: filter_status_conditions - description: | - This step will filter out hosts for which the hypervisor status conditions - do not meet the expected values, for example, that the hypervisor is ready - and not disabled. - # Note: filter_external_customer is intentionally omitted. - # Domains are currently not considered in failover reservations. - - name: filter_allowed_projects - description: | - This step filters hosts based on allowed projects defined in the - hypervisor resource. Note that hosts allowing all projects are still - accessible and will not be filtered out. In this way some hypervisors - are made accessible to some projects only. - - name: filter_capabilities - description: | - This step will filter out hosts that do not meet the compute capabilities - requested by the nova flavor extra specs, like `{"arch": "x86_64", - "maxphysaddr:bits": 46, ...}`. - - Note: currently, advanced boolean/numeric operators for the capabilities - like `>`, `!`, ... are not supported because they are not used by any of our - flavors in production. - - name: filter_instance_group_affinity - description: | - This step selects hosts in the instance group specified in the nova - scheduler request spec. - - name: filter_instance_group_anti_affinity - description: | - This step selects hosts not in the instance group specified in the nova - scheduler request spec, but only until the max_server_per_host limit is - reached (default = 1). - - name: filter_live_migratable - description: | - This step ensures that the target host of a live migration can accept - the migrating VM, by checking cpu architecture, cpu features, emulated - devices, and cpu modes. - - name: filter_requested_destination - description: | - This step filters hosts based on the `requested_destination` instruction - from the nova scheduler request spec. It supports filtering by host and - by aggregates. Aggregates use AND logic between list elements, with - comma-separated UUIDs within an element using OR logic. - weighers: - - name: kvm_prefer_smaller_hosts - params: - - {key: resourceWeights, floatMapValue: {"memory": 1.0}} - description: | - This step pulls virtual machines onto smaller hosts (by capacity). This - ensures that larger hosts are not overly fragmented with small VMs, - and can still accommodate larger VMs when they need to be scheduled. - - name: kvm_instance_group_soft_affinity - description: | - This weigher implements the "soft affinity" and "soft anti-affinity" policy - for instance groups in nova. - - It assigns a weight to each host based on how many instances of the same - instance group are already running on that host. The more instances of the - same group on a host, the lower (for soft-anti-affinity) or higher - (for soft-affinity) the weight, which makes it less likely or more likely, - respectively, for the scheduler to choose that host for new instances of - the same group. - - name: kvm_binpack - multiplier: -1.0 # inverted = balancing - params: - - {key: resourceWeights, floatMapValue: {"memory": 1.0}} - description: | - This step implements a balancing weigher for workloads on kvm hypervisors, - which is the opposite of binpacking. Instead of pulling the requested vm - into the smallest gaps possible, it spreads the load to ensure - workloads are balanced across hosts. In this pipeline, the balancing will - focus on general purpose virtual machines. ---- -apiVersion: cortex.cloud/v1alpha1 -kind: Pipeline metadata: name: kvm-descheduler spec: @@ -408,7 +284,6 @@ spec: compute hosts in order to optimize resource usage and performance. This is the pipeline used for KVM hypervisors (qemu and cloud-hypervisor). type: detector - createHistory: true detectors: - name: avoid_high_steal_pct description: | @@ -432,7 +307,6 @@ spec: Use case: When a VM needs failover protection and there's an existing reservation on a host, this pipeline validates the host is still suitable for the VM. type: filter-weigher - createHistory: false filters: - name: filter_host_instructions description: | @@ -496,7 +370,6 @@ spec: that the reservation host can still accommodate all allocated VMs. If validation fails for any VM, the reservation is deleted (nack). type: filter-weigher - createHistory: false filters: - name: filter_host_instructions description: | @@ -571,7 +444,6 @@ spec: and all reservation blockings so that only raw hardware capacity is considered. type: filter-weigher - createDecisions: false # Fetch all placement candidates, ignoring nova's preselection. ignorePreselection: true filters: diff --git a/helm/bundles/cortex-pods/templates/pipelines.yaml b/helm/bundles/cortex-pods/templates/pipelines.yaml index edf59daa2..e51608d50 100644 --- a/helm/bundles/cortex-pods/templates/pipelines.yaml +++ b/helm/bundles/cortex-pods/templates/pipelines.yaml @@ -8,7 +8,6 @@ spec: description: | This pipeline is used to schedule pods onto nodes. type: filter-weigher - createHistory: false filters: - name: noop description: | diff --git a/internal/scheduling/reservations/capacity/controller.go b/internal/scheduling/reservations/capacity/controller.go index 100a0ffa9..44bf02ac9 100644 --- a/internal/scheduling/reservations/capacity/controller.go +++ b/internal/scheduling/reservations/capacity/controller.go @@ -265,7 +265,7 @@ func (c *Controller) probeScheduler( AvailabilityZone: az, Pipeline: pipeline, EligibleHosts: eligibleHosts, - }, scheduling.Options{}) + }, scheduling.Options{SkipHistory: true}) if err != nil { return 0, 0, fmt.Errorf("scheduler call failed (pipeline=%s): %w", pipeline, err) } diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index ed45a5cf3..14cb65406 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -23,7 +23,8 @@ const ( // PipelineNewFailoverReservation is used to find a host for creating a new reservation. // It validates host compatibility AND checks capacity. - PipelineNewFailoverReservation = "kvm-new-failover-reservation" + // Uses the general-purpose pipeline; LockReservations and SkipHistory are set via Options. + PipelineNewFailoverReservation = "kvm-general-purpose-load-balancing" // PipelineAcknowledgeFailoverReservation is used to validate that a failover reservation // is still valid for all its allocated VMs. It sends an evacuation-style scheduling request @@ -92,7 +93,7 @@ func (c *FailoverReservationController) queryHypervisorsFromScheduler(ctx contex "eligibleHypervisors", len(eligibleHypervisors), "ignoreHypervisors", ignoreHypervisors) - scheduleResp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{LockReservations: true}) + scheduleResp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{LockReservations: true, SkipHistory: true}) if err != nil { logger.Error(err, "failed to schedule failover reservation", "vmUUID", vm.UUID, "pipeline", pipeline) return nil, fmt.Errorf("failed to schedule failover reservation: %w", err) From 5c6ecae659b8a8a3896aeda77e8cf10ec994afe4 Mon Sep 17 00:00:00 2001 From: mblos Date: Mon, 11 May 2026 09:58:40 +0200 Subject: [PATCH 15/19] refactor --- docs/reservations/failover-reservations.md | 8 ++++++-- .../nova/plugins/filters/filter_has_enough_capacity.go | 4 ++-- internal/scheduling/reservations/capacity/controller.go | 7 ++++--- .../scheduling/reservations/capacity/controller_test.go | 3 ++- .../reservations/failover/reservation_scheduling.go | 8 ++++---- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/reservations/failover-reservations.md b/docs/reservations/failover-reservations.md index 14f785ae1..34a6ccc3f 100644 --- a/docs/reservations/failover-reservations.md +++ b/docs/reservations/failover-reservations.md @@ -144,20 +144,24 @@ We use three different scheduler pipelines for failover reservations, each servi **Why:** When reusing a reservation, capacity is already reserved on the target host. We only need to verify that the VM is compatible with the host (traits, capabilities, AZ, etc.) without checking if there's enough free capacity. -### `kvm-new-failover-reservation` +Options: `ReadOnly: true, SkipHistory: true` — pure compatibility check, no state mutations. + +### `kvm-general-purpose-load-balancing` (new reservation) **Used when:** Creating a new failover reservation. **Why:** When creating a new reservation, we need to find a host that: 1. Is compatible with the VM (traits, capabilities, AZ, etc.) 2. Has enough free capacity to accommodate the VM if it needs to evacuate -This is the most restrictive pipeline since we're actually reserving new capacity. +Options: `LockReservations: true, SkipHistory: true` — capacity check must see true remaining capacity with all reservation slots locked. ### `kvm-acknowledge-failover-reservation` **Used when:** Validating that an existing reservation is still valid (watch-based reconciliation). **Why:** Periodically we need to verify that a VM could still evacuate to its reserved host. This sends an evacuation-style scheduling request with only the reservation's host as the eligible target. If the scheduler rejects it, the reservation is no longer valid and should be deleted so the periodic controller can create a new one on a valid host. +Options: `ReadOnly: true, SkipHistory: true` — validation only, no state mutations. + ## Data Model ### VM Struct diff --git a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go index c0bbeae83..b379bf3e4 100644 --- a/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go +++ b/internal/scheduling/nova/plugins/filters/filter_has_enough_capacity.go @@ -86,7 +86,7 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa } // Subtract allocated resources (skip when ignoring allocations for empty-datacenter capacity queries). - if !s.Options.IgnoreAllocations { + if !s.Options.IgnoreAllocations && !opts.AssumeEmptyHosts { for resourceName, allocated := range hv.Status.Allocation { free, ok := freeResourcesByHost[hv.Name][resourceName] if !ok { @@ -197,7 +197,7 @@ func (s *FilterHasEnoughCapacity) Run(traceLog *slog.Logger, request api.Externa // When ignoring allocations (empty-datacenter scenario) VM resources are not // deducted, so the confirmed-VM adjustment would under-block: always use the // full slot instead. - !s.Options.IgnoreAllocations && + !s.Options.IgnoreAllocations && !opts.AssumeEmptyHosts && // if the reservation is not being migrated, block only unused resources reservation.Spec.TargetHost == reservation.Status.Host && reservation.Spec.CommittedResourceReservation != nil && diff --git a/internal/scheduling/reservations/capacity/controller.go b/internal/scheduling/reservations/capacity/controller.go index 34e664d81..b9d0315b6 100644 --- a/internal/scheduling/reservations/capacity/controller.go +++ b/internal/scheduling/reservations/capacity/controller.go @@ -148,8 +148,8 @@ func (c *Controller) reconcileOne( cur := existingByName[flavor.Name] cur.FlavorName = flavor.Name - totalVMSlots, totalHosts, totalErr := c.probeScheduler(ctx, flavor, az, c.config.TotalPipeline, hvByName) - placeableVMs, placeableHosts, placeableErr := c.probeScheduler(ctx, flavor, az, c.config.PlaceablePipeline, hvByName) + totalVMSlots, totalHosts, totalErr := c.probeScheduler(ctx, flavor, az, c.config.TotalPipeline, hvByName, scheduling.Options{SkipHistory: true, AssumeEmptyHosts: true}) + placeableVMs, placeableHosts, placeableErr := c.probeScheduler(ctx, flavor, az, c.config.PlaceablePipeline, hvByName, scheduling.Options{SkipHistory: true}) if totalErr != nil { allFresh = false @@ -247,6 +247,7 @@ func (c *Controller) probeScheduler( flavor compute.FlavorInGroup, az, pipeline string, hvByName map[string]hv1.Hypervisor, + opts scheduling.Options, ) (capacity, hosts int64, err error) { flavorBytes := int64(flavor.MemoryMB) * 1024 * 1024 //nolint:gosec @@ -272,7 +273,7 @@ func (c *Controller) probeScheduler( AvailabilityZone: az, Pipeline: pipeline, EligibleHosts: eligibleHosts, - }, scheduling.Options{SkipHistory: true}) + }, opts) if err != nil { return 0, 0, fmt.Errorf("scheduler call failed (pipeline=%s): %w", pipeline, err) } diff --git a/internal/scheduling/reservations/capacity/controller_test.go b/internal/scheduling/reservations/capacity/controller_test.go index 69a4e80bb..56d43d6f2 100644 --- a/internal/scheduling/reservations/capacity/controller_test.go +++ b/internal/scheduling/reservations/capacity/controller_test.go @@ -21,6 +21,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" schedulerapi "github.com/cobaltcore-dev/cortex/api/external/nova" + "github.com/cobaltcore-dev/cortex/api/scheduling" "github.com/cobaltcore-dev/cortex/api/v1alpha1" "github.com/cobaltcore-dev/cortex/internal/knowledge/extractor/plugins/compute" "github.com/cobaltcore-dev/cortex/internal/scheduling/reservations" @@ -429,7 +430,7 @@ func TestProbeScheduler_CapacityCalculation(t *testing.T) { } flavor := compute.FlavorInGroup{Name: "test-flavor", MemoryMB: memMB} - capacity, hosts, err := c.probeScheduler(context.Background(), flavor, "az-a", "test-pipeline", hvByName) + capacity, hosts, err := c.probeScheduler(context.Background(), flavor, "az-a", "test-pipeline", hvByName, scheduling.Options{SkipHistory: true}) if err != nil { t.Fatalf("probeScheduler failed: %v", err) } diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index 14cb65406..b156f5f58 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -32,7 +32,7 @@ const ( PipelineAcknowledgeFailoverReservation = "kvm-acknowledge-failover-reservation" ) -func (c *FailoverReservationController) queryHypervisorsFromScheduler(ctx context.Context, vm VM, allHypervisors []string, pipeline string, resSpec resolvedReservationSpec) ([]string, error) { +func (c *FailoverReservationController) queryHypervisorsFromScheduler(ctx context.Context, vm VM, allHypervisors []string, pipeline string, resSpec resolvedReservationSpec, opts scheduling.Options) ([]string, error) { logger := LoggerFromContext(ctx) // Build list of eligible hypervisors (excluding VM's current hypervisor) @@ -93,7 +93,7 @@ func (c *FailoverReservationController) queryHypervisorsFromScheduler(ctx contex "eligibleHypervisors", len(eligibleHypervisors), "ignoreHypervisors", ignoreHypervisors) - scheduleResp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{LockReservations: true, SkipHistory: true}) + scheduleResp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, opts) if err != nil { logger.Error(err, "failed to schedule failover reservation", "vmUUID", vm.UUID, "pipeline", pipeline) return nil, fmt.Errorf("failed to schedule failover reservation: %w", err) @@ -123,7 +123,7 @@ func (c *FailoverReservationController) tryReuseExistingReservation( logger := LoggerFromContext(ctx) - validHypervisors, err := c.queryHypervisorsFromScheduler(ctx, vm, allHypervisors, PipelineReuseFailoverReservation, resSpec) + validHypervisors, err := c.queryHypervisorsFromScheduler(ctx, vm, allHypervisors, PipelineReuseFailoverReservation, resSpec, scheduling.Options{ReadOnly: true, SkipHistory: true}) if err != nil { logger.Error(err, "failed to get potential hypervisors for VM", "vmUUID", vm.UUID) return nil @@ -266,7 +266,7 @@ func (c *FailoverReservationController) scheduleAndBuildNewFailoverReservation( // Get potential hypervisors from scheduler using the reservation spec resources // (which may be sized to the LargestFlavor from the flavor group) - validHypervisors, err := c.queryHypervisorsFromScheduler(ctx, vm, allHypervisors, PipelineNewFailoverReservation, resSpec) + validHypervisors, err := c.queryHypervisorsFromScheduler(ctx, vm, allHypervisors, PipelineNewFailoverReservation, resSpec, scheduling.Options{LockReservations: true, SkipHistory: true}) if err != nil { return nil, fmt.Errorf("failed to get potential hypervisors for VM: %w", err) } From 2871d18ed729db6438c0897a32f69b23f2ae8003 Mon Sep 17 00:00:00 2001 From: mblos Date: Mon, 11 May 2026 14:34:12 +0200 Subject: [PATCH 16/19] fix --- .../cortex-nova/templates/pipelines_kvm.yaml | 33 +++++-------------- .../reservations/capacity/controller.go | 6 +++- .../failover/reservation_scheduling.go | 2 +- .../reservations/scheduler_client.go | 4 +++ 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/helm/bundles/cortex-nova/templates/pipelines_kvm.yaml b/helm/bundles/cortex-nova/templates/pipelines_kvm.yaml index e11edb281..92a67110b 100644 --- a/helm/bundles/cortex-nova/templates/pipelines_kvm.yaml +++ b/helm/bundles/cortex-nova/templates/pipelines_kvm.yaml @@ -70,14 +70,8 @@ spec: - name: filter_has_enough_capacity description: | This step will filter out hosts that do not have enough available capacity - to host the requested flavor. If enabled, this step will subtract the - current reservations residing on this host from the available capacity. - params: - # If reserved space should be locked even for matching requests. - # For the reservations pipeline, we don't want to unlock - # reserved space, to avoid reservations for the same project - # and flavor to overlap. - - {key: lockReserved, boolValue: false} + to host the requested flavor. Reservation unlocking behavior is controlled + by the call-time Options.LockReservations flag, not by a step param. - name: filter_allowed_projects description: | This step filters hosts based on allowed projects defined in the @@ -208,14 +202,8 @@ spec: - name: filter_has_enough_capacity description: | This step will filter out hosts that do not have enough available capacity - to host the requested flavor. If enabled, this step will subtract the - current reservations residing on this host from the available capacity. - params: - # If reserved space should be locked even for matching requests. - # For the reservations pipeline, we don't want to unlock - # reserved space, to avoid reservations for the same project - # and flavor to overlap. - - {key: lockReserved, boolValue: false} + to host the requested flavor. Reservation unlocking behavior is controlled + by the call-time Options.LockReservations flag, not by a step param. - name: filter_allowed_projects description: | This step filters hosts based on allowed projects defined in the @@ -379,10 +367,9 @@ spec: - name: filter_has_enough_capacity description: | This step will filter out hosts that do not have enough available capacity - to host the requested flavor. Reservations are considered to ensure we - don't double-book capacity. - params: - - {key: lockReserved, boolValue: true} + to host the requested flavor. Reservations are locked via the call-time + Options.LockReservations flag (set by the ack caller) to prevent false-positive + unlocking of CR slots during evacuation validation. - name: filter_has_requested_traits description: | This step filters hosts that do not have the requested traits given by the @@ -453,10 +440,8 @@ spec: - name: filter_has_enough_capacity description: | Filters hosts that cannot fit the flavor based on raw hardware capacity. - VM allocations and all reservation types are ignored to represent an - empty datacenter scenario. - params: - - {key: ignoredReservationTypes, stringListValue: ["CommittedResourceReservation", "FailoverReservation"]} + VM allocations and all reservation types are ignored via call-time Options + (AssumeEmptyHosts + IgnoredReservationTypes set by the capacity controller). - name: filter_has_requested_traits description: | Ensures hosts have the hardware traits required by the flavor. diff --git a/internal/scheduling/reservations/capacity/controller.go b/internal/scheduling/reservations/capacity/controller.go index b9d0315b6..b67281dcf 100644 --- a/internal/scheduling/reservations/capacity/controller.go +++ b/internal/scheduling/reservations/capacity/controller.go @@ -148,7 +148,11 @@ func (c *Controller) reconcileOne( cur := existingByName[flavor.Name] cur.FlavorName = flavor.Name - totalVMSlots, totalHosts, totalErr := c.probeScheduler(ctx, flavor, az, c.config.TotalPipeline, hvByName, scheduling.Options{SkipHistory: true, AssumeEmptyHosts: true}) + totalVMSlots, totalHosts, totalErr := c.probeScheduler(ctx, flavor, az, c.config.TotalPipeline, hvByName, scheduling.Options{ + SkipHistory: true, + AssumeEmptyHosts: true, + IgnoredReservationTypes: []v1alpha1.ReservationType{v1alpha1.ReservationTypeCommittedResource, v1alpha1.ReservationTypeFailover}, + }) placeableVMs, placeableHosts, placeableErr := c.probeScheduler(ctx, flavor, az, c.config.PlaceablePipeline, hvByName, scheduling.Options{SkipHistory: true}) if totalErr != nil { diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index b156f5f58..aae810849 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -224,7 +224,7 @@ func (c *FailoverReservationController) validateVMViaSchedulerEvacuation( "vmCurrentHost", vm.CurrentHypervisor, "pipeline", PipelineAcknowledgeFailoverReservation) - resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{ReadOnly: true, SkipHistory: true}) + resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{ReadOnly: true, LockReservations: true, SkipHistory: true}) if err != nil { logger.Error(err, "failed to validate VM for reservation host", "vmUUID", vm.UUID, "reservationHost", reservationHost) return false, fmt.Errorf("failed to validate VM for reservation host: %w", err) diff --git a/internal/scheduling/reservations/scheduler_client.go b/internal/scheduling/reservations/scheduler_client.go index c23e66354..fb3ef269b 100644 --- a/internal/scheduling/reservations/scheduler_client.go +++ b/internal/scheduling/reservations/scheduler_client.go @@ -93,6 +93,10 @@ type ScheduleReservationResponse struct { func (c *SchedulerClient) ScheduleReservation(ctx context.Context, req ScheduleReservationRequest, opts scheduling.Options) (*ScheduleReservationResponse, error) { logger := loggerFromContext(ctx) + if err := opts.Validate(); err != nil { + return nil, fmt.Errorf("invalid scheduling options: %w", err) + } + // Build weights map (all zero for reservations) weights := make(map[string]float64, len(req.EligibleHosts)) for _, host := range req.EligibleHosts { From 4ee2c6a40292e38b2c1bdc98e2a80771bb1db45d Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 12 May 2026 08:11:16 +0200 Subject: [PATCH 17/19] fixes --- api/scheduling/options.go | 13 +++++-------- api/scheduling/options_test.go | 4 +--- .../machines/filter_weigher_pipeline_controller.go | 2 +- .../pods/filter_weigher_pipeline_controller.go | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/api/scheduling/options.go b/api/scheduling/options.go index c182fbdd4..dc9366d98 100644 --- a/api/scheduling/options.go +++ b/api/scheduling/options.go @@ -15,22 +15,19 @@ import ( type Options struct { // ReadOnly means the pipeline run does not modify shared scheduling state (reservations, // history, inflight records). Concurrent read-only runs are safe under a shared read lock. - // Note: the controller may still write the Decision status after Run() regardless of this flag. ReadOnly bool `json:"read_only,omitempty"` - // LockReservations prevents reservation unlocking, e.g. in the capacity filter. - // Set when finding hosts for new reservations (failover, CR) to see true available capacity. + // LockReservations prevents reservation unlocking, i.e. considering those as unavailable resources. LockReservations bool `json:"lock_reservations,omitempty"` - // AssumeEmptyHosts treats all hosts as having no running VMs. + // AssumeEmptyHosts ignores running instances on hosts, considering them as empty. AssumeEmptyHosts bool `json:"assume_empty_hosts,omitempty"` - // IgnoredReservationTypes lists reservation types the capacity filter skips entirely. + // IgnoredReservationTypes lists reservation types whose reserved capacity the capacity filter does not block. IgnoredReservationTypes []v1alpha1.ReservationType `json:"ignored_reservation_types,omitempty"` // MaxCandidates limits the number of hosts returned after weighing. 0 means no limit. MaxCandidates int `json:"max_candidates,omitempty"` // SkipHistory skips recording the placement decision in placement history. - // Defaults to false so Nova requests record history without needing to set this explicitly. SkipHistory bool `json:"skip_history,omitempty"` - // CreateInflight creates pessimistic blocking reservations for all returned candidates. + // CreateInflight creates pessimistic blocking reservations. CreateInflight bool `json:"create_inflight,omitempty"` } @@ -40,7 +37,7 @@ func (o Options) Validate() error { return errors.New("read-only runs must not write scheduling history: set SkipHistory=true") } if o.ReadOnly && o.CreateInflight { - return errors.New("ReadOnly and CreateInflight are mutually exclusive: read-only runs must not create inflight reservations") + return errors.New("read-only runs cannot create inflight reservations") } return nil } diff --git a/api/scheduling/options_test.go b/api/scheduling/options_test.go index 870609281..e20b8e0df 100644 --- a/api/scheduling/options_test.go +++ b/api/scheduling/options_test.go @@ -12,12 +12,10 @@ func TestOptions_Validate(t *testing.T) { wantErr bool }{ {"zero value is valid", Options{}, false}, - {"write run, history recorded by default", Options{}, false}, {"write run with inflight", Options{CreateInflight: true}, false}, {"read-only run, skipping history", Options{ReadOnly: true, SkipHistory: true}, false}, {"ReadOnly without SkipHistory is invalid", Options{ReadOnly: true}, true}, - {"ReadOnly + CreateInflight is invalid", Options{ReadOnly: true, CreateInflight: true}, true}, - {"ReadOnly + both invalid", Options{ReadOnly: true, CreateInflight: true}, true}, + {"ReadOnly + CreateInflight is invalid", Options{ReadOnly: true, SkipHistory: true, CreateInflight: true}, true}, } for _, tt := range tests { diff --git a/internal/scheduling/machines/filter_weigher_pipeline_controller.go b/internal/scheduling/machines/filter_weigher_pipeline_controller.go index 29627814f..0063010d8 100644 --- a/internal/scheduling/machines/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/machines/filter_weigher_pipeline_controller.go @@ -132,7 +132,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision return errors.New("no machine pools available for scheduling") } - // Execute the scheduling pipeline. + // Execute the scheduling pipeline. Options not set: machine scheduling always records history. request := ironcore.MachinePipelineRequest{Pools: pools.Items} result, err := pipeline.Run(request) if !request.Options.SkipHistory { diff --git a/internal/scheduling/pods/filter_weigher_pipeline_controller.go b/internal/scheduling/pods/filter_weigher_pipeline_controller.go index aa28d988f..cfbd06315 100644 --- a/internal/scheduling/pods/filter_weigher_pipeline_controller.go +++ b/internal/scheduling/pods/filter_weigher_pipeline_controller.go @@ -146,7 +146,7 @@ func (c *FilterWeigherPipelineController) process(ctx context.Context, decision return errors.New("no nodes available for scheduling") } - // Execute the scheduling pipeline. + // Execute the scheduling pipeline. Options not set: pod scheduling always records history. request := pods.PodPipelineRequest{Nodes: nodes.Items, Pod: *pod} result, err := pipeline.Run(request) if !request.Options.SkipHistory { From 532200d0b6ef01066c202c94908127562ff9a4ff Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 12 May 2026 08:29:31 +0200 Subject: [PATCH 18/19] refactor --- api/scheduling/options.go | 6 +++--- api/scheduling/options_test.go | 5 ++--- docs/apis.md | 2 ++ internal/scheduling/reservations/capacity/controller.go | 3 ++- .../reservations/commitments/reservation_controller.go | 2 +- .../reservations/failover/reservation_scheduling.go | 6 +++--- 6 files changed, 13 insertions(+), 11 deletions(-) diff --git a/api/scheduling/options.go b/api/scheduling/options.go index dc9366d98..0ad2877fc 100644 --- a/api/scheduling/options.go +++ b/api/scheduling/options.go @@ -27,8 +27,8 @@ type Options struct { // SkipHistory skips recording the placement decision in placement history. SkipHistory bool `json:"skip_history,omitempty"` - // CreateInflight creates pessimistic blocking reservations. - CreateInflight bool `json:"create_inflight,omitempty"` + // SkipInflight skips creating pessimistic blocking reservations for returned candidates. + SkipInflight bool `json:"skip_inflight,omitempty"` } // Validate checks for mutually exclusive or inconsistent option combinations. @@ -36,7 +36,7 @@ func (o Options) Validate() error { if o.ReadOnly && !o.SkipHistory { return errors.New("read-only runs must not write scheduling history: set SkipHistory=true") } - if o.ReadOnly && o.CreateInflight { + if o.ReadOnly && !o.SkipInflight { return errors.New("read-only runs cannot create inflight reservations") } return nil diff --git a/api/scheduling/options_test.go b/api/scheduling/options_test.go index e20b8e0df..5283c3317 100644 --- a/api/scheduling/options_test.go +++ b/api/scheduling/options_test.go @@ -12,10 +12,9 @@ func TestOptions_Validate(t *testing.T) { wantErr bool }{ {"zero value is valid", Options{}, false}, - {"write run with inflight", Options{CreateInflight: true}, false}, - {"read-only run, skipping history", Options{ReadOnly: true, SkipHistory: true}, false}, + {"read-only run, skipping history and inflight", Options{ReadOnly: true, SkipHistory: true, SkipInflight: true}, false}, {"ReadOnly without SkipHistory is invalid", Options{ReadOnly: true}, true}, - {"ReadOnly + CreateInflight is invalid", Options{ReadOnly: true, SkipHistory: true, CreateInflight: true}, true}, + {"ReadOnly without SkipInflight is invalid", Options{ReadOnly: true, SkipHistory: true}, true}, } for _, tt := range tests { diff --git a/docs/apis.md b/docs/apis.md index d3a2d9416..41c47dca9 100644 --- a/docs/apis.md +++ b/docs/apis.md @@ -71,6 +71,8 @@ Pipelines bundle scheduling steps together. Filters are mandatory, while weigher The state of the pipeline is propagated automatically through the states of its steps. Check the pipeline state object to determine if the pipeline can currently be executed or not. +Pipeline behavior has two configuration layers: static per-step params defined in the Pipeline CRD YAML (thresholds, weights, traits), and call-time `Options` set by the controller invoking the pipeline (e.g. whether to record history, lock reservations, or skip VM allocation accounting). See [Pipeline Scheduling Options](configurable-pipeline-concept.md) for details. + ### Decisions ```bash diff --git a/internal/scheduling/reservations/capacity/controller.go b/internal/scheduling/reservations/capacity/controller.go index b67281dcf..6e40cd562 100644 --- a/internal/scheduling/reservations/capacity/controller.go +++ b/internal/scheduling/reservations/capacity/controller.go @@ -150,10 +150,11 @@ func (c *Controller) reconcileOne( totalVMSlots, totalHosts, totalErr := c.probeScheduler(ctx, flavor, az, c.config.TotalPipeline, hvByName, scheduling.Options{ SkipHistory: true, + SkipInflight: true, AssumeEmptyHosts: true, IgnoredReservationTypes: []v1alpha1.ReservationType{v1alpha1.ReservationTypeCommittedResource, v1alpha1.ReservationTypeFailover}, }) - placeableVMs, placeableHosts, placeableErr := c.probeScheduler(ctx, flavor, az, c.config.PlaceablePipeline, hvByName, scheduling.Options{SkipHistory: true}) + placeableVMs, placeableHosts, placeableErr := c.probeScheduler(ctx, flavor, az, c.config.PlaceablePipeline, hvByName, scheduling.Options{SkipHistory: true, SkipInflight: true}) if totalErr != nil { allFresh = false diff --git a/internal/scheduling/reservations/commitments/reservation_controller.go b/internal/scheduling/reservations/commitments/reservation_controller.go index 4cc545a81..04d7733bc 100644 --- a/internal/scheduling/reservations/commitments/reservation_controller.go +++ b/internal/scheduling/reservations/commitments/reservation_controller.go @@ -294,7 +294,7 @@ func (r *CommitmentReservationController) Reconcile(ctx context.Context, req ctr IgnoredReservationTypes: nil, MaxCandidates: 1, SkipHistory: true, - CreateInflight: false, // not a VM placement; no pessimistic blocking needed + SkipInflight: false, // TODO pessimistic blocking needed, will be addressed in follow up ticket } scheduleResp, err := r.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduleOpts) diff --git a/internal/scheduling/reservations/failover/reservation_scheduling.go b/internal/scheduling/reservations/failover/reservation_scheduling.go index aae810849..63bf5b0f5 100644 --- a/internal/scheduling/reservations/failover/reservation_scheduling.go +++ b/internal/scheduling/reservations/failover/reservation_scheduling.go @@ -123,7 +123,7 @@ func (c *FailoverReservationController) tryReuseExistingReservation( logger := LoggerFromContext(ctx) - validHypervisors, err := c.queryHypervisorsFromScheduler(ctx, vm, allHypervisors, PipelineReuseFailoverReservation, resSpec, scheduling.Options{ReadOnly: true, SkipHistory: true}) + validHypervisors, err := c.queryHypervisorsFromScheduler(ctx, vm, allHypervisors, PipelineReuseFailoverReservation, resSpec, scheduling.Options{ReadOnly: true, SkipHistory: true, SkipInflight: true}) if err != nil { logger.Error(err, "failed to get potential hypervisors for VM", "vmUUID", vm.UUID) return nil @@ -224,7 +224,7 @@ func (c *FailoverReservationController) validateVMViaSchedulerEvacuation( "vmCurrentHost", vm.CurrentHypervisor, "pipeline", PipelineAcknowledgeFailoverReservation) - resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{ReadOnly: true, LockReservations: true, SkipHistory: true}) + resp, err := c.SchedulerClient.ScheduleReservation(ctx, scheduleReq, scheduling.Options{ReadOnly: true, LockReservations: true, SkipHistory: true, SkipInflight: true}) if err != nil { logger.Error(err, "failed to validate VM for reservation host", "vmUUID", vm.UUID, "reservationHost", reservationHost) return false, fmt.Errorf("failed to validate VM for reservation host: %w", err) @@ -266,7 +266,7 @@ func (c *FailoverReservationController) scheduleAndBuildNewFailoverReservation( // Get potential hypervisors from scheduler using the reservation spec resources // (which may be sized to the LargestFlavor from the flavor group) - validHypervisors, err := c.queryHypervisorsFromScheduler(ctx, vm, allHypervisors, PipelineNewFailoverReservation, resSpec, scheduling.Options{LockReservations: true, SkipHistory: true}) + validHypervisors, err := c.queryHypervisorsFromScheduler(ctx, vm, allHypervisors, PipelineNewFailoverReservation, resSpec, scheduling.Options{LockReservations: true, SkipHistory: true, SkipInflight: true}) if err != nil { return nil, fmt.Errorf("failed to get potential hypervisors for VM: %w", err) } From b535ec019098c02fad9b0cdabf155b75a455226a Mon Sep 17 00:00:00 2001 From: mblos Date: Tue, 12 May 2026 09:02:27 +0200 Subject: [PATCH 19/19] . --- docs/apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apis.md b/docs/apis.md index 80e721b12..f4d44dcd4 100644 --- a/docs/apis.md +++ b/docs/apis.md @@ -75,7 +75,7 @@ Pipelines bundle scheduling steps together. Filters are mandatory, while weigher The state of the pipeline is propagated automatically through the states of its steps. Check the pipeline state object to determine if the pipeline can currently be executed or not. -Pipeline behavior has two configuration layers: static per-step params defined in the Pipeline CRD YAML (thresholds, weights, traits), and call-time `Options` set by the controller invoking the pipeline (e.g. whether to record history, lock reservations, or skip VM allocation accounting). See [Pipeline Scheduling Options](configurable-pipeline-concept.md) for details. +Pipeline behavior has two configuration layers: static per-step params defined in the Pipeline CRD YAML (thresholds, weights, traits), and call-time `Options` set by the controller invoking the pipeline (e.g. whether to record history, lock reservations, or skip VM allocation accounting). ### Decisions