Skip to content

Commit 3a1c5b9

Browse files
authored
Merge pull request #6788 from adrianmoisey/vpa-ignore-namespace
Add option to ignore namespaces
2 parents 797952a + 74e7c5f commit 3a1c5b9

File tree

10 files changed

+353
-40
lines changed

10 files changed

+353
-40
lines changed

vertical-pod-autoscaler/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- [Starting multiple recommenders](#starting-multiple-recommenders)
2424
- [Using CPU management with static policy](#using-cpu-management-with-static-policy)
2525
- [Controlling eviction behavior based on scaling direction and resource](#controlling-eviction-behavior-based-on-scaling-direction-and-resource)
26+
- [Limiting which namespaces are used](#limiting-which-namespaces-are-used)
2627
- [Known limitations](#known-limitations)
2728
- [Related links](#related-links)
2829

@@ -376,6 +377,16 @@ vpa-post-processor.kubernetes.io/{containerName}_integerCPU=true
376377
```
377378
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.
378379

380+
### Limiting which namespaces are used
381+
382+
By default the VPA will run against all namespaces. You can limit that behaviour by setting the following options:
383+
384+
1. `ignored-vpa-object-namespaces` - A comma separated list of namespaces to ignore
385+
1. `vpa-object-namespace` - A single namespace to monitor
386+
387+
These options cannot be used together and are mutually exclusive.
388+
389+
379390
# Known limitations
380391

381392
* Whenever VPA updates the pod resources, the pod is recreated, which causes all

vertical-pod-autoscaler/pkg/admission-controller/config.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ func configTLS(cfg certsConfig, minTlsVersion, ciphers string, stop <-chan struc
9090

9191
// register this webhook admission controller with the kube-apiserver
9292
// by creating MutatingWebhookConfiguration.
93-
func selfRegistration(clientset kubernetes.Interface, caCert []byte, namespace, serviceName, url string, registerByURL bool, timeoutSeconds int32) {
94-
time.Sleep(10 * time.Second)
93+
func selfRegistration(clientset kubernetes.Interface, caCert []byte, webHookDelay time.Duration, namespace, serviceName, url string, registerByURL bool, timeoutSeconds int32, selectedNamespace string, ignoredNamespaces []string) {
94+
time.Sleep(webHookDelay)
9595
client := clientset.AdmissionregistrationV1().MutatingWebhookConfigurations()
9696
_, err := client.Get(context.TODO(), webhookConfigName, metav1.GetOptions{})
9797
if err == nil {
@@ -111,6 +111,29 @@ func selfRegistration(clientset kubernetes.Interface, caCert []byte, namespace,
111111
sideEffects := admissionregistration.SideEffectClassNone
112112
failurePolicy := admissionregistration.Ignore
113113
RegisterClientConfig.CABundle = caCert
114+
115+
var namespaceSelector metav1.LabelSelector
116+
if len(ignoredNamespaces) > 0 {
117+
namespaceSelector = metav1.LabelSelector{
118+
MatchExpressions: []metav1.LabelSelectorRequirement{
119+
{
120+
Key: "kubernetes.io/metadata.name",
121+
Operator: metav1.LabelSelectorOpNotIn,
122+
Values: ignoredNamespaces,
123+
},
124+
},
125+
}
126+
} else if len(selectedNamespace) > 0 {
127+
namespaceSelector = metav1.LabelSelector{
128+
MatchExpressions: []metav1.LabelSelectorRequirement{
129+
{
130+
Key: "kubernetes.io/metadata.name",
131+
Operator: metav1.LabelSelectorOpIn,
132+
Values: []string{selectedNamespace},
133+
},
134+
},
135+
}
136+
}
114137
webhookConfig := &admissionregistration.MutatingWebhookConfiguration{
115138
ObjectMeta: metav1.ObjectMeta{
116139
Name: webhookConfigName,
@@ -137,10 +160,11 @@ func selfRegistration(clientset kubernetes.Interface, caCert []byte, namespace,
137160
},
138161
},
139162
},
140-
FailurePolicy: &failurePolicy,
141-
ClientConfig: RegisterClientConfig,
142-
SideEffects: &sideEffects,
143-
TimeoutSeconds: &timeoutSeconds,
163+
FailurePolicy: &failurePolicy,
164+
ClientConfig: RegisterClientConfig,
165+
SideEffects: &sideEffects,
166+
TimeoutSeconds: &timeoutSeconds,
167+
NamespaceSelector: &namespaceSelector,
144168
},
145169
},
146170
}

vertical-pod-autoscaler/pkg/admission-controller/config_test.go

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"context"
2121
"testing"
22+
"time"
2223

2324
"github.com/stretchr/testify/assert"
2425
admissionregistration "k8s.io/api/admissionregistration/v1"
@@ -30,13 +31,16 @@ func TestSelfRegistrationBase(t *testing.T) {
3031

3132
testClientSet := fake.NewSimpleClientset()
3233
caCert := []byte("fake")
34+
webHookDelay := 0 * time.Second
3335
namespace := "default"
3436
serviceName := "vpa-service"
3537
url := "http://example.com/"
3638
registerByURL := true
3739
timeoutSeconds := int32(32)
40+
selectedNamespace := ""
41+
ignoredNamespaces := []string{}
3842

39-
selfRegistration(testClientSet, caCert, namespace, serviceName, url, registerByURL, timeoutSeconds)
43+
selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces)
4044

4145
webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations()
4246
webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{})
@@ -70,13 +74,16 @@ func TestSelfRegistrationWithURL(t *testing.T) {
7074

7175
testClientSet := fake.NewSimpleClientset()
7276
caCert := []byte("fake")
77+
webHookDelay := 0 * time.Second
7378
namespace := "default"
7479
serviceName := "vpa-service"
7580
url := "http://example.com/"
7681
registerByURL := true
7782
timeoutSeconds := int32(32)
83+
selectedNamespace := ""
84+
ignoredNamespaces := []string{}
7885

79-
selfRegistration(testClientSet, caCert, namespace, serviceName, url, registerByURL, timeoutSeconds)
86+
selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces)
8087

8188
webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations()
8289
webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{})
@@ -95,13 +102,16 @@ func TestSelfRegistrationWithOutURL(t *testing.T) {
95102

96103
testClientSet := fake.NewSimpleClientset()
97104
caCert := []byte("fake")
105+
webHookDelay := 0 * time.Second
98106
namespace := "default"
99107
serviceName := "vpa-service"
100108
url := "http://example.com/"
101109
registerByURL := false
102110
timeoutSeconds := int32(32)
111+
selectedNamespace := ""
112+
ignoredNamespaces := []string{}
103113

104-
selfRegistration(testClientSet, caCert, namespace, serviceName, url, registerByURL, timeoutSeconds)
114+
selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces)
105115

106116
webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations()
107117
webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{})
@@ -117,3 +127,66 @@ func TestSelfRegistrationWithOutURL(t *testing.T) {
117127

118128
assert.Nil(t, webhook.ClientConfig.URL, "expected URL to be set")
119129
}
130+
131+
func TestSelfRegistrationWithIgnoredNamespaces(t *testing.T) {
132+
133+
testClientSet := fake.NewSimpleClientset()
134+
caCert := []byte("fake")
135+
webHookDelay := 0 * time.Second
136+
namespace := "default"
137+
serviceName := "vpa-service"
138+
url := "http://example.com/"
139+
registerByURL := false
140+
timeoutSeconds := int32(32)
141+
selectedNamespace := ""
142+
ignoredNamespaces := []string{"test"}
143+
144+
selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces)
145+
146+
webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations()
147+
webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{})
148+
149+
assert.NoError(t, err, "expected no error fetching webhook configuration")
150+
151+
assert.Len(t, webhookConfig.Webhooks, 1, "expected one webhook configuration")
152+
webhook := webhookConfig.Webhooks[0]
153+
154+
assert.NotNil(t, webhook.NamespaceSelector.MatchExpressions, "expected namespace selector not to be nil")
155+
assert.Len(t, webhook.NamespaceSelector.MatchExpressions, 1, "expected one match expression")
156+
157+
matchExpression := webhook.NamespaceSelector.MatchExpressions[0]
158+
assert.Equal(t, matchExpression.Operator, metav1.LabelSelectorOpNotIn, "expected namespace operator to be OpNotIn")
159+
assert.Equal(t, matchExpression.Values, ignoredNamespaces, "expected namespace selector match expression to be equal")
160+
}
161+
162+
func TestSelfRegistrationWithSelectedNamespaces(t *testing.T) {
163+
164+
testClientSet := fake.NewSimpleClientset()
165+
caCert := []byte("fake")
166+
webHookDelay := 0 * time.Second
167+
namespace := "default"
168+
serviceName := "vpa-service"
169+
url := "http://example.com/"
170+
registerByURL := false
171+
timeoutSeconds := int32(32)
172+
selectedNamespace := "test"
173+
ignoredNamespaces := []string{}
174+
175+
selfRegistration(testClientSet, caCert, webHookDelay, namespace, serviceName, url, registerByURL, timeoutSeconds, selectedNamespace, ignoredNamespaces)
176+
177+
webhookConfigInterface := testClientSet.AdmissionregistrationV1().MutatingWebhookConfigurations()
178+
webhookConfig, err := webhookConfigInterface.Get(context.TODO(), webhookConfigName, metav1.GetOptions{})
179+
180+
assert.NoError(t, err, "expected no error fetching webhook configuration")
181+
182+
assert.Len(t, webhookConfig.Webhooks, 1, "expected one webhook configuration")
183+
webhook := webhookConfig.Webhooks[0]
184+
185+
assert.NotNil(t, webhook.NamespaceSelector.MatchExpressions, "expected namespace selector not to be nil")
186+
assert.Len(t, webhook.NamespaceSelector.MatchExpressions, 1, "expected one match expression")
187+
188+
matchExpression := webhook.NamespaceSelector.MatchExpressions[0]
189+
assert.Equal(t, metav1.LabelSelectorOpIn, matchExpression.Operator, "expected namespace operator to be OpIn")
190+
assert.Equal(t, matchExpression.Operator, metav1.LabelSelectorOpIn, "expected namespace operator to be OpIn")
191+
assert.Equal(t, matchExpression.Values, []string{selectedNamespace}, "expected namespace selector match expression to be equal")
192+
}

vertical-pod-autoscaler/pkg/admission-controller/main.go

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"net/http"
2323
"os"
24+
"strings"
2425
"time"
2526

2627
apiv1 "k8s.io/api/core/v1"
@@ -51,6 +52,7 @@ const (
5152
scaleCacheEntryLifetime time.Duration = time.Hour
5253
scaleCacheEntryFreshnessTime time.Duration = 10 * time.Minute
5354
scaleCacheEntryJitterFactor float64 = 1.
55+
webHookDelay = 10 * time.Second
5456
)
5557

5658
var (
@@ -63,26 +65,31 @@ var (
6365
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.")
6466
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.")
6567

66-
port = flag.Int("port", 8000, "The port to listen on.")
67-
address = flag.String("address", ":8944", "The address to expose Prometheus metrics.")
68-
kubeconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
69-
kubeApiQps = flag.Float64("kube-api-qps", 5.0, `QPS limit when making requests to Kubernetes apiserver`)
70-
kubeApiBurst = flag.Float64("kube-api-burst", 10.0, `QPS burst limit when making requests to Kubernetes apiserver`)
71-
namespace = os.Getenv("NAMESPACE")
72-
serviceName = flag.String("webhook-service", "vpa-webhook", "Kubernetes service under which webhook is registered. Used when registerByURL is set to false.")
73-
webhookAddress = flag.String("webhook-address", "", "Address under which webhook is registered. Used when registerByURL is set to true.")
74-
webhookPort = flag.String("webhook-port", "", "Server Port for Webhook")
75-
webhookTimeout = flag.Int("webhook-timeout-seconds", 30, "Timeout in seconds that the API server should wait for this webhook to respond before failing.")
76-
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.")
77-
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")
78-
vpaObjectNamespace = flag.String("vpa-object-namespace", apiv1.NamespaceAll, "Namespace to search for VPA objects. Empty means all namespaces will be used.")
68+
port = flag.Int("port", 8000, "The port to listen on.")
69+
address = flag.String("address", ":8944", "The address to expose Prometheus metrics.")
70+
kubeconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
71+
kubeApiQps = flag.Float64("kube-api-qps", 5.0, `QPS limit when making requests to Kubernetes apiserver`)
72+
kubeApiBurst = flag.Float64("kube-api-burst", 10.0, `QPS burst limit when making requests to Kubernetes apiserver`)
73+
namespace = os.Getenv("NAMESPACE")
74+
serviceName = flag.String("webhook-service", "vpa-webhook", "Kubernetes service under which webhook is registered. Used when registerByURL is set to false.")
75+
webhookAddress = flag.String("webhook-address", "", "Address under which webhook is registered. Used when registerByURL is set to true.")
76+
webhookPort = flag.String("webhook-port", "", "Server Port for Webhook")
77+
webhookTimeout = flag.Int("webhook-timeout-seconds", 30, "Timeout in seconds that the API server should wait for this webhook to respond before failing.")
78+
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.")
79+
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")
80+
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.")
81+
ignoredVpaObjectNamespaces = flag.String("ignored-vpa-object-namespaces", "", "Comma separated list of namespaces to ignore. Must not be used if vpa-object-namespace is used.")
7982
)
8083

8184
func main() {
8285
klog.InitFlags(nil)
8386
kube_flag.InitFlags()
8487
klog.V(1).Infof("Vertical Pod Autoscaler %s Admission Controller", common.VerticalPodAutoscalerVersion)
8588

89+
if len(*vpaObjectNamespace) > 0 && len(*ignoredVpaObjectNamespaces) > 0 {
90+
klog.Fatalf("--vpa-object-namespace and --ignored-vpa-object-namespaces are mutually exclusive and can't be set together.")
91+
}
92+
8693
healthCheck := metrics.NewHealthCheck(time.Minute)
8794
metrics.Initialize(*address, healthCheck)
8895
metrics_admission.Register()
@@ -136,9 +143,10 @@ func main() {
136143
TLSConfig: configTLS(*certsConfiguration, *minTlsVersion, *ciphers, stopCh),
137144
}
138145
url := fmt.Sprintf("%v:%v", *webhookAddress, *webhookPort)
146+
ignoredNamespaces := strings.Split(*ignoredVpaObjectNamespaces, ",")
139147
go func() {
140148
if *registerWebhook {
141-
selfRegistration(kubeClient, readFile(*certsConfiguration.clientCaFile), namespace, *serviceName, url, *registerByURL, int32(*webhookTimeout))
149+
selfRegistration(kubeClient, readFile(*certsConfiguration.clientCaFile), webHookDelay, namespace, *serviceName, url, *registerByURL, int32(*webhookTimeout), *vpaObjectNamespace, ignoredNamespaces)
142150
}
143151
// Start status updates after the webhook is initialized.
144152
statusUpdater.Run(stopCh)

vertical-pod-autoscaler/pkg/recommender/input/cluster_feeder.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package input
1919
import (
2020
"context"
2121
"fmt"
22+
"slices"
2223
"time"
2324

2425
apiv1 "k8s.io/api/core/v1"
@@ -87,6 +88,7 @@ type ClusterStateFeederFactory struct {
8788
MemorySaveMode bool
8889
ControllerFetcher controllerfetcher.ControllerFetcher
8990
RecommenderName string
91+
IgnoredNamespaces []string
9092
}
9193

9294
// Make creates new ClusterStateFeeder with internal data providers, based on kube client.
@@ -103,6 +105,7 @@ func (m ClusterStateFeederFactory) Make() *clusterStateFeeder {
103105
memorySaveMode: m.MemorySaveMode,
104106
controllerFetcher: m.ControllerFetcher,
105107
recommenderName: m.RecommenderName,
108+
ignoredNamespaces: m.IgnoredNamespaces,
106109
}
107110
}
108111

@@ -192,6 +195,7 @@ type clusterStateFeeder struct {
192195
memorySaveMode bool
193196
controllerFetcher controllerfetcher.ControllerFetcher
194197
recommenderName string
198+
ignoredNamespaces []string
195199
}
196200

197201
func (feeder *clusterStateFeeder) InitFromHistoryProvider(historyProvider history.HistoryProvider) {
@@ -332,6 +336,12 @@ func filterVPAs(feeder *clusterStateFeeder, allVpaCRDs []*vpa_types.VerticalPodA
332336
continue
333337
}
334338
}
339+
340+
if slices.Contains(feeder.ignoredNamespaces, vpaCRD.ObjectMeta.Namespace) {
341+
klog.V(6).Infof("Ignoring vpaCRD %s in namespace %s as namespace is ignored", vpaCRD.Name, vpaCRD.Namespace)
342+
continue
343+
}
344+
335345
vpaCRDs = append(vpaCRDs, vpaCRD)
336346
}
337347
return vpaCRDs

0 commit comments

Comments
 (0)