sigs.k8s.io/cluster-api@v1.6.3/internal/test/envtest/environment.go (about) 1 /* 2 Copyright 2020 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 envtest 18 19 import ( 20 "context" 21 "fmt" 22 "net" 23 "os" 24 "path" 25 "path/filepath" 26 goruntime "runtime" 27 "strconv" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/onsi/ginkgo/v2" 33 "github.com/pkg/errors" 34 admissionv1 "k8s.io/api/admissionregistration/v1" 35 corev1 "k8s.io/api/core/v1" 36 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 37 apierrors "k8s.io/apimachinery/pkg/api/errors" 38 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 39 kerrors "k8s.io/apimachinery/pkg/util/errors" 40 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 41 "k8s.io/apimachinery/pkg/util/wait" 42 "k8s.io/client-go/kubernetes/scheme" 43 "k8s.io/client-go/rest" 44 "k8s.io/klog/v2" 45 "k8s.io/klog/v2/klogr" 46 ctrl "sigs.k8s.io/controller-runtime" 47 "sigs.k8s.io/controller-runtime/pkg/client" 48 "sigs.k8s.io/controller-runtime/pkg/envtest" 49 "sigs.k8s.io/controller-runtime/pkg/manager" 50 metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 51 "sigs.k8s.io/controller-runtime/pkg/webhook" 52 53 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 54 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" 55 bootstrapwebhooks "sigs.k8s.io/cluster-api/bootstrap/kubeadm/webhooks" 56 "sigs.k8s.io/cluster-api/cmd/clusterctl/log" 57 controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" 58 controlplanewebhooks "sigs.k8s.io/cluster-api/controlplane/kubeadm/webhooks" 59 addonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1" 60 addonswebhooks "sigs.k8s.io/cluster-api/exp/addons/webhooks" 61 expv1 "sigs.k8s.io/cluster-api/exp/api/v1beta1" 62 ipamv1 "sigs.k8s.io/cluster-api/exp/ipam/api/v1beta1" 63 expipamwebhooks "sigs.k8s.io/cluster-api/exp/ipam/webhooks" 64 runtimev1 "sigs.k8s.io/cluster-api/exp/runtime/api/v1alpha1" 65 expapiwebhooks "sigs.k8s.io/cluster-api/exp/webhooks" 66 "sigs.k8s.io/cluster-api/internal/test/builder" 67 internalwebhooks "sigs.k8s.io/cluster-api/internal/webhooks" 68 runtimewebhooks "sigs.k8s.io/cluster-api/internal/webhooks/runtime" 69 "sigs.k8s.io/cluster-api/util/kubeconfig" 70 "sigs.k8s.io/cluster-api/version" 71 "sigs.k8s.io/cluster-api/webhooks" 72 ) 73 74 func init() { 75 klog.InitFlags(nil) 76 logger := klogr.New() 77 // Use klog as the internal logger for this envtest environment. 78 log.SetLogger(logger) 79 // Additionally force all of the controllers to use the Ginkgo logger. 80 ctrl.SetLogger(logger) 81 // Add logger for ginkgo. 82 klog.SetOutput(ginkgo.GinkgoWriter) 83 84 // Calculate the scheme. 85 utilruntime.Must(apiextensionsv1.AddToScheme(scheme.Scheme)) 86 utilruntime.Must(admissionv1.AddToScheme(scheme.Scheme)) 87 utilruntime.Must(clusterv1.AddToScheme(scheme.Scheme)) 88 utilruntime.Must(bootstrapv1.AddToScheme(scheme.Scheme)) 89 utilruntime.Must(expv1.AddToScheme(scheme.Scheme)) 90 utilruntime.Must(addonsv1.AddToScheme(scheme.Scheme)) 91 utilruntime.Must(controlplanev1.AddToScheme(scheme.Scheme)) 92 utilruntime.Must(admissionv1.AddToScheme(scheme.Scheme)) 93 utilruntime.Must(runtimev1.AddToScheme(scheme.Scheme)) 94 utilruntime.Must(ipamv1.AddToScheme(scheme.Scheme)) 95 } 96 97 // RunInput is the input for Run. 98 type RunInput struct { 99 M *testing.M 100 ManagerUncachedObjs []client.Object 101 SetupIndexes func(ctx context.Context, mgr ctrl.Manager) 102 SetupReconcilers func(ctx context.Context, mgr ctrl.Manager) 103 SetupEnv func(e *Environment) 104 MinK8sVersion string 105 } 106 107 // Run executes the tests of the given testing.M in a test environment. 108 // Note: The environment will be created in this func and should not be created before. This func takes a *Environment 109 // 110 // because our tests require access to the *Environment. We use this field to make the created Environment available 111 // to the consumer. 112 // 113 // Note: Test environment creation can be skipped by setting the environment variable `CAPI_DISABLE_TEST_ENV` 114 // to a non-empty value. This only makes sense when executing tests which don't require the test environment, 115 // e.g. tests using only the fake client. 116 // Note: It's possible to write a kubeconfig for the test environment to a file by setting `CAPI_TEST_ENV_KUBECONFIG`. 117 // Note: It's possible to skip stopping the test env after the tests have been run by setting `CAPI_TEST_ENV_SKIP_STOP` 118 // to a non-empty value. 119 func Run(ctx context.Context, input RunInput) int { 120 if os.Getenv("CAPI_DISABLE_TEST_ENV") != "" { 121 klog.Info("Skipping test env start as CAPI_DISABLE_TEST_ENV is set") 122 return input.M.Run() 123 } 124 125 // Bootstrapping test environment 126 env := newEnvironment(input.ManagerUncachedObjs...) 127 128 if input.SetupIndexes != nil { 129 input.SetupIndexes(ctx, env.Manager) 130 } 131 if input.SetupReconcilers != nil { 132 input.SetupReconcilers(ctx, env.Manager) 133 } 134 135 // Start the environment. 136 env.start(ctx) 137 138 if kubeconfigPath := os.Getenv("CAPI_TEST_ENV_KUBECONFIG"); kubeconfigPath != "" { 139 klog.Infof("Writing test env kubeconfig to %q", kubeconfigPath) 140 config := kubeconfig.FromEnvTestConfig(env.Config, &clusterv1.Cluster{ 141 ObjectMeta: metav1.ObjectMeta{Name: "test"}, 142 }) 143 if err := os.WriteFile(kubeconfigPath, config, 0600); err != nil { 144 panic(errors.Wrapf(err, "failed to write the test env kubeconfig")) 145 } 146 } 147 148 if input.MinK8sVersion != "" { 149 if err := version.CheckKubernetesVersion(env.Config, input.MinK8sVersion); err != nil { 150 fmt.Printf("[ERROR] Cannot run tests after failing version check: %v\n", err) 151 if err := env.stop(); err != nil { 152 fmt.Println("[WARNING] Failed to stop the test environment") 153 } 154 return 1 155 } 156 } 157 158 // Expose the environment. 159 input.SetupEnv(env) 160 161 // Run tests 162 code := input.M.Run() 163 164 if skipStop := os.Getenv("CAPI_TEST_ENV_SKIP_STOP"); skipStop != "" { 165 klog.Info("Skipping test env stop as CAPI_TEST_ENV_SKIP_STOP is set") 166 return code 167 } 168 169 // Tearing down the test environment 170 if err := env.stop(); err != nil { 171 panic(fmt.Sprintf("Failed to stop the test environment: %v", err)) 172 } 173 return code 174 } 175 176 var ( 177 cacheSyncBackoff = wait.Backoff{ 178 Duration: 100 * time.Millisecond, 179 Factor: 1.5, 180 Steps: 8, 181 Jitter: 0.4, 182 } 183 ) 184 185 // Environment encapsulates a Kubernetes local test environment. 186 type Environment struct { 187 manager.Manager 188 client.Client 189 Config *rest.Config 190 191 env *envtest.Environment 192 cancelManager context.CancelFunc 193 } 194 195 // newEnvironment creates a new environment spinning up a local api-server. 196 // 197 // This function should be called only once for each package you're running tests within, 198 // usually the environment is initialized in a suite_test.go file within a `BeforeSuite` ginkgo block. 199 func newEnvironment(uncachedObjs ...client.Object) *Environment { 200 // Get the root of the current file to use in CRD paths. 201 _, filename, _, _ := goruntime.Caller(0) //nolint:dogsled 202 root := path.Join(path.Dir(filename), "..", "..", "..") 203 204 // Create the test environment. 205 env := &envtest.Environment{ 206 ErrorIfCRDPathMissing: true, 207 CRDDirectoryPaths: []string{ 208 filepath.Join(root, "config", "crd", "bases"), 209 filepath.Join(root, "controlplane", "kubeadm", "config", "crd", "bases"), 210 filepath.Join(root, "bootstrap", "kubeadm", "config", "crd", "bases"), 211 }, 212 CRDs: []*apiextensionsv1.CustomResourceDefinition{ 213 builder.GenericBootstrapConfigCRD.DeepCopy(), 214 builder.GenericBootstrapConfigTemplateCRD.DeepCopy(), 215 builder.GenericControlPlaneCRD.DeepCopy(), 216 builder.GenericControlPlaneTemplateCRD.DeepCopy(), 217 builder.GenericInfrastructureMachineCRD.DeepCopy(), 218 builder.GenericInfrastructureMachineTemplateCRD.DeepCopy(), 219 builder.GenericInfrastructureMachinePoolCRD.DeepCopy(), 220 builder.GenericInfrastructureMachinePoolTemplateCRD.DeepCopy(), 221 builder.GenericInfrastructureClusterCRD.DeepCopy(), 222 builder.GenericInfrastructureClusterTemplateCRD.DeepCopy(), 223 builder.GenericRemediationCRD.DeepCopy(), 224 builder.GenericRemediationTemplateCRD.DeepCopy(), 225 builder.TestInfrastructureClusterTemplateCRD.DeepCopy(), 226 builder.TestInfrastructureClusterCRD.DeepCopy(), 227 builder.TestInfrastructureMachineTemplateCRD.DeepCopy(), 228 builder.TestInfrastructureMachinePoolCRD.DeepCopy(), 229 builder.TestInfrastructureMachinePoolTemplateCRD.DeepCopy(), 230 builder.TestInfrastructureMachineCRD.DeepCopy(), 231 builder.TestBootstrapConfigTemplateCRD.DeepCopy(), 232 builder.TestBootstrapConfigCRD.DeepCopy(), 233 builder.TestControlPlaneTemplateCRD.DeepCopy(), 234 builder.TestControlPlaneCRD.DeepCopy(), 235 }, 236 // initialize webhook here to be able to test the envtest install via webhookOptions 237 // This should set LocalServingCertDir and LocalServingPort that are used below. 238 WebhookInstallOptions: initWebhookInstallOptions(), 239 } 240 241 if _, err := env.Start(); err != nil { 242 err = kerrors.NewAggregate([]error{err, env.Stop()}) 243 panic(err) 244 } 245 246 // Localhost is used on MacOS to avoid Firewall warning popups. 247 host := "localhost" 248 if strings.EqualFold(os.Getenv("USE_EXISTING_CLUSTER"), "true") { 249 // 0.0.0.0 is required on Linux when using kind because otherwise the kube-apiserver running in kind 250 // is unable to reach the webhook, because the webhook would be only listening on 127.0.0.1. 251 // Somehow that's not an issue on MacOS. 252 if goruntime.GOOS == "linux" { 253 host = "0.0.0.0" 254 } 255 } 256 257 options := manager.Options{ 258 Scheme: scheme.Scheme, 259 Metrics: metricsserver.Options{ 260 BindAddress: "0", 261 }, 262 Client: client.Options{ 263 Cache: &client.CacheOptions{ 264 DisableFor: uncachedObjs, 265 }, 266 }, 267 WebhookServer: webhook.NewServer( 268 webhook.Options{ 269 Port: env.WebhookInstallOptions.LocalServingPort, 270 CertDir: env.WebhookInstallOptions.LocalServingCertDir, 271 Host: host, 272 }, 273 ), 274 } 275 276 mgr, err := ctrl.NewManager(env.Config, options) 277 if err != nil { 278 klog.Fatalf("Failed to start testenv manager: %v", err) 279 } 280 281 // Set minNodeStartupTimeout for Test, so it does not need to be at least 30s 282 internalwebhooks.SetMinNodeStartupTimeout(metav1.Duration{Duration: 1 * time.Millisecond}) 283 284 if err := (&webhooks.Cluster{Client: mgr.GetClient()}).SetupWebhookWithManager(mgr); err != nil { 285 klog.Fatalf("unable to create webhook: %+v", err) 286 } 287 if err := (&webhooks.ClusterClass{Client: mgr.GetClient()}).SetupWebhookWithManager(mgr); err != nil { 288 klog.Fatalf("unable to create webhook: %+v", err) 289 } 290 if err := (&webhooks.Machine{}).SetupWebhookWithManager(mgr); err != nil { 291 klog.Fatalf("unable to create webhook: %+v", err) 292 } 293 if err := (&webhooks.MachineHealthCheck{}).SetupWebhookWithManager(mgr); err != nil { 294 klog.Fatalf("unable to create webhook: %+v", err) 295 } 296 if err := (&webhooks.Machine{}).SetupWebhookWithManager(mgr); err != nil { 297 klog.Fatalf("unable to create webhook: %+v", err) 298 } 299 if err := (&webhooks.MachineSet{}).SetupWebhookWithManager(mgr); err != nil { 300 klog.Fatalf("unable to create webhook: %+v", err) 301 } 302 if err := (&webhooks.MachineDeployment{}).SetupWebhookWithManager(mgr); err != nil { 303 klog.Fatalf("unable to create webhook: %+v", err) 304 } 305 if err := (&bootstrapwebhooks.KubeadmConfig{}).SetupWebhookWithManager(mgr); err != nil { 306 klog.Fatalf("unable to create webhook: %+v", err) 307 } 308 if err := (&bootstrapwebhooks.KubeadmConfigTemplate{}).SetupWebhookWithManager(mgr); err != nil { 309 klog.Fatalf("unable to create webhook: %+v", err) 310 } 311 if err := (&controlplanewebhooks.KubeadmControlPlaneTemplate{}).SetupWebhookWithManager(mgr); err != nil { 312 klog.Fatalf("unable to create webhook: %+v", err) 313 } 314 if err := (&controlplanewebhooks.KubeadmControlPlane{}).SetupWebhookWithManager(mgr); err != nil { 315 klog.Fatalf("unable to create webhook: %+v", err) 316 } 317 if err := (&addonswebhooks.ClusterResourceSet{}).SetupWebhookWithManager(mgr); err != nil { 318 klog.Fatalf("unable to create webhook for crs: %+v", err) 319 } 320 if err := (&addonswebhooks.ClusterResourceSetBinding{}).SetupWebhookWithManager(mgr); err != nil { 321 klog.Fatalf("unable to create webhook for ClusterResourceSetBinding: %+v", err) 322 } 323 if err := (&expapiwebhooks.MachinePool{}).SetupWebhookWithManager(mgr); err != nil { 324 klog.Fatalf("unable to create webhook for machinepool: %+v", err) 325 } 326 if err := (&runtimewebhooks.ExtensionConfig{}).SetupWebhookWithManager(mgr); err != nil { 327 klog.Fatalf("unable to create webhook for extensionconfig: %+v", err) 328 } 329 if err := (&expipamwebhooks.IPAddress{}).SetupWebhookWithManager(mgr); err != nil { 330 klog.Fatalf("unable to create webhook for ipaddress: %v", err) 331 } 332 if err := (&expipamwebhooks.IPAddressClaim{}).SetupWebhookWithManager(mgr); err != nil { 333 klog.Fatalf("unable to create webhook for ipaddressclaim: %v", err) 334 } 335 336 return &Environment{ 337 Manager: mgr, 338 Client: mgr.GetClient(), 339 Config: mgr.GetConfig(), 340 env: env, 341 } 342 } 343 344 // start starts the manager. 345 func (e *Environment) start(ctx context.Context) { 346 ctx, cancel := context.WithCancel(ctx) 347 e.cancelManager = cancel 348 349 go func() { 350 fmt.Println("Starting the test environment manager") 351 if err := e.Manager.Start(ctx); err != nil { 352 panic(fmt.Sprintf("Failed to start the test environment manager: %v", err)) 353 } 354 }() 355 <-e.Manager.Elected() 356 e.waitForWebhooks() 357 } 358 359 // stop stops the test environment. 360 func (e *Environment) stop() error { 361 fmt.Println("Stopping the test environment") 362 e.cancelManager() 363 return e.env.Stop() 364 } 365 366 // waitForWebhooks waits for the webhook server to be available. 367 func (e *Environment) waitForWebhooks() { 368 port := e.env.WebhookInstallOptions.LocalServingPort 369 370 klog.V(2).Infof("Waiting for webhook port %d to be open prior to running tests", port) 371 timeout := 1 * time.Second 372 for { 373 time.Sleep(1 * time.Second) 374 conn, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(port)), timeout) 375 if err != nil { 376 klog.V(2).Infof("Webhook port is not ready, will retry in %v: %s", timeout, err) 377 continue 378 } 379 if err := conn.Close(); err != nil { 380 klog.V(2).Infof("Closing connection when testing if webhook port is ready failed: %v", err) 381 } 382 klog.V(2).Info("Webhook port is now open. Continuing with tests...") 383 return 384 } 385 } 386 387 // CreateKubeconfigSecret generates a new Kubeconfig secret from the envtest config. 388 func (e *Environment) CreateKubeconfigSecret(ctx context.Context, cluster *clusterv1.Cluster) error { 389 return e.Create(ctx, kubeconfig.GenerateSecret(cluster, kubeconfig.FromEnvTestConfig(e.Config, cluster))) 390 } 391 392 // Cleanup deletes all the given objects. 393 func (e *Environment) Cleanup(ctx context.Context, objs ...client.Object) error { 394 errs := []error{} 395 for _, o := range objs { 396 err := e.Client.Delete(ctx, o) 397 if apierrors.IsNotFound(err) { 398 continue 399 } 400 errs = append(errs, err) 401 } 402 return kerrors.NewAggregate(errs) 403 } 404 405 // CleanupAndWait deletes all the given objects and waits for the cache to be updated accordingly. 406 // 407 // NOTE: Waiting for the cache to be updated helps in preventing test flakes due to the cache sync delays. 408 func (e *Environment) CleanupAndWait(ctx context.Context, objs ...client.Object) error { 409 if err := e.Cleanup(ctx, objs...); err != nil { 410 return err 411 } 412 413 // Makes sure the cache is updated with the deleted object 414 errs := []error{} 415 for _, o := range objs { 416 // Ignoring namespaces because in testenv the namespace cleaner is not running. 417 if o.GetObjectKind().GroupVersionKind().GroupKind() == corev1.SchemeGroupVersion.WithKind("Namespace").GroupKind() { 418 continue 419 } 420 421 oCopy := o.DeepCopyObject().(client.Object) 422 key := client.ObjectKeyFromObject(o) 423 err := wait.ExponentialBackoff( 424 cacheSyncBackoff, 425 func() (done bool, err error) { 426 if err := e.Get(ctx, key, oCopy); err != nil { 427 if apierrors.IsNotFound(err) { 428 return true, nil 429 } 430 return false, err 431 } 432 return false, nil 433 }) 434 errs = append(errs, errors.Wrapf(err, "key %s, %s is not being deleted from the testenv client cache", o.GetObjectKind().GroupVersionKind().String(), key)) 435 } 436 return kerrors.NewAggregate(errs) 437 } 438 439 // CreateAndWait creates the given object and waits for the cache to be updated accordingly. 440 // 441 // NOTE: Waiting for the cache to be updated helps in preventing test flakes due to the cache sync delays. 442 func (e *Environment) CreateAndWait(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { 443 if err := e.Client.Create(ctx, obj, opts...); err != nil { 444 return err 445 } 446 447 // Makes sure the cache is updated with the new object 448 objCopy := obj.DeepCopyObject().(client.Object) 449 key := client.ObjectKeyFromObject(obj) 450 if err := wait.ExponentialBackoff( 451 cacheSyncBackoff, 452 func() (done bool, err error) { 453 if err := e.Get(ctx, key, objCopy); err != nil { 454 if apierrors.IsNotFound(err) { 455 return false, nil 456 } 457 return false, err 458 } 459 return true, nil 460 }); err != nil { 461 return errors.Wrapf(err, "object %s, %s is not being added to the testenv client cache", obj.GetObjectKind().GroupVersionKind().String(), key) 462 } 463 return nil 464 } 465 466 // PatchAndWait creates or updates the given object using server-side apply and waits for the cache to be updated accordingly. 467 // 468 // NOTE: Waiting for the cache to be updated helps in preventing test flakes due to the cache sync delays. 469 func (e *Environment) PatchAndWait(ctx context.Context, obj client.Object, opts ...client.PatchOption) error { 470 key := client.ObjectKeyFromObject(obj) 471 objCopy := obj.DeepCopyObject().(client.Object) 472 if err := e.GetAPIReader().Get(ctx, key, objCopy); err != nil { 473 if !apierrors.IsNotFound(err) { 474 return err 475 } 476 } 477 // Store old resource version, empty string if not found. 478 oldResourceVersion := objCopy.GetResourceVersion() 479 480 if err := e.Client.Patch(ctx, obj, client.Apply, opts...); err != nil { 481 return err 482 } 483 484 // Makes sure the cache is updated with the new object 485 if err := wait.ExponentialBackoff( 486 cacheSyncBackoff, 487 func() (done bool, err error) { 488 if err := e.Get(ctx, key, objCopy); err != nil { 489 if apierrors.IsNotFound(err) { 490 return false, nil 491 } 492 return false, err 493 } 494 if objCopy.GetResourceVersion() == oldResourceVersion { 495 return false, nil 496 } 497 return true, nil 498 }); err != nil { 499 return errors.Wrapf(err, "object %s, %s is not being added to or did not get updated in the testenv client cache", obj.GetObjectKind().GroupVersionKind().String(), key) 500 } 501 return nil 502 } 503 504 // CreateNamespace creates a new namespace with a generated name. 505 func (e *Environment) CreateNamespace(ctx context.Context, generateName string) (*corev1.Namespace, error) { 506 ns := &corev1.Namespace{ 507 ObjectMeta: metav1.ObjectMeta{ 508 GenerateName: fmt.Sprintf("%s-", generateName), 509 Labels: map[string]string{ 510 "testenv/original-name": generateName, 511 }, 512 }, 513 } 514 if err := e.Client.Create(ctx, ns); err != nil { 515 return nil, err 516 } 517 518 return ns, nil 519 }