sigs.k8s.io/cluster-api@v1.7.1/exp/runtime/internal/controllers/extensionconfig_controller_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 controllers
    18  
    19  import (
    20  	"context"
    21  	"crypto/tls"
    22  	"encoding/json"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"testing"
    26  	"time"
    27  
    28  	. "github.com/onsi/gomega"
    29  	"github.com/pkg/errors"
    30  	corev1 "k8s.io/api/core/v1"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
    34  	utilfeature "k8s.io/component-base/featuregate/testing"
    35  	"k8s.io/utils/ptr"
    36  	ctrl "sigs.k8s.io/controller-runtime"
    37  	"sigs.k8s.io/controller-runtime/pkg/client"
    38  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    39  
    40  	runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1"
    41  	runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
    42  	runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1"
    43  	"sigs.k8s.io/cluster-api/feature"
    44  	runtimeclient "sigs.k8s.io/cluster-api/internal/runtime/client"
    45  	runtimeregistry "sigs.k8s.io/cluster-api/internal/runtime/registry"
    46  	fakev1alpha1 "sigs.k8s.io/cluster-api/internal/runtime/test/v1alpha1"
    47  	"sigs.k8s.io/cluster-api/util"
    48  )
    49  
    50  func TestExtensionReconciler_Reconcile(t *testing.T) {
    51  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
    52  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)()
    53  
    54  	g := NewWithT(t)
    55  	ns, err := env.CreateNamespace(ctx, "test-extension-config")
    56  	g.Expect(err).ToNot(HaveOccurred())
    57  
    58  	cat := runtimecatalog.New()
    59  	g.Expect(fakev1alpha1.AddToCatalog(cat)).To(Succeed())
    60  
    61  	registry := runtimeregistry.New()
    62  	runtimeClient := runtimeclient.New(runtimeclient.Options{
    63  		Catalog:  cat,
    64  		Registry: registry,
    65  	})
    66  
    67  	g.Expect(runtimehooksv1.AddToCatalog(cat)).To(Succeed())
    68  
    69  	r := &Reconciler{
    70  		Client:        env.GetClient(),
    71  		APIReader:     env.GetAPIReader(),
    72  		RuntimeClient: runtimeClient,
    73  	}
    74  
    75  	caCertSecret := fakeCASecret(ns.Name, "ext1-webhook", testcerts.CACert)
    76  	server, err := fakeSecureExtensionServer(discoveryHandler("first", "second", "third"))
    77  	g.Expect(err).ToNot(HaveOccurred())
    78  	defer server.Close()
    79  	extensionConfig := fakeExtensionConfigForURL(ns.Name, "ext1", server.URL)
    80  	extensionConfig.Annotations[runtimev1.InjectCAFromSecretAnnotation] = caCertSecret.GetNamespace() + "/" + caCertSecret.GetName()
    81  
    82  	// Create the secret which contains the ca certificate.
    83  	g.Expect(env.CreateAndWait(ctx, caCertSecret)).To(Succeed())
    84  	defer func() {
    85  		g.Expect(env.CleanupAndWait(ctx, caCertSecret)).To(Succeed())
    86  	}()
    87  	// Create the ExtensionConfig.
    88  	g.Expect(env.CreateAndWait(ctx, extensionConfig)).To(Succeed())
    89  	defer func() {
    90  		g.Expect(env.CleanupAndWait(ctx, extensionConfig)).To(Succeed())
    91  	}()
    92  	t.Run("fail reconcile if registry has not been warmed up", func(*testing.T) {
    93  		// Attempt to reconcile. This will be an error as the registry has not been warmed up at this point.
    94  		res, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(extensionConfig)})
    95  		g.Expect(err).ToNot(HaveOccurred())
    96  		// If the registry isn't warm the reconcile loop will return Requeue: True
    97  		g.Expect(res.Requeue).To(BeTrue())
    98  	})
    99  
   100  	t.Run("successful reconcile and discovery on ExtensionConfig create", func(*testing.T) {
   101  		// Warm up the registry before trying reconciliation again.
   102  		warmup := &warmupRunnable{
   103  			Client:        env.GetClient(),
   104  			APIReader:     env.GetAPIReader(),
   105  			RuntimeClient: runtimeClient,
   106  		}
   107  		g.Expect(warmup.Start(ctx)).To(Succeed())
   108  
   109  		// Reconcile the extension and assert discovery has succeeded.
   110  		_, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(extensionConfig)})
   111  		g.Expect(err).ToNot(HaveOccurred())
   112  
   113  		config := &runtimev1.ExtensionConfig{}
   114  		g.Expect(env.GetAPIReader().Get(ctx, util.ObjectKey(extensionConfig), config)).To(Succeed())
   115  
   116  		// Expect three handlers for the extension and expect the name to be the handler name plus the extension name.
   117  		handlers := config.Status.Handlers
   118  		g.Expect(handlers).To(HaveLen(3))
   119  		g.Expect(handlers[0].Name).To(Equal("first.ext1"))
   120  		g.Expect(handlers[1].Name).To(Equal("second.ext1"))
   121  		g.Expect(handlers[2].Name).To(Equal("third.ext1"))
   122  
   123  		conditions := config.GetConditions()
   124  		g.Expect(conditions).To(HaveLen(1))
   125  		g.Expect(conditions[0].Status).To(Equal(corev1.ConditionTrue))
   126  		g.Expect(conditions[0].Type).To(Equal(runtimev1.RuntimeExtensionDiscoveredCondition))
   127  		_, err = registry.Get("first.ext1")
   128  		g.Expect(err).ToNot(HaveOccurred())
   129  		_, err = registry.Get("second.ext1")
   130  		g.Expect(err).ToNot(HaveOccurred())
   131  		_, err = registry.Get("third.ext1")
   132  		g.Expect(err).ToNot(HaveOccurred())
   133  	})
   134  
   135  	t.Run("Successful reconcile and discovery on Extension update", func(*testing.T) {
   136  		// Start a new ExtensionServer where the second handler is removed.
   137  		updatedServer, err := fakeSecureExtensionServer(discoveryHandler("first", "third"))
   138  		g.Expect(err).ToNot(HaveOccurred())
   139  		defer updatedServer.Close()
   140  		// Close the original server  it's no longer serving.
   141  		server.Close()
   142  
   143  		// Patch the extension with the new server endpoint.
   144  		patch := client.MergeFrom(extensionConfig.DeepCopy())
   145  		extensionConfig.Spec.ClientConfig.URL = &updatedServer.URL
   146  
   147  		g.Expect(env.Patch(ctx, extensionConfig, patch)).To(Succeed())
   148  
   149  		// Wait until the object is updated in the client cache before continuing.
   150  		g.Eventually(func() error {
   151  			conf := &runtimev1.ExtensionConfig{}
   152  			err := env.Get(ctx, util.ObjectKey(extensionConfig), conf)
   153  			if err != nil {
   154  				return err
   155  			}
   156  			if *conf.Spec.ClientConfig.URL != updatedServer.URL {
   157  				return errors.Errorf("URL not set on updated object: got: %s, want: %s", *conf.Spec.ClientConfig.URL, updatedServer.URL)
   158  			}
   159  			return nil
   160  		}, 30*time.Second, 100*time.Millisecond).Should(BeNil())
   161  
   162  		// Reconcile the extension and assert discovery has succeeded.
   163  		_, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(extensionConfig)})
   164  		g.Expect(err).ToNot(HaveOccurred())
   165  
   166  		var config runtimev1.ExtensionConfig
   167  		g.Expect(env.GetAPIReader().Get(ctx, util.ObjectKey(extensionConfig), &config)).To(Succeed())
   168  
   169  		// Expect two handlers for the extension and expect the name to be the handler name plus the extension name.
   170  		handlers := config.Status.Handlers
   171  		g.Expect(handlers).To(HaveLen(2))
   172  		g.Expect(handlers[0].Name).To(Equal("first.ext1"))
   173  		g.Expect(handlers[1].Name).To(Equal("third.ext1"))
   174  		conditions := config.GetConditions()
   175  		g.Expect(conditions).To(HaveLen(1))
   176  		g.Expect(conditions[0].Status).To(Equal(corev1.ConditionTrue))
   177  		g.Expect(conditions[0].Type).To(Equal(runtimev1.RuntimeExtensionDiscoveredCondition))
   178  
   179  		_, err = registry.Get("first.ext1")
   180  		g.Expect(err).ToNot(HaveOccurred())
   181  		_, err = registry.Get("third.ext1")
   182  		g.Expect(err).ToNot(HaveOccurred())
   183  
   184  		// Second should not be found in the registry:
   185  		_, err = registry.Get("second.ext1")
   186  		g.Expect(err).To(HaveOccurred())
   187  	})
   188  	t.Run("Successful reconcile and deregister on ExtensionConfig delete", func(*testing.T) {
   189  		g.Expect(env.CleanupAndWait(ctx, extensionConfig)).To(Succeed())
   190  		_, err = r.Reconcile(ctx, ctrl.Request{NamespacedName: util.ObjectKey(extensionConfig)})
   191  		g.Expect(env.Get(ctx, util.ObjectKey(extensionConfig), extensionConfig)).To(Not(Succeed()))
   192  		_, err = registry.Get("first.ext1")
   193  		g.Expect(err).To(HaveOccurred())
   194  		_, err = registry.Get("third.ext1")
   195  		g.Expect(err).To(HaveOccurred())
   196  	})
   197  }
   198  
   199  func TestExtensionReconciler_discoverExtensionConfig(t *testing.T) {
   200  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true)()
   201  	defer utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)()
   202  	g := NewWithT(t)
   203  	ns, err := env.CreateNamespace(ctx, "test-runtime-extension")
   204  	g.Expect(err).ToNot(HaveOccurred())
   205  
   206  	t.Run("test discovery of a single extension", func(*testing.T) {
   207  		cat := runtimecatalog.New()
   208  		g.Expect(fakev1alpha1.AddToCatalog(cat)).To(Succeed())
   209  
   210  		registry := runtimeregistry.New()
   211  		g.Expect(runtimehooksv1.AddToCatalog(cat)).To(Succeed())
   212  		extensionName := "ext1"
   213  		srv1, err := fakeSecureExtensionServer(discoveryHandler("first"))
   214  		g.Expect(err).ToNot(HaveOccurred())
   215  		defer srv1.Close()
   216  
   217  		runtimeClient := runtimeclient.New(runtimeclient.Options{
   218  			Catalog:  cat,
   219  			Registry: registry,
   220  		})
   221  
   222  		extensionConfig := fakeExtensionConfigForURL(ns.Name, extensionName, srv1.URL)
   223  		extensionConfig.Spec.ClientConfig.CABundle = testcerts.CACert
   224  
   225  		discoveredExtensionConfig, err := discoverExtensionConfig(ctx, runtimeClient, extensionConfig)
   226  		g.Expect(err).ToNot(HaveOccurred())
   227  
   228  		// Expect exactly one handler and expect the name to be the handler name plus the extension name.
   229  		handlers := discoveredExtensionConfig.Status.Handlers
   230  		g.Expect(handlers).To(HaveLen(1))
   231  		g.Expect(handlers[0].Name).To(Equal("first.ext1"))
   232  
   233  		// Expect exactly one condition and expect the condition to have type RuntimeExtensionDiscoveredCondition and
   234  		// Status true.
   235  		conditions := discoveredExtensionConfig.GetConditions()
   236  		g.Expect(conditions).To(HaveLen(1))
   237  		g.Expect(conditions[0].Status).To(Equal(corev1.ConditionTrue))
   238  		g.Expect(conditions[0].Type).To(Equal(runtimev1.RuntimeExtensionDiscoveredCondition))
   239  	})
   240  	t.Run("fail discovery for non-running extension", func(*testing.T) {
   241  		cat := runtimecatalog.New()
   242  		g.Expect(fakev1alpha1.AddToCatalog(cat)).To(Succeed())
   243  		registry := runtimeregistry.New()
   244  		g.Expect(runtimehooksv1.AddToCatalog(cat)).To(Succeed())
   245  		extensionName := "ext1"
   246  
   247  		// Don't set up a server to run the extensionDiscovery handler.
   248  		// srv1 := fakeSecureExtensionServer(discoveryHandler("first"))
   249  		// defer srv1.Close()
   250  
   251  		runtimeClient := runtimeclient.New(runtimeclient.Options{
   252  			Catalog:  cat,
   253  			Registry: registry,
   254  		})
   255  
   256  		extensionConfig := fakeExtensionConfigForURL(ns.Name, extensionName, "https://localhost:31239")
   257  		extensionConfig.Spec.ClientConfig.CABundle = testcerts.CACert
   258  
   259  		discoveredExtensionConfig, err := discoverExtensionConfig(ctx, runtimeClient, extensionConfig)
   260  		g.Expect(err).To(HaveOccurred())
   261  
   262  		// Expect exactly one handler and expect the name to be the handler name plus the extension name.
   263  		handlers := discoveredExtensionConfig.Status.Handlers
   264  		g.Expect(handlers).To(BeEmpty())
   265  
   266  		// Expect exactly one condition and expect the condition to have type RuntimeExtensionDiscoveredCondition and
   267  		// Status false.
   268  		conditions := discoveredExtensionConfig.GetConditions()
   269  		g.Expect(conditions).To(HaveLen(1))
   270  		g.Expect(conditions[0].Status).To(Equal(corev1.ConditionFalse))
   271  		g.Expect(conditions[0].Type).To(Equal(runtimev1.RuntimeExtensionDiscoveredCondition))
   272  	})
   273  }
   274  
   275  func Test_reconcileCABundle(t *testing.T) {
   276  	g := NewWithT(t)
   277  
   278  	scheme := runtime.NewScheme()
   279  	g.Expect(corev1.AddToScheme(scheme)).To(Succeed())
   280  
   281  	tests := []struct {
   282  		name         string
   283  		client       client.Client
   284  		config       *runtimev1.ExtensionConfig
   285  		wantCABundle []byte
   286  		wantErr      bool
   287  	}{
   288  		{
   289  			name:    "No-op because no annotation is set",
   290  			client:  fake.NewClientBuilder().WithScheme(scheme).Build(),
   291  			config:  fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "", ""),
   292  			wantErr: false,
   293  		},
   294  		{
   295  			name: "Inject ca-bundle",
   296  			client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
   297  				fakeCASecret("some-namespace", "some-ca-secret", []byte("some-ca-data")),
   298  			).Build(),
   299  			config:       fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
   300  			wantCABundle: []byte(`some-ca-data`),
   301  			wantErr:      false,
   302  		},
   303  		{
   304  			name: "Update ca-bundle",
   305  			client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
   306  				fakeCASecret("some-namespace", "some-ca-secret", []byte("some-new-data")),
   307  			).Build(),
   308  			config:       fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", "some-old-ca-data"),
   309  			wantCABundle: []byte(`some-new-data`),
   310  			wantErr:      false,
   311  		},
   312  		{
   313  			name:    "Fail because secret does not exist",
   314  			client:  fake.NewClientBuilder().WithScheme(scheme).WithObjects().Build(),
   315  			config:  fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
   316  			wantErr: true,
   317  		},
   318  		{
   319  			name: "Fail because secret does not contain a ca.crt",
   320  			client: fake.NewClientBuilder().WithScheme(scheme).WithObjects(
   321  				fakeCASecret("some-namespace", "some-ca-secret", nil),
   322  			).Build(),
   323  			config:  fakeCAInjectionRuntimeExtensionConfig("some-namespace", "some-extension-config", "some-namespace/some-ca-secret", ""),
   324  			wantErr: true,
   325  		},
   326  	}
   327  	for _, tt := range tests {
   328  		t.Run(tt.name, func(t *testing.T) {
   329  			g := NewWithT(t)
   330  
   331  			err := reconcileCABundle(context.TODO(), tt.client, tt.config)
   332  			g.Expect(err != nil).To(Equal(tt.wantErr))
   333  
   334  			g.Expect(tt.config.Spec.ClientConfig.CABundle).To(Equal(tt.wantCABundle))
   335  		})
   336  	}
   337  }
   338  
   339  func discoveryHandler(handlerList ...string) func(http.ResponseWriter, *http.Request) {
   340  	handlers := []runtimehooksv1.ExtensionHandler{}
   341  	for _, name := range handlerList {
   342  		handlers = append(handlers, runtimehooksv1.ExtensionHandler{
   343  			Name: name,
   344  			RequestHook: runtimehooksv1.GroupVersionHook{
   345  				Hook:       "FakeHook",
   346  				APIVersion: fakev1alpha1.GroupVersion.String(),
   347  			},
   348  		})
   349  	}
   350  	response := &runtimehooksv1.DiscoveryResponse{
   351  		TypeMeta: metav1.TypeMeta{
   352  			Kind:       "DiscoveryResponse",
   353  			APIVersion: runtimehooksv1.GroupVersion.String(),
   354  		},
   355  		Handlers: handlers,
   356  	}
   357  	respBody, err := json.Marshal(response)
   358  	if err != nil {
   359  		panic(err)
   360  	}
   361  
   362  	return func(w http.ResponseWriter, _ *http.Request) {
   363  		w.WriteHeader(http.StatusOK)
   364  		_, _ = w.Write(respBody)
   365  	}
   366  }
   367  
   368  func fakeExtensionConfigForURL(namespace, name, url string) *runtimev1.ExtensionConfig {
   369  	return &runtimev1.ExtensionConfig{
   370  		TypeMeta: metav1.TypeMeta{
   371  			Kind:       "ExtensionConfig",
   372  			APIVersion: runtimehooksv1.GroupVersion.String(),
   373  		},
   374  		ObjectMeta: metav1.ObjectMeta{
   375  			Name:        name,
   376  			Namespace:   namespace,
   377  			Annotations: map[string]string{},
   378  		},
   379  		Spec: runtimev1.ExtensionConfigSpec{
   380  			ClientConfig: runtimev1.ClientConfig{
   381  				URL: ptr.To(url),
   382  			},
   383  			NamespaceSelector: nil,
   384  		},
   385  	}
   386  }
   387  
   388  func fakeSecureExtensionServer(discoveryHandler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) {
   389  	mux := http.NewServeMux()
   390  	mux.HandleFunc("/", discoveryHandler)
   391  
   392  	sCert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  	testServer := httptest.NewUnstartedServer(mux)
   397  	testServer.TLS = &tls.Config{
   398  		MinVersion:   tls.VersionTLS13,
   399  		Certificates: []tls.Certificate{sCert},
   400  	}
   401  	testServer.StartTLS()
   402  
   403  	return testServer, nil
   404  }
   405  
   406  func fakeCASecret(namespace, name string, caData []byte) *corev1.Secret {
   407  	secret := &corev1.Secret{
   408  		ObjectMeta: metav1.ObjectMeta{
   409  			Name:      name,
   410  			Namespace: namespace,
   411  		},
   412  		Data: map[string][]byte{},
   413  	}
   414  	if caData != nil {
   415  		secret.Data["ca.crt"] = caData
   416  	}
   417  	return secret
   418  }
   419  
   420  func fakeCAInjectionRuntimeExtensionConfig(namespace, name, annotationString, caBundleData string) *runtimev1.ExtensionConfig {
   421  	ext := &runtimev1.ExtensionConfig{
   422  		ObjectMeta: metav1.ObjectMeta{
   423  			Name:        name,
   424  			Namespace:   namespace,
   425  			Annotations: map[string]string{},
   426  		},
   427  	}
   428  	if annotationString != "" {
   429  		ext.Annotations[runtimev1.InjectCAFromSecretAnnotation] = annotationString
   430  	}
   431  	if caBundleData != "" {
   432  		ext.Spec.ClientConfig.CABundle = []byte(caBundleData)
   433  	}
   434  	return ext
   435  }