Skip to content

Commit 2b16a80

Browse files
feat(vmclass): add max allocatable resources (#480)
add max allocatable resources --------- Signed-off-by: yaroslavborbat <[email protected]>
1 parent 3b7eb01 commit 2b16a80

File tree

9 files changed

+158
-42
lines changed

9 files changed

+158
-42
lines changed

api/core/v1alpha2/virtual_machine_class.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,12 @@ type VirtualMachineClassStatus struct {
202202
// It is not displayed for the types: `Host`, `HostPassthrough`
203203
//
204204
// +kubebuilder:example={node-1, node-2}
205-
AvailableNodes []string `json:"availableNodes,omitempty"`
206-
Conditions []metav1.Condition `json:"conditions,omitempty"`
207-
// The generation last processed by the controller
205+
AvailableNodes []string `json:"availableNodes,omitempty"`
206+
// The maximum amount of free CPU and Memory resources observed among all available nodes.
207+
// +kubebuilder:example={"maxAllocatableResources: {\"cpu\": 1, \"memory\": \"10Gi\"}"}
208+
MaxAllocatableResources corev1.ResourceList `json:"maxAllocatableResources,omitempty"`
209+
Conditions []metav1.Condition `json:"conditions,omitempty"`
210+
// The generation last processed by the controller.
208211
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
209212
}
210213

api/core/v1alpha2/zz_generated.deepcopy.go

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/pkg/apiserver/api/generated/openapi/zz_generated.openapi.go

+16-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crds/doc-ru-virtualmachineclasses.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ spec:
176176
description: |
177177
Список узлов, поддерживающих эту модель процессора.
178178
Не отображается для типов: `Host`, `HostPassthrough`.
179+
maxAllocatableResources:
180+
description: |
181+
Максимальные размеры свободных ресурсов процессора и памяти, найденные среди всех доступных узлов.
179182
conditions:
180183
description: |
181184
Последнее подтвержденное состояние данного ресурса.

crds/virtualmachineclasses.yaml

+14-1
Original file line numberDiff line numberDiff line change
@@ -479,8 +479,21 @@ spec:
479479
type: string
480480
type: array
481481
type: object
482+
maxAllocatableResources:
483+
additionalProperties:
484+
anyOf:
485+
- type: integer
486+
- type: string
487+
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
488+
x-kubernetes-int-or-string: true
489+
description:
490+
The maximum amount of free CPU and Memory resources observed
491+
among all available nodes.
492+
example:
493+
- 'maxAllocatableResources: {"cpu": 1, "memory": "10Gi"}'
494+
type: object
482495
observedGeneration:
483-
description: The generation last processed by the controller
496+
description: The generation last processed by the controller.
484497
format: int64
485498
type: integer
486499
phase:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
Copyright 2024 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package common
18+
19+
import (
20+
"slices"
21+
)
22+
23+
type FilterFunc[T any] func(obj *T) (skip bool)
24+
25+
func Filter[T any](objs []T, skips ...FilterFunc[T]) []T {
26+
if len(skips) == 0 {
27+
return slices.Clone(objs)
28+
}
29+
var filtered []T
30+
loop:
31+
for _, o := range objs {
32+
for _, skip := range skips {
33+
if skip(&o) {
34+
continue loop
35+
}
36+
}
37+
filtered = append(filtered, o)
38+
}
39+
return filtered
40+
}

images/virtualization-artifact/pkg/controller/vmclass/internal/discovery.go

+25-13
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"strings"
2525

2626
corev1 "k8s.io/api/core/v1"
27+
"k8s.io/apimachinery/pkg/api/resource"
2728
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829
virtv1 "kubevirt.io/api/core/v1"
2930
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -59,27 +60,17 @@ func (h *DiscoveryHandler) Handle(ctx context.Context, s state.VirtualMachineCla
5960

6061
cpuType := current.Spec.CPU.Type
6162

62-
if cpuType == virtv2.CPUTypeHostPassthrough || cpuType == virtv2.CPUTypeHost {
63-
cb := conditions.NewConditionBuilder(vmclasscondition.TypeDiscovered).
64-
Generation(current.GetGeneration()).
65-
Message(fmt.Sprintf("Discovery not needed for cpu.type %q", cpuType)).
66-
Reason(vmclasscondition.ReasonDiscoverySkip).
67-
Status(metav1.ConditionFalse)
68-
69-
conditions.SetCondition(cb, &changed.Status.Conditions)
70-
return reconcile.Result{}, nil
71-
}
72-
7363
nodes, err := s.Nodes(ctx)
7464
if err != nil {
7565
return reconcile.Result{}, err
7666
}
67+
7768
availableNodes, err := s.AvailableNodes(nodes)
7869
if err != nil {
7970
return reconcile.Result{}, err
8071
}
81-
availableNodeNames := make([]string, len(availableNodes))
8272

73+
availableNodeNames := make([]string, len(availableNodes))
8374
for i, n := range availableNodes {
8475
availableNodeNames[i] = n.GetName()
8576
}
@@ -123,14 +114,14 @@ func (h *DiscoveryHandler) Handle(ctx context.Context, s state.VirtualMachineCla
123114
Reason(vmclasscondition.ReasonDiscoverySkip).
124115
Status(metav1.ConditionFalse)
125116
}
126-
127117
conditions.SetCondition(cb, &changed.Status.Conditions)
128118

129119
sort.Strings(availableNodeNames)
130120
sort.Strings(featuresEnabled)
131121
sort.Strings(featuresNotEnabled)
132122

133123
changed.Status.AvailableNodes = availableNodeNames
124+
changed.Status.MaxAllocatableResources = h.maxAllocatableResources(availableNodes)
134125
changed.Status.CpuFeatures = virtv2.CpuFeatures{
135126
Enabled: featuresEnabled,
136127
NotEnabledCommon: featuresNotEnabled,
@@ -163,3 +154,24 @@ func (h *DiscoveryHandler) discoveryCommonFeatures(nodes []corev1.Node) []string
163154
}
164155
return features
165156
}
157+
158+
func (h *DiscoveryHandler) maxAllocatableResources(nodes []corev1.Node) corev1.ResourceList {
159+
var (
160+
resourceList corev1.ResourceList = make(map[corev1.ResourceName]resource.Quantity)
161+
resourceNames = []corev1.ResourceName{corev1.ResourceCPU, corev1.ResourceMemory}
162+
)
163+
164+
for _, node := range nodes {
165+
for _, resourceName := range resourceNames {
166+
newQ := node.Status.Allocatable[resourceName]
167+
if newQ.IsZero() {
168+
continue
169+
}
170+
oldQ := resourceList[resourceName]
171+
if newQ.Cmp(oldQ) == 1 {
172+
resourceList[resourceName] = newQ
173+
}
174+
}
175+
}
176+
return resourceList
177+
}

images/virtualization-artifact/pkg/controller/vmclass/internal/state/state.go

+8-20
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,8 @@ func (s *state) VirtualMachines(ctx context.Context) ([]virtv2.VirtualMachine, e
6767
return vms.Items, nil
6868
}
6969

70-
type filterFunc func(node *corev1.Node) (skip bool)
71-
72-
func nodeFilter(nodes []corev1.Node, filters ...filterFunc) []corev1.Node {
73-
if len(filters) == 0 {
74-
return nodes
75-
}
76-
var filtered []corev1.Node
77-
loop:
78-
for _, node := range nodes {
79-
for _, f := range filters {
80-
if f(&node) {
81-
continue loop
82-
}
83-
}
84-
filtered = append(filtered, node)
85-
}
86-
return filtered
70+
func nodeFilter(nodes []corev1.Node, filters ...common.FilterFunc[corev1.Node]) []corev1.Node {
71+
return common.Filter[corev1.Node](nodes, filters...)
8772
}
8873

8974
func (s *state) Nodes(ctx context.Context) ([]corev1.Node, error) {
@@ -94,12 +79,12 @@ func (s *state) Nodes(ctx context.Context) ([]corev1.Node, error) {
9479
var (
9580
curr = s.vmClass.Current()
9681
matchLabels map[string]string
97-
filters []filterFunc
82+
filters []common.FilterFunc[corev1.Node]
9883
)
9984

10085
switch curr.Spec.CPU.Type {
10186
case virtv2.CPUTypeHost, virtv2.CPUTypeHostPassthrough:
102-
return nil, nil
87+
// each node
10388
case virtv2.CPUTypeDiscovery:
10489
matchLabels = curr.Spec.CPU.Discovery.NodeSelector.MatchLabels
10590
filters = append(filters, func(node *corev1.Node) bool {
@@ -132,10 +117,13 @@ func (s *state) AvailableNodes(nodes []corev1.Node) ([]corev1.Node, error) {
132117
if s.vmClass == nil || s.vmClass.IsEmpty() {
133118
return nil, nil
134119
}
120+
if len(nodes) == 0 {
121+
return nodes, nil
122+
}
135123

136124
nodeSelector := s.vmClass.Current().Spec.NodeSelector
137125

138-
filters := []filterFunc{
126+
filters := []common.FilterFunc[corev1.Node]{
139127
func(node *corev1.Node) bool {
140128
return !common.MatchLabels(node.GetLabels(), nodeSelector.MatchLabels)
141129
},

images/virtualization-artifact/pkg/controller/vmclass/vmclass_reconciler.go

+39-3
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"slices"
2324

2425
corev1 "k8s.io/api/core/v1"
26+
"k8s.io/component-helpers/scheduling/corev1/nodeaffinity"
2527
"sigs.k8s.io/controller-runtime/pkg/client"
2628
"sigs.k8s.io/controller-runtime/pkg/controller"
29+
"sigs.k8s.io/controller-runtime/pkg/event"
2730
"sigs.k8s.io/controller-runtime/pkg/handler"
2831
"sigs.k8s.io/controller-runtime/pkg/manager"
2932
"sigs.k8s.io/controller-runtime/pkg/predicate"
@@ -74,14 +77,47 @@ func (r *Reconciler) SetupController(_ context.Context, mgr manager.Manager, ctr
7477
if err != nil {
7578
return nil
7679
}
80+
7781
for _, class := range classList.Items {
78-
if common.MatchLabelSelector(node.GetLabels(), class.Spec.CPU.Discovery.NodeSelector) {
79-
result = append(result, reconcile.Request{NamespacedName: common.NamespacedName(&class)})
82+
if slices.Contains(class.Status.AvailableNodes, node.GetName()) {
83+
result = append(result, reconcile.Request{
84+
NamespacedName: common.NamespacedName(&class),
85+
})
86+
continue
87+
}
88+
if !common.MatchLabels(node.GetLabels(), class.Spec.NodeSelector.MatchLabels) {
89+
continue
90+
}
91+
ns, err := nodeaffinity.NewNodeSelector(&corev1.NodeSelector{
92+
NodeSelectorTerms: []corev1.NodeSelectorTerm{{MatchExpressions: class.Spec.NodeSelector.MatchExpressions}},
93+
})
94+
if err != nil || !ns.Match(node) {
95+
continue
8096
}
97+
result = append(result, reconcile.Request{
98+
NamespacedName: common.NamespacedName(&class),
99+
})
81100
}
82101
return result
83102
}),
84-
predicate.LabelChangedPredicate{},
103+
predicate.Or(
104+
predicate.LabelChangedPredicate{},
105+
predicate.Funcs{
106+
CreateFunc: func(e event.CreateEvent) bool { return true },
107+
DeleteFunc: func(e event.DeleteEvent) bool { return true },
108+
UpdateFunc: func(e event.UpdateEvent) bool {
109+
oldNode := e.ObjectOld.(*corev1.Node)
110+
newNode := e.ObjectNew.(*corev1.Node)
111+
if !oldNode.Status.Allocatable[corev1.ResourceCPU].Equal(newNode.Status.Allocatable[corev1.ResourceCPU]) {
112+
return true
113+
}
114+
if !oldNode.Status.Allocatable[corev1.ResourceMemory].Equal(newNode.Status.Allocatable[corev1.ResourceMemory]) {
115+
return true
116+
}
117+
return false
118+
},
119+
},
120+
),
85121
); err != nil {
86122
return fmt.Errorf("error setting watch on Node: %w", err)
87123
}

0 commit comments

Comments
 (0)