Skip to content

Commit 0eb3064

Browse files
committed
add tests, implement efficiency optimization, refactor
1 parent 600abe8 commit 0eb3064

25 files changed

+1167
-515
lines changed

api/clusters/v1alpha1/cluster_types.go

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import (
44
"encoding/json"
55
"strings"
66

7+
"github.com/openmcp-project/controller-utils/pkg/collections"
78
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
89
"k8s.io/apimachinery/pkg/runtime"
9-
"k8s.io/apimachinery/pkg/util/sets"
1010
)
1111

1212
// ClusterSpec defines the desired state of Cluster
@@ -136,34 +136,40 @@ func (cs *ClusterStatus) SetProviderStatus(from any) error {
136136

137137
// GetTenancyCount returns the number of ClusterRequests currently pointing to this cluster.
138138
// This is determined by counting the finalizers that have the corresponding prefix.
139-
// Note that only unique finalizers are counted, so if there are multiple identical request finalizers
140-
// (which should not happen), this method's return value might not match the actual number of finalizers with the prefix.
141139
func (c *Cluster) GetTenancyCount() int {
142-
return c.GetRequestUIDs().Len()
140+
return collections.AggregateMap(c.GetRequestUIDs(), func(_ string, count int, current int) int {
141+
return count + current
142+
}, 0)
143143
}
144144

145-
// GetRequestUIDs returns the UIDs of all ClusterRequests that have marked this cluster with a corresponding finalizer.
146-
func (c *Cluster) GetRequestUIDs() sets.Set[string] {
147-
res := sets.New[string]()
145+
// GetRequestUIDs returns the UIDs of all ClusterRequests that have marked this cluster with a corresponding finalizer,
146+
// mapped to their respective counts.
147+
// Note that a regular request is currently expected to have exactly one finalizer, so the counts for each UID should be 1.
148+
func (c *Cluster) GetRequestUIDs() map[string]int {
149+
res := map[string]int{}
148150
for _, fin := range c.Finalizers {
149151
if strings.HasPrefix(fin, RequestFinalizerOnClusterPrefix) {
150-
res.Insert(strings.TrimPrefix(fin, RequestFinalizerOnClusterPrefix))
152+
res[strings.TrimPrefix(fin, RequestFinalizerOnClusterPrefix)]++
151153
}
152154
}
153155
return res
154156
}
155157

156-
// GetPreemptiveTenancyCount works like GetTenancyCount, but for preemptive ClusterRequests.
158+
// GetPreemptiveTenancyCount returns the number of PreemptiveClusterRequests currently pointing to this cluster.
159+
// This is determined by counting the finalizers that have the corresponding prefix.
157160
func (c *Cluster) GetPreemptiveTenancyCount() int {
158-
return c.GetPreemptiveRequestUIDs().Len()
161+
return collections.AggregateMap(c.GetPreemptiveRequestUIDs(), func(_ string, count int, current int) int {
162+
return count + current
163+
}, 0)
159164
}
160165

161-
// GetPreemptiveRequestUIDs returns the UIDs of all preemptive ClusterRequests that have marked this cluster with a corresponding finalizer.
162-
func (c *Cluster) GetPreemptiveRequestUIDs() sets.Set[string] {
163-
res := sets.New[string]()
166+
// GetPreemptiveRequestUIDs returns the UIDs of all PreemptiveClusterRequests that have marked this cluster with a corresponding finalizer,
167+
// mapped to their respective counts.
168+
func (c *Cluster) GetPreemptiveRequestUIDs() map[string]int {
169+
res := map[string]int{}
164170
for _, fin := range c.Finalizers {
165171
if strings.HasPrefix(fin, PreemptiveRequestFinalizerOnClusterPrefix) {
166-
res.Insert(strings.TrimPrefix(fin, PreemptiveRequestFinalizerOnClusterPrefix))
172+
res[strings.TrimPrefix(fin, PreemptiveRequestFinalizerOnClusterPrefix)]++
167173
}
168174
}
169175
return res

api/clusters/v1alpha1/constants.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,20 @@ const (
7070
// For AccessRequests, the generic controller that is part of the openMCP Operator sets it.
7171
ProviderLabel = GroupName + "/provider"
7272

73-
// DeleteWithoutRequestsLabel marks that the corresponding cluster can be deleted if the scheduler removes the last request pointing to it.
74-
// Its value must be "true" for the label to take effect.
75-
DeleteWithoutRequestsLabel = GroupName + "/delete-without-requests"
7673
// ProfileLabel is used to make the profile information easily accessible on AccessRequests.
7774
ProfileLabel = GroupName + "/profile"
7875
)
7976

8077
const (
78+
// DeleteWithoutRequestsLabel marks that the corresponding cluster can be deleted if the scheduler removes the last request pointing to it.
79+
// Its value must be "true" for the label to take effect.
80+
DeleteWithoutRequestsLabel = GroupName + "/delete-without-requests"
8181
// ClusterRequestFinalizer is the finalizer used on ClusterRequest resources
8282
ClusterRequestFinalizer = GroupName + "/request"
8383
// RequestFinalizerOnClusterPrefix is the prefix for the finalizers that mark a Cluster as being referenced by a ClusterRequest.
8484
RequestFinalizerOnClusterPrefix = "request." + GroupName + "/"
8585
// PreemptiveRequestFinalizerOnClusterPrefix is the prefix for the finalizers that mark a Cluster as being referenced by a preemptive ClusterRequest.
8686
PreemptiveRequestFinalizerOnClusterPrefix = "preemptive." + GroupName + "/"
87+
// SchedulerCreationIDLabel can be used to track which scheduling operation created a specific Cluster. Mainly for internal purposes.
88+
SchedulerCreationIDLabel = GroupName + "/creation-id"
8789
)

api/crds/manifests/clusters.openmcp.cloud_preemptiveclusterrequests.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ spec:
2323
- jsonPath: .spec.purpose
2424
name: Purpose
2525
type: string
26-
- jsonPath: .spec.workloadCount
26+
- jsonPath: .spec.workload
2727
name: Workload
2828
type: string
2929
- jsonPath: .status.phase
@@ -59,15 +59,15 @@ spec:
5959
x-kubernetes-validations:
6060
- message: purpose is immutable
6161
rule: self == oldSelf
62-
workloadCount:
62+
workload:
6363
description: |-
64-
WorkloadCount specifies for how many workloads this preemptive cluster request should account.
64+
Workload specifies for how many workloads this preemptive cluster request should account.
6565
Must be greater than 0.
6666
minimum: 1
6767
type: integer
6868
required:
6969
- purpose
70-
- workloadCount
70+
- workload
7171
type: object
7272
status:
7373
properties:

internal/controllers/scheduler/controller.go

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -215,48 +215,32 @@ func (r *ClusterScheduler) handleDelete(ctx context.Context, req reconcile.Reque
215215

216216
log.Info("Deleting resource")
217217

218-
// fetch all clusters and filter for the ones that have a finalizer from this request
219-
fin := cr.FinalizerForCluster()
220-
clusterList := &clustersv1alpha1.ClusterList{}
221-
if err := r.PlatformCluster.Client().List(ctx, clusterList, client.MatchingLabelsSelector{Selector: r.Config.Selectors.Clusters.Completed()}); err != nil {
222-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error listing Clusters: %w", err), cconst.ReasonPlatformClusterInteractionProblem)
218+
// fetch cluster definition
219+
purpose := cr.Spec.Purpose
220+
cDef, ok := r.Config.PurposeMappings[purpose]
221+
if !ok {
222+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("no cluster definition found for purpose '%s'", purpose), cconst.ReasonConfigurationProblem)
223223
return rr
224224
}
225-
clusters := make([]*clustersv1alpha1.Cluster, len(clusterList.Items))
226-
for i := range clusterList.Items {
227-
clusters[i] = &clusterList.Items[i]
225+
226+
// fetch relevant clusters
227+
clusters, rerr := fetchRelevantClusters(ctx, r.PlatformCluster.Client(), r.Config.Scope, cDef, cr.Spec.Purpose, cr.Namespace)
228+
if rerr != nil {
229+
rr.ReconcileError = rerr
230+
return rr
228231
}
229-
clusters = filters.FilterSlice(clusters, func(args ...any) bool {
230-
c, ok := args[0].(*clustersv1alpha1.Cluster)
231-
if !ok {
232-
return false
233-
}
234-
return slices.Contains(c.Finalizers, fin)
235-
})
236232

237-
// remove finalizer from all clusters
238-
errs := errutils.NewReasonableErrorList()
239-
for _, c := range clusters {
240-
log.Debug("Removing finalizer from cluster", "clusterName", c.Name, "clusterNamespace", c.Namespace, "finalizer", fin)
241-
oldCluster := c.DeepCopy()
242-
if RemoveFinalizer(c, fin, true) {
243-
if err := r.PlatformCluster.Client().Patch(ctx, c, client.MergeFrom(oldCluster)); err != nil {
244-
errs.Append(errutils.WithReason(fmt.Errorf("error patching finalizer '%s' on cluster '%s/%s': %w", fin, c.Namespace, c.Name, err), cconst.ReasonPlatformClusterInteractionProblem))
245-
}
246-
}
247-
if c.GetTenancyCount() == 0 && c.GetPreemptiveTenancyCount() == 0 && ctrlutils.HasLabelWithValue(c, clustersv1alpha1.DeleteWithoutRequestsLabel, "true") {
248-
log.Info("Deleting cluster without requests", "clusterName", c.Name, "clusterNamespace", c.Namespace)
249-
if err := r.PlatformCluster.Client().Delete(ctx, c); err != nil {
250-
if apierrors.IsNotFound(err) {
251-
log.Info("Cluster already deleted", "clusterName", c.Name, "clusterNamespace", c.Namespace)
252-
} else {
253-
errs.Append(errutils.WithReason(fmt.Errorf("error deleting cluster '%s/%s': %w", c.Namespace, c.Name, err), cconst.ReasonPlatformClusterInteractionProblem))
254-
}
255-
}
256-
}
233+
// run the scheduling algorithm to remove the finalizers from this request
234+
sr, err := Schedule(ctx, clusters, cDef, r.Config.Strategy, NewSchedulingRequest(string(cr.UID), 0, true, cr.Namespace, cr.Spec.Purpose))
235+
if err != nil {
236+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error unscheduling request '%s': %w", req.String(), err), cconst.ReasonSchedulingFailed)
237+
return rr
257238
}
258-
rr.ReconcileError = errs.Aggregate()
259-
if rr.ReconcileError != nil {
239+
240+
// apply required changes to the cluster
241+
_, err = sr.Apply(ctx, r.PlatformCluster.Client())
242+
if err != nil {
243+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error applying scheduling result for request '%s': %w", req.String(), err), cconst.ReasonPlatformClusterInteractionProblem)
260244
return rr
261245
}
262246

internal/controllers/scheduler/controller_preemptive.go

Lines changed: 22 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package scheduler
33
import (
44
"context"
55
"fmt"
6-
"slices"
76

87
apierrors "k8s.io/apimachinery/pkg/api/errors"
98
ctrl "sigs.k8s.io/controller-runtime"
@@ -12,7 +11,6 @@ import (
1211
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1312

1413
"github.com/openmcp-project/controller-utils/pkg/clusters"
15-
"github.com/openmcp-project/controller-utils/pkg/collections/filters"
1614
ctrlutils "github.com/openmcp-project/controller-utils/pkg/controller"
1715
errutils "github.com/openmcp-project/controller-utils/pkg/errors"
1816
"github.com/openmcp-project/controller-utils/pkg/logging"
@@ -151,48 +149,32 @@ func (r *PreemptiveScheduler) handleDelete(ctx context.Context, req reconcile.Re
151149

152150
log.Info("Deleting resource")
153151

154-
// fetch all clusters and filter for the ones that have a finalizer from this request
155-
fin := pcr.FinalizerForCluster()
156-
clusterList := &clustersv1alpha1.ClusterList{}
157-
if err := r.PlatformCluster.Client().List(ctx, clusterList, client.MatchingLabelsSelector{Selector: r.Config.Selectors.Clusters.Completed()}); err != nil {
158-
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error listing Clusters: %w", err), cconst.ReasonPlatformClusterInteractionProblem)
152+
// fetch cluster definition
153+
purpose := pcr.Spec.Purpose
154+
cDef, ok := r.Config.PurposeMappings[purpose]
155+
if !ok {
156+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("no cluster definition found for purpose '%s'", purpose), cconst.ReasonConfigurationProblem)
159157
return rr
160158
}
161-
clusters := make([]*clustersv1alpha1.Cluster, len(clusterList.Items))
162-
for i := range clusterList.Items {
163-
clusters[i] = &clusterList.Items[i]
159+
160+
// fetch relevant clusters
161+
clusters, rerr := fetchRelevantClusters(ctx, r.PlatformCluster.Client(), r.Config.Scope, cDef, pcr.Spec.Purpose, pcr.Namespace)
162+
if rerr != nil {
163+
rr.ReconcileError = rerr
164+
return rr
164165
}
165-
clusters = filters.FilterSlice(clusters, func(args ...any) bool {
166-
c, ok := args[0].(*clustersv1alpha1.Cluster)
167-
if !ok {
168-
return false
169-
}
170-
return slices.Contains(c.Finalizers, fin)
171-
})
172-
173-
// remove finalizer from all clusters
174-
errs := errutils.NewReasonableErrorList()
175-
for _, c := range clusters {
176-
log.Debug("Removing finalizers from cluster", "clusterName", c.Name, "clusterNamespace", c.Namespace, "finalizer", fin)
177-
oldCluster := c.DeepCopy()
178-
if RemoveFinalizer(c, fin, true) {
179-
if err := r.PlatformCluster.Client().Patch(ctx, c, client.MergeFrom(oldCluster)); err != nil {
180-
errs.Append(errutils.WithReason(fmt.Errorf("error patching finalizer '%s' on cluster '%s/%s': %w", fin, c.Namespace, c.Name, err), cconst.ReasonPlatformClusterInteractionProblem))
181-
}
182-
}
183-
if c.GetTenancyCount() == 0 && c.GetPreemptiveTenancyCount() == 0 && ctrlutils.HasLabelWithValue(c, clustersv1alpha1.DeleteWithoutRequestsLabel, "true") {
184-
log.Info("Deleting cluster without requests", "clusterName", c.Name, "clusterNamespace", c.Namespace)
185-
if err := r.PlatformCluster.Client().Delete(ctx, c); err != nil {
186-
if apierrors.IsNotFound(err) {
187-
log.Info("Cluster already deleted", "clusterName", c.Name, "clusterNamespace", c.Namespace)
188-
} else {
189-
errs.Append(errutils.WithReason(fmt.Errorf("error deleting cluster '%s/%s': %w", c.Namespace, c.Name, err), cconst.ReasonPlatformClusterInteractionProblem))
190-
}
191-
}
192-
}
166+
167+
// run the scheduling algorithm to remove the finalizers from this request
168+
sr, err := Schedule(ctx, clusters, cDef, r.Config.Strategy, NewSchedulingRequest(string(pcr.UID), pcr.Spec.Workload, true, pcr.Namespace, pcr.Spec.Purpose))
169+
if err != nil {
170+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error unscheduling request '%s': %w", req.String(), err), cconst.ReasonSchedulingFailed)
171+
return rr
193172
}
194-
rr.ReconcileError = errs.Aggregate()
195-
if rr.ReconcileError != nil {
173+
174+
// apply required changes to the cluster
175+
_, err = sr.Apply(ctx, r.PlatformCluster.Client())
176+
if err != nil {
177+
rr.ReconcileError = errutils.WithReason(fmt.Errorf("error applying scheduling result for request '%s': %w", req.String(), err), cconst.ReasonPlatformClusterInteractionProblem)
196178
return rr
197179
}
198180

0 commit comments

Comments
 (0)