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 }