k8s.io/kubernetes@v1.29.3/test/integration/apiserver/admissionwebhook/duplicate_owner_ref_test.go (about)

     1  /*
     2  Copyright 2020 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  	"bytes"
    21  	"context"
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"strings"
    30  	"testing"
    31  	"time"
    32  
    33  	v1 "k8s.io/api/admission/v1"
    34  	admissionv1 "k8s.io/api/admissionregistration/v1"
    35  	corev1 "k8s.io/api/core/v1"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/types"
    38  	"k8s.io/apimachinery/pkg/util/uuid"
    39  	"k8s.io/apimachinery/pkg/util/wait"
    40  	"k8s.io/apiserver/pkg/endpoints/handlers"
    41  	clientset "k8s.io/client-go/kubernetes"
    42  	restclient "k8s.io/client-go/rest"
    43  	kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    44  	"k8s.io/kubernetes/test/integration/framework"
    45  )
    46  
    47  // TestMutatingWebhookDuplicateOwnerReferences ensures that the API server
    48  // handler correctly deduplicates owner references if a mutating webhook
    49  // patches create/update requests with duplicate owner references.
    50  func TestMutatingWebhookDuplicateOwnerReferences(t *testing.T) {
    51  	roots := x509.NewCertPool()
    52  	if !roots.AppendCertsFromPEM(localhostCert) {
    53  		t.Fatal("Failed to append Cert from PEM")
    54  	}
    55  	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
    56  	if err != nil {
    57  		t.Fatalf("Failed to build cert with error: %+v", err)
    58  	}
    59  
    60  	webhookServer := httptest.NewUnstartedServer(newDuplicateOwnerReferencesWebhookHandler(t))
    61  	webhookServer.TLS = &tls.Config{
    62  		RootCAs:      roots,
    63  		Certificates: []tls.Certificate{cert},
    64  	}
    65  	webhookServer.StartTLS()
    66  	defer webhookServer.Close()
    67  
    68  	s := kubeapiservertesting.StartTestServerOrDie(t,
    69  		kubeapiservertesting.NewDefaultTestServerOptions(), []string{
    70  			"--disable-admission-plugins=ServiceAccount",
    71  		}, framework.SharedEtcd())
    72  	defer s.TearDownFn()
    73  
    74  	b := &bytes.Buffer{}
    75  	warningWriter := restclient.NewWarningWriter(b, restclient.WarningWriterOptions{})
    76  	s.ClientConfig.WarningHandler = warningWriter
    77  	client := clientset.NewForConfigOrDie(s.ClientConfig)
    78  	if _, err := client.CoreV1().Pods("default").Create(
    79  		context.TODO(), duplicateOwnerReferencesMarkerFixture, metav1.CreateOptions{}); err != nil {
    80  		t.Fatal(err)
    81  	}
    82  
    83  	fail := admissionv1.Fail
    84  	none := admissionv1.SideEffectClassNone
    85  	mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1.MutatingWebhookConfiguration{
    86  		ObjectMeta: metav1.ObjectMeta{Name: "dup-owner-references.admission.integration.test"},
    87  		Webhooks: []admissionv1.MutatingWebhook{{
    88  			Name: "dup-owner-references.admission.integration.test",
    89  			ClientConfig: admissionv1.WebhookClientConfig{
    90  				URL:      &webhookServer.URL,
    91  				CABundle: localhostCert,
    92  			},
    93  			Rules: []admissionv1.RuleWithOperations{{
    94  				Operations: []admissionv1.OperationType{admissionv1.Create, admissionv1.Update},
    95  				Rule:       admissionv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
    96  			}},
    97  			FailurePolicy:           &fail,
    98  			AdmissionReviewVersions: []string{"v1", "v1beta1"},
    99  			SideEffects:             &none,
   100  		}},
   101  	}, metav1.CreateOptions{})
   102  	if err != nil {
   103  		t.Fatal(err)
   104  	}
   105  	defer func() {
   106  		err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{})
   107  		if err != nil {
   108  			t.Fatal(err)
   109  		}
   110  	}()
   111  
   112  	// Make sure dedup happens in patch requests
   113  	var pod *corev1.Pod
   114  	var lastErr string
   115  	// wait until new webhook is called
   116  	expectedWarning := fmt.Sprintf(handlers.DuplicateOwnerReferencesAfterMutatingAdmissionWarningFormat,
   117  		duplicateOwnerReferencesMarkerFixture.OwnerReferences[0].UID)
   118  	if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
   119  		pod, err = client.CoreV1().Pods("default").Patch(context.TODO(), duplicateOwnerReferencesMarkerFixture.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
   120  		if err != nil {
   121  			return false, err
   122  		}
   123  		if warningWriter.WarningCount() == 0 {
   124  			lastErr = fmt.Sprintf("no warning, owner references: %v", pod.OwnerReferences)
   125  			return false, nil
   126  		}
   127  		if !strings.Contains(b.String(), expectedWarning) {
   128  			lastErr = fmt.Sprintf("unexpected warning, expected: %v, got: %v",
   129  				expectedWarning, b.String())
   130  			return false, nil
   131  		}
   132  		if len(pod.OwnerReferences) != 1 {
   133  			lastErr = fmt.Sprintf("unexpected owner references, expected one entry, got: %v",
   134  				pod.OwnerReferences)
   135  			return false, nil
   136  		}
   137  		return true, nil
   138  	}); err != nil {
   139  		t.Fatalf("failed to wait for apiserver handling webhook mutation: %v, last error: %v", err, lastErr)
   140  	}
   141  	if strings.Contains(b.String(), ".metadata.ownerReferences contains duplicate entries,") {
   142  		t.Errorf("unexpected warning happened before mutating admission")
   143  	}
   144  	if warningWriter.WarningCount() != 1 {
   145  		t.Errorf("expected one warning, got: %v", warningWriter.WarningCount())
   146  	}
   147  	b.Reset()
   148  
   149  	// Make sure dedup happens in update requests
   150  	pod, err = client.CoreV1().Pods("default").Update(context.TODO(), pod, metav1.UpdateOptions{})
   151  	if err != nil {
   152  		t.Fatal(err)
   153  	}
   154  	if warningWriter.WarningCount() != 2 {
   155  		t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount())
   156  	}
   157  	if !strings.Contains(b.String(), expectedWarning) {
   158  		t.Errorf("unexpected warning, expected: %v, got: %v",
   159  			expectedWarning, b.String())
   160  	}
   161  	if strings.Contains(b.String(), ".metadata.ownerReferences contains duplicate entries,") {
   162  		t.Errorf("unexpected warning happened before mutating admission")
   163  	}
   164  	b.Reset()
   165  
   166  	if err := client.CoreV1().Pods("default").Delete(context.TODO(), duplicateOwnerReferencesMarkerFixture.Name, metav1.DeleteOptions{}); err != nil {
   167  		t.Fatalf("failed to delete marker pod: %v", err)
   168  	}
   169  	// expect no more warning
   170  	if warningWriter.WarningCount() != 2 {
   171  		t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount())
   172  	}
   173  
   174  }
   175  
   176  func newDuplicateOwnerReferencesWebhookHandler(t *testing.T) http.Handler {
   177  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   178  		defer r.Body.Close()
   179  		data, err := io.ReadAll(r.Body)
   180  		if err != nil {
   181  			http.Error(w, err.Error(), http.StatusBadRequest)
   182  		}
   183  		review := v1.AdmissionReview{}
   184  		if err := json.Unmarshal(data, &review); err != nil {
   185  			http.Error(w, err.Error(), http.StatusBadRequest)
   186  		}
   187  
   188  		if len(review.Request.Object.Raw) == 0 {
   189  			http.Error(w, err.Error(), http.StatusBadRequest)
   190  			return
   191  		}
   192  		pod := &corev1.Pod{}
   193  		if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil {
   194  			http.Error(w, err.Error(), http.StatusBadRequest)
   195  			return
   196  		}
   197  
   198  		review.Response = &v1.AdmissionResponse{
   199  			Allowed: true,
   200  			UID:     review.Request.UID,
   201  			Result:  &metav1.Status{Message: "admitted"},
   202  		}
   203  		if len(pod.OwnerReferences) > 0 {
   204  			review.Response.Patch = []byte(fmt.Sprintf(`[{"op":"add","path":"/metadata/ownerReferences/-","value":{"apiVersion":"v1", "kind": "Node", "name": "fake-node", "uid": "%v"}}]`, pod.OwnerReferences[0].UID))
   205  			jsonPatch := v1.PatchTypeJSONPatch
   206  			review.Response.PatchType = &jsonPatch
   207  		}
   208  
   209  		w.Header().Set("Content-Type", "application/json")
   210  		if err := json.NewEncoder(w).Encode(review); err != nil {
   211  			t.Errorf("Marshal of response failed with error: %v", err)
   212  		}
   213  	})
   214  }
   215  
   216  var duplicateOwnerReferencesMarkerFixture = &corev1.Pod{
   217  	ObjectMeta: metav1.ObjectMeta{
   218  		Namespace: "default",
   219  		Name:      "duplicate-owner-references-test-marker",
   220  		OwnerReferences: []metav1.OwnerReference{{
   221  			APIVersion: "v1",
   222  			Kind:       "Node",
   223  			Name:       "fake-node",
   224  			UID:        uuid.NewUUID(),
   225  		}},
   226  	},
   227  	Spec: corev1.PodSpec{
   228  		Containers: []corev1.Container{{
   229  			Name:  "fake-name",
   230  			Image: "fakeimage",
   231  		}},
   232  	},
   233  }