diff --git a/vertical-pod-autoscaler/common/flags.go b/vertical-pod-autoscaler/common/flags.go index fc449d2ae60e..ef6d8b5dfb54 100644 --- a/vertical-pod-autoscaler/common/flags.go +++ b/vertical-pod-autoscaler/common/flags.go @@ -35,18 +35,38 @@ type CommonFlags struct { IgnoredVpaObjectNamespaces string } +// DefaultCommonConfig returns the default values for common flags +func DefaultCommonConfig() *CommonFlags { + return &CommonFlags{ + KubeConfig: "", + KubeApiQps: 50.0, + KubeApiBurst: 100.0, + EnableProfiling: false, + VpaObjectNamespace: apiv1.NamespaceAll, + IgnoredVpaObjectNamespaces: "", + } +} + // InitCommonFlags initializes the common flags func InitCommonFlags() *CommonFlags { - cf := &CommonFlags{} - flag.StringVar(&cf.KubeConfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") - flag.Float64Var(&cf.KubeApiQps, "kube-api-qps", 50.0, "QPS limit when making requests to Kubernetes apiserver") - flag.Float64Var(&cf.KubeApiBurst, "kube-api-burst", 100.0, "QPS burst limit when making requests to Kubernetes apiserver") - flag.BoolVar(&cf.EnableProfiling, "profiling", false, "Is debug/pprof endpoint enabled") - flag.StringVar(&cf.VpaObjectNamespace, "vpa-object-namespace", apiv1.NamespaceAll, "Specifies the namespace to search for VPA objects. Leave empty to include all namespaces. If provided, the garbage collector will only clean this namespace.") - flag.StringVar(&cf.IgnoredVpaObjectNamespaces, "ignored-vpa-object-namespaces", "", "A comma-separated list of namespaces to ignore when searching for VPA objects. Leave empty to avoid ignoring any namespaces. These namespaces will not be cleaned by the garbage collector.") + cf := DefaultCommonConfig() + flag.StringVar(&cf.KubeConfig, "kubeconfig", cf.KubeConfig, "Path to a kubeconfig. Only required if out-of-cluster.") + flag.Float64Var(&cf.KubeApiQps, "kube-api-qps", cf.KubeApiQps, "QPS limit when making requests to Kubernetes apiserver") + flag.Float64Var(&cf.KubeApiBurst, "kube-api-burst", cf.KubeApiBurst, "QPS burst limit when making requests to Kubernetes apiserver") + flag.BoolVar(&cf.EnableProfiling, "profiling", cf.EnableProfiling, "Is debug/pprof endpoint enabled") + flag.StringVar(&cf.VpaObjectNamespace, "vpa-object-namespace", cf.VpaObjectNamespace, "Specifies the namespace to search for VPA objects. Leave empty to include all namespaces. If provided, the garbage collector will only clean this namespace.") + flag.StringVar(&cf.IgnoredVpaObjectNamespaces, "ignored-vpa-object-namespaces", cf.IgnoredVpaObjectNamespaces, "A comma-separated list of namespaces to ignore when searching for VPA objects. Leave empty to avoid ignoring any namespaces. These namespaces will not be cleaned by the garbage collector.") return cf } +// ValidateCommonConfig performs validation of the common flags +func ValidateCommonConfig(config *CommonFlags) { + if len(config.VpaObjectNamespace) > 0 && len(config.IgnoredVpaObjectNamespaces) > 0 { + klog.ErrorS(nil, "--vpa-object-namespace and --ignored-vpa-object-namespaces are mutually exclusive and can't be set together.") + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } +} + // InitLoggingFlags initializes the logging flags func InitLoggingFlags() { // Set the default log level to 4 (info) diff --git a/vertical-pod-autoscaler/go.mod b/vertical-pod-autoscaler/go.mod index 8320bc654d4b..a305d3e298ed 100644 --- a/vertical-pod-autoscaler/go.mod +++ b/vertical-pod-autoscaler/go.mod @@ -14,12 +14,57 @@ require ( golang.org/x/time v0.14.0 k8s.io/api v0.35.0 k8s.io/apimachinery v0.35.0 + k8s.io/autoscaler/vertical-pod-autoscaler/e2e v0.0.0-20260102205634-86115b534e59 k8s.io/client-go v0.35.0 k8s.io/code-generator v0.35.0 k8s.io/component-base v0.35.0 k8s.io/klog/v2 v2.130.1 k8s.io/metrics v0.35.0 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + sigs.k8s.io/controller-runtime v0.22.4 +) + +require ( + cel.dev/expr v0.25.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/cel-go v0.26.1 // indirect + github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/onsi/ginkgo/v2 v2.27.3 // indirect + github.com/onsi/gomega v1.38.3 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 // indirect + google.golang.org/grpc v1.77.0 // indirect + k8s.io/apiextensions-apiserver v0.35.0 // indirect + k8s.io/apiserver v0.35.0 // indirect + k8s.io/component-helpers v0.35.0 // indirect + k8s.io/controller-manager v0.35.0 // indirect + k8s.io/kubectl v0.35.0 // indirect + k8s.io/kubelet v0.35.0 // indirect + k8s.io/kubernetes v1.35.0 // indirect + k8s.io/pod-security-admission v0.35.0 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect ) exclude ( @@ -70,14 +115,14 @@ require ( go.uber.org/mock v0.6.0 go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.30.0 // indirect + golang.org/x/mod v0.31.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.40.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/vertical-pod-autoscaler/go.sum b/vertical-pod-autoscaler/go.sum index 0be62029f70c..37d9dfbcd6a0 100644 --- a/vertical-pod-autoscaler/go.sum +++ b/vertical-pod-autoscaler/go.sum @@ -1,9 +1,17 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -11,14 +19,31 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= @@ -55,19 +80,31 @@ github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6 github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f h1:HU1RgM6NALf/KW9HEY6zry3ADbDKcmpQ+hJedoNGQYQ= +github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -80,6 +117,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -90,10 +133,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= -github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= -github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8= +github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -105,6 +152,8 @@ github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+L github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -113,28 +162,65 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM= +golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= @@ -149,12 +235,22 @@ golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2 h1:7LRqPCEdE4TP4/9psdaB7F2nhZFfBiGJomA5sojLWdU= +google.golang.org/genproto/googleapis/api v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2 h1:2I6GHUeJ/4shcDpoUlLs/2WPnhg7yJwvXtqcMJt9liA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= +google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -164,28 +260,51 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= +k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= +k8s.io/autoscaler/vertical-pod-autoscaler/e2e v0.0.0-20260102205634-86115b534e59 h1:Sxi7b8VVMhrHJKemHY42kphYhdvK9Smma/Wd6/sjERM= +k8s.io/autoscaler/vertical-pod-autoscaler/e2e v0.0.0-20260102205634-86115b534e59/go.mod h1:yVD+VHWNZsIVdqwQ4SH3GgPxOEcW3HT3DVJqjJmRAeI= k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= k8s.io/code-generator v0.35.0 h1:TvrtfKYZTm9oDF2z+veFKSCcgZE3Igv0svY+ehCmjHQ= k8s.io/code-generator v0.35.0/go.mod h1:iS1gvVf3c/T71N5DOGYO+Gt3PdJ6B9LYSvIyQ4FHzgc= k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= +k8s.io/component-helpers v0.35.0 h1:wcXv7HJRksgVjM4VlXJ1CNFBpyDHruRI99RrBtrJceA= +k8s.io/component-helpers v0.35.0/go.mod h1:ahX0m/LTYmu7fL3W8zYiIwnQ/5gT28Ex4o2pymF63Co= +k8s.io/controller-manager v0.35.0 h1:KteodmfVIRzfZ3RDaxhnHb72rswBxEngvdL9vuZOA9A= +k8s.io/controller-manager v0.35.0/go.mod h1:1bVuPNUG6/dpWpevsJpXioS0E0SJnZ7I/Wqc9Awyzm4= k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= +k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= +k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c= +k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA= +k8s.io/kubernetes v1.35.0 h1:PUOojD8c8E3csMP5NX+nLLne6SGqZjrYCscptyBfWMY= +k8s.io/kubernetes v1.35.0/go.mod h1:Tzk9Y9W/XUFFFgTUVg+BAowoFe+Pc7koGLuaiLHdcFg= k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8= k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M= +k8s.io/pod-security-admission v0.35.0 h1:tT3UHC+Q1mpFRe4IoVTu20ZAx+kqgKBZnewRnsDcyfc= +k8s.io/pod-security-admission v0.35.0/go.mod h1:S+57PAqNo6DaUYjmtINiiXlYnEdShrOVMwSc7C4oYPg= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 h1:hSfpvjjTQXQY2Fol2CS0QHMNs/WI1MOSGzCm1KhM5ec= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= +sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A= +sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= diff --git a/vertical-pod-autoscaler/hack/run-integration-tests.sh b/vertical-pod-autoscaler/hack/run-integration-tests.sh new file mode 100755 index 000000000000..b03ab0b0e2b1 --- /dev/null +++ b/vertical-pod-autoscaler/hack/run-integration-tests.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# Copyright 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. + +# This script sets up and runs the VPA integration tests using controller-runtime's envtest. + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +VPA_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +INTEGRATION_DIR="${VPA_DIR}/integration" + +# Kubernetes version to use for envtest binaries +# This should match the k8s.io/* dependency versions in go.mod +ENVTEST_K8S_VERSION="${ENVTEST_K8S_VERSION:-1.35.x}" + +# Directory to store envtest binaries +ENVTEST_ASSETS_DIR="${ENVTEST_ASSETS_DIR:-${HOME}/.local/share/kubebuilder-envtest}" + +echo "==> Setting up envtest environment..." + +# Check if setup-envtest is installed +if ! command -v setup-envtest &> /dev/null; then + echo "==> Installing setup-envtest..." + go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest +fi + +# Setup envtest binaries and get the path +echo "==> Downloading envtest binaries for Kubernetes ${ENVTEST_K8S_VERSION}..." +KUBEBUILDER_ASSETS="$(setup-envtest use "${ENVTEST_K8S_VERSION}" --bin-dir "${ENVTEST_ASSETS_DIR}" -p path)" +export KUBEBUILDER_ASSETS + +echo "==> Using envtest binaries from: ${KUBEBUILDER_ASSETS}" + +# Change to integration test directory +cd "${INTEGRATION_DIR}" + +# Run the tests +echo "==> Running integration tests..." +go test -tags=integration -v -timeout 300s "$@" + +echo "==> Integration tests completed successfully!" diff --git a/vertical-pod-autoscaler/integration/main_test.go b/vertical-pod-autoscaler/integration/main_test.go new file mode 100644 index 000000000000..e5488d47c378 --- /dev/null +++ b/vertical-pod-autoscaler/integration/main_test.go @@ -0,0 +1,114 @@ +//go:build integration +// +build integration + +/* +Copyright 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 integration + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "testing" + + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + vpa_clientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned" +) + +// Global test environment shared across all tests +var ( + testEnv *envtest.Environment + restConfig *rest.Config + kubeClient *clientset.Clientset + vpaClient *vpa_clientset.Clientset + kubeconfig string +) + +// TestMain sets up the integration test environment once for all tests. +// This is more efficient than setting up envtest for each individual test. +func TestMain(m *testing.M) { + var err error + var code int + + defer func() { + // Cleanup + if kubeconfig != "" { + _ = os.Remove(kubeconfig) + } + if testEnv != nil { + _ = testEnv.Stop() + } + os.Exit(code) + }() + + // Setup envtest + if err = setupTestEnv(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to setup test environment: %v\n", err) + code = 1 + return + } + + // Run tests + code = m.Run() +} + +func setupTestEnv() error { + // Get the path to the CRD YAML file relative to this test file + _, thisFile, _, _ := runtime.Caller(0) + crdPath := filepath.Join(filepath.Dir(thisFile), "..", "deploy", "vpa-v1-crd-gen.yaml") + + // envtest looks for binaries in the following order: + // 1. KUBEBUILDER_ASSETS environment variable + // 2. Default path: /usr/local/kubebuilder/bin + // To install the binaries, run: + // go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + // setup-envtest use --bin-dir /usr/local/kubebuilder/bin + // Or set KUBEBUILDER_ASSETS to point to the directory containing etcd and kube-apiserver + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{crdPath}, + ErrorIfCRDPathMissing: true, + } + + var err error + restConfig, err = testEnv.Start() + if err != nil { + return fmt.Errorf("failed to start envtest: %w. Make sure KUBEBUILDER_ASSETS is set or binaries are installed. "+ + "Run: go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest && "+ + "eval $(setup-envtest use -p env)", err) + } + + kubeClient, err = clientset.NewForConfig(restConfig) + if err != nil { + return fmt.Errorf("failed to create kube client: %w", err) + } + + vpaClientConfig := rest.CopyConfig(restConfig) + vpaClientConfig.ContentType = "application/json" + vpaClient, err = vpa_clientset.NewForConfig(vpaClientConfig) + if err != nil { + return fmt.Errorf("failed to create VPA client: %w", err) + } + + // Create a kubeconfig file for the recommender to use + kubeconfig = createKubeconfigFileForRestConfig(restConfig) + + return nil +} diff --git a/vertical-pod-autoscaler/integration/recommender_test.go b/vertical-pod-autoscaler/integration/recommender_test.go new file mode 100644 index 000000000000..cbe55ec67fa9 --- /dev/null +++ b/vertical-pod-autoscaler/integration/recommender_test.go @@ -0,0 +1,389 @@ +//go:build integration +// +build integration + +/* +Copyright 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 integration + +import ( + "context" + "testing" + "time" + + apiv1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + + "k8s.io/autoscaler/vertical-pod-autoscaler/common" + "k8s.io/autoscaler/vertical-pod-autoscaler/e2e/utils" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/app" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test" +) + +func TestRecommenderWithNamespaceFiltering(t *testing.T) { + ctx := t.Context() + + // Create test namespaces + watchedNS := "ns-filtering-watched" + ignoredNS := "ns-filtering-ignored" + + for _, ns := range []string{watchedNS, ignoredNS} { + _, err := kubeClient.CoreV1().Namespaces().Create(ctx, &apiv1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: ns}, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create namespace %s: %v", ns, err) + } + defer func(ns string) { + _ = kubeClient.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}) + }(ns) + } + + // Create VPA objects in both namespaces + for _, ns := range []string{watchedNS, ignoredNS} { + vpa := test.VerticalPodAutoscaler(). + WithName("test-vpa"). + WithContainer("hamster"). + WithNamespace(ns). + WithTargetRef(utils.HamsterTargetRef). + Get() + + _, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers(ns).Create(ctx, vpa, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create VPA in namespace %s: %v", ns, err) + } + } + + // Configure the recommender to watch only the watched namespace + config := app.DefaultRecommenderConfig() + config.CommonFlags = &common.CommonFlags{ + KubeConfig: kubeconfig, + VpaObjectNamespace: watchedNS, // Only watch the watched namespace + IgnoredVpaObjectNamespaces: "", + } + config.MetricsFetcherInterval = 1 * time.Second // Short interval for testing + + _, cancel := startRecommender(t, config) + defer cancel() + + // Wait for the recommender to process the VPA in the watched namespace. + // The recommender should add status conditions to VPAs it manages. + err := wait.PollUntilContextTimeout(ctx, 1*time.Second, 50*time.Second, true, func(ctx context.Context) (done bool, err error) { + watchedVPA, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers(watchedNS).Get(ctx, "test-vpa", metav1.GetOptions{}) + if err != nil { + return false, err + } + // watched namespace should have status updates + if len(watchedVPA.Status.Conditions) > 0 { + return true, nil + } + return false, nil + }) + if err != nil { + t.Fatalf("VPA in watched namespace should have status conditions: %v", err) + } + + // Fetch VPA in the ignored namespace. + // The recommender should NOT have added a status conditions to this VPA. + ignoredVPA, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers(ignoredNS).Get(ctx, "test-vpa", metav1.GetOptions{}) + if err != nil { + t.Fatalf("Unable to get VPA in ignored namespace: %v", err) + } + + if len(ignoredVPA.Status.Conditions) != 0 { + t.Fatal("VPA in ignored namespace should NOT have status conditions") + } +} + +func TestRecommenderWithNamespaceExclusions(t *testing.T) { + ctx := t.Context() + + // Create test namespaces + watchedNS := "ns-exclusions-watched" + ignoredNS := "ns-exclusions-ignored" + + for _, ns := range []string{watchedNS, ignoredNS} { + _, err := kubeClient.CoreV1().Namespaces().Create(ctx, &apiv1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: ns}, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create namespace %s: %v", ns, err) + } + defer func(ns string) { + _ = kubeClient.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}) + }(ns) + } + + // Create VPA objects in both namespaces + for _, ns := range []string{watchedNS, ignoredNS} { + vpa := test.VerticalPodAutoscaler(). + WithName("test-vpa"). + WithContainer("hamster"). + WithNamespace(ns). + WithTargetRef(utils.HamsterTargetRef). + Get() + + _, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers(ns).Create(ctx, vpa, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create VPA in namespace %s: %v", ns, err) + } + } + + // Configure the recommender to exclude the ignored namespace + config := app.DefaultRecommenderConfig() + config.CommonFlags = &common.CommonFlags{ + KubeConfig: kubeconfig, + VpaObjectNamespace: "", // Watch all namespaces + IgnoredVpaObjectNamespaces: ignoredNS, + } + config.MetricsFetcherInterval = 1 * time.Second // Short interval for testing + + _, cancel := startRecommender(t, config) + defer cancel() + + // Wait for the recommender to process the VPA in the watched namespace. + // The recommender should add status conditions to VPAs it manages. + err := wait.PollUntilContextTimeout(ctx, 1*time.Second, 50*time.Second, true, func(ctx context.Context) (done bool, err error) { + watchedVPA, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers(watchedNS).Get(ctx, "test-vpa", metav1.GetOptions{}) + if err != nil { + return false, err + } + // watched namespace should have status updates + if len(watchedVPA.Status.Conditions) > 0 { + return true, nil + } + return false, nil + }) + if err != nil { + t.Fatalf("VPA in watched namespace should have status conditions: %v", err) + } + + // Fetch VPA in the ignored namespace. + // The recommender should NOT have added a status conditions to this VPA. + ignoredVPA, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers(ignoredNS).Get(ctx, "test-vpa", metav1.GetOptions{}) + if err != nil { + t.Fatalf("Unable to get VPA in ignored namespace: %v", err) + } + + if len(ignoredVPA.Status.Conditions) != 0 { + t.Fatal("VPA in ignored namespace should NOT have status conditions") + } +} + +func TestCRDCheckpointGC(t *testing.T) { + ctx := t.Context() + + ns := "checkpoint-gc-test" + + // Create test namespace + _, err := kubeClient.CoreV1().Namespaces().Create(ctx, &apiv1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: ns}, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create namespace %s: %v", ns, err) + } + defer func(ns string) { + _ = kubeClient.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}) + }(ns) + + // Create a Deployment that the VPA will target + deploymentLabel := map[string]string{"app": "hamster"} + deployment := newHamsterDeployment(ns, 1, deploymentLabel) + + _, err = kubeClient.AppsV1().Deployments(ns).Create(ctx, deployment, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create Deployment in namespace %s: %v", ns, err) + } + + // Create Pods matching the deployment (simulating what the deployment controller would do) + pod := test.Pod(). + WithName("hamster-pod-0"). + WithLabels(deploymentLabel). + WithPhase(apiv1.PodRunning). + AddContainer(test.Container(). + WithName("hamster"). + WithImage("busybox"). + WithCPURequest(resource.MustParse("100m")). + WithMemRequest(resource.MustParse("50Mi")). + Get()). + AddContainerStatus(apiv1.ContainerStatus{ + Name: "hamster", + Ready: true, + State: apiv1.ContainerState{ + Running: &apiv1.ContainerStateRunning{ + StartedAt: metav1.Now(), + }, + }, + }). + Get() + pod.Namespace = ns + + createdPod, err := kubeClient.CoreV1().Pods(ns).Create(ctx, pod, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create Pod in namespace %s: %v", ns, err) + } + + // Since the API server ignores status on Create, we need to update it separately + createdPod.Status = pod.Status + _, err = kubeClient.CoreV1().Pods(ns).UpdateStatus(ctx, createdPod, metav1.UpdateOptions{}) + if err != nil { + t.Fatalf("Failed to update Pod status in namespace %s: %v", ns, err) + } + + // Create the VPA targeting the deployment + vpa := test.VerticalPodAutoscaler(). + WithName("test-vpa"). + WithNamespace(ns). + WithContainer("hamster"). + WithTargetRef(utils.HamsterTargetRef). + Get() + + _, err = vpaClient.AutoscalingV1().VerticalPodAutoscalers(ns).Create(ctx, vpa, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create VPA in namespace %s: %v", ns, err) + } + + // Configure the recommender to watch only the watched namespace + config := app.DefaultRecommenderConfig() + config.CommonFlags = &common.CommonFlags{ + KubeConfig: kubeconfig, + } + config.MetricsFetcherInterval = 1 * time.Second // Short interval for testing + config.CheckpointsGCInterval = 1 * time.Second // Short interval for testing + + _, cancel := startRecommender(t, config) + defer cancel() + + err = wait.PollUntilContextTimeout(ctx, 1*time.Second, 50*time.Second, true, func(ctx context.Context) (done bool, err error) { + _, err = vpaClient.AutoscalingV1().VerticalPodAutoscalerCheckpoints(ns).Get(ctx, "test-vpa-hamster", metav1.GetOptions{}) + if err == nil { + return true, nil // Checkpoint found + } + if apierrors.IsNotFound(err) { + return false, nil // Not found yet, keep polling + } + return false, err // Real error, stop and fail + }) + + if err != nil { + t.Fatalf("Timed out waiting for checkpoint to be created: %v", err) + } + + err = vpaClient.AutoscalingV1().VerticalPodAutoscalers(ns).Delete(ctx, "test-vpa", metav1.DeleteOptions{}) + if err != nil { + t.Fatalf("Failed to delete VPA: %v", err) + } + + err = wait.PollUntilContextTimeout(ctx, 1*time.Second, 50*time.Second, true, func(ctx context.Context) (done bool, err error) { + cp, err := vpaClient.AutoscalingV1().VerticalPodAutoscalerCheckpoints(ns).Get(ctx, "test-vpa-hamster", metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return true, nil // Checkpoint was deleted by GC - this is what we're testing for + } + return false, err // Some other error (e.g., server shutdown) - fail the test + } + t.Log("Found Checkpoint, still waiting for GC", cp.Name) + return false, nil + }) + + if err != nil { + t.Fatalf("Timed out waiting for VPA Checkpoint to be garbage collected: %v", err) + } +} + +func TestRecommenderName(t *testing.T) { + ctx := t.Context() + + ns := "recommender-name-test" + + // Create test namespace + _, err := kubeClient.CoreV1().Namespaces().Create(ctx, &apiv1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: ns}, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create namespace %s: %v", ns, err) + } + defer func(ns string) { + _ = kubeClient.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}) + }(ns) + + // Create two VPAs that target different recommenders: + // - "vpa-for-custom-recommender" uses recommender "custom-recommender" + // - "vpa-for-default-recommender" uses recommender "default" (empty string means default) + vpaCustom := test.VerticalPodAutoscaler(). + WithName("vpa-for-custom-recommender"). + WithContainer("hamster"). + WithNamespace(ns). + WithRecommender("custom-recommender"). + WithTargetRef(utils.HamsterTargetRef). + Get() + + vpaDefault := test.VerticalPodAutoscaler(). + WithName("vpa-for-default-recommender"). + WithContainer("hamster"). + WithNamespace(ns). + WithTargetRef(utils.HamsterTargetRef). // No WithRecommender = uses default recommender + Get() + + _, err = vpaClient.AutoscalingV1().VerticalPodAutoscalers(ns).Create(ctx, vpaCustom, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create VPA: %v", err) + } + + _, err = vpaClient.AutoscalingV1().VerticalPodAutoscalers(ns).Create(ctx, vpaDefault, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create VPA: %v", err) + } + + // Start a recommender named "custom-recommender" + // It should only process VPAs that specify this recommender name + config := app.DefaultRecommenderConfig() + config.CommonFlags = &common.CommonFlags{ + KubeConfig: kubeconfig, + } + config.MetricsFetcherInterval = 1 * time.Second + config.RecommenderName = "custom-recommender" + + _, cancel := startRecommender(t, config) + defer cancel() + + // The VPA targeting "custom-recommender" should get status updates + err = wait.PollUntilContextTimeout(ctx, 1*time.Second, 50*time.Second, true, func(ctx context.Context) (done bool, err error) { + vpa, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers(ns).Get(ctx, "vpa-for-custom-recommender", metav1.GetOptions{}) + if err != nil { + return false, err + } + if len(vpa.Status.Conditions) > 0 { + return true, nil + } + return false, nil + }) + if err != nil { + t.Fatalf("VPA targeting custom-recommender should have status conditions: %v", err) + } + + vpa, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers(ns).Get(ctx, "vpa-for-default-recommender", metav1.GetOptions{}) + if err != nil { + t.Fatalf("Unable to get VPA for the default recommender: %v", err) + } + // We expect NO conditions - if we see any, the test should fail + if len(vpa.Status.Conditions) > 0 { + t.Fatal("VPA targeting default recommender should NOT have status conditions (custom-recommender should ignore it)") + } +} diff --git a/vertical-pod-autoscaler/integration/utils.go b/vertical-pod-autoscaler/integration/utils.go new file mode 100644 index 000000000000..186c9b52f7f1 --- /dev/null +++ b/vertical-pod-autoscaler/integration/utils.go @@ -0,0 +1,125 @@ +//go:build integration +// +build integration + +/* +Copyright 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 integration + +import ( + "context" + "os" + "testing" + "time" + + appsv1 "k8s.io/api/apps/v1" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/app" +) + +func createKubeconfigFileForRestConfig(restConfig *rest.Config) string { + clusters := make(map[string]*clientcmdapi.Cluster) + clusters["default-cluster"] = &clientcmdapi.Cluster{ + Server: restConfig.Host, + TLSServerName: restConfig.ServerName, + CertificateAuthorityData: restConfig.CAData, + } + contexts := make(map[string]*clientcmdapi.Context) + contexts["default-context"] = &clientcmdapi.Context{ + Cluster: "default-cluster", + AuthInfo: "default-user", + } + authinfos := make(map[string]*clientcmdapi.AuthInfo) + authinfos["default-user"] = &clientcmdapi.AuthInfo{ + ClientCertificateData: restConfig.CertData, + ClientKeyData: restConfig.KeyData, + Token: restConfig.BearerToken, + } + clientConfig := clientcmdapi.Config{ + Kind: "Config", + APIVersion: "v1", + Clusters: clusters, + Contexts: contexts, + CurrentContext: "default-context", + AuthInfos: authinfos, + } + kubeConfigFile, _ := os.CreateTemp("", "kubeconfig") + _ = clientcmd.WriteToFile(clientConfig, kubeConfigFile.Name()) + return kubeConfigFile.Name() +} + +// startRecommender creates and starts a recommender app with the given config. +// It returns a context for the recommender and a cancel function that should be deferred. +// The recommender runs in a background goroutine. +func startRecommender(t *testing.T, config *app.RecommenderConfig) (recommenderCtx context.Context, cancel func()) { + t.Helper() + + recommenderApp, err := app.NewRecommenderApp(config) + if err != nil { + t.Fatalf("Failed to create recommender app: %v", err) + } + + recommenderCtx, recommenderCancel := context.WithTimeout(context.Background(), 15*time.Second) + + // Start the recommender in a goroutine + errChan := make(chan error, 1) + doneChan := make(chan struct{}) + leaderElection := app.DefaultLeaderElectionConfiguration() + leaderElection.LeaderElect = false // Disable leader election for testing + + go func() { + defer close(doneChan) + t.Log("Starting recommender app...") + err := recommenderApp.Run(recommenderCtx, leaderElection) + if err != nil && recommenderCtx.Err() == nil { + errChan <- err + } + }() + + return recommenderCtx, recommenderCancel +} + +// newHamsterDeployment creates a simple hamster deployment for testing. +func newHamsterDeployment(ns string, replicas int32, labels map[string]string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "hamster-deployment", + Namespace: ns, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: apiv1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: apiv1.PodSpec{ + Containers: []apiv1.Container{{ + Name: "hamster", + Image: "busybox", + }}, + }, + }, + }, + } +} diff --git a/vertical-pod-autoscaler/pkg/admission-controller/main.go b/vertical-pod-autoscaler/pkg/admission-controller/main.go index efb633bad6a6..2c95a07e7c02 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/main.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/main.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "context" "flag" "fmt" "net/http" @@ -99,12 +100,14 @@ func main() { config := common.CreateKubeConfigOrDie(commonFlags.KubeConfig, float32(commonFlags.KubeApiQps), int(commonFlags.KubeApiBurst)) + ctx := context.Background() + vpaClient := vpa_clientset.NewForConfigOrDie(config) vpaLister := vpa_api_util.NewVpasLister(vpaClient, make(chan struct{}), commonFlags.VpaObjectNamespace) kubeClient := kube_client.NewForConfigOrDie(config) factory := informers.NewSharedInformerFactory(kubeClient, defaultResyncPeriod) - targetSelectorFetcher := target.NewVpaTargetSelectorFetcher(config, kubeClient, factory) - controllerFetcher := controllerfetcher.NewControllerFetcher(config, kubeClient, factory, scaleCacheEntryFreshnessTime, scaleCacheEntryLifetime, scaleCacheEntryJitterFactor) + targetSelectorFetcher := target.NewVpaTargetSelectorFetcher(ctx, config, kubeClient, factory) + controllerFetcher := controllerfetcher.NewControllerFetcher(ctx, config, kubeClient, factory, scaleCacheEntryFreshnessTime, scaleCacheEntryLifetime, scaleCacheEntryJitterFactor) podPreprocessor := pod.NewDefaultPreProcessor() vpaPreprocessor := vpa.NewDefaultPreProcessor() var limitRangeCalculator limitrange.LimitRangeCalculator diff --git a/vertical-pod-autoscaler/pkg/recommender/app/app.go b/vertical-pod-autoscaler/pkg/recommender/app/app.go new file mode 100644 index 000000000000..92a209cfd012 --- /dev/null +++ b/vertical-pod-autoscaler/pkg/recommender/app/app.go @@ -0,0 +1,324 @@ +/* +Copyright 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 app + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/client-go/informers" + kube_client "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/leaderelection" + "k8s.io/client-go/tools/leaderelection/resourcelock" + componentbaseconfig "k8s.io/component-base/config" + "k8s.io/klog/v2" + resourceclient "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1" + + "k8s.io/autoscaler/vertical-pod-autoscaler/common" + vpa_clientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/checkpoint" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/history" + input_metrics "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/metrics" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/logic" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/routines" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target" + controllerfetcher "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target/controller_fetcher" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics" + metrics_quality "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics/quality" + metrics_recommender "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics/recommender" + metrics_resources "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics/resources" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/server" + vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa" +) + +const ( + // aggregateContainerStateGCInterval defines how often expired AggregateContainerStates are garbage collected. + aggregateContainerStateGCInterval = 1 * time.Hour + scaleCacheEntryLifetime time.Duration = time.Hour + scaleCacheEntryFreshnessTime time.Duration = 10 * time.Minute + scaleCacheEntryJitterFactor float64 = 1. + scaleCacheLoopPeriod = 7 * time.Second + defaultResyncPeriod time.Duration = 10 * time.Minute +) + +// RecommenderApp represents the recommender application +type RecommenderApp struct { + config *RecommenderConfig +} + +// NewRecommenderApp creates a new RecommenderApp with the given configuration +func NewRecommenderApp(config *RecommenderConfig) (*RecommenderApp, error) { + if config == nil { + return nil, fmt.Errorf("config cannot be nil") + } + + // Load bearer token from file if specified + if config.PrometheusBearerTokenFile != "" { + fileContent, err := os.ReadFile(config.PrometheusBearerTokenFile) + if err != nil { + return nil, fmt.Errorf("unable to read bearer token file %s: %w", config.PrometheusBearerTokenFile, err) + } + config.PrometheusBearerToken = strings.TrimSpace(string(fileContent)) + } + + return &RecommenderApp{ + config: config, + }, nil +} + +// Run starts the recommender with the given context +func (app *RecommenderApp) Run(ctx context.Context, leaderElection componentbaseconfig.LeaderElectionConfiguration) error { + stopCh := make(chan struct{}) + // Close stopCh when context is canceled to signal all goroutines to stop + go func() { + <-ctx.Done() + close(stopCh) + }() + + healthCheck := metrics.NewHealthCheck(app.config.MetricsFetcherInterval * 5) + metrics_recommender.Register() + metrics_quality.Register() + metrics_resources.Register() + server.InitializeWithContext(ctx, &app.config.CommonFlags.EnableProfiling, healthCheck, &app.config.Address) + + if !leaderElection.LeaderElect { + return app.run(ctx, stopCh, healthCheck) + } + id, err := os.Hostname() + if err != nil { + return fmt.Errorf("unable to get hostname: %w", err) + } + + id = id + "_" + string(uuid.NewUUID()) + + config := common.CreateKubeConfigOrDie(app.config.CommonFlags.KubeConfig, float32(app.config.CommonFlags.KubeApiQps), int(app.config.CommonFlags.KubeApiBurst)) + kubeClient := kube_client.NewForConfigOrDie(config) + + lock, err := resourcelock.New( + leaderElection.ResourceLock, + leaderElection.ResourceNamespace, + leaderElection.ResourceName, + kubeClient.CoreV1(), + kubeClient.CoordinationV1(), + resourcelock.ResourceLockConfig{ + Identity: id, + }, + ) + if err != nil { + return fmt.Errorf("unable to create leader election lock: %w", err) + } + + leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ + Lock: lock, + LeaseDuration: leaderElection.LeaseDuration.Duration, + RenewDeadline: leaderElection.RenewDeadline.Duration, + RetryPeriod: leaderElection.RetryPeriod.Duration, + ReleaseOnCancel: true, + Callbacks: leaderelection.LeaderCallbacks{ + OnStartedLeading: func(_ context.Context) { + if err := app.run(ctx, stopCh, healthCheck); err != nil { + klog.Fatalf("Error running recommender: %v", err) + } + }, + OnStoppedLeading: func() { + klog.Fatal("lost master") + }, + }, + }) + + return nil +} + +func (app *RecommenderApp) run(ctx context.Context, stopCh chan struct{}, healthCheck *metrics.HealthCheck) error { + config := common.CreateKubeConfigOrDie(app.config.CommonFlags.KubeConfig, float32(app.config.CommonFlags.KubeApiQps), int(app.config.CommonFlags.KubeApiBurst)) + kubeClient := kube_client.NewForConfigOrDie(config) + clusterState := model.NewClusterState(aggregateContainerStateGCInterval) + factory := informers.NewSharedInformerFactoryWithOptions(kubeClient, defaultResyncPeriod, informers.WithNamespace(app.config.CommonFlags.VpaObjectNamespace)) + controllerFetcher := controllerfetcher.NewControllerFetcher(ctx, config, kubeClient, factory, scaleCacheEntryFreshnessTime, scaleCacheEntryLifetime, scaleCacheEntryJitterFactor) + podLister, oomObserver := input.NewPodListerAndOOMObserver(ctx, kubeClient, app.config.CommonFlags.VpaObjectNamespace, stopCh) + + factory.Start(stopCh) + informerMap := factory.WaitForCacheSync(stopCh) + for kind, synced := range informerMap { + if !synced { + return fmt.Errorf("could not sync cache for the %s informer", kind.String()) + } + } + + model.InitializeAggregationsConfig(model.NewAggregationsConfig( + app.config.MemoryAggregationInterval, + app.config.MemoryAggregationIntervalCount, + app.config.MemoryHistogramDecayHalfLife, + app.config.CpuHistogramDecayHalfLife, + app.config.OOMBumpUpRatio, + app.config.OOMMinBumpUp, + )) + + useCheckpoints := app.config.Storage != "prometheus" + + var postProcessors []routines.RecommendationPostProcessor + if app.config.PostProcessorCPUasInteger { + postProcessors = append(postProcessors, &routines.IntegerCPUPostProcessor{}) + } + + globalMaxAllowed := app.initGlobalMaxAllowed() + // CappingPostProcessor, should always come in the last position for post-processing + postProcessors = append(postProcessors, routines.NewCappingRecommendationProcessor(globalMaxAllowed)) + + var source input_metrics.PodMetricsLister + if app.config.UseExternalMetrics { + resourceMetrics := map[apiv1.ResourceName]string{} + if app.config.ExternalCpuMetric != "" { + resourceMetrics[apiv1.ResourceCPU] = app.config.ExternalCpuMetric + } + if app.config.ExternalMemoryMetric != "" { + resourceMetrics[apiv1.ResourceMemory] = app.config.ExternalMemoryMetric + } + externalClientOptions := &input_metrics.ExternalClientOptions{ + ResourceMetrics: resourceMetrics, + ContainerNameLabel: app.config.CtrNameLabel, + } + klog.V(1).InfoS("Using External Metrics", "options", externalClientOptions) + source = input_metrics.NewExternalClient(config, clusterState, *externalClientOptions) + } else { + klog.V(1).InfoS("Using Metrics Server") + source = input_metrics.NewPodMetricsesSource(resourceclient.NewForConfigOrDie(config)) + } + + ignoredNamespaces := strings.Split(app.config.CommonFlags.IgnoredVpaObjectNamespaces, ",") + + clusterStateFeeder := input.ClusterStateFeederFactory{ + PodLister: podLister, + OOMObserver: oomObserver, + KubeClient: kubeClient, + MetricsClient: input_metrics.NewMetricsClient(source, app.config.CommonFlags.VpaObjectNamespace, "default-metrics-client"), + VpaCheckpointClient: vpa_clientset.NewForConfigOrDie(config).AutoscalingV1(), + VpaLister: vpa_api_util.NewVpasLister(vpa_clientset.NewForConfigOrDie(config), stopCh, app.config.CommonFlags.VpaObjectNamespace), + VpaCheckpointLister: vpa_api_util.NewVpaCheckpointLister(vpa_clientset.NewForConfigOrDie(config), stopCh, app.config.CommonFlags.VpaObjectNamespace), + ClusterState: clusterState, + SelectorFetcher: target.NewVpaTargetSelectorFetcher(ctx, config, kubeClient, factory), + MemorySaveMode: app.config.MemorySaver, + ControllerFetcher: controllerFetcher, + RecommenderName: app.config.RecommenderName, + IgnoredNamespaces: ignoredNamespaces, + VpaObjectNamespace: app.config.CommonFlags.VpaObjectNamespace, + }.Make() + controllerFetcher.Start(ctx, scaleCacheLoopPeriod) + + recommender := routines.RecommenderFactory{ + ClusterState: clusterState, + ClusterStateFeeder: clusterStateFeeder, + ControllerFetcher: controllerFetcher, + CheckpointWriter: checkpoint.NewCheckpointWriter(clusterState, vpa_clientset.NewForConfigOrDie(config).AutoscalingV1()), + VpaClient: vpa_clientset.NewForConfigOrDie(config).AutoscalingV1(), + PodResourceRecommender: logic.CreatePodResourceRecommender(), + RecommendationPostProcessors: postProcessors, + CheckpointsGCInterval: app.config.CheckpointsGCInterval, + UseCheckpoints: useCheckpoints, + UpdateWorkerCount: app.config.UpdateWorkerCount, + }.Make() + + promQueryTimeout, err := time.ParseDuration(app.config.QueryTimeout) + if err != nil { + return fmt.Errorf("could not parse --prometheus-query-timeout as a time.Duration: %w", err) + } + + if useCheckpoints { + recommender.GetClusterStateFeeder().InitFromCheckpoints(ctx) + } else { + historyConfig := history.PrometheusHistoryProviderConfig{ + Address: app.config.PrometheusAddress, + Insecure: app.config.PrometheusInsecure, + QueryTimeout: promQueryTimeout, + HistoryLength: app.config.HistoryLength, + HistoryResolution: app.config.HistoryResolution, + PodLabelPrefix: app.config.PodLabelPrefix, + PodLabelsMetricName: app.config.PodLabelsMetricName, + PodNamespaceLabel: app.config.PodNamespaceLabel, + PodNameLabel: app.config.PodNameLabel, + CtrNamespaceLabel: app.config.CtrNamespaceLabel, + CtrPodNameLabel: app.config.CtrPodNameLabel, + CtrNameLabel: app.config.CtrNameLabel, + CadvisorMetricsJobName: app.config.PrometheusJobName, + Namespace: app.config.CommonFlags.VpaObjectNamespace, + Authentication: history.PrometheusCredentials{ + BearerToken: app.config.PrometheusBearerToken, + Username: app.config.Username, + Password: app.config.Password, + }, + } + provider, err := history.NewPrometheusHistoryProvider(historyConfig) + if err != nil { + return fmt.Errorf("could not initialize history provider: %w", err) + } + recommender.GetClusterStateFeeder().InitFromHistoryProvider(provider) + } + + // Start updating health check endpoint. + healthCheck.StartMonitoring() + + ticker := time.NewTicker(app.config.MetricsFetcherInterval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + recommender.RunOnce() + healthCheck.UpdateLastActivity() + } + } +} + +func (app *RecommenderApp) initGlobalMaxAllowed() apiv1.ResourceList { + result := make(apiv1.ResourceList) + if !app.config.MaxAllowedCPU.IsZero() { + result[apiv1.ResourceCPU] = app.config.MaxAllowedCPU.Quantity + } + if !app.config.MaxAllowedMemory.IsZero() { + result[apiv1.ResourceMemory] = app.config.MaxAllowedMemory.Quantity + } + return result +} + +const ( + defaultLeaseDuration = 15 * time.Second + defaultRenewDeadline = 10 * time.Second + defaultRetryPeriod = 2 * time.Second +) + +// DefaultLeaderElectionConfiguration returns the default leader election configuration +func DefaultLeaderElectionConfiguration() componentbaseconfig.LeaderElectionConfiguration { + return componentbaseconfig.LeaderElectionConfiguration{ + LeaderElect: false, + LeaseDuration: metav1.Duration{Duration: defaultLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: defaultRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: defaultRetryPeriod}, + ResourceLock: resourcelock.LeasesResourceLock, + // This was changed from "vpa-recommender" to avoid conflicts with managed VPA deployments. + ResourceName: "vpa-recommender-lease", + ResourceNamespace: metav1.NamespaceSystem, + } +} diff --git a/vertical-pod-autoscaler/pkg/recommender/app/config.go b/vertical-pod-autoscaler/pkg/recommender/app/config.go new file mode 100644 index 000000000000..b21a073e3faf --- /dev/null +++ b/vertical-pod-autoscaler/pkg/recommender/app/config.go @@ -0,0 +1,157 @@ +/* +Copyright 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 app + +import ( + "os" + "strings" + "time" + + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/klog/v2" + + "k8s.io/autoscaler/vertical-pod-autoscaler/common" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/routines" +) + +// RecommenderConfig holds all configuration for the recommender component +type RecommenderConfig struct { + // Common flags + CommonFlags *common.CommonFlags + + // Recommender-specific flags + RecommenderName string + MetricsFetcherInterval time.Duration + CheckpointsGCInterval time.Duration + Address string + Storage string + MemorySaver bool + UpdateWorkerCount int + + // Prometheus history provider configuration + PrometheusAddress string + PrometheusInsecure bool + PrometheusJobName string + HistoryLength string + HistoryResolution string + QueryTimeout string + PodLabelPrefix string + PodLabelsMetricName string + PodNamespaceLabel string + PodNameLabel string + CtrNamespaceLabel string + CtrPodNameLabel string + CtrNameLabel string + Username string + Password string + PrometheusBearerToken string + PrometheusBearerTokenFile string + + // External metrics provider configuration + UseExternalMetrics bool + ExternalCpuMetric string + ExternalMemoryMetric string + + // Aggregation configuration + MemoryAggregationInterval time.Duration + MemoryAggregationIntervalCount int64 + MemoryHistogramDecayHalfLife time.Duration + CpuHistogramDecayHalfLife time.Duration + OOMBumpUpRatio float64 + OOMMinBumpUp float64 + + // Post processors configuration + PostProcessorCPUasInteger bool + MaxAllowedCPU resource.QuantityValue + MaxAllowedMemory resource.QuantityValue +} + +// DefaultRecommenderConfig returns a RecommenderConfig with default values +func DefaultRecommenderConfig() *RecommenderConfig { + return &RecommenderConfig{ + CommonFlags: common.DefaultCommonConfig(), + + // Recommender-specific flags + RecommenderName: input.DefaultRecommenderName, + MetricsFetcherInterval: 1 * time.Minute, + CheckpointsGCInterval: 10 * time.Minute, + Address: ":8942", + Storage: "", + MemorySaver: false, + UpdateWorkerCount: 10, + + // Prometheus history provider flags + PrometheusAddress: "http://prometheus.monitoring.svc", + PrometheusInsecure: false, + PrometheusJobName: "kubernetes-cadvisor", + HistoryLength: "8d", + HistoryResolution: "1h", + QueryTimeout: "5m", + PodLabelPrefix: "pod_label_", + PodLabelsMetricName: "up{job=\"kubernetes-pods\"}", + PodNamespaceLabel: "kubernetes_namespace", + PodNameLabel: "kubernetes_pod_name", + CtrNamespaceLabel: "namespace", + CtrPodNameLabel: "pod_name", + CtrNameLabel: "name", + Username: "", + Password: "", + PrometheusBearerToken: "", + PrometheusBearerTokenFile: "", + + // External metrics provider flags + UseExternalMetrics: false, + ExternalCpuMetric: "", + ExternalMemoryMetric: "", + + // Aggregation configuration flags + MemoryAggregationInterval: model.DefaultMemoryAggregationInterval, + MemoryAggregationIntervalCount: model.DefaultMemoryAggregationIntervalCount, + MemoryHistogramDecayHalfLife: model.DefaultMemoryHistogramDecayHalfLife, + CpuHistogramDecayHalfLife: model.DefaultCPUHistogramDecayHalfLife, + OOMBumpUpRatio: model.DefaultOOMBumpUpRatio, + OOMMinBumpUp: model.DefaultOOMMinBumpUp, + + // Post processors flags + PostProcessorCPUasInteger: false, + MaxAllowedCPU: resource.QuantityValue{}, + MaxAllowedMemory: resource.QuantityValue{}, + } +} + +// ValidateRecommenderConfig performs validation of the recommender flags +func ValidateRecommenderConfig(config *RecommenderConfig) { + if *routines.MinCheckpointsPerRun != 10 { // Default value is 10 + klog.InfoS("DEPRECATION WARNING: The 'min-checkpoints' flag is deprecated and has no effect. It will be removed in a future release.") + } + + if config.PrometheusBearerToken != "" && config.PrometheusBearerTokenFile != "" && config.Username != "" { + klog.ErrorS(nil, "--bearer-token, --bearer-token-file and --username are mutually exclusive and can't be set together.") + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } + + if config.PrometheusBearerTokenFile != "" { + fileContent, err := os.ReadFile(config.PrometheusBearerTokenFile) + if err != nil { + klog.ErrorS(err, "Unable to read bearer token file", "filename", config.PrometheusBearerTokenFile) + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } + config.PrometheusBearerTokenFile = strings.TrimSpace(string(fileContent)) + } +} diff --git a/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go b/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go index 9f997e54f271..74fec1475301 100644 --- a/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go +++ b/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go @@ -138,7 +138,12 @@ func WatchEvictionEventsWithRetries(ctx context.Context, kubeClient kube_client. // Wait between attempts, retrying too often breaks API server. waitTime := wait.Jitter(evictionWatchRetryWait, evictionWatchJitterFactor) klog.V(1).InfoS("An attempt to watch eviction events finished", "waitTime", waitTime) - time.Sleep(waitTime) + // Use a timer that can be interrupted by context cancellation + select { + case <-ctx.Done(): + return + case <-time.After(waitTime): + } } } }() diff --git a/vertical-pod-autoscaler/pkg/recommender/main.go b/vertical-pod-autoscaler/pkg/recommender/main.go index cda2d831da8c..aee9204b6405 100644 --- a/vertical-pod-autoscaler/pkg/recommender/main.go +++ b/vertical-pod-autoscaler/pkg/recommender/main.go @@ -18,365 +18,94 @@ package main import ( "context" - "flag" - "fmt" - "os" - "strings" - "time" "github.com/spf13/pflag" - apiv1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/uuid" - "k8s.io/client-go/informers" - kube_client "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/leaderelection" - "k8s.io/client-go/tools/leaderelection/resourcelock" kube_flag "k8s.io/component-base/cli/flag" - componentbaseconfig "k8s.io/component-base/config" componentbaseoptions "k8s.io/component-base/config/options" "k8s.io/klog/v2" - resourceclient "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1" "k8s.io/autoscaler/vertical-pod-autoscaler/common" - vpa_clientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned" "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/checkpoint" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/history" - input_metrics "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/metrics" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/logic" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/routines" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target" - controllerfetcher "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target/controller_fetcher" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics" - metrics_quality "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics/quality" - metrics_recommender "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics/recommender" - metrics_resources "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/metrics/resources" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/server" - vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/app" ) -var ( - recommenderName = flag.String("recommender-name", input.DefaultRecommenderName, "Set the recommender name. Recommender will generate recommendations for VPAs that configure the same recommender name. If the recommender name is left as default it will also generate recommendations that don't explicitly specify recommender. You shouldn't run two recommenders with the same name in a cluster.") - metricsFetcherInterval = flag.Duration("recommender-interval", 1*time.Minute, `How often metrics should be fetched`) - checkpointsGCInterval = flag.Duration("checkpoints-gc-interval", 10*time.Minute, `How often orphaned checkpoints should be garbage collected`) - address = flag.String("address", ":8942", "The address to expose Prometheus metrics.") - storage = flag.String("storage", "", `Specifies storage mode. Supported values: prometheus, checkpoint (default)`) - memorySaver = flag.Bool("memory-saver", false, `If true, only track pods which have an associated VPA`) - updateWorkerCount = flag.Int("update-worker-count", 10, "Number of concurrent workers to update VPA recommendations and checkpoints. When increasing this setting, make sure the client-side rate limits ('kube-api-qps' and 'kube-api-burst') are either increased or turned off as well. Determines the minimum number of VPA checkpoints written per recommender loop.") -) - -// Prometheus history provider flags -var ( - prometheusAddress = flag.String("prometheus-address", "http://prometheus.monitoring.svc", `Where to reach for Prometheus metrics`) - prometheusInsecure = flag.Bool("prometheus-insecure", false, `Skip tls verify if https is used in the prometheus-address`) - prometheusJobName = flag.String("prometheus-cadvisor-job-name", "kubernetes-cadvisor", `Name of the prometheus job name which scrapes the cAdvisor metrics`) - historyLength = flag.String("history-length", "8d", `How much time back prometheus have to be queried to get historical metrics`) - historyResolution = flag.String("history-resolution", "1h", `Resolution at which Prometheus is queried for historical metrics`) - queryTimeout = flag.String("prometheus-query-timeout", "5m", `How long to wait before killing long queries`) - podLabelPrefix = flag.String("pod-label-prefix", "pod_label_", `Which prefix to look for pod labels in metrics`) - podLabelsMetricName = flag.String("metric-for-pod-labels", "up{job=\"kubernetes-pods\"}", `Which metric to look for pod labels in metrics`) - podNamespaceLabel = flag.String("pod-namespace-label", "kubernetes_namespace", `Label name to look for pod namespaces`) - podNameLabel = flag.String("pod-name-label", "kubernetes_pod_name", `Label name to look for pod names`) - ctrNamespaceLabel = flag.String("container-namespace-label", "namespace", `Label name to look for container namespaces`) - ctrPodNameLabel = flag.String("container-pod-name-label", "pod_name", `Label name to look for container pod names`) - ctrNameLabel = flag.String("container-name-label", "name", `Label name to look for container names`) - username = flag.String("username", "", "The username used in the prometheus server basic auth. Can also be set via the PROMETHEUS_USERNAME environment variable") - password = flag.String("password", "", "The password used in the prometheus server basic auth. Can also be set via the PROMETHEUS_PASSWORD environment variable") - prometheusBearerToken = flag.String("prometheus-bearer-token", "", "The bearer token used in the Prometheus server bearer token auth") - prometheusBearerTokenFile = flag.String("prometheus-bearer-token-file", "", "Path to the bearer token file used for authentication by the Prometheus server") -) - -// External metrics provider flags -var ( - useExternalMetrics = flag.Bool("use-external-metrics", false, "ALPHA. Use an external metrics provider instead of metrics_server.") - externalCpuMetric = flag.String("external-metrics-cpu-metric", "", "ALPHA. Metric to use with external metrics provider for CPU usage.") - externalMemoryMetric = flag.String("external-metrics-memory-metric", "", "ALPHA. Metric to use with external metrics provider for memory usage.") -) - -// Aggregation configuration flags -var ( - memoryAggregationInterval = flag.Duration("memory-aggregation-interval", model.DefaultMemoryAggregationInterval, `The length of a single interval, for which the peak memory usage is computed. Memory usage peaks are aggregated in multiples of this interval. In other words there is one memory usage sample per interval (the maximum usage over that interval)`) - memoryAggregationIntervalCount = flag.Int64("memory-aggregation-interval-count", model.DefaultMemoryAggregationIntervalCount, `The number of consecutive memory-aggregation-intervals which make up the MemoryAggregationWindowLength which in turn is the period for memory usage aggregation by VPA. In other words, MemoryAggregationWindowLength = memory-aggregation-interval * memory-aggregation-interval-count.`) - memoryHistogramDecayHalfLife = flag.Duration("memory-histogram-decay-half-life", model.DefaultMemoryHistogramDecayHalfLife, `The amount of time it takes a historical memory usage sample to lose half of its weight. In other words, a fresh usage sample is twice as 'important' as one with age equal to the half life period.`) - cpuHistogramDecayHalfLife = flag.Duration("cpu-histogram-decay-half-life", model.DefaultCPUHistogramDecayHalfLife, `The amount of time it takes a historical CPU usage sample to lose half of its weight.`) - oomBumpUpRatio = flag.Float64("oom-bump-up-ratio", model.DefaultOOMBumpUpRatio, `Default memory bump up ratio when OOM occurs. This value applies to all VPAs unless overridden in the VPA spec. Default is 1.2.`) - oomMinBumpUp = flag.Float64("oom-min-bump-up-bytes", model.DefaultOOMMinBumpUp, `Default minimal increase of memory (in bytes) when OOM occurs. This value applies to all VPAs unless overridden in the VPA spec. Default is 100 * 1024 * 1024 (100Mi).`) -) +var config *app.RecommenderConfig -// Post processors flags -var ( +func main() { + config = app.DefaultRecommenderConfig() + config.CommonFlags = common.InitCommonFlags() + + fs := pflag.CommandLine + fs.StringVar(&config.RecommenderName, "recommender-name", config.RecommenderName, "Set the recommender name. Recommender will generate recommendations for VPAs that configure the same recommender name. If the recommender name is left as default it will also generate recommendations that don't explicitly specify recommender. You shouldn't run two recommenders with the same name in a cluster.") + fs.DurationVar(&config.MetricsFetcherInterval, "recommender-interval", config.MetricsFetcherInterval, `How often metrics should be fetched`) + fs.DurationVar(&config.CheckpointsGCInterval, "checkpoints-gc-interval", config.CheckpointsGCInterval, `How often orphaned checkpoints should be garbage collected`) + fs.StringVar(&config.Address, "address", ":8942", "The address to expose Prometheus metrics.") + fs.StringVar(&config.Storage, "storage", config.Storage, `Specifies storage mode. Supported values: prometheus, checkpoint (default)`) + fs.BoolVar(&config.MemorySaver, "memory-saver", false, `If true, only track pods which have an associated VPA`) + fs.IntVar(&config.UpdateWorkerCount, "update-worker-count", 10, "Number of concurrent workers to update VPA recommendations and checkpoints. When increasing this setting, make sure the client-side rate limits ('kube-api-qps' and 'kube-api-burst') are either increased or turned off as well. Determines the minimum number of VPA checkpoints written per recommender loop.") + + // Prometheus history provider flags + fs.StringVar(&config.PrometheusAddress, "prometheus-address", config.PrometheusAddress, `Where to reach for Prometheus metrics`) + fs.BoolVar(&config.PrometheusInsecure, "prometheus-insecure", config.PrometheusInsecure, `Skip tls verify if https is used in the prometheus-address`) + fs.StringVar(&config.PrometheusJobName, "prometheus-cadvisor-job-name", config.PrometheusJobName, `Name of the prometheus job name which scrapes the cAdvisor metrics`) + fs.StringVar(&config.HistoryLength, "history-length", config.HistoryLength, `How much time back prometheus have to be queried to get historical metrics`) + fs.StringVar(&config.HistoryResolution, "history-resolution", config.HistoryResolution, `Resolution at which Prometheus is queried for historical metrics`) + fs.StringVar(&config.QueryTimeout, "prometheus-query-timeout", config.QueryTimeout, `How long to wait before killing long queries`) + fs.StringVar(&config.PodLabelPrefix, "pod-label-prefix", config.PodLabelPrefix, `Which prefix to look for pod labels in metrics`) + fs.StringVar(&config.PodLabelsMetricName, "metric-for-pod-labels", config.PodLabelsMetricName, `Which metric to look for pod labels in metrics`) + fs.StringVar(&config.PodNamespaceLabel, "pod-namespace-label", config.PodNamespaceLabel, `Label name to look for pod namespaces`) + fs.StringVar(&config.PodNameLabel, "pod-name-label", config.PodNameLabel, `Label name to look for pod names`) + fs.StringVar(&config.CtrNamespaceLabel, "container-namespace-label", config.CtrNamespaceLabel, `Label name to look for container namespaces`) + fs.StringVar(&config.CtrPodNameLabel, "container-pod-name-label", config.CtrPodNameLabel, `Label name to look for container pod names`) + fs.StringVar(&config.CtrNameLabel, "container-name-label", config.CtrNameLabel, `Label name to look for container names`) + fs.StringVar(&config.Username, "username", config.Username, "The username used in the prometheus server basic auth. Can also be set via the PROMETHEUS_USERNAME environment variable") + fs.StringVar(&config.Password, "password", config.Password, "The password used in the prometheus server basic auth. Can also be set via the PROMETHEUS_PASSWORD environment variable") + fs.StringVar(&config.PrometheusBearerToken, "prometheus-bearer-token", config.PrometheusBearerToken, "The bearer token used in the Prometheus server bearer token auth") + fs.StringVar(&config.PrometheusBearerTokenFile, "prometheus-bearer-token-file", config.PrometheusBearerTokenFile, "Path to the bearer token file used for authentication by the Prometheus server") + + // External metrics provider flags + fs.BoolVar(&config.UseExternalMetrics, "use-external-metrics", config.UseExternalMetrics, "ALPHA. Use an external metrics provider instead of metrics_server.") + fs.StringVar(&config.ExternalCpuMetric, "external-metrics-cpu-metric", config.ExternalCpuMetric, "ALPHA. Metric to use with external metrics provider for CPU usage.") + fs.StringVar(&config.ExternalMemoryMetric, "external-metrics-memory-metric", config.ExternalMemoryMetric, "ALPHA. Metric to use with external metrics provider for memory usage.") + + // Aggregation configuration flags + fs.DurationVar(&config.MemoryAggregationInterval, "memory-aggregation-interval", config.MemoryAggregationInterval, `The length of a single interval, for which the peak memory usage is computed. Memory usage peaks are aggregated in multiples of this interval. In other words there is one memory usage sample per interval (the maximum usage over that interval)`) + fs.Int64Var(&config.MemoryAggregationIntervalCount, "memory-aggregation-interval-count", config.MemoryAggregationIntervalCount, `The number of consecutive memory-aggregation-intervals which make up the MemoryAggregationWindowLength which in turn is the period for memory usage aggregation by VPA. In other words, MemoryAggregationWindowLength = memory-aggregation-interval * memory-aggregation-interval-count.`) + fs.DurationVar(&config.MemoryHistogramDecayHalfLife, "memory-histogram-decay-half-life", config.MemoryHistogramDecayHalfLife, `The amount of time it takes a historical memory usage sample to lose half of its weight. In other words, a fresh usage sample is twice as 'important' as one with age equal to the half life period.`) + fs.DurationVar(&config.CpuHistogramDecayHalfLife, "cpu-histogram-decay-half-life", config.CpuHistogramDecayHalfLife, `The amount of time it takes a historical CPU usage sample to lose half of its weight.`) + fs.Float64Var(&config.OOMBumpUpRatio, "oom-bump-up-ratio", config.OOMBumpUpRatio, `Default memory bump up ratio when OOM occurs. This value applies to all VPAs unless overridden in the VPA spec. Default is 1.2.`) + fs.Float64Var(&config.OOMMinBumpUp, "oom-min-bump-up-bytes", config.OOMMinBumpUp, `Default minimal increase of memory (in bytes) when OOM occurs. This value applies to all VPAs unless overridden in the VPA spec. Default is 100 * 1024 * 1024 (100Mi).`) + + // Post processors flags // CPU as integer to benefit for CPU management Static Policy ( https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/#static-policy ) - postProcessorCPUasInteger = flag.Bool("cpu-integer-post-processor-enabled", false, "Enable the cpu-integer recommendation post processor. The post processor will round up CPU recommendations to a whole CPU for pods which were opted in by setting an appropriate label on VPA object (experimental)") - maxAllowedCPU = resource.QuantityValue{} - maxAllowedMemory = resource.QuantityValue{} -) - -const ( - // aggregateContainerStateGCInterval defines how often expired AggregateContainerStates are garbage collected. - aggregateContainerStateGCInterval = 1 * time.Hour - scaleCacheEntryLifetime time.Duration = time.Hour - scaleCacheEntryFreshnessTime time.Duration = 10 * time.Minute - scaleCacheEntryJitterFactor float64 = 1. - scaleCacheLoopPeriod = 7 * time.Second - defaultResyncPeriod time.Duration = 10 * time.Minute -) + fs.BoolVar(&config.PostProcessorCPUasInteger, "cpu-integer-post-processor-enabled", config.PostProcessorCPUasInteger, "Enable the cpu-integer recommendation post processor. The post processor will round up CPU recommendations to a whole CPU for pods which were opted in by setting an appropriate label on VPA object (experimental)") + fs.Var(&config.MaxAllowedCPU, "container-recommendation-max-allowed-cpu", "Maximum amount of CPU that will be recommended for a container. VerticalPodAutoscaler-level maximum allowed takes precedence over the global maximum allowed.") + fs.Var(&config.MaxAllowedMemory, "container-recommendation-max-allowed-memory", "Maximum amount of memory that will be recommended for a container. VerticalPodAutoscaler-level maximum allowed takes precedence over the global maximum allowed.") -func init() { - flag.Var(&maxAllowedCPU, "container-recommendation-max-allowed-cpu", "Maximum amount of CPU that will be recommended for a container. VerticalPodAutoscaler-level maximum allowed takes precedence over the global maximum allowed.") - flag.Var(&maxAllowedMemory, "container-recommendation-max-allowed-memory", "Maximum amount of memory that will be recommended for a container. VerticalPodAutoscaler-level maximum allowed takes precedence over the global maximum allowed.") -} - -func main() { - commonFlags := common.InitCommonFlags() klog.InitFlags(nil) common.InitLoggingFlags() - leaderElection := defaultLeaderElectionConfiguration() + leaderElection := app.DefaultLeaderElectionConfiguration() componentbaseoptions.BindLeaderElectionFlags(&leaderElection, pflag.CommandLine) features.MutableFeatureGate.AddFlag(pflag.CommandLine) kube_flag.InitFlags() - klog.V(1).InfoS("Vertical Pod Autoscaler Recommender", "version", common.VerticalPodAutoscalerVersion(), "recommenderName", *recommenderName) + klog.V(1).InfoS("Vertical Pod Autoscaler Recommender", "version", common.VerticalPodAutoscalerVersion(), "recommenderName", config.RecommenderName) - if len(commonFlags.VpaObjectNamespace) > 0 && len(commonFlags.IgnoredVpaObjectNamespaces) > 0 { - klog.ErrorS(nil, "--vpa-object-namespace and --ignored-vpa-object-namespaces are mutually exclusive and can't be set together.") - klog.FlushAndExit(klog.ExitFlushTimeout, 1) - } - - if *routines.MinCheckpointsPerRun != 10 { // Default value is 10 - klog.InfoS("DEPRECATION WARNING: The 'min-checkpoints' flag is deprecated and has no effect. It will be removed in a future release.") - } + common.ValidateCommonConfig(config.CommonFlags) + app.ValidateRecommenderConfig(config) - if *prometheusBearerToken != "" && *prometheusBearerTokenFile != "" && *username != "" { - klog.ErrorS(nil, "--bearer-token, --bearer-token-file and --username are mutually exclusive and can't be set together.") + recommenderApp, err := app.NewRecommenderApp(config) + if err != nil { + klog.ErrorS(err, "Failed to create recommender app") klog.FlushAndExit(klog.ExitFlushTimeout, 1) } - if *prometheusBearerTokenFile != "" { - fileContent, err := os.ReadFile(*prometheusBearerTokenFile) - if err != nil { - klog.ErrorS(err, "Unable to read bearer token file", "filename", *prometheusBearerTokenFile) - klog.FlushAndExit(klog.ExitFlushTimeout, 1) - } - *prometheusBearerToken = strings.TrimSpace(string(fileContent)) - } - ctx := context.Background() - - healthCheck := metrics.NewHealthCheck(*metricsFetcherInterval * 5) - metrics_recommender.Register() - metrics_quality.Register() - metrics_resources.Register() - server.Initialize(&commonFlags.EnableProfiling, healthCheck, address) - - if !leaderElection.LeaderElect { - run(ctx, healthCheck, commonFlags) - } else { - id, err := os.Hostname() - if err != nil { - klog.ErrorS(err, "Unable to get hostname") - klog.FlushAndExit(klog.ExitFlushTimeout, 1) - } - id = id + "_" + string(uuid.NewUUID()) - - config := common.CreateKubeConfigOrDie(commonFlags.KubeConfig, float32(commonFlags.KubeApiQps), int(commonFlags.KubeApiBurst)) - kubeClient := kube_client.NewForConfigOrDie(config) - - lock, err := resourcelock.New( - leaderElection.ResourceLock, - leaderElection.ResourceNamespace, - leaderElection.ResourceName, - kubeClient.CoreV1(), - kubeClient.CoordinationV1(), - resourcelock.ResourceLockConfig{ - Identity: id, - }, - ) - if err != nil { - klog.ErrorS(err, "Unable to create leader election lock") - klog.FlushAndExit(klog.ExitFlushTimeout, 1) - } - - leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ - Lock: lock, - LeaseDuration: leaderElection.LeaseDuration.Duration, - RenewDeadline: leaderElection.RenewDeadline.Duration, - RetryPeriod: leaderElection.RetryPeriod.Duration, - ReleaseOnCancel: true, - Callbacks: leaderelection.LeaderCallbacks{ - OnStartedLeading: func(_ context.Context) { - run(ctx, healthCheck, commonFlags) - }, - OnStoppedLeading: func() { - klog.Fatal("lost master") - }, - }, - }) - } -} - -const ( - defaultLeaseDuration = 15 * time.Second - defaultRenewDeadline = 10 * time.Second - defaultRetryPeriod = 2 * time.Second -) - -func defaultLeaderElectionConfiguration() componentbaseconfig.LeaderElectionConfiguration { - return componentbaseconfig.LeaderElectionConfiguration{ - LeaderElect: false, - LeaseDuration: metav1.Duration{Duration: defaultLeaseDuration}, - RenewDeadline: metav1.Duration{Duration: defaultRenewDeadline}, - RetryPeriod: metav1.Duration{Duration: defaultRetryPeriod}, - ResourceLock: resourcelock.LeasesResourceLock, - // This was changed from "vpa-recommender" to avoid conflicts with managed VPA deployments. - ResourceName: "vpa-recommender-lease", - ResourceNamespace: metav1.NamespaceSystem, - } -} - -func run(ctx context.Context, healthCheck *metrics.HealthCheck, commonFlag *common.CommonFlags) { - // Create a stop channel that will be used to signal shutdown - stopCh := make(chan struct{}) - defer close(stopCh) - config := common.CreateKubeConfigOrDie(commonFlag.KubeConfig, float32(commonFlag.KubeApiQps), int(commonFlag.KubeApiBurst)) - kubeClient := kube_client.NewForConfigOrDie(config) - clusterState := model.NewClusterState(aggregateContainerStateGCInterval) - factory := informers.NewSharedInformerFactoryWithOptions(kubeClient, defaultResyncPeriod, informers.WithNamespace(commonFlag.VpaObjectNamespace)) - controllerFetcher := controllerfetcher.NewControllerFetcher(config, kubeClient, factory, scaleCacheEntryFreshnessTime, scaleCacheEntryLifetime, scaleCacheEntryJitterFactor) - podLister, oomObserver := input.NewPodListerAndOOMObserver(ctx, kubeClient, commonFlag.VpaObjectNamespace, stopCh) - - factory.Start(stopCh) - informerMap := factory.WaitForCacheSync(stopCh) - for kind, synced := range informerMap { - if !synced { - klog.ErrorS(nil, fmt.Sprintf("Could not sync cache for the %s informer", kind.String())) - klog.FlushAndExit(klog.ExitFlushTimeout, 1) - } - } - - model.InitializeAggregationsConfig(model.NewAggregationsConfig(*memoryAggregationInterval, *memoryAggregationIntervalCount, *memoryHistogramDecayHalfLife, *cpuHistogramDecayHalfLife, *oomBumpUpRatio, *oomMinBumpUp)) - - useCheckpoints := *storage != "prometheus" - - var postProcessors []routines.RecommendationPostProcessor - if *postProcessorCPUasInteger { - postProcessors = append(postProcessors, &routines.IntegerCPUPostProcessor{}) - } - - globalMaxAllowed := initGlobalMaxAllowed() - // CappingPostProcessor, should always come in the last position for post-processing - postProcessors = append(postProcessors, routines.NewCappingRecommendationProcessor(globalMaxAllowed)) - var source input_metrics.PodMetricsLister - if *useExternalMetrics { - resourceMetrics := map[apiv1.ResourceName]string{} - if externalCpuMetric != nil && *externalCpuMetric != "" { - resourceMetrics[apiv1.ResourceCPU] = *externalCpuMetric - } - if externalMemoryMetric != nil && *externalMemoryMetric != "" { - resourceMetrics[apiv1.ResourceMemory] = *externalMemoryMetric - } - externalClientOptions := &input_metrics.ExternalClientOptions{ResourceMetrics: resourceMetrics, ContainerNameLabel: *ctrNameLabel} - klog.V(1).InfoS("Using External Metrics", "options", externalClientOptions) - source = input_metrics.NewExternalClient(config, clusterState, *externalClientOptions) - } else { - klog.V(1).InfoS("Using Metrics Server") - source = input_metrics.NewPodMetricsesSource(resourceclient.NewForConfigOrDie(config)) - } - - ignoredNamespaces := strings.Split(commonFlag.IgnoredVpaObjectNamespaces, ",") - - clusterStateFeeder := input.ClusterStateFeederFactory{ - PodLister: podLister, - OOMObserver: oomObserver, - KubeClient: kubeClient, - MetricsClient: input_metrics.NewMetricsClient(source, commonFlag.VpaObjectNamespace, "default-metrics-client"), - VpaCheckpointClient: vpa_clientset.NewForConfigOrDie(config).AutoscalingV1(), - VpaLister: vpa_api_util.NewVpasLister(vpa_clientset.NewForConfigOrDie(config), make(chan struct{}), commonFlag.VpaObjectNamespace), - VpaCheckpointLister: vpa_api_util.NewVpaCheckpointLister(vpa_clientset.NewForConfigOrDie(config), make(chan struct{}), commonFlag.VpaObjectNamespace), - ClusterState: clusterState, - SelectorFetcher: target.NewVpaTargetSelectorFetcher(config, kubeClient, factory), - MemorySaveMode: *memorySaver, - ControllerFetcher: controllerFetcher, - RecommenderName: *recommenderName, - IgnoredNamespaces: ignoredNamespaces, - VpaObjectNamespace: commonFlag.VpaObjectNamespace, - }.Make() - controllerFetcher.Start(ctx, scaleCacheLoopPeriod) - - recommender := routines.RecommenderFactory{ - ClusterState: clusterState, - ClusterStateFeeder: clusterStateFeeder, - ControllerFetcher: controllerFetcher, - CheckpointWriter: checkpoint.NewCheckpointWriter(clusterState, vpa_clientset.NewForConfigOrDie(config).AutoscalingV1()), - VpaClient: vpa_clientset.NewForConfigOrDie(config).AutoscalingV1(), - PodResourceRecommender: logic.CreatePodResourceRecommender(), - RecommendationPostProcessors: postProcessors, - CheckpointsGCInterval: *checkpointsGCInterval, - UseCheckpoints: useCheckpoints, - UpdateWorkerCount: *updateWorkerCount, - }.Make() - - promQueryTimeout, err := time.ParseDuration(*queryTimeout) - if err != nil { - klog.ErrorS(err, "Could not parse --prometheus-query-timeout as a time.Duration") + if err := recommenderApp.Run(ctx, leaderElection); err != nil { + klog.ErrorS(err, "Error running recommender") klog.FlushAndExit(klog.ExitFlushTimeout, 1) } - if useCheckpoints { - recommender.GetClusterStateFeeder().InitFromCheckpoints(ctx) - } else { - config := history.PrometheusHistoryProviderConfig{ - Address: *prometheusAddress, - Insecure: *prometheusInsecure, - QueryTimeout: promQueryTimeout, - HistoryLength: *historyLength, - HistoryResolution: *historyResolution, - PodLabelPrefix: *podLabelPrefix, - PodLabelsMetricName: *podLabelsMetricName, - PodNamespaceLabel: *podNamespaceLabel, - PodNameLabel: *podNameLabel, - CtrNamespaceLabel: *ctrNamespaceLabel, - CtrPodNameLabel: *ctrPodNameLabel, - CtrNameLabel: *ctrNameLabel, - CadvisorMetricsJobName: *prometheusJobName, - Namespace: commonFlag.VpaObjectNamespace, - Authentication: history.PrometheusCredentials{ - BearerToken: *prometheusBearerToken, - Username: *username, - Password: *password, - }, - } - provider, err := history.NewPrometheusHistoryProvider(config) - if err != nil { - klog.ErrorS(err, "Could not initialize history provider") - klog.FlushAndExit(klog.ExitFlushTimeout, 1) - } - recommender.GetClusterStateFeeder().InitFromHistoryProvider(provider) - } - - // Start updating health check endpoint. - healthCheck.StartMonitoring() - - ticker := time.Tick(*metricsFetcherInterval) - for range ticker { - recommender.RunOnce() - healthCheck.UpdateLastActivity() - } -} - -func initGlobalMaxAllowed() apiv1.ResourceList { - result := make(apiv1.ResourceList) - if !maxAllowedCPU.IsZero() { - result[apiv1.ResourceCPU] = maxAllowedCPU.Quantity - } - if !maxAllowedMemory.IsZero() { - result[apiv1.ResourceMemory] = maxAllowedMemory.Quantity - } - - return result } diff --git a/vertical-pod-autoscaler/pkg/target/controller_fetcher/controller_fetcher.go b/vertical-pod-autoscaler/pkg/target/controller_fetcher/controller_fetcher.go index 67a24582a92a..a00f712857ba 100644 --- a/vertical-pod-autoscaler/pkg/target/controller_fetcher/controller_fetcher.go +++ b/vertical-pod-autoscaler/pkg/target/controller_fetcher/controller_fetcher.go @@ -112,7 +112,7 @@ func (f *controllerFetcher) Start(ctx context.Context, loopPeriod time.Duration) } // NewControllerFetcher returns a new instance of controllerFetcher -func NewControllerFetcher(config *rest.Config, kubeClient kube_client.Interface, factory informers.SharedInformerFactory, betweenRefreshes, lifeTime time.Duration, jitterFactor float64) *controllerFetcher { +func NewControllerFetcher(ctx context.Context, config *rest.Config, kubeClient kube_client.Interface, factory informers.SharedInformerFactory, betweenRefreshes, lifeTime time.Duration, jitterFactor float64) *controllerFetcher { discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) if err != nil { klog.ErrorS(err, "Could not create discoveryClient") @@ -122,9 +122,9 @@ func NewControllerFetcher(config *rest.Config, kubeClient kube_client.Interface, restClient := kubeClient.CoreV1().RESTClient() cachedDiscoveryClient := cacheddiscovery.NewMemCacheClient(discoveryClient) mapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient) - go wait.Until(func() { + go wait.UntilWithContext(ctx, func(ctx context.Context) { mapper.Reset() - }, discoveryResetPeriod, make(chan struct{})) + }, discoveryResetPeriod) informersMap := map[wellKnownController]cache.SharedIndexInformer{ daemonSet: factory.Apps().V1().DaemonSets().Informer(), diff --git a/vertical-pod-autoscaler/pkg/target/fetcher.go b/vertical-pod-autoscaler/pkg/target/fetcher.go index ccf6c12cd52a..e8938a9d6a6a 100644 --- a/vertical-pod-autoscaler/pkg/target/fetcher.go +++ b/vertical-pod-autoscaler/pkg/target/fetcher.go @@ -67,7 +67,7 @@ const ( ) // NewVpaTargetSelectorFetcher returns new instance of VpaTargetSelectorFetcher -func NewVpaTargetSelectorFetcher(config *rest.Config, kubeClient kube_client.Interface, factory informers.SharedInformerFactory) VpaTargetSelectorFetcher { +func NewVpaTargetSelectorFetcher(ctx context.Context, config *rest.Config, kubeClient kube_client.Interface, factory informers.SharedInformerFactory) VpaTargetSelectorFetcher { discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) if err != nil { klog.ErrorS(err, "Could not create discoveryClient") @@ -77,9 +77,9 @@ func NewVpaTargetSelectorFetcher(config *rest.Config, kubeClient kube_client.Int restClient := kubeClient.CoreV1().RESTClient() cachedDiscoveryClient := cacheddiscovery.NewMemCacheClient(discoveryClient) mapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient) - go wait.Until(func() { + go wait.UntilWithContext(ctx, func(ctx context.Context) { mapper.Reset() - }, discoveryResetPeriod, make(chan struct{})) + }, discoveryResetPeriod) informersMap := map[wellKnownController]cache.SharedIndexInformer{ daemonSet: factory.Apps().V1().DaemonSets().Informer(), diff --git a/vertical-pod-autoscaler/pkg/updater/main.go b/vertical-pod-autoscaler/pkg/updater/main.go index 8394fd54b29c..8ace4e324a12 100644 --- a/vertical-pod-autoscaler/pkg/updater/main.go +++ b/vertical-pod-autoscaler/pkg/updater/main.go @@ -175,12 +175,13 @@ func defaultLeaderElectionConfiguration() componentbaseconfig.LeaderElectionConf func run(healthCheck *metrics.HealthCheck, commonFlag *common.CommonFlags) { stopCh := make(chan struct{}) defer close(stopCh) + ctx := context.Background() config := common.CreateKubeConfigOrDie(commonFlag.KubeConfig, float32(commonFlag.KubeApiQps), int(commonFlag.KubeApiBurst)) kubeClient := kube_client.NewForConfigOrDie(config) vpaClient := vpa_clientset.NewForConfigOrDie(config) factory := informers.NewSharedInformerFactory(kubeClient, defaultResyncPeriod) - targetSelectorFetcher := target.NewVpaTargetSelectorFetcher(config, kubeClient, factory) - controllerFetcher := controllerfetcher.NewControllerFetcher(config, kubeClient, factory, scaleCacheEntryFreshnessTime, scaleCacheEntryLifetime, scaleCacheEntryJitterFactor) + targetSelectorFetcher := target.NewVpaTargetSelectorFetcher(ctx, config, kubeClient, factory) + controllerFetcher := controllerfetcher.NewControllerFetcher(ctx, config, kubeClient, factory, scaleCacheEntryFreshnessTime, scaleCacheEntryLifetime, scaleCacheEntryJitterFactor) var limitRangeCalculator limitrange.LimitRangeCalculator limitRangeCalculator, err := limitrange.NewLimitsRangeCalculator(factory) if err != nil { diff --git a/vertical-pod-autoscaler/pkg/utils/metrics/quality/quality.go b/vertical-pod-autoscaler/pkg/utils/metrics/quality/quality.go index 11c3d18ce96e..2cc1e6c67931 100644 --- a/vertical-pod-autoscaler/pkg/utils/metrics/quality/quality.go +++ b/vertical-pod-autoscaler/pkg/utils/metrics/quality/quality.go @@ -120,15 +120,15 @@ var ( // Register initializes all VPA quality metrics func Register() { - prometheus.MustRegister(usageRecommendationRelativeDiff) - prometheus.MustRegister(usageMissingRecommendationCounter) - prometheus.MustRegister(cpuRecommendationOverUsageDiff) - prometheus.MustRegister(memoryRecommendationOverUsageDiff) - prometheus.MustRegister(cpuRecommendationLowerOrEqualUsageDiff) - prometheus.MustRegister(memoryRecommendationLowerOrEqualUsageDiff) - prometheus.MustRegister(cpuRecommendations) - prometheus.MustRegister(memoryRecommendations) - prometheus.MustRegister(relativeRecommendationChange) + _ = prometheus.Register(usageRecommendationRelativeDiff) + _ = prometheus.Register(usageMissingRecommendationCounter) + _ = prometheus.Register(cpuRecommendationOverUsageDiff) + _ = prometheus.Register(memoryRecommendationOverUsageDiff) + _ = prometheus.Register(cpuRecommendationLowerOrEqualUsageDiff) + _ = prometheus.Register(memoryRecommendationLowerOrEqualUsageDiff) + _ = prometheus.Register(cpuRecommendations) + _ = prometheus.Register(memoryRecommendations) + _ = prometheus.Register(relativeRecommendationChange) } // observeUsageRecommendationRelativeDiff records relative diff between usage and diff --git a/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender.go b/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender.go index 1a2d5aee45b4..e8f623511505 100644 --- a/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender.go +++ b/vertical-pod-autoscaler/pkg/utils/metrics/recommender/recommender.go @@ -115,7 +115,19 @@ type ObjectCounter struct { // Register initializes all metrics for VPA Recommender func Register() { - prometheus.MustRegister(vpaObjectCount, recommendationLatency, functionLatency, aggregateContainerStatesCount, metricServerResponses, prometheusClientRequestsCount, prometheusClientRequestsDuration) + collectors := []prometheus.Collector{ + vpaObjectCount, + recommendationLatency, + functionLatency, + aggregateContainerStatesCount, + metricServerResponses, + prometheusClientRequestsCount, + prometheusClientRequestsDuration, + } + for _, c := range collectors { + // Ignore AlreadyRegisteredError + _ = prometheus.Register(c) + } } // NewExecutionTimer provides a timer for Recommender's RunOnce execution diff --git a/vertical-pod-autoscaler/pkg/utils/metrics/resources/resources.go b/vertical-pod-autoscaler/pkg/utils/metrics/resources/resources.go index 8c2badf5f9ac..9ab03fdbd7cf 100644 --- a/vertical-pod-autoscaler/pkg/utils/metrics/resources/resources.go +++ b/vertical-pod-autoscaler/pkg/utils/metrics/resources/resources.go @@ -56,7 +56,7 @@ var ( // Register initializes all metrics for VPA resources func Register() { - prometheus.MustRegister(getResourcesCount) + _ = prometheus.Register(getResourcesCount) } // RecordGetResourcesCount records how many times VPA requested the resources ( diff --git a/vertical-pod-autoscaler/pkg/utils/server/server.go b/vertical-pod-autoscaler/pkg/utils/server/server.go index 20b98cb8b820..a88806e6242f 100644 --- a/vertical-pod-autoscaler/pkg/utils/server/server.go +++ b/vertical-pod-autoscaler/pkg/utils/server/server.go @@ -18,6 +18,7 @@ limitations under the License. package server import ( + "context" "net/http" "net/http/pprof" @@ -29,6 +30,12 @@ import ( // Initialize sets up Prometheus to expose metrics & (optionally) health-check and profiling on the given address func Initialize(enableProfiling *bool, healthCheck *metrics.HealthCheck, address *string) { + InitializeWithContext(context.Background(), enableProfiling, healthCheck, address) +} + +// InitializeWithContext sets up Prometheus to expose metrics & (optionally) health-check and profiling on the given address. +// The server will shut down gracefully when the context is canceled. +func InitializeWithContext(ctx context.Context, enableProfiling *bool, healthCheck *metrics.HealthCheck, address *string) { go func() { mux := http.NewServeMux() @@ -45,8 +52,23 @@ func Initialize(enableProfiling *bool, healthCheck *metrics.HealthCheck, address mux.HandleFunc("/debug/pprof/trace", pprof.Trace) } - err := http.ListenAndServe(*address, mux) - klog.ErrorS(err, "Failed to start metrics") - klog.FlushAndExit(klog.ExitFlushTimeout, 1) + server := &http.Server{ + Addr: *address, + Handler: mux, + } + + // Start server shutdown when context is canceled + go func() { + <-ctx.Done() + if err := server.Shutdown(context.Background()); err != nil { + klog.ErrorS(err, "Failed to shutdown metrics server") + } + }() + + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + klog.ErrorS(err, "Failed to start metrics") + klog.FlushAndExit(klog.ExitFlushTimeout, 1) + } }() } diff --git a/vertical-pod-autoscaler/pkg/utils/test/test_container.go b/vertical-pod-autoscaler/pkg/utils/test/test_container.go index a2336fdcb818..c49eb3c53597 100644 --- a/vertical-pod-autoscaler/pkg/utils/test/test_container.go +++ b/vertical-pod-autoscaler/pkg/utils/test/test_container.go @@ -23,6 +23,7 @@ import ( type containerBuilder struct { name string + image string cpuRequest *resource.Quantity memRequest *resource.Quantity cpuLimit *resource.Quantity @@ -40,6 +41,12 @@ func (cb *containerBuilder) WithName(name string) *containerBuilder { return &r } +func (cb *containerBuilder) WithImage(image string) *containerBuilder { + r := *cb + r.image = image + return &r +} + func (cb *containerBuilder) WithCPURequest(cpuRequest resource.Quantity) *containerBuilder { r := *cb r.cpuRequest = &cpuRequest @@ -66,7 +73,8 @@ func (cb *containerBuilder) WithMemLimit(memLimit resource.Quantity) *containerB func (cb *containerBuilder) Get() apiv1.Container { container := apiv1.Container{ - Name: cb.name, + Name: cb.name, + Image: cb.image, Resources: apiv1.ResourceRequirements{ Requests: apiv1.ResourceList{}, Limits: apiv1.ResourceList{},