diff --git a/vertical-pod-autoscaler/README.md b/vertical-pod-autoscaler/README.md index 23d6d4437ccd..efa7e4c23597 100644 --- a/vertical-pod-autoscaler/README.md +++ b/vertical-pod-autoscaler/README.md @@ -23,6 +23,7 @@ - [Starting multiple recommenders](#starting-multiple-recommenders) - [Using CPU management with static policy](#using-cpu-management-with-static-policy) - [Controlling eviction behavior based on scaling direction and resource](#controlling-eviction-behavior-based-on-scaling-direction-and-resource) + - [Limiting which namespaces are used](#limiting-which-namespaces-are-used) - [Known limitations](#known-limitations) - [Related links](#related-links) @@ -376,6 +377,16 @@ vpa-post-processor.kubernetes.io/{containerName}_integerCPU=true ``` Note that this doesn't prevent scaling down entirely, as Pods may get recreated for different reasons, resulting in a new recommendation being applied. See [the original AEP](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler/enhancements/4831-control-eviction-behavior) for more context and usage information. + ### Limiting which namespaces are used + + By default the VPA will run against all namespaces. You can limit that behaviour by setting the following options: + +1. `ignored-vpa-object-namespaces` - A comma separated list of namespaces to ignore +1. `vpa-object-namespace` - A single namespace to monitor + +These options cannot be used together and are mutually exclusive. + + # Known limitations * Whenever VPA updates the pod resources, the pod is recreated, which causes all diff --git a/vertical-pod-autoscaler/pkg/admission-controller/config.go b/vertical-pod-autoscaler/pkg/admission-controller/config.go index 2ec852e180d4..06b5ad879038 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/config.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/config.go @@ -90,8 +90,8 @@ func configTLS(cfg certsConfig, minTlsVersion, ciphers string, stop <-chan struc // register this webhook admission controller with the kube-apiserver // by creating MutatingWebhookConfiguration. -func selfRegistration(clientset kubernetes.Interface, caCert []byte, namespace, serviceName, url string, registerByURL bool, timeoutSeconds int32) { - time.Sleep(10 * time.Second) +func selfRegistration(clientset kubernetes.Interface, caCert []byte, webHookDelay time.Duration, namespace, serviceName, url string, registerByURL bool, timeoutSeconds int32, selectedNamespace string, ignoredNamespaces []string) { + time.Sleep(webHookDelay) client := clientset.AdmissionregistrationV1().MutatingWebhookConfigurations() _, err := client.Get(context.TODO(), webhookConfigName, metav1.GetOptions{}) if err == nil { @@ -111,6 +111,29 @@ func selfRegistration(clientset kubernetes.Interface, caCert []byte, namespace, sideEffects := admissionregistration.SideEffectClassNone failurePolicy := admissionregistration.Ignore RegisterClientConfig.CABundle = caCert + + var namespaceSelector metav1.LabelSelector + if len(ignoredNamespaces) > 0 { + namespaceSelector = metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "kubernetes.io/metadata.name", + Operator: metav1.LabelSelectorOpNotIn, + Values: ignoredNamespaces, + }, + }, + } + } else if len(selectedNamespace) > 0 { + namespaceSelector = metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "kubernetes.io/metadata.name", + Operator: metav1.LabelSelectorOpIn, + Values: []string{selectedNamespace}, + }, + }, + } + } webhookConfig := &admissionregistration.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: webhookConfigName, @@ -137,10 +160,11 @@ func selfRegistration(clientset kubernetes.Interface, caCert []byte, namespace, }, }, }, - FailurePolicy: &failurePolicy, - ClientConfig: RegisterClientConfig, - SideEffects: &sideEffects, - TimeoutSeconds: &timeoutSeconds, + FailurePolicy: &failurePolicy, + ClientConfig: RegisterClientConfig, + SideEffects: &sideEffects, + TimeoutSeconds: &timeoutSeconds, + NamespaceSelector: &namespaceSelector, }, }, } diff --git a/vertical-pod-autoscaler/pkg/admission-controller/config_test.go b/vertical-pod-autoscaler/pkg/admission-controller/config_test.go index 032987657a18..8f91eadad92d 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/config_test.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/config_test.go @@ -19,6 +19,7 @@ package main import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" admissionregistration "k8s.io/api/admissionregistration/v1" @@ -30,13 +31,16 @@ func TestSelfRegistrationBase(t *testing.T) { testClientSet := fake.NewSimpleClientset() caCert := []byte("fake") + webHookDelay := 0 * time.Second namespace := "default" serviceName := "vpa-service" url := "http://example.com/" registerByURL := true timeoutSeconds := int32(32) + selectedNamespace := "" + ignoredNamespaces := []string{} - selfRegistration(testClientSet, caCert, namespace, serviceName, url, registerByURL, timeoutSeconds) + selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces) webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations() webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{}) @@ -70,13 +74,16 @@ func TestSelfRegistrationWithURL(t *testing.T) { testClientSet := fake.NewSimpleClientset() caCert := []byte("fake") + webHookDelay := 0 * time.Second namespace := "default" serviceName := "vpa-service" url := "http://example.com/" registerByURL := true timeoutSeconds := int32(32) + selectedNamespace := "" + ignoredNamespaces := []string{} - selfRegistration(testClientSet, caCert, namespace, serviceName, url, registerByURL, timeoutSeconds) + selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces) webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations() webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{}) @@ -95,13 +102,16 @@ func TestSelfRegistrationWithOutURL(t *testing.T) { testClientSet := fake.NewSimpleClientset() caCert := []byte("fake") + webHookDelay := 0 * time.Second namespace := "default" serviceName := "vpa-service" url := "http://example.com/" registerByURL := false timeoutSeconds := int32(32) + selectedNamespace := "" + ignoredNamespaces := []string{} - selfRegistration(testClientSet, caCert, namespace, serviceName, url, registerByURL, timeoutSeconds) + selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces) webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations() webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{}) @@ -117,3 +127,66 @@ func TestSelfRegistrationWithOutURL(t *testing.T) { assert.Nil(t, webhook.ClientConfig.URL, "expected URL to be set") } + +func TestSelfRegistrationWithIgnoredNamespaces(t *testing.T) { + + testClientSet := fake.NewSimpleClientset() + caCert := []byte("fake") + webHookDelay := 0 * time.Second + namespace := "default" + serviceName := "vpa-service" + url := "http://example.com/" + registerByURL := false + timeoutSeconds := int32(32) + selectedNamespace := "" + ignoredNamespaces := []string{"test"} + + selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces) + + webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations() + webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{}) + + assert.NoError(t, err, "expected no error fetching webhook configuration") + + assert.Len(t, webhookConfig.Webhooks, 1, "expected one webhook configuration") + webhook := webhookConfig.Webhooks[0] + + assert.NotNil(t, webhook.NamespaceSelector.MatchExpressions, "expected namespace selector not to be nil") + assert.Len(t, webhook.NamespaceSelector.MatchExpressions, 1, "expected one match expression") + + matchExpression := webhook.NamespaceSelector.MatchExpressions[0] + assert.Equal(t, matchExpression.Operator, metav1.LabelSelectorOpNotIn, "expected namespace operator to be OpNotIn") + assert.Equal(t, matchExpression.Values, ignoredNamespaces, "expected namespace selector match expression to be equal") +} + +func TestSelfRegistrationWithSelectedNamespaces(t *testing.T) { + + testClientSet := fake.NewSimpleClientset() + caCert := []byte("fake") + webHookDelay := 0 * time.Second + namespace := "default" + serviceName := "vpa-service" + url := "http://example.com/" + registerByURL := false + timeoutSeconds := int32(32) + selectedNamespace := "test" + ignoredNamespaces := []string{} + + selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces) + + webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations() + webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{}) + + assert.NoError(t, err, "expected no error fetching webhook configuration") + + assert.Len(t, webhookConfig.Webhooks, 1, "expected one webhook configuration") + webhook := webhookConfig.Webhooks[0] + + assert.NotNil(t, webhook.NamespaceSelector.MatchExpressions, "expected namespace selector not to be nil") + assert.Len(t, webhook.NamespaceSelector.MatchExpressions, 1, "expected one match expression") + + matchExpression := webhook.NamespaceSelector.MatchExpressions[0] + assert.Equal(t, metav1.LabelSelectorOpIn, matchExpression.Operator, "expected namespace operator to be OpIn") + assert.Equal(t, matchExpression.Operator, metav1.LabelSelectorOpIn, "expected namespace operator to be OpIn") + assert.Equal(t, matchExpression.Values, []string{selectedNamespace}, "expected namespace selector match expression to be equal") +} diff --git a/vertical-pod-autoscaler/pkg/admission-controller/main.go b/vertical-pod-autoscaler/pkg/admission-controller/main.go index 16a3500e58ef..3c925a1c6958 100644 --- a/vertical-pod-autoscaler/pkg/admission-controller/main.go +++ b/vertical-pod-autoscaler/pkg/admission-controller/main.go @@ -21,6 +21,7 @@ import ( "fmt" "net/http" "os" + "strings" "time" apiv1 "k8s.io/api/core/v1" @@ -51,6 +52,7 @@ const ( scaleCacheEntryLifetime time.Duration = time.Hour scaleCacheEntryFreshnessTime time.Duration = 10 * time.Minute scaleCacheEntryJitterFactor float64 = 1. + webHookDelay = 10 * time.Second ) var ( @@ -63,19 +65,20 @@ var ( ciphers = flag.String("tls-ciphers", "", "A comma-separated or colon-separated list of ciphers to accept. Only works when min-tls-version is set to tls1_2.") minTlsVersion = flag.String("min-tls-version", "tls1_2", "The minimum TLS version to accept. Must be set to either tls1_2 (default) or tls1_3.") - port = flag.Int("port", 8000, "The port to listen on.") - address = flag.String("address", ":8944", "The address to expose Prometheus metrics.") - kubeconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") - kubeApiQps = flag.Float64("kube-api-qps", 5.0, `QPS limit when making requests to Kubernetes apiserver`) - kubeApiBurst = flag.Float64("kube-api-burst", 10.0, `QPS burst limit when making requests to Kubernetes apiserver`) - namespace = os.Getenv("NAMESPACE") - serviceName = flag.String("webhook-service", "vpa-webhook", "Kubernetes service under which webhook is registered. Used when registerByURL is set to false.") - webhookAddress = flag.String("webhook-address", "", "Address under which webhook is registered. Used when registerByURL is set to true.") - webhookPort = flag.String("webhook-port", "", "Server Port for Webhook") - webhookTimeout = flag.Int("webhook-timeout-seconds", 30, "Timeout in seconds that the API server should wait for this webhook to respond before failing.") - registerWebhook = flag.Bool("register-webhook", true, "If set to true, admission webhook object will be created on start up to register with the API server.") - registerByURL = flag.Bool("register-by-url", false, "If set to true, admission webhook will be registered by URL (webhookAddress:webhookPort) instead of by service name") - vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects. Empty means all namespaces will be used.") + port = flag.Int("port", 8000, "The port to listen on.") + address = flag.String("address", ":8944", "The address to expose Prometheus metrics.") + kubeconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") + kubeApiQps = flag.Float64("kube-api-qps", 5.0, `QPS limit when making requests to Kubernetes apiserver`) + kubeApiBurst = flag.Float64("kube-api-burst", 10.0, `QPS burst limit when making requests to Kubernetes apiserver`) + namespace = os.Getenv("NAMESPACE") + serviceName = flag.String("webhook-service", "vpa-webhook", "Kubernetes service under which webhook is registered. Used when registerByURL is set to false.") + webhookAddress = flag.String("webhook-address", "", "Address under which webhook is registered. Used when registerByURL is set to true.") + webhookPort = flag.String("webhook-port", "", "Server Port for Webhook") + webhookTimeout = flag.Int("webhook-timeout-seconds", 30, "Timeout in seconds that the API server should wait for this webhook to respond before failing.") + registerWebhook = flag.Bool("register-webhook", true, "If set to true, admission webhook object will be created on start up to register with the API server.") + registerByURL = flag.Bool("register-by-url", false, "If set to true, admission webhook will be registered by URL (webhookAddress:webhookPort) instead of by service name") + vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects. Empty means all namespaces will be used. Must not be used if ignored-vpa-object-namespaces is set.") + ignoredVpaObjectNamespaces = flag.String("ignored-vpa-object-namespaces", "", "Comma separated list of namespaces to ignore. Must not be used if vpa-object-namespace is used.") ) func main() { @@ -83,6 +86,10 @@ func main() { kube_flag.InitFlags() klog.V(1).Infof("Vertical Pod Autoscaler %s Admission Controller", common.VerticalPodAutoscalerVersion) + if len(*vpaObjectNamespace) > 0 && len(*ignoredVpaObjectNamespaces) > 0 { + klog.Fatalf("--vpa-object-namespace and --ignored-vpa-object-namespaces are mutually exclusive and can't be set together.") + } + healthCheck := metrics.NewHealthCheck(time.Minute) metrics.Initialize(*address, healthCheck) metrics_admission.Register() @@ -136,9 +143,10 @@ func main() { TLSConfig: configTLS(*certsConfiguration, *minTlsVersion, *ciphers, stopCh), } url := fmt.Sprintf("%v:%v", *webhookAddress, *webhookPort) + ignoredNamespaces := strings.Split(*ignoredVpaObjectNamespaces, ",") go func() { if *registerWebhook { - selfRegistration(kubeClient, readFile(*certsConfiguration.clientCaFile), namespace, *serviceName, url, *registerByURL, int32(*webhookTimeout)) + selfRegistration(kubeClient, readFile(*certsConfiguration.clientCaFile), webHookDelay, namespace, *serviceName, url, *registerByURL, int32(*webhookTimeout), *vpaObjectNamespace, ignoredNamespaces) } // Start status updates after the webhook is initialized. statusUpdater.Run(stopCh) diff --git a/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go b/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go index 9dd5da9df3b5..cf1fff4b2dcb 100644 --- a/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go +++ b/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go @@ -19,6 +19,7 @@ package input import ( "context" "fmt" + "slices" "time" apiv1 "k8s.io/api/core/v1" @@ -87,6 +88,7 @@ type ClusterStateFeederFactory struct { MemorySaveMode bool ControllerFetcher controllerfetcher.ControllerFetcher RecommenderName string + IgnoredNamespaces []string } // Make creates new ClusterStateFeeder with internal data providers, based on kube client. @@ -103,6 +105,7 @@ func (m ClusterStateFeederFactory) Make() *clusterStateFeeder { memorySaveMode: m.MemorySaveMode, controllerFetcher: m.ControllerFetcher, recommenderName: m.RecommenderName, + ignoredNamespaces: m.IgnoredNamespaces, } } @@ -192,6 +195,7 @@ type clusterStateFeeder struct { memorySaveMode bool controllerFetcher controllerfetcher.ControllerFetcher recommenderName string + ignoredNamespaces []string } func (feeder *clusterStateFeeder) InitFromHistoryProvider(historyProvider history.HistoryProvider) { @@ -332,6 +336,12 @@ func filterVPAs(feeder *clusterStateFeeder, allVpaCRDs []*vpa_types.VerticalPodA continue } } + + if slices.Contains(feeder.ignoredNamespaces, vpaCRD.ObjectMeta.Namespace) { + klog.V(6).Infof("Ignoring vpaCRD %s in namespace %s as namespace is ignored", vpaCRD.Name, vpaCRD.Namespace) + continue + } + vpaCRDs = append(vpaCRDs, vpaCRD) } return vpaCRDs diff --git a/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder_test.go b/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder_test.go index e2d2a81d17a5..04220adbd0f6 100644 --- a/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder_test.go +++ b/vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder_test.go @@ -32,7 +32,7 @@ import ( "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/history" "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/input/spec" "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model" - "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target/controller_fetcher" + controllerfetcher "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target/controller_fetcher" target_mock "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/target/mock" "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/test" ) @@ -568,3 +568,66 @@ func TestFilterVPAs(t *testing.T) { assert.ElementsMatch(t, expectedResult, result) } + +func TestFilterVPAsIgnoreNamespaces(t *testing.T) { + + vpa1 := &vpa_types.VerticalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace1", + }, + Spec: vpa_types.VerticalPodAutoscalerSpec{ + Recommenders: []*vpa_types.VerticalPodAutoscalerRecommenderSelector{ + {Name: DefaultRecommenderName}, + }, + }, + } + vpa2 := &vpa_types.VerticalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "namespace2", + }, + Spec: vpa_types.VerticalPodAutoscalerSpec{ + Recommenders: []*vpa_types.VerticalPodAutoscalerRecommenderSelector{ + {Name: DefaultRecommenderName}, + }, + }, + } + vpa3 := &vpa_types.VerticalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ignore1", + }, + Spec: vpa_types.VerticalPodAutoscalerSpec{ + Recommenders: []*vpa_types.VerticalPodAutoscalerRecommenderSelector{ + {Name: DefaultRecommenderName}, + }, + }, + } + vpa4 := &vpa_types.VerticalPodAutoscaler{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ignore2", + }, + Spec: vpa_types.VerticalPodAutoscalerSpec{ + Recommenders: []*vpa_types.VerticalPodAutoscalerRecommenderSelector{ + {Name: DefaultRecommenderName}, + }, + }, + } + + allVpaCRDs := []*vpa_types.VerticalPodAutoscaler{vpa1, vpa2, vpa3, vpa4} + + feeder := &clusterStateFeeder{ + recommenderName: DefaultRecommenderName, + ignoredNamespaces: []string{"ignore1", "ignore2"}, + } + + // Set expected results + expectedResult := []*vpa_types.VerticalPodAutoscaler{vpa1, vpa2} + + // Run the filterVPAs function + result := filterVPAs(feeder, allVpaCRDs) + + if len(result) != len(expectedResult) { + t.Fatalf("expected %d VPAs, got %d", len(expectedResult), len(result)) + } + + assert.ElementsMatch(t, expectedResult, result) +} diff --git a/vertical-pod-autoscaler/pkg/recommender/main.go b/vertical-pod-autoscaler/pkg/recommender/main.go index f87831b007ba..7a73d5816d79 100644 --- a/vertical-pod-autoscaler/pkg/recommender/main.go +++ b/vertical-pod-autoscaler/pkg/recommender/main.go @@ -20,6 +20,7 @@ import ( "context" "flag" "os" + "strings" "time" "github.com/spf13/pflag" @@ -66,20 +67,21 @@ var ( storage = flag.String("storage", "", `Specifies storage mode. Supported values: prometheus, checkpoint (default)`) // prometheus history provider configs - 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`) - vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects and pod stats. Empty means all namespaces will be used.") - username = flag.String("username", "", "The username used in the prometheus server basic auth") - password = flag.String("password", "", "The password used in the prometheus server basic auth") - memorySaver = flag.Bool("memory-saver", false, `If true, only track pods which have an associated VPA`) + 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`) + vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects and pod stats. Empty means all namespaces will be used. Must not be used if ignored-vpa-object-namespaces is set.") + ignoredVpaObjectNamespaces = flag.String("ignored-vpa-object-namespaces", "", "Comma separated list of namespaces to ignore. Must not be used if vpa-object-namespace is used.") + username = flag.String("username", "", "The username used in the prometheus server basic auth") + password = flag.String("password", "", "The password used in the prometheus server basic auth") + memorySaver = flag.Bool("memory-saver", false, `If true, only track pods which have an associated VPA`) // external metrics provider config 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.") @@ -121,6 +123,10 @@ func main() { kube_flag.InitFlags() klog.V(1).Infof("Vertical Pod Autoscaler %s Recommender: %v", common.VerticalPodAutoscalerVersion, *recommenderName) + if len(*vpaObjectNamespace) > 0 && len(*ignoredVpaObjectNamespaces) > 0 { + klog.Fatalf("--vpa-object-namespace and --ignored-vpa-object-namespaces are mutually exclusive and can't be set together.") + } + healthCheck := metrics.NewHealthCheck(*metricsFetcherInterval * 5) metrics.Initialize(*address, healthCheck) metrics_recommender.Register() @@ -224,6 +230,8 @@ func run(healthCheck *metrics.HealthCheck) { source = input_metrics.NewPodMetricsesSource(resourceclient.NewForConfigOrDie(config)) } + ignoredNamespaces := strings.Split(*ignoredVpaObjectNamespaces, ",") + clusterStateFeeder := input.ClusterStateFeederFactory{ PodLister: podLister, OOMObserver: oomObserver, @@ -236,6 +244,7 @@ func run(healthCheck *metrics.HealthCheck) { MemorySaveMode: *memorySaver, ControllerFetcher: controllerFetcher, RecommenderName: *recommenderName, + IgnoredNamespaces: ignoredNamespaces, }.Make() controllerFetcher.Start(context.Background(), scaleCacheLoopPeriod) diff --git a/vertical-pod-autoscaler/pkg/updater/logic/updater.go b/vertical-pod-autoscaler/pkg/updater/logic/updater.go index 7e9c7f8a1e2f..605eab683200 100644 --- a/vertical-pod-autoscaler/pkg/updater/logic/updater.go +++ b/vertical-pod-autoscaler/pkg/updater/logic/updater.go @@ -19,6 +19,7 @@ package logic import ( "context" "fmt" + "slices" "time" "golang.org/x/time/rate" @@ -66,6 +67,7 @@ type updater struct { useAdmissionControllerStatus bool statusValidator status.Validator controllerFetcher controllerfetcher.ControllerFetcher + ignoredNamespaces []string } // NewUpdater creates Updater with given configuration @@ -84,6 +86,7 @@ func NewUpdater( controllerFetcher controllerfetcher.ControllerFetcher, priorityProcessor priority.PriorityProcessor, namespace string, + ignoredNamespaces []string, ) (Updater, error) { evictionRateLimiter := getRateLimiter(evictionRateLimit, evictionRateBurst) factory, err := eviction.NewPodsEvictionRestrictionFactory(kubeClient, minReplicasForEvicition, evictionToleranceFraction) @@ -107,6 +110,7 @@ func NewUpdater( status.AdmissionControllerStatusName, statusNamespace, ), + ignoredNamespaces: ignoredNamespaces, }, nil } @@ -137,6 +141,10 @@ func (u *updater) RunOnce(ctx context.Context) { vpas := make([]*vpa_api_util.VpaWithSelector, 0) for _, vpa := range vpaList { + if slices.Contains(u.ignoredNamespaces, vpa.Namespace) { + klog.V(3).Infof("skipping VPA object %s in namespace %s as namespace is ignored", vpa.Name, vpa.Namespace) + continue + } if vpa_api_util.GetUpdateMode(vpa) != vpa_types.UpdateModeRecreate && vpa_api_util.GetUpdateMode(vpa) != vpa_types.UpdateModeAuto { klog.V(3).Infof("skipping VPA object %s because its mode is not \"Recreate\" or \"Auto\"", klog.KObj(vpa)) diff --git a/vertical-pod-autoscaler/pkg/updater/logic/updater_test.go b/vertical-pod-autoscaler/pkg/updater/logic/updater_test.go index 9a3f10dcca20..c376f5a5a876 100644 --- a/vertical-pod-autoscaler/pkg/updater/logic/updater_test.go +++ b/vertical-pod-autoscaler/pkg/updater/logic/updater_test.go @@ -260,3 +260,100 @@ func newFakeValidator(isValid bool) status.Validator { func (f *fakeValidator) IsStatusValid(statusTimeout time.Duration) (bool, error) { return f.isValid, nil } + +func TestRunOnceIgnoreNamespaceMatchingPods(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + replicas := int32(5) + livePods := 5 + labels := map[string]string{"app": "testingApp"} + selector := parseLabelSelector("app = testingApp") + + containerName := "container1" + rc := apiv1.ReplicationController{ + TypeMeta: metav1.TypeMeta{ + Kind: "ReplicationController", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rc", + Namespace: "default", + }, + Spec: apiv1.ReplicationControllerSpec{ + Replicas: &replicas, + }, + } + pods := make([]*apiv1.Pod, livePods) + eviction := &test.PodsEvictionRestrictionMock{} + + for i := range pods { + pods[i] = test.Pod().WithName("test_"+strconv.Itoa(i)). + AddContainer(test.Container().WithName(containerName).WithCPURequest(resource.MustParse("1")).WithMemRequest(resource.MustParse("100M")).Get()). + WithCreator(&rc.ObjectMeta, &rc.TypeMeta). + Get() + + pods[i].Labels = labels + eviction.On("CanEvict", pods[i]).Return(true) + eviction.On("Evict", pods[i], nil).Return(nil) + } + + factory := &fakeEvictFactory{eviction} + vpaLister := &test.VerticalPodAutoscalerListerMock{} + + podLister := &test.PodListerMock{} + podLister.On("List").Return(pods, nil) + targetRef := &v1.CrossVersionObjectReference{ + Kind: rc.Kind, + Name: rc.Name, + APIVersion: rc.APIVersion, + } + vpaObj := test.VerticalPodAutoscaler(). + WithNamespace("default"). + WithContainer(containerName). + WithTarget("2", "200M"). + WithMinAllowed(containerName, "1", "100M"). + WithMaxAllowed(containerName, "3", "1G"). + WithTargetRef(targetRef).Get() + + vpaLister.On("List").Return([]*vpa_types.VerticalPodAutoscaler{vpaObj}, nil).Once() + + mockSelectorFetcher := target_mock.NewMockVpaTargetSelectorFetcher(ctrl) + mockSelectorFetcher.EXPECT().Fetch(gomock.Eq(vpaObj)).Return(selector, nil) + + updater := &updater{ + vpaLister: vpaLister, + podLister: podLister, + evictionFactory: factory, + evictionRateLimiter: rate.NewLimiter(rate.Inf, 0), + evictionAdmission: priority.NewDefaultPodEvictionAdmission(), + recommendationProcessor: &test.FakeRecommendationProcessor{}, + selectorFetcher: mockSelectorFetcher, + controllerFetcher: controllerfetcher.FakeControllerFetcher{}, + useAdmissionControllerStatus: true, + priorityProcessor: priority.NewProcessor(), + ignoredNamespaces: []string{"not-default"}, + statusValidator: newFakeValidator(true), + } + + updater.RunOnce(context.Background()) + eviction.AssertNumberOfCalls(t, "Evict", 5) +} + +func TestRunOnceIgnoreNamespaceMatching(t *testing.T) { + eviction := &test.PodsEvictionRestrictionMock{} + vpaLister := &test.VerticalPodAutoscalerListerMock{} + vpaObj := test.VerticalPodAutoscaler(). + WithNamespace("default"). + WithContainer("container").Get() + + vpaLister.On("List").Return([]*vpa_types.VerticalPodAutoscaler{vpaObj}, nil).Once() + + updater := &updater{ + vpaLister: vpaLister, + ignoredNamespaces: []string{"default"}, + } + + updater.RunOnce(context.Background()) + eviction.AssertNumberOfCalls(t, "Evict", 0) +} diff --git a/vertical-pod-autoscaler/pkg/updater/main.go b/vertical-pod-autoscaler/pkg/updater/main.go index 7589005a164e..3a72faad8607 100644 --- a/vertical-pod-autoscaler/pkg/updater/main.go +++ b/vertical-pod-autoscaler/pkg/updater/main.go @@ -20,6 +20,7 @@ import ( "context" "flag" "os" + "strings" "time" "github.com/spf13/pflag" @@ -72,8 +73,9 @@ var ( useAdmissionControllerStatus = flag.Bool("use-admission-controller-status", true, "If true, updater will only evict pods when admission controller status is valid.") - namespace = os.Getenv("NAMESPACE") - vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects. Empty means all namespaces will be used.") + namespace = os.Getenv("NAMESPACE") + vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects. Empty means all namespaces will be used. Must not be used if ignored-vpa-object-namespaces is set.") + ignoredVpaObjectNamespaces = flag.String("ignored-vpa-object-namespaces", "", "Comma separated list of namespaces to ignore. Must not be used if vpa-object-namespace is used.") ) const ( @@ -92,6 +94,10 @@ func main() { kube_flag.InitFlags() klog.V(1).Infof("Vertical Pod Autoscaler %s Updater", common.VerticalPodAutoscalerVersion) + if len(*vpaObjectNamespace) > 0 && len(*ignoredVpaObjectNamespaces) > 0 { + klog.Fatalf("--vpa-object-namespace and --ignored-vpa-object-namespaces are mutually exclusive and can't be set together.") + } + healthCheck := metrics.NewHealthCheck(*updaterInterval * 5) metrics.Initialize(*address, healthCheck) metrics_updater.Register() @@ -175,6 +181,9 @@ func run(healthCheck *metrics.HealthCheck) { if namespace != "" { admissionControllerStatusNamespace = namespace } + + ignoredNamespaces := strings.Split(*ignoredVpaObjectNamespaces, ",") + // TODO: use SharedInformerFactory in updater updater, err := updater.NewUpdater( kubeClient, @@ -191,6 +200,7 @@ func run(healthCheck *metrics.HealthCheck) { controllerFetcher, priority.NewProcessor(), *vpaObjectNamespace, + ignoredNamespaces, ) if err != nil { klog.Fatalf("Failed to create updater: %v", err)