knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/conversion_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  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"strings"
    27  	"testing"
    28  
    29  	"github.com/google/go-cmp/cmp"
    30  	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    31  	"golang.org/x/sync/errgroup"
    32  
    33  	apixv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/runtime"
    36  	"k8s.io/apimachinery/pkg/types"
    37  
    38  	kubeclient "knative.dev/pkg/client/injection/kube/client/fake"
    39  	"knative.dev/pkg/observability/metrics/metricstest"
    40  )
    41  
    42  type fixedConversionController struct {
    43  	path     string
    44  	response *apixv1.ConversionResponse
    45  }
    46  
    47  var _ ConversionController = (*fixedConversionController)(nil)
    48  
    49  func (fcc *fixedConversionController) Path() string {
    50  	return fcc.path
    51  }
    52  
    53  func (fcc *fixedConversionController) Convert(context.Context, *apixv1.ConversionRequest) *apixv1.ConversionResponse {
    54  	return fcc.response
    55  }
    56  
    57  func TestConversionEmptyRequestBody(t *testing.T) {
    58  	c := &fixedConversionController{
    59  		path:     "/bazinga",
    60  		response: &apixv1.ConversionResponse{},
    61  	}
    62  
    63  	testEmptyRequestBody(t, c)
    64  }
    65  
    66  func TestConversionValidResponse(t *testing.T) {
    67  	cc := &fixedConversionController{
    68  		path: "/bazinga",
    69  		response: &apixv1.ConversionResponse{
    70  			UID: types.UID("some-uid"),
    71  			Result: metav1.Status{
    72  				Status: metav1.StatusSuccess,
    73  			},
    74  		},
    75  	}
    76  	test := testSetup(t, withController(cc))
    77  
    78  	eg, _ := errgroup.WithContext(test.ctx)
    79  	eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) })
    80  	defer func() {
    81  		test.cancel()
    82  		if err := eg.Wait(); err != nil {
    83  			t.Error("Unable to run controller:", err)
    84  		}
    85  	}()
    86  
    87  	if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil {
    88  		t.Fatal("waitForServerAvailable() =", err)
    89  	}
    90  	tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options)
    91  	if err != nil {
    92  		t.Fatal("createSecureTLSClient() =", err)
    93  	}
    94  
    95  	review := apixv1.ConversionReview{
    96  		TypeMeta: metav1.TypeMeta{
    97  			APIVersion: "apiextensions.k8s.io/v1",
    98  			Kind:       "ConversionReview",
    99  		},
   100  		Request: &apixv1.ConversionRequest{
   101  			UID:               types.UID("some-uid"),
   102  			DesiredAPIVersion: "example.com/v1",
   103  			Objects:           []runtime.RawExtension{},
   104  		},
   105  	}
   106  
   107  	reqBuf := new(bytes.Buffer)
   108  	err = json.NewEncoder(reqBuf).Encode(&review)
   109  	if err != nil {
   110  		t.Fatal("Failed to marshal conversion review:", err)
   111  	}
   112  
   113  	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s%s", test.addr, cc.Path()), reqBuf)
   114  	if err != nil {
   115  		t.Fatal("http.NewRequest() =", err)
   116  	}
   117  	req.Header.Add("Content-Type", "application/json")
   118  
   119  	response, err := tlsClient.Do(req)
   120  	if err != nil {
   121  		t.Fatal("Failed to get response", err)
   122  	}
   123  
   124  	defer response.Body.Close()
   125  
   126  	if got, want := response.StatusCode, http.StatusOK; got != want {
   127  		t.Errorf("Response status code = %v, wanted %v", got, want)
   128  	}
   129  
   130  	responseBody, err := io.ReadAll(response.Body)
   131  	if err != nil {
   132  		t.Fatal("Failed to read response body", err)
   133  	}
   134  
   135  	reviewResponse := apixv1.ConversionReview{}
   136  
   137  	err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&reviewResponse)
   138  	if err != nil {
   139  		t.Fatal("Failed to decode response:", err)
   140  	}
   141  
   142  	if reviewResponse.Response.UID != "some-uid" {
   143  		t.Errorf("expected the response uid to be the stubbed version")
   144  	}
   145  
   146  	if diff := cmp.Diff(review.TypeMeta, reviewResponse.TypeMeta); diff != "" {
   147  		t.Errorf("expected the response typeMeta to be the same as the request (-want, +got)\n%s", diff)
   148  	}
   149  
   150  	assertConversionMetrics(t, test, cc.response.Result.Status)
   151  }
   152  
   153  func TestConversionInvalidResponse(t *testing.T) {
   154  	cc := &fixedConversionController{
   155  		path: "/bazinga",
   156  		response: &apixv1.ConversionResponse{
   157  			UID: types.UID("some-uid"),
   158  			Result: metav1.Status{
   159  				Status: metav1.StatusFailure,
   160  			},
   161  		},
   162  	}
   163  	test := testSetup(t, withController(cc))
   164  
   165  	eg, _ := errgroup.WithContext(test.ctx)
   166  	eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) })
   167  	defer func() {
   168  		test.cancel()
   169  		if err := eg.Wait(); err != nil {
   170  			t.Error("Unable to run controller:", err)
   171  		}
   172  	}()
   173  
   174  	if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil {
   175  		t.Fatal("waitForServerAvailable() =", err)
   176  	}
   177  	tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options)
   178  	if err != nil {
   179  		t.Fatal("createSecureTLSClient() =", err)
   180  	}
   181  
   182  	review := apixv1.ConversionReview{
   183  		TypeMeta: metav1.TypeMeta{
   184  			APIVersion: "apiextensions.k8s.io/v1",
   185  			Kind:       "ConversionReview",
   186  		},
   187  		Request: &apixv1.ConversionRequest{
   188  			UID:               types.UID("some-uid"),
   189  			DesiredAPIVersion: "example.com/v1",
   190  			Objects:           []runtime.RawExtension{},
   191  		},
   192  	}
   193  
   194  	reqBuf := new(bytes.Buffer)
   195  	err = json.NewEncoder(reqBuf).Encode(&review)
   196  	if err != nil {
   197  		t.Fatal("Failed to marshal conversion review:", err)
   198  	}
   199  
   200  	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s%s", test.addr, cc.Path()), reqBuf)
   201  	if err != nil {
   202  		t.Fatal("http.NewRequest() =", err)
   203  	}
   204  	req.Header.Add("Content-Type", "application/json")
   205  
   206  	response, err := tlsClient.Do(req)
   207  	if err != nil {
   208  		t.Fatal("Failed to get response", err)
   209  	}
   210  
   211  	defer response.Body.Close()
   212  
   213  	if got, want := response.StatusCode, http.StatusOK; got != want {
   214  		t.Errorf("Response status code = %v, wanted %v", got, want)
   215  	}
   216  
   217  	responseBody, err := io.ReadAll(response.Body)
   218  	if err != nil {
   219  		t.Fatal("Failed to read response body", err)
   220  	}
   221  
   222  	reviewResponse := apixv1.ConversionReview{}
   223  
   224  	err = json.NewDecoder(bytes.NewReader(responseBody)).Decode(&reviewResponse)
   225  	if err != nil {
   226  		t.Fatal("Failed to decode response:", err)
   227  	}
   228  
   229  	if reviewResponse.Response.UID != "some-uid" {
   230  		t.Errorf("expected the response uid to be the stubbed version")
   231  	}
   232  
   233  	if reviewResponse.Response.Result.Status != metav1.StatusFailure {
   234  		t.Errorf("expected the response uid to be the stubbed version")
   235  	}
   236  
   237  	assertConversionMetrics(t, test, cc.response.Result.Status)
   238  }
   239  
   240  func assertConversionMetrics(t *testing.T, tc testContext, status string) {
   241  	metricstest.AssertMetrics(t, tc.metricReader,
   242  		metricstest.MetricsPresent(
   243  			otelhttp.ScopeName,
   244  			"http.server.request.body.size",
   245  			"http.server.response.body.size",
   246  			"http.server.request.duration",
   247  		),
   248  		metricstest.MetricsPresent(
   249  			scopeName,
   250  			"kn.webhook.handler.duration",
   251  		),
   252  		metricstest.HasAttributes(
   253  			"", // any scope
   254  			"", // any metric
   255  			WebhookTypeAttr.With(WebhookTypeConversion),
   256  			GroupAttr.With("example.com"),
   257  			VersionAttr.With("v1"),
   258  			StatusAttr.With(strings.ToLower(status)),
   259  		),
   260  	)
   261  }