/* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package admissionwebhook import ( "bytes" "context" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" v1 "k8s.io/api/admission/v1" admissionv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/endpoints/handlers" clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" "k8s.io/kubernetes/test/integration/framework" ) // TestMutatingWebhookDuplicateOwnerReferences ensures that the API server // handler correctly deduplicates owner references if a mutating webhook // patches create/update requests with duplicate owner references. func TestMutatingWebhookDuplicateOwnerReferences(t *testing.T) { roots := x509.NewCertPool() if !roots.AppendCertsFromPEM(localhostCert) { t.Fatal("Failed to append Cert from PEM") } cert, err := tls.X509KeyPair(localhostCert, localhostKey) if err != nil { t.Fatalf("Failed to build cert with error: %+v", err) } webhookServer := httptest.NewUnstartedServer(newDuplicateOwnerReferencesWebhookHandler(t)) webhookServer.TLS = &tls.Config{ RootCAs: roots, Certificates: []tls.Certificate{cert}, } webhookServer.StartTLS() defer webhookServer.Close() s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{ "--disable-admission-plugins=ServiceAccount", }, framework.SharedEtcd()) defer s.TearDownFn() b := &bytes.Buffer{} warningWriter := restclient.NewWarningWriter(b, restclient.WarningWriterOptions{}) s.ClientConfig.WarningHandler = warningWriter client := clientset.NewForConfigOrDie(s.ClientConfig) if _, err := client.CoreV1().Pods("default").Create( context.TODO(), duplicateOwnerReferencesMarkerFixture, metav1.CreateOptions{}); err != nil { t.Fatal(err) } fail := admissionv1.Fail none := admissionv1.SideEffectClassNone mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1.MutatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{Name: "dup-owner-references.admission.integration.test"}, Webhooks: []admissionv1.MutatingWebhook{{ Name: "dup-owner-references.admission.integration.test", ClientConfig: admissionv1.WebhookClientConfig{ URL: &webhookServer.URL, CABundle: localhostCert, }, Rules: []admissionv1.RuleWithOperations{{ Operations: []admissionv1.OperationType{admissionv1.Create, admissionv1.Update}, Rule: admissionv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}}, }}, FailurePolicy: &fail, AdmissionReviewVersions: []string{"v1", "v1beta1"}, SideEffects: &none, }}, }, metav1.CreateOptions{}) if err != nil { t.Fatal(err) } defer func() { err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{}) if err != nil { t.Fatal(err) } }() // Make sure dedup happens in patch requests var pod *corev1.Pod var lastErr string // wait until new webhook is called expectedWarning := fmt.Sprintf(handlers.DuplicateOwnerReferencesAfterMutatingAdmissionWarningFormat, duplicateOwnerReferencesMarkerFixture.OwnerReferences[0].UID) if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) { pod, err = client.CoreV1().Pods("default").Patch(context.TODO(), duplicateOwnerReferencesMarkerFixture.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{}) if err != nil { return false, err } if warningWriter.WarningCount() == 0 { lastErr = fmt.Sprintf("no warning, owner references: %v", pod.OwnerReferences) return false, nil } if !strings.Contains(b.String(), expectedWarning) { lastErr = fmt.Sprintf("unexpected warning, expected: %v, got: %v", expectedWarning, b.String()) return false, nil } if len(pod.OwnerReferences) != 1 { lastErr = fmt.Sprintf("unexpected owner references, expected one entry, got: %v", pod.OwnerReferences) return false, nil } return true, nil }); err != nil { t.Fatalf("failed to wait for apiserver handling webhook mutation: %v, last error: %v", err, lastErr) } if strings.Contains(b.String(), ".metadata.ownerReferences contains duplicate entries,") { t.Errorf("unexpected warning happened before mutating admission") } if warningWriter.WarningCount() != 1 { t.Errorf("expected one warning, got: %v", warningWriter.WarningCount()) } b.Reset() // Make sure dedup happens in update requests pod, err = client.CoreV1().Pods("default").Update(context.TODO(), pod, metav1.UpdateOptions{}) if err != nil { t.Fatal(err) } if warningWriter.WarningCount() != 2 { t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount()) } if !strings.Contains(b.String(), expectedWarning) { t.Errorf("unexpected warning, expected: %v, got: %v", expectedWarning, b.String()) } if strings.Contains(b.String(), ".metadata.ownerReferences contains duplicate entries,") { t.Errorf("unexpected warning happened before mutating admission") } b.Reset() if err := client.CoreV1().Pods("default").Delete(context.TODO(), duplicateOwnerReferencesMarkerFixture.Name, metav1.DeleteOptions{}); err != nil { t.Fatalf("failed to delete marker pod: %v", err) } // expect no more warning if warningWriter.WarningCount() != 2 { t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount()) } } func newDuplicateOwnerReferencesWebhookHandler(t *testing.T) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() data, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } review := v1.AdmissionReview{} if err := json.Unmarshal(data, &review); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) } if len(review.Request.Object.Raw) == 0 { http.Error(w, err.Error(), http.StatusBadRequest) return } pod := &corev1.Pod{} if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } review.Response = &v1.AdmissionResponse{ Allowed: true, UID: review.Request.UID, Result: &metav1.Status{Message: "admitted"}, } if len(pod.OwnerReferences) > 0 { 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)) jsonPatch := v1.PatchTypeJSONPatch review.Response.PatchType = &jsonPatch } w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(review); err != nil { t.Errorf("Marshal of response failed with error: %v", err) } }) } var duplicateOwnerReferencesMarkerFixture = &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "duplicate-owner-references-test-marker", OwnerReferences: []metav1.OwnerReference{{ APIVersion: "v1", Kind: "Node", Name: "fake-node", UID: uuid.NewUUID(), }}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Name: "fake-name", Image: "fakeimage", }}, }, }