Skip to content

Commit 9440674

Browse files
authored
Merge pull request #7962 from maxcao13/in-place-updates-updater
VPA: (InPlaceOrRecreate) Allow updater to actuate InPlaceOrRecreate updates
2 parents 656a69f + 7819a2b commit 9440674

24 files changed

+2045
-543
lines changed

vertical-pod-autoscaler/deploy/vpa-rbac.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,32 @@ rules:
121121
- create
122122
---
123123
apiVersion: rbac.authorization.k8s.io/v1
124+
kind: ClusterRole
125+
metadata:
126+
name: system:vpa-updater-in-place
127+
rules:
128+
- apiGroups:
129+
- ""
130+
resources:
131+
- pods/resize
132+
- pods # required for patching vpaInPlaceUpdated annotations onto the pod
133+
verbs:
134+
- patch
135+
---
136+
apiVersion: rbac.authorization.k8s.io/v1
137+
kind: ClusterRoleBinding
138+
metadata:
139+
name: system:vpa-updater-in-place-binding
140+
roleRef:
141+
apiGroup: rbac.authorization.k8s.io
142+
kind: ClusterRole
143+
name: system:vpa-updater-in-place
144+
subjects:
145+
- kind: ServiceAccount
146+
name: vpa-updater
147+
namespace: kube-system
148+
---
149+
apiVersion: rbac.authorization.k8s.io/v1
124150
kind: ClusterRoleBinding
125151
metadata:
126152
name: system:metrics-reader

vertical-pod-autoscaler/hack/vpa-process-yaml.sh

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,22 @@ function print_help {
2424
echo "separator and substituting REGISTRY and TAG for pod images"
2525
}
2626

27-
# Requires input from stdin, otherwise hangs. Checks for "admission-controller", "updater", or "recommender", and
28-
# applies the respective kubectl patch command to add the feature gates specified in the FEATURE_GATES environment variable.
27+
# Requires input from stdin, otherwise hangs. If the input is a Deployment manifest,
28+
# apply kubectl patch to add feature gates specified in the FEATURE_GATES environment variable.
2929
# e.g. cat file.yaml | apply_feature_gate
3030
function apply_feature_gate() {
3131
local input=""
3232
while IFS= read -r line; do
3333
input+="$line"$'\n'
3434
done
3535

36-
if [ -n "${FEATURE_GATES}" ]; then
37-
if echo "$input" | grep -q "admission-controller"; then
38-
echo "$input" | kubectl patch --type=json --local -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates='"${FEATURE_GATES}"'"}]' -o yaml -f -
39-
elif echo "$input" | grep -q "updater" || echo "$input" | grep -q "recommender"; then
40-
echo "$input" | kubectl patch --type=json --local -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args", "value": ["--feature-gates='"${FEATURE_GATES}"'"]}]' -o yaml -f -
36+
# matching precisely "kind: Deployment" to avoid matching "kind: DeploymentConfig" or a line with extra whitespace
37+
if echo "$input" | grep -qE '^kind: Deployment$'; then
38+
if [ -n "${FEATURE_GATES}" ]; then
39+
if ! echo "$input" | kubectl patch --type=json --local -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value": "--feature-gates='"${FEATURE_GATES}"'"}]' -o yaml -f - 2>/dev/null; then
40+
# If it fails, there was no args field, so we need to add it
41+
echo "$input" | kubectl patch --type=json --local -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args", "value": ["--feature-gates='"${FEATURE_GATES}"'"]}]' -o yaml -f -
42+
fi
4143
else
4244
echo "$input"
4345
fi

vertical-pod-autoscaler/pkg/admission-controller/resource/pod/handler_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ type fakePatchCalculator struct {
5454
err error
5555
}
5656

57+
func (*fakePatchCalculator) PatchResourceTarget() patch.PatchResourceTarget {
58+
return patch.Pod
59+
}
60+
5761
func (c *fakePatchCalculator) CalculatePatches(_ *apiv1.Pod, _ *vpa_types.VerticalPodAutoscaler) (
5862
[]resource_admission.PatchRecord, error) {
5963
return c.patches, c.err

vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch/calculator.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,22 @@ import (
2323
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
2424
)
2525

26+
// PatchResourceTarget is the type of resource that can be patched.
27+
type PatchResourceTarget string
28+
29+
const (
30+
// Pod refers to the pod resource itself.
31+
Pod PatchResourceTarget = "Pod"
32+
// Resize refers to the resize subresource of the pod.
33+
Resize PatchResourceTarget = "Resize"
34+
35+
// Future subresources can be added here.
36+
// e.g. Status PatchResourceTarget = "Status"
37+
)
38+
2639
// Calculator is capable of calculating required patches for pod.
2740
type Calculator interface {
2841
CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource.PatchRecord, error)
42+
// PatchResourceTarget returns the resource this calculator should calculate patches for.
43+
PatchResourceTarget() PatchResourceTarget
2944
}

vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch/observed_containers.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ func (*observedContainers) CalculatePatches(pod *core.Pod, _ *vpa_types.Vertical
3131
return []resource_admission.PatchRecord{GetAddAnnotationPatch(annotations.VpaObservedContainersLabel, vpaObservedContainersValue)}, nil
3232
}
3333

34+
func (*observedContainers) PatchResourceTarget() PatchResourceTarget {
35+
return Pod
36+
}
37+
3438
// NewObservedContainersCalculator returns calculator for
3539
// observed containers patches.
3640
func NewObservedContainersCalculator() Calculator {

vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch/resource_updates.go

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"strings"
2222

2323
core "k8s.io/api/core/v1"
24-
"k8s.io/apimachinery/pkg/api/resource"
2524

2625
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
2726
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
@@ -47,6 +46,10 @@ func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider
4746
}
4847
}
4948

49+
func (*resourcesUpdatesPatchCalculator) PatchResourceTarget() PatchResourceTarget {
50+
return Pod
51+
}
52+
5053
func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
5154
result := []resource_admission.PatchRecord{}
5255

@@ -78,7 +81,7 @@ func getContainerPatch(pod *core.Pod, i int, annotationsPerContainer vpa_api_uti
7881
// Add empty resources object if missing.
7982
if pod.Spec.Containers[i].Resources.Limits == nil &&
8083
pod.Spec.Containers[i].Resources.Requests == nil {
81-
patches = append(patches, getPatchInitializingEmptyResources(i))
84+
patches = append(patches, GetPatchInitializingEmptyResources(i))
8285
}
8386

8487
annotations, found := annotationsPerContainer[pod.Spec.Containers[i].Name]
@@ -96,34 +99,11 @@ func getContainerPatch(pod *core.Pod, i int, annotationsPerContainer vpa_api_uti
9699
func appendPatchesAndAnnotations(patches []resource_admission.PatchRecord, annotations []string, current core.ResourceList, containerIndex int, resources core.ResourceList, fieldName, resourceName string) ([]resource_admission.PatchRecord, []string) {
97100
// Add empty object if it's missing and we're about to fill it.
98101
if current == nil && len(resources) > 0 {
99-
patches = append(patches, getPatchInitializingEmptyResourcesSubfield(containerIndex, fieldName))
102+
patches = append(patches, GetPatchInitializingEmptyResourcesSubfield(containerIndex, fieldName))
100103
}
101104
for resource, request := range resources {
102-
patches = append(patches, getAddResourceRequirementValuePatch(containerIndex, fieldName, resource, request))
105+
patches = append(patches, GetAddResourceRequirementValuePatch(containerIndex, fieldName, resource, request))
103106
annotations = append(annotations, fmt.Sprintf("%s %s", resource, resourceName))
104107
}
105108
return patches, annotations
106109
}
107-
108-
func getAddResourceRequirementValuePatch(i int, kind string, resource core.ResourceName, quantity resource.Quantity) resource_admission.PatchRecord {
109-
return resource_admission.PatchRecord{
110-
Op: "add",
111-
Path: fmt.Sprintf("/spec/containers/%d/resources/%s/%s", i, kind, resource),
112-
Value: quantity.String()}
113-
}
114-
115-
func getPatchInitializingEmptyResources(i int) resource_admission.PatchRecord {
116-
return resource_admission.PatchRecord{
117-
Op: "add",
118-
Path: fmt.Sprintf("/spec/containers/%d/resources", i),
119-
Value: core.ResourceRequirements{},
120-
}
121-
}
122-
123-
func getPatchInitializingEmptyResourcesSubfield(i int, kind string) resource_admission.PatchRecord {
124-
return resource_admission.PatchRecord{
125-
Op: "add",
126-
Path: fmt.Sprintf("/spec/containers/%d/resources/%s", i, kind),
127-
Value: core.ResourceList{},
128-
}
129-
}

vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch/util.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ package patch
1919
import (
2020
"fmt"
2121

22+
core "k8s.io/api/core/v1"
23+
"k8s.io/apimachinery/pkg/api/resource"
24+
2225
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
2326
)
2427

@@ -39,3 +42,30 @@ func GetAddAnnotationPatch(annotationName, annotationValue string) resource_admi
3942
Value: annotationValue,
4043
}
4144
}
45+
46+
// GetAddResourceRequirementValuePatch returns a patch record to add resource requirements to a container.
47+
func GetAddResourceRequirementValuePatch(i int, kind string, resource core.ResourceName, quantity resource.Quantity) resource_admission.PatchRecord {
48+
return resource_admission.PatchRecord{
49+
Op: "add",
50+
Path: fmt.Sprintf("/spec/containers/%d/resources/%s/%s", i, kind, resource),
51+
Value: quantity.String()}
52+
}
53+
54+
// GetPatchInitializingEmptyResources returns a patch record to initialize an empty resources object for a container.
55+
func GetPatchInitializingEmptyResources(i int) resource_admission.PatchRecord {
56+
return resource_admission.PatchRecord{
57+
Op: "add",
58+
Path: fmt.Sprintf("/spec/containers/%d/resources", i),
59+
Value: core.ResourceRequirements{},
60+
}
61+
}
62+
63+
// GetPatchInitializingEmptyResourcesSubfield returns a patch record to initialize an empty subfield
64+
// (e.g., "requests" or "limits") within a container's resources object.
65+
func GetPatchInitializingEmptyResourcesSubfield(i int, kind string) resource_admission.PatchRecord {
66+
return resource_admission.PatchRecord{
67+
Op: "add",
68+
Path: fmt.Sprintf("/spec/containers/%d/resources/%s", i, kind),
69+
Value: core.ResourceList{},
70+
}
71+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
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 inplace
18+
19+
import (
20+
core "k8s.io/api/core/v1"
21+
22+
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
23+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
24+
25+
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
26+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
27+
)
28+
29+
type inPlaceUpdate struct{}
30+
31+
// CalculatePatches returns a patch that adds a "vpaInPlaceUpdated" annotation
32+
// to the pod, marking it as having been requested to be updated in-place by VPA.
33+
func (*inPlaceUpdate) CalculatePatches(pod *core.Pod, _ *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
34+
vpaInPlaceUpdatedValue := annotations.GetVpaInPlaceUpdatedValue()
35+
return []resource_admission.PatchRecord{patch.GetAddAnnotationPatch(annotations.VpaInPlaceUpdatedLabel, vpaInPlaceUpdatedValue)}, nil
36+
}
37+
38+
func (*inPlaceUpdate) PatchResourceTarget() patch.PatchResourceTarget {
39+
return patch.Pod
40+
}
41+
42+
// NewInPlaceUpdatedCalculator returns calculator for
43+
// observed containers patches.
44+
func NewInPlaceUpdatedCalculator() patch.Calculator {
45+
return &inPlaceUpdate{}
46+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
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 inplace
18+
19+
import (
20+
"fmt"
21+
22+
core "k8s.io/api/core/v1"
23+
24+
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
25+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
26+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
27+
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
28+
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
29+
)
30+
31+
type resourcesInplaceUpdatesPatchCalculator struct {
32+
recommendationProvider recommendation.Provider
33+
}
34+
35+
// NewResourceInPlaceUpdatesCalculator returns a calculator for
36+
// in-place resource update patches.
37+
func NewResourceInPlaceUpdatesCalculator(recommendationProvider recommendation.Provider) patch.Calculator {
38+
return &resourcesInplaceUpdatesPatchCalculator{
39+
recommendationProvider: recommendationProvider,
40+
}
41+
}
42+
43+
// PatchResourceTarget returns the resize subresource to apply calculator patches.
44+
func (*resourcesInplaceUpdatesPatchCalculator) PatchResourceTarget() patch.PatchResourceTarget {
45+
return patch.Resize
46+
}
47+
48+
// CalculatePatches calculates a JSON patch from a VPA's recommendation to send to the pod "resize" subresource as an in-place resize.
49+
func (c *resourcesInplaceUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
50+
result := []resource_admission.PatchRecord{}
51+
52+
containersResources, _, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
53+
if err != nil {
54+
return []resource_admission.PatchRecord{}, fmt.Errorf("Failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
55+
}
56+
57+
for i, containerResources := range containersResources {
58+
newPatches := getContainerPatch(pod, i, containerResources)
59+
result = append(result, newPatches...)
60+
}
61+
62+
return result, nil
63+
}
64+
65+
func getContainerPatch(pod *core.Pod, i int, containerResources vpa_api_util.ContainerResources) []resource_admission.PatchRecord {
66+
var patches []resource_admission.PatchRecord
67+
// Add empty resources object if missing.
68+
if pod.Spec.Containers[i].Resources.Limits == nil &&
69+
pod.Spec.Containers[i].Resources.Requests == nil {
70+
patches = append(patches, patch.GetPatchInitializingEmptyResources(i))
71+
}
72+
73+
patches = appendPatches(patches, pod.Spec.Containers[i].Resources.Requests, i, containerResources.Requests, "requests")
74+
patches = appendPatches(patches, pod.Spec.Containers[i].Resources.Limits, i, containerResources.Limits, "limits")
75+
76+
return patches
77+
}
78+
79+
func appendPatches(patches []resource_admission.PatchRecord, current core.ResourceList, containerIndex int, resources core.ResourceList, fieldName string) []resource_admission.PatchRecord {
80+
// Add empty object if it's missing and we're about to fill it.
81+
if current == nil && len(resources) > 0 {
82+
patches = append(patches, patch.GetPatchInitializingEmptyResourcesSubfield(containerIndex, fieldName))
83+
}
84+
for resource, request := range resources {
85+
patches = append(patches, patch.GetAddResourceRequirementValuePatch(containerIndex, fieldName, resource, request))
86+
}
87+
return patches
88+
}

0 commit comments

Comments
 (0)