Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions vertical-pod-autoscaler/deploy/vpa-rbac.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,32 @@ rules:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: system:vpa-updater-in-place
rules:
- apiGroups:
- ""
resources:
- pods/resize
- pods # required for patching vpaInPlaceUpdated annotations onto the pod
verbs:
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:vpa-updater-in-place-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:vpa-updater-in-place
subjects:
- kind: ServiceAccount
name: vpa-updater
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:metrics-reader
Expand Down
16 changes: 9 additions & 7 deletions vertical-pod-autoscaler/hack/vpa-process-yaml.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,22 @@ function print_help {
echo "separator and substituting REGISTRY and TAG for pod images"
}

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

if [ -n "${FEATURE_GATES}" ]; then
if echo "$input" | grep -q "admission-controller"; then
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 -
elif echo "$input" | grep -q "updater" || echo "$input" | grep -q "recommender"; then
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 -
# matching precisely "kind: Deployment" to avoid matching "kind: DeploymentConfig" or a line with extra whitespace
if echo "$input" | grep -qE '^kind: Deployment$'; then
if [ -n "${FEATURE_GATES}" ]; then
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
# If it fails, there was no args field, so we need to add it
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 -
fi
else
echo "$input"
fi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ type fakePatchCalculator struct {
err error
}

func (*fakePatchCalculator) PatchResourceTarget() patch.PatchResourceTarget {
return patch.Pod
}

func (c *fakePatchCalculator) CalculatePatches(_ *apiv1.Pod, _ *vpa_types.VerticalPodAutoscaler) (
[]resource_admission.PatchRecord, error) {
return c.patches, c.err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,22 @@ import (
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
)

// PatchResourceTarget is the type of resource that can be patched.
type PatchResourceTarget string

const (
// Pod refers to the pod resource itself.
Pod PatchResourceTarget = "Pod"
// Resize refers to the resize subresource of the pod.
Resize PatchResourceTarget = "Resize"

// Future subresources can be added here.
// e.g. Status PatchResourceTarget = "Status"
)

// Calculator is capable of calculating required patches for pod.
type Calculator interface {
CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource.PatchRecord, error)
// PatchResourceTarget returns the resource this calculator should calculate patches for.
PatchResourceTarget() PatchResourceTarget
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func (*observedContainers) CalculatePatches(pod *core.Pod, _ *vpa_types.Vertical
return []resource_admission.PatchRecord{GetAddAnnotationPatch(annotations.VpaObservedContainersLabel, vpaObservedContainersValue)}, nil
}

func (*observedContainers) PatchResourceTarget() PatchResourceTarget {
return Pod
}

// NewObservedContainersCalculator returns calculator for
// observed containers patches.
func NewObservedContainersCalculator() Calculator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"strings"

core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"

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

func (*resourcesUpdatesPatchCalculator) PatchResourceTarget() PatchResourceTarget {
return Pod
}

func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
result := []resource_admission.PatchRecord{}

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

annotations, found := annotationsPerContainer[pod.Spec.Containers[i].Name]
Expand All @@ -96,34 +99,11 @@ func getContainerPatch(pod *core.Pod, i int, annotationsPerContainer vpa_api_uti
func appendPatchesAndAnnotations(patches []resource_admission.PatchRecord, annotations []string, current core.ResourceList, containerIndex int, resources core.ResourceList, fieldName, resourceName string) ([]resource_admission.PatchRecord, []string) {
// Add empty object if it's missing and we're about to fill it.
if current == nil && len(resources) > 0 {
patches = append(patches, getPatchInitializingEmptyResourcesSubfield(containerIndex, fieldName))
patches = append(patches, GetPatchInitializingEmptyResourcesSubfield(containerIndex, fieldName))
}
for resource, request := range resources {
patches = append(patches, getAddResourceRequirementValuePatch(containerIndex, fieldName, resource, request))
patches = append(patches, GetAddResourceRequirementValuePatch(containerIndex, fieldName, resource, request))
annotations = append(annotations, fmt.Sprintf("%s %s", resource, resourceName))
}
return patches, annotations
}

func getAddResourceRequirementValuePatch(i int, kind string, resource core.ResourceName, quantity resource.Quantity) resource_admission.PatchRecord {
return resource_admission.PatchRecord{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%d/resources/%s/%s", i, kind, resource),
Value: quantity.String()}
}

func getPatchInitializingEmptyResources(i int) resource_admission.PatchRecord {
return resource_admission.PatchRecord{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%d/resources", i),
Value: core.ResourceRequirements{},
}
}

func getPatchInitializingEmptyResourcesSubfield(i int, kind string) resource_admission.PatchRecord {
return resource_admission.PatchRecord{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%d/resources/%s", i, kind),
Value: core.ResourceList{},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package patch
import (
"fmt"

core "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"

resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
)

Expand All @@ -39,3 +42,30 @@ func GetAddAnnotationPatch(annotationName, annotationValue string) resource_admi
Value: annotationValue,
}
}

// GetAddResourceRequirementValuePatch returns a patch record to add resource requirements to a container.
func GetAddResourceRequirementValuePatch(i int, kind string, resource core.ResourceName, quantity resource.Quantity) resource_admission.PatchRecord {
return resource_admission.PatchRecord{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%d/resources/%s/%s", i, kind, resource),
Value: quantity.String()}
}

// GetPatchInitializingEmptyResources returns a patch record to initialize an empty resources object for a container.
func GetPatchInitializingEmptyResources(i int) resource_admission.PatchRecord {
return resource_admission.PatchRecord{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%d/resources", i),
Value: core.ResourceRequirements{},
}
}

// GetPatchInitializingEmptyResourcesSubfield returns a patch record to initialize an empty subfield
// (e.g., "requests" or "limits") within a container's resources object.
func GetPatchInitializingEmptyResourcesSubfield(i int, kind string) resource_admission.PatchRecord {
return resource_admission.PatchRecord{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%d/resources/%s", i, kind),
Value: core.ResourceList{},
}
}
44 changes: 44 additions & 0 deletions vertical-pod-autoscaler/pkg/updater/inplace/inplace_updated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package inplace

import (
core "k8s.io/api/core/v1"

resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"

vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
)

type inPlaceUpdate struct{}

func (*inPlaceUpdate) CalculatePatches(pod *core.Pod, _ *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
vpaInPlaceUpdatedValue := annotations.GetVpaInPlaceUpdatedValue()
return []resource_admission.PatchRecord{patch.GetAddAnnotationPatch(annotations.VpaInPlaceUpdatedLabel, vpaInPlaceUpdatedValue)}, nil
}

func (*inPlaceUpdate) PatchResourceTarget() patch.PatchResourceTarget {
return patch.Pod
}

// NewInPlaceUpdatedCalculator returns calculator for
// observed containers patches.
func NewInPlaceUpdatedCalculator() patch.Calculator {
return &inPlaceUpdate{}
}
88 changes: 88 additions & 0 deletions vertical-pod-autoscaler/pkg/updater/inplace/resource_updates.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package inplace

import (
"fmt"

core "k8s.io/api/core/v1"

resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
)

type resourcesInplaceUpdatesPatchCalculator struct {
recommendationProvider recommendation.Provider
}

// NewResourceInPlaceUpdatesCalculator returns a calculator for
// in-place resource update patches.
func NewResourceInPlaceUpdatesCalculator(recommendationProvider recommendation.Provider) patch.Calculator {
return &resourcesInplaceUpdatesPatchCalculator{
recommendationProvider: recommendationProvider,
}
}

// PatchResourceTarget returns the resize subresource to apply calculator patches.
func (*resourcesInplaceUpdatesPatchCalculator) PatchResourceTarget() patch.PatchResourceTarget {
return patch.Resize
}

// CalculatePatches calculates a JSON patch from a VPA's recommendation to send to the pod "resize" subresource as an in-place resize.
func (c *resourcesInplaceUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
result := []resource_admission.PatchRecord{}

containersResources, _, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
if err != nil {
return []resource_admission.PatchRecord{}, fmt.Errorf("Failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
}

for i, containerResources := range containersResources {
newPatches := getContainerPatch(pod, i, containerResources)
result = append(result, newPatches...)
}

return result, nil
}

func getContainerPatch(pod *core.Pod, i int, containerResources vpa_api_util.ContainerResources) []resource_admission.PatchRecord {
var patches []resource_admission.PatchRecord
// Add empty resources object if missing.
if pod.Spec.Containers[i].Resources.Limits == nil &&
pod.Spec.Containers[i].Resources.Requests == nil {
patches = append(patches, patch.GetPatchInitializingEmptyResources(i))
}

patches = appendPatches(patches, pod.Spec.Containers[i].Resources.Requests, i, containerResources.Requests, "requests")
patches = appendPatches(patches, pod.Spec.Containers[i].Resources.Limits, i, containerResources.Limits, "limits")

return patches
}

func appendPatches(patches []resource_admission.PatchRecord, current core.ResourceList, containerIndex int, resources core.ResourceList, fieldName string) []resource_admission.PatchRecord {
// Add empty object if it's missing and we're about to fill it.
if current == nil && len(resources) > 0 {
patches = append(patches, patch.GetPatchInitializingEmptyResourcesSubfield(containerIndex, fieldName))
}
for resource, request := range resources {
patches = append(patches, patch.GetAddResourceRequirementValuePatch(containerIndex, fieldName, resource, request))
}
return patches
}
Loading
Loading