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/integration/go.mod b/vertical-pod-autoscaler/integration/go.mod new file mode 100644 index 000000000000..421e4894ded1 --- /dev/null +++ b/vertical-pod-autoscaler/integration/go.mod @@ -0,0 +1,203 @@ +module k8s.io/autoscaler/vertical-pod-autoscaler/integration + +go 1.25.0 + +toolchain go1.25.3 + +exclude ( + k8s.io/client-go v1.4.0 + k8s.io/client-go v1.5.0 + k8s.io/client-go v1.5.1 + k8s.io/client-go v1.5.2 +) + +replace ( + k8s.io/api => k8s.io/api v0.35.0 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery => k8s.io/apimachinery v0.35.0 + k8s.io/apiserver => k8s.io/apiserver v0.35.0 + k8s.io/autoscaler => ../../ + k8s.io/autoscaler/vertical-pod-autoscaler => ../ + k8s.io/autoscaler/vertical-pod-autoscaler/e2e => ../e2e + k8s.io/cli-runtime => k8s.io/cli-runtime v0.35.0 + k8s.io/client-go => k8s.io/client-go v0.35.0 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.35.0 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.35.0 + k8s.io/code-generator => k8s.io/code-generator v0.35.0 + k8s.io/component-base => k8s.io/component-base v0.35.0 + k8s.io/component-helpers => k8s.io/component-helpers v0.35.0 + k8s.io/controller-manager => k8s.io/controller-manager v0.35.0 + k8s.io/cri-api => k8s.io/cri-api v0.35.0 + k8s.io/cri-client => k8s.io/cri-client v0.35.0 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.35.0 + k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.35.0 + k8s.io/endpointslice => k8s.io/endpointslice v0.35.0 + k8s.io/externaljwt => k8s.io/externaljwt v0.35.0 + k8s.io/kms => k8s.io/kms v0.35.0 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.35.0 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.35.0 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.35.0 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.35.0 + k8s.io/kubectl => k8s.io/kubectl v0.35.0 + k8s.io/kubelet => k8s.io/kubelet v0.35.0 + k8s.io/metrics => k8s.io/metrics v0.35.0 + k8s.io/mount-utils => k8s.io/mount-utils v0.35.0 + k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.35.0 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.35.0 + k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.35.0 + k8s.io/sample-controller => k8s.io/sample-controller v0.35.0 +) + +require ( + k8s.io/api v0.35.0 + k8s.io/apiextensions-apiserver v0.35.0 + k8s.io/apimachinery v0.35.0 + k8s.io/autoscaler/vertical-pod-autoscaler v1.5.1 + k8s.io/autoscaler/vertical-pod-autoscaler/e2e v0.0.0-00010101000000-000000000000 + k8s.io/client-go v0.35.0 + k8s.io/kubernetes v1.35.0 +) + +require ( + cel.dev/expr v0.25.1 // indirect + cyphar.com/go-pathrs v0.2.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-oidc v2.3.0+incompatible // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.26.1 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/opencontainers/selinux v1.13.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pquerna/cachecontrol v0.1.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.etcd.io/etcd/api/v3 v3.6.7 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect + go.etcd.io/etcd/client/v3 v3.6.7 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.39.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/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // 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/time v0.14.0 // indirect + golang.org/x/tools v0.40.0 // 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 + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiserver v0.35.0 // indirect + k8s.io/cloud-provider v0.35.0 // indirect + k8s.io/cluster-bootstrap v0.0.0 // indirect + k8s.io/component-base v0.35.0 // indirect + k8s.io/component-helpers v0.35.0 // indirect + k8s.io/controller-manager v0.35.0 // indirect + k8s.io/csi-translation-lib v0.35.0 // indirect + k8s.io/dynamic-resource-allocation v0.35.0 // indirect + k8s.io/endpointslice v0.0.0 // indirect + k8s.io/externaljwt v0.0.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kms v0.35.0 // indirect + k8s.io/kube-aggregator v0.0.0 // indirect + k8s.io/kube-controller-manager v0.0.0 // indirect + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect + k8s.io/kube-proxy v0.0.0 // indirect + k8s.io/kube-scheduler v0.35.0 // indirect + k8s.io/kubectl v0.35.0 // indirect + k8s.io/kubelet v0.35.0 // indirect + k8s.io/metrics v0.35.0 // indirect + k8s.io/mount-utils v0.35.0 // indirect + k8s.io/pod-security-admission v0.35.0 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/vertical-pod-autoscaler/integration/go.sum b/vertical-pod-autoscaler/integration/go.sum new file mode 100644 index 000000000000..38ee67289162 --- /dev/null +++ b/vertical-pod-autoscaler/integration/go.sum @@ -0,0 +1,428 @@ +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cyphar.com/go-pathrs v0.2.2 h1:y9w7hxbkr3zEL78Fjzeg4HEhs2xNy+fbwHiHGJJY2Xo= +cyphar.com/go-pathrs v0.2.2/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +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/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0= +github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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/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= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +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-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/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +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/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +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= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +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= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +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/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/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= +github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= +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= +github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc= +github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +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= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.7 h1:7BNJ2gQmc3DNM+9cRkv7KkGQDayElg8x3X+tFDYS+E0= +go.etcd.io/etcd/api/v3 v3.6.7/go.mod h1:xJ81TLj9hxrYYEDmXTeKURMeY3qEDN24hqe+q7KhbnI= +go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/AhBVs= +go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q= +go.etcd.io/etcd/client/v3 v3.6.7 h1:9WqA5RpIBtdMxAy1ukXLAdtg2pAxNqW5NUoO2wQrE6U= +go.etcd.io/etcd/client/v3 v3.6.7/go.mod h1:2XfROY56AXnUqGsvl+6k29wrwsSbEh1lAouQB1vHpeE= +go.etcd.io/etcd/pkg/v3 v3.6.5 h1:byxWB4AqIKI4SBmquZUG1WGtvMfMaorXFoCcFbVeoxM= +go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU= +go.etcd.io/etcd/server/v3 v3.6.5 h1:4RbUb1Bd4y1WkBHmuF+cZII83JNQMuNXzyjwigQ06y0= +go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0= +go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= +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/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70= +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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +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= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +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/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/cloud-provider v0.35.0 h1:syiBCQbKh2gho/S1BkIl006Dc44pV8eAtGZmv5NMe7M= +k8s.io/cloud-provider v0.35.0/go.mod h1:7grN+/Nt5Hf7tnSGPT3aErt4K7aQpygyCrGpbrQbzNc= +k8s.io/cluster-bootstrap v0.35.0 h1:VXnil8zw+FikqvytJYLB8wcvjxbUCyqMkiC//k426Y0= +k8s.io/cluster-bootstrap v0.35.0/go.mod h1:X6sjEjVUFSfFNIzJ6VAIuwwh2QiDtsVX1xZgcGX4gD8= +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/cri-api v0.35.0 h1:fxLSKyJHqbyCSUsg1rW4DRpmjSEM/elZ1GXzYTSLoDQ= +k8s.io/cri-api v0.35.0/go.mod h1:Cnt29u/tYl1Se1cBRL30uSZ/oJ5TaIp4sZm1xDLvcMc= +k8s.io/cri-client v0.35.0 h1:U1K4bteO93yioUS38804ybN+kWaon9zrzVtB37I3fCs= +k8s.io/cri-client v0.35.0/go.mod h1:XG5GkuuSpxvungsJVzW58NyWBoGSQhMMJmE5c66m9N8= +k8s.io/csi-translation-lib v0.35.0 h1:jdVC/9rv3lfHl5/MFQXqIVcEZEOXPbl4IPI8cczPdWw= +k8s.io/csi-translation-lib v0.35.0/go.mod h1:/6R70QdDxBCrMkrLhIBLP4mdtL35hEoJ5a/c2s1k9z8= +k8s.io/dynamic-resource-allocation v0.35.0 h1:St6dsCCylLg3HiFPcyHzFF8YQO6yziUDaVRLGdkrNH8= +k8s.io/dynamic-resource-allocation v0.35.0/go.mod h1:uaFga3VJtwyfpfZwpuJG7mlurWGQaaiGUa+QZmooz2U= +k8s.io/endpointslice v0.35.0 h1:zLcYAHUhAApGld1kHS4klp6HTRIkn3r4uxitHJiN7PU= +k8s.io/endpointslice v0.35.0/go.mod h1:mHF/Zw1jYWpLWb2MRBrcmZlYvYXQuCcEgK+5TMaQiUE= +k8s.io/externaljwt v0.35.0 h1:t2y/WYpxBB9/90DnLAB4cKmi7BdgaVCQZrwwASHjMsU= +k8s.io/externaljwt v0.35.0/go.mod h1:BbmVxkdvNrL2ukF4m/AN2D2FBD3vp2df497nfcVl2Nc= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kms v0.35.0 h1:/x87FED2kDSo66csKtcYCEHsxF/DBlNl7LfJ1fVQs1o= +k8s.io/kms v0.35.0/go.mod h1:VT+4ekZAdrZDMgShK37vvlyHUVhwI9t/9tvh0AyCWmQ= +k8s.io/kube-aggregator v0.35.0 h1:FBtbuRFA7Ohe2QKirFZcJf8rgimC8oSaNiCi4pdU5xw= +k8s.io/kube-aggregator v0.35.0/go.mod h1:vKBRpQUfDryb7udwUwF3eCSvv3AJNgHtL4PGl6PqAg8= +k8s.io/kube-controller-manager v0.35.0 h1:NRMhS5mz6E/D2beojOj2/HcCa2thRPVf4vp4QxmFkjo= +k8s.io/kube-controller-manager v0.35.0/go.mod h1:M32fFkRScIW9KSPYUQBVqdjYrja5F2s4v5NrHEUVIJ8= +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/kube-proxy v0.35.0 h1:erv2wYmGZ6nyu/FtmaIb+ORD3q2rfZ4Fhn7VXs/8cPQ= +k8s.io/kube-proxy v0.35.0/go.mod h1:bd9lpN3uLLOOWc/CFZbkPEi9DTkzQQymbE8FqSU4bWk= +k8s.io/kube-scheduler v0.35.0 h1:1uzns+SQl1ols3RXH/oxhOPd4fuicXliqWTBLbqIVRM= +k8s.io/kube-scheduler v0.35.0/go.mod h1:/56k23VdXC19Pa7Mx6uQ2YW0gsw5VJ30RgGGZUeeyD8= +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/mount-utils v0.35.0 h1:UDE8RDeqmQh1u/yRd+GZC2EpDibiyAfmMEsm43lKNQI= +k8s.io/mount-utils v0.35.0/go.mod h1:ppC4d+mUpfbAJr/V2E8vvxeCEckNM+S5b0kQBQjd3Pw= +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/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= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/vertical-pod-autoscaler/integration/main_test.go b/vertical-pod-autoscaler/integration/main_test.go new file mode 100644 index 000000000000..4ac382b8eb80 --- /dev/null +++ b/vertical-pod-autoscaler/integration/main_test.go @@ -0,0 +1,29 @@ +/* +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 ( + "testing" + + "k8s.io/kubernetes/test/integration/framework" +) + +// TestMain sets up the integration test environment with etcd. +// This is required for tests that use kubeapiservertesting.StartTestServerOrDie. +func TestMain(m *testing.M) { + framework.EtcdMain(m.Run) +} diff --git a/vertical-pod-autoscaler/integration/recommender_test.go b/vertical-pod-autoscaler/integration/recommender_test.go new file mode 100644 index 000000000000..22766c7a0a58 --- /dev/null +++ b/vertical-pod-autoscaler/integration/recommender_test.go @@ -0,0 +1,373 @@ +/* +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/kubernetes/test/integration/framework" + + "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() + + server, kubeClient, vpaClient := createTestServerAndInstallCRDsWithClients(t) + defer server.TearDownFn() + + kubeconfig, cleanup := setupKubeconfig(t, server.ClientConfig) + defer cleanup() + + // Create test namespaces + watchedNS := "watched-ns" + ignoredNS := "ignored-ns" + + for _, ns := range []string{watchedNS, ignoredNS} { + ns := framework.CreateNamespaceOrDie(kubeClient, ns, t) + defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) + } + + // 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() + + server, kubeClient, vpaClient := createTestServerAndInstallCRDsWithClients(t) + defer server.TearDownFn() + + kubeconfig, cleanup := setupKubeconfig(t, server.ClientConfig) + defer cleanup() + + // Create test namespaces + watchedNS := "watched-ns" + ignoredNS := "ignored-ns" + + for _, ns := range []string{watchedNS, ignoredNS} { + ns := framework.CreateNamespaceOrDie(kubeClient, ns, t) + defer framework.DeleteNamespaceOrDie(kubeClient, ns, t) + } + + // 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() + + server, kubeClient, vpaClient := createTestServerAndInstallCRDsWithClients(t) + defer server.TearDownFn() + + kubeconfig, cleanup := setupKubeconfig(t, server.ClientConfig) + defer cleanup() + + ns := "default" + + // 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() + + server, _, vpaClient := createTestServerAndInstallCRDsWithClients(t) + defer server.TearDownFn() + + kubeconfig, cleanup := setupKubeconfig(t, server.ClientConfig) + defer cleanup() + + // 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("default"). + WithRecommender("custom-recommender"). + WithTargetRef(utils.HamsterTargetRef). + Get() + + vpaDefault := test.VerticalPodAutoscaler(). + WithName("vpa-for-default-recommender"). + WithContainer("hamster"). + WithNamespace("default"). + WithTargetRef(utils.HamsterTargetRef). // No WithRecommender = uses default recommender + Get() + + _, err := vpaClient.AutoscalingV1().VerticalPodAutoscalers("default").Create(ctx, vpaCustom, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("Failed to create VPA: %v", err) + } + + _, err = vpaClient.AutoscalingV1().VerticalPodAutoscalers("default").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("default").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("default").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..eb108592e778 --- /dev/null +++ b/vertical-pod-autoscaler/integration/utils.go @@ -0,0 +1,243 @@ +/* +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 ( + "bufio" + "context" + "io" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + appsv1 "k8s.io/api/apps/v1" + apiv1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/util/yaml" + clientset "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/test/integration/framework" + + vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + vpa_clientset "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned" + "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/app" +) + +// installVPACRDs installs the VPA CustomResourceDefinitions from the YAML file +func installVPACRDs(ctx context.Context, client apiextensionsclientset.Interface) 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") + + file, err := os.Open(crdPath) + if err != nil { + return err + } + defer file.Close() + + var crdNames []string + reader := yaml.NewYAMLReader(bufio.NewReader(file)) + for { + data, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + return err + } + + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := yaml.Unmarshal(data, crd); err != nil { + return err + } + + // Skip empty documents + if crd.Name == "" { + continue + } + + _, err = client.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{}) + if err != nil { + return err + } + crdNames = append(crdNames, crd.Name) + } + + // Wait for all CRDs to be established + for _, name := range crdNames { + if err := waitForCRDEstablished(ctx, client, name); err != nil { + return err + } + } + + return nil +} + +// waitForCRDEstablished waits for a CRD to be established and ready to accept resources +func waitForCRDEstablished(ctx context.Context, client apiextensionsclientset.Interface, name string) error { + return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 30*time.Second, true, func(ctx context.Context) (bool, error) { + crd, err := client.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, err + } + for _, cond := range crd.Status.Conditions { + if cond.Type == apiextensionsv1.Established && cond.Status == apiextensionsv1.ConditionTrue { + return true, nil + } + } + return false, nil + }) +} + +// sigs.k8s.io/controller-runtime/pkg/envtest +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() +} + +func makeVPACP(ns string) *vpa_types.VerticalPodAutoscalerCheckpoint { + return &vpa_types.VerticalPodAutoscalerCheckpoint{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-vpa", + Namespace: ns, + }, + Spec: vpa_types.VerticalPodAutoscalerCheckpointSpec{ + VPAObjectName: "test-vpa", + ContainerName: "A", + }, + } +} + +func createTestServerAndInstallCRDsWithClients(t *testing.T) (*kubeapiservertesting.TestServer, *clientset.Clientset, *vpa_clientset.Clientset) { + etcdOptions := framework.DefaultEtcdOptions() + server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), &etcdOptions.StorageConfig) + + apiextensionsClient := apiextensionsclientset.NewForConfigOrDie(server.ClientConfig) + + if err := installVPACRDs(context.Background(), apiextensionsClient); err != nil { + t.Fatalf("Failed to install VPA CRD: %v", err) + } + + kubeClient := clientset.NewForConfigOrDie(server.ClientConfig) + + vpaClientConfig := rest.CopyConfig(server.ClientConfig) + vpaClientConfig.ContentType = "application/json" + vpaClient := vpa_clientset.NewForConfigOrDie(vpaClientConfig) + + return server, kubeClient, vpaClient +} + +func setupKubeconfig(t *testing.T, restConfig *rest.Config) (kubeconfigPath string, cleanup func()) { + t.Helper() + kubeconfig := createKubeconfigFileForRestConfig(restConfig) + t.Logf("Using kubeconfig file: %s", kubeconfig) + return kubeconfig, func() { + os.Remove(kubeconfig) + } +} + +// 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{},