...

Source file src/k8s.io/kubernetes/test/integration/apiserver/admissionwebhook/mutating_webhook_gvk_conversion_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  	"sync"
    28  	"testing"
    29  	"time"
    30  
    31  	admissionv1 "k8s.io/api/admission/v1"
    32  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    33  	corev1 "k8s.io/api/core/v1"
    34  	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    35  	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
    36  	"k8s.io/apiextensions-apiserver/test/integration/fixtures"
    37  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    38  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    39  	"k8s.io/apimachinery/pkg/runtime"
    40  	"k8s.io/apimachinery/pkg/runtime/schema"
    41  	"k8s.io/apimachinery/pkg/runtime/serializer"
    42  	"k8s.io/apimachinery/pkg/types"
    43  	"k8s.io/apimachinery/pkg/util/wait"
    44  	"k8s.io/client-go/dynamic"
    45  	clientset "k8s.io/client-go/kubernetes"
    46  	apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    47  	"k8s.io/kubernetes/test/integration/etcd"
    48  	"k8s.io/kubernetes/test/integration/framework"
    49  )
    50  
    51  var (
    52  	runtimeSchemeGVKTest = runtime.NewScheme()
    53  	codecFactoryGVKTest  = serializer.NewCodecFactory(runtimeSchemeGVKTest)
    54  	deserializerGVKTest  = codecFactoryGVKTest.UniversalDeserializer()
    55  )
    56  
    57  type admissionTypeChecker struct {
    58  	mu       sync.Mutex
    59  	upCh     chan struct{}
    60  	upOnce   sync.Once
    61  	requests []*admissionv1.AdmissionRequest
    62  }
    63  
    64  func (r *admissionTypeChecker) Reset() chan struct{} {
    65  	r.mu.Lock()
    66  	defer r.mu.Unlock()
    67  	r.upCh = make(chan struct{})
    68  	r.upOnce = sync.Once{}
    69  	r.requests = []*admissionv1.AdmissionRequest{}
    70  	return r.upCh
    71  }
    72  
    73  func (r *admissionTypeChecker) TypeCheck(req *admissionv1.AdmissionRequest, version string) *admissionv1.AdmissionResponse {
    74  	r.mu.Lock()
    75  	defer r.mu.Unlock()
    76  	r.requests = append(r.requests, req)
    77  	raw := req.Object.Raw
    78  	var into runtime.Object
    79  	if _, gvk, err := deserializerGVKTest.Decode(raw, nil, into); err != nil {
    80  		if gvk.Version != version {
    81  			return &admissionv1.AdmissionResponse{
    82  				UID:     req.UID,
    83  				Allowed: false,
    84  			}
    85  		}
    86  	}
    87  
    88  	return &admissionv1.AdmissionResponse{
    89  		UID:     req.UID,
    90  		Allowed: true,
    91  	}
    92  }
    93  
    94  func (r *admissionTypeChecker) MarkerReceived() {
    95  	r.mu.Lock()
    96  	defer r.mu.Unlock()
    97  	r.upOnce.Do(func() {
    98  		close(r.upCh)
    99  	})
   100  }
   101  
   102  func newAdmissionTypeCheckerHandler(recorder *admissionTypeChecker) http.Handler {
   103  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   104  		defer r.Body.Close()
   105  		data, err := io.ReadAll(r.Body)
   106  		if err != nil {
   107  			http.Error(w, err.Error(), 400)
   108  		}
   109  		review := admissionv1.AdmissionReview{}
   110  		if err := json.Unmarshal(data, &review); err != nil {
   111  			http.Error(w, err.Error(), 400)
   112  		}
   113  
   114  		switch r.URL.Path {
   115  		case "/marker":
   116  			recorder.MarkerReceived()
   117  			return
   118  		case "/v1":
   119  			review.Response = recorder.TypeCheck(review.Request, "v1")
   120  		case "/v2":
   121  			review.Response = recorder.TypeCheck(review.Request, "v2")
   122  		}
   123  
   124  		w.Header().Set("Content-Type", "application/json")
   125  		if err := json.NewEncoder(w).Encode(review); err != nil {
   126  			http.Error(w, err.Error(), 400)
   127  			return
   128  		}
   129  
   130  	})
   131  }
   132  
   133  // Test_MutatingWebhookConvertsGVKWithMatchPolicyEquivalent tests if a equivalent resource is properly converted between mutating webhooks
   134  func Test_MutatingWebhookConvertsGVKWithMatchPolicyEquivalent(t *testing.T) {
   135  
   136  	roots := x509.NewCertPool()
   137  	if !roots.AppendCertsFromPEM(localhostCert) {
   138  		t.Fatal("Failed to append Cert from PEM")
   139  	}
   140  	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
   141  	if err != nil {
   142  		t.Fatalf("Failed to build cert with error: %+v", err)
   143  	}
   144  
   145  	typeChecker := &admissionTypeChecker{}
   146  
   147  	webhookServer := httptest.NewUnstartedServer(newAdmissionTypeCheckerHandler(typeChecker))
   148  	webhookServer.TLS = &tls.Config{
   149  		RootCAs:      roots,
   150  		Certificates: []tls.Certificate{cert},
   151  	}
   152  	webhookServer.StartTLS()
   153  	defer webhookServer.Close()
   154  
   155  	upCh := typeChecker.Reset()
   156  	server, err := apiservertesting.StartTestServer(t, nil, []string{
   157  		"--disable-admission-plugins=ServiceAccount",
   158  	}, framework.SharedEtcd())
   159  	if err != nil {
   160  		t.Fatal(err)
   161  	}
   162  	defer server.TearDownFn()
   163  
   164  	etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, versionedCustomResourceDefinition())
   165  	if err != nil {
   166  		t.Fatal(err)
   167  	}
   168  
   169  	config := server.ClientConfig
   170  
   171  	client, err := clientset.NewForConfig(config)
   172  	if err != nil {
   173  		t.Fatal(err)
   174  	}
   175  
   176  	// Write markers to a separate namespace to avoid cross-talk
   177  	markerNs := "marker"
   178  	_, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: markerNs}}, metav1.CreateOptions{})
   179  	if err != nil {
   180  		t.Fatal(err)
   181  	}
   182  
   183  	// Create a marker object to use to check for the webhook configurations to be ready.
   184  	marker, err := client.CoreV1().Pods(markerNs).Create(context.TODO(), newMarkerPodGVKConversion(markerNs), metav1.CreateOptions{})
   185  	if err != nil {
   186  		t.Fatal(err)
   187  	}
   188  
   189  	equivalent := admissionregistrationv1.Equivalent
   190  	ignore := admissionregistrationv1.Ignore
   191  
   192  	v1Endpoint := webhookServer.URL + "/v1"
   193  	markerEndpoint := webhookServer.URL + "/marker"
   194  	v2Endpoint := webhookServer.URL + "/v2"
   195  	mutatingWebhook := &admissionregistrationv1.MutatingWebhookConfiguration{
   196  		ObjectMeta: metav1.ObjectMeta{
   197  			Name: "admission.integration.test",
   198  		},
   199  		Webhooks: []admissionregistrationv1.MutatingWebhook{
   200  			{
   201  				Name: "admission.integration.test.v2",
   202  				Rules: []admissionregistrationv1.RuleWithOperations{{
   203  					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
   204  					Rule: admissionregistrationv1.Rule{
   205  						APIGroups:   []string{"awesome.example.com"},
   206  						APIVersions: []string{"v2"},
   207  						Resources:   []string{"*/*"},
   208  					},
   209  				}},
   210  				MatchPolicy: &equivalent,
   211  				ClientConfig: admissionregistrationv1.WebhookClientConfig{
   212  					URL:      &v2Endpoint,
   213  					CABundle: localhostCert,
   214  				},
   215  				FailurePolicy:           &ignore,
   216  				SideEffects:             &noSideEffects,
   217  				AdmissionReviewVersions: []string{"v1"},
   218  				MatchConditions: []admissionregistrationv1.MatchCondition{
   219  					{
   220  						Name:       "test-v2",
   221  						Expression: "object.apiVersion == 'awesome.example.com/v2'",
   222  					},
   223  				},
   224  			},
   225  			{
   226  				Name: "admission.integration.test",
   227  				Rules: []admissionregistrationv1.RuleWithOperations{{
   228  					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
   229  					Rule: admissionregistrationv1.Rule{
   230  						APIGroups:   []string{"awesome.example.com"},
   231  						APIVersions: []string{"v1"},
   232  						Resources:   []string{"*/*"},
   233  					},
   234  				}},
   235  				MatchPolicy: &equivalent,
   236  				ClientConfig: admissionregistrationv1.WebhookClientConfig{
   237  					URL:      &v1Endpoint,
   238  					CABundle: localhostCert,
   239  				},
   240  				SideEffects:             &noSideEffects,
   241  				AdmissionReviewVersions: []string{"v1"},
   242  				MatchConditions: []admissionregistrationv1.MatchCondition{
   243  					{
   244  						Name:       "test-v1",
   245  						Expression: "object.apiVersion == 'awesome.example.com/v1'",
   246  					},
   247  				},
   248  			},
   249  			{
   250  				Name: "admission.integration.test.marker",
   251  				Rules: []admissionregistrationv1.RuleWithOperations{{
   252  					Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
   253  					Rule:       admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
   254  				}},
   255  				ClientConfig: admissionregistrationv1.WebhookClientConfig{
   256  					URL:      &markerEndpoint,
   257  					CABundle: localhostCert,
   258  				},
   259  				NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{
   260  					corev1.LabelMetadataName: "marker",
   261  				}},
   262  				ObjectSelector:          &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}},
   263  				SideEffects:             &noSideEffects,
   264  				AdmissionReviewVersions: []string{"v1"},
   265  			},
   266  		},
   267  	}
   268  
   269  	mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), mutatingWebhook, metav1.CreateOptions{})
   270  	if err != nil {
   271  		t.Fatal(err)
   272  	}
   273  	defer func() {
   274  		err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{})
   275  		if err != nil {
   276  			t.Fatal(err)
   277  		}
   278  	}()
   279  
   280  	// wait until new webhook is called the first time
   281  	if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
   282  		_, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
   283  		select {
   284  		case <-upCh:
   285  			return true, nil
   286  		default:
   287  			t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
   288  			return false, nil
   289  		}
   290  	}); err != nil {
   291  		t.Fatal(err)
   292  	}
   293  	dynamicClient, err := dynamic.NewForConfig(config)
   294  	if err != nil {
   295  		t.Fatal(err)
   296  	}
   297  
   298  	v1Resource := &unstructured.Unstructured{
   299  		Object: map[string]interface{}{
   300  			"apiVersion": "awesome.example.com" + "/" + "v1",
   301  			"kind":       "Panda",
   302  			"metadata": map[string]interface{}{
   303  				"name": "v1-bears",
   304  			},
   305  		},
   306  	}
   307  
   308  	v2Resource := &unstructured.Unstructured{
   309  		Object: map[string]interface{}{
   310  			"apiVersion": "awesome.example.com" + "/" + "v2",
   311  			"kind":       "Panda",
   312  			"metadata": map[string]interface{}{
   313  				"name": "v2-bears",
   314  			},
   315  		},
   316  	}
   317  
   318  	_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.example.com", Version: "v1", Resource: "pandas"}).Create(context.TODO(), v1Resource, metav1.CreateOptions{})
   319  	if err != nil {
   320  		t.Errorf("error1 %v", err.Error())
   321  	}
   322  
   323  	_, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.example.com", Version: "v2", Resource: "pandas"}).Create(context.TODO(), v2Resource, metav1.CreateOptions{})
   324  	if err != nil {
   325  		t.Errorf("error2 %v", err.Error())
   326  	}
   327  
   328  	if len(typeChecker.requests) != 4 {
   329  		t.Errorf("expected 4 request got %v", len(typeChecker.requests))
   330  	}
   331  }
   332  
   333  func newMarkerPodGVKConversion(namespace string) *corev1.Pod {
   334  	return &corev1.Pod{
   335  		ObjectMeta: metav1.ObjectMeta{
   336  			Namespace: namespace,
   337  			Name:      "marker",
   338  			Labels: map[string]string{
   339  				"marker": "true",
   340  			},
   341  		},
   342  		Spec: corev1.PodSpec{
   343  			Containers: []corev1.Container{{
   344  				Name:  "fake-name",
   345  				Image: "fakeimage",
   346  			}},
   347  		},
   348  	}
   349  }
   350  
   351  // Copied from etcd.GetCustomResourceDefinitionData
   352  func versionedCustomResourceDefinition() *apiextensionsv1.CustomResourceDefinition {
   353  	return &apiextensionsv1.CustomResourceDefinition{
   354  		ObjectMeta: metav1.ObjectMeta{
   355  			Name: "pandas.awesome.example.com",
   356  		},
   357  		Spec: apiextensionsv1.CustomResourceDefinitionSpec{
   358  			Group: "awesome.example.com",
   359  			Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
   360  				{
   361  					Name:    "v1",
   362  					Served:  true,
   363  					Storage: true,
   364  					Schema:  fixtures.AllowAllSchema(),
   365  					Subresources: &apiextensionsv1.CustomResourceSubresources{
   366  						Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
   367  						Scale: &apiextensionsv1.CustomResourceSubresourceScale{
   368  							SpecReplicasPath:   ".spec.replicas",
   369  							StatusReplicasPath: ".status.replicas",
   370  							LabelSelectorPath:  func() *string { path := ".status.selector"; return &path }(),
   371  						},
   372  					},
   373  				},
   374  				{
   375  					Name:    "v2",
   376  					Served:  true,
   377  					Storage: false,
   378  					Schema:  fixtures.AllowAllSchema(),
   379  					Subresources: &apiextensionsv1.CustomResourceSubresources{
   380  						Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
   381  						Scale: &apiextensionsv1.CustomResourceSubresourceScale{
   382  							SpecReplicasPath:   ".spec.replicas",
   383  							StatusReplicasPath: ".status.replicas",
   384  							LabelSelectorPath:  func() *string { path := ".status.selector"; return &path }(),
   385  						},
   386  					},
   387  				},
   388  			},
   389  			Scope: apiextensionsv1.ClusterScoped,
   390  			Names: apiextensionsv1.CustomResourceDefinitionNames{
   391  				Plural: "pandas",
   392  				Kind:   "Panda",
   393  			},
   394  		},
   395  	}
   396  }
   397  

View as plain text