...

Source file src/k8s.io/kubernetes/test/integration/apiserver/admissionwebhook/match_conditions_test.go

Documentation: k8s.io/kubernetes/test/integration/apiserver/admissionwebhook

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package admissionwebhook
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"io"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"strconv"
    28  	"sync"
    29  	"testing"
    30  	"time"
    31  
    32  	admissionv1 "k8s.io/api/admission/v1"
    33  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    34  	corev1 "k8s.io/api/core/v1"
    35  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/types"
    38  	"k8s.io/apimachinery/pkg/util/wait"
    39  	clientset "k8s.io/client-go/kubernetes"
    40  	apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    41  	"k8s.io/kubernetes/test/integration/framework"
    42  )
    43  
    44  type admissionRecorder struct {
    45  	mu       sync.Mutex
    46  	upCh     chan struct{}
    47  	upOnce   sync.Once
    48  	requests []*admissionv1.AdmissionRequest
    49  }
    50  
    51  func (r *admissionRecorder) Record(req *admissionv1.AdmissionRequest) {
    52  	r.mu.Lock()
    53  	defer r.mu.Unlock()
    54  	r.requests = append(r.requests, req)
    55  }
    56  
    57  func (r *admissionRecorder) MarkerReceived() {
    58  	r.mu.Lock()
    59  	defer r.mu.Unlock()
    60  	r.upOnce.Do(func() {
    61  		close(r.upCh)
    62  	})
    63  }
    64  
    65  func (r *admissionRecorder) Reset() chan struct{} {
    66  	r.mu.Lock()
    67  	defer r.mu.Unlock()
    68  	r.requests = []*admissionv1.AdmissionRequest{}
    69  	r.upCh = make(chan struct{})
    70  	r.upOnce = sync.Once{}
    71  	return r.upCh
    72  }
    73  
    74  func newMatchConditionHandler(recorder *admissionRecorder) http.Handler {
    75  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    76  		defer r.Body.Close()
    77  		data, err := io.ReadAll(r.Body)
    78  		if err != nil {
    79  			http.Error(w, err.Error(), 400)
    80  		}
    81  		review := admissionv1.AdmissionReview{}
    82  		if err := json.Unmarshal(data, &review); err != nil {
    83  			http.Error(w, err.Error(), 400)
    84  		}
    85  
    86  		review.Response = &admissionv1.AdmissionResponse{
    87  			Allowed: true,
    88  			UID:     review.Request.UID,
    89  			Result:  &metav1.Status{Message: "admitted"},
    90  		}
    91  
    92  		w.Header().Set("Content-Type", "application/json")
    93  		if err := json.NewEncoder(w).Encode(review); err != nil {
    94  			http.Error(w, err.Error(), 400)
    95  			return
    96  		}
    97  
    98  		switch r.URL.Path {
    99  		case "/marker":
   100  			recorder.MarkerReceived()
   101  			return
   102  		}
   103  
   104  		recorder.Record(review.Request)
   105  	})
   106  }
   107  
   108  // TestMatchConditions tests ValidatingWebhookConfigurations and MutatingWebhookConfigurations that validates different cases of matchCondition fields
   109  func TestMatchConditions(t *testing.T) {
   110  
   111  	fail := admissionregistrationv1.Fail
   112  	ignore := admissionregistrationv1.Ignore
   113  
   114  	testcases := []struct {
   115  		name            string
   116  		matchConditions []admissionregistrationv1.MatchCondition
   117  		pods            []*corev1.Pod
   118  		matchedPods     []*corev1.Pod
   119  		expectErrorPod  bool
   120  		failPolicy      *admissionregistrationv1.FailurePolicyType
   121  	}{
   122  		{
   123  			name: "pods in namespace kube-system is ignored",
   124  			matchConditions: []admissionregistrationv1.MatchCondition{
   125  				{
   126  					Name:       "pods-in-kube-system-exempt.kubernetes.io",
   127  					Expression: "object.metadata.namespace != 'kube-system'",
   128  				},
   129  			},
   130  			pods: []*corev1.Pod{
   131  				matchConditionsTestPod("test1", "kube-system"),
   132  				matchConditionsTestPod("test2", "default"),
   133  			},
   134  			matchedPods: []*corev1.Pod{
   135  				matchConditionsTestPod("test2", "default"),
   136  			},
   137  		},
   138  		{
   139  			name: "matchConditions are ANDed together",
   140  			matchConditions: []admissionregistrationv1.MatchCondition{
   141  				{
   142  					Name:       "pods-in-kube-system-exempt.kubernetes.io",
   143  					Expression: "object.metadata.namespace != 'kube-system'",
   144  				},
   145  				{
   146  					Name:       "pods-with-name-test1.kubernetes.io",
   147  					Expression: "object.metadata.name == 'test1'",
   148  				},
   149  			},
   150  			pods: []*corev1.Pod{
   151  				matchConditionsTestPod("test1", "kube-system"),
   152  				matchConditionsTestPod("test1", "default"),
   153  				matchConditionsTestPod("test2", "default"),
   154  			},
   155  			matchedPods: []*corev1.Pod{
   156  				matchConditionsTestPod("test1", "default"),
   157  			},
   158  		},
   159  		{
   160  			name: "mix of true, error and false should not match and not call webhook",
   161  			matchConditions: []admissionregistrationv1.MatchCondition{
   162  				{
   163  					Name:       "test1",
   164  					Expression: "object.nonExistentProperty == 'someval'",
   165  				},
   166  				{
   167  					Name:       "test2",
   168  					Expression: "true",
   169  				},
   170  				{
   171  					Name:       "test3",
   172  					Expression: "false",
   173  				},
   174  				{
   175  					Name:       "test4",
   176  					Expression: "true",
   177  				},
   178  				{
   179  					Name:       "test5",
   180  					Expression: "object.nonExistentProperty == 'someval'",
   181  				},
   182  			},
   183  			pods: []*corev1.Pod{
   184  				matchConditionsTestPod("test1", "kube-system"),
   185  				matchConditionsTestPod("test2", "default"),
   186  			},
   187  			matchedPods:    []*corev1.Pod{},
   188  			expectErrorPod: false,
   189  		},
   190  		{
   191  			name: "mix of true and error should reject request without fail policy",
   192  			matchConditions: []admissionregistrationv1.MatchCondition{
   193  				{
   194  					Name:       "test1",
   195  					Expression: "object.nonExistentProperty == 'someval'",
   196  				},
   197  				{
   198  					Name:       "test2",
   199  					Expression: "true",
   200  				},
   201  				{
   202  					Name:       "test4",
   203  					Expression: "true",
   204  				},
   205  				{
   206  					Name:       "test5",
   207  					Expression: "object.nonExistentProperty == 'someval'",
   208  				},
   209  			},
   210  			pods: []*corev1.Pod{
   211  				matchConditionsTestPod("test1", "kube-system"),
   212  				matchConditionsTestPod("test2", "default"),
   213  			},
   214  			matchedPods:    []*corev1.Pod{},
   215  			expectErrorPod: true,
   216  		},
   217  		{
   218  			name: "mix of true and error should reject request with fail policy fail",
   219  			matchConditions: []admissionregistrationv1.MatchCondition{
   220  				{
   221  					Name:       "test1",
   222  					Expression: "object.nonExistentProperty == 'someval'",
   223  				},
   224  				{
   225  					Name:       "test2",
   226  					Expression: "true",
   227  				},
   228  				{
   229  					Name:       "test4",
   230  					Expression: "true",
   231  				},
   232  				{
   233  					Name:       "test5",
   234  					Expression: "object.nonExistentProperty == 'someval'",
   235  				},
   236  			},
   237  			pods: []*corev1.Pod{
   238  				matchConditionsTestPod("test1", "kube-system"),
   239  				matchConditionsTestPod("test2", "default"),
   240  			},
   241  			matchedPods:    []*corev1.Pod{},
   242  			failPolicy:     &fail,
   243  			expectErrorPod: true,
   244  		},
   245  		{
   246  			name: "mix of true and error should match request and call webhook with fail policy ignore",
   247  			matchConditions: []admissionregistrationv1.MatchCondition{
   248  				{
   249  					Name:       "tes1",
   250  					Expression: "object.nonExistentProperty == 'someval'",
   251  				},
   252  				{
   253  					Name:       "test2",
   254  					Expression: "true",
   255  				},
   256  				{
   257  					Name:       "test4",
   258  					Expression: "true",
   259  				},
   260  				{
   261  					Name:       "test5",
   262  					Expression: "object.nonExistentProperty == 'someval'",
   263  				},
   264  			},
   265  			pods: []*corev1.Pod{
   266  				matchConditionsTestPod("test1", "kube-system"),
   267  				matchConditionsTestPod("test2", "default"),
   268  			},
   269  			matchedPods: []*corev1.Pod{},
   270  			failPolicy:  &ignore,
   271  		},
   272  		{
   273  			name: "has access to oldObject",
   274  			matchConditions: []admissionregistrationv1.MatchCondition{
   275  				{
   276  					Name:       "old-object-is-null.kubernetes.io",
   277  					Expression: "oldObject == null",
   278  				},
   279  			},
   280  			pods: []*corev1.Pod{
   281  				matchConditionsTestPod("test2", "default"),
   282  			},
   283  			matchedPods: []*corev1.Pod{
   284  				matchConditionsTestPod("test2", "default"),
   285  			},
   286  		},
   287  	}
   288  
   289  	roots := x509.NewCertPool()
   290  	if !roots.AppendCertsFromPEM(localhostCert) {
   291  		t.Fatal("Failed to append Cert from PEM")
   292  	}
   293  	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
   294  	if err != nil {
   295  		t.Fatalf("Failed to build cert with error: %+v", err)
   296  	}
   297  
   298  	recorder := &admissionRecorder{requests: []*admissionv1.AdmissionRequest{}}
   299  
   300  	webhookServer := httptest.NewUnstartedServer(newMatchConditionHandler(recorder))
   301  	webhookServer.TLS = &tls.Config{
   302  		RootCAs:      roots,
   303  		Certificates: []tls.Certificate{cert},
   304  	}
   305  	webhookServer.StartTLS()
   306  	defer webhookServer.Close()
   307  
   308  	dryRunCreate := metav1.CreateOptions{
   309  		DryRun: []string{metav1.DryRunAll},
   310  	}
   311  
   312  	for _, testcase := range testcases {
   313  		t.Run(testcase.name, func(t *testing.T) {
   314  			upCh := recorder.Reset()
   315  
   316  			server, err := apiservertesting.StartTestServer(t, nil, []string{
   317  				"--disable-admission-plugins=ServiceAccount",
   318  			}, framework.SharedEtcd())
   319  			if err != nil {
   320  				t.Fatal(err)
   321  			}
   322  			defer server.TearDownFn()
   323  
   324  			config := server.ClientConfig
   325  
   326  			client, err := clientset.NewForConfig(config)
   327  			if err != nil {
   328  				t.Fatal(err)
   329  			}
   330  
   331  			// Write markers to a separate namespace to avoid cross-talk
   332  			markerNs := "marker"
   333  			_, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: markerNs}}, metav1.CreateOptions{})
   334  			if err != nil {
   335  				t.Fatal(err)
   336  			}
   337  
   338  			// Create a marker object to use to check for the webhook configurations to be ready.
   339  			marker, err := client.CoreV1().Pods(markerNs).Create(context.TODO(), newMarkerPod(markerNs), metav1.CreateOptions{})
   340  			if err != nil {
   341  				t.Fatal(err)
   342  			}
   343  
   344  			endpoint := webhookServer.URL
   345  			markerEndpoint := webhookServer.URL + "/marker"
   346  			validatingwebhook := &admissionregistrationv1.ValidatingWebhookConfiguration{
   347  				ObjectMeta: metav1.ObjectMeta{
   348  					Name: "admission.integration.test",
   349  				},
   350  				Webhooks: []admissionregistrationv1.ValidatingWebhook{
   351  					{
   352  						Name: "admission.integration.test",
   353  						Rules: []admissionregistrationv1.RuleWithOperations{{
   354  							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
   355  							Rule: admissionregistrationv1.Rule{
   356  								APIGroups:   []string{""},
   357  								APIVersions: []string{"v1"},
   358  								Resources:   []string{"pods"},
   359  							},
   360  						}},
   361  						ClientConfig: admissionregistrationv1.WebhookClientConfig{
   362  							URL:      &endpoint,
   363  							CABundle: localhostCert,
   364  						},
   365  						// ignore pods in the marker namespace
   366  						NamespaceSelector: &metav1.LabelSelector{
   367  							MatchExpressions: []metav1.LabelSelectorRequirement{
   368  								{
   369  									Key:      corev1.LabelMetadataName,
   370  									Operator: metav1.LabelSelectorOpNotIn,
   371  									Values:   []string{"marker"},
   372  								},
   373  							}},
   374  						FailurePolicy:           testcase.failPolicy,
   375  						SideEffects:             &noSideEffects,
   376  						AdmissionReviewVersions: []string{"v1"},
   377  						MatchConditions:         testcase.matchConditions,
   378  					},
   379  					{
   380  						Name: "admission.integration.test.marker",
   381  						Rules: []admissionregistrationv1.RuleWithOperations{{
   382  							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
   383  							Rule:       admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
   384  						}},
   385  						ClientConfig: admissionregistrationv1.WebhookClientConfig{
   386  							URL:      &markerEndpoint,
   387  							CABundle: localhostCert,
   388  						},
   389  						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{
   390  							corev1.LabelMetadataName: "marker",
   391  						}},
   392  						ObjectSelector:          &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}},
   393  						FailurePolicy:           testcase.failPolicy,
   394  						SideEffects:             &noSideEffects,
   395  						AdmissionReviewVersions: []string{"v1"},
   396  					},
   397  				},
   398  			}
   399  
   400  			validatingcfg, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), validatingwebhook, metav1.CreateOptions{})
   401  			if err != nil {
   402  				t.Fatal(err)
   403  			}
   404  
   405  			vhwHasBeenCleanedUp := false
   406  			defer func() {
   407  				if !vhwHasBeenCleanedUp {
   408  					err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(context.TODO(), validatingcfg.GetName(), metav1.DeleteOptions{})
   409  					if err != nil {
   410  						t.Fatal(err)
   411  					}
   412  				}
   413  			}()
   414  
   415  			// wait until new webhook is called the first time
   416  			if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
   417  				_, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
   418  				select {
   419  				case <-upCh:
   420  					return true, nil
   421  				default:
   422  					t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
   423  					return false, nil
   424  				}
   425  			}); err != nil {
   426  				t.Fatal(err)
   427  			}
   428  
   429  			for _, pod := range testcase.pods {
   430  				_, err := client.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, dryRunCreate)
   431  				if !testcase.expectErrorPod && err != nil {
   432  					t.Fatalf("unexpected error creating test pod: %v", err)
   433  				} else if testcase.expectErrorPod && err == nil {
   434  					t.Fatal("expected error creating pods")
   435  				}
   436  			}
   437  
   438  			if len(recorder.requests) != len(testcase.matchedPods) {
   439  				t.Errorf("unexpected requests %v, expected %v", recorder.requests, testcase.matchedPods)
   440  			}
   441  
   442  			for i, request := range recorder.requests {
   443  				if request.Name != testcase.matchedPods[i].Name {
   444  					t.Errorf("unexpected pod name %v, expected %v", request.Name, testcase.matchedPods[i].Name)
   445  				}
   446  				if request.Namespace != testcase.matchedPods[i].Namespace {
   447  					t.Errorf("unexpected pod namespace %v, expected %v", request.Namespace, testcase.matchedPods[i].Namespace)
   448  				}
   449  			}
   450  
   451  			//Reset and rerun against mutating webhook configuration
   452  			//TODO: private helper function for validation after creating vwh or mwh
   453  			upCh = recorder.Reset()
   454  			err = client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Delete(context.TODO(), validatingcfg.GetName(), metav1.DeleteOptions{})
   455  			if err != nil {
   456  				t.Fatal(err)
   457  			} else {
   458  				vhwHasBeenCleanedUp = true
   459  			}
   460  
   461  			mutatingwebhook := &admissionregistrationv1.MutatingWebhookConfiguration{
   462  				ObjectMeta: metav1.ObjectMeta{
   463  					Name: "admission.integration.test",
   464  				},
   465  				Webhooks: []admissionregistrationv1.MutatingWebhook{
   466  					{
   467  						Name: "admission.integration.test",
   468  						Rules: []admissionregistrationv1.RuleWithOperations{{
   469  							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
   470  							Rule: admissionregistrationv1.Rule{
   471  								APIGroups:   []string{""},
   472  								APIVersions: []string{"v1"},
   473  								Resources:   []string{"pods"},
   474  							},
   475  						}},
   476  						ClientConfig: admissionregistrationv1.WebhookClientConfig{
   477  							URL:      &endpoint,
   478  							CABundle: localhostCert,
   479  						},
   480  						// ignore pods in the marker namespace
   481  						NamespaceSelector: &metav1.LabelSelector{
   482  							MatchExpressions: []metav1.LabelSelectorRequirement{
   483  								{
   484  									Key:      corev1.LabelMetadataName,
   485  									Operator: metav1.LabelSelectorOpNotIn,
   486  									Values:   []string{"marker"},
   487  								},
   488  							}},
   489  						FailurePolicy:           testcase.failPolicy,
   490  						SideEffects:             &noSideEffects,
   491  						AdmissionReviewVersions: []string{"v1"},
   492  						MatchConditions:         testcase.matchConditions,
   493  					},
   494  					{
   495  						Name: "admission.integration.test.marker",
   496  						Rules: []admissionregistrationv1.RuleWithOperations{{
   497  							Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
   498  							Rule:       admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
   499  						}},
   500  						ClientConfig: admissionregistrationv1.WebhookClientConfig{
   501  							URL:      &markerEndpoint,
   502  							CABundle: localhostCert,
   503  						},
   504  						NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{
   505  							corev1.LabelMetadataName: "marker",
   506  						}},
   507  						ObjectSelector:          &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}},
   508  						FailurePolicy:           testcase.failPolicy,
   509  						SideEffects:             &noSideEffects,
   510  						AdmissionReviewVersions: []string{"v1"},
   511  					},
   512  				},
   513  			}
   514  
   515  			mutatingcfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), mutatingwebhook, metav1.CreateOptions{})
   516  			if err != nil {
   517  				t.Fatal(err)
   518  			}
   519  			defer func() {
   520  				err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingcfg.GetName(), metav1.DeleteOptions{})
   521  				if err != nil {
   522  					t.Fatal(err)
   523  				}
   524  			}()
   525  
   526  			// wait until new webhook is called the first time
   527  			if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
   528  				_, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
   529  				select {
   530  				case <-upCh:
   531  					return true, nil
   532  				default:
   533  					t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
   534  					return false, nil
   535  				}
   536  			}); err != nil {
   537  				t.Fatal(err)
   538  			}
   539  
   540  			for _, pod := range testcase.pods {
   541  				_, err = client.CoreV1().Pods(pod.Namespace).Create(context.TODO(), pod, dryRunCreate)
   542  				if testcase.expectErrorPod == false && err != nil {
   543  					t.Fatalf("unexpected error creating test pod: %v", err)
   544  				} else if testcase.expectErrorPod == true && err == nil {
   545  					t.Fatal("expected error creating pods")
   546  				}
   547  			}
   548  
   549  			if len(recorder.requests) != len(testcase.matchedPods) {
   550  				t.Errorf("unexpected requests %v, expected %v", recorder.requests, testcase.matchedPods)
   551  			}
   552  
   553  			for i, request := range recorder.requests {
   554  				if request.Name != testcase.matchedPods[i].Name {
   555  					t.Errorf("unexpected pod name %v, expected %v", request.Name, testcase.matchedPods[i].Name)
   556  				}
   557  				if request.Namespace != testcase.matchedPods[i].Namespace {
   558  					t.Errorf("unexpected pod namespace %v, expected %v", request.Namespace, testcase.matchedPods[i].Namespace)
   559  				}
   560  			}
   561  		})
   562  	}
   563  }
   564  
   565  func TestMatchConditions_validation(t *testing.T) {
   566  
   567  	server := apiservertesting.StartTestServerOrDie(t, nil, []string{
   568  		"--disable-admission-plugins=ServiceAccount",
   569  	}, framework.SharedEtcd())
   570  	defer server.TearDownFn()
   571  
   572  	client := clientset.NewForConfigOrDie(server.ClientConfig)
   573  
   574  	testcases := []struct {
   575  		name            string
   576  		matchConditions []admissionregistrationv1.MatchCondition
   577  		expectError     bool
   578  	}{{
   579  		name: "valid match condition",
   580  		matchConditions: []admissionregistrationv1.MatchCondition{{
   581  			Name:       "true",
   582  			Expression: "true",
   583  		}},
   584  		expectError: false,
   585  	}, {
   586  		name: "multiple valid match conditions",
   587  		matchConditions: []admissionregistrationv1.MatchCondition{{
   588  			Name:       "exclude-leases",
   589  			Expression: "!(request.resource.group == 'coordination.k8s.io' && request.resource.resource == 'leases')",
   590  		}, {
   591  			Name:       "exclude-kubelet-requests",
   592  			Expression: "!('system:nodes' in request.userInfo.groups)",
   593  		}, {
   594  			Name:       "breakglass",
   595  			Expression: "!authorizer.group('admissionregistration.k8s.io').resource('validatingwebhookconfigurations').name('my-webhook.example.com').check('breakglass').allowed()",
   596  		}},
   597  		expectError: false,
   598  	}, {
   599  		name: "invalid field should error",
   600  		matchConditions: []admissionregistrationv1.MatchCondition{{
   601  			Name:       "old-object-is-null.kubernetes.io",
   602  			Expression: "imnotafield == null",
   603  		}},
   604  		expectError: true,
   605  	}, {
   606  		name: "missing expression should error",
   607  		matchConditions: []admissionregistrationv1.MatchCondition{{
   608  			Name: "old-object-is-null.kubernetes.io",
   609  		}},
   610  		expectError: true,
   611  	}, {
   612  		name: "missing name should error",
   613  		matchConditions: []admissionregistrationv1.MatchCondition{{
   614  			Expression: "oldObject == null",
   615  		}},
   616  		expectError: true,
   617  	}, {
   618  		name: "empty name should error",
   619  		matchConditions: []admissionregistrationv1.MatchCondition{{
   620  			Name:       "",
   621  			Expression: "oldObject == null",
   622  		}},
   623  		expectError: true,
   624  	}, {
   625  		name: "empty expression should error",
   626  		matchConditions: []admissionregistrationv1.MatchCondition{{
   627  			Name:       "test-empty-expression.kubernetes.io",
   628  			Expression: "",
   629  		}},
   630  		expectError: true,
   631  	}, {
   632  		name: "duplicate name should error",
   633  		matchConditions: []admissionregistrationv1.MatchCondition{{
   634  			Name:       "test1",
   635  			Expression: "oldObject == null",
   636  		}, {
   637  			Name:       "test1",
   638  			Expression: "oldObject == null",
   639  		}},
   640  		expectError: true,
   641  	}, {
   642  		name: "name must be qualified name",
   643  		matchConditions: []admissionregistrationv1.MatchCondition{{
   644  			Name:       " test1",
   645  			Expression: "oldObject == null",
   646  		}},
   647  		expectError: true,
   648  	}, {
   649  		name:            "less than 65 match conditions should pass",
   650  		matchConditions: repeatedMatchConditions(64),
   651  		expectError:     false,
   652  	}, {
   653  		name:            "more than 64 match conditions should error",
   654  		matchConditions: repeatedMatchConditions(65),
   655  		expectError:     true,
   656  	},
   657  	}
   658  
   659  	dryRunCreate := metav1.CreateOptions{
   660  		DryRun: []string{metav1.DryRunAll},
   661  	}
   662  	endpoint := "https://localhost:1234/server"
   663  	for _, testcase := range testcases {
   664  		t.Run(testcase.name, func(t *testing.T) {
   665  			rules := []admissionregistrationv1.RuleWithOperations{{
   666  				Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
   667  				Rule: admissionregistrationv1.Rule{
   668  					APIGroups:   []string{""},
   669  					APIVersions: []string{"v1"},
   670  					Resources:   []string{"pods"},
   671  				},
   672  			}}
   673  			clientConfig := admissionregistrationv1.WebhookClientConfig{
   674  				URL:      &endpoint,
   675  				CABundle: localhostCert,
   676  			}
   677  			versions := []string{"v1"}
   678  			validatingwebhook := &admissionregistrationv1.ValidatingWebhookConfiguration{
   679  				ObjectMeta: metav1.ObjectMeta{
   680  					Name: "admission.integration.test",
   681  				},
   682  				Webhooks: []admissionregistrationv1.ValidatingWebhook{
   683  					{
   684  						Name:                    "admission.integration.test",
   685  						Rules:                   rules,
   686  						ClientConfig:            clientConfig,
   687  						SideEffects:             &noSideEffects,
   688  						AdmissionReviewVersions: versions,
   689  						MatchConditions:         testcase.matchConditions,
   690  					},
   691  				},
   692  			}
   693  
   694  			_, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), validatingwebhook, dryRunCreate)
   695  			if testcase.expectError {
   696  				if err == nil {
   697  					t.Fatalf("Expected error creating ValidatingWebhookConfiguration; got nil")
   698  				} else if !apierrors.IsInvalid(err) {
   699  					t.Errorf("Expected Invalid error creating ValidatingWebhookConfiguration; got: %v", err)
   700  				}
   701  			} else if !testcase.expectError && err != nil {
   702  				t.Fatalf("Unexpected error creating ValidatingWebhookConfiguration: %v", err)
   703  			}
   704  
   705  			mutatingwebhook := &admissionregistrationv1.MutatingWebhookConfiguration{
   706  				ObjectMeta: metav1.ObjectMeta{
   707  					Name: "admission.integration.test",
   708  				},
   709  				Webhooks: []admissionregistrationv1.MutatingWebhook{
   710  					{
   711  						Name:                    "admission.integration.test",
   712  						Rules:                   rules,
   713  						ClientConfig:            clientConfig,
   714  						SideEffects:             &noSideEffects,
   715  						AdmissionReviewVersions: versions,
   716  						MatchConditions:         testcase.matchConditions,
   717  					},
   718  				},
   719  			}
   720  
   721  			_, err = client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), mutatingwebhook, dryRunCreate)
   722  			if testcase.expectError {
   723  				if err == nil {
   724  					t.Fatalf("Expected error creating MutatingWebhookConfiguration; got: nil")
   725  				} else if !apierrors.IsInvalid(err) {
   726  					t.Errorf("Expected Invalid error creating MutatingWebhookConfiguration; got: %v", err)
   727  				}
   728  			} else if !testcase.expectError && err != nil {
   729  				t.Fatalf("Unexpected error creating MutatingWebhookConfiguration: %v", err)
   730  			}
   731  		})
   732  	}
   733  }
   734  
   735  func matchConditionsTestPod(name, ns string) *corev1.Pod {
   736  	return &corev1.Pod{
   737  		ObjectMeta: metav1.ObjectMeta{
   738  			Name:      name,
   739  			Namespace: ns,
   740  		},
   741  		Spec: corev1.PodSpec{
   742  			Containers: []corev1.Container{
   743  				{
   744  					Name:  "test",
   745  					Image: "test",
   746  				},
   747  			},
   748  		},
   749  	}
   750  }
   751  
   752  func newMarkerPod(namespace string) *corev1.Pod {
   753  	return &corev1.Pod{
   754  		ObjectMeta: metav1.ObjectMeta{
   755  			Namespace: namespace,
   756  			Name:      "marker",
   757  			Labels: map[string]string{
   758  				"marker": "true",
   759  			},
   760  		},
   761  		Spec: corev1.PodSpec{
   762  			Containers: []corev1.Container{{
   763  				Name:  "fake-name",
   764  				Image: "fakeimage",
   765  			}},
   766  		},
   767  	}
   768  }
   769  
   770  func repeatedMatchConditions(size int) []admissionregistrationv1.MatchCondition {
   771  	matchConditions := make([]admissionregistrationv1.MatchCondition, 0, size)
   772  	for i := 0; i < size; i++ {
   773  		matchConditions = append(matchConditions, admissionregistrationv1.MatchCondition{
   774  			Name:       "repeated-" + strconv.Itoa(i),
   775  			Expression: "true",
   776  		})
   777  	}
   778  	return matchConditions
   779  }
   780  

View as plain text