1
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
50
51
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
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
124 expectedWarning := fmt.Sprintf(fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, "")
125
126
127
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
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
171
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 }
239
View as plain text