sigs.k8s.io/cluster-api@v1.7.1/internal/runtime/client/client_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes 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 client
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"encoding/json"
    23  	"fmt"
    24  	"net/http"
    25  	"net/http/httptest"
    26  	"reflect"
    27  	"regexp"
    28  	"testing"
    29  
    30  	. "github.com/onsi/gomega"
    31  	corev1 "k8s.io/api/core/v1"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/labels"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
    36  	"k8s.io/utils/ptr"
    37  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    38  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    39  
    40  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    41  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    42  	runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
    43  	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
    44  	runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry"
    45  	fakev1alpha1 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha1"
    46  	fakev1alpha2 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha2"
    47  )
    48  
    49  func TestClient_httpCall(t *testing.T) {
    50  	g := NewWithT(t)
    51  
    52  	tableTests := []struct {
    53  		name     string
    54  		request  runtime.Object
    55  		response runtime.Object
    56  		opts     *httpCallOptions
    57  		wantErr  bool
    58  	}{
    59  		{
    60  			name:     "error if request, response and options are nil",
    61  			request:  nil,
    62  			response: nil,
    63  			opts:     nil,
    64  			wantErr:  true,
    65  		},
    66  		{
    67  			name:     "error if catalog is not set",
    68  			request:  &fakev1alpha1.FakeRequest{},
    69  			response: &fakev1alpha1.FakeResponse{},
    70  			opts: &httpCallOptions{
    71  				catalog: nil,
    72  			},
    73  			wantErr: true,
    74  		},
    75  		{
    76  			name:     "error if hooks is not registered with catalog",
    77  			request:  &fakev1alpha1.FakeRequest{},
    78  			response: &fakev1alpha1.FakeResponse{},
    79  			opts: &httpCallOptions{
    80  				catalog: runtimecatalog.New(),
    81  			},
    82  			wantErr: true,
    83  		},
    84  		{
    85  			name: "succeed for valid request and response objects",
    86  			request: &fakev1alpha1.FakeRequest{
    87  				TypeMeta: metav1.TypeMeta{
    88  					Kind:       "FakeRequest",
    89  					APIVersion: fakev1alpha1.GroupVersion.Identifier(),
    90  				},
    91  			},
    92  			response: &fakev1alpha1.FakeResponse{},
    93  			opts: func() *httpCallOptions {
    94  				c := runtimecatalog.New()
    95  				g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed())
    96  
    97  				// get same gvh for hook by using the FakeHook and catalog
    98  				gvh, err := c.GroupVersionHook(fakev1alpha1.FakeHook)
    99  				g.Expect(err).To(Succeed())
   100  
   101  				return &httpCallOptions{
   102  					catalog:         c,
   103  					registrationGVH: gvh,
   104  					hookGVH:         gvh,
   105  				}
   106  			}(),
   107  			wantErr: false,
   108  		},
   109  		{
   110  			name: "success if request and response are valid objects - with conversion",
   111  			request: &fakev1alpha2.FakeRequest{
   112  				TypeMeta: metav1.TypeMeta{
   113  					Kind:       "FakeRequest",
   114  					APIVersion: fakev1alpha2.GroupVersion.Identifier(),
   115  				},
   116  			},
   117  			response: &fakev1alpha2.FakeResponse{},
   118  			opts: func() *httpCallOptions {
   119  				c := runtimecatalog.New()
   120  				// register fakev1alpha1 and fakev1alpha2 to enable conversion
   121  				g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed())
   122  				g.Expect(fakev1alpha2.AddToCatalog(c)).To(Succeed())
   123  
   124  				// get same gvh for hook by using the FakeHook and catalog
   125  				registrationGVH, err := c.GroupVersionHook(fakev1alpha1.FakeHook)
   126  				g.Expect(err).To(Succeed())
   127  				hookGVH, err := c.GroupVersionHook(fakev1alpha2.FakeHook)
   128  				g.Expect(err).To(Succeed())
   129  
   130  				return &httpCallOptions{
   131  					catalog:         c,
   132  					registrationGVH: registrationGVH,
   133  					hookGVH:         hookGVH,
   134  				}
   135  			}(),
   136  			wantErr: false,
   137  		},
   138  		{
   139  			name:     "succeed if request doesn't define TypeMeta",
   140  			request:  &fakev1alpha2.FakeRequest{},
   141  			response: &fakev1alpha2.FakeResponse{},
   142  			opts: func() *httpCallOptions {
   143  				c := runtimecatalog.New()
   144  				// register fakev1alpha1 and fakev1alpha2 to enable conversion
   145  				g.Expect(fakev1alpha2.AddToCatalog(c)).To(Succeed())
   146  
   147  				// get same gvh for hook by using the FakeHook and catalog
   148  				gvh, err := c.GroupVersionHook(fakev1alpha2.FakeHook)
   149  				g.Expect(err).To(Succeed())
   150  
   151  				return &httpCallOptions{
   152  					catalog:         c,
   153  					registrationGVH: gvh,
   154  					hookGVH:         gvh,
   155  				}
   156  			}(),
   157  			wantErr: false,
   158  		},
   159  		{
   160  			name:     "success if request doesn't define TypeMeta - with conversion",
   161  			request:  &fakev1alpha2.FakeRequest{},
   162  			response: &fakev1alpha2.FakeResponse{},
   163  			opts: func() *httpCallOptions {
   164  				c := runtimecatalog.New()
   165  				// register fakev1alpha1 and fakev1alpha2 to enable conversion
   166  				g.Expect(fakev1alpha1.AddToCatalog(c)).To(Succeed())
   167  				g.Expect(fakev1alpha2.AddToCatalog(c)).To(Succeed())
   168  
   169  				// get same gvh for hook by using the FakeHook and catalog
   170  				registrationGVH, err := c.GroupVersionHook(fakev1alpha1.FakeHook)
   171  				g.Expect(err).To(Succeed())
   172  				hookGVH, err := c.GroupVersionHook(fakev1alpha2.FakeHook)
   173  				g.Expect(err).To(Succeed())
   174  
   175  				return &httpCallOptions{
   176  					catalog:         c,
   177  					hookGVH:         hookGVH,
   178  					registrationGVH: registrationGVH,
   179  				}
   180  			}(),
   181  			wantErr: false,
   182  		},
   183  	}
   184  	for _, tt := range tableTests {
   185  		t.Run(tt.name, func(*testing.T) {
   186  			// a http server is only required if we have a valid catalog, otherwise httpCall will not reach out to the server
   187  			if tt.opts != nil && tt.opts.catalog != nil {
   188  				// create http server with fakeHookHandler
   189  				mux := http.NewServeMux()
   190  				mux.HandleFunc("/", fakeHookHandler)
   191  
   192  				srv := newUnstartedTLSServer(mux)
   193  				srv.StartTLS()
   194  				defer srv.Close()
   195  
   196  				// set url to srv for in tt.opts
   197  				tt.opts.config.URL = ptr.To(srv.URL)
   198  				tt.opts.config.CABundle = testcerts.CACert
   199  			}
   200  
   201  			err := httpCall(context.TODO(), tt.request, tt.response, tt.opts)
   202  			if tt.wantErr {
   203  				g.Expect(err).To(HaveOccurred())
   204  			} else {
   205  				g.Expect(err).ToNot(HaveOccurred())
   206  			}
   207  		})
   208  	}
   209  }
   210  
   211  func fakeHookHandler(w http.ResponseWriter, _ *http.Request) {
   212  	response := &fakev1alpha1.FakeResponse{
   213  		TypeMeta: metav1.TypeMeta{
   214  			Kind:       "FakeHookResponse",
   215  			APIVersion: fakev1alpha1.GroupVersion.Identifier(),
   216  		},
   217  		Second: "",
   218  		First:  1,
   219  	}
   220  	respBody, err := json.Marshal(response)
   221  	if err != nil {
   222  		panic(err)
   223  	}
   224  	w.WriteHeader(http.StatusOK)
   225  	_, _ = w.Write(respBody)
   226  }
   227  
   228  func TestURLForExtension(t *testing.T) {
   229  	type args struct {
   230  		config               runtimev1.ClientConfig
   231  		gvh                  runtimecatalog.GroupVersionHook
   232  		extensionHandlerName string
   233  	}
   234  
   235  	type want struct {
   236  		scheme string
   237  		host   string
   238  		path   string
   239  	}
   240  
   241  	gvh := runtimecatalog.GroupVersionHook{
   242  		Group:   "test.runtime.cluster.x-k8s.io",
   243  		Version: "v1alpha1",
   244  		Hook:    "testhook.test-extension",
   245  	}
   246  
   247  	tests := []struct {
   248  		name    string
   249  		args    args
   250  		want    want
   251  		wantErr bool
   252  	}{
   253  		{
   254  			name: "ClientConfig using service should have correct URL values",
   255  			args: args{
   256  				config: runtimev1.ClientConfig{
   257  					Service: &runtimev1.ServiceReference{
   258  						Namespace: "test1",
   259  						Name:      "extension-service",
   260  						Port:      ptr.To[int32](8443),
   261  					},
   262  				},
   263  				gvh:                  gvh,
   264  				extensionHandlerName: "test-handler",
   265  			},
   266  			want: want{
   267  				scheme: "https",
   268  				host:   "extension-service.test1.svc:8443",
   269  				path:   runtimecatalog.GVHToPath(gvh, "test-handler"),
   270  			},
   271  			wantErr: false,
   272  		},
   273  		{
   274  			name: "ClientConfig using service and CAbundle should have correct URL values",
   275  			args: args{
   276  				config: runtimev1.ClientConfig{
   277  					Service: &runtimev1.ServiceReference{
   278  						Namespace: "test1",
   279  						Name:      "extension-service",
   280  						Port:      ptr.To[int32](8443),
   281  					},
   282  					CABundle: []byte("some-ca-data"),
   283  				},
   284  				gvh:                  gvh,
   285  				extensionHandlerName: "test-handler",
   286  			},
   287  			want: want{
   288  				scheme: "https",
   289  				host:   "extension-service.test1.svc:8443",
   290  				path:   runtimecatalog.GVHToPath(gvh, "test-handler"),
   291  			},
   292  			wantErr: false,
   293  		},
   294  		{
   295  			name: "ClientConfig using URL should have correct URL values",
   296  			args: args{
   297  				config: runtimev1.ClientConfig{
   298  					URL: ptr.To("https://extension-host.com"),
   299  				},
   300  				gvh:                  gvh,
   301  				extensionHandlerName: "test-handler",
   302  			},
   303  			want: want{
   304  				scheme: "https",
   305  				host:   "extension-host.com",
   306  				path:   runtimecatalog.GVHToPath(gvh, "test-handler"),
   307  			},
   308  			wantErr: false,
   309  		},
   310  		{
   311  			name: "should error if both Service and URL are missing",
   312  			args: args{
   313  				config:               runtimev1.ClientConfig{},
   314  				gvh:                  gvh,
   315  				extensionHandlerName: "test-handler",
   316  			},
   317  			wantErr: true,
   318  		},
   319  	}
   320  
   321  	for _, tt := range tests {
   322  		t.Run(tt.name, func(t *testing.T) {
   323  			g := NewWithT(t)
   324  			u, err := urlForExtension(tt.args.config, tt.args.gvh, tt.args.extensionHandlerName)
   325  			if tt.wantErr {
   326  				g.Expect(err).To(HaveOccurred())
   327  			} else {
   328  				g.Expect(err).ToNot(HaveOccurred())
   329  				g.Expect(u.Scheme).To(Equal(tt.want.scheme))
   330  				g.Expect(u.Host).To(Equal(tt.want.host))
   331  				g.Expect(u.Path).To(Equal(tt.want.path))
   332  			}
   333  		})
   334  	}
   335  }
   336  
   337  func Test_defaultAndValidateDiscoveryResponse(t *testing.T) {
   338  	var invalidFailurePolicy runtimehooksv1.FailurePolicy = "DONT_FAIL"
   339  	cat := runtimecatalog.New()
   340  	_ = fakev1alpha1.AddToCatalog(cat)
   341  
   342  	tests := []struct {
   343  		name      string
   344  		discovery *runtimehooksv1.DiscoveryResponse
   345  		wantErr   bool
   346  	}{
   347  		{
   348  			name: "succeed with valid skeleton DiscoveryResponse",
   349  			discovery: &runtimehooksv1.DiscoveryResponse{
   350  				TypeMeta: metav1.TypeMeta{
   351  					Kind:       "DiscoveryResponse",
   352  					APIVersion: runtimehooksv1.GroupVersion.String(),
   353  				},
   354  				Handlers: []runtimehooksv1.ExtensionHandler{{
   355  					Name: "extension",
   356  					RequestHook: runtimehooksv1.GroupVersionHook{
   357  						Hook:       "FakeHook",
   358  						APIVersion: fakev1alpha1.GroupVersion.String(),
   359  					},
   360  				}},
   361  			},
   362  			wantErr: false,
   363  		},
   364  		{
   365  			name: "error if handler name has capital letters",
   366  			discovery: &runtimehooksv1.DiscoveryResponse{
   367  				TypeMeta: metav1.TypeMeta{
   368  					Kind:       "DiscoveryResponse",
   369  					APIVersion: runtimehooksv1.GroupVersion.String(),
   370  				},
   371  				Handlers: []runtimehooksv1.ExtensionHandler{{
   372  					Name: "HAS-CAPITAL-LETTERS",
   373  					RequestHook: runtimehooksv1.GroupVersionHook{
   374  						Hook:       "FakeHook",
   375  						APIVersion: fakev1alpha1.GroupVersion.String(),
   376  					},
   377  				}},
   378  			},
   379  			wantErr: true,
   380  		},
   381  		{
   382  			name: "error if handler name has full stops",
   383  			discovery: &runtimehooksv1.DiscoveryResponse{
   384  				TypeMeta: metav1.TypeMeta{
   385  					Kind:       "DiscoveryResponse",
   386  					APIVersion: runtimehooksv1.GroupVersion.String(),
   387  				},
   388  				Handlers: []runtimehooksv1.ExtensionHandler{{
   389  					Name: "has.full.stops",
   390  					RequestHook: runtimehooksv1.GroupVersionHook{
   391  						Hook:       "FakeHook",
   392  						APIVersion: fakev1alpha1.GroupVersion.String(),
   393  					},
   394  				}},
   395  			},
   396  			wantErr: true,
   397  		},
   398  		{
   399  			name: "error with Timeout of over 30 seconds",
   400  			discovery: &runtimehooksv1.DiscoveryResponse{
   401  				TypeMeta: metav1.TypeMeta{
   402  					Kind:       "DiscoveryResponse",
   403  					APIVersion: runtimehooksv1.GroupVersion.String(),
   404  				},
   405  				Handlers: []runtimehooksv1.ExtensionHandler{{
   406  					Name: "ext1",
   407  					RequestHook: runtimehooksv1.GroupVersionHook{
   408  						Hook:       "FakeHook",
   409  						APIVersion: fakev1alpha1.GroupVersion.String(),
   410  					},
   411  					TimeoutSeconds: ptr.To[int32](100),
   412  				}},
   413  			},
   414  			wantErr: true,
   415  		},
   416  		{
   417  			name: "error with Timeout of less than 0",
   418  			discovery: &runtimehooksv1.DiscoveryResponse{
   419  				TypeMeta: metav1.TypeMeta{
   420  					Kind:       "DiscoveryResponse",
   421  					APIVersion: runtimehooksv1.GroupVersion.String(),
   422  				},
   423  				Handlers: []runtimehooksv1.ExtensionHandler{{
   424  					Name: "ext1",
   425  					RequestHook: runtimehooksv1.GroupVersionHook{
   426  						Hook:       "FakeHook",
   427  						APIVersion: fakev1alpha1.GroupVersion.String(),
   428  					},
   429  					TimeoutSeconds: ptr.To[int32](-1),
   430  				}},
   431  			},
   432  			wantErr: true,
   433  		},
   434  		{
   435  			name: "error with FailurePolicy not Fail or Ignore",
   436  			discovery: &runtimehooksv1.DiscoveryResponse{
   437  				TypeMeta: metav1.TypeMeta{
   438  					Kind:       "DiscoveryResponse",
   439  					APIVersion: runtimehooksv1.GroupVersion.String(),
   440  				},
   441  				Handlers: []runtimehooksv1.ExtensionHandler{{
   442  					Name: "ext1",
   443  					RequestHook: runtimehooksv1.GroupVersionHook{
   444  						Hook:       "FakeHook",
   445  						APIVersion: fakev1alpha1.GroupVersion.String(),
   446  					},
   447  					TimeoutSeconds: ptr.To[int32](20),
   448  					FailurePolicy:  &invalidFailurePolicy,
   449  				}},
   450  			},
   451  			wantErr: true,
   452  		},
   453  		{
   454  			name: "error when handler name is duplicated",
   455  			discovery: &runtimehooksv1.DiscoveryResponse{
   456  				TypeMeta: metav1.TypeMeta{
   457  					Kind:       "DiscoveryResponse",
   458  					APIVersion: runtimehooksv1.GroupVersion.String(),
   459  				},
   460  				Handlers: []runtimehooksv1.ExtensionHandler{
   461  					{
   462  						Name: "ext1",
   463  						RequestHook: runtimehooksv1.GroupVersionHook{
   464  							Hook:       "FakeHook",
   465  							APIVersion: fakev1alpha1.GroupVersion.String(),
   466  						},
   467  					},
   468  					{
   469  						Name: "ext1",
   470  						RequestHook: runtimehooksv1.GroupVersionHook{
   471  							Hook:       "FakeHook",
   472  							APIVersion: fakev1alpha1.GroupVersion.String(),
   473  						},
   474  					},
   475  					{
   476  						Name: "ext2",
   477  						RequestHook: runtimehooksv1.GroupVersionHook{
   478  							Hook:       "FakeHook",
   479  							APIVersion: fakev1alpha1.GroupVersion.String(),
   480  						},
   481  					},
   482  				},
   483  			},
   484  			wantErr: true,
   485  		},
   486  		{
   487  			name: "error if handler GroupVersionHook is not registered",
   488  			discovery: &runtimehooksv1.DiscoveryResponse{
   489  				TypeMeta: metav1.TypeMeta{
   490  					Kind:       "DiscoveryResponse",
   491  					APIVersion: runtimehooksv1.GroupVersion.String(),
   492  				},
   493  				Handlers: []runtimehooksv1.ExtensionHandler{{
   494  					Name: "ext1",
   495  					RequestHook: runtimehooksv1.GroupVersionHook{
   496  						Hook: "FakeHook",
   497  						// Version v1alpha2 is not registered with the catalog
   498  						APIVersion: fakev1alpha2.GroupVersion.String(),
   499  					},
   500  				}},
   501  			},
   502  			wantErr: true,
   503  		},
   504  		{
   505  			name: "error if handler GroupVersion can not be parsed",
   506  			discovery: &runtimehooksv1.DiscoveryResponse{
   507  				TypeMeta: metav1.TypeMeta{
   508  					Kind:       "DiscoveryResponse",
   509  					APIVersion: runtimehooksv1.GroupVersion.String(),
   510  				},
   511  				Handlers: []runtimehooksv1.ExtensionHandler{{
   512  					Name: "ext1",
   513  					RequestHook: runtimehooksv1.GroupVersionHook{
   514  						Hook: "FakeHook",
   515  						// Version v1alpha2 is not registered with the catalog
   516  						APIVersion: "too/many/slashes",
   517  					},
   518  				}},
   519  			},
   520  			wantErr: true,
   521  		},
   522  	}
   523  	for _, tt := range tests {
   524  		t.Run(tt.name, func(t *testing.T) {
   525  			if err := defaultAndValidateDiscoveryResponse(cat, tt.discovery); (err != nil) != tt.wantErr {
   526  				t.Errorf("defaultAndValidateDiscoveryResponse() error = %v, wantErr %v", err, tt.wantErr)
   527  			}
   528  		})
   529  	}
   530  }
   531  
   532  func TestClient_CallExtension(t *testing.T) {
   533  	ns := &corev1.Namespace{
   534  		TypeMeta: metav1.TypeMeta{
   535  			Kind:       "Namespace",
   536  			APIVersion: corev1.SchemeGroupVersion.String(),
   537  		},
   538  		ObjectMeta: metav1.ObjectMeta{
   539  			Name: "foo",
   540  		},
   541  	}
   542  	fpFail := runtimev1.FailurePolicyFail
   543  	fpIgnore := runtimev1.FailurePolicyIgnore
   544  
   545  	validExtensionHandlerWithFailPolicy := runtimev1.ExtensionConfig{
   546  		Spec: runtimev1.ExtensionConfigSpec{
   547  			ClientConfig: runtimev1.ClientConfig{
   548  				// Set a fake URL, in test cases where we start the test server the URL will be overridden.
   549  				URL:      ptr.To("https://127.0.0.1/"),
   550  				CABundle: testcerts.CACert,
   551  			},
   552  			NamespaceSelector: &metav1.LabelSelector{},
   553  		},
   554  		Status: runtimev1.ExtensionConfigStatus{
   555  			Handlers: []runtimev1.ExtensionHandler{
   556  				{
   557  					Name: "valid-extension",
   558  					RequestHook: runtimev1.GroupVersionHook{
   559  						APIVersion: fakev1alpha1.GroupVersion.String(),
   560  						Hook:       "FakeHook",
   561  					},
   562  					TimeoutSeconds: ptr.To[int32](1),
   563  					FailurePolicy:  &fpFail,
   564  				},
   565  			},
   566  		},
   567  	}
   568  	validExtensionHandlerWithIgnorePolicy := runtimev1.ExtensionConfig{
   569  		Spec: runtimev1.ExtensionConfigSpec{
   570  			ClientConfig: runtimev1.ClientConfig{
   571  				// Set a fake URL, in test cases where we start the test server the URL will be overridden.
   572  				URL:      ptr.To("https://127.0.0.1/"),
   573  				CABundle: testcerts.CACert,
   574  			},
   575  			NamespaceSelector: &metav1.LabelSelector{}},
   576  		Status: runtimev1.ExtensionConfigStatus{
   577  			Handlers: []runtimev1.ExtensionHandler{
   578  				{
   579  					Name: "valid-extension",
   580  					RequestHook: runtimev1.GroupVersionHook{
   581  						APIVersion: fakev1alpha1.GroupVersion.String(),
   582  						Hook:       "FakeHook",
   583  					},
   584  					TimeoutSeconds: ptr.To[int32](1),
   585  					FailurePolicy:  &fpIgnore,
   586  				},
   587  			},
   588  		},
   589  	}
   590  	type args struct {
   591  		hook     runtimecatalog.Hook
   592  		name     string
   593  		request  runtimehooksv1.RequestObject
   594  		response runtimehooksv1.ResponseObject
   595  	}
   596  	tests := []struct {
   597  		name                       string
   598  		registeredExtensionConfigs []runtimev1.ExtensionConfig
   599  		args                       args
   600  		testServer                 testServerConfig
   601  		wantErr                    bool
   602  	}{
   603  		{
   604  			name:                       "should fail when hook and request/response are not compatible",
   605  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy},
   606  			testServer: testServerConfig{
   607  				start: false,
   608  			},
   609  			args: args{
   610  				hook:     fakev1alpha1.FakeHook,
   611  				name:     "valid-extension",
   612  				request:  &fakev1alpha1.SecondFakeRequest{},
   613  				response: &fakev1alpha1.SecondFakeResponse{},
   614  			},
   615  			wantErr: true,
   616  		},
   617  		{
   618  			name:                       "should fail when hook GVH does not match the registered ExtensionHandler",
   619  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy},
   620  			testServer: testServerConfig{
   621  				start: false,
   622  			},
   623  			args: args{
   624  				hook:     fakev1alpha1.SecondFakeHook,
   625  				name:     "valid-extension",
   626  				request:  &fakev1alpha1.SecondFakeRequest{},
   627  				response: &fakev1alpha1.SecondFakeResponse{},
   628  			},
   629  			wantErr: true,
   630  		},
   631  		{
   632  			name:                       "should fail if ExtensionHandler is not registered",
   633  			registeredExtensionConfigs: nil,
   634  			testServer: testServerConfig{
   635  				start: true,
   636  				responses: map[string]testServerResponse{
   637  					"/*": response(runtimehooksv1.ResponseStatusSuccess),
   638  				},
   639  			},
   640  			args: args{
   641  				hook:     fakev1alpha1.FakeHook,
   642  				name:     "unregistered-extension",
   643  				request:  &fakev1alpha1.FakeRequest{},
   644  				response: &fakev1alpha1.FakeResponse{},
   645  			},
   646  			wantErr: true,
   647  		},
   648  		{
   649  			name:                       "should succeed when calling ExtensionHandler with success response and FailurePolicyFail",
   650  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy},
   651  			testServer: testServerConfig{
   652  				start: true,
   653  				responses: map[string]testServerResponse{
   654  					"/*": response(runtimehooksv1.ResponseStatusSuccess),
   655  				},
   656  			},
   657  			args: args{
   658  				hook:     fakev1alpha1.FakeHook,
   659  				name:     "valid-extension",
   660  				request:  &fakev1alpha1.FakeRequest{},
   661  				response: &fakev1alpha1.FakeResponse{},
   662  			},
   663  			wantErr: false,
   664  		},
   665  		{
   666  			name:                       "should succeed when calling ExtensionHandler with success response and FailurePolicyIgnore",
   667  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithIgnorePolicy},
   668  			testServer: testServerConfig{
   669  				start: true,
   670  				responses: map[string]testServerResponse{
   671  					"/*": response(runtimehooksv1.ResponseStatusSuccess),
   672  				},
   673  			},
   674  			args: args{
   675  				hook:     fakev1alpha1.FakeHook,
   676  				name:     "valid-extension",
   677  				request:  &fakev1alpha1.FakeRequest{},
   678  				response: &fakev1alpha1.FakeResponse{},
   679  			},
   680  			wantErr: false,
   681  		},
   682  		{
   683  			name:                       "should fail when calling ExtensionHandler with failure response and FailurePolicyFail",
   684  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy},
   685  			testServer: testServerConfig{
   686  				start: true,
   687  				responses: map[string]testServerResponse{
   688  					"/*": response(runtimehooksv1.ResponseStatusFailure),
   689  				},
   690  			},
   691  			args: args{
   692  				hook:     fakev1alpha1.FakeHook,
   693  				name:     "valid-extension",
   694  				request:  &fakev1alpha1.FakeRequest{},
   695  				response: &fakev1alpha1.FakeResponse{},
   696  			},
   697  			wantErr: true,
   698  		},
   699  		{
   700  			name:                       "should fail when calling ExtensionHandler with failure response and FailurePolicyIgnore",
   701  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithIgnorePolicy},
   702  			testServer: testServerConfig{
   703  				start: true,
   704  				responses: map[string]testServerResponse{
   705  					"/*": response(runtimehooksv1.ResponseStatusFailure),
   706  				},
   707  			},
   708  			args: args{
   709  				hook:     fakev1alpha1.FakeHook,
   710  				name:     "valid-extension",
   711  				request:  &fakev1alpha1.FakeRequest{},
   712  				response: &fakev1alpha1.FakeResponse{},
   713  			},
   714  			wantErr: true,
   715  		},
   716  
   717  		{
   718  			name:                       "should succeed with unreachable extension and FailurePolicyIgnore",
   719  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithIgnorePolicy},
   720  			testServer: testServerConfig{
   721  				start: false,
   722  			},
   723  			args: args{
   724  				hook:     fakev1alpha1.FakeHook,
   725  				name:     "valid-extension",
   726  				request:  &fakev1alpha1.FakeRequest{},
   727  				response: &fakev1alpha1.FakeResponse{},
   728  			},
   729  			wantErr: false,
   730  		},
   731  		{
   732  			name:                       "should fail with unreachable extension and FailurePolicyFail",
   733  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{validExtensionHandlerWithFailPolicy},
   734  			testServer: testServerConfig{
   735  				start: false,
   736  			},
   737  			args: args{
   738  				hook:     fakev1alpha1.FakeHook,
   739  				name:     "valid-extension",
   740  				request:  &fakev1alpha1.FakeRequest{},
   741  				response: &fakev1alpha1.FakeResponse{},
   742  			},
   743  			wantErr: true,
   744  		},
   745  	}
   746  
   747  	for _, tt := range tests {
   748  		t.Run(tt.name, func(t *testing.T) {
   749  			g := NewWithT(t)
   750  
   751  			if tt.testServer.start {
   752  				srv := createSecureTestServer(tt.testServer)
   753  				srv.StartTLS()
   754  				defer srv.Close()
   755  
   756  				// Set the URL to the real address of the test server.
   757  				for i := range tt.registeredExtensionConfigs {
   758  					tt.registeredExtensionConfigs[i].Spec.ClientConfig.URL = ptr.To(fmt.Sprintf("https://%s/", srv.Listener.Addr().String()))
   759  				}
   760  			}
   761  
   762  			cat := runtimecatalog.New()
   763  			_ = fakev1alpha1.AddToCatalog(cat)
   764  			_ = fakev1alpha2.AddToCatalog(cat)
   765  			fakeClient := fake.NewClientBuilder().
   766  				WithObjects(ns).
   767  				Build()
   768  
   769  			c := New(Options{
   770  				Catalog:  cat,
   771  				Registry: registry(tt.registeredExtensionConfigs),
   772  				Client:   fakeClient,
   773  			})
   774  
   775  			obj := &clusterv1.Cluster{
   776  				ObjectMeta: metav1.ObjectMeta{
   777  					Name:      "cluster",
   778  					Namespace: "foo",
   779  				},
   780  			}
   781  			err := c.CallExtension(context.Background(), tt.args.hook, obj, tt.args.name, tt.args.request, tt.args.response)
   782  
   783  			if tt.wantErr {
   784  				g.Expect(err).To(HaveOccurred())
   785  			} else {
   786  				g.Expect(err).ToNot(HaveOccurred())
   787  			}
   788  		})
   789  	}
   790  }
   791  
   792  func TestPrepareRequest(t *testing.T) {
   793  	t.Run("request should have the correct settings", func(t *testing.T) {
   794  		tests := []struct {
   795  			name         string
   796  			request      runtimehooksv1.RequestObject
   797  			registration *runtimeregistry.ExtensionRegistration
   798  			want         map[string]string
   799  		}{
   800  			{
   801  				name: "settings in request should be preserved as is if there are not setting in the registration",
   802  				request: &runtimehooksv1.BeforeClusterCreateRequest{
   803  					CommonRequest: runtimehooksv1.CommonRequest{
   804  						Settings: map[string]string{
   805  							"key1": "value1",
   806  						},
   807  					},
   808  				},
   809  				registration: &runtimeregistry.ExtensionRegistration{},
   810  				want: map[string]string{
   811  					"key1": "value1",
   812  				},
   813  			},
   814  			{
   815  				name: "settings in registration should be used as is if there are no settings in the request",
   816  				request: &runtimehooksv1.BeforeClusterCreateRequest{
   817  					CommonRequest: runtimehooksv1.CommonRequest{},
   818  				},
   819  				registration: &runtimeregistry.ExtensionRegistration{
   820  					Settings: map[string]string{
   821  						"key1": "value1",
   822  					},
   823  				},
   824  				want: map[string]string{
   825  					"key1": "value1",
   826  				},
   827  			},
   828  			{
   829  				name: "settings in request and registry should be merged with request taking precedence",
   830  				request: &runtimehooksv1.BeforeClusterCreateRequest{
   831  					CommonRequest: runtimehooksv1.CommonRequest{
   832  						Settings: map[string]string{
   833  							"key1": "value1",
   834  							"key2": "value2",
   835  						},
   836  					},
   837  				},
   838  				registration: &runtimeregistry.ExtensionRegistration{
   839  					Settings: map[string]string{
   840  						"key1": "value11",
   841  						"key3": "value3",
   842  					},
   843  				},
   844  				want: map[string]string{
   845  					"key1": "value1",
   846  					"key2": "value2",
   847  					"key3": "value3",
   848  				},
   849  			},
   850  		}
   851  
   852  		for _, tt := range tests {
   853  			t.Run(tt.name, func(t *testing.T) {
   854  				g := NewWithT(t)
   855  				var originalRegistrationSettings map[string]string
   856  				if tt.registration.Settings != nil {
   857  					originalRegistrationSettings = map[string]string{}
   858  					for k, v := range tt.registration.Settings {
   859  						originalRegistrationSettings[k] = v
   860  					}
   861  				}
   862  
   863  				g.Expect(cloneAndAddSettings(tt.request, tt.registration.Settings).GetSettings()).To(Equal(tt.want))
   864  				// Make sure that the original settings in the registration are not modified.
   865  				g.Expect(tt.registration.Settings).To(Equal(originalRegistrationSettings))
   866  			})
   867  		}
   868  	})
   869  }
   870  
   871  func TestClient_CallAllExtensions(t *testing.T) {
   872  	ns := &corev1.Namespace{
   873  		TypeMeta: metav1.TypeMeta{
   874  			Kind:       "Namespace",
   875  			APIVersion: corev1.SchemeGroupVersion.String(),
   876  		},
   877  		ObjectMeta: metav1.ObjectMeta{
   878  			Name: "foo",
   879  		},
   880  	}
   881  	fpFail := runtimev1.FailurePolicyFail
   882  
   883  	extensionConfig := runtimev1.ExtensionConfig{
   884  		Spec: runtimev1.ExtensionConfigSpec{
   885  			ClientConfig: runtimev1.ClientConfig{
   886  				// Set a fake URL, in test cases where we start the test server the URL will be overridden.
   887  				URL:      ptr.To("https://127.0.0.1/"),
   888  				CABundle: testcerts.CACert,
   889  			},
   890  			NamespaceSelector: &metav1.LabelSelector{},
   891  		},
   892  		Status: runtimev1.ExtensionConfigStatus{
   893  			Handlers: []runtimev1.ExtensionHandler{
   894  				{
   895  					Name: "first-extension",
   896  					RequestHook: runtimev1.GroupVersionHook{
   897  						APIVersion: fakev1alpha1.GroupVersion.String(),
   898  						Hook:       "FakeHook",
   899  					},
   900  					TimeoutSeconds: ptr.To[int32](1),
   901  					FailurePolicy:  &fpFail,
   902  				},
   903  				{
   904  					Name: "second-extension",
   905  					RequestHook: runtimev1.GroupVersionHook{
   906  						APIVersion: fakev1alpha1.GroupVersion.String(),
   907  						Hook:       "FakeHook",
   908  					},
   909  					TimeoutSeconds: ptr.To[int32](1),
   910  					FailurePolicy:  &fpFail,
   911  				},
   912  				{
   913  					Name: "third-extension",
   914  					RequestHook: runtimev1.GroupVersionHook{
   915  						APIVersion: fakev1alpha1.GroupVersion.String(),
   916  						Hook:       "FakeHook",
   917  					},
   918  					TimeoutSeconds: ptr.To[int32](1),
   919  					FailurePolicy:  &fpFail,
   920  				},
   921  			},
   922  		},
   923  	}
   924  
   925  	type args struct {
   926  		hook     runtimecatalog.Hook
   927  		request  runtimehooksv1.RequestObject
   928  		response runtimehooksv1.ResponseObject
   929  	}
   930  	tests := []struct {
   931  		name                       string
   932  		registeredExtensionConfigs []runtimev1.ExtensionConfig
   933  		args                       args
   934  		testServer                 testServerConfig
   935  		wantErr                    bool
   936  	}{
   937  		{
   938  			name:                       "should fail when hook and request/response are not compatible",
   939  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig},
   940  			testServer: testServerConfig{
   941  				start: false,
   942  			},
   943  			args: args{
   944  				hook:     fakev1alpha1.SecondFakeHook,
   945  				request:  &fakev1alpha1.FakeRequest{},
   946  				response: &fakev1alpha1.FakeResponse{},
   947  			},
   948  			wantErr: true,
   949  		},
   950  		{
   951  			name:                       "should succeed when no ExtensionHandlers are registered for the hook",
   952  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{},
   953  			testServer: testServerConfig{
   954  				start: false,
   955  			},
   956  			args: args{
   957  				hook:     fakev1alpha1.FakeHook,
   958  				request:  &fakev1alpha1.FakeRequest{},
   959  				response: &fakev1alpha1.FakeResponse{},
   960  			},
   961  			wantErr: false,
   962  		},
   963  		{
   964  			name:                       "should succeed when calling ExtensionHandlers with success responses",
   965  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig},
   966  			testServer: testServerConfig{
   967  				start: true,
   968  				responses: map[string]testServerResponse{
   969  					"/*": response(runtimehooksv1.ResponseStatusSuccess),
   970  				},
   971  			},
   972  			args: args{
   973  				hook:     fakev1alpha1.FakeHook,
   974  				request:  &fakev1alpha1.FakeRequest{},
   975  				response: &fakev1alpha1.FakeResponse{},
   976  			},
   977  			wantErr: false,
   978  		},
   979  		{
   980  			name:                       "should fail when calling ExtensionHandlers with failure responses",
   981  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig},
   982  			testServer: testServerConfig{
   983  				start: true,
   984  				responses: map[string]testServerResponse{
   985  					"/*": response(runtimehooksv1.ResponseStatusFailure),
   986  				},
   987  			},
   988  			args: args{
   989  				hook:     fakev1alpha1.FakeHook,
   990  				request:  &fakev1alpha1.FakeRequest{},
   991  				response: &fakev1alpha1.FakeResponse{},
   992  			},
   993  			wantErr: true,
   994  		},
   995  		{
   996  			name:                       "should fail when one of the ExtensionHandlers returns a failure responses",
   997  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig},
   998  			testServer: testServerConfig{
   999  				start: true,
  1000  				responses: map[string]testServerResponse{
  1001  					"/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/first-extension.*":  response(runtimehooksv1.ResponseStatusSuccess),
  1002  					"/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/second-extension.*": response(runtimehooksv1.ResponseStatusFailure),
  1003  					"/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/third-extension.*":  response(runtimehooksv1.ResponseStatusSuccess),
  1004  				},
  1005  			},
  1006  			args: args{
  1007  				hook:     fakev1alpha1.FakeHook,
  1008  				request:  &fakev1alpha1.FakeRequest{},
  1009  				response: &fakev1alpha1.FakeResponse{},
  1010  			},
  1011  			wantErr: true,
  1012  		},
  1013  		{
  1014  			name:                       "should fail when one of the ExtensionHandlers returns 404",
  1015  			registeredExtensionConfigs: []runtimev1.ExtensionConfig{extensionConfig},
  1016  			testServer: testServerConfig{
  1017  				start: true,
  1018  				responses: map[string]testServerResponse{
  1019  					"/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/first-extension.*":  response(runtimehooksv1.ResponseStatusSuccess),
  1020  					"/test.runtime.cluster.x-k8s.io/v1alpha1/fakehook/second-extension.*": response(runtimehooksv1.ResponseStatusFailure),
  1021  					// third-extension has no handler.
  1022  				},
  1023  			},
  1024  			args: args{
  1025  				hook:     fakev1alpha1.FakeHook,
  1026  				request:  &fakev1alpha1.FakeRequest{},
  1027  				response: &fakev1alpha1.FakeResponse{},
  1028  			},
  1029  			wantErr: true,
  1030  		},
  1031  	}
  1032  	for _, tt := range tests {
  1033  		t.Run(tt.name, func(t *testing.T) {
  1034  			g := NewWithT(t)
  1035  
  1036  			if tt.testServer.start {
  1037  				srv := createSecureTestServer(tt.testServer)
  1038  				srv.StartTLS()
  1039  				defer srv.Close()
  1040  
  1041  				// Set the URL to the real address of the test server.
  1042  				for i := range tt.registeredExtensionConfigs {
  1043  					tt.registeredExtensionConfigs[i].Spec.ClientConfig.URL = ptr.To(fmt.Sprintf("https://%s/", srv.Listener.Addr().String()))
  1044  				}
  1045  			}
  1046  
  1047  			cat := runtimecatalog.New()
  1048  			_ = fakev1alpha1.AddToCatalog(cat)
  1049  			_ = fakev1alpha2.AddToCatalog(cat)
  1050  			fakeClient := fake.NewClientBuilder().
  1051  				WithObjects(ns).
  1052  				Build()
  1053  			c := New(Options{
  1054  				Catalog:  cat,
  1055  				Registry: registry(tt.registeredExtensionConfigs),
  1056  				Client:   fakeClient,
  1057  			})
  1058  
  1059  			obj := &clusterv1.Cluster{
  1060  				ObjectMeta: metav1.ObjectMeta{
  1061  					Name:      "cluster",
  1062  					Namespace: "foo",
  1063  				},
  1064  			}
  1065  			err := c.CallAllExtensions(context.Background(), tt.args.hook, obj, tt.args.request, tt.args.response)
  1066  
  1067  			if tt.wantErr {
  1068  				g.Expect(err).To(HaveOccurred())
  1069  			} else {
  1070  				g.Expect(err).ToNot(HaveOccurred())
  1071  			}
  1072  		})
  1073  	}
  1074  }
  1075  
  1076  func Test_client_matchNamespace(t *testing.T) {
  1077  	g := NewWithT(t)
  1078  	foo := &corev1.Namespace{
  1079  		TypeMeta: metav1.TypeMeta{
  1080  			Kind:       "Namespace",
  1081  			APIVersion: corev1.SchemeGroupVersion.String(),
  1082  		},
  1083  		ObjectMeta: metav1.ObjectMeta{
  1084  			Name: "foo",
  1085  			Labels: map[string]string{
  1086  				corev1.LabelMetadataName: "foo",
  1087  			},
  1088  		},
  1089  	}
  1090  	bar := &corev1.Namespace{
  1091  		TypeMeta: metav1.TypeMeta{
  1092  			Kind:       "Namespace",
  1093  			APIVersion: corev1.SchemeGroupVersion.String(),
  1094  		},
  1095  		ObjectMeta: metav1.ObjectMeta{
  1096  			Name: "bar",
  1097  			Labels: map[string]string{
  1098  				corev1.LabelMetadataName: "bar",
  1099  			},
  1100  		},
  1101  	}
  1102  	matchingMatchExpressions, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
  1103  		MatchExpressions: []metav1.LabelSelectorRequirement{
  1104  			{
  1105  				Key:      corev1.LabelMetadataName,
  1106  				Operator: metav1.LabelSelectorOpIn,
  1107  				Values:   []string{"foo", "bar"},
  1108  			},
  1109  		},
  1110  	})
  1111  	g.Expect(err).ToNot(HaveOccurred())
  1112  	notMatchingMatchExpressions, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{
  1113  		MatchExpressions: []metav1.LabelSelectorRequirement{
  1114  			{
  1115  				Key:      corev1.LabelMetadataName,
  1116  				Operator: metav1.LabelSelectorOpIn,
  1117  				Values:   []string{"non-existing", "other"},
  1118  			},
  1119  		},
  1120  	})
  1121  	g.Expect(err).ToNot(HaveOccurred())
  1122  	tests := []struct {
  1123  		name               string
  1124  		selector           labels.Selector
  1125  		namespace          string
  1126  		existingNamespaces []ctrlclient.Object
  1127  		want               bool
  1128  		wantErr            bool
  1129  	}{
  1130  		{
  1131  			name:               "match with single label selector",
  1132  			selector:           labels.SelectorFromSet(labels.Set{corev1.LabelMetadataName: foo.Name}),
  1133  			namespace:          "foo",
  1134  			existingNamespaces: []ctrlclient.Object{foo, bar},
  1135  			want:               true,
  1136  			wantErr:            false,
  1137  		},
  1138  		{
  1139  			name:               "error with non-existent namespace",
  1140  			selector:           labels.SelectorFromSet(labels.Set{corev1.LabelMetadataName: foo.Name}),
  1141  			namespace:          "non-existent",
  1142  			existingNamespaces: []ctrlclient.Object{foo, bar},
  1143  			want:               false,
  1144  			wantErr:            true,
  1145  		},
  1146  		{
  1147  			name:               "doesn't match if namespaceSelector doesn't match namespace",
  1148  			selector:           labels.SelectorFromSet(labels.Set{corev1.LabelMetadataName: bar.Name}),
  1149  			namespace:          "foo",
  1150  			existingNamespaces: []ctrlclient.Object{foo, bar},
  1151  			want:               false,
  1152  			wantErr:            false,
  1153  		},
  1154  		{
  1155  			name:               "match if match expressions match namespace",
  1156  			selector:           matchingMatchExpressions,
  1157  			namespace:          "bar",
  1158  			existingNamespaces: []ctrlclient.Object{foo, bar},
  1159  			want:               true,
  1160  			wantErr:            false,
  1161  		},
  1162  		{
  1163  			name:               "doesn't match if match expressions doesn't match namespace",
  1164  			selector:           notMatchingMatchExpressions,
  1165  			namespace:          "foo",
  1166  			existingNamespaces: []ctrlclient.Object{foo, bar},
  1167  			want:               false,
  1168  			wantErr:            false,
  1169  		},
  1170  	}
  1171  	for _, tt := range tests {
  1172  		t.Run(tt.name, func(t *testing.T) {
  1173  			c := client{
  1174  				client: fake.NewClientBuilder().
  1175  					WithObjects(tt.existingNamespaces...).
  1176  					Build(),
  1177  			}
  1178  			got, err := c.matchNamespace(context.Background(), tt.selector, tt.namespace)
  1179  			if (err != nil) != tt.wantErr {
  1180  				t.Errorf("matchNamespace() error = %v, wantErr %v", err, tt.wantErr)
  1181  				return
  1182  			}
  1183  			if got != tt.want {
  1184  				t.Errorf("matchNamespace() got = %v, want %v", got, tt.want)
  1185  			}
  1186  		})
  1187  	}
  1188  }
  1189  
  1190  func Test_aggregateResponses(t *testing.T) {
  1191  	tests := []struct {
  1192  		name              string
  1193  		aggregateResponse runtimehooksv1.ResponseObject
  1194  		responses         []runtimehooksv1.ResponseObject
  1195  		want              runtimehooksv1.ResponseObject
  1196  	}{
  1197  		{
  1198  			name:              "Aggregate response if there is only one response",
  1199  			aggregateResponse: fakeSuccessResponse(""),
  1200  			responses: []runtimehooksv1.ResponseObject{
  1201  				fakeSuccessResponse("test"),
  1202  			},
  1203  			want: fakeSuccessResponse("test"),
  1204  		},
  1205  		{
  1206  			name:              "Aggregate retry response if there is only one response",
  1207  			aggregateResponse: fakeRetryableSuccessResponse(0, ""),
  1208  			responses: []runtimehooksv1.ResponseObject{
  1209  				fakeRetryableSuccessResponse(5, "test"),
  1210  			},
  1211  			want: fakeRetryableSuccessResponse(5, "test"),
  1212  		},
  1213  		{
  1214  			name:              "Aggregate retry responses to lowest non-zero retryAfterSeconds value",
  1215  			aggregateResponse: fakeRetryableSuccessResponse(0, ""),
  1216  			responses: []runtimehooksv1.ResponseObject{
  1217  				fakeRetryableSuccessResponse(0, "test1"),
  1218  				fakeRetryableSuccessResponse(1, "test2"),
  1219  				fakeRetryableSuccessResponse(5, ""),
  1220  				fakeRetryableSuccessResponse(4, ""),
  1221  				fakeRetryableSuccessResponse(3, ""),
  1222  			},
  1223  			want: fakeRetryableSuccessResponse(1, "test1, test2"),
  1224  		},
  1225  	}
  1226  	for _, tt := range tests {
  1227  		t.Run(tt.name, func(t *testing.T) {
  1228  			aggregateSuccessfulResponses(tt.aggregateResponse, tt.responses)
  1229  
  1230  			if !reflect.DeepEqual(tt.aggregateResponse, tt.want) {
  1231  				t.Errorf("aggregateSuccessfulResponses() got = %v, want %v", tt.aggregateResponse, tt.want)
  1232  			}
  1233  		})
  1234  	}
  1235  }
  1236  
  1237  type testServerConfig struct {
  1238  	start     bool
  1239  	responses map[string]testServerResponse
  1240  }
  1241  
  1242  type testServerResponse struct {
  1243  	response           runtime.Object
  1244  	responseStatusCode int
  1245  }
  1246  
  1247  func response(status runtimehooksv1.ResponseStatus) testServerResponse {
  1248  	return testServerResponse{
  1249  		response: &fakev1alpha1.FakeResponse{
  1250  			CommonResponse: runtimehooksv1.CommonResponse{
  1251  				Status: status,
  1252  			},
  1253  		},
  1254  		responseStatusCode: http.StatusOK,
  1255  	}
  1256  }
  1257  
  1258  func createSecureTestServer(server testServerConfig) *httptest.Server {
  1259  	mux := http.NewServeMux()
  1260  	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  1261  		// Write the response for the first match in tt.testServer.responses.
  1262  		for pathRegex, resp := range server.responses {
  1263  			if !regexp.MustCompile(pathRegex).MatchString(r.URL.Path) {
  1264  				continue
  1265  			}
  1266  
  1267  			respBody, err := json.Marshal(resp.response)
  1268  			if err != nil {
  1269  				panic(err)
  1270  			}
  1271  			w.WriteHeader(resp.responseStatusCode)
  1272  			_, _ = w.Write(respBody)
  1273  			return
  1274  		}
  1275  
  1276  		// Otherwise write a 404.
  1277  		w.WriteHeader(http.StatusNotFound)
  1278  	})
  1279  
  1280  	srv := newUnstartedTLSServer(mux)
  1281  
  1282  	return srv
  1283  }
  1284  
  1285  func registry(configs []runtimev1.ExtensionConfig) runtimeregistry.ExtensionRegistry {
  1286  	registry := runtimeregistry.New()
  1287  	err := registry.WarmUp(&runtimev1.ExtensionConfigList{
  1288  		Items: configs,
  1289  	})
  1290  	if err != nil {
  1291  		panic(err)
  1292  	}
  1293  	return registry
  1294  }
  1295  
  1296  func fakeSuccessResponse(message string) *fakev1alpha1.FakeResponse {
  1297  	return &fakev1alpha1.FakeResponse{
  1298  		TypeMeta: metav1.TypeMeta{
  1299  			Kind:       "FakeResponse",
  1300  			APIVersion: "v1alpha1",
  1301  		},
  1302  		CommonResponse: runtimehooksv1.CommonResponse{
  1303  			Message: message,
  1304  			Status:  runtimehooksv1.ResponseStatusSuccess,
  1305  		},
  1306  	}
  1307  }
  1308  
  1309  func fakeRetryableSuccessResponse(retryAfterSeconds int32, message string) *fakev1alpha1.RetryableFakeResponse {
  1310  	return &fakev1alpha1.RetryableFakeResponse{
  1311  		TypeMeta: metav1.TypeMeta{
  1312  			Kind:       "FakeResponse",
  1313  			APIVersion: "v1alpha1",
  1314  		},
  1315  		CommonResponse: runtimehooksv1.CommonResponse{
  1316  			Message: message,
  1317  			Status:  runtimehooksv1.ResponseStatusSuccess,
  1318  		},
  1319  		CommonRetryResponse: runtimehooksv1.CommonRetryResponse{
  1320  			RetryAfterSeconds: retryAfterSeconds,
  1321  		},
  1322  	}
  1323  }
  1324  
  1325  func newUnstartedTLSServer(handler http.Handler) *httptest.Server {
  1326  	cert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey)
  1327  	if err != nil {
  1328  		panic(err)
  1329  	}
  1330  	srv := httptest.NewUnstartedServer(handler)
  1331  	srv.TLS = &tls.Config{
  1332  		MinVersion:   tls.VersionTLS13,
  1333  		Certificates: []tls.Certificate{cert},
  1334  	}
  1335  	return srv
  1336  }
  1337  
  1338  func TestNameForHandler(t *testing.T) {
  1339  	tests := []struct {
  1340  		name            string
  1341  		handler         runtimehooksv1.ExtensionHandler
  1342  		extensionConfig *runtimev1.ExtensionConfig
  1343  		want            string
  1344  		wantErr         bool
  1345  	}{
  1346  		{
  1347  			name:            "return well formatted name",
  1348  			handler:         runtimehooksv1.ExtensionHandler{Name: "discover-variables"},
  1349  			extensionConfig: &runtimev1.ExtensionConfig{ObjectMeta: metav1.ObjectMeta{Name: "runtime1"}},
  1350  			want:            "discover-variables.runtime1",
  1351  		},
  1352  		{
  1353  			name:            "return well formatted name",
  1354  			handler:         runtimehooksv1.ExtensionHandler{Name: "discover-variables"},
  1355  			extensionConfig: nil,
  1356  			wantErr:         true,
  1357  		},
  1358  	}
  1359  	for _, tt := range tests {
  1360  		t.Run(tt.name, func(t *testing.T) {
  1361  			got, err := NameForHandler(tt.handler, tt.extensionConfig)
  1362  			if (err != nil) != tt.wantErr {
  1363  				t.Errorf("NameForHandler() error = %v, wantErr %v", err, tt.wantErr)
  1364  				return
  1365  			}
  1366  			if got != tt.want {
  1367  				t.Errorf("NameForHandler() got = %v, want %v", got, tt.want)
  1368  			}
  1369  		})
  1370  	}
  1371  }
  1372  
  1373  func TestExtensionNameFromHandlerName(t *testing.T) {
  1374  	tests := []struct {
  1375  		name                  string
  1376  		registeredHandlerName string
  1377  		want                  string
  1378  		wantErr               bool
  1379  	}{
  1380  		{
  1381  			name:                  "Get name from correctly formatted handler name",
  1382  			registeredHandlerName: "discover-variables.runtime1",
  1383  			want:                  "runtime1",
  1384  		},
  1385  		{
  1386  			name: "error from incorrectly formatted handler name",
  1387  			// Two periods make this name badly formed.
  1388  			registeredHandlerName: "discover-variables.runtime.1",
  1389  			wantErr:               true,
  1390  		},
  1391  	}
  1392  	for _, tt := range tests {
  1393  		t.Run(tt.name, func(t *testing.T) {
  1394  			got, err := ExtensionNameFromHandlerName(tt.registeredHandlerName)
  1395  			if (err != nil) != tt.wantErr {
  1396  				t.Errorf("ExtensionNameFromHandlerName() error = %v, wantErr %v", err, tt.wantErr)
  1397  				return
  1398  			}
  1399  			if got != tt.want {
  1400  				t.Errorf("ExtensionNameFromHandlerName() got = %v, want %v", got, tt.want)
  1401  			}
  1402  		})
  1403  	}
  1404  }