knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/admission_integration_test.go (about) 1 /* 2 Copyright 2020 The Knative 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 webhook 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "io" 24 "net/http" 25 "net/url" 26 "path" 27 "strings" 28 "testing" 29 "time" 30 31 "github.com/google/go-cmp/cmp" 32 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" 33 "golang.org/x/sync/errgroup" 34 jsonpatch "gomodules.xyz/jsonpatch/v2" 35 36 admissionv1 "k8s.io/api/admission/v1" 37 authenticationv1 "k8s.io/api/authentication/v1" 38 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 39 40 "knative.dev/pkg/apis" 41 kubeclient "knative.dev/pkg/client/injection/kube/client/fake" 42 "knative.dev/pkg/observability/metrics/metricstest" 43 ) 44 45 type fixedAdmissionController struct { 46 path string 47 response *admissionv1.AdmissionResponse 48 } 49 50 var _ AdmissionController = (*fixedAdmissionController)(nil) 51 52 func (fac *fixedAdmissionController) Path() string { 53 return fac.path 54 } 55 56 func (fac *fixedAdmissionController) Admit(ctx context.Context, req *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { 57 r := apis.GetHTTPRequest(ctx) 58 if r == nil { 59 panic("nil request!") 60 } else if r.URL.Path != fac.path { 61 panic("wrong path!") 62 } 63 return fac.response 64 } 65 66 type readBodyTwiceAdmissionController struct { 67 path string 68 response *admissionv1.AdmissionResponse 69 } 70 71 var _ AdmissionController = (*readBodyTwiceAdmissionController)(nil) 72 73 func (rbtac *readBodyTwiceAdmissionController) Path() string { 74 return rbtac.path 75 } 76 77 func (rbtac *readBodyTwiceAdmissionController) Admit(ctx context.Context, req *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse { 78 r := apis.GetHTTPRequest(ctx) 79 if r == nil { 80 panic("nil request!") 81 } else if r.URL.Path != rbtac.path { 82 panic("wrong path!") 83 } 84 85 var review admissionv1.AdmissionReview 86 if err := json.NewDecoder(r.Body).Decode(&review); err != nil { 87 panic("body closed!") 88 } 89 return rbtac.response 90 } 91 92 func TestAdmissionEmptyRequestBody(t *testing.T) { 93 c := &fixedAdmissionController{ 94 path: "/bazinga", 95 response: &admissionv1.AdmissionResponse{}, 96 } 97 98 testEmptyRequestBody(t, c) 99 } 100 101 func TestAdmissionValidResponseForResourceTLS(t *testing.T) { 102 ac := &fixedAdmissionController{ 103 path: "/bazinga", 104 response: &admissionv1.AdmissionResponse{Allowed: true}, 105 } 106 test := testSetup(t, withController(ac)) 107 108 eg, _ := errgroup.WithContext(test.ctx) 109 eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) }) 110 defer func() { 111 test.cancel() 112 if err := eg.Wait(); err != nil { 113 t.Error("Unable to run controller:", err) 114 } 115 }() 116 117 if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil { 118 t.Fatal("waitForServerAvailable() =", err) 119 } 120 tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options) 121 if err != nil { 122 t.Fatal("createSecureTLSClient() =", err) 123 } 124 125 admissionreq := &admissionv1.AdmissionRequest{ 126 Operation: admissionv1.Create, 127 Kind: metav1.GroupVersionKind{ 128 Group: "pkg.knative.dev", 129 Version: "v1alpha1", 130 Kind: "Resource", 131 }, 132 } 133 testRev := createResource("testrev") 134 marshaled, err := json.Marshal(testRev) 135 if err != nil { 136 t.Fatal("Failed to marshal resource:", err) 137 } 138 139 admissionreq.Resource.Group = "pkg.knative.dev" 140 admissionreq.Object.Raw = marshaled 141 rev := &admissionv1.AdmissionReview{ 142 Request: admissionreq, 143 } 144 145 reqBuf := new(bytes.Buffer) 146 err = json.NewEncoder(reqBuf).Encode(&rev) 147 if err != nil { 148 t.Fatal("Failed to marshal admission review:", err) 149 } 150 151 u, err := url.Parse("https://" + test.addr) 152 if err != nil { 153 t.Fatal("bad url", err) 154 } 155 156 u.Path = path.Join(u.Path, ac.Path()) 157 158 req, err := http.NewRequest(http.MethodGet, u.String(), reqBuf) 159 if err != nil { 160 t.Fatal("http.NewRequest() =", err) 161 } 162 req.Header.Add("Content-Type", "application/json") 163 164 doneCh := make(chan struct{}) 165 launchedCh := make(chan struct{}) 166 go func() { 167 defer close(doneCh) 168 169 close(launchedCh) 170 response, err := tlsClient.Do(req) 171 if err != nil { 172 t.Error("Failed to get response", err) 173 return 174 } 175 176 if got, want := response.StatusCode, http.StatusOK; got != want { 177 t.Errorf("Response status code = %v, wanted %v", got, want) 178 return 179 } 180 181 defer response.Body.Close() 182 responseBody, err := io.ReadAll(response.Body) 183 if err != nil { 184 t.Error("Failed to read response body", err) 185 return 186 } 187 188 reviewResponse := admissionv1.AdmissionReview{} 189 190 err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&reviewResponse) 191 if err != nil { 192 t.Error("Failed to decode response:", err) 193 return 194 } 195 196 if diff := cmp.Diff(rev.TypeMeta, reviewResponse.TypeMeta); diff != "" { 197 t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff) 198 return 199 } 200 201 assertAdmissionMetrics(t, test, ac.response.Allowed) 202 }() 203 204 // Wait for the goroutine to launch. 205 <-launchedCh 206 207 // Check that Admit calls block when they are initiated before informers sync. 208 select { 209 case <-time.After(100 * time.Millisecond): 210 case <-doneCh: 211 t.Fatal("Admit was called before informers had synced.") 212 } 213 214 // Signal the webhook that informers have synced. 215 test.webhook.InformersHaveSynced() 216 217 // Check that after informers have synced that things start completing immediately (including outstanding requests). 218 select { 219 case <-doneCh: 220 case <-time.After(5 * time.Second): 221 t.Error("Timed out waiting on Admit to complete after informers synced.") 222 } 223 } 224 225 func TestAdmissionValidResponseForResource(t *testing.T) { 226 ac := &fixedAdmissionController{ 227 path: "/bazinga", 228 response: &admissionv1.AdmissionResponse{Allowed: true}, 229 } 230 test := testSetup(t, withController(ac), withNoTLS()) 231 232 eg, _ := errgroup.WithContext(test.ctx) 233 eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) }) 234 defer func() { 235 test.cancel() 236 if err := eg.Wait(); err != nil { 237 t.Error("Unable to run controller:", err) 238 } 239 }() 240 241 if err := waitForNonTLSServerAvailable(t, test.addr, testTimeout); err != nil { 242 t.Fatal("waitForServerAvailable() =", err) 243 } 244 client := createNonTLSClient() 245 246 admissionreq := &admissionv1.AdmissionRequest{ 247 Operation: admissionv1.Create, 248 Kind: metav1.GroupVersionKind{ 249 Group: "pkg.knative.dev", 250 Version: "v1alpha1", 251 Kind: "Resource", 252 }, 253 } 254 testRev := createResource("testrev") 255 marshaled, err := json.Marshal(testRev) 256 if err != nil { 257 t.Fatal("Failed to marshal resource:", err) 258 } 259 260 admissionreq.Resource.Group = "pkg.knative.dev" 261 admissionreq.Object.Raw = marshaled 262 rev := &admissionv1.AdmissionReview{ 263 Request: admissionreq, 264 } 265 266 reqBuf := new(bytes.Buffer) 267 err = json.NewEncoder(reqBuf).Encode(&rev) 268 if err != nil { 269 t.Fatal("Failed to marshal admission review:", err) 270 } 271 272 u, err := url.Parse("http://" + test.addr) 273 if err != nil { 274 t.Fatal("bad url", err) 275 } 276 277 u.Path = path.Join(u.Path, ac.Path()) 278 279 req, err := http.NewRequest(http.MethodGet, u.String(), reqBuf) 280 if err != nil { 281 t.Fatal("http.NewRequest() =", err) 282 } 283 req.Header.Add("Content-Type", "application/json") 284 285 doneCh := make(chan struct{}) 286 launchedCh := make(chan struct{}) 287 go func() { 288 defer close(doneCh) 289 290 close(launchedCh) 291 response, err := client.Do(req) 292 if err != nil { 293 t.Error("Failed to get response", err) 294 return 295 } 296 297 if got, want := response.StatusCode, http.StatusOK; got != want { 298 t.Errorf("Response status code = %v, wanted %v", got, want) 299 return 300 } 301 302 defer response.Body.Close() 303 responseBody, err := io.ReadAll(response.Body) 304 if err != nil { 305 t.Error("Failed to read response body", err) 306 return 307 } 308 309 reviewResponse := admissionv1.AdmissionReview{} 310 311 err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&reviewResponse) 312 if err != nil { 313 t.Error("Failed to decode response:", err) 314 return 315 } 316 317 if diff := cmp.Diff(rev.TypeMeta, reviewResponse.TypeMeta); diff != "" { 318 t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff) 319 return 320 } 321 322 assertAdmissionMetrics(t, test, ac.response.Allowed) 323 }() 324 325 // Wait for the goroutine to launch. 326 <-launchedCh 327 328 // Check that Admit calls block when they are initiated before informers sync. 329 select { 330 case <-time.After(100 * time.Millisecond): 331 case <-doneCh: 332 t.Fatal("Admit was called before informers had synced.") 333 } 334 335 // Signal the webhook that informers have synced. 336 test.webhook.InformersHaveSynced() 337 338 // Check that after informers have synced that things start completing immediately (including outstanding requests). 339 select { 340 case <-doneCh: 341 case <-time.After(5 * time.Second): 342 t.Error("Timed out waiting on Admit to complete after informers synced.") 343 } 344 } 345 346 func TestAdmissionInvalidResponseForResource(t *testing.T) { 347 expectedError := "everything is fine." 348 ac := &fixedAdmissionController{ 349 path: "/booger", 350 response: MakeErrorStatus(expectedError, ""), 351 } 352 test := testSetup(t, withController(ac)) 353 354 eg, _ := errgroup.WithContext(test.ctx) 355 eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) }) 356 test.webhook.InformersHaveSynced() 357 defer func() { 358 test.cancel() 359 if err := eg.Wait(); err != nil { 360 t.Error("Unable to run controller:", err) 361 } 362 }() 363 364 if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil { 365 t.Fatal("waitForServerAvailable() =", err) 366 } 367 tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options) 368 if err != nil { 369 t.Fatal("createSecureTLSClient() =", err) 370 } 371 372 resource := createResource(testResourceName) 373 374 resource.Spec.FieldWithValidation = "not the right value" 375 marshaled, err := json.Marshal(resource) 376 if err != nil { 377 t.Fatal("Failed to marshal resource:", err) 378 } 379 380 admissionreq := &admissionv1.AdmissionRequest{ 381 Operation: admissionv1.Create, 382 Kind: metav1.GroupVersionKind{ 383 Group: "pkg.knative.dev", 384 Version: "v1alpha1", 385 Kind: "Resource", 386 }, 387 UserInfo: authenticationv1.UserInfo{ 388 Username: user1, 389 }, 390 } 391 392 admissionreq.Resource.Group = "pkg.knative.dev" 393 admissionreq.Object.Raw = marshaled 394 395 rev := &admissionv1.AdmissionReview{ 396 Request: admissionreq, 397 } 398 reqBuf := new(bytes.Buffer) 399 err = json.NewEncoder(reqBuf).Encode(&rev) 400 if err != nil { 401 t.Fatal("Failed to marshal admission review:", err) 402 } 403 404 u, err := url.Parse("https://" + test.addr) 405 if err != nil { 406 t.Fatal("bad url", err) 407 } 408 409 u.Path = path.Join(u.Path, ac.Path()) 410 411 req, err := http.NewRequest(http.MethodGet, u.String(), reqBuf) 412 if err != nil { 413 t.Fatal("http.NewRequest() =", err) 414 } 415 416 req.Header.Add("Content-Type", "application/json") 417 418 response, err := tlsClient.Do(req) 419 if err != nil { 420 t.Fatal("Failed to receive response", err) 421 } 422 423 if got, want := response.StatusCode, http.StatusOK; got != want { 424 t.Errorf("Response status code = %v, wanted %v", got, want) 425 } 426 427 defer response.Body.Close() 428 respBody, err := io.ReadAll(response.Body) 429 if err != nil { 430 t.Fatal("Failed to read response body", err) 431 } 432 433 reviewResponse := admissionv1.AdmissionReview{} 434 435 err = json.NewDecoder(bytes.NewReader(respBody)).Decode(&reviewResponse) 436 if err != nil { 437 t.Fatal("Failed to decode response:", err) 438 } 439 440 var respPatch []jsonpatch.JsonPatchOperation 441 err = json.Unmarshal(reviewResponse.Response.Patch, &respPatch) 442 if err == nil { 443 t.Fatalf("Expected to fail JSON unmarshal of response") 444 } 445 446 if got, want := reviewResponse.Response.Result.Status, "Failure"; got != want { 447 t.Errorf("Response status = %v, wanted %v", got, want) 448 } 449 450 if !strings.Contains(reviewResponse.Response.Result.Message, expectedError) { 451 t.Error("Received unexpected response status message", reviewResponse.Response.Result.Message) 452 } 453 454 assertAdmissionMetrics(t, test, ac.response.Allowed) 455 } 456 457 func TestAdmissionWarningResponseForResource(t *testing.T) { 458 // Test that our single warning below (with newlines) should be turned into 459 // these three warnings 460 expectedWarnings := []string{"everything is not fine.", "like really", "for sure"} 461 ac := &fixedAdmissionController{ 462 path: "/warnmeplease", 463 response: &admissionv1.AdmissionResponse{ 464 Allowed: true, 465 Warnings: []string{"everything is not fine.\nlike really\nfor sure"}, 466 }, 467 } 468 test := testSetup(t, withController(ac)) 469 470 eg, _ := errgroup.WithContext(test.ctx) 471 eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) }) 472 test.webhook.InformersHaveSynced() 473 defer func() { 474 test.cancel() 475 if err := eg.Wait(); err != nil { 476 t.Error("Unable to run controller:", err) 477 } 478 }() 479 480 if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil { 481 t.Fatal("waitForServerAvailable() =", err) 482 } 483 tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options) 484 if err != nil { 485 t.Fatal("createSecureTLSClient() =", err) 486 } 487 488 resource := createResource(testResourceName) 489 490 marshaled, err := json.Marshal(resource) 491 if err != nil { 492 t.Fatal("Failed to marshal resource:", err) 493 } 494 495 admissionreq := &admissionv1.AdmissionRequest{ 496 Operation: admissionv1.Create, 497 Kind: metav1.GroupVersionKind{ 498 Group: "pkg.knative.dev", 499 Version: "v1alpha1", 500 Kind: "Resource", 501 }, 502 UserInfo: authenticationv1.UserInfo{ 503 Username: user1, 504 }, 505 } 506 507 admissionreq.Resource.Group = "pkg.knative.dev" 508 admissionreq.Object.Raw = marshaled 509 510 rev := &admissionv1.AdmissionReview{ 511 Request: admissionreq, 512 } 513 reqBuf := new(bytes.Buffer) 514 err = json.NewEncoder(reqBuf).Encode(&rev) 515 if err != nil { 516 t.Fatal("Failed to marshal admission review:", err) 517 } 518 519 u, err := url.Parse("https://" + test.addr) 520 if err != nil { 521 t.Fatal("bad url", err) 522 } 523 524 u.Path = path.Join(u.Path, ac.Path()) 525 526 req, err := http.NewRequest(http.MethodGet, u.String(), reqBuf) 527 if err != nil { 528 t.Fatal("http.NewRequest() =", err) 529 } 530 531 req.Header.Add("Content-Type", "application/json") 532 533 response, err := tlsClient.Do(req) 534 if err != nil { 535 t.Fatal("Failed to receive response", err) 536 } 537 538 if got, want := response.StatusCode, http.StatusOK; got != want { 539 t.Errorf("Response status code = %v, wanted %v", got, want) 540 } 541 542 defer response.Body.Close() 543 respBody, err := io.ReadAll(response.Body) 544 if err != nil { 545 t.Fatal("Failed to read response body", err) 546 } 547 548 reviewResponse := admissionv1.AdmissionReview{} 549 550 err = json.NewDecoder(bytes.NewReader(respBody)).Decode(&reviewResponse) 551 if err != nil { 552 t.Fatal("Failed to decode response:", err) 553 } 554 555 warnings := reviewResponse.Response.Warnings 556 if len(warnings) != 3 { 557 t.Errorf("Received unexpected warnings, wanted 3 got: %s", reviewResponse.Response.Warnings) 558 } 559 for i, w := range warnings { 560 if expectedWarnings[i] != w { 561 t.Errorf("Unexpected warning want %s got %s", expectedWarnings[i], w) 562 } 563 } 564 565 assertAdmissionMetrics(t, test, ac.response.Allowed) 566 } 567 568 func TestAdmissionValidResponseForRequestBody(t *testing.T) { 569 ac := &readBodyTwiceAdmissionController{ 570 path: "/bazinga", 571 response: &admissionv1.AdmissionResponse{ 572 Allowed: true, 573 }, 574 } 575 test := testSetup(t, withController(ac), withNoTLS()) 576 577 eg, _ := errgroup.WithContext(test.ctx) 578 eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) }) 579 defer func() { 580 test.cancel() 581 if err := eg.Wait(); err != nil { 582 t.Error("Unable to run controller:", err) 583 } 584 }() 585 586 if err := waitForNonTLSServerAvailable(t, test.addr, testTimeout); err != nil { 587 t.Fatal("waitForServerAvailable() =", err) 588 } 589 client := createNonTLSClient() 590 591 admissionreq := &admissionv1.AdmissionRequest{ 592 Operation: admissionv1.Create, 593 Kind: metav1.GroupVersionKind{ 594 Group: "pkg.knative.dev", 595 Version: "v1alpha1", 596 Kind: "Resource", 597 }, 598 } 599 testRev := createResource("testrev") 600 marshaled, err := json.Marshal(testRev) 601 if err != nil { 602 t.Fatal("Failed to marshal resource:", err) 603 } 604 605 admissionreq.Resource.Group = "pkg.knative.dev" 606 admissionreq.Object.Raw = marshaled 607 rev := &admissionv1.AdmissionReview{ 608 Request: admissionreq, 609 } 610 611 reqBuf := new(bytes.Buffer) 612 err = json.NewEncoder(reqBuf).Encode(&rev) 613 if err != nil { 614 t.Fatal("Failed to marshal admission review:", err) 615 } 616 617 u, err := url.Parse("http://" + test.addr) 618 if err != nil { 619 t.Fatal("bad url", err) 620 } 621 622 u.Path = path.Join(u.Path, ac.Path()) 623 624 req, err := http.NewRequest(http.MethodGet, u.String(), reqBuf) 625 if err != nil { 626 t.Fatal("http.NewRequest() =", err) 627 } 628 req.Header.Add("Content-Type", "application/json") 629 630 doneCh := make(chan struct{}) 631 launchedCh := make(chan struct{}) 632 go func() { 633 defer close(doneCh) 634 635 close(launchedCh) 636 response, err := client.Do(req) 637 if err != nil { 638 t.Error("Failed to get response", err) 639 return 640 } 641 642 if got, want := response.StatusCode, http.StatusOK; got != want { 643 t.Errorf("Response status code = %v, wanted %v", got, want) 644 return 645 } 646 647 defer response.Body.Close() 648 responseBody, err := io.ReadAll(response.Body) 649 if err != nil { 650 t.Error("Failed to read response body", err) 651 return 652 } 653 654 reviewResponse := admissionv1.AdmissionReview{} 655 656 err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&reviewResponse) 657 if err != nil { 658 t.Error("Failed to decode response:", err) 659 return 660 } 661 662 if diff := cmp.Diff(rev.TypeMeta, reviewResponse.TypeMeta); diff != "" { 663 t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff) 664 return 665 } 666 667 assertAdmissionMetrics(t, test, ac.response.Allowed) 668 }() 669 670 // Wait for the goroutine to launch. 671 <-launchedCh 672 673 // Check that Admit calls block when they are initiated before informers sync. 674 select { 675 case <-time.After(100 * time.Millisecond): 676 case <-doneCh: 677 t.Fatal("Admit was called before informers had synced.") 678 } 679 680 // Signal the webhook that informers have synced. 681 test.webhook.InformersHaveSynced() 682 683 // Check that after informers have synced that things start completing immediately (including outstanding requests). 684 select { 685 case <-doneCh: 686 case <-time.After(5 * time.Second): 687 t.Error("Timed out waiting on Admit to complete after informers synced.") 688 } 689 } 690 691 func assertAdmissionMetrics(t *testing.T, tc testContext, allowed bool) { 692 status := metav1.StatusFailure 693 if allowed { 694 status = metav1.StatusSuccess 695 } 696 metricstest.AssertMetrics(t, tc.metricReader, 697 metricstest.MetricsPresent( 698 otelhttp.ScopeName, 699 "http.server.request.body.size", 700 "http.server.response.body.size", 701 "http.server.request.duration", 702 ), 703 metricstest.MetricsPresent( 704 scopeName, 705 "kn.webhook.handler.duration", 706 ), 707 metricstest.HasAttributes( 708 "", // any scope 709 "", // any metric 710 WebhookTypeAttr.With(WebhookTypeAdmission), 711 OperationAttr.With("CREATE"), 712 GroupAttr.With("pkg.knative.dev"), 713 VersionAttr.With("v1alpha1"), 714 KindAttr.With("Resource"), 715 SubresourceAttr.With(""), 716 StatusAttr.With(strings.ToLower(status)), 717 ), 718 ) 719 }