diff --git a/charts/library/common-test/tests/vpa/names_test.yaml b/charts/library/common-test/tests/vpa/names_test.yaml new file mode 100644 index 00000000000..2c9554d03c8 --- /dev/null +++ b/charts/library/common-test/tests/vpa/names_test.yaml @@ -0,0 +1,63 @@ +suite: vpa name test +templates: + - common.yaml +release: + name: test-release-name + namespace: test-release-namespace +tests: + - it: should generate correct vpa name + set: + workload: + workload-name: + enabled: true + primary: true + type: Deployment + podSpec: {} + workload-name-2: + enabled: true + type: Deployment + podSpec: {} + vpa: + vpa-name: + enabled: true + asserts: + - documentIndex: &deploymentDoc 0 + isKind: + of: Deployment + - documentIndex: *deploymentDoc + isAPIVersion: + of: apps/v1 + - documentIndex: *deploymentDoc + equal: + path: metadata.name + value: test-release-name-common-test + - documentIndex: &deploymentDoc 1 + isKind: + of: Deployment + - documentIndex: *deploymentDoc + isAPIVersion: + of: apps/v1 + - documentIndex: *deploymentDoc + equal: + path: metadata.name + value: test-release-name-common-test-workload-name-2 + - documentIndex: &vpaDoc 2 + isKind: + of: VerticalPodAutoscaler + - documentIndex: *vpaDoc + isAPIVersion: + of: autoscaling.k8s.io/v1 + - documentIndex: *vpaDoc + equal: + path: metadata.name + value: test-release-name-common-test + - documentIndex: &otherVPADoc 3 + isKind: + of: VerticalPodAutoscaler + - documentIndex: *otherVPADoc + isAPIVersion: + of: autoscaling.k8s.io/v1 + - documentIndex: *otherVPADoc + equal: + path: metadata.name + value: test-release-name-common-test-workload-name-2 diff --git a/charts/library/common-test/tests/vpa/validation_test.yaml b/charts/library/common-test/tests/vpa/validation_test.yaml new file mode 100644 index 00000000000..b8b652394b8 --- /dev/null +++ b/charts/library/common-test/tests/vpa/validation_test.yaml @@ -0,0 +1,156 @@ +suite: vpa validation test +templates: + - common.yaml +release: + name: test-release-name + namespace: test-release-namespace +tests: + - it: should fail with name longer than 63 characters + set: + workload: &workload + workload-name: + enabled: true + primary: true + type: Deployment + podSpec: {} + vpa: + ? other-vertical-pod-autoscale-name-super-long-name-that-is-longer-than-63-characters + : enabled: true + asserts: + - failedTemplate: + errorMessage: Name [other-vertical-pod-autoscale-name-super-long-name-that-is-longer-than-63-characters] is not valid. Must start and end with an alphanumeric lowercase character. It can contain '-'. And must be at most 63 characters. + + - it: should fail with name starting with underscore + set: + workload: *workload + vpa: + _other-vpa-name: + enabled: true + asserts: + - failedTemplate: + errorMessage: Name [_other-vpa-name] is not valid. Must start and end with an alphanumeric lowercase character. It can contain '-'. And must be at most 63 characters. + + - it: should fail with labels not a dict + set: + workload: *workload + vpa: + vpa-name: + enabled: true + labels: "not a dict" + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Expected [labels] to be a dictionary, but got [string] + + - it: should fail with annotations not a dict + set: + workload: *workload + vpa: + vpa-name: + enabled: true + annotations: "not a dict" + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Expected [annotations] to be a dictionary, but got [string] + + - it: should fail with updatePolicy not a dict + set: + workload: *workload + vpa: + vpa-name: + enabled: true + updatePolicy: "not a dict" + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Expected [updatePolicy] to be a dictionary, but got [string] + + - it: should fail with updatePolicy.updateMode not valid + set: + workload: *workload + vpa: + vpa-name: + enabled: true + updatePolicy: + updateMode: invalid + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Value [invalid] on [vpa.vpa-name.updatePolicy.updateMode] is not valid. Must be one of [Auto, Off, Initial, Recreate] + + - it: should fail with updatePolicy.minReplicas not a number + set: + workload: *workload + vpa: + vpa-name: + enabled: true + updatePolicy: + minReplicas: "some-number" + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Value [some-number] on [vpa.vpa-name.updatePolicy.minReplicas] must be greater than 0. + + - it: should fail with updatePolicy.minReplicas not a positive number + set: + workload: *workload + vpa: + vpa-name: + enabled: true + updatePolicy: + minReplicas: -1 + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Value [-1] on [vpa.vpa-name.updatePolicy.minReplicas] must be greater than 0. + + - it: should fail with evictionRequirements not a list + set: + workload: *workload + vpa: + vpa-name: + enabled: true + updatePolicy: + evictionRequirements: "not a list" + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Value on [vpa.vpa-name.updatePolicy.evictionRequirements] must be a list, but got [string] + + - it: should fail with evictionRequirements.resources not a list + set: + workload: *workload + vpa: + vpa-name: + enabled: true + updatePolicy: + evictionRequirements: + - resources: "not a list" + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Value on [vpa.vpa-name.updatePolicy.evictionRequirements.0.resources] must be a list, but got [string] + + - it: should fail with evictionRequirements.resources.0 not a valid resource + set: + workload: *workload + vpa: + vpa-name: + enabled: true + updatePolicy: + evictionRequirements: + - resources: + - cpu + - memory + - some-resource + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Value [some-resource] on [vpa.vpa-name.updatePolicy.evictionRequirements.0.resources.2] is not valid. Must be one of [cpu, memory] + + - it: should fail with evictionRequirements.changeRequirement not a valid change requirement + set: + workload: *workload + vpa: + vpa-name: + enabled: true + updatePolicy: + evictionRequirements: + - resources: + - cpu + changeRequirement: some-change-requirement + asserts: + - failedTemplate: + errorMessage: Vertical Pod Autoscaler - Value [some-change-requirement] on [vpa.vpa-name.updatePolicy.evictionRequirements.0.changeRequirement] is not valid. Must be one of [TargetHigherThanRequests, TargetLowerThanRequests] diff --git a/charts/library/common/Chart.yaml b/charts/library/common/Chart.yaml index 3acc7116e1d..db9025e4938 100644 --- a/charts/library/common/Chart.yaml +++ b/charts/library/common/Chart.yaml @@ -48,4 +48,4 @@ sources: - https://hub.docker.com/_/ - https://hub.docker.com/r/mikefarah/yq type: library -version: 28.4.0 +version: 28.5.0 diff --git a/charts/library/common/templates/class/_verticalPodAutoscaler.tpl b/charts/library/common/templates/class/_verticalPodAutoscaler.tpl new file mode 100644 index 00000000000..976dbb1ab38 --- /dev/null +++ b/charts/library/common/templates/class/_verticalPodAutoscaler.tpl @@ -0,0 +1,45 @@ +{{/* +This template serves as a blueprint for vertical pod autoscaler objects that are created +using the common library. +*/}} +{{- define "tc.v1.common.class.vpa" -}} + {{- $rootCtx := .rootCtx -}} + {{- $objectData := .objectData -}} + + {{- $_ := set $objectData "updatePolicy" ($objectData.updatePolicy | default dict) -}} + {{- $_ := set $objectData "resourcePolicy" ($objectData.resourcePolicy | default dict) }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: {{ $objectData.name }} + namespace: {{ include "tc.v1.common.lib.metadata.namespace" (dict "rootCtx" $rootCtx "objectData" $objectData "caller" "VPA") }} + {{- $labels := (mustMerge ($objectData.labels | default dict) (include "tc.v1.common.lib.metadata.allLabels" $rootCtx | fromYaml)) -}} + {{- with (include "tc.v1.common.lib.metadata.render" (dict "rootCtx" $rootCtx "labels" $labels) | trim) }} + labels: + {{- . | nindent 4 }} + {{- end -}} + {{- $annotations := (mustMerge ($objectData.annotations | default dict) (include "tc.v1.common.lib.metadata.allAnnotations" $rootCtx | fromYaml)) -}} + {{- with (include "tc.v1.common.lib.metadata.render" (dict "rootCtx" $rootCtx "annotations" $annotations) | trim) }} + annotations: + {{- . | nindent 4 }} + {{- end }} +spec: + targetRef: + apiVersion: apps/v1 + kind: {{ $objectData.workload.type }} + name: {{ $objectData.name }} + updatePolicy: + updateMode: {{ $objectData.updatePolicy.updateMode | default "Auto" }} + {{- with $objectData.updatePolicy.minReplicas }} + minReplicas: {{ . }} + {{- end -}} + {{- if $objectData.updatePolicy.evictionRequirements }} + evictionRequirements: + {{- range $req := $objectData.updatePolicy.evictionRequirements }} + - resources: {{ $req.resources | toJson }} + changeRequirement: {{ $req.changeRequirement }} + {{- end }} + {{- end -}} + +{{- end -}} diff --git a/charts/library/common/templates/lib/vpa/_validation.tpl b/charts/library/common/templates/lib/vpa/_validation.tpl new file mode 100644 index 00000000000..bb7b9027bb1 --- /dev/null +++ b/charts/library/common/templates/lib/vpa/_validation.tpl @@ -0,0 +1,55 @@ +{{- define "tc.v1.common.lib.vpa.validation" -}} + {{- $objectData := .objectData -}} + {{- $rootCtx := .rootCtx -}} + + {{- $updPolicy := $objectData.updatePolicy -}} + {{- if $updPolicy -}} + {{- if not (kindIs "map" $updPolicy) -}} + {{- fail (printf "Vertical Pod Autoscaler - Expected [updatePolicy] to be a dictionary, but got [%s]" (kindOf $updPolicy)) -}} + {{- end -}} + + {{- if $updPolicy.updateMode -}} + {{- $validModes := list "Auto" "Off" "Initial" "Recreate" -}} + {{- if not (mustHas $updPolicy.updateMode $validModes) -}} + {{- fail (printf "Vertical Pod Autoscaler - Value [%s] on [vpa.%s.updatePolicy.updateMode] is not valid. Must be one of [%s]" $updPolicy.updateMode $objectData.vpaName (join ", " $validModes)) -}} + {{- end -}} + {{- end -}} + + {{- if and $updPolicy.minReplicas (le ($updPolicy.minReplicas | int) 0) -}} + {{- fail (printf "Vertical Pod Autoscaler - Value [%v] on [vpa.%s.updatePolicy.minReplicas] must be greater than 0." $updPolicy.minReplicas $objectData.vpaName) -}} + {{- end -}} + + {{- if $updPolicy.evictionRequirements -}} + {{- if not (kindIs "slice" $updPolicy.evictionRequirements) -}} + {{- fail (printf "Vertical Pod Autoscaler - Value on [vpa.%s.updatePolicy.evictionRequirements] must be a list, but got [%s]" $objectData.vpaName (kindOf $updPolicy.evictionRequirements)) -}} + {{- end -}} + {{- range $idx, $req := $updPolicy.evictionRequirements -}} + {{- if not (kindIs "map" $req) -}} + {{- fail (printf "Vertical Pod Autoscaler - Value on [vpa.%s.updatePolicy.evictionRequirements.%d] must be a map, but got [%s]" $objectData.vpaName $idx (kindOf $req)) -}} + {{- end -}} + + {{- if not $req.resources -}} + {{- fail (printf "Vertical Pod Autoscaler - Value on [vpa.%s.updatePolicy.evictionRequirements.%d.resources] is required." $objectData.vpaName $idx) -}} + {{- end -}} + + {{- if not (kindIs "slice" $req.resources) -}} + {{- fail (printf "Vertical Pod Autoscaler - Value on [vpa.%s.updatePolicy.evictionRequirements.%d.resources] must be a list, but got [%s]" $objectData.vpaName $idx (kindOf $req.resources)) -}} + {{- end -}} + + {{- $validResources := (list "cpu" "memory") -}} + {{- range $x, $r := $req.resources -}} + {{- if not (mustHas $r $validResources) -}} + {{- fail (printf "Vertical Pod Autoscaler - Value [%s] on [vpa.%s.updatePolicy.evictionRequirements.%d.resources.%d] is not valid. Must be one of [%s]" $r $objectData.vpaName $idx $x (join ", " $validResources)) -}} + {{- end -}} + {{- end -}} + + {{- $validReq := (list "TargetHigherThanRequests" "TargetLowerThanRequests") -}} + {{- if not (mustHas $req.changeRequirement $validReq) -}} + {{- fail (printf "Vertical Pod Autoscaler - Value [%s] on [vpa.%s.updatePolicy.evictionRequirements.%d.changeRequirement] is not valid. Must be one of [%s]" $req.changeRequirement $objectData.vpaName $idx (join ", " $validReq)) -}} + {{- end -}} + + {{- end -}} + {{- end -}} + + {{- end -}} +{{- end -}} diff --git a/charts/library/common/templates/loader/_apply.tpl b/charts/library/common/templates/loader/_apply.tpl index 987996f2e01..20a634d22d9 100644 --- a/charts/library/common/templates/loader/_apply.tpl +++ b/charts/library/common/templates/loader/_apply.tpl @@ -73,4 +73,7 @@ {{/* Render Cert-Manager Certificates(s) */}} {{- include "tc.v1.common.spawner.certificate" . | nindent 0 -}} + {{/* Render Vertical Pod Autoscaler */}} + {{ include "tc.v1.common.spawner.vpa" . | nindent 0 -}} + {{- end -}} diff --git a/charts/library/common/templates/spawner/_verticalPodAutoscaler.tpl b/charts/library/common/templates/spawner/_verticalPodAutoscaler.tpl new file mode 100644 index 00000000000..97f83fa25ef --- /dev/null +++ b/charts/library/common/templates/spawner/_verticalPodAutoscaler.tpl @@ -0,0 +1,59 @@ +{{/* Vertical Pod Autoscaler Spawner */}} +{{/* Call this template: +{{ include "tc.v1.common.spawner.vpa" $ -}} +*/}} + +{{- define "tc.v1.common.spawner.vpa" -}} + {{- $fullname := include "tc.v1.common.lib.chart.names.fullname" $ -}} + {{- range $name, $vpa := .Values.vpa -}} + {{- $enabledVPA := (include "tc.v1.common.lib.util.enabled" (dict + "rootCtx" $ "objectData" $vpa + "name" $name "caller" "Vertical Pod Autoscaler" + "key" "vpa")) -}} + + {{- if ne $enabledVPA "true" -}}{{- continue -}}{{- end -}} + + {{- $objectData := (mustDeepCopy $vpa) -}} + {{- $_ := set $objectData "vpaName" $name -}} + {{- include "tc.v1.common.lib.vpa.validation" (dict "objectData" $objectData "rootCtx" $) -}} + {{- include "tc.v1.common.lib.chart.names.validation" (dict "name" $name) -}} + + {{- range $workloadName, $workload := $.Values.workload -}} + + {{- $enabled := (include "tc.v1.common.lib.util.enabled" (dict + "rootCtx" $ "objectData" $workload + "name" $name "caller" "vpa" + "key" "workload")) -}} + + {{- if ne $enabled "true" -}}{{- continue -}}{{- end -}} + + {{/* Create a copy of the workload */}} + {{- $_ := set $objectData "workload" (mustDeepCopy $workload) -}} + + {{/* Generate the name of the vpa */}} + {{- $objectName := $fullname -}} + {{- if not $objectData.workload.primary -}} + {{- $objectName = printf "%s-%s" $fullname $workloadName -}} + {{- end -}} + + {{/* Perform validations */}} + {{- include "tc.v1.common.lib.chart.names.validation" (dict "name" $objectName) -}} + {{- include "tc.v1.common.lib.metadata.validation" (dict "objectData" $objectData "caller" "Vertical Pod Autoscaler") -}} + + {{/* Set the name of the workload */}} + {{- $_ := set $objectData "name" $objectName -}} + + {{/* Short name is the one that defined on the chart, used on selectors */}} + {{- $_ := set $objectData "shortName" $workloadName -}} + + {{- if or (not $objectData.targetSelector) (hasKey $objectData.targetSelector $workloadName) -}} + {{/* Call class to create the object */}} + {{- $types := (list "Deployment" "StatefulSet" "DaemonSet") -}} + {{- if (mustHas $objectData.workload.type $types) -}} + {{- include "tc.v1.common.class.vpa" (dict "rootCtx" $ "objectData" $objectData) -}} + {{- end -}} + {{- end -}} + + {{- end -}} + {{- end -}} +{{- end -}} diff --git a/charts/library/common/values.yaml b/charts/library/common/values.yaml index 1ede6196d31..97e921aa97a 100644 --- a/charts/library/common/values.yaml +++ b/charts/library/common/values.yaml @@ -195,6 +195,24 @@ TZ: UTC diagnosticMode: enabled: false +# -- Vertical pod autoscaler +vpa: + main: + enabled: false + targetSelector: [] + # updatePolicy: + # updateMode: auto + resourcePolicy: + containerPolicies: + - containerName: '*' + minAllowed: + cpu: 50m + memory: 50Mi + maxAllowed: + cpu: 8000m + memory: 20Gi + controlledResources: ["cpu", "memory"] + # -- (docs/service/README.md) service: main: