knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/webhook_integration_test.go (about)

     1  /*
     2  Copyright 2018 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  	"context"
    21  	"fmt"
    22  	"io"
    23  	"net"
    24  	"net/http"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	"golang.org/x/sync/errgroup"
    30  
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	kubeclient "knative.dev/pkg/client/injection/kube/client/fake"
    33  
    34  	"go.opentelemetry.io/otel/sdk/metric"
    35  
    36  	"knative.dev/pkg/system"
    37  	pkgtest "knative.dev/pkg/testing"
    38  	certresources "knative.dev/pkg/webhook/certificates/resources"
    39  
    40  	_ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake"
    41  )
    42  
    43  // createResource creates a testing.Resource with the given name in the system namespace.
    44  func createResource(name string) *pkgtest.Resource {
    45  	return &pkgtest.Resource{
    46  		ObjectMeta: metav1.ObjectMeta{
    47  			Namespace: system.Namespace(),
    48  			Name:      name,
    49  		},
    50  		Spec: pkgtest.ResourceSpec{
    51  			FieldWithValidation: "magic value",
    52  		},
    53  	}
    54  }
    55  
    56  const testTimeout = 10 * time.Second
    57  
    58  func TestMissingContentType(t *testing.T) {
    59  	test := testSetup(t)
    60  
    61  	eg, _ := errgroup.WithContext(test.ctx)
    62  	eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) })
    63  	test.webhook.InformersHaveSynced()
    64  	defer func() {
    65  		test.cancel()
    66  		if err := eg.Wait(); err != nil {
    67  			t.Error("Unable to run controller:", err)
    68  		}
    69  	}()
    70  
    71  	if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil {
    72  		t.Fatal("waitForServerAvailable() =", err)
    73  	}
    74  
    75  	tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options)
    76  	if err != nil {
    77  		t.Fatal("createSecureTLSClient() =", err)
    78  	}
    79  
    80  	req, err := http.NewRequest(http.MethodGet, "https://"+test.addr, nil)
    81  	if err != nil {
    82  		t.Fatal("http.NewRequest() =", err)
    83  	}
    84  
    85  	response, err := tlsClient.Do(req)
    86  	if err != nil {
    87  		t.Fatalf("Received %v error from server %s", err, test.addr)
    88  	}
    89  
    90  	if got, want := response.StatusCode, http.StatusUnsupportedMediaType; got != want {
    91  		t.Errorf("Response status code = %v, wanted %v", got, want)
    92  	}
    93  
    94  	defer response.Body.Close()
    95  	responseBody, err := io.ReadAll(response.Body)
    96  	if err != nil {
    97  		t.Fatal("Failed to read response body", err)
    98  	}
    99  
   100  	if !strings.Contains(string(responseBody), "invalid Content-Type") {
   101  		t.Errorf("Response body to contain 'invalid Content-Type' , got = '%s'", string(responseBody))
   102  	}
   103  }
   104  
   105  func TestServerWithCustomSecret(t *testing.T) {
   106  	test := testSetup(t, withServerCertificateName("tls.crt"), withServerPrivateKeyName("tls.key"))
   107  
   108  	eg, _ := errgroup.WithContext(test.ctx)
   109  	eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) })
   110  	test.webhook.InformersHaveSynced()
   111  	defer func() {
   112  		test.cancel()
   113  		if err := eg.Wait(); err != nil {
   114  			t.Error("Unable to run controller:", err)
   115  		}
   116  	}()
   117  
   118  	pollErr := waitForServerAvailable(t, test.addr, testTimeout)
   119  	if pollErr != nil {
   120  		t.Fatal("waitForServerAvailable() =", pollErr)
   121  	}
   122  }
   123  
   124  func testEmptyRequestBody(t *testing.T, controller any) {
   125  	test := testSetup(t, withController(controller))
   126  
   127  	eg, _ := errgroup.WithContext(test.ctx)
   128  	eg.Go(func() error { return test.webhook.Run(test.ctx.Done()) })
   129  	test.webhook.InformersHaveSynced()
   130  	defer func() {
   131  		test.cancel()
   132  		if err := eg.Wait(); err != nil {
   133  			t.Error("Unable to run controller:", err)
   134  		}
   135  	}()
   136  
   137  	if err := waitForServerAvailable(t, test.addr, testTimeout); err != nil {
   138  		t.Fatal("waitForServerAvailable() =", err)
   139  	}
   140  
   141  	tlsClient, err := createSecureTLSClient(t, kubeclient.Get(test.ctx), &test.webhook.Options)
   142  	if err != nil {
   143  		t.Fatal("createSecureTLSClient() =", err)
   144  	}
   145  
   146  	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/bazinga", test.addr), nil)
   147  	if err != nil {
   148  		t.Fatal("http.NewRequest() =", err)
   149  	}
   150  
   151  	req.Header.Add("Content-Type", "application/json")
   152  
   153  	response, err := tlsClient.Do(req)
   154  	if err != nil {
   155  		t.Fatal("failed to get resp", err)
   156  	}
   157  
   158  	if got, want := response.StatusCode, http.StatusBadRequest; got != want {
   159  		t.Errorf("Response status code = %v, wanted %v", got, want)
   160  	}
   161  	defer response.Body.Close()
   162  
   163  	responseBody, err := io.ReadAll(response.Body)
   164  	if err != nil {
   165  		t.Fatal("Failed to read response body", err)
   166  	}
   167  
   168  	if !strings.Contains(string(responseBody), "could not decode body") {
   169  		t.Errorf("Response body to contain 'decode failure information' , got = %q", string(responseBody))
   170  	}
   171  }
   172  
   173  func TestSetupWebhookHTTPServerError(t *testing.T) {
   174  	defaultOpts := newDefaultOptions()
   175  	defaultOpts.Port = -1 // invalid port
   176  	ctx, wh, cancel := newNonRunningTestWebhook(t, defaultOpts)
   177  	defer cancel()
   178  	kubeClient := kubeclient.Get(ctx)
   179  
   180  	nsErr := createNamespace(t, kubeClient, metav1.NamespaceSystem)
   181  	if nsErr != nil {
   182  		t.Fatal("createNamespace() =", nsErr)
   183  	}
   184  	cMapsErr := createTestConfigMap(t, kubeClient)
   185  	if cMapsErr != nil {
   186  		t.Fatal("createTestConfigMap() =", cMapsErr)
   187  	}
   188  
   189  	stopCh := make(chan struct{})
   190  	errCh := make(chan error)
   191  	go func() {
   192  		if err := wh.Run(stopCh); err != nil {
   193  			errCh <- err
   194  		}
   195  	}()
   196  
   197  	select {
   198  	case <-time.After(6 * time.Second):
   199  		t.Error("Timeout in testing bootstrap webhook http server failed")
   200  	case errItem := <-errCh:
   201  		if !strings.Contains(errItem.Error(), "bootstrap failed") {
   202  			t.Error("Expected bootstrap webhook http server failed")
   203  		}
   204  	}
   205  }
   206  
   207  func testSetup(t *testing.T, opts ...func(*testOptions)) testContext {
   208  	t.Helper()
   209  
   210  	// ephemeral port
   211  	l, err := net.Listen("tcp", ":0")
   212  	if err != nil {
   213  		t.Fatal("unable to get ephemeral port: ", err)
   214  	}
   215  
   216  	testOpts := &testOptions{
   217  		Options: newDefaultOptions(),
   218  	}
   219  
   220  	reader := metric.NewManualReader()
   221  	provider := metric.NewMeterProvider(metric.WithReader(reader))
   222  	testOpts.Options.MeterProvider = provider
   223  
   224  	for _, opt := range opts {
   225  		opt(testOpts)
   226  	}
   227  
   228  	ctx, wh, cancel := newNonRunningTestWebhook(t, testOpts.Options, testOpts.controllers...)
   229  	wh.testListener = l
   230  
   231  	// Create certificate
   232  	secret, err := certresources.MakeSecret(ctx, testOpts.SecretName, system.Namespace(), testOpts.ServiceName)
   233  	if err != nil {
   234  		t.Fatalf("failed to create certificate")
   235  	}
   236  
   237  	if testOpts.ServerCertificateName != "" {
   238  		secret.Data[testOpts.ServerCertificateName] = secret.Data[certresources.ServerCert]
   239  		delete(secret.Data, certresources.ServerCert)
   240  	}
   241  
   242  	if testOpts.ServerPrivateKeyName != "" {
   243  		secret.Data[testOpts.ServerPrivateKeyName] = secret.Data[certresources.ServerKey]
   244  		delete(secret.Data, certresources.ServerKey)
   245  	}
   246  
   247  	kubeClient := kubeclient.Get(ctx)
   248  
   249  	if _, err := kubeClient.CoreV1().Secrets(secret.Namespace).Create(context.Background(), secret, metav1.CreateOptions{}); err != nil {
   250  		t.Fatalf("failed to create secret")
   251  	}
   252  
   253  	return testContext{
   254  		webhook:      wh,
   255  		addr:         l.Addr().String(),
   256  		ctx:          ctx,
   257  		cancel:       cancel,
   258  		metricReader: reader,
   259  	}
   260  }
   261  
   262  type testContext struct {
   263  	webhook      *Webhook
   264  	addr         string
   265  	ctx          context.Context
   266  	cancel       context.CancelFunc
   267  	metricReader *metric.ManualReader
   268  }
   269  
   270  type testOptions struct {
   271  	Options
   272  	controllers []any
   273  }
   274  
   275  func withController(controller any) func(o *testOptions) {
   276  	return func(o *testOptions) {
   277  		o.controllers = append(o.controllers, controller)
   278  	}
   279  }
   280  
   281  func withServerCertificateName(name string) func(o *testOptions) {
   282  	return func(o *testOptions) {
   283  		o.ServerCertificateName = name
   284  	}
   285  }
   286  
   287  func withServerPrivateKeyName(name string) func(o *testOptions) {
   288  	return func(o *testOptions) {
   289  		o.ServerPrivateKeyName = name
   290  	}
   291  }
   292  
   293  func withNoTLS() func(o *testOptions) {
   294  	return func(o *testOptions) {
   295  		o.SecretName = ""
   296  	}
   297  }