Skip to content

Commit c627b7d

Browse files
Merge pull request #11 from smartnews/v1.0.4-patch
feat: upgrade karpenter to 1.0.4
2 parents 7b24b44 + b909646 commit c627b7d

13 files changed

+147
-10
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ coverage.html
44
*.test
55
*.cpuprofile
66
*.heapprofile
7+
*.swp
78

89
# Common in OSs and IDEs
910
.idea

kwok/apis/crds/karpenter.kwok.sh_kwoknodeclasses.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.16.3
6+
controller-gen.kubebuilder.io/version: v0.16.5
77
name: kwoknodeclasses.karpenter.kwok.sh
88
spec:
99
group: karpenter.kwok.sh

kwok/charts/crds/karpenter.sh_nodeclaims.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.16.3
6+
controller-gen.kubebuilder.io/version: v0.16.5
77
name: nodeclaims.karpenter.sh
88
spec:
99
group: karpenter.sh

kwok/charts/crds/karpenter.sh_nodepools.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.16.3
6+
controller-gen.kubebuilder.io/version: v0.16.5
77
name: nodepools.karpenter.sh
88
spec:
99
group: karpenter.sh
@@ -155,6 +155,13 @@ spec:
155155
- WhenEmpty
156156
- WhenEmptyOrUnderutilized
157157
type: string
158+
utilizationThreshold:
159+
description: |-
160+
UtilizationThreshold is defined as sum of requested resources divided by capacity
161+
below which a node can be considered for disruption.
162+
maximum: 100
163+
minimum: 1
164+
type: integer
158165
required:
159166
- consolidateAfter
160167
type: object

pkg/apis/crds/karpenter.sh_nodeclaims.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.16.3
6+
controller-gen.kubebuilder.io/version: v0.16.5
77
name: nodeclaims.karpenter.sh
88
spec:
99
group: karpenter.sh

pkg/apis/crds/karpenter.sh_nodepools.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.16.3
6+
controller-gen.kubebuilder.io/version: v0.16.5
77
name: nodepools.karpenter.sh
88
spec:
99
group: karpenter.sh
@@ -155,6 +155,13 @@ spec:
155155
- WhenEmpty
156156
- WhenEmptyOrUnderutilized
157157
type: string
158+
utilizationThreshold:
159+
description: |-
160+
UtilizationThreshold is defined as sum of requested resources divided by capacity
161+
below which a node can be considered for disruption.
162+
maximum: 100
163+
minimum: 1
164+
type: integer
158165
required:
159166
- consolidateAfter
160167
type: object

pkg/apis/v1/nodepool.go

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ type Disruption struct {
7474
// +kubebuilder:validation:Enum:={WhenEmpty,WhenEmptyOrUnderutilized}
7575
// +optional
7676
ConsolidationPolicy ConsolidationPolicy `json:"consolidationPolicy,omitempty"`
77+
// UtilizationThreshold is defined as sum of requested resources divided by capacity
78+
// below which a node can be considered for disruption.
79+
// +kubebuilder:validation:Minimum:=1
80+
// +kubebuilder:validation:Maximum:=100
81+
// +optional
82+
UtilizationThreshold *int `json:"utilizationThreshold,omitempty"`
7783
// Budgets is a list of Budgets.
7884
// If there are multiple active budgets, Karpenter uses
7985
// the most restrictive value. If left undefined,

pkg/apis/v1/zz_generated.deepcopy.go

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

pkg/controllers/nodeclaim/disruption/consolidation.go

+58
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,21 @@ package disruption
1818

1919
import (
2020
"context"
21+
"fmt"
2122

2223
"github.com/samber/lo"
24+
"k8s.io/apimachinery/pkg/api/resource"
2325
"k8s.io/utils/clock"
2426
"sigs.k8s.io/controller-runtime/pkg/client"
2527
"sigs.k8s.io/controller-runtime/pkg/log"
2628
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2729

30+
"sigs.k8s.io/karpenter/pkg/utils/node"
31+
32+
corev1 "k8s.io/api/core/v1"
33+
2834
v1 "sigs.k8s.io/karpenter/pkg/apis/v1"
35+
nodeclaimutil "sigs.k8s.io/karpenter/pkg/utils/nodeclaim"
2936
)
3037

3138
// Consolidation is a nodeclaim sub-controller that adds or removes status conditions on empty nodeclaims based on consolidateAfter
@@ -69,10 +76,61 @@ func (c *Consolidation) Reconcile(ctx context.Context, nodePool *v1.NodePool, no
6976
return reconcile.Result{RequeueAfter: consolidatableTime.Sub(c.clock.Now())}, nil
7077
}
7178

79+
// Get the node to check utilization
80+
n, err := nodeclaimutil.NodeForNodeClaim(ctx, c.kubeClient, nodeClaim)
81+
if err != nil {
82+
if nodeclaimutil.IsDuplicateNodeError(err) || nodeclaimutil.IsNodeNotFoundError(err) {
83+
return reconcile.Result{}, nil
84+
}
85+
return reconcile.Result{}, err
86+
}
87+
// Check the node utilization if the utilizationThreshold is specified, the node can be disruptted only if the utilization is below the threshold.
88+
threshold := nodePool.Spec.Disruption.UtilizationThreshold
89+
if threshold != nil {
90+
pods, err := node.GetPods(ctx, c.kubeClient, n)
91+
if err != nil {
92+
return reconcile.Result{}, fmt.Errorf("retrieving node pods, %w", err)
93+
}
94+
cpu, err := calculateUtilizationOfResource(n, corev1.ResourceCPU, pods)
95+
if err != nil {
96+
return reconcile.Result{}, fmt.Errorf("failed to calculate CPU, %w", err)
97+
}
98+
memory, err := calculateUtilizationOfResource(n, corev1.ResourceMemory, pods)
99+
if err != nil {
100+
return reconcile.Result{}, fmt.Errorf("failed to calculate memory, %w", err)
101+
}
102+
if cpu > float64(*threshold)/100 || memory > float64(*threshold)/100 {
103+
if hasConsolidatableCondition {
104+
_ = nodeClaim.StatusConditions().Clear(v1.ConditionTypeConsolidatable)
105+
log.FromContext(ctx).V(1).Info("removing consolidatable status condition due to high utilization")
106+
}
107+
}
108+
}
109+
72110
// 6. Otherwise, add the consolidatable status condition
73111
nodeClaim.StatusConditions().SetTrue(v1.ConditionTypeConsolidatable)
74112
if !hasConsolidatableCondition {
75113
log.FromContext(ctx).V(1).Info("marking consolidatable")
76114
}
77115
return reconcile.Result{}, nil
78116
}
117+
118+
// CalculateUtilizationOfResource calculates utilization of a given resource for a node.
119+
func calculateUtilizationOfResource(node *corev1.Node, resourceName corev1.ResourceName, pods []*corev1.Pod) (float64, error) {
120+
allocatable, found := node.Status.Allocatable[resourceName]
121+
if !found {
122+
return 0, fmt.Errorf("failed to get %v from %s", resourceName, node.Name)
123+
}
124+
if allocatable.MilliValue() == 0 {
125+
return 0, fmt.Errorf("%v is 0 at %s", resourceName, node.Name)
126+
}
127+
podsRequest := resource.MustParse("0")
128+
for _, pod := range pods {
129+
for _, container := range pod.Spec.Containers {
130+
if resourceValue, found := container.Resources.Requests[resourceName]; found {
131+
podsRequest.Add(resourceValue)
132+
}
133+
}
134+
}
135+
return float64(podsRequest.MilliValue()) / float64(allocatable.MilliValue()), nil
136+
}

pkg/controllers/provisioning/provisioner.go

+34-4
Original file line numberDiff line numberDiff line change
@@ -336,26 +336,56 @@ func (p *Provisioner) Schedule(ctx context.Context) (scheduler.Results, error) {
336336
return scheduler.Results{}, err
337337
}
338338
pods := append(pendingPods, deletingNodePods...)
339+
// filter pods which are alredy handled in last 3 minute
340+
targetPods := lo.FilterMap(pods, func(pod *corev1.Pod, _ int) (*corev1.Pod, bool) {
341+
if p.isPodHandled(ctx, pod) {
342+
return nil, false
343+
}
344+
return pod, true
345+
})
339346
// nothing to schedule, so just return success
340-
if len(pods) == 0 {
347+
if len(targetPods) == 0 {
341348
return scheduler.Results{}, nil
342349
}
343-
s, err := p.NewScheduler(ctx, pods, nodes.Active())
350+
s, err := p.NewScheduler(ctx, targetPods, nodes.Active())
344351
if err != nil {
345352
if errors.Is(err, ErrNodePoolsNotFound) {
346353
log.FromContext(ctx).Info("no nodepools found")
347354
return scheduler.Results{}, nil
348355
}
349356
return scheduler.Results{}, fmt.Errorf("creating scheduler, %w", err)
350357
}
351-
results := s.Solve(ctx, pods).TruncateInstanceTypes(scheduler.MaxInstanceTypes)
358+
results := s.Solve(ctx, targetPods).TruncateInstanceTypes(scheduler.MaxInstanceTypes)
352359
if len(results.NewNodeClaims) > 0 {
353-
log.FromContext(ctx).WithValues("Pods", pretty.Slice(lo.Map(pods, func(p *corev1.Pod, _ int) string { return klog.KRef(p.Namespace, p.Name).String() }), 5), "duration", time.Since(start)).Info("found provisionable pod(s)")
360+
log.FromContext(ctx).WithValues("Pods", pretty.Slice(lo.Map(targetPods, func(p *corev1.Pod, _ int) string { return klog.KRef(p.Namespace, p.Name).String() }), 5), "duration", time.Since(start)).Info("found provisionable pod(s)")
354361
}
355362
results.Record(ctx, p.recorder, p.cluster)
356363
return results, nil
357364
}
358365

366+
func (p *Provisioner) isPodHandled(ctx context.Context, pod *corev1.Pod) bool {
367+
var events corev1.EventList
368+
filter := client.MatchingFields{
369+
"namespace": pod.Namespace,
370+
"involvedObject.kind": "Pod",
371+
"involvedObject.name": pod.Name,
372+
"reason": "HandledByKarpenter",
373+
}
374+
if err := p.kubeClient.List(ctx, &events, filter); err == nil {
375+
for _, event := range events.Items {
376+
// ignore the pod if it's already handled in 3 minute
377+
if time.Now().Before(event.LastTimestamp.Time.Add(3 * time.Minute)) {
378+
log.FromContext(ctx).Info(fmt.Sprintf("pod %s/%s is handled", pod.Namespace, pod.Name))
379+
return true
380+
}
381+
}
382+
} else {
383+
log.FromContext(ctx).Error(err, fmt.Sprintf("failed to get event for %s/%s", pod.Namespace, pod.Name))
384+
}
385+
p.recorder.Publish(scheduler.PodHandledEvent(pod))
386+
return false
387+
}
388+
359389
func (p *Provisioner) Create(ctx context.Context, n *scheduler.NodeClaim, opts ...option.Function[LaunchOptions]) (string, error) {
360390
ctx = log.IntoContext(ctx, log.FromContext(ctx).WithValues("NodePool", klog.KRef("", n.NodePoolName)))
361391
options := option.Resolve(opts...)

pkg/controllers/provisioning/scheduling/events.go

+11
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,14 @@ func PodFailedToScheduleEvent(pod *corev1.Pod, err error) events.Event {
5959
DedupeTimeout: 5 * time.Minute,
6060
}
6161
}
62+
63+
func PodHandledEvent(pod *corev1.Pod) events.Event {
64+
return events.Event{
65+
InvolvedObject: pod,
66+
Type: corev1.EventTypeNormal,
67+
Reason: "HandledByKarpenter",
68+
Message: "Pod is handled by karpenter",
69+
DedupeValues: []string{string(pod.UID)},
70+
DedupeTimeout: 5 * time.Minute,
71+
}
72+
}

pkg/operator/operator.go

+12
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,18 @@ func NewOperator() (context.Context, *Operator) {
198198
lo.Must0(mgr.GetFieldIndexer().IndexField(ctx, &corev1.Node{}, "spec.providerID", func(o client.Object) []string {
199199
return []string{o.(*corev1.Node).Spec.ProviderID}
200200
}), "failed to setup node provider id indexer")
201+
lo.Must0(mgr.GetFieldIndexer().IndexField(ctx, &corev1.Event{}, "involvedObject.kind", func(o client.Object) []string {
202+
return []string{o.(*corev1.Event).InvolvedObject.Kind}
203+
}), "failed to setup event kind indexer")
204+
lo.Must0(mgr.GetFieldIndexer().IndexField(ctx, &corev1.Event{}, "involvedObject.name", func(o client.Object) []string {
205+
return []string{o.(*corev1.Event).InvolvedObject.Name}
206+
}), "failed to setup event name indexer")
207+
lo.Must0(mgr.GetFieldIndexer().IndexField(ctx, &corev1.Event{}, "namespace", func(o client.Object) []string {
208+
return []string{o.(*corev1.Event).Namespace}
209+
}), "failed to setup event namespace indexer")
210+
lo.Must0(mgr.GetFieldIndexer().IndexField(ctx, &corev1.Event{}, "reason", func(o client.Object) []string {
211+
return []string{o.(*corev1.Event).Reason}
212+
}), "failed to setup event reason indexer")
201213
lo.Must0(mgr.GetFieldIndexer().IndexField(ctx, &v1.NodeClaim{}, "status.providerID", func(o client.Object) []string {
202214
return []string{o.(*v1.NodeClaim).Status.ProviderID}
203215
}), "failed to setup nodeclaim provider id indexer")

pkg/test/v1alpha1/crds/karpenter.test.sh_testnodeclasses.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
33
kind: CustomResourceDefinition
44
metadata:
55
annotations:
6-
controller-gen.kubebuilder.io/version: v0.16.3
6+
controller-gen.kubebuilder.io/version: v0.16.5
77
name: testnodeclasses.karpenter.test.sh
88
spec:
99
group: karpenter.test.sh

0 commit comments

Comments
 (0)