k8s.io/kubernetes@v1.29.3/test/integration/apiserver/admissionwebhook/invalid_managedFields_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 v1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" 38 "k8s.io/apimachinery/pkg/types" 39 "k8s.io/apimachinery/pkg/util/managedfields" 40 "k8s.io/apimachinery/pkg/util/validation/field" 41 "k8s.io/apimachinery/pkg/util/wait" 42 "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" 43 clientset "k8s.io/client-go/kubernetes" 44 restclient "k8s.io/client-go/rest" 45 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 46 "k8s.io/kubernetes/test/integration/framework" 47 ) 48 49 // TestMutatingWebhookResetsInvalidManagedFields ensures that the API server 50 // resets managedFields to their state before admission if a mutating webhook 51 // patches create/update requests with invalid managedFields. 52 func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { 53 roots := x509.NewCertPool() 54 if !roots.AppendCertsFromPEM(localhostCert) { 55 t.Fatal("Failed to append Cert from PEM") 56 } 57 cert, err := tls.X509KeyPair(localhostCert, localhostKey) 58 if err != nil { 59 t.Fatalf("Failed to build cert with error: %+v", err) 60 } 61 62 webhookServer := httptest.NewUnstartedServer(newInvalidManagedFieldsWebhookHandler(t)) 63 webhookServer.TLS = &tls.Config{ 64 RootCAs: roots, 65 Certificates: []tls.Certificate{cert}, 66 } 67 webhookServer.StartTLS() 68 defer webhookServer.Close() 69 70 s := kubeapiservertesting.StartTestServerOrDie(t, 71 kubeapiservertesting.NewDefaultTestServerOptions(), []string{ 72 "--disable-admission-plugins=ServiceAccount", 73 }, framework.SharedEtcd()) 74 defer s.TearDownFn() 75 76 recordedWarnings := &bytes.Buffer{} 77 warningWriter := restclient.NewWarningWriter(recordedWarnings, restclient.WarningWriterOptions{}) 78 s.ClientConfig.WarningHandler = warningWriter 79 client := clientset.NewForConfigOrDie(s.ClientConfig) 80 81 if _, err := client.CoreV1().Pods("default").Create( 82 context.TODO(), invalidManagedFieldsMarkerFixture, metav1.CreateOptions{}); err != nil { 83 t.Fatal(err) 84 } 85 // make sure we delete the pod even on a failed test 86 defer func() { 87 if err := client.CoreV1().Pods("default").Delete(context.TODO(), invalidManagedFieldsMarkerFixture.Name, metav1.DeleteOptions{}); err != nil { 88 t.Fatalf("failed to delete marker pod: %v", err) 89 } 90 }() 91 92 fail := admissionv1.Fail 93 none := admissionv1.SideEffectClassNone 94 mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1.MutatingWebhookConfiguration{ 95 ObjectMeta: metav1.ObjectMeta{Name: "invalid-managedfields.admission.integration.test"}, 96 Webhooks: []admissionv1.MutatingWebhook{{ 97 Name: "invalid-managedfields.admission.integration.test", 98 ClientConfig: admissionv1.WebhookClientConfig{ 99 URL: &webhookServer.URL, 100 CABundle: localhostCert, 101 }, 102 Rules: []admissionv1.RuleWithOperations{{ 103 Operations: []admissionv1.OperationType{admissionv1.Create, admissionv1.Update}, 104 Rule: admissionv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}}, 105 }}, 106 FailurePolicy: &fail, 107 AdmissionReviewVersions: []string{"v1", "v1beta1"}, 108 SideEffects: &none, 109 }}, 110 }, metav1.CreateOptions{}) 111 if err != nil { 112 t.Fatal(err) 113 } 114 defer func() { 115 err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{}) 116 if err != nil { 117 t.Fatal(err) 118 } 119 }() 120 121 var pod *corev1.Pod 122 var lastErr error 123 // We just expect the general warning text as the detail order might change 124 expectedWarning := fmt.Sprintf(fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, "") 125 126 // Make sure reset happens on patch requests 127 // wait until new webhook is called 128 if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) { 129 defer recordedWarnings.Reset() 130 pod, err = client.CoreV1().Pods("default").Patch(context.TODO(), invalidManagedFieldsMarkerFixture.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{}) 131 if err != nil { 132 return false, err 133 } 134 if err := validateManagedFieldsAndDecode(pod.ManagedFields); err != nil { 135 lastErr = err 136 return false, nil 137 } 138 if warningWriter.WarningCount() != 1 { 139 lastErr = fmt.Errorf("expected one warning, got: %v", warningWriter.WarningCount()) 140 return false, nil 141 } 142 if !strings.Contains(recordedWarnings.String(), expectedWarning) { 143 lastErr = fmt.Errorf("unexpected warning, expected: \n%v\n, got: \n%v", 144 expectedWarning, recordedWarnings.String()) 145 return false, nil 146 } 147 lastErr = nil 148 return true, nil 149 }); err != nil || lastErr != nil { 150 t.Fatalf("failed to wait for apiserver handling webhook mutation: %v, last error: %v", err, lastErr) 151 } 152 153 // Make sure reset happens in update requests 154 pod, err = client.CoreV1().Pods("default").Update(context.TODO(), pod, metav1.UpdateOptions{}) 155 if err != nil { 156 t.Fatal(err) 157 } 158 if err := validateManagedFieldsAndDecode(pod.ManagedFields); err != nil { 159 t.Error(err) 160 } 161 if warningWriter.WarningCount() != 2 { 162 t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount()) 163 } 164 if !strings.Contains(recordedWarnings.String(), expectedWarning) { 165 t.Errorf("unexpected warning, expected: \n%v\n, got: \n%v", 166 expectedWarning, recordedWarnings.String()) 167 } 168 } 169 170 // validate against both decoding and validation to make sure we use the hardest rule between the both to reset 171 // with decoding being as strict as it gets, only using it should be enough in admission 172 func validateManagedFieldsAndDecode(managedFields []metav1.ManagedFieldsEntry) error { 173 if err := managedfields.ValidateManagedFields(managedFields); err != nil { 174 return err 175 176 } 177 validationErrs := v1validation.ValidateManagedFields(managedFields, field.NewPath("metadata").Child("managedFields")) 178 return validationErrs.ToAggregate() 179 } 180 181 func newInvalidManagedFieldsWebhookHandler(t *testing.T) http.Handler { 182 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 183 defer r.Body.Close() 184 data, err := io.ReadAll(r.Body) 185 if err != nil { 186 http.Error(w, err.Error(), http.StatusBadRequest) 187 } 188 review := v1.AdmissionReview{} 189 if err := json.Unmarshal(data, &review); err != nil { 190 http.Error(w, err.Error(), http.StatusBadRequest) 191 } 192 193 if len(review.Request.Object.Raw) == 0 { 194 http.Error(w, err.Error(), http.StatusBadRequest) 195 return 196 } 197 pod := &corev1.Pod{} 198 if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil { 199 http.Error(w, err.Error(), http.StatusBadRequest) 200 return 201 } 202 203 review.Response = &v1.AdmissionResponse{ 204 Allowed: true, 205 UID: review.Request.UID, 206 Result: &metav1.Status{Message: "admitted"}, 207 } 208 209 if len(pod.ManagedFields) != 0 { 210 t.Logf("corrupting managedFields %v", pod.ManagedFields) 211 review.Response.Patch = []byte(`[ 212 {"op":"remove","path":"/metadata/managedFields/0/apiVersion"}, 213 {"op":"remove","path":"/metadata/managedFields/0/fieldsV1"}, 214 {"op":"remove","path":"/metadata/managedFields/0/fieldsType"} 215 ]`) 216 jsonPatch := v1.PatchTypeJSONPatch 217 review.Response.PatchType = &jsonPatch 218 } 219 220 w.Header().Set("Content-Type", "application/json") 221 if err := json.NewEncoder(w).Encode(review); err != nil { 222 t.Errorf("Marshal of response failed with error: %v", err) 223 } 224 }) 225 } 226 227 var invalidManagedFieldsMarkerFixture = &corev1.Pod{ 228 ObjectMeta: metav1.ObjectMeta{ 229 Namespace: "default", 230 Name: "invalid-managedfields-test-marker", 231 }, 232 Spec: corev1.PodSpec{ 233 Containers: []corev1.Container{{ 234 Name: "fake-name", 235 Image: "fakeimage", 236 }}, 237 }, 238 }