1
16
17 package admissionwebhook
18
19 import (
20 "context"
21 "crypto/tls"
22 "crypto/x509"
23 "encoding/json"
24 "fmt"
25 "io"
26 "net/http"
27 "net/http/httptest"
28 "os"
29 "reflect"
30 "strconv"
31 "strings"
32 "sync"
33 "testing"
34 "time"
35
36 "k8s.io/api/admission/v1beta1"
37 admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
38 corev1 "k8s.io/api/core/v1"
39 schedulingv1 "k8s.io/api/scheduling/v1"
40 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
41 "k8s.io/apimachinery/pkg/types"
42 "k8s.io/apimachinery/pkg/util/wait"
43 auditinternal "k8s.io/apiserver/pkg/apis/audit"
44 auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
45 clientset "k8s.io/client-go/kubernetes"
46 "k8s.io/client-go/rest"
47 utiltesting "k8s.io/client-go/util/testing"
48 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
49 "k8s.io/kubernetes/test/integration/framework"
50 "k8s.io/kubernetes/test/utils"
51 )
52
53 const (
54 testReinvocationClientUsername = "webhook-reinvocation-integration-client"
55 auditPolicy = `
56 apiVersion: audit.k8s.io/v1
57 kind: Policy
58 rules:
59 - level: Request
60 resources:
61 - group: "" # core
62 resources: ["pods"]
63 `
64 )
65
66
67 func TestWebhookReinvocationPolicyWithWatchCache(t *testing.T) {
68 testWebhookReinvocationPolicy(t, true)
69 }
70
71
72 func TestWebhookReinvocationPolicyWithoutWatchCache(t *testing.T) {
73 testWebhookReinvocationPolicy(t, false)
74 }
75
76 func mutationAnnotationValue(configuration, webhook string, mutated bool) string {
77 return fmt.Sprintf(`{"configuration":"%s","webhook":"%s","mutated":%t}`, configuration, webhook, mutated)
78 }
79
80 func patchAnnotationValue(configuration, webhook string, patch string) string {
81 return strings.Replace(fmt.Sprintf(`{"configuration": "%s", "webhook": "%s", "patch": %s, "patchType": "JSONPatch"}`, configuration, webhook, patch), " ", "", -1)
82 }
83
84
85 func testWebhookReinvocationPolicy(t *testing.T, watchCache bool) {
86 reinvokeNever := admissionregistrationv1.NeverReinvocationPolicy
87 reinvokeIfNeeded := admissionregistrationv1.IfNeededReinvocationPolicy
88
89 type testWebhook struct {
90 path string
91 policy *admissionregistrationv1.ReinvocationPolicyType
92 objectSelector *metav1.LabelSelector
93 }
94
95 testCases := []struct {
96 name string
97 initialPriorityClass string
98 webhooks []testWebhook
99 expectLabels map[string]string
100 expectInvocations map[string]int
101 expectError bool
102 errorContains string
103 expectAuditMutationAnnotations map[string]string
104 expectAuditPatchAnnotations map[string]string
105 }{
106 {
107 name: "no reinvocation for in-tree only mutation",
108 initialPriorityClass: "low-priority",
109 webhooks: []testWebhook{
110 {path: "/noop", policy: &reinvokeIfNeeded},
111 },
112 expectInvocations: map[string]int{"/noop": 1},
113 expectAuditMutationAnnotations: map[string]string{
114 "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-0", "admission.integration.test.0.noop", false),
115 },
116 },
117 {
118 name: "no webhook reinvocation for webhook when no in-tree reinvocation mutations",
119 initialPriorityClass: "low-priority",
120 webhooks: []testWebhook{
121 {path: "/addlabel", policy: &reinvokeIfNeeded},
122 },
123 expectInvocations: map[string]int{"/addlabel": 1},
124 expectAuditPatchAnnotations: map[string]string{
125 "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue("admission.integration.test-1", "admission.integration.test.0.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
126 },
127 expectAuditMutationAnnotations: map[string]string{
128 "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-1", "admission.integration.test.0.addlabel", true),
129 },
130 },
131 {
132 name: "webhook is reinvoked after in-tree reinvocation",
133 initialPriorityClass: "low-priority",
134 webhooks: []testWebhook{
135
136 {path: "/setpriority", policy: &reinvokeIfNeeded},
137 },
138 expectInvocations: map[string]int{"/setpriority": 2},
139 expectAuditPatchAnnotations: map[string]string{
140 "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue("admission.integration.test-2", "admission.integration.test.0.setpriority", `[{"op": "add", "path": "/spec/priorityClassName", "value": "high-priority"},{"op": "remove", "path": "/spec/priority"}]`),
141 },
142 expectAuditMutationAnnotations: map[string]string{
143 "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-2", "admission.integration.test.0.setpriority", true),
144 "mutation.webhook.admission.k8s.io/round_1_index_0": mutationAnnotationValue("admission.integration.test-2", "admission.integration.test.0.setpriority", false),
145 },
146 },
147 {
148 name: "no reinvocation of webhook B when in-tree or prior webhook mutations",
149 initialPriorityClass: "low-priority",
150 webhooks: []testWebhook{
151 {path: "/addlabel", policy: &reinvokeIfNeeded},
152 {path: "/conditionaladdlabel", policy: &reinvokeIfNeeded},
153 },
154 expectLabels: map[string]string{"x": "true", "a": "true", "b": "true"},
155 expectInvocations: map[string]int{"/addlabel": 2, "/conditionaladdlabel": 1},
156 expectAuditPatchAnnotations: map[string]string{
157 "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue("admission.integration.test-3", "admission.integration.test.0.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
158 "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-3", "admission.integration.test.1.conditionaladdlabel", `[{"op": "add", "path": "/metadata/labels/b", "value": "true"}]`),
159 },
160 expectAuditMutationAnnotations: map[string]string{
161 "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-3", "admission.integration.test.0.addlabel", true),
162 "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-3", "admission.integration.test.1.conditionaladdlabel", true),
163 "mutation.webhook.admission.k8s.io/round_1_index_0": mutationAnnotationValue("admission.integration.test-3", "admission.integration.test.0.addlabel", false),
164 },
165 },
166 {
167 name: "all webhooks reinvoked when any webhook reinvocation causes mutation",
168 initialPriorityClass: "low-priority",
169 webhooks: []testWebhook{
170 {path: "/settrue", policy: &reinvokeIfNeeded},
171 {path: "/setfalse", policy: &reinvokeIfNeeded},
172 },
173 expectLabels: map[string]string{"x": "true", "fight": "false"},
174 expectInvocations: map[string]int{"/settrue": 2, "/setfalse": 2},
175 expectAuditPatchAnnotations: map[string]string{
176 "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue("admission.integration.test-4", "admission.integration.test.0.settrue", `[{"op": "replace", "path": "/metadata/labels/fight", "value": "true"}]`),
177 "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-4", "admission.integration.test.1.setfalse", `[{"op": "replace", "path": "/metadata/labels/fight", "value": "false"}]`),
178 "patch.webhook.admission.k8s.io/round_1_index_0": patchAnnotationValue("admission.integration.test-4", "admission.integration.test.0.settrue", `[{"op": "replace", "path": "/metadata/labels/fight", "value": "true"}]`),
179 "patch.webhook.admission.k8s.io/round_1_index_1": patchAnnotationValue("admission.integration.test-4", "admission.integration.test.1.setfalse", `[{"op": "replace", "path": "/metadata/labels/fight", "value": "false"}]`),
180 },
181 expectAuditMutationAnnotations: map[string]string{
182 "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-4", "admission.integration.test.0.settrue", true),
183 "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-4", "admission.integration.test.1.setfalse", true),
184 "mutation.webhook.admission.k8s.io/round_1_index_0": mutationAnnotationValue("admission.integration.test-4", "admission.integration.test.0.settrue", true),
185 "mutation.webhook.admission.k8s.io/round_1_index_1": mutationAnnotationValue("admission.integration.test-4", "admission.integration.test.1.setfalse", true),
186 },
187 },
188 {
189 name: "no reinvocation of webhook B when in-tree or prior webhook mutations",
190 initialPriorityClass: "low-priority",
191 webhooks: []testWebhook{
192 {path: "/conditionaladdlabel", policy: &reinvokeIfNeeded, objectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "true"}}},
193 {path: "/addlabel", policy: &reinvokeIfNeeded},
194 },
195 expectLabels: map[string]string{"x": "true", "a": "true"},
196 expectInvocations: map[string]int{"/addlabel": 1, "/conditionaladdlabel": 0},
197 expectAuditPatchAnnotations: map[string]string{
198 "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-5", "admission.integration.test.1.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
199 },
200 expectAuditMutationAnnotations: map[string]string{
201 "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-5", "admission.integration.test.1.addlabel", true),
202 },
203 },
204 {
205 name: "invalid priority class set by webhook should result in error from in-tree priority plugin",
206 webhooks: []testWebhook{
207
208 {path: "/setinvalidpriority", policy: &reinvokeIfNeeded},
209 },
210 expectError: true,
211 errorContains: "no PriorityClass with name invalid was found",
212 expectInvocations: map[string]int{"/setinvalidpriority": 1},
213 },
214 {
215 name: "'reinvoke never' policy respected",
216 webhooks: []testWebhook{
217 {path: "/conditionaladdlabel", policy: &reinvokeNever},
218 {path: "/addlabel", policy: &reinvokeNever},
219 },
220 expectLabels: map[string]string{"x": "true", "a": "true"},
221 expectInvocations: map[string]int{"/conditionaladdlabel": 1, "/addlabel": 1},
222 expectAuditPatchAnnotations: map[string]string{
223 "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-7", "admission.integration.test.1.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
224 },
225 expectAuditMutationAnnotations: map[string]string{
226 "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-7", "admission.integration.test.0.conditionaladdlabel", false),
227 "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-7", "admission.integration.test.1.addlabel", true),
228 },
229 },
230 {
231 name: "'reinvoke never' (by default) policy respected",
232 webhooks: []testWebhook{
233 {path: "/conditionaladdlabel", policy: nil},
234 {path: "/addlabel", policy: nil},
235 },
236 expectLabels: map[string]string{"x": "true", "a": "true"},
237 expectInvocations: map[string]int{"/conditionaladdlabel": 1, "/addlabel": 1},
238 expectAuditPatchAnnotations: map[string]string{
239 "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-8", "admission.integration.test.1.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
240 },
241 expectAuditMutationAnnotations: map[string]string{
242 "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-8", "admission.integration.test.0.conditionaladdlabel", false),
243 "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-8", "admission.integration.test.1.addlabel", true),
244 },
245 },
246 }
247
248 roots := x509.NewCertPool()
249 if !roots.AppendCertsFromPEM(localhostCert) {
250 t.Fatal("Failed to append Cert from PEM")
251 }
252 cert, err := tls.X509KeyPair(localhostCert, localhostKey)
253 if err != nil {
254 t.Fatalf("Failed to build cert with error: %+v", err)
255 }
256
257 recorder := &invocationRecorder{counts: map[string]int{}}
258 webhookServer := httptest.NewUnstartedServer(newReinvokeWebhookHandler(recorder))
259 webhookServer.TLS = &tls.Config{
260
261 RootCAs: roots,
262 Certificates: []tls.Certificate{cert},
263 }
264 webhookServer.StartTLS()
265 defer webhookServer.Close()
266
267
268 policyFile, err := os.CreateTemp("", "audit-policy.yaml")
269 if err != nil {
270 t.Fatalf("Failed to create audit policy file: %v", err)
271 }
272 defer os.Remove(policyFile.Name())
273 if _, err := policyFile.Write([]byte(auditPolicy)); err != nil {
274 t.Fatalf("Failed to write audit policy file: %v", err)
275 }
276 if err := policyFile.Close(); err != nil {
277 t.Fatalf("Failed to close audit policy file: %v", err)
278 }
279
280
281 logFile, err := os.CreateTemp("", "audit.log")
282 if err != nil {
283 t.Fatalf("Failed to create audit log file: %v", err)
284 }
285 defer utiltesting.CloseAndRemove(t, logFile)
286
287 s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
288 "--disable-admission-plugins=ServiceAccount",
289 fmt.Sprintf("--watch-cache=%v", watchCache),
290 "--audit-policy-file", policyFile.Name(),
291 "--audit-log-version", "audit.k8s.io/v1",
292 "--audit-log-mode", "blocking",
293 "--audit-log-path", logFile.Name(),
294 }, framework.SharedEtcd())
295 defer s.TearDownFn()
296
297
298
299
300
301 clientConfig := rest.CopyConfig(s.ClientConfig)
302 clientConfig.Impersonate.UserName = testReinvocationClientUsername
303 clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
304 client, err := clientset.NewForConfig(clientConfig)
305 if err != nil {
306 t.Fatalf("unexpected error: %v", err)
307 }
308
309 for priorityClass, priority := range map[string]int{"low-priority": 1, "high-priority": 10} {
310 _, err = client.SchedulingV1().PriorityClasses().Create(context.TODO(), &schedulingv1.PriorityClass{ObjectMeta: metav1.ObjectMeta{Name: priorityClass}, Value: int32(priority)}, metav1.CreateOptions{})
311 if err != nil {
312 t.Fatal(err)
313 }
314 }
315
316 for i, tt := range testCases {
317 t.Run(tt.name, func(t *testing.T) {
318 upCh := recorder.Reset()
319 testCaseID := strconv.Itoa(i)
320 ns := "reinvoke-" + testCaseID
321 nsLabels := map[string]string{"test-case": testCaseID}
322 _, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns, Labels: nsLabels}}, metav1.CreateOptions{})
323 if err != nil {
324 t.Fatal(err)
325 }
326
327
328 markerNs := ns + "-markers"
329 markerNsLabels := map[string]string{"test-markers": testCaseID}
330 _, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: markerNs, Labels: markerNsLabels}}, metav1.CreateOptions{})
331 if err != nil {
332 t.Fatal(err)
333 }
334
335
336 marker, err := client.CoreV1().Pods(markerNs).Create(context.TODO(), newReinvocationMarkerFixture(markerNs), metav1.CreateOptions{})
337 if err != nil {
338 t.Fatal(err)
339 }
340
341 fail := admissionregistrationv1.Fail
342 webhooks := []admissionregistrationv1.MutatingWebhook{}
343 for j, webhook := range tt.webhooks {
344 endpoint := webhookServer.URL + webhook.path
345 name := fmt.Sprintf("admission.integration.test.%d.%s", j, strings.TrimPrefix(webhook.path, "/"))
346 webhooks = append(webhooks, admissionregistrationv1.MutatingWebhook{
347 Name: name,
348 ClientConfig: admissionregistrationv1.WebhookClientConfig{
349 URL: &endpoint,
350 CABundle: localhostCert,
351 },
352 Rules: []admissionregistrationv1.RuleWithOperations{{
353 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
354 Rule: admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
355 }},
356 ObjectSelector: webhook.objectSelector,
357 NamespaceSelector: &metav1.LabelSelector{MatchLabels: nsLabels},
358 FailurePolicy: &fail,
359 ReinvocationPolicy: webhook.policy,
360 AdmissionReviewVersions: []string{"v1beta1"},
361 SideEffects: &noSideEffects,
362 })
363 }
364
365 markerEndpoint := webhookServer.URL + "/marker"
366 webhooks = append(webhooks, admissionregistrationv1.MutatingWebhook{
367 Name: "admission.integration.test.marker",
368 ClientConfig: admissionregistrationv1.WebhookClientConfig{
369 URL: &markerEndpoint,
370 CABundle: localhostCert,
371 },
372 Rules: []admissionregistrationv1.RuleWithOperations{{
373 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
374 Rule: admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
375 }},
376 NamespaceSelector: &metav1.LabelSelector{MatchLabels: markerNsLabels},
377 ObjectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}},
378 AdmissionReviewVersions: []string{"v1beta1"},
379 SideEffects: &noSideEffects,
380 })
381
382 cfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.MutatingWebhookConfiguration{
383 ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("admission.integration.test-%d", i)},
384 Webhooks: webhooks,
385 }, metav1.CreateOptions{})
386 if err != nil {
387 t.Fatal(err)
388 }
389 defer func() {
390 err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), cfg.GetName(), metav1.DeleteOptions{})
391 if err != nil {
392 t.Fatal(err)
393 }
394 }()
395
396
397 if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
398 _, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
399 select {
400 case <-upCh:
401 return true, nil
402 default:
403 t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
404 return false, nil
405 }
406 }); err != nil {
407 t.Fatal(err)
408 }
409
410 pod := &corev1.Pod{
411 ObjectMeta: metav1.ObjectMeta{
412 Namespace: ns,
413 Name: "labeled",
414 Labels: map[string]string{"x": "true"},
415 },
416 Spec: corev1.PodSpec{
417 Containers: []corev1.Container{{
418 Name: "fake-name",
419 Image: "fakeimage",
420 }},
421 },
422 }
423 if tt.initialPriorityClass != "" {
424 pod.Spec.PriorityClassName = tt.initialPriorityClass
425 }
426 obj, err := client.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
427
428 if tt.expectError {
429 if err == nil {
430 t.Fatalf("expected error but got none")
431 }
432 if tt.errorContains != "" {
433 if !strings.Contains(err.Error(), tt.errorContains) {
434 t.Errorf("expected an error saying %q, but got: %v", tt.errorContains, err)
435 }
436 }
437 return
438 }
439
440 if err != nil {
441 t.Fatal(err)
442 }
443
444 if tt.expectLabels != nil {
445 labels := obj.GetLabels()
446 if !reflect.DeepEqual(tt.expectLabels, labels) {
447 t.Errorf("expected labels '%v', but got '%v'", tt.expectLabels, labels)
448 }
449 }
450
451 for k, v := range tt.expectInvocations {
452 if recorder.GetCount(k) != v {
453 t.Errorf("expected %d invocations of %s, but got %d", v, k, recorder.GetCount(k))
454 }
455 }
456
457 stream, err := os.OpenFile(logFile.Name(), os.O_RDWR, 0600)
458 if err != nil {
459 t.Errorf("unexpected error: %v", err)
460 }
461 defer stream.Close()
462 missing, err := utils.CheckAuditLines(stream, expectedAuditEvents(tt.expectAuditMutationAnnotations, tt.expectAuditPatchAnnotations, ns), auditv1.SchemeGroupVersion)
463 if err != nil {
464 t.Errorf("unexpected error checking audit lines: %v", err)
465 }
466 if len(missing.MissingEvents) > 0 {
467 t.Errorf("failed to get expected events -- missing: %s", missing)
468 }
469 if err := stream.Truncate(0); err != nil {
470 t.Errorf("unexpected error truncate file: %v", err)
471 }
472 if _, err := stream.Seek(0, 0); err != nil {
473 t.Errorf("unexpected error reset offset: %v", err)
474 }
475 })
476 }
477 }
478
479 type invocationRecorder struct {
480 mu sync.Mutex
481 upCh chan struct{}
482 upOnce sync.Once
483 counts map[string]int
484 }
485
486
487
488 func (i *invocationRecorder) Reset() chan struct{} {
489 i.mu.Lock()
490 defer i.mu.Unlock()
491 i.counts = map[string]int{}
492 i.upCh = make(chan struct{})
493 i.upOnce = sync.Once{}
494 return i.upCh
495 }
496
497 func (i *invocationRecorder) MarkerReceived() {
498 i.mu.Lock()
499 defer i.mu.Unlock()
500 i.upOnce.Do(func() {
501 close(i.upCh)
502 })
503 }
504
505 func (i *invocationRecorder) GetCount(path string) int {
506 i.mu.Lock()
507 defer i.mu.Unlock()
508 return i.counts[path]
509 }
510
511 func (i *invocationRecorder) IncrementCount(path string) {
512 i.mu.Lock()
513 defer i.mu.Unlock()
514 i.counts[path]++
515 }
516
517 func newReinvokeWebhookHandler(recorder *invocationRecorder) http.Handler {
518 patch := func(w http.ResponseWriter, patch string) {
519 w.Header().Set("Content-Type", "application/json")
520 pt := v1beta1.PatchTypeJSONPatch
521 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
522 Response: &v1beta1.AdmissionResponse{
523 Allowed: true,
524 PatchType: &pt,
525 Patch: []byte(patch),
526 },
527 })
528 }
529 allow := func(w http.ResponseWriter) {
530 w.Header().Set("Content-Type", "application/json")
531 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
532 Response: &v1beta1.AdmissionResponse{
533 Allowed: true,
534 },
535 })
536 }
537 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
538 defer r.Body.Close()
539 data, err := io.ReadAll(r.Body)
540 if err != nil {
541 http.Error(w, err.Error(), 400)
542 }
543 review := v1beta1.AdmissionReview{}
544 if err := json.Unmarshal(data, &review); err != nil {
545 http.Error(w, err.Error(), 400)
546 }
547 if review.Request.UserInfo.Username != testReinvocationClientUsername {
548
549 allow(w)
550 return
551 }
552
553 if len(review.Request.Object.Raw) == 0 {
554 http.Error(w, err.Error(), 400)
555 }
556 pod := &corev1.Pod{}
557 if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil {
558 http.Error(w, err.Error(), 400)
559 }
560
561 recorder.IncrementCount(r.URL.Path)
562
563 switch r.URL.Path {
564 case "/marker":
565
566
567 recorder.MarkerReceived()
568 allow(w)
569 return
570 case "/noop":
571 allow(w)
572 case "/settrue":
573 patch(w, `[{"op": "replace", "path": "/metadata/labels/fight", "value": "true"}]`)
574 case "/setfalse":
575 patch(w, `[{"op": "replace", "path": "/metadata/labels/fight", "value": "false"}]`)
576 case "/addlabel":
577 labels := pod.GetLabels()
578 if a, ok := labels["a"]; !ok || a != "true" {
579 patch(w, `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`)
580 return
581 }
582 allow(w)
583 case "/conditionaladdlabel":
584 labels := pod.GetLabels()
585 if _, ok := labels["a"]; ok {
586 patch(w, `[{"op": "add", "path": "/metadata/labels/b", "value": "true"}]`)
587 return
588 }
589 allow(w)
590 case "/setpriority":
591 if pod.Spec.PriorityClassName != "high-priority" {
592 if pod.Spec.Priority != nil {
593 patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "high-priority"},{"op": "remove", "path": "/spec/priority"}]`)
594 } else {
595 patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "high-priority"}]`)
596 }
597 return
598 }
599 allow(w)
600 case "/setinvalidpriority":
601 patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "invalid"}]`)
602 default:
603 http.NotFound(w, r)
604 }
605 })
606 }
607
608 func expectedAuditEvents(webhookMutationAnnotations, webhookPatchAnnotations map[string]string, namespace string) []utils.AuditEvent {
609 return []utils.AuditEvent{
610 {
611 Level: auditinternal.LevelRequest,
612 Stage: auditinternal.StageResponseComplete,
613 RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/pods", namespace),
614 Verb: "create",
615 Code: 201,
616 User: "system:apiserver",
617 ImpersonatedUser: testReinvocationClientUsername,
618 ImpersonatedGroups: "system:authenticated,system:masters",
619 Resource: "pods",
620 Namespace: namespace,
621 AuthorizeDecision: "allow",
622 RequestObject: true,
623 ResponseObject: false,
624 AdmissionWebhookMutationAnnotations: webhookMutationAnnotations,
625 AdmissionWebhookPatchAnnotations: webhookPatchAnnotations,
626 },
627 }
628 }
629
630 func newReinvocationMarkerFixture(namespace string) *corev1.Pod {
631 return &corev1.Pod{
632 ObjectMeta: metav1.ObjectMeta{
633 Namespace: namespace,
634 Name: "marker",
635 Labels: map[string]string{
636 "marker": "true",
637 },
638 },
639 Spec: corev1.PodSpec{
640 Containers: []corev1.Container{{
641 Name: "fake-name",
642 Image: "fakeimage",
643 }},
644 },
645 }
646 }
647
View as plain text