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  }