Skip to content

Commit d1eafec

Browse files
authored
Support concatenated aggregates in filter requested destination (#637)
1 parent 4d30ee7 commit d1eafec

2 files changed

Lines changed: 108 additions & 14 deletions

File tree

internal/scheduling/nova/plugins/filters/filter_requested_destination.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"log/slog"
99
"slices"
10+
"strings"
1011

1112
api "github.com/cobaltcore-dev/cortex/api/external/nova"
1213
"github.com/cobaltcore-dev/cortex/internal/scheduling/lib"
@@ -38,8 +39,11 @@ type FilterRequestedDestinationStep struct {
3839
}
3940

4041
// processRequestedAggregates filters hosts based on the requested aggregates.
41-
// It removes hosts that are not part of any of the requested aggregates,
42-
// respecting the IgnoredAggregates option. Returns early without filtering
42+
// The aggregates list uses AND logic between elements, meaning a host must match
43+
// ALL elements to pass. Each element can contain comma-separated UUIDs which use
44+
// OR logic, meaning the host only needs to match ONE of the UUIDs in that group.
45+
// Example: ["agg1", "agg2,agg3"] means host must be in agg1 AND (agg2 OR agg3).
46+
// Respects the IgnoredAggregates option and returns early without filtering
4347
// if all requested aggregates are in the ignored list.
4448
func (s *FilterRequestedDestinationStep) processRequestedAggregates(
4549
traceLog *slog.Logger,
@@ -51,13 +55,12 @@ func (s *FilterRequestedDestinationStep) processRequestedAggregates(
5155
if len(aggregates) == 0 {
5256
return
5357
}
58+
// Filter out ignored aggregates
5459
aggregatesToConsider := make([]string, 0, len(aggregates))
5560
for _, agg := range aggregates {
56-
if slices.Contains(s.Options.IgnoredAggregates, agg) {
57-
traceLog.Info("ignoring aggregate in requested_destination as it is in the ignored list", "aggregate", agg)
58-
continue
61+
if !slices.Contains(s.Options.IgnoredAggregates, agg) {
62+
aggregatesToConsider = append(aggregatesToConsider, agg)
5963
}
60-
aggregatesToConsider = append(aggregatesToConsider, agg)
6164
}
6265
if len(aggregatesToConsider) == 0 {
6366
traceLog.Info("all aggregates in requested_destination are in the ignored list, skipping aggregate filtering")
@@ -74,14 +77,24 @@ func (s *FilterRequestedDestinationStep) processRequestedAggregates(
7477
for _, agg := range hv.Status.Aggregates {
7578
hvAggregateUUIDs = append(hvAggregateUUIDs, agg.UUID)
7679
}
77-
found := false
78-
for _, reqAgg := range aggregatesToConsider {
79-
if slices.Contains(hvAggregateUUIDs, reqAgg) {
80-
found = true
80+
// All outer elements must match (AND logic)
81+
// Each element can be comma-separated UUIDs (OR logic within the group)
82+
allMatch := true
83+
for _, reqAggGroup := range aggregatesToConsider {
84+
reqAggs := strings.Split(reqAggGroup, ",")
85+
groupMatch := false
86+
for _, reqAgg := range reqAggs {
87+
if slices.Contains(hvAggregateUUIDs, reqAgg) {
88+
groupMatch = true
89+
break
90+
}
91+
}
92+
if !groupMatch {
93+
allMatch = false
8194
break
8295
}
8396
}
84-
if !found {
97+
if !allMatch {
8598
delete(activations, host)
8699
traceLog.Info(
87100
"filtered out host not in requested_destination aggregates",

internal/scheduling/nova/plugins/filters/filter_requested_destination_test.go

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,13 +200,14 @@ func TestFilterRequestedDestinationStep_Run(t *testing.T) {
200200
expectErr: false,
201201
},
202202
{
203-
name: "Filter by multiple aggregates",
203+
name: "Filter by multiple aggregates with OR syntax (comma-separated)",
204204
request: api.ExternalSchedulerRequest{
205205
Spec: api.NovaObject[api.NovaSpec]{
206206
Data: api.NovaSpec{
207207
RequestedDestination: &api.NovaObject[api.NovaRequestedDestination]{
208208
Data: api.NovaRequestedDestination{
209-
Aggregates: []string{"aggregate1", "aggregate3"},
209+
// "aggregate1,aggregate3" means host must be in aggregate1 OR aggregate3
210+
Aggregates: []string{"aggregate1,aggregate3"},
210211
},
211212
},
212213
},
@@ -241,12 +242,13 @@ func TestFilterRequestedDestinationStep_Run(t *testing.T) {
241242
expectErr: false,
242243
},
243244
{
244-
name: "Filter by aggregates - hosts in both spec and status",
245+
name: "Filter by multiple aggregates with AND logic",
245246
request: api.ExternalSchedulerRequest{
246247
Spec: api.NovaObject[api.NovaSpec]{
247248
Data: api.NovaSpec{
248249
RequestedDestination: &api.NovaObject[api.NovaRequestedDestination]{
249250
Data: api.NovaRequestedDestination{
251+
// ["aggregate1", "aggregate2"] means host must be in aggregate1 AND aggregate2
250252
Aggregates: []string{"aggregate1", "aggregate2"},
251253
},
252254
},
@@ -258,6 +260,85 @@ func TestFilterRequestedDestinationStep_Run(t *testing.T) {
258260
{ComputeHost: "host3"},
259261
},
260262
},
263+
hypervisors: []hv1.Hypervisor{
264+
{
265+
ObjectMeta: metav1.ObjectMeta{Name: "host1"},
266+
Status: hv1.HypervisorStatus{Aggregates: []hv1.Aggregate{{UUID: "aggregate1"}}},
267+
},
268+
{
269+
ObjectMeta: metav1.ObjectMeta{Name: "host2"},
270+
Status: hv1.HypervisorStatus{Aggregates: []hv1.Aggregate{{UUID: "aggregate1"}, {UUID: "aggregate2"}}},
271+
},
272+
{
273+
ObjectMeta: metav1.ObjectMeta{Name: "host3"},
274+
Status: hv1.HypervisorStatus{Aggregates: []hv1.Aggregate{{UUID: "aggregate2"}}},
275+
},
276+
},
277+
expectedHosts: []string{"host2"},
278+
filteredHosts: []string{"host1", "host3"},
279+
expectErr: false,
280+
},
281+
{
282+
name: "Filter by mixed AND/OR aggregates",
283+
request: api.ExternalSchedulerRequest{
284+
Spec: api.NovaObject[api.NovaSpec]{
285+
Data: api.NovaSpec{
286+
RequestedDestination: &api.NovaObject[api.NovaRequestedDestination]{
287+
Data: api.NovaRequestedDestination{
288+
// Host must be in (aggregate1 OR aggregate2) AND aggregate3
289+
Aggregates: []string{"aggregate1,aggregate2", "aggregate3"},
290+
},
291+
},
292+
},
293+
},
294+
Hosts: []api.ExternalSchedulerHost{
295+
{ComputeHost: "host1"},
296+
{ComputeHost: "host2"},
297+
{ComputeHost: "host3"},
298+
{ComputeHost: "host4"},
299+
},
300+
},
301+
hypervisors: []hv1.Hypervisor{
302+
{
303+
ObjectMeta: metav1.ObjectMeta{Name: "host1"},
304+
Status: hv1.HypervisorStatus{Aggregates: []hv1.Aggregate{{UUID: "aggregate1"}, {UUID: "aggregate3"}}},
305+
},
306+
{
307+
ObjectMeta: metav1.ObjectMeta{Name: "host2"},
308+
Status: hv1.HypervisorStatus{Aggregates: []hv1.Aggregate{{UUID: "aggregate2"}, {UUID: "aggregate3"}}},
309+
},
310+
{
311+
ObjectMeta: metav1.ObjectMeta{Name: "host3"},
312+
Status: hv1.HypervisorStatus{Aggregates: []hv1.Aggregate{{UUID: "aggregate1"}}},
313+
},
314+
{
315+
ObjectMeta: metav1.ObjectMeta{Name: "host4"},
316+
Status: hv1.HypervisorStatus{Aggregates: []hv1.Aggregate{{UUID: "aggregate3"}}},
317+
},
318+
},
319+
expectedHosts: []string{"host1", "host2"},
320+
filteredHosts: []string{"host3", "host4"},
321+
expectErr: false,
322+
},
323+
{
324+
name: "Filter by aggregates with OR syntax - hosts in both spec and status",
325+
request: api.ExternalSchedulerRequest{
326+
Spec: api.NovaObject[api.NovaSpec]{
327+
Data: api.NovaSpec{
328+
RequestedDestination: &api.NovaObject[api.NovaRequestedDestination]{
329+
Data: api.NovaRequestedDestination{
330+
// "aggregate1,aggregate2" means host must be in aggregate1 OR aggregate2
331+
Aggregates: []string{"aggregate1,aggregate2"},
332+
},
333+
},
334+
},
335+
},
336+
Hosts: []api.ExternalSchedulerHost{
337+
{ComputeHost: "host1"},
338+
{ComputeHost: "host2"},
339+
{ComputeHost: "host3"},
340+
},
341+
},
261342
hypervisors: []hv1.Hypervisor{
262343
{
263344
ObjectMeta: metav1.ObjectMeta{Name: "host1"},

0 commit comments

Comments
 (0)