Skip to content

Commit 178a5e9

Browse files
committed
add 'local' selectors to cluster definitions in scheduler config
1 parent c49b020 commit 178a5e9

File tree

8 files changed

+161
-7
lines changed

8 files changed

+161
-7
lines changed

internal/config/config_scheduler.go

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package config
22

33
import (
44
"fmt"
5+
"maps"
56
"slices"
67

78
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -24,10 +25,13 @@ type SchedulerConfig struct {
2425
Strategy Strategy `json:"strategy"`
2526

2627
// +optional
27-
Selectors *SchedulerSelectors `json:"selectors,omitempty"`
28+
Selectors *SchedulerSelectors `json:"selectors,omitempty"`
29+
// Note that CompletedSelectors.Clusters holds the global cluster selector.
30+
// During Complete(), the local selector is merged with the global one (or set to the global one if nil).
31+
// This means that always the local completed selector should be used, unless the task is not tied to a specific ClusterDefinition.
2832
CompletedSelectors CompletedSchedulerSelectors `json:"-"`
2933

30-
PurposeMappings map[string]ClusterDefinition `json:"purposeMappings"`
34+
PurposeMappings map[string]*ClusterDefinition `json:"purposeMappings"`
3135
}
3236

3337
type SchedulerScope string
@@ -51,7 +55,9 @@ type ClusterDefinition struct {
5155
// Must be equal to or greater than 0 otherwise, with 0 meaning "unlimited".
5256
TenancyCount int `json:"tenancyCount,omitempty"`
5357

54-
Template ClusterTemplate `json:"template"`
58+
Template ClusterTemplate `json:"template"`
59+
Selector *metav1.LabelSelector `json:"selector,omitempty"`
60+
CompletedSelector labels.Selector `json:"-"`
5561
}
5662

5763
type ClusterTemplate struct {
@@ -76,7 +82,7 @@ func (c *SchedulerConfig) Default(_ *field.Path) error {
7682
c.Strategy = STRATEGY_BALANCED
7783
}
7884
if c.PurposeMappings == nil {
79-
c.PurposeMappings = map[string]ClusterDefinition{}
85+
c.PurposeMappings = map[string]*ClusterDefinition{}
8086
}
8187
return nil
8288
}
@@ -118,7 +124,11 @@ func (c *SchedulerConfig) Validate(fldPath *field.Path) error {
118124
for purpose, definition := range c.PurposeMappings {
119125
pPath := fldPath.Key(purpose)
120126
if purpose == "" {
121-
errs = append(errs, field.Invalid(pPath, purpose, "purpose must not be empty"))
127+
errs = append(errs, field.Invalid(fldPath, purpose, "purpose must not be empty"))
128+
}
129+
if definition == nil {
130+
errs = append(errs, field.Required(pPath, "definition must not be nil"))
131+
continue
122132
}
123133
if definition.TenancyCount < 0 {
124134
errs = append(errs, field.Invalid(pPath.Child("tenancyCount"), definition.TenancyCount, "tenancyCount must be greater than or equal to 0"))
@@ -137,7 +147,18 @@ func (c *SchedulerConfig) Validate(fldPath *field.Path) error {
137147
errs = append(errs, field.Invalid(pPath.Child("tenancyCount"), definition.TenancyCount, fmt.Sprintf("tenancyCount must be 0 if the template specifies '%s' tenancy", string(clustersv1alpha1.TENANCY_EXCLUSIVE))))
138148
}
139149
if cls != nil && !cls.Matches(labels.Set(definition.Template.Labels)) {
140-
errs = append(errs, field.Invalid(pPath.Child("template").Child("metadata").Child("labels"), definition.Template.Labels, "labels do not match specified cluster selector"))
150+
errs = append(errs, field.Invalid(pPath.Child("template").Child("metadata").Child("labels"), definition.Template.Labels, "labels do not match specified global cluster selector"))
151+
}
152+
var lcls labels.Selector
153+
if definition.Selector != nil {
154+
var err error
155+
lcls, err = metav1.LabelSelectorAsSelector(definition.Selector)
156+
if err != nil {
157+
errs = append(errs, field.Invalid(pPath.Child("selector"), definition.Selector, err.Error()))
158+
}
159+
}
160+
if lcls != nil && !lcls.Matches(labels.Set(definition.Template.Labels)) {
161+
errs = append(errs, field.Invalid(pPath.Child("template").Child("metadata").Child("labels"), definition.Template.Labels, "labels do not match specified local cluster selector"))
141162
}
142163
}
143164
return errs.ToAggregate()
@@ -166,5 +187,37 @@ func (c *SchedulerConfig) Complete(fldPath *field.Path) error {
166187
if c.CompletedSelectors.ClusterRequests == nil {
167188
c.CompletedSelectors.ClusterRequests = labels.Everything()
168189
}
190+
191+
for purpose, definition := range c.PurposeMappings {
192+
pPath := fldPath.Child("purposeMappings").Key(purpose)
193+
if definition.Selector != nil {
194+
var combinedSelector *metav1.LabelSelector
195+
if c.Selectors.Clusters == nil {
196+
combinedSelector = definition.Selector
197+
} else if definition.Selector == nil {
198+
combinedSelector = c.Selectors.Clusters
199+
} else {
200+
combinedSelector = c.Selectors.Clusters.DeepCopy()
201+
if combinedSelector.MatchLabels == nil {
202+
combinedSelector.MatchLabels = definition.Selector.MatchLabels
203+
} else if definition.Selector.MatchLabels != nil {
204+
maps.Insert(combinedSelector.MatchLabels, maps.All(definition.Selector.MatchLabels))
205+
}
206+
if combinedSelector.MatchExpressions == nil {
207+
combinedSelector.MatchExpressions = definition.Selector.MatchExpressions
208+
} else if definition.Selector.MatchExpressions != nil {
209+
combinedSelector.MatchExpressions = append(combinedSelector.MatchExpressions, definition.Selector.MatchExpressions...)
210+
}
211+
}
212+
var err error
213+
definition.CompletedSelector, err = metav1.LabelSelectorAsSelector(combinedSelector)
214+
if err != nil {
215+
return field.Invalid(pPath.Child("selector"), combinedSelector, fmt.Sprintf("the combination of the global and local selector is invalid: %s", err.Error()))
216+
}
217+
} else {
218+
definition.CompletedSelector = c.CompletedSelectors.Clusters
219+
}
220+
}
221+
169222
return nil
170223
}

internal/controllers/scheduler/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func (r *ClusterScheduler) reconcile(ctx context.Context, log logging.Logger, re
149149
namespace = cDef.Template.Namespace
150150
}
151151
clusterList := &clustersv1alpha1.ClusterList{}
152-
if err := r.OnboardingCluster.Client().List(ctx, clusterList, client.InNamespace(namespace), client.MatchingLabelsSelector{Selector: r.Config.CompletedSelectors.Clusters}); err != nil {
152+
if err := r.OnboardingCluster.Client().List(ctx, clusterList, client.InNamespace(namespace), client.MatchingLabelsSelector{Selector: cDef.CompletedSelector}); err != nil {
153153
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error listing Clusters: %w", err), cconst.ReasonOnboardingClusterInteractionProblem)
154154
return rr
155155
}

internal/controllers/scheduler/controller_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,4 +365,38 @@ var _ = Describe("Scheduler", func() {
365365

366366
})
367367

368+
It("should combine cluster label selectors correctly", func() {
369+
_, env := defaultTestSetup("testdata", "test-04")
370+
371+
// should use the existing cluster
372+
req := &clustersv1alpha1.ClusterRequest{}
373+
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("shared", "foo"), req)).To(Succeed())
374+
Expect(req.Status.Cluster).To(BeNil())
375+
env.ShouldReconcile(testutils.RequestFromObject(req))
376+
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(req), req)).To(Succeed())
377+
Expect(req.Status.Cluster).ToNot(BeNil())
378+
Expect(req.Status.Cluster.Name).To(Equal("shared"))
379+
Expect(req.Status.Cluster.Namespace).To(Equal("foo"))
380+
381+
// should create a new cluster
382+
req2 := &clustersv1alpha1.ClusterRequest{}
383+
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("shared2", "foo"), req2)).To(Succeed())
384+
Expect(req2.Status.Cluster).To(BeNil())
385+
env.ShouldReconcile(testutils.RequestFromObject(req2))
386+
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(req2), req2)).To(Succeed())
387+
Expect(req2.Status.Cluster).ToNot(BeNil())
388+
Expect(req2.Status.Cluster.Name).To(Equal("shared2"))
389+
Expect(req2.Status.Cluster.Namespace).To(Equal("foo"))
390+
391+
// should use the existing cluster
392+
req3 := &clustersv1alpha1.ClusterRequest{}
393+
Expect(env.Client().Get(env.Ctx, ctrlutils.ObjectKey("shared3", "foo"), req3)).To(Succeed())
394+
Expect(req3.Status.Cluster).To(BeNil())
395+
env.ShouldReconcile(testutils.RequestFromObject(req3))
396+
Expect(env.Client().Get(env.Ctx, client.ObjectKeyFromObject(req3), req3)).To(Succeed())
397+
Expect(req3.Status.Cluster).ToNot(BeNil())
398+
Expect(req3.Status.Cluster.Name).To(Equal("shared2"))
399+
Expect(req3.Status.Cluster.Namespace).To(Equal("foo"))
400+
})
401+
368402
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: Cluster
3+
metadata:
4+
name: shared
5+
namespace: foo
6+
labels:
7+
foo.bar.baz/foobar: "true"
8+
spec:
9+
profile: test-profile
10+
kubernetes:
11+
version: "1.32"
12+
purposes:
13+
- test
14+
- shared
15+
- shared2
16+
tenancy: Shared
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: ClusterRequest
3+
metadata:
4+
name: shared
5+
namespace: foo
6+
uid: 4d6aa495-54c0-4df2-bc7b-05b103f02e69
7+
spec:
8+
purpose: shared
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: ClusterRequest
3+
metadata:
4+
name: shared2
5+
namespace: foo
6+
uid: bf7d8a52-ea1e-4303-a0f9-bf2ef0cc21ea
7+
spec:
8+
purpose: shared2
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: clusters.openmcp.cloud/v1alpha1
2+
kind: ClusterRequest
3+
metadata:
4+
name: shared3
5+
namespace: foo
6+
uid: 7642e165-196c-478f-a225-765bd97a29d8
7+
spec:
8+
purpose: shared2
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
scheduler:
2+
scope: Cluster
3+
selectors:
4+
clusters:
5+
matchLabels:
6+
foo.bar.baz/foobar: "true"
7+
purposeMappings:
8+
shared:
9+
template:
10+
metadata:
11+
labels:
12+
foo.bar.baz/foobar: "true"
13+
spec:
14+
profile: test-profile
15+
tenancy: Shared
16+
shared2:
17+
selector:
18+
matchLabels:
19+
foo.bar.baz/foobaz: "true"
20+
template:
21+
metadata:
22+
labels:
23+
foo.bar.baz/foobar: "true"
24+
foo.bar.baz/foobaz: "true"
25+
spec:
26+
profile: test-profile
27+
tenancy: Shared

0 commit comments

Comments
 (0)