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 }