From f109de6964b2c19db9b7906993d4435746a25dc8 Mon Sep 17 00:00:00 2001 From: fabriziopandini Date: Wed, 25 Jun 2025 21:08:56 +0200 Subject: [PATCH 1/4] Remove DefaulterRemoveUnknownOrOmitableFields mutating webhook option (again) --- .golangci.yml | 2 +- Makefile | 14 +- .../internal/webhooks/kubeadmconfig.go | 2 +- .../webhooks/kubeadmconfigtemplate.go | 4 +- controllers/crdmigrator/crd_migrator_test.go | 72 +- .../webhooks/kubeadm_control_plane.go | 2 +- .../webhooks/kubeadmcontrolplanetemplate.go | 2 +- internal/test/envtest/environment.go | 58 + .../upgrade/clusterctl_upgrade_test.go | 1174 +++++++++++++++++ internal/topology/upgrade/doc.go | 19 + internal/topology/upgrade/suite_test.go | 135 ++ .../test.cluster.x-k8s.io_testresources.yaml | 136 ++ ...luster.x-k8s.io_testresourcetemplates.yaml | 127 ++ .../test/t1/v1beta1/groupversion_info.go | 43 + .../topology/upgrade/test/t1/v1beta1/types.go | 147 +++ .../test/t1/v1beta1/zz_generated.deepcopy.go | 267 ++++ .../upgrade/test/t1/webhook/webhook.go | 90 ++ .../test.cluster.x-k8s.io_testresources.yaml | 216 +++ ...luster.x-k8s.io_testresourcetemplates.yaml | 238 ++++ .../upgrade/test/t2/v1beta1/conversion.go | 61 + .../topology/upgrade/test/t2/v1beta1/doc.go | 22 + .../test/t2/v1beta1/groupversion_info.go | 46 + .../topology/upgrade/test/t2/v1beta1/types.go | 141 ++ .../t2/v1beta1/zz_generated.conversion.go | 392 ++++++ .../test/t2/v1beta1/zz_generated.deepcopy.go | 266 ++++ .../upgrade/test/t2/v1beta2/conversion.go | 20 + .../test/t2/v1beta2/groupversion_info.go | 43 + .../topology/upgrade/test/t2/v1beta2/types.go | 146 ++ .../test/t2/v1beta2/zz_generated.deepcopy.go | 266 ++++ .../upgrade/test/t2/webhook/webhook.go | 90 ++ 30 files changed, 4166 insertions(+), 75 deletions(-) create mode 100644 internal/topology/upgrade/clusterctl_upgrade_test.go create mode 100644 internal/topology/upgrade/doc.go create mode 100644 internal/topology/upgrade/suite_test.go create mode 100644 internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml create mode 100644 internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml create mode 100644 internal/topology/upgrade/test/t1/v1beta1/groupversion_info.go create mode 100644 internal/topology/upgrade/test/t1/v1beta1/types.go create mode 100644 internal/topology/upgrade/test/t1/v1beta1/zz_generated.deepcopy.go create mode 100644 internal/topology/upgrade/test/t1/webhook/webhook.go create mode 100644 internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml create mode 100644 internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml create mode 100644 internal/topology/upgrade/test/t2/v1beta1/conversion.go create mode 100644 internal/topology/upgrade/test/t2/v1beta1/doc.go create mode 100644 internal/topology/upgrade/test/t2/v1beta1/groupversion_info.go create mode 100644 internal/topology/upgrade/test/t2/v1beta1/types.go create mode 100644 internal/topology/upgrade/test/t2/v1beta1/zz_generated.conversion.go create mode 100644 internal/topology/upgrade/test/t2/v1beta1/zz_generated.deepcopy.go create mode 100644 internal/topology/upgrade/test/t2/v1beta2/conversion.go create mode 100644 internal/topology/upgrade/test/t2/v1beta2/groupversion_info.go create mode 100644 internal/topology/upgrade/test/t2/v1beta2/types.go create mode 100644 internal/topology/upgrade/test/t2/v1beta2/zz_generated.deepcopy.go create mode 100644 internal/topology/upgrade/test/t2/webhook/webhook.go diff --git a/.golangci.yml b/.golangci.yml index f8a72a4a17ad..2a34fac657cc 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -354,7 +354,7 @@ linters: - linters: - revive # Ignoring stylistic checks for generated code - path: .*(api|types)\/.*\/conversion.*\.go$ + path: .*(api|types|test)\/.*\/conversion.*\.go$ # By convention, receiver names in a method should reflect their identity. text: 'receiver-naming: receiver name' - linters: diff --git a/Makefile b/Makefile index bee193bfa570..d7532f539444 100644 --- a/Makefile +++ b/Makefile @@ -307,6 +307,14 @@ generate-manifests-core: $(CONTROLLER_GEN) $(KUSTOMIZE) ## Generate manifests e. paths=./util/test/builder/... \ crd:crdVersions=v1 \ output:crd:dir=./util/test/builder/crd + $(CONTROLLER_GEN) \ + paths=./internal/topology/upgrade/test/t1/... \ + crd:crdVersions=v1 \ + output:crd:dir=./internal/topology/upgrade/test/t1/crd + $(CONTROLLER_GEN) \ + paths=./internal/topology/upgrade/test/t2/... \ + crd:crdVersions=v1 \ + output:crd:dir=./internal/topology/upgrade/test/t2/crd $(CONTROLLER_GEN) \ paths=./controllers/crdmigrator/test/t1/... \ crd:crdVersions=v1 \ @@ -403,6 +411,7 @@ generate-go-deepcopy-core: $(CONTROLLER_GEN) ## Generate deepcopy go code for co paths=./internal/runtime/test/... \ paths=./cmd/clusterctl/... \ paths=./util/test/builder/... \ + paths=./internal/topology/upgrade/test/... \ paths=./util/deprecated/v1beta1/test/builder/... \ paths=./controllers/crdmigrator/test/... @@ -454,13 +463,14 @@ generate-go-conversions-core: ## Run all generate-go-conversions-core-* targets .PHONY: generate-go-conversions-core-api generate-go-conversions-core-api: $(CONVERSION_GEN) ## Generate conversions go code for core api - $(MAKE) clean-generated-conversions SRC_DIRS="./api/core/v1beta1,./internal/api/core/v1alpha3,./internal/api/core/v1alpha4" + $(MAKE) clean-generated-conversions SRC_DIRS="./api/core/v1beta1,./internal/api/core/v1alpha3,./internal/api/core/v1alpha4,./internal/topology/upgrade/test/t2/v1beta1" $(CONVERSION_GEN) \ --output-file=zz_generated.conversion.go \ --go-header-file=./hack/boilerplate/boilerplate.generatego.txt \ ./internal/api/core/v1alpha3 \ ./internal/api/core/v1alpha4 \ - ./api/core/v1beta1 + ./api/core/v1beta1 \ + ./internal/topology/upgrade/test/t2/v1beta1 .PHONY: generate-go-conversions-addons-api generate-go-conversions-addons-api: $(CONVERSION_GEN) ## Generate conversions go code for addons api diff --git a/bootstrap/kubeadm/internal/webhooks/kubeadmconfig.go b/bootstrap/kubeadm/internal/webhooks/kubeadmconfig.go index 7a7c24936f48..fbd0655dd26c 100644 --- a/bootstrap/kubeadm/internal/webhooks/kubeadmconfig.go +++ b/bootstrap/kubeadm/internal/webhooks/kubeadmconfig.go @@ -33,7 +33,7 @@ import ( func (webhook *KubeadmConfig) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&bootstrapv1.KubeadmConfig{}). - WithDefaulter(webhook, admission.DefaulterRemoveUnknownOrOmitableFields). + WithDefaulter(webhook). WithValidator(webhook). Complete() } diff --git a/bootstrap/kubeadm/internal/webhooks/kubeadmconfigtemplate.go b/bootstrap/kubeadm/internal/webhooks/kubeadmconfigtemplate.go index 70212813645b..df808c7ad60b 100644 --- a/bootstrap/kubeadm/internal/webhooks/kubeadmconfigtemplate.go +++ b/bootstrap/kubeadm/internal/webhooks/kubeadmconfigtemplate.go @@ -21,7 +21,7 @@ import ( "fmt" apierrors "k8s.io/apimachinery/pkg/api/errors" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -32,7 +32,7 @@ import ( func (webhook *KubeadmConfigTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&bootstrapv1.KubeadmConfigTemplate{}). - WithDefaulter(webhook, admission.DefaulterRemoveUnknownOrOmitableFields). + WithDefaulter(webhook). WithValidator(webhook). Complete() } diff --git a/controllers/crdmigrator/crd_migrator_test.go b/controllers/crdmigrator/crd_migrator_test.go index 08cfbc188a4c..51acf4d76c06 100644 --- a/controllers/crdmigrator/crd_migrator_test.go +++ b/controllers/crdmigrator/crd_migrator_test.go @@ -18,7 +18,6 @@ package crdmigrator import ( "context" - "fmt" "path" "path/filepath" goruntime "runtime" @@ -29,7 +28,6 @@ import ( . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -38,14 +36,12 @@ import ( "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/util/sets" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/util/retry" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/config" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/manager" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -59,6 +55,7 @@ import ( ) func TestReconcile(t *testing.T) { + _, filename, _, _ := goruntime.Caller(0) //nolint:dogsled crdName := "testclusters.test.cluster.x-k8s.io" crdObjectKey := client.ObjectKey{Name: crdName} @@ -170,7 +167,7 @@ func TestReconcile(t *testing.T) { }() t.Logf("T1: Install CRDs") - g.Expect(installCRDs(ctx, env.GetClient(), "test/t1/crd")).To(Succeed()) + g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", "test", "t1", "crd"))).To(Succeed()) validateStoredVersions(t, g, crdObjectKey, "v1beta1") t.Logf("T1: Start Manager") @@ -211,7 +208,7 @@ func TestReconcile(t *testing.T) { stopManager(cancelManager, managerStopped) t.Logf("T2: Install CRDs") - g.Expect(installCRDs(ctx, env.GetClient(), "test/t2/crd")).To(Succeed()) + g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", "test", "t2", "crd"))).To(Succeed()) validateStoredVersions(t, g, crdObjectKey, "v1beta1", "v1beta2") t.Logf("T2: Start Manager") @@ -245,7 +242,7 @@ func TestReconcile(t *testing.T) { stopManager(cancelManager, managerStopped) t.Logf("T3: Install CRDs") - g.Expect(installCRDs(ctx, env.GetClient(), "test/t3/crd")).To(Succeed()) + g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", "test", "t3", "crd"))).To(Succeed()) // Stored versions didn't change. if skipCRDMigrationPhases.Has(StorageVersionMigrationPhase) { validateStoredVersions(t, g, crdObjectKey, "v1beta1", "v1beta2") @@ -284,7 +281,7 @@ func TestReconcile(t *testing.T) { stopManager(cancelManager, managerStopped) t.Logf("T4: Install CRDs") - err = installCRDs(ctx, env.GetClient(), "test/t4/crd") + err = env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", "test", "t4", "crd")) if skipCRDMigrationPhases.Has(StorageVersionMigrationPhase) { // If storage version migration was skipped before, we now cannot deploy CRDs that remove v1beta1. g.Expect(err).To(HaveOccurred()) @@ -489,65 +486,6 @@ func stopManager(cancelManager context.CancelFunc, managerStopped chan struct{}) <-managerStopped } -func installCRDs(ctx context.Context, c client.Client, crdPath string) error { - // Get the root of the current file to use in CRD paths. - _, filename, _, _ := goruntime.Caller(0) //nolint:dogsled - - installOpts := envtest.CRDInstallOptions{ - Scheme: env.GetScheme(), - MaxTime: 10 * time.Second, - PollInterval: 100 * time.Millisecond, - Paths: []string{ - filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", crdPath), - }, - ErrorIfPathMissing: true, - } - - // Read the CRD YAMLs into options.CRDs. - if err := envtest.ReadCRDFiles(&installOpts); err != nil { - return fmt.Errorf("unable to read CRD files: %w", err) - } - - // Apply the CRDs. - if err := applyCRDs(ctx, c, installOpts.CRDs); err != nil { - return fmt.Errorf("unable to create CRD instances: %w", err) - } - - // Wait for the CRDs to appear in discovery. - if err := envtest.WaitForCRDs(env.GetConfig(), installOpts.CRDs, installOpts); err != nil { - return fmt.Errorf("something went wrong waiting for CRDs to appear as API resources: %w", err) - } - - return nil -} - -func applyCRDs(ctx context.Context, c client.Client, crds []*apiextensionsv1.CustomResourceDefinition) error { - for _, crd := range crds { - existingCrd := crd.DeepCopy() - err := c.Get(ctx, client.ObjectKey{Name: crd.GetName()}, existingCrd) - switch { - case apierrors.IsNotFound(err): - if err := c.Create(ctx, crd); err != nil { - return fmt.Errorf("unable to create CRD %s: %w", crd.GetName(), err) - } - case err != nil: - return fmt.Errorf("unable to get CRD %s to check if it exists: %w", crd.GetName(), err) - default: - if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { - if err := c.Get(ctx, client.ObjectKey{Name: crd.GetName()}, existingCrd); err != nil { - return err - } - // Note: Intentionally only overwriting spec and thus preserving metadata labels, annotations, etc. - existingCrd.Spec = crd.Spec - return c.Update(ctx, existingCrd) - }); err != nil { - return fmt.Errorf("unable to update CRD %s: %w", crd.GetName(), err) - } - } - } - return nil -} - type noopWebhookServer struct { webhook.Server } diff --git a/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go b/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go index 83c948f94d03..fa04e04627e1 100644 --- a/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go +++ b/controlplane/kubeadm/internal/webhooks/kubeadm_control_plane.go @@ -46,7 +46,7 @@ import ( func (webhook *KubeadmControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&controlplanev1.KubeadmControlPlane{}). - WithDefaulter(webhook, admission.DefaulterRemoveUnknownOrOmitableFields). + WithDefaulter(webhook). WithValidator(webhook). Complete() } diff --git a/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplanetemplate.go b/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplanetemplate.go index 6251f7d814d8..aa52aeffb25f 100644 --- a/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplanetemplate.go +++ b/controlplane/kubeadm/internal/webhooks/kubeadmcontrolplanetemplate.go @@ -36,7 +36,7 @@ import ( func (webhook *KubeadmControlPlaneTemplate) SetupWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&controlplanev1.KubeadmControlPlaneTemplate{}). - WithDefaulter(webhook, admission.DefaulterRemoveUnknownOrOmitableFields). + WithDefaulter(webhook). WithValidator(webhook). Complete() } diff --git a/internal/test/envtest/environment.go b/internal/test/envtest/environment.go index 95093cb69a3b..bdbc1063bf5a 100644 --- a/internal/test/envtest/environment.go +++ b/internal/test/envtest/environment.go @@ -44,6 +44,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" + "k8s.io/client-go/util/retry" "k8s.io/component-base/logs" logsv1 "k8s.io/component-base/logs/api/v1" "k8s.io/klog/v2" @@ -618,3 +619,60 @@ func verifyPanicMetrics() error { return nil } + +// ApplyCRDs allows you to add or replace CRDs after test env has been started. +func (e *Environment) ApplyCRDs(ctx context.Context, crdPath string) error { + installOpts := envtest.CRDInstallOptions{ + Scheme: e.GetScheme(), + MaxTime: 10 * time.Second, + PollInterval: 100 * time.Millisecond, + Paths: []string{ + crdPath, + }, + ErrorIfPathMissing: true, + } + + // Read the CRD YAMLs into options.CRDs. + if err := envtest.ReadCRDFiles(&installOpts); err != nil { + return fmt.Errorf("unable to read CRD files: %w", err) + } + + // Apply the CRDs. + if err := applyCRDs(ctx, e.GetClient(), installOpts.CRDs); err != nil { + return fmt.Errorf("unable to create CRD instances: %w", err) + } + + // Wait for the CRDs to appear in discovery. + if err := envtest.WaitForCRDs(e.GetConfig(), installOpts.CRDs, installOpts); err != nil { + return fmt.Errorf("something went wrong waiting for CRDs to appear as API resources: %w", err) + } + + return nil +} + +func applyCRDs(ctx context.Context, c client.Client, crds []*apiextensionsv1.CustomResourceDefinition) error { + for _, crd := range crds { + existingCrd := crd.DeepCopy() + err := c.Get(ctx, client.ObjectKey{Name: crd.GetName()}, existingCrd) + switch { + case apierrors.IsNotFound(err): + if err := c.Create(ctx, crd); err != nil { + return fmt.Errorf("unable to create CRD %s: %w", crd.GetName(), err) + } + case err != nil: + return fmt.Errorf("unable to get CRD %s to check if it exists: %w", crd.GetName(), err) + default: + if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + if err := c.Get(ctx, client.ObjectKey{Name: crd.GetName()}, existingCrd); err != nil { + return err + } + // Note: Intentionally only overwriting spec and thus preserving metadata labels, annotations, etc. + existingCrd.Spec = crd.Spec + return c.Update(ctx, existingCrd) + }); err != nil { + return fmt.Errorf("unable to update CRD %s: %w", crd.GetName(), err) + } + } + } + return nil +} diff --git a/internal/topology/upgrade/clusterctl_upgrade_test.go b/internal/topology/upgrade/clusterctl_upgrade_test.go new file mode 100644 index 000000000000..e1e19f233a06 --- /dev/null +++ b/internal/topology/upgrade/clusterctl_upgrade_test.go @@ -0,0 +1,1174 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package upgrade + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "path" + "path/filepath" + goruntime "runtime" + "strconv" + "testing" + "time" + + . "github.com/onsi/gomega" + "github.com/pkg/errors" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilfeature "k8s.io/component-base/featuregate/testing" + "k8s.io/klog/v2" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "sigs.k8s.io/controller-runtime/pkg/webhook/conversion" + + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + "sigs.k8s.io/cluster-api/feature" + "sigs.k8s.io/cluster-api/internal/contract" + testt1v1beta1 "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t1/v1beta1" + testt1webhook "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t1/webhook" + testt2v1beta1 "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t2/v1beta1" + testt2v1beta2 "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t2/v1beta2" + testt2webhook "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t2/webhook" + "sigs.k8s.io/cluster-api/util/conditions" + "sigs.k8s.io/cluster-api/util/test/builder" +) + +const ( + allowOmitableFields = true + doNotAllowOmitableFields = false +) + +// allObjs can be used to look at all the objects / how they change across generation while debugging. +var allObjs map[string]map[string]map[int64]client.Object + +func addToAllObj(cluster, prefix string, obj client.Object) { + ref := fmt.Sprintf("%s - %s, %s", prefix, klog.KObj(obj), obj.GetName()) + if allObjs == nil { + allObjs = make(map[string]map[string]map[int64]client.Object) + } + if _, ok := allObjs[cluster]; !ok { + allObjs[cluster] = make(map[string]map[int64]client.Object) + } + if _, ok := allObjs[cluster][ref]; !ok { + allObjs[cluster][ref] = make(map[int64]client.Object) + } + allObjs[cluster][ref][obj.GetGeneration()] = obj +} + +// TestDropDefaulterRemoveUnknownOrOmitableFields validates effects of removing the DropDefaulterRemoveUnknownOrOmitableFields option +// from provider's defaulting webhooks. +// TL;DR this should cause a rollout only when rebasing. +func TestDropDefaulterRemoveUnknownOrOmitableFields(t *testing.T) { + utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true) + g := NewWithT(t) + + ns, err := env.CreateNamespace(ctx, "test-optional-required") + g.Expect(err).ToNot(HaveOccurred()) + + // T1 - mimic an existing environment with a provider using the DefaulterRemoveUnknownOrOmitableFields option in its own defaulting webhook. + + // setupT1CRDAndWebHooks setups CRD and web hooks at t1. + // At t1 we have a CRD in v1beta1, and the defaulting web hook is using the DefaulterRemoveUnknownOrOmitableFields option. + t.Log("setupT1CRDAndWebHooks") + ct1, t1CheckObj, t1CheckTemplate, t1webhookConfig, t1TemplateWebhookConfig := setupT1CRDAndWebHooks(g, ns) + + // create clusterClasst1 using v1beta1 references + // Notably, this cluster class implements patches adding omitable fields. + t.Log("create clusterClasst1") + clusterClasst1 := createT1ClusterClass(g, ns, ct1) + + // create cluster1 using clusterClasst1 + t.Log("create cluster1 using clusterClasst1") + cluster1 := createClusterWithClusterClass(g, ns.Name, "cluster1", clusterClasst1) + + // check cluster1 is stable (no infinite reconcile or unexpected template rotation). + // Also, checks that the omitable fields that are added by ClusterClass patches are dropped by the DefaulterRemoveUnknownOrOmitableFields. + t.Log("check cluster1 is stable") + var cluster1Refs map[clusterv1.ContractVersionedObjectReference]int64 + g.Eventually(func() error { + cluster1Refs, err = getClusterTopologyReferences(cluster1, "v1beta1", checkOmitableField(doNotAllowOmitableFields)) // The defaulting webhook drops the omitable field when using v1beta1. + if err != nil { + return err + } + return nil + }, 5*time.Second).Should(Succeed()) + assertClusterTopologyIsStable(g, cluster1Refs, ns.Name, "v1beta1") + + // T2 - mimic a clusterctl upgrade for a provider dropping DefaulterRemoveUnknownOrOmitableFields from its own defaulting webhook. + + // setupT2CRDAndWebHooks setups CRD and web hooks at t2,. + // At t2 we have a CRD in v1beta1 and v1beta2 with the conversion webhook, and the defaulting web hook without the DefaulterRemoveUnknownOrOmitableFields option. + t.Log("setupT2CRDAndWebHooks") + + err = env.Delete(ctx, t1TemplateWebhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + + err = env.Delete(ctx, t1webhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + + ct2, t2webhookConfig, t2TemplateWebhookConfig := setupT2CRDAndWebHooks(g, ns, t1CheckObj, t1CheckTemplate) + + // create a new CC using v1beta2 types to be used for rebase later. + t.Log("create clusterClasst2") + clusterClasst2 := createT2ClusterClass(g, ns, ct2) + + // force reconcile on cluster 1. + t.Log("force reconcile on cluster 1") + cluster1New := cluster1.DeepCopy() + if cluster1New.Annotations == nil { + cluster1New.Annotations = map[string]string{} + } + cluster1New.Annotations["force-reconcile"] = "" + + err = env.Patch(ctx, cluster1New, client.MergeFrom(cluster1)) + g.Expect(err).ToNot(HaveOccurred()) + + // check cluster1 (created at t1, with a cluster class using v1beta1 references) is still stable after the clusterctl upgrade. + t.Log("check cluster1 is still stable") + assertClusterTopologyIsStable(g, cluster1Refs, ns.Name, "v1beta2") + + // rebase cluster1 to a CC created at t2, using v1beta2. + // Also, checks that the omitable fields that are added by ClusterClass is now present. + t.Log("rebase cluster1 to clusterClasst2, check is stable and rollout happens") + cluster1New = cluster1New.DeepCopy() + cluster1New.Spec.Topology.ClassRef.Name = clusterClasst2.Name + + err = env.Patch(ctx, cluster1New, client.MergeFrom(cluster1)) + g.Expect(err).ToNot(HaveOccurred()) + + var cluster1RefsNew map[clusterv1.ContractVersionedObjectReference]int64 + g.Consistently(func() error { + cluster1RefsNew, err = getClusterTopologyReferences(cluster1, "v1beta2", checkOmitableField(allowOmitableFields)) // The defaulting webhook do not drop anymore the omitable field when using v1beta2. + if err != nil { + return err + } + return nil + }, 5*time.Second).Should(Succeed()) + assertRollout(g, cluster1, cluster1Refs, cluster1RefsNew) // The omitable field should trigger rollout. + assertClusterTopologyIsStable(g, cluster1RefsNew, ns.Name, "v1beta2") + + // create cluster2 using clusterClasst1 + // Also, checks that the omitable fields that are added by ClusterClass patches are dropped by the conversion webhook. + t.Log("create cluster2 using clusterClasst1") + cluster2 := createClusterWithClusterClass(g, ns.Name, "cluster3", clusterClasst1) + + var cluster2Refs map[clusterv1.ContractVersionedObjectReference]int64 + g.Eventually(func() error { + cluster2Refs, err = getClusterTopologyReferences(cluster2, "v1beta2", checkOmitableField(doNotAllowOmitableFields)) // The conversion webhook drops the omitable field when using v1beta1. + if err != nil { + return err + } + return nil + }, 5*time.Second).Should(Succeed()) + + t.Log("check cluster2 is stable") + assertClusterTopologyIsStable(g, cluster2Refs, ns.Name, "v1beta2") + + // rebase cluster2 to a CC created at t2, using v1beta2. + // Also, checks that the omitable fields that are added by ClusterClass is now present. + t.Log("rebase cluster2 to clusterClasst2, check is stable and rollout happens") + cluster2New := cluster2.DeepCopy() + cluster2New.Spec.Topology.ClassRef.Name = clusterClasst2.Name + + err = env.Patch(ctx, cluster2New, client.MergeFrom(cluster2)) + g.Expect(err).ToNot(HaveOccurred()) + + var cluster2RefsNew map[clusterv1.ContractVersionedObjectReference]int64 + g.Consistently(func() error { + cluster2RefsNew, err = getClusterTopologyReferences(cluster2, "v1beta2", checkOmitableField(allowOmitableFields)) // The defaulting webhook do not drop anymore the omitable field when using v1beta2. + if err != nil { + return err + } + return nil + }, 5*time.Second).Should(Succeed()) + assertRollout(g, cluster2, cluster2Refs, cluster2RefsNew) // The omitable field should trigger rollout. + assertClusterTopologyIsStable(g, cluster2RefsNew, ns.Name, "v1beta2") + + // Cleanup + err = env.Delete(ctx, t2TemplateWebhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + + err = env.Delete(ctx, t2webhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + + g.Expect(env.Delete(ctx, ns)).To(Succeed()) +} + +func createClusterWithClusterClass(g *WithT, namespace, name string, clusterClass *clusterv1.ClusterClass) *clusterv1.Cluster { + machineDeploymentTopology1 := builder.MachineDeploymentTopology("md1"). + WithClass("md-class1"). + WithReplicas(3). + Build() + + cluster := builder.Cluster(namespace, name). + WithTopology( + builder.ClusterTopology(). + WithClass(clusterClass.Name). + WithMachineDeployment(machineDeploymentTopology1). + WithVersion("1.32.2"). + WithControlPlaneReplicas(1). + Build()). + Build() + + g.Expect(env.CreateAndWait(ctx, cluster)).To(Succeed()) + addToAllObj(cluster.Name, "cluster", cluster) + return cluster +} + +// createT1ClusterClass creates a CC with v1beta1 references and a patch adding the omitable field. +func createT1ClusterClass(g *WithT, ns *corev1.Namespace, ct1 client.Client) *clusterv1.ClusterClass { + infrastructureClusterTemplate1 := &testt1v1beta1.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-infra-cluster-template-v1beta1-t1", + }, + } + g.Expect(ct1.Create(ctx, infrastructureClusterTemplate1)).To(Succeed()) + + controlPlaneTemplate := &testt1v1beta1.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-control-plane-template-v1beta1-t1", + }, + } + + g.Expect(ct1.Create(ctx, controlPlaneTemplate)).To(Succeed()) + + infrastructureMachineTemplate1 := &testt1v1beta1.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-infra-machine-template-v1beta1-t1", + }, + } + + g.Expect(ct1.Create(ctx, infrastructureMachineTemplate1)).To(Succeed()) + + bootstrapTemplate := &testt1v1beta1.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-bootstrap-config-template-v1beta1-t1", + }, + } + + g.Expect(ct1.Create(ctx, bootstrapTemplate)).To(Succeed()) + + // Waits for the env client to get in cache object we created with client ct1. + waitForObjects(g, ct1.Scheme(), infrastructureClusterTemplate1, infrastructureClusterTemplate1, controlPlaneTemplate, infrastructureMachineTemplate1, bootstrapTemplate) + + machineDeploymentClass1 := clusterv1.MachineDeploymentClass{ + Class: "md-class1", + Template: clusterv1.MachineDeploymentClassTemplate{ + Infrastructure: clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: infrastructureMachineTemplate1.Name, + APIVersion: testt1v1beta1.GroupVersion.String(), + }, + }, + Bootstrap: clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: bootstrapTemplate.Name, + APIVersion: testt1v1beta1.GroupVersion.String(), + }, + }, + }, + } + + clusterClass := &clusterv1.ClusterClass{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterClass", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-class-t1", + Namespace: ns.Name, + }, + Spec: clusterv1.ClusterClassSpec{ + Infrastructure: clusterv1.InfrastructureClass{ + ClusterClassTemplate: clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: infrastructureClusterTemplate1.Name, + APIVersion: testt1v1beta1.GroupVersion.String(), + }, + }, + }, + ControlPlane: clusterv1.ControlPlaneClass{ + ClusterClassTemplate: clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: controlPlaneTemplate.Name, + APIVersion: testt1v1beta1.GroupVersion.String(), + }, + }, + MachineInfrastructure: &clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: infrastructureClusterTemplate1.Name, + APIVersion: testt1v1beta1.GroupVersion.String(), + }, + }, + }, + Workers: clusterv1.WorkersClass{ + MachineDeployments: []clusterv1.MachineDeploymentClass{ + machineDeploymentClass1, + }, + }, + Patches: []clusterv1.ClusterClassPatch{ + { + // Add the omitable fields. + // Note: + // - At t1, omitable fields are then dropped by DefaulterRemoveUnknownOrOmitableFields options in the defaulter webhook + // - At t2, omitable fields are then dropped by the conversion webhook + Name: "patch-t1", + Definitions: []clusterv1.PatchDefinition{ + { + Selector: clusterv1.PatchSelector{ + APIVersion: testt1v1beta1.GroupVersion.String(), + Kind: "TestResourceTemplate", + MatchResources: clusterv1.PatchSelectorMatch{ + InfrastructureCluster: true, + ControlPlane: true, + MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ + Names: []string{machineDeploymentClass1.Class}, + }, + }, + }, + JSONPatches: []clusterv1.JSONPatch{ + { + Op: "add", + Path: "/spec/template/spec/omitable", + Value: &apiextensionsv1.JSON{Raw: []byte(`""`)}, + }, + }, + }, + }, + }, + }, + }, + } + + g.Expect(env.CreateAndWait(ctx, clusterClass)).To(Succeed()) + return clusterClass +} + +func waitForObjects(g *WithT, scheme *runtime.Scheme, objs ...client.Object) { + for _, obj := range objs { + gvk, err := apiutil.GVKForObject(obj, scheme) + g.Expect(err).ToNot(HaveOccurred()) + g.Eventually(func() error { + objUnstructured := &unstructured.Unstructured{} + objUnstructured.SetGroupVersionKind(gvk) + return env.Get(ctx, client.ObjectKeyFromObject(obj), objUnstructured) + }, 5*time.Second).Should(Succeed()) + } +} + +// createT2ClusterClass creates a CC with v1beta2 references and a patch adding the omitable field. +func createT2ClusterClass(g *WithT, ns *corev1.Namespace, ct2 client.Client) *clusterv1.ClusterClass { + infrastructureClusterTemplate1 := &testt2v1beta2.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-infra-cluster-template-v1beta2-t2", + }, + } + + g.Expect(ct2.Create(ctx, infrastructureClusterTemplate1)).To(Succeed()) + + controlPlaneTemplate := &testt2v1beta2.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-control-plane-template-v1beta2-t2", + }, + } + + g.Expect(ct2.Create(ctx, controlPlaneTemplate)).To(Succeed()) + + infrastructureMachineTemplate1 := &testt2v1beta2.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-infra-machine-template-v1beta2-t2", + }, + } + + g.Expect(ct2.Create(ctx, infrastructureMachineTemplate1)).To(Succeed()) + + bootstrapTemplate := &testt2v1beta2.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "test-bootstrap-config-template-v1beta2-t2", + }, + } + + g.Expect(ct2.Create(ctx, bootstrapTemplate)).To(Succeed()) + + // Waits for the env client to get in cache object we created with client ct2. + waitForObjects(g, ct2.Scheme(), infrastructureClusterTemplate1, infrastructureClusterTemplate1, controlPlaneTemplate, infrastructureMachineTemplate1, bootstrapTemplate) + + machineDeploymentClass1 := clusterv1.MachineDeploymentClass{ + Class: "md-class1", + Template: clusterv1.MachineDeploymentClassTemplate{ + Infrastructure: clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: infrastructureMachineTemplate1.Name, + APIVersion: testt2v1beta2.GroupVersion.String(), + }, + }, + Bootstrap: clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: bootstrapTemplate.Name, + APIVersion: testt2v1beta2.GroupVersion.String(), + }, + }, + }, + } + + clusterClass := &clusterv1.ClusterClass{ + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterClass", + APIVersion: clusterv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster-class-t2", + Namespace: ns.Name, + }, + Spec: clusterv1.ClusterClassSpec{ + Infrastructure: clusterv1.InfrastructureClass{ + ClusterClassTemplate: clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: infrastructureClusterTemplate1.Name, + APIVersion: testt2v1beta2.GroupVersion.String(), + }, + }, + }, + ControlPlane: clusterv1.ControlPlaneClass{ + ClusterClassTemplate: clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: controlPlaneTemplate.Name, + APIVersion: testt2v1beta2.GroupVersion.String(), + }, + }, + MachineInfrastructure: &clusterv1.ClusterClassTemplate{ + Ref: &clusterv1.ClusterClassTemplateReference{ + Kind: "TestResourceTemplate", + Name: infrastructureClusterTemplate1.Name, + APIVersion: testt2v1beta2.GroupVersion.String(), + }, + }, + }, + Workers: clusterv1.WorkersClass{ + MachineDeployments: []clusterv1.MachineDeploymentClass{ + machineDeploymentClass1, + }, + }, + Patches: []clusterv1.ClusterClassPatch{ + { + // Add the omitable fields. + // Note: the omitable fields are not dropped because the DefaulterRemoveUnknownOrOmitableFields options is not used anymore. + Name: "patch-t2", + Definitions: []clusterv1.PatchDefinition{ + { + Selector: clusterv1.PatchSelector{ + APIVersion: testt2v1beta2.GroupVersion.String(), + Kind: "TestResourceTemplate", + MatchResources: clusterv1.PatchSelectorMatch{ + InfrastructureCluster: true, + ControlPlane: true, + MachineDeploymentClass: &clusterv1.PatchSelectorMatchMachineDeploymentClass{ + Names: []string{machineDeploymentClass1.Class}, + }, + }, + }, + JSONPatches: []clusterv1.JSONPatch{ + { + Op: "add", + Path: "/spec/template/spec/omitable", + Value: &apiextensionsv1.JSON{Raw: []byte(`""`)}, + }, + }, + }, + }, + }, + }, + }, + } + + g.Expect(env.CreateAndWait(ctx, clusterClass)).To(Succeed()) + return clusterClass +} + +// getClusterTopologyReferences gets all the references for a test cluster + the corresponding generation. +// NOTE: it also stores all the objects / how they change across generation into allObjs for debugging. +func getClusterTopologyReferences(cluster *clusterv1.Cluster, version string, additionalChecks func(*unstructured.Unstructured) error) (map[clusterv1.ContractVersionedObjectReference]int64, error) { + refs := map[clusterv1.ContractVersionedObjectReference]int64{} + // Get all the references + + actualCluster := &clusterv1.Cluster{} + if err := env.Get(ctx, client.ObjectKeyFromObject(cluster), actualCluster); err != nil { + return nil, errors.Wrapf(err, "failed to get cluster %s", cluster.Name) + } + if c := conditions.Get(actualCluster, clusterv1.ClusterTopologyReconciledCondition); c != nil && c.Status != metav1.ConditionTrue { + return nil, errors.Errorf("cluster %s topology is not reconciled", cluster.Name) + } + + addToAllObj(cluster.Name, "cluster", actualCluster) + refs[clusterv1.ContractVersionedObjectReference{ + APIGroup: actualCluster.GroupVersionKind().Group, + Kind: actualCluster.GroupVersionKind().Kind, + Name: actualCluster.GetName(), + }] = actualCluster.GetGeneration() + + if actualCluster.Spec.InfrastructureRef == nil { + return nil, errors.New("cluster actualCluster.spec.infrastructureRef is not yet set") + } + refObj, err := getReferencedObject(ctx, env.GetClient(), actualCluster.Spec.InfrastructureRef, version, actualCluster.Namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to get referenced actualCluster.spec.infrastructureRef") + } + addToAllObj(cluster.Name, ".spec.infrastructureRef", refObj) + refs[clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + }] = refObj.GetGeneration() + if additionalChecks != nil { + if err := additionalChecks(refObj); err != nil { + return nil, errors.Wrap(err, "failed additional checks on actualCluster.spec.infrastructureRef") + } + } + + if actualCluster.Spec.ControlPlaneRef == nil { + return nil, errors.New("cluster actualCluster.spec.controlPlaneRef is not yet set") + } + refObj, err = getReferencedObject(ctx, env.GetClient(), actualCluster.Spec.ControlPlaneRef, version, actualCluster.Namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to get referenced actualCluster.spec.controlPlaneRef") + } + addToAllObj(cluster.Name, ".spec.controlPlaneRef", refObj) + refs[clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + }] = refObj.GetGeneration() + if additionalChecks != nil { + if err := additionalChecks(refObj); err != nil { + return nil, errors.Wrap(err, "failed additional checks on actualCluster.spec.controlPlaneRef") + } + } + + cpInfraRef, err := contract.ControlPlane().MachineTemplate().InfrastructureRef().Get(refObj) + if err != nil { + return nil, errors.Wrap(err, "cluster controplPlane.spec.machineTemplate.infrastructureRef is not yet set") + } + refObj, err = getReferencedObject(ctx, env.GetClient(), cpInfraRef, version, actualCluster.Namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to get referenced controplPlane.spec.machineTemplate.infrastructureRef") + } + addToAllObj(cluster.Name, "controlPlane.spec.machineTemplate.infrastructureRef", refObj) + refs[clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + }] = refObj.GetGeneration() + + machineDeployments := &clusterv1.MachineDeploymentList{} + if err = env.List(ctx, machineDeployments, client.InNamespace(cluster.Namespace), client.MatchingLabels{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.ClusterTopologyOwnedLabel: "", + }); err != nil { + return nil, errors.Wrap(err, "failed to list machineDeployments") + } + if len(machineDeployments.Items) != 1 { + return nil, errors.Errorf("expected 1 machineDeployment, got %d", len(machineDeployments.Items)) + } + + for _, md := range machineDeployments.Items { + refs[clusterv1.ContractVersionedObjectReference{ + APIGroup: md.GroupVersionKind().Group, + Kind: md.GroupVersionKind().Kind, + Name: md.GetName(), + }] = md.GetGeneration() + addToAllObj(cluster.Name, "machineDeployment "+md.Name, &md) + + refObj, err = getReferencedObject(ctx, env.GetClient(), &md.Spec.Template.Spec.InfrastructureRef, version, actualCluster.Namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to get referenced machineDeployment.spec.template.spec.infrastructureRef") + } + addToAllObj(cluster.Name, "machineDeployment "+md.Name+" .spec.template.spec.infrastructureRef", refObj) + refs[clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + }] = refObj.GetGeneration() + if additionalChecks != nil { + if err := additionalChecks(refObj); err != nil { + return nil, errors.Wrap(err, "failed additional checks on machineDeployment.spec.template.spec.infrastructureRef") + } + } + + if md.Spec.Template.Spec.Bootstrap.ConfigRef == nil { + return nil, errors.New("cluster machineDeployment.spec.template.spec.bootstrap.configRef is not yet set") + } + refObj, err = getReferencedObject(ctx, env.GetClient(), md.Spec.Template.Spec.Bootstrap.ConfigRef, version, actualCluster.Namespace) + if err != nil { + return nil, errors.Wrap(err, "failed to get referenced machineDeployment.spec.template.spec.bootstrap.configRef") + } + addToAllObj(cluster.Name, "machineDeployment "+md.Name+" .spec.template.spec.bootstrap.configRef", refObj) + refs[clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + }] = refObj.GetGeneration() + if additionalChecks != nil { + if err := additionalChecks(refObj); err != nil { + return nil, errors.Wrap(err, "failed additional checks on machineDeployment.spec.template.spec.bootstrap.configRef") + } + } + } + return refs, nil +} + +func getReferencedObject(ctx context.Context, c client.Client, ref *clusterv1.ContractVersionedObjectReference, version, namespace string) (*unstructured.Unstructured, error) { + refObj := &unstructured.Unstructured{} + refObj.SetGroupVersionKind(ref.GroupKind().WithVersion(version)) + if err := c.Get(ctx, client.ObjectKey{Name: ref.Name, Namespace: namespace}, refObj); err != nil { + return nil, err + } + return refObj, nil +} + +func checkOmitableField(allowOmitable bool) func(obj *unstructured.Unstructured) error { + return func(obj *unstructured.Unstructured) error { + switch obj.GetKind() { + case "TestResource": + _, exists, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "omitable") + if err != nil { + return err + } + if exists && !allowOmitable { + return errors.Errorf("expected to not contain omitable field") + } + case "TestResourceTemplate": + _, exists, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "template", "spec", "omitable") + if err != nil { + return err + } + if exists && !allowOmitable { + return errors.Errorf("expected to not contain omitable field") + } + } + return nil + } +} + +// assertRollout assert a rollout happened by checking referenced template are changed or referencedObjects increased generation. +func assertRollout(g *WithT, cluster *clusterv1.Cluster, refsBefore, refsAfter map[clusterv1.ContractVersionedObjectReference]int64) { + actualCluster := &clusterv1.Cluster{} + err := env.Get(ctx, client.ObjectKeyFromObject(cluster), actualCluster) + g.Expect(err).To(Succeed()) + + clusterRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: actualCluster.GroupVersionKind().Group, + Kind: actualCluster.GroupVersionKind().Kind, + Name: actualCluster.GetName(), + } + g.Expect(refsAfter[clusterRef]).To(Equal(refsBefore[clusterRef]+1), "cluster unexpected change") // Cluster is expected to have an additional generation due to the rebase + + refObj, err := getReferencedObject(ctx, env.GetClient(), actualCluster.Spec.InfrastructureRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + infraClusterRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsAfter[infraClusterRef]).To(Equal(refsBefore[infraClusterRef]+1), "cluster.spec.infrastructureRef has unexpected generation") // InfrastructureRef is expected to have an additional generation due to the rebase. + + refObj, err = getReferencedObject(ctx, env.GetClient(), actualCluster.Spec.ControlPlaneRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + controlPlaneRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsAfter[controlPlaneRef]).To(Equal(refsBefore[controlPlaneRef]+1), "cluster.spec.controlPlaneRef has unexpected generation") // controlPlaneRef is expected to have an additional generation due to the rebase. + + cpInfraRef, err := contract.ControlPlane().MachineTemplate().InfrastructureRef().Get(refObj) + g.Expect(err).To(Succeed()) + refObj, err = getReferencedObject(ctx, env.GetClient(), cpInfraRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + controlPlaneInfraRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsBefore).ToNot(HaveKey(controlPlaneInfraRef), "controlPlane.spec.machineTemplate.infrastructureRef did not rollout") + g.Expect(refsAfter[controlPlaneInfraRef]).To(Equal(int64(1)), "controlPlane.spec.machineTemplate.infrastructureRef has unexpected generation") + + machineDeployments := &clusterv1.MachineDeploymentList{} + err = env.List(ctx, machineDeployments, client.InNamespace(cluster.Namespace), client.MatchingLabels{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.ClusterTopologyOwnedLabel: "", + }) + g.Expect(err).To(Succeed()) + g.Expect(machineDeployments.Items).To(HaveLen(1)) + + for _, md := range machineDeployments.Items { + mdRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: md.GroupVersionKind().GroupVersion().String(), + Kind: md.GroupVersionKind().Kind, + Name: md.GetName(), + } + g.Expect(refsAfter[mdRef]).To(Equal(refsBefore[mdRef]), "machineDeployment "+md.Name+" unexpected change") + + refObj, err = getReferencedObject(ctx, env.GetClient(), &md.Spec.Template.Spec.InfrastructureRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + mdInfraRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsBefore).ToNot(HaveKey(mdInfraRef), "machineDeployment "+md.Name+" .spec.template.spec.infrastructureRef did not rollout") + g.Expect(refsAfter[mdInfraRef]).To(Equal(int64(1)), "machineDeployment "+md.Name+" .spec.template.spec.infrastructureRef has unexpected generation") + + refObj, err = getReferencedObject(ctx, env.GetClient(), md.Spec.Template.Spec.Bootstrap.ConfigRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + mdBootstrapRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsBefore).ToNot(HaveKey(mdBootstrapRef), "machineDeployment "+md.Name+" .spec.template.spec.bootstrap.configRef did not rollout") + g.Expect(refsAfter[mdBootstrapRef]).To(Equal(int64(1)), "machineDeployment "+md.Name+" .spec.template.spec.bootstrap.configRef has unexpected generation") + } +} + +// assertNoRollout assert a rollout did not happened by checking referenced template or referencedObjects did not changed/increased generation. +// +//nolint:unused +func assertNoRollout(g *WithT, cluster *clusterv1.Cluster, refsBefore, refsAfter map[clusterv1.ContractVersionedObjectReference]int64) { + actualCluster := &clusterv1.Cluster{} + err := env.Get(ctx, client.ObjectKeyFromObject(cluster), actualCluster) + g.Expect(err).To(Succeed()) + + clusterRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: actualCluster.GroupVersionKind().Group, + Kind: actualCluster.GroupVersionKind().Kind, + Name: actualCluster.GetName(), + } + g.Expect(refsAfter[clusterRef]).To(Equal(refsBefore[clusterRef]+1), "cluster unexpected change") // Cluster is expected to have an additional generation due to the rebase + + refObj, err := getReferencedObject(ctx, env.GetClient(), actualCluster.Spec.InfrastructureRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + infraClusterRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsAfter[infraClusterRef]).To(Equal(refsBefore[infraClusterRef]), "cluster.spec.infrastructureRef unexpected change") + + refObj, err = getReferencedObject(ctx, env.GetClient(), actualCluster.Spec.ControlPlaneRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + controlPlaneRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsAfter[controlPlaneRef]).To(Equal(refsBefore[controlPlaneRef]), "cluster.spec.controlPlaneRef unexpected change") + + cpInfraRef, err := contract.ControlPlane().MachineTemplate().InfrastructureRef().Get(refObj) + g.Expect(err).To(Succeed()) + refObj, err = getReferencedObject(ctx, env.GetClient(), cpInfraRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + controlPlaneInfraRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsAfter[controlPlaneInfraRef]).To(Equal(refsBefore[controlPlaneInfraRef]), "controlPlane.spec.machineTemplate.infrastructureRef unexpected change triggering a rollout") + + machineDeployments := &clusterv1.MachineDeploymentList{} + err = env.List(ctx, machineDeployments, client.InNamespace(cluster.Namespace), client.MatchingLabels{ + clusterv1.ClusterNameLabel: cluster.Name, + clusterv1.ClusterTopologyOwnedLabel: "", + }) + g.Expect(err).To(Succeed()) + g.Expect(machineDeployments.Items).To(HaveLen(1)) + + for _, md := range machineDeployments.Items { + mdRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: md.GroupVersionKind().GroupVersion().String(), + Kind: md.GroupVersionKind().Kind, + Name: md.GetName(), + } + g.Expect(refsAfter[mdRef]).To(Equal(refsBefore[mdRef]), "machineDeployment "+md.Name+" unexpected change") + + refObj, err = getReferencedObject(ctx, env.GetClient(), &md.Spec.Template.Spec.InfrastructureRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + mdInfraRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsAfter[mdInfraRef]).To(Equal(refsBefore[mdInfraRef]), "machineDeployment "+md.Name+" .spec.template.spec.infrastructureRef unexpected change triggering a rollout") + + refObj, err = getReferencedObject(ctx, env.GetClient(), md.Spec.Template.Spec.Bootstrap.ConfigRef, "v1beta2", cluster.Namespace) + g.Expect(err).To(Succeed()) + mdBootstrapRef := clusterv1.ContractVersionedObjectReference{ + APIGroup: refObj.GroupVersionKind().Group, + Kind: refObj.GetKind(), + Name: refObj.GetName(), + } + g.Expect(refsAfter[mdBootstrapRef]).To(Equal(refsBefore[mdBootstrapRef]), "machineDeployment "+md.Name+" .spec.template.spec.bootstrap.configRef unexpected change triggering a rollout") + } +} + +// assertClusterTopologyIsStable checks a cluster topology is stable ensuring all the objects included cluster, md and referenced template or referencedObjects do not changed/increased generation. +func assertClusterTopologyIsStable(g *WithT, refs map[clusterv1.ContractVersionedObjectReference]int64, namespace, version string) { + g.Eventually(func(g Gomega) { + for r, generation := range refs { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(r.GroupKind().WithVersion(version)) + err := env.GetClient().Get(ctx, client.ObjectKey{Name: r.Name, Namespace: namespace}, obj) + g.Expect(err).ToNot(HaveOccurred()) + if generation != obj.GetGeneration() { + refs[r] = obj.GetGeneration() + } + g.Expect(obj.GetGeneration()).To(Equal(generation), "generation is not getting stable for %s/%s, %s", r.Kind, r.GroupKind().WithVersion(version).GroupVersion().String(), r.Name) + } + }, 5*time.Second, 1*time.Second).Should(Succeed(), "Resource versions never became stable") + + g.Consistently(func(g Gomega) { + for r, generation := range refs { + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(r.GroupKind().WithVersion(version)) + err := env.GetClient().Get(ctx, client.ObjectKey{Name: r.Name, Namespace: namespace}, obj) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(obj.GetGeneration()).To(Equal(generation), "generation is not remaining stable for %s/%s, %s", r.Kind, r.GroupKind().WithVersion(version).GroupVersion().String(), r.Name) + } + }, 10*time.Second, 2*time.Second).Should(Succeed(), "Resource versions didn't stay stable") +} + +// setupT1CRDAndWebHooks setups CRD and web hooks at t1. +// At t1 we have a CRD in v1beta1, and the defaulting web hook is using the DefaulterRemoveUnknownOrOmitableFields option. +func setupT1CRDAndWebHooks(g *WithT, ns *corev1.Namespace) (client.Client, *testt1v1beta1.TestResource, *testt1v1beta1.TestResourceTemplate, *admissionv1.MutatingWebhookConfiguration, *admissionv1.MutatingWebhookConfiguration) { + _, filename, _, _ := goruntime.Caller(0) //nolint:dogsled + + // Create a scheme + t1Scheme := runtime.NewScheme() + _ = testt1v1beta1.AddToScheme(t1Scheme) + + // Install CRD + err := env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "test", "t1", "crd")) + g.Expect(err).ToNot(HaveOccurred()) + + // Set contract label on CRDs + // NOTE: for sake of simplicity we are setting v1beta2 contract for v1beta1 API version. + err = addOrReplaceContractLabels(ctx, env, "testresourcetemplates.test.cluster.x-k8s.io", testt1v1beta1.GroupVersion.Version) + g.Expect(err).ToNot(HaveOccurred()) + + err = addOrReplaceContractLabels(ctx, env, "testresources.test.cluster.x-k8s.io", testt1v1beta1.GroupVersion.Version) + g.Expect(err).ToNot(HaveOccurred()) + + // Install the defaulter webhook for v1beta1 and test it works + t1TemplateDefaulter := admission.WithCustomDefaulter(t1Scheme, &testt1v1beta1.TestResourceTemplate{}, &testt1webhook.TestResourceTemplate{}, admission.DefaulterRemoveUnknownOrOmitableFields) + t1TemplateWebhookConfig, err := newMutatingWebhookConfigurationForCustomDefaulter(ns, testt1v1beta1.GroupVersion.WithResource("testresourcetemplates"), t1TemplateDefaulter) + g.Expect(err).ToNot(HaveOccurred()) + + err = env.Create(ctx, t1TemplateWebhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + + t1Defaulter := admission.WithCustomDefaulter(t1Scheme, &testt1v1beta1.TestResource{}, &testt1webhook.TestResource{}, admission.DefaulterRemoveUnknownOrOmitableFields) + t1webhookConfig, err := newMutatingWebhookConfigurationForCustomDefaulter(ns, testt1v1beta1.GroupVersion.WithResource("testresources"), t1Defaulter) + g.Expect(err).ToNot(HaveOccurred()) + + err = env.Create(ctx, t1webhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + + ct1, err := client.New(env.Config, client.Options{Scheme: t1Scheme}) + g.Expect(err).ToNot(HaveOccurred()) + + // Test if web hook is working + g.Eventually(ctx, func(g Gomega) { + t1TemplateObj := &testt1v1beta1.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-setup-dryrun", + Namespace: ns.Name, + }, + } + g.Expect(ct1.Create(ctx, t1TemplateObj, client.DryRunAll)).To(Succeed()) + g.Expect(t1TemplateObj.Annotations).To(HaveKey("default-t1")) + }, 5*time.Second).Should(Succeed()) + + g.Eventually(ctx, func(g Gomega) { + t1Obj := &testt1v1beta1.TestResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-setup-dryrun", + Namespace: ns.Name, + }, + } + g.Expect(ct1.Create(ctx, t1Obj, client.DryRunAll)).To(Succeed()) + g.Expect(t1Obj.Annotations).To(HaveKey("default-t1")) + }, 5*time.Second).Should(Succeed()) + + // Create an object + t1CheckTemplate := &testt1v1beta1.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-setup", + Namespace: ns.Name, + }, + } + + err = ct1.Create(ctx, t1CheckTemplate) + g.Expect(err).ToNot(HaveOccurred()) + + t1CheckObj := &testt1v1beta1.TestResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-setup", + Namespace: ns.Name, + }, + } + + err = ct1.Create(ctx, t1CheckObj) + g.Expect(err).ToNot(HaveOccurred()) + + return ct1, t1CheckObj, t1CheckTemplate, t1webhookConfig, t1TemplateWebhookConfig +} + +// setupT2CRDAndWebHooks setups CRD and web hooks at t2. +// At t2 we have a CRD in v1beta1 and v1beta2 with the conversion web hook, and the defaulting web hook without the DefaulterRemoveUnknownOrOmitableFields option. +func setupT2CRDAndWebHooks(g *WithT, ns *corev1.Namespace, t1CheckObj *testt1v1beta1.TestResource, t1CheckTemplate *testt1v1beta1.TestResourceTemplate) (client.Client, *admissionv1.MutatingWebhookConfiguration, *admissionv1.MutatingWebhookConfiguration) { + _, filename, _, _ := goruntime.Caller(0) //nolint:dogsled + + // Create a scheme + t2Scheme := runtime.NewScheme() + _ = testt2v1beta1.AddToScheme(t2Scheme) + _ = testt2v1beta2.AddToScheme(t2Scheme) + + // Install CRD + err := env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "test", "t2", "crd")) + g.Expect(err).ToNot(HaveOccurred()) + + // Set contract label on CRDs + // NOTE: for sake of simplicity we are setting v1beta2 contract both for v1beta1 API version and v1beta2 API version. + err = addOrReplaceContractLabels(ctx, env, "testresourcetemplates.test.cluster.x-k8s.io", testt2v1beta1.GroupVersion.Version+"_"+testt2v1beta2.GroupVersion.Version) + g.Expect(err).ToNot(HaveOccurred()) + + err = addOrReplaceContractLabels(ctx, env, "testresources.test.cluster.x-k8s.io", testt2v1beta1.GroupVersion.Version+"_"+testt2v1beta2.GroupVersion.Version) + g.Expect(err).ToNot(HaveOccurred()) + + // Install the defaulter webhook for v1beta2 + t2TemplateDefaulter := admission.WithCustomDefaulter(t2Scheme, &testt2v1beta2.TestResourceTemplate{}, &testt2webhook.TestResourceTemplate{}) + t2TemplateWebhookConfig, err := newMutatingWebhookConfigurationForCustomDefaulter(ns, testt2v1beta2.GroupVersion.WithResource("testresourcetemplates"), t2TemplateDefaulter) + g.Expect(err).ToNot(HaveOccurred()) + + err = env.Create(ctx, t2TemplateWebhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + + t2Defaulter := admission.WithCustomDefaulter(t2Scheme, &testt2v1beta2.TestResource{}, &testt2webhook.TestResource{}) + t2webhookConfig, err := newMutatingWebhookConfigurationForCustomDefaulter(ns, testt2v1beta2.GroupVersion.WithResource("testresources"), t2Defaulter) + g.Expect(err).ToNot(HaveOccurred()) + + err = env.Create(ctx, t2webhookConfig) + g.Expect(err).ToNot(HaveOccurred()) + + // Install the conversion webhook + err = addOrReplaceConversionWebhookHandler(ctx, env.GetClient(), "testresourcetemplates.test.cluster.x-k8s.io", conversion.NewWebhookHandler(t2Scheme)) + g.Expect(err).ToNot(HaveOccurred()) + + err = addOrReplaceConversionWebhookHandler(ctx, env.GetClient(), "testresources.test.cluster.x-k8s.io", conversion.NewWebhookHandler(t2Scheme)) + g.Expect(err).ToNot(HaveOccurred()) + + ct2, err := client.New(env.Config, client.Options{Scheme: t2Scheme}) + g.Expect(err).ToNot(HaveOccurred()) + + // Test the defaulter works + g.Eventually(ctx, func(g Gomega) { + t2TemplateObj := &testt2v1beta2.TestResourceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-setup-dryrun", + Namespace: ns.Name, + }, + } + g.Expect(ct2.Create(ctx, t2TemplateObj, client.DryRunAll)).To(Succeed()) + g.Expect(t2TemplateObj.Annotations).ToNot(HaveKey("default-t1")) + g.Expect(t2TemplateObj.Annotations).To(HaveKey("default-t2")) + g.Expect(t2TemplateObj.Annotations).ToNot(HaveKey("conversion")) + }, 5*time.Second).Should(Succeed()) + + g.Eventually(ctx, func(g Gomega) { + t2Obj := &testt2v1beta2.TestResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-setup-dryrun", + Namespace: ns.Name, + }, + } + g.Expect(ct2.Create(ctx, t2Obj, client.DryRunAll)).To(Succeed()) + g.Expect(t2Obj.Annotations).ToNot(HaveKey("default-t1")) + g.Expect(t2Obj.Annotations).To(HaveKey("default-t2")) + g.Expect(t2Obj.Annotations).ToNot(HaveKey("conversion")) + }, 5*time.Second).Should(Succeed()) + + // Test the conversion works + g.Eventually(ctx, func(g Gomega) { + t2TemplateObj := &testt2v1beta2.TestResourceTemplate{} + err = ct2.Get(ctx, client.ObjectKeyFromObject(t1CheckTemplate), t2TemplateObj) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(t2TemplateObj.Annotations).To(HaveKey("default-t1")) + g.Expect(t2TemplateObj.Annotations).ToNot(HaveKey("default-t2")) + g.Expect(t2TemplateObj.Annotations).To(HaveKey("conversionTo")) + }, 5*time.Second).Should(Succeed()) + + g.Eventually(ctx, func(g Gomega) { + t2Obj := &testt2v1beta2.TestResource{} + err = ct2.Get(ctx, client.ObjectKeyFromObject(t1CheckObj), t2Obj) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(t2Obj.Annotations).To(HaveKey("default-t1")) + g.Expect(t2Obj.Annotations).ToNot(HaveKey("default-t2")) + g.Expect(t2Obj.Annotations).To(HaveKey("conversionTo")) + }, 5*time.Second).Should(Succeed()) + + return ct2, t2webhookConfig, t2TemplateWebhookConfig +} + +func newMutatingWebhookConfigurationForCustomDefaulter(ns *corev1.Namespace, gvr schema.GroupVersionResource, handler http.Handler) (*admissionv1.MutatingWebhookConfiguration, error) { + webhookServer := env.GetWebhookServer().(*webhook.DefaultServer) + + // Calculate webhook host and path. + // Note: This is done the same way as in our envtest package, but the webhook is set up to work only for objects in the test namespace. + webhookPath := fmt.Sprintf("/%s/%s-%s-defaulting-webhook", ns.Name, gvr.Resource, gvr.Version) + webhookHost := "127.0.0.1" + + // Register the webhook. + webhookServer.Register(webhookPath, handler) + + // Calculate the MutatingWebhookConfiguration + caBundle, err := os.ReadFile(filepath.Join(webhookServer.Options.CertDir, webhookServer.Options.CertName)) + if err != nil { + return nil, err + } + + sideEffectNone := admissionv1.SideEffectClassNone + webhookConfig := &admissionv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s-webhook-config", ns.Name, gvr.Resource), + }, + Webhooks: []admissionv1.MutatingWebhook{ + { + Name: fmt.Sprintf("%s.%s.test.cluster.x-k8s.io", ns.Name, gvr.Resource), + ClientConfig: admissionv1.WebhookClientConfig{ + URL: ptr.To(fmt.Sprintf("https://%s%s", net.JoinHostPort(webhookHost, strconv.Itoa(webhookServer.Options.Port)), webhookPath)), + CABundle: caBundle, + }, + Rules: []admissionv1.RuleWithOperations{ + { + Operations: []admissionv1.OperationType{ + admissionv1.Create, + admissionv1.Update, + }, + Rule: admissionv1.Rule{ + APIGroups: []string{gvr.Group}, + APIVersions: []string{gvr.Version}, + Resources: []string{gvr.Resource}, + }, + }, + }, + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + corev1.LabelMetadataName: ns.Name, + }, + }, + AdmissionReviewVersions: []string{"v1"}, + SideEffects: &sideEffectNone, + }, + }, + } + return webhookConfig, nil +} + +func addOrReplaceConversionWebhookHandler(ctx context.Context, c client.Client, crdName string, handler http.Handler) error { + webhookServer := env.GetWebhookServer().(*webhook.DefaultServer) + + // Calculate webhook host and path. + // Note: This is done the same way as in our envtest package. + webhookPath := fmt.Sprintf("/%s/conversion", crdName) + webhookHost := "127.0.0.1" + + // Register the webhook. + webhookServer.Register(webhookPath, handler) + + // Get the CA bundle generated by test env. + caBundle, err := os.ReadFile(filepath.Join(webhookServer.Options.CertDir, webhookServer.Options.CertName)) + if err != nil { + return err + } + + // Get the CRD + crd := &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: crdName}, + } + if err := c.Get(ctx, client.ObjectKeyFromObject(crd), crd); err != nil { + return err + } + + // Update the CRD with the web hook configuration + crdNew := crd.DeepCopy() + crdNew.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{ + Strategy: "Webhook", + Webhook: &apiextensionsv1.WebhookConversion{ + ClientConfig: &apiextensionsv1.WebhookClientConfig{ + URL: ptr.To(fmt.Sprintf("https://%s%s", net.JoinHostPort(webhookHost, strconv.Itoa(webhookServer.Options.Port)), webhookPath)), + CABundle: caBundle, + }, + ConversionReviewVersions: []string{"v1", "v1beta1"}, + }, + } + return c.Patch(ctx, crdNew, client.MergeFrom(crd)) +} + +func addOrReplaceContractLabels(ctx context.Context, c client.Client, crdName string, contractLabelValue string) error { + crd := &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: crdName}, + } + if err := c.Get(ctx, client.ObjectKeyFromObject(crd), crd); err != nil { + return err + } + + // Update the CRD with the contract label + crdNew := crd.DeepCopy() + if crdNew.Labels == nil { + crdNew.Labels = map[string]string{} + } + crdNew.Labels[clusterv1.GroupVersion.String()] = contractLabelValue + return c.Patch(ctx, crdNew, client.MergeFrom(crd)) +} diff --git a/internal/topology/upgrade/doc.go b/internal/topology/upgrade/doc.go new file mode 100644 index 000000000000..0e9f53d18b43 --- /dev/null +++ b/internal/topology/upgrade/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package upgrade implements test scenarios to validate the behavior of the +// topology controller after upgrades, changes in the API, changes in the webhooks, etc. +package upgrade diff --git a/internal/topology/upgrade/suite_test.go b/internal/topology/upgrade/suite_test.go new file mode 100644 index 000000000000..75db9afb399c --- /dev/null +++ b/internal/topology/upgrade/suite_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package upgrade + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" + "sigs.k8s.io/cluster-api/api/core/v1beta2/index" + "sigs.k8s.io/cluster-api/controllers" + "sigs.k8s.io/cluster-api/controllers/clustercache" + "sigs.k8s.io/cluster-api/controllers/remote" + "sigs.k8s.io/cluster-api/internal/controllers/clusterclass" + fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake" + "sigs.k8s.io/cluster-api/internal/test/envtest" +) + +var ( + ctx = ctrl.SetupSignalHandler() + fakeScheme = runtime.NewScheme() + env *envtest.Environment +) + +func init() { + _ = clientgoscheme.AddToScheme(fakeScheme) + _ = clusterv1.AddToScheme(fakeScheme) + _ = apiextensionsv1.AddToScheme(fakeScheme) + _ = corev1.AddToScheme(fakeScheme) +} +func TestMain(m *testing.M) { + setupIndexes := func(ctx context.Context, mgr ctrl.Manager) { + if err := index.AddDefaultIndexes(ctx, mgr); err != nil { + panic(fmt.Sprintf("unable to setup index: %v", err)) + } + // Set up the ClusterClassName index explicitly here. This index is ordinarily created in + // index.AddDefaultIndexes. That doesn't happen here because the ClusterClass feature flag is not set. + if err := index.ByClusterClassRef(ctx, mgr); err != nil { + panic(fmt.Sprintf("unable to setup index: %v", err)) + } + } + setupReconcilers := func(ctx context.Context, mgr ctrl.Manager) { + clusterCache, err := clustercache.SetupWithManager(ctx, mgr, clustercache.Options{ + SecretClient: mgr.GetClient(), + Cache: clustercache.CacheOptions{ + Indexes: []clustercache.CacheOptionsIndex{clustercache.NodeProviderIDIndex}, + }, + Client: clustercache.ClientOptions{ + UserAgent: remote.DefaultClusterAPIUserAgent("test-controller-manager"), + Cache: clustercache.ClientCacheOptions{ + DisableFor: []client.Object{ + // Don't cache ConfigMaps & Secrets. + &corev1.ConfigMap{}, + &corev1.Secret{}, + }, + }, + }, + }, controller.Options{MaxConcurrentReconciles: 10}) + if err != nil { + panic(fmt.Sprintf("Failed to create ClusterCache: %v", err)) + } + go func() { + <-ctx.Done() + clusterCache.(interface{ Shutdown() }).Shutdown() + }() + + if err := (&controllers.ClusterTopologyReconciler{ + Client: mgr.GetClient(), + APIReader: mgr.GetAPIReader(), + ClusterCache: clusterCache, + RuntimeClient: fakeruntimeclient.NewRuntimeClientBuilder().Build(), + }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 1}); err != nil { + panic(fmt.Sprintf("unable to create topology cluster reconciler: %v", err)) + } + if err := (&clusterclass.Reconciler{ + Client: mgr.GetClient(), + RuntimeClient: fakeruntimeclient.NewRuntimeClientBuilder().Build(), + }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: 5}); err != nil { + panic(fmt.Sprintf("unable to create clusterclass reconciler: %v", err)) + } + } + SetDefaultEventuallyPollingInterval(100 * time.Millisecond) + SetDefaultEventuallyTimeout(30 * time.Second) + req, _ := labels.NewRequirement(clusterv1.ClusterNameLabel, selection.Exists, nil) + clusterSecretCacheSelector := labels.NewSelector().Add(*req) + os.Exit(envtest.Run(ctx, envtest.RunInput{ + M: m, + ManagerCacheOptions: cache.Options{ + ByObject: map[client.Object]cache.ByObject{ + // Only cache Secrets with the cluster name label. + // This is similar to the real world. + &corev1.Secret{}: { + Label: clusterSecretCacheSelector, + }, + }, + }, + ManagerUncachedObjs: []client.Object{ + &corev1.ConfigMap{}, + &corev1.Secret{}, + }, + SetupEnv: func(e *envtest.Environment) { env = e }, + SetupIndexes: setupIndexes, + SetupReconcilers: setupReconcilers, + MinK8sVersion: "v1.22.0", // ClusterClass uses server side apply that went GA in 1.22; we do not support previous version because of bug/inconsistent behaviours in the older release. + })) +} diff --git a/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml new file mode 100644 index 000000000000..ecd710e8c767 --- /dev/null +++ b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml @@ -0,0 +1,136 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: testresources.test.cluster.x-k8s.io +spec: + group: test.cluster.x-k8s.io + names: + kind: TestResource + listKind: TestResourceList + plural: testresources + singular: testresource + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: TestResource defines a test resource. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TestResourceSpec defines the resource spec. + properties: + machineTemplate: + description: TestResourceMachineTemplateSpec define the spec for machineTemplate + in a resource. + properties: + infrastructureRef: + description: TestContractVersionedObjectReference is a reference + to a resource for which the version is inferred from contract + labels. + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + metadata: + description: |- + ObjectMeta is metadata that all persisted resources must have, which includes all objects + users must create. This is a copy of customizable fields from metav1.ObjectMeta. + + ObjectMeta is embedded in `Machine.Spec`, `MachineDeployment.Template` and `MachineSet.Template`, + which are not top-level Kubernetes objects. Given that metav1.ObjectMeta has lots of special cases + and read-only fields which end up in the generated CRD validation, having it as a subset simplifies + the API and some issues that can impact user experience. + + During the [upgrade to controller-tools@v2](https://github.com/kubernetes-sigs/cluster-api/pull/1054) + for v1alpha2, we noticed a failure would occur running Cluster API test suite against the new CRDs, + specifically `spec.metadata.creationTimestamp in body must be of type string: "null"`. + The investigation showed that `controller-tools@v2` behaves differently than its previous version + when handling types from [metav1](k8s.io/apimachinery/pkg/apis/meta/v1) package. + + In more details, we found that embedded (non-top level) types that embedded `metav1.ObjectMeta` + had validation properties, including for `creationTimestamp` (metav1.Time). + The `metav1.Time` type specifies a custom json marshaller that, when IsZero() is true, returns `null` + which breaks validation because the field isn't marked as nullable. + + In future versions, controller-tools@v2 might allow overriding the type and validation for embedded + types. When that happens, this hack should be revisited. + properties: + annotations: + additionalProperties: + type: string + description: |- + annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + labels is a map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + type: object + type: object + omitable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + status: + description: TestResourceStatus defines the status of a TestResource. + properties: + availableReplicas: + format: int32 + type: integer + readyReplicas: + format: int32 + type: integer + replicas: + format: int32 + type: integer + upToDateReplicas: + format: int32 + type: integer + version: + maxLength: 256 + minLength: 1 + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml new file mode 100644 index 000000000000..81a48e43b5be --- /dev/null +++ b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml @@ -0,0 +1,127 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: testresourcetemplates.test.cluster.x-k8s.io +spec: + group: test.cluster.x-k8s.io + names: + kind: TestResourceTemplate + listKind: TestResourceTemplateList + plural: testresourcetemplates + singular: testresourcetemplate + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: TestResourceTemplate defines a test resource template. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TestResourceTemplateSpec defines the spec of a TestResourceTemplate. + properties: + template: + description: TestResourceTemplateResource defines the template resource + of a TestResourceTemplate. + properties: + spec: + description: spec is the desired state of KubeadmControlPlaneTemplateResource. + properties: + machineTemplate: + description: TestResourceMachineTemplateSpec define the spec + for machineTemplate in a resource. + properties: + infrastructureRef: + description: TestContractVersionedObjectReference is a + reference to a resource for which the version is inferred + from contract labels. + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + metadata: + description: |- + ObjectMeta is metadata that all persisted resources must have, which includes all objects + users must create. This is a copy of customizable fields from metav1.ObjectMeta. + + ObjectMeta is embedded in `Machine.Spec`, `MachineDeployment.Template` and `MachineSet.Template`, + which are not top-level Kubernetes objects. Given that metav1.ObjectMeta has lots of special cases + and read-only fields which end up in the generated CRD validation, having it as a subset simplifies + the API and some issues that can impact user experience. + + During the [upgrade to controller-tools@v2](https://github.com/kubernetes-sigs/cluster-api/pull/1054) + for v1alpha2, we noticed a failure would occur running Cluster API test suite against the new CRDs, + specifically `spec.metadata.creationTimestamp in body must be of type string: "null"`. + The investigation showed that `controller-tools@v2` behaves differently than its previous version + when handling types from [metav1](k8s.io/apimachinery/pkg/apis/meta/v1) package. + + In more details, we found that embedded (non-top level) types that embedded `metav1.ObjectMeta` + had validation properties, including for `creationTimestamp` (metav1.Time). + The `metav1.Time` type specifies a custom json marshaller that, when IsZero() is true, returns `null` + which breaks validation because the field isn't marked as nullable. + + In future versions, controller-tools@v2 might allow overriding the type and validation for embedded + types. When that happens, this hack should be revisited. + properties: + annotations: + additionalProperties: + type: string + description: |- + annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + labels is a map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + type: object + type: object + omitable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + required: + - spec + type: object + required: + - template + type: object + type: object + served: true + storage: true diff --git a/internal/topology/upgrade/test/t1/v1beta1/groupversion_info.go b/internal/topology/upgrade/test/t1/v1beta1/groupversion_info.go new file mode 100644 index 000000000000..2a630cdf0919 --- /dev/null +++ b/internal/topology/upgrade/test/t1/v1beta1/groupversion_info.go @@ -0,0 +1,43 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // GroupVersion is group version used to test CRD migration. + GroupVersion = schema.GroupVersion{Group: "test.cluster.x-k8s.io", Version: "v1beta1"} + + // schemeBuilder is used to add go types to the GroupVersionKind scheme. + schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme adds the types to the given scheme. + AddToScheme = schemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &TestResourceTemplate{}, &TestResourceTemplateList{}, + &TestResource{}, &TestResourceList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/internal/topology/upgrade/test/t1/v1beta1/types.go b/internal/topology/upgrade/test/t1/v1beta1/types.go new file mode 100644 index 000000000000..879a8d168622 --- /dev/null +++ b/internal/topology/upgrade/test/t1/v1beta1/types.go @@ -0,0 +1,147 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 contains test types. +// +kubebuilder:object:generate=true +// +groupName=test.cluster.x-k8s.io +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" +) + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=testresourcetemplates,scope=Namespaced +// +kubebuilder:storageversion + +// TestResourceTemplate defines a test resource template. +type TestResourceTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec TestResourceTemplateSpec `json:"spec,omitempty"` +} + +// TestResourceTemplateSpec defines the spec of a TestResourceTemplate. +type TestResourceTemplateSpec struct { + // +required + Template TestResourceTemplateResource `json:"template"` +} + +// TestResourceTemplateResource defines the template resource of a TestResourceTemplate. +type TestResourceTemplateResource struct { + // spec is the desired state of KubeadmControlPlaneTemplateResource. + // +required + Spec TestResourceSpec `json:"spec"` +} + +// TestResourceTemplateList is a list of TestResourceTemplate. +// +kubebuilder:object:root=true +type TestResourceTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestResourceTemplate `json:"items"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=testresources,scope=Namespaced +// +kubebuilder:storageversion +// +kubebuilder:subresource:status + +// TestResource defines a test resource. +type TestResource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec TestResourceSpec `json:"spec,omitempty"` + Status TestResourceStatus `json:"status,omitempty"` +} + +// TestResourceSpec defines the resource spec. +type TestResourceSpec struct { // NOTE: we are using testDefaulterT1 field to test if the defaulter works. + // mandatory field from the Cluster API contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // Mandatory field from the Cluster API contract - version support + + // +optional + Version string `json:"version,omitempty"` + + // Mandatory field from the Cluster API contract - machine support + + // +required + MachineTemplate TestResourceMachineTemplateSpec `json:"machineTemplate"` + + // General purpose fields to be used in different test scenario. + + // +optional + Omitable string `json:"omitable,omitempty"` +} + +// TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. +type TestResourceMachineTemplateSpec struct { + // +optional + ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty"` + + // +optional + InfrastructureRef TestContractVersionedObjectReference `json:"infrastructureRef"` +} + +// TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. +type TestContractVersionedObjectReference struct { + // +optional + Kind string `json:"kind"` + + // +optional + Name string `json:"name"` + + // +optional + APIGroup string `json:"apiGroup"` +} + +// TestResourceStatus defines the status of a TestResource. +type TestResourceStatus struct { + // mandatory field from the Cluster API contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // +optional + ReadyReplicas *int32 `json:"readyReplicas,omitempty"` + + // +optional + AvailableReplicas *int32 `json:"availableReplicas,omitempty"` + + // +optional + UpToDateReplicas *int32 `json:"upToDateReplicas,omitempty"` + + // Mandatory field from the Cluster API contract - version support + + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + Version *string `json:"version,omitempty"` +} + +// TestResourceList is a list of TestResource. +// +kubebuilder:object:root=true +type TestResourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestResource `json:"items"` +} diff --git a/internal/topology/upgrade/test/t1/v1beta1/zz_generated.deepcopy.go b/internal/topology/upgrade/test/t1/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..c1923a658b70 --- /dev/null +++ b/internal/topology/upgrade/test/t1/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,267 @@ +//go:build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestContractVersionedObjectReference) DeepCopyInto(out *TestContractVersionedObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestContractVersionedObjectReference. +func (in *TestContractVersionedObjectReference) DeepCopy() *TestContractVersionedObjectReference { + if in == nil { + return nil + } + out := new(TestContractVersionedObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResource) DeepCopyInto(out *TestResource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResource. +func (in *TestResource) DeepCopy() *TestResource { + if in == nil { + return nil + } + out := new(TestResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceList) DeepCopyInto(out *TestResourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceList. +func (in *TestResourceList) DeepCopy() *TestResourceList { + if in == nil { + return nil + } + out := new(TestResourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceMachineTemplateSpec) DeepCopyInto(out *TestResourceMachineTemplateSpec) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.InfrastructureRef = in.InfrastructureRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceMachineTemplateSpec. +func (in *TestResourceMachineTemplateSpec) DeepCopy() *TestResourceMachineTemplateSpec { + if in == nil { + return nil + } + out := new(TestResourceMachineTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceSpec) DeepCopyInto(out *TestResourceSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + in.MachineTemplate.DeepCopyInto(&out.MachineTemplate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceSpec. +func (in *TestResourceSpec) DeepCopy() *TestResourceSpec { + if in == nil { + return nil + } + out := new(TestResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceStatus) DeepCopyInto(out *TestResourceStatus) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.ReadyReplicas != nil { + in, out := &in.ReadyReplicas, &out.ReadyReplicas + *out = new(int32) + **out = **in + } + if in.AvailableReplicas != nil { + in, out := &in.AvailableReplicas, &out.AvailableReplicas + *out = new(int32) + **out = **in + } + if in.UpToDateReplicas != nil { + in, out := &in.UpToDateReplicas, &out.UpToDateReplicas + *out = new(int32) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceStatus. +func (in *TestResourceStatus) DeepCopy() *TestResourceStatus { + if in == nil { + return nil + } + out := new(TestResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplate) DeepCopyInto(out *TestResourceTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplate. +func (in *TestResourceTemplate) DeepCopy() *TestResourceTemplate { + if in == nil { + return nil + } + out := new(TestResourceTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateList) DeepCopyInto(out *TestResourceTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestResourceTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateList. +func (in *TestResourceTemplateList) DeepCopy() *TestResourceTemplateList { + if in == nil { + return nil + } + out := new(TestResourceTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateResource) DeepCopyInto(out *TestResourceTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateResource. +func (in *TestResourceTemplateResource) DeepCopy() *TestResourceTemplateResource { + if in == nil { + return nil + } + out := new(TestResourceTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateSpec) DeepCopyInto(out *TestResourceTemplateSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateSpec. +func (in *TestResourceTemplateSpec) DeepCopy() *TestResourceTemplateSpec { + if in == nil { + return nil + } + out := new(TestResourceTemplateSpec) + in.DeepCopyInto(out) + return out +} diff --git a/internal/topology/upgrade/test/t1/webhook/webhook.go b/internal/topology/upgrade/test/t1/webhook/webhook.go new file mode 100644 index 000000000000..ffe21a9650c7 --- /dev/null +++ b/internal/topology/upgrade/test/t1/webhook/webhook.go @@ -0,0 +1,90 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package webhook define webhooks for the v1beta1 API. +package webhook + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + testv1 "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t1/v1beta1" +) + +// TestResourceTemplate defines CustomDefaulter and CustomValidator for a TestResourceTemplate. +type TestResourceTemplate struct{} + +var _ webhook.CustomDefaulter = &TestResourceTemplate{} +var _ webhook.CustomValidator = &TestResourceTemplate{} + +// Default a TestResourceTemplate. +func (webhook *TestResourceTemplate) Default(_ context.Context, obj runtime.Object) error { + r := obj.(*testv1.TestResourceTemplate) + if r.Annotations == nil { + r.Annotations = map[string]string{} + } + r.Annotations["default-t1"] = "" + return nil +} + +// ValidateCreate a TestResourceTemplate. +func (webhook *TestResourceTemplate) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateUpdate a TestResourceTemplate. +func (webhook *TestResourceTemplate) ValidateUpdate(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateDelete a TestResourceTemplate. +func (webhook *TestResourceTemplate) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// TestResource defines CustomDefaulter and CustomValidator for a TestResource. +type TestResource struct{} + +var _ webhook.CustomDefaulter = &TestResource{} +var _ webhook.CustomValidator = &TestResource{} + +// Default a TestResource. +func (webhook *TestResource) Default(_ context.Context, obj runtime.Object) error { + r := obj.(*testv1.TestResource) + if r.Annotations == nil { + r.Annotations = map[string]string{} + } + r.Annotations["default-t1"] = "" + return nil +} + +// ValidateCreate a TestResource. +func (webhook *TestResource) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateUpdate a TestResource. +func (webhook *TestResource) ValidateUpdate(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateDelete a TestResource. +func (webhook *TestResource) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} diff --git a/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml new file mode 100644 index 000000000000..b27135b8bfa3 --- /dev/null +++ b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml @@ -0,0 +1,216 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: testresources.test.cluster.x-k8s.io +spec: + group: test.cluster.x-k8s.io + names: + kind: TestResource + listKind: TestResourceList + plural: testresources + singular: testresource + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: TestResource defines a test resource. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TestResourceSpec defines the resource spec. + properties: + machineTemplate: + description: TestResourceMachineTemplateSpec define the spec for machineTemplate + in a resource. + properties: + infrastructureRef: + description: TestContractVersionedObjectReference is a reference + to a resource for which the version is inferred from contract + labels. + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + metadata: + description: |- + ObjectMeta is metadata that all persisted resources must have, which includes all objects + users must create. This is a copy of customizable fields from metav1.ObjectMeta. + + ObjectMeta is embedded in `Machine.Spec`, `MachineDeployment.Template` and `MachineSet.Template`, + which are not top-level Kubernetes objects. Given that metav1.ObjectMeta has lots of special cases + and read-only fields which end up in the generated CRD validation, having it as a subset simplifies + the API and some issues that can impact user experience. + + During the [upgrade to controller-tools@v2](https://github.com/kubernetes-sigs/cluster-api/pull/1054) + for v1alpha2, we noticed a failure would occur running Cluster API test suite against the new CRDs, + specifically `spec.metadata.creationTimestamp in body must be of type string: "null"`. + The investigation showed that `controller-tools@v2` behaves differently than its previous version + when handling types from [metav1](k8s.io/apimachinery/pkg/apis/meta/v1) package. + + In more details, we found that embedded (non-top level) types that embedded `metav1.ObjectMeta` + had validation properties, including for `creationTimestamp` (metav1.Time). + The `metav1.Time` type specifies a custom json marshaller that, when IsZero() is true, returns `null` + which breaks validation because the field isn't marked as nullable. + + In future versions, controller-tools@v2 might allow overriding the type and validation for embedded + types. When that happens, this hack should be revisited. + properties: + annotations: + additionalProperties: + type: string + description: |- + annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + labels is a map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + type: object + type: object + omitable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + type: object + served: true + storage: false + subresources: + status: {} + - name: v1beta2 + schema: + openAPIV3Schema: + description: TestResource defines a test resource. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TestResourceSpec defines the resource spec. + properties: + machineTemplate: + description: TestResourceMachineTemplateSpec define the spec for machineTemplate + in a resource. + properties: + infrastructureRef: + description: TestContractVersionedObjectReference is a reference + to a resource for which the version is inferred from contract + labels. + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + metadata: + description: |- + ObjectMeta is metadata that all persisted resources must have, which includes all objects + users must create. This is a copy of customizable fields from metav1.ObjectMeta. + + ObjectMeta is embedded in `Machine.Spec`, `MachineDeployment.Template` and `MachineSet.Template`, + which are not top-level Kubernetes objects. Given that metav1.ObjectMeta has lots of special cases + and read-only fields which end up in the generated CRD validation, having it as a subset simplifies + the API and some issues that can impact user experience. + + During the [upgrade to controller-tools@v2](https://github.com/kubernetes-sigs/cluster-api/pull/1054) + for v1alpha2, we noticed a failure would occur running Cluster API test suite against the new CRDs, + specifically `spec.metadata.creationTimestamp in body must be of type string: "null"`. + The investigation showed that `controller-tools@v2` behaves differently than its previous version + when handling types from [metav1](k8s.io/apimachinery/pkg/apis/meta/v1) package. + + In more details, we found that embedded (non-top level) types that embedded `metav1.ObjectMeta` + had validation properties, including for `creationTimestamp` (metav1.Time). + The `metav1.Time` type specifies a custom json marshaller that, when IsZero() is true, returns `null` + which breaks validation because the field isn't marked as nullable. + + In future versions, controller-tools@v2 might allow overriding the type and validation for embedded + types. When that happens, this hack should be revisited. + properties: + annotations: + additionalProperties: + type: string + description: |- + annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + labels is a map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + type: object + type: object + omitable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml new file mode 100644 index 000000000000..0dedc69d266b --- /dev/null +++ b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml @@ -0,0 +1,238 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: testresourcetemplates.test.cluster.x-k8s.io +spec: + group: test.cluster.x-k8s.io + names: + kind: TestResourceTemplate + listKind: TestResourceTemplateList + plural: testresourcetemplates + singular: testresourcetemplate + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: TestResourceTemplate defines a test resource template. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TestResourceTemplateSpec defines the spec of a TestResourceTemplate. + properties: + template: + description: TestResourceTemplateResource defines the template resource + of a TestResourceTemplate. + properties: + spec: + description: spec is the desired state of KubeadmControlPlaneTemplateResource. + properties: + machineTemplate: + description: TestResourceMachineTemplateSpec define the spec + for machineTemplate in a resource. + properties: + infrastructureRef: + description: TestContractVersionedObjectReference is a + reference to a resource for which the version is inferred + from contract labels. + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + metadata: + description: |- + ObjectMeta is metadata that all persisted resources must have, which includes all objects + users must create. This is a copy of customizable fields from metav1.ObjectMeta. + + ObjectMeta is embedded in `Machine.Spec`, `MachineDeployment.Template` and `MachineSet.Template`, + which are not top-level Kubernetes objects. Given that metav1.ObjectMeta has lots of special cases + and read-only fields which end up in the generated CRD validation, having it as a subset simplifies + the API and some issues that can impact user experience. + + During the [upgrade to controller-tools@v2](https://github.com/kubernetes-sigs/cluster-api/pull/1054) + for v1alpha2, we noticed a failure would occur running Cluster API test suite against the new CRDs, + specifically `spec.metadata.creationTimestamp in body must be of type string: "null"`. + The investigation showed that `controller-tools@v2` behaves differently than its previous version + when handling types from [metav1](k8s.io/apimachinery/pkg/apis/meta/v1) package. + + In more details, we found that embedded (non-top level) types that embedded `metav1.ObjectMeta` + had validation properties, including for `creationTimestamp` (metav1.Time). + The `metav1.Time` type specifies a custom json marshaller that, when IsZero() is true, returns `null` + which breaks validation because the field isn't marked as nullable. + + In future versions, controller-tools@v2 might allow overriding the type and validation for embedded + types. When that happens, this hack should be revisited. + properties: + annotations: + additionalProperties: + type: string + description: |- + annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + labels is a map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + type: object + type: object + omitable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + required: + - spec + type: object + required: + - template + type: object + type: object + served: true + storage: false + - name: v1beta2 + schema: + openAPIV3Schema: + description: TestResourceTemplate defines a test resource template. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: TestResourceTemplateSpec defines the spec of a TestResourceTemplate. + properties: + template: + description: TestResourceTemplateResource defines the template resource + of a TestResourceTemplate. + properties: + spec: + description: spec is the desired state of KubeadmControlPlaneTemplateResource. + properties: + machineTemplate: + description: TestResourceMachineTemplateSpec define the spec + for machineTemplate in a resource. + properties: + infrastructureRef: + description: TestContractVersionedObjectReference is a + reference to a resource for which the version is inferred + from contract labels. + properties: + apiGroup: + type: string + kind: + type: string + name: + type: string + type: object + metadata: + description: |- + ObjectMeta is metadata that all persisted resources must have, which includes all objects + users must create. This is a copy of customizable fields from metav1.ObjectMeta. + + ObjectMeta is embedded in `Machine.Spec`, `MachineDeployment.Template` and `MachineSet.Template`, + which are not top-level Kubernetes objects. Given that metav1.ObjectMeta has lots of special cases + and read-only fields which end up in the generated CRD validation, having it as a subset simplifies + the API and some issues that can impact user experience. + + During the [upgrade to controller-tools@v2](https://github.com/kubernetes-sigs/cluster-api/pull/1054) + for v1alpha2, we noticed a failure would occur running Cluster API test suite against the new CRDs, + specifically `spec.metadata.creationTimestamp in body must be of type string: "null"`. + The investigation showed that `controller-tools@v2` behaves differently than its previous version + when handling types from [metav1](k8s.io/apimachinery/pkg/apis/meta/v1) package. + + In more details, we found that embedded (non-top level) types that embedded `metav1.ObjectMeta` + had validation properties, including for `creationTimestamp` (metav1.Time). + The `metav1.Time` type specifies a custom json marshaller that, when IsZero() is true, returns `null` + which breaks validation because the field isn't marked as nullable. + + In future versions, controller-tools@v2 might allow overriding the type and validation for embedded + types. When that happens, this hack should be revisited. + properties: + annotations: + additionalProperties: + type: string + description: |- + annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: http://kubernetes.io/docs/user-guide/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + labels is a map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: http://kubernetes.io/docs/user-guide/labels + type: object + type: object + type: object + omitable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + required: + - spec + type: object + required: + - template + type: object + type: object + served: true + storage: true diff --git a/internal/topology/upgrade/test/t2/v1beta1/conversion.go b/internal/topology/upgrade/test/t2/v1beta1/conversion.go new file mode 100644 index 000000000000..9cfbd5cf3977 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta1/conversion.go @@ -0,0 +1,61 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "sigs.k8s.io/controller-runtime/pkg/conversion" + + testv1 "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t2/v1beta2" +) + +func (src *TestResourceTemplate) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*testv1.TestResourceTemplate) + if err := Convert_v1beta1_TestResourceTemplate_To_v1beta2_TestResourceTemplate(src, dst, nil); err != nil { + return err + } + + if dst.Annotations == nil { + dst.Annotations = map[string]string{} + } + dst.Annotations["conversionTo"] = "" + return nil +} + +func (dst *TestResourceTemplate) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*testv1.TestResourceTemplate) + + return Convert_v1beta2_TestResourceTemplate_To_v1beta1_TestResourceTemplate(src, dst, nil) +} + +func (src *TestResource) ConvertTo(dstRaw conversion.Hub) error { + dst := dstRaw.(*testv1.TestResource) + if err := Convert_v1beta1_TestResource_To_v1beta2_TestResource(src, dst, nil); err != nil { + return err + } + + if dst.Annotations == nil { + dst.Annotations = map[string]string{} + } + dst.Annotations["conversionTo"] = "" + return nil +} + +func (dst *TestResource) ConvertFrom(srcRaw conversion.Hub) error { + src := srcRaw.(*testv1.TestResource) + + return Convert_v1beta2_TestResource_To_v1beta1_TestResource(src, dst, nil) +} diff --git a/internal/topology/upgrade/test/t2/v1beta1/doc.go b/internal/topology/upgrade/test/t2/v1beta1/doc.go new file mode 100644 index 000000000000..734a09dd5707 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta1/doc.go @@ -0,0 +1,22 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta1 contains test types. +// +k8s:openapi-gen=true +// +k8s:conversion-gen=sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t2/v1beta2 +// +kubebuilder:object:generate=true +// +groupName=test.cluster.x-k8s.io +package v1beta1 diff --git a/internal/topology/upgrade/test/t2/v1beta1/groupversion_info.go b/internal/topology/upgrade/test/t2/v1beta1/groupversion_info.go new file mode 100644 index 000000000000..1a22f13a94d8 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta1/groupversion_info.go @@ -0,0 +1,46 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // GroupVersion is group version used to test CRD migration. + GroupVersion = schema.GroupVersion{Group: "test.cluster.x-k8s.io", Version: "v1beta1"} + + // schemeBuilder is used to add go types to the GroupVersionKind scheme. + schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme adds the types to the given scheme. + AddToScheme = schemeBuilder.AddToScheme + + // localSchemeBuilder is used for type conversions. + localSchemeBuilder = schemeBuilder +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &TestResourceTemplate{}, &TestResourceTemplateList{}, + &TestResource{}, &TestResourceList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/internal/topology/upgrade/test/t2/v1beta1/types.go b/internal/topology/upgrade/test/t2/v1beta1/types.go new file mode 100644 index 000000000000..46a961163b3f --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta1/types.go @@ -0,0 +1,141 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" +) + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=testresourcetemplates,scope=Namespaced + +// TestResourceTemplate defines a test resource template. +type TestResourceTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec TestResourceTemplateSpec `json:"spec,omitempty"` +} + +// TestResourceTemplateSpec defines the spec of a TestResourceTemplate. +type TestResourceTemplateSpec struct { + // +required + Template TestResourceTemplateResource `json:"template"` +} + +// TestResourceTemplateResource defines the template resource of a TestResourceTemplate. +type TestResourceTemplateResource struct { + // spec is the desired state of KubeadmControlPlaneTemplateResource. + // +required + Spec TestResourceSpec `json:"spec"` +} + +// TestResourceTemplateList is a list of TestResourceTemplate. +// +kubebuilder:object:root=true +type TestResourceTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestResourceTemplate `json:"items"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=testresources,scope=Namespaced +// +kubebuilder:subresource:status + +// TestResource defines a test resource. +type TestResource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec TestResourceSpec `json:"spec,omitempty"` +} + +// TestResourceSpec defines the resource spec. +type TestResourceSpec struct { + // mandatory field from the Cluster API contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // Mandatory field from the Cluster API contract - version support + + // +optional + Version string `json:"version,omitempty"` + + // Mandatory field from the Cluster API contract - machine support + + // +required + MachineTemplate TestResourceMachineTemplateSpec `json:"machineTemplate"` + + // General purpose fields to be used in different test scenario. + + // +optional + Omitable string `json:"omitable,omitempty"` +} + +// TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. +type TestResourceMachineTemplateSpec struct { + // +optional + ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty"` + + // +optional + InfrastructureRef TestContractVersionedObjectReference `json:"infrastructureRef"` +} + +// TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. +type TestContractVersionedObjectReference struct { + // +optional + Kind string `json:"kind"` + + // +optional + Name string `json:"name"` + + // +optional + APIGroup string `json:"apiGroup"` +} + +// TestResourceStatus defines the status of a TestResource. +type TestResourceStatus struct { + // mandatory field from the Cluster API contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // +optional + ReadyReplicas *int32 `json:"readyReplicas,omitempty"` + + // +optional + AvailableReplicas *int32 `json:"availableReplicas,omitempty"` + + // +optional + UpToDateReplicas *int32 `json:"upToDateReplicas,omitempty"` + + // Mandatory field from the Cluster API contract - version support + + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + Version *string `json:"version,omitempty"` +} + +// TestResourceList is a list of TestResource. +// +kubebuilder:object:root=true +type TestResourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestResource `json:"items"` +} diff --git a/internal/topology/upgrade/test/t2/v1beta1/zz_generated.conversion.go b/internal/topology/upgrade/test/t2/v1beta1/zz_generated.conversion.go new file mode 100644 index 000000000000..449f2a7abaa1 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta1/zz_generated.conversion.go @@ -0,0 +1,392 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by conversion-gen. DO NOT EDIT. + +package v1beta1 + +import ( + unsafe "unsafe" + + conversion "k8s.io/apimachinery/pkg/conversion" + runtime "k8s.io/apimachinery/pkg/runtime" + v1beta2 "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t2/v1beta2" +) + +func init() { + localSchemeBuilder.Register(RegisterConversions) +} + +// RegisterConversions adds conversion functions to the given scheme. +// Public to allow building arbitrary schemes. +func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*TestContractVersionedObjectReference)(nil), (*v1beta2.TestContractVersionedObjectReference)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestContractVersionedObjectReference_To_v1beta2_TestContractVersionedObjectReference(a.(*TestContractVersionedObjectReference), b.(*v1beta2.TestContractVersionedObjectReference), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestContractVersionedObjectReference)(nil), (*TestContractVersionedObjectReference)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestContractVersionedObjectReference_To_v1beta1_TestContractVersionedObjectReference(a.(*v1beta2.TestContractVersionedObjectReference), b.(*TestContractVersionedObjectReference), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResource)(nil), (*v1beta2.TestResource)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResource_To_v1beta2_TestResource(a.(*TestResource), b.(*v1beta2.TestResource), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResource)(nil), (*TestResource)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResource_To_v1beta1_TestResource(a.(*v1beta2.TestResource), b.(*TestResource), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResourceList)(nil), (*v1beta2.TestResourceList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResourceList_To_v1beta2_TestResourceList(a.(*TestResourceList), b.(*v1beta2.TestResourceList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResourceList)(nil), (*TestResourceList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResourceList_To_v1beta1_TestResourceList(a.(*v1beta2.TestResourceList), b.(*TestResourceList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResourceMachineTemplateSpec)(nil), (*v1beta2.TestResourceMachineTemplateSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResourceMachineTemplateSpec_To_v1beta2_TestResourceMachineTemplateSpec(a.(*TestResourceMachineTemplateSpec), b.(*v1beta2.TestResourceMachineTemplateSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResourceMachineTemplateSpec)(nil), (*TestResourceMachineTemplateSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResourceMachineTemplateSpec_To_v1beta1_TestResourceMachineTemplateSpec(a.(*v1beta2.TestResourceMachineTemplateSpec), b.(*TestResourceMachineTemplateSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResourceSpec)(nil), (*v1beta2.TestResourceSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResourceSpec_To_v1beta2_TestResourceSpec(a.(*TestResourceSpec), b.(*v1beta2.TestResourceSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResourceSpec)(nil), (*TestResourceSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResourceSpec_To_v1beta1_TestResourceSpec(a.(*v1beta2.TestResourceSpec), b.(*TestResourceSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResourceStatus)(nil), (*v1beta2.TestResourceStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResourceStatus_To_v1beta2_TestResourceStatus(a.(*TestResourceStatus), b.(*v1beta2.TestResourceStatus), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResourceStatus)(nil), (*TestResourceStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResourceStatus_To_v1beta1_TestResourceStatus(a.(*v1beta2.TestResourceStatus), b.(*TestResourceStatus), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResourceTemplate)(nil), (*v1beta2.TestResourceTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResourceTemplate_To_v1beta2_TestResourceTemplate(a.(*TestResourceTemplate), b.(*v1beta2.TestResourceTemplate), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResourceTemplate)(nil), (*TestResourceTemplate)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResourceTemplate_To_v1beta1_TestResourceTemplate(a.(*v1beta2.TestResourceTemplate), b.(*TestResourceTemplate), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResourceTemplateList)(nil), (*v1beta2.TestResourceTemplateList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResourceTemplateList_To_v1beta2_TestResourceTemplateList(a.(*TestResourceTemplateList), b.(*v1beta2.TestResourceTemplateList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResourceTemplateList)(nil), (*TestResourceTemplateList)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResourceTemplateList_To_v1beta1_TestResourceTemplateList(a.(*v1beta2.TestResourceTemplateList), b.(*TestResourceTemplateList), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResourceTemplateResource)(nil), (*v1beta2.TestResourceTemplateResource)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResourceTemplateResource_To_v1beta2_TestResourceTemplateResource(a.(*TestResourceTemplateResource), b.(*v1beta2.TestResourceTemplateResource), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResourceTemplateResource)(nil), (*TestResourceTemplateResource)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResourceTemplateResource_To_v1beta1_TestResourceTemplateResource(a.(*v1beta2.TestResourceTemplateResource), b.(*TestResourceTemplateResource), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*TestResourceTemplateSpec)(nil), (*v1beta2.TestResourceTemplateSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_TestResourceTemplateSpec_To_v1beta2_TestResourceTemplateSpec(a.(*TestResourceTemplateSpec), b.(*v1beta2.TestResourceTemplateSpec), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*v1beta2.TestResourceTemplateSpec)(nil), (*TestResourceTemplateSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_TestResourceTemplateSpec_To_v1beta1_TestResourceTemplateSpec(a.(*v1beta2.TestResourceTemplateSpec), b.(*TestResourceTemplateSpec), scope) + }); err != nil { + return err + } + return nil +} + +func autoConvert_v1beta1_TestContractVersionedObjectReference_To_v1beta2_TestContractVersionedObjectReference(in *TestContractVersionedObjectReference, out *v1beta2.TestContractVersionedObjectReference, s conversion.Scope) error { + out.Kind = in.Kind + out.Name = in.Name + out.APIGroup = in.APIGroup + return nil +} + +// Convert_v1beta1_TestContractVersionedObjectReference_To_v1beta2_TestContractVersionedObjectReference is an autogenerated conversion function. +func Convert_v1beta1_TestContractVersionedObjectReference_To_v1beta2_TestContractVersionedObjectReference(in *TestContractVersionedObjectReference, out *v1beta2.TestContractVersionedObjectReference, s conversion.Scope) error { + return autoConvert_v1beta1_TestContractVersionedObjectReference_To_v1beta2_TestContractVersionedObjectReference(in, out, s) +} + +func autoConvert_v1beta2_TestContractVersionedObjectReference_To_v1beta1_TestContractVersionedObjectReference(in *v1beta2.TestContractVersionedObjectReference, out *TestContractVersionedObjectReference, s conversion.Scope) error { + out.Kind = in.Kind + out.Name = in.Name + out.APIGroup = in.APIGroup + return nil +} + +// Convert_v1beta2_TestContractVersionedObjectReference_To_v1beta1_TestContractVersionedObjectReference is an autogenerated conversion function. +func Convert_v1beta2_TestContractVersionedObjectReference_To_v1beta1_TestContractVersionedObjectReference(in *v1beta2.TestContractVersionedObjectReference, out *TestContractVersionedObjectReference, s conversion.Scope) error { + return autoConvert_v1beta2_TestContractVersionedObjectReference_To_v1beta1_TestContractVersionedObjectReference(in, out, s) +} + +func autoConvert_v1beta1_TestResource_To_v1beta2_TestResource(in *TestResource, out *v1beta2.TestResource, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1beta1_TestResourceSpec_To_v1beta2_TestResourceSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta1_TestResource_To_v1beta2_TestResource is an autogenerated conversion function. +func Convert_v1beta1_TestResource_To_v1beta2_TestResource(in *TestResource, out *v1beta2.TestResource, s conversion.Scope) error { + return autoConvert_v1beta1_TestResource_To_v1beta2_TestResource(in, out, s) +} + +func autoConvert_v1beta2_TestResource_To_v1beta1_TestResource(in *v1beta2.TestResource, out *TestResource, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1beta2_TestResourceSpec_To_v1beta1_TestResourceSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta2_TestResource_To_v1beta1_TestResource is an autogenerated conversion function. +func Convert_v1beta2_TestResource_To_v1beta1_TestResource(in *v1beta2.TestResource, out *TestResource, s conversion.Scope) error { + return autoConvert_v1beta2_TestResource_To_v1beta1_TestResource(in, out, s) +} + +func autoConvert_v1beta1_TestResourceList_To_v1beta2_TestResourceList(in *TestResourceList, out *v1beta2.TestResourceList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]v1beta2.TestResource)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1beta1_TestResourceList_To_v1beta2_TestResourceList is an autogenerated conversion function. +func Convert_v1beta1_TestResourceList_To_v1beta2_TestResourceList(in *TestResourceList, out *v1beta2.TestResourceList, s conversion.Scope) error { + return autoConvert_v1beta1_TestResourceList_To_v1beta2_TestResourceList(in, out, s) +} + +func autoConvert_v1beta2_TestResourceList_To_v1beta1_TestResourceList(in *v1beta2.TestResourceList, out *TestResourceList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]TestResource)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1beta2_TestResourceList_To_v1beta1_TestResourceList is an autogenerated conversion function. +func Convert_v1beta2_TestResourceList_To_v1beta1_TestResourceList(in *v1beta2.TestResourceList, out *TestResourceList, s conversion.Scope) error { + return autoConvert_v1beta2_TestResourceList_To_v1beta1_TestResourceList(in, out, s) +} + +func autoConvert_v1beta1_TestResourceMachineTemplateSpec_To_v1beta2_TestResourceMachineTemplateSpec(in *TestResourceMachineTemplateSpec, out *v1beta2.TestResourceMachineTemplateSpec, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1beta1_TestContractVersionedObjectReference_To_v1beta2_TestContractVersionedObjectReference(&in.InfrastructureRef, &out.InfrastructureRef, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta1_TestResourceMachineTemplateSpec_To_v1beta2_TestResourceMachineTemplateSpec is an autogenerated conversion function. +func Convert_v1beta1_TestResourceMachineTemplateSpec_To_v1beta2_TestResourceMachineTemplateSpec(in *TestResourceMachineTemplateSpec, out *v1beta2.TestResourceMachineTemplateSpec, s conversion.Scope) error { + return autoConvert_v1beta1_TestResourceMachineTemplateSpec_To_v1beta2_TestResourceMachineTemplateSpec(in, out, s) +} + +func autoConvert_v1beta2_TestResourceMachineTemplateSpec_To_v1beta1_TestResourceMachineTemplateSpec(in *v1beta2.TestResourceMachineTemplateSpec, out *TestResourceMachineTemplateSpec, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1beta2_TestContractVersionedObjectReference_To_v1beta1_TestContractVersionedObjectReference(&in.InfrastructureRef, &out.InfrastructureRef, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta2_TestResourceMachineTemplateSpec_To_v1beta1_TestResourceMachineTemplateSpec is an autogenerated conversion function. +func Convert_v1beta2_TestResourceMachineTemplateSpec_To_v1beta1_TestResourceMachineTemplateSpec(in *v1beta2.TestResourceMachineTemplateSpec, out *TestResourceMachineTemplateSpec, s conversion.Scope) error { + return autoConvert_v1beta2_TestResourceMachineTemplateSpec_To_v1beta1_TestResourceMachineTemplateSpec(in, out, s) +} + +func autoConvert_v1beta1_TestResourceSpec_To_v1beta2_TestResourceSpec(in *TestResourceSpec, out *v1beta2.TestResourceSpec, s conversion.Scope) error { + out.Replicas = (*int32)(unsafe.Pointer(in.Replicas)) + out.Version = in.Version + if err := Convert_v1beta1_TestResourceMachineTemplateSpec_To_v1beta2_TestResourceMachineTemplateSpec(&in.MachineTemplate, &out.MachineTemplate, s); err != nil { + return err + } + out.Omitable = in.Omitable + return nil +} + +// Convert_v1beta1_TestResourceSpec_To_v1beta2_TestResourceSpec is an autogenerated conversion function. +func Convert_v1beta1_TestResourceSpec_To_v1beta2_TestResourceSpec(in *TestResourceSpec, out *v1beta2.TestResourceSpec, s conversion.Scope) error { + return autoConvert_v1beta1_TestResourceSpec_To_v1beta2_TestResourceSpec(in, out, s) +} + +func autoConvert_v1beta2_TestResourceSpec_To_v1beta1_TestResourceSpec(in *v1beta2.TestResourceSpec, out *TestResourceSpec, s conversion.Scope) error { + out.Replicas = (*int32)(unsafe.Pointer(in.Replicas)) + out.Version = in.Version + if err := Convert_v1beta2_TestResourceMachineTemplateSpec_To_v1beta1_TestResourceMachineTemplateSpec(&in.MachineTemplate, &out.MachineTemplate, s); err != nil { + return err + } + out.Omitable = in.Omitable + return nil +} + +// Convert_v1beta2_TestResourceSpec_To_v1beta1_TestResourceSpec is an autogenerated conversion function. +func Convert_v1beta2_TestResourceSpec_To_v1beta1_TestResourceSpec(in *v1beta2.TestResourceSpec, out *TestResourceSpec, s conversion.Scope) error { + return autoConvert_v1beta2_TestResourceSpec_To_v1beta1_TestResourceSpec(in, out, s) +} + +func autoConvert_v1beta1_TestResourceStatus_To_v1beta2_TestResourceStatus(in *TestResourceStatus, out *v1beta2.TestResourceStatus, s conversion.Scope) error { + out.Replicas = (*int32)(unsafe.Pointer(in.Replicas)) + out.ReadyReplicas = (*int32)(unsafe.Pointer(in.ReadyReplicas)) + out.AvailableReplicas = (*int32)(unsafe.Pointer(in.AvailableReplicas)) + out.UpToDateReplicas = (*int32)(unsafe.Pointer(in.UpToDateReplicas)) + out.Version = (*string)(unsafe.Pointer(in.Version)) + return nil +} + +// Convert_v1beta1_TestResourceStatus_To_v1beta2_TestResourceStatus is an autogenerated conversion function. +func Convert_v1beta1_TestResourceStatus_To_v1beta2_TestResourceStatus(in *TestResourceStatus, out *v1beta2.TestResourceStatus, s conversion.Scope) error { + return autoConvert_v1beta1_TestResourceStatus_To_v1beta2_TestResourceStatus(in, out, s) +} + +func autoConvert_v1beta2_TestResourceStatus_To_v1beta1_TestResourceStatus(in *v1beta2.TestResourceStatus, out *TestResourceStatus, s conversion.Scope) error { + out.Replicas = (*int32)(unsafe.Pointer(in.Replicas)) + out.ReadyReplicas = (*int32)(unsafe.Pointer(in.ReadyReplicas)) + out.AvailableReplicas = (*int32)(unsafe.Pointer(in.AvailableReplicas)) + out.UpToDateReplicas = (*int32)(unsafe.Pointer(in.UpToDateReplicas)) + out.Version = (*string)(unsafe.Pointer(in.Version)) + return nil +} + +// Convert_v1beta2_TestResourceStatus_To_v1beta1_TestResourceStatus is an autogenerated conversion function. +func Convert_v1beta2_TestResourceStatus_To_v1beta1_TestResourceStatus(in *v1beta2.TestResourceStatus, out *TestResourceStatus, s conversion.Scope) error { + return autoConvert_v1beta2_TestResourceStatus_To_v1beta1_TestResourceStatus(in, out, s) +} + +func autoConvert_v1beta1_TestResourceTemplate_To_v1beta2_TestResourceTemplate(in *TestResourceTemplate, out *v1beta2.TestResourceTemplate, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1beta1_TestResourceTemplateSpec_To_v1beta2_TestResourceTemplateSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta1_TestResourceTemplate_To_v1beta2_TestResourceTemplate is an autogenerated conversion function. +func Convert_v1beta1_TestResourceTemplate_To_v1beta2_TestResourceTemplate(in *TestResourceTemplate, out *v1beta2.TestResourceTemplate, s conversion.Scope) error { + return autoConvert_v1beta1_TestResourceTemplate_To_v1beta2_TestResourceTemplate(in, out, s) +} + +func autoConvert_v1beta2_TestResourceTemplate_To_v1beta1_TestResourceTemplate(in *v1beta2.TestResourceTemplate, out *TestResourceTemplate, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1beta2_TestResourceTemplateSpec_To_v1beta1_TestResourceTemplateSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta2_TestResourceTemplate_To_v1beta1_TestResourceTemplate is an autogenerated conversion function. +func Convert_v1beta2_TestResourceTemplate_To_v1beta1_TestResourceTemplate(in *v1beta2.TestResourceTemplate, out *TestResourceTemplate, s conversion.Scope) error { + return autoConvert_v1beta2_TestResourceTemplate_To_v1beta1_TestResourceTemplate(in, out, s) +} + +func autoConvert_v1beta1_TestResourceTemplateList_To_v1beta2_TestResourceTemplateList(in *TestResourceTemplateList, out *v1beta2.TestResourceTemplateList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]v1beta2.TestResourceTemplate)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1beta1_TestResourceTemplateList_To_v1beta2_TestResourceTemplateList is an autogenerated conversion function. +func Convert_v1beta1_TestResourceTemplateList_To_v1beta2_TestResourceTemplateList(in *TestResourceTemplateList, out *v1beta2.TestResourceTemplateList, s conversion.Scope) error { + return autoConvert_v1beta1_TestResourceTemplateList_To_v1beta2_TestResourceTemplateList(in, out, s) +} + +func autoConvert_v1beta2_TestResourceTemplateList_To_v1beta1_TestResourceTemplateList(in *v1beta2.TestResourceTemplateList, out *TestResourceTemplateList, s conversion.Scope) error { + out.ListMeta = in.ListMeta + out.Items = *(*[]TestResourceTemplate)(unsafe.Pointer(&in.Items)) + return nil +} + +// Convert_v1beta2_TestResourceTemplateList_To_v1beta1_TestResourceTemplateList is an autogenerated conversion function. +func Convert_v1beta2_TestResourceTemplateList_To_v1beta1_TestResourceTemplateList(in *v1beta2.TestResourceTemplateList, out *TestResourceTemplateList, s conversion.Scope) error { + return autoConvert_v1beta2_TestResourceTemplateList_To_v1beta1_TestResourceTemplateList(in, out, s) +} + +func autoConvert_v1beta1_TestResourceTemplateResource_To_v1beta2_TestResourceTemplateResource(in *TestResourceTemplateResource, out *v1beta2.TestResourceTemplateResource, s conversion.Scope) error { + if err := Convert_v1beta1_TestResourceSpec_To_v1beta2_TestResourceSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta1_TestResourceTemplateResource_To_v1beta2_TestResourceTemplateResource is an autogenerated conversion function. +func Convert_v1beta1_TestResourceTemplateResource_To_v1beta2_TestResourceTemplateResource(in *TestResourceTemplateResource, out *v1beta2.TestResourceTemplateResource, s conversion.Scope) error { + return autoConvert_v1beta1_TestResourceTemplateResource_To_v1beta2_TestResourceTemplateResource(in, out, s) +} + +func autoConvert_v1beta2_TestResourceTemplateResource_To_v1beta1_TestResourceTemplateResource(in *v1beta2.TestResourceTemplateResource, out *TestResourceTemplateResource, s conversion.Scope) error { + if err := Convert_v1beta2_TestResourceSpec_To_v1beta1_TestResourceSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta2_TestResourceTemplateResource_To_v1beta1_TestResourceTemplateResource is an autogenerated conversion function. +func Convert_v1beta2_TestResourceTemplateResource_To_v1beta1_TestResourceTemplateResource(in *v1beta2.TestResourceTemplateResource, out *TestResourceTemplateResource, s conversion.Scope) error { + return autoConvert_v1beta2_TestResourceTemplateResource_To_v1beta1_TestResourceTemplateResource(in, out, s) +} + +func autoConvert_v1beta1_TestResourceTemplateSpec_To_v1beta2_TestResourceTemplateSpec(in *TestResourceTemplateSpec, out *v1beta2.TestResourceTemplateSpec, s conversion.Scope) error { + if err := Convert_v1beta1_TestResourceTemplateResource_To_v1beta2_TestResourceTemplateResource(&in.Template, &out.Template, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta1_TestResourceTemplateSpec_To_v1beta2_TestResourceTemplateSpec is an autogenerated conversion function. +func Convert_v1beta1_TestResourceTemplateSpec_To_v1beta2_TestResourceTemplateSpec(in *TestResourceTemplateSpec, out *v1beta2.TestResourceTemplateSpec, s conversion.Scope) error { + return autoConvert_v1beta1_TestResourceTemplateSpec_To_v1beta2_TestResourceTemplateSpec(in, out, s) +} + +func autoConvert_v1beta2_TestResourceTemplateSpec_To_v1beta1_TestResourceTemplateSpec(in *v1beta2.TestResourceTemplateSpec, out *TestResourceTemplateSpec, s conversion.Scope) error { + if err := Convert_v1beta2_TestResourceTemplateResource_To_v1beta1_TestResourceTemplateResource(&in.Template, &out.Template, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta2_TestResourceTemplateSpec_To_v1beta1_TestResourceTemplateSpec is an autogenerated conversion function. +func Convert_v1beta2_TestResourceTemplateSpec_To_v1beta1_TestResourceTemplateSpec(in *v1beta2.TestResourceTemplateSpec, out *TestResourceTemplateSpec, s conversion.Scope) error { + return autoConvert_v1beta2_TestResourceTemplateSpec_To_v1beta1_TestResourceTemplateSpec(in, out, s) +} diff --git a/internal/topology/upgrade/test/t2/v1beta1/zz_generated.deepcopy.go b/internal/topology/upgrade/test/t2/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 000000000000..3fb0faf7e0b5 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,266 @@ +//go:build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestContractVersionedObjectReference) DeepCopyInto(out *TestContractVersionedObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestContractVersionedObjectReference. +func (in *TestContractVersionedObjectReference) DeepCopy() *TestContractVersionedObjectReference { + if in == nil { + return nil + } + out := new(TestContractVersionedObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResource) DeepCopyInto(out *TestResource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResource. +func (in *TestResource) DeepCopy() *TestResource { + if in == nil { + return nil + } + out := new(TestResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceList) DeepCopyInto(out *TestResourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceList. +func (in *TestResourceList) DeepCopy() *TestResourceList { + if in == nil { + return nil + } + out := new(TestResourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceMachineTemplateSpec) DeepCopyInto(out *TestResourceMachineTemplateSpec) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.InfrastructureRef = in.InfrastructureRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceMachineTemplateSpec. +func (in *TestResourceMachineTemplateSpec) DeepCopy() *TestResourceMachineTemplateSpec { + if in == nil { + return nil + } + out := new(TestResourceMachineTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceSpec) DeepCopyInto(out *TestResourceSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + in.MachineTemplate.DeepCopyInto(&out.MachineTemplate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceSpec. +func (in *TestResourceSpec) DeepCopy() *TestResourceSpec { + if in == nil { + return nil + } + out := new(TestResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceStatus) DeepCopyInto(out *TestResourceStatus) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.ReadyReplicas != nil { + in, out := &in.ReadyReplicas, &out.ReadyReplicas + *out = new(int32) + **out = **in + } + if in.AvailableReplicas != nil { + in, out := &in.AvailableReplicas, &out.AvailableReplicas + *out = new(int32) + **out = **in + } + if in.UpToDateReplicas != nil { + in, out := &in.UpToDateReplicas, &out.UpToDateReplicas + *out = new(int32) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceStatus. +func (in *TestResourceStatus) DeepCopy() *TestResourceStatus { + if in == nil { + return nil + } + out := new(TestResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplate) DeepCopyInto(out *TestResourceTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplate. +func (in *TestResourceTemplate) DeepCopy() *TestResourceTemplate { + if in == nil { + return nil + } + out := new(TestResourceTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateList) DeepCopyInto(out *TestResourceTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestResourceTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateList. +func (in *TestResourceTemplateList) DeepCopy() *TestResourceTemplateList { + if in == nil { + return nil + } + out := new(TestResourceTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateResource) DeepCopyInto(out *TestResourceTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateResource. +func (in *TestResourceTemplateResource) DeepCopy() *TestResourceTemplateResource { + if in == nil { + return nil + } + out := new(TestResourceTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateSpec) DeepCopyInto(out *TestResourceTemplateSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateSpec. +func (in *TestResourceTemplateSpec) DeepCopy() *TestResourceTemplateSpec { + if in == nil { + return nil + } + out := new(TestResourceTemplateSpec) + in.DeepCopyInto(out) + return out +} diff --git a/internal/topology/upgrade/test/t2/v1beta2/conversion.go b/internal/topology/upgrade/test/t2/v1beta2/conversion.go new file mode 100644 index 000000000000..0adf7499c011 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta2/conversion.go @@ -0,0 +1,20 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +func (*TestResourceTemplate) Hub() {} +func (*TestResource) Hub() {} diff --git a/internal/topology/upgrade/test/t2/v1beta2/groupversion_info.go b/internal/topology/upgrade/test/t2/v1beta2/groupversion_info.go new file mode 100644 index 000000000000..0e3804918190 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta2/groupversion_info.go @@ -0,0 +1,43 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + // GroupVersion is group version used to test CRD migration. + GroupVersion = schema.GroupVersion{Group: "test.cluster.x-k8s.io", Version: "v1beta2"} + + // schemeBuilder is used to add go types to the GroupVersionKind scheme. + schemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + + // AddToScheme adds the types to the given scheme. + AddToScheme = schemeBuilder.AddToScheme +) + +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(GroupVersion, + &TestResourceTemplate{}, &TestResourceTemplateList{}, + &TestResource{}, &TestResourceList{}, + ) + metav1.AddToGroupVersion(scheme, GroupVersion) + return nil +} diff --git a/internal/topology/upgrade/test/t2/v1beta2/types.go b/internal/topology/upgrade/test/t2/v1beta2/types.go new file mode 100644 index 000000000000..be71a87508c9 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta2/types.go @@ -0,0 +1,146 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1beta2 contains test types. +// +kubebuilder:object:generate=true +// +groupName=test.cluster.x-k8s.io +package v1beta2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2" +) + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=testresourcetemplates,scope=Namespaced +// +kubebuilder:storageversion + +// TestResourceTemplate defines a test resource template. +type TestResourceTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec TestResourceTemplateSpec `json:"spec,omitempty"` +} + +// TestResourceTemplateSpec defines the spec of a TestResourceTemplate. +type TestResourceTemplateSpec struct { + // +required + Template TestResourceTemplateResource `json:"template"` +} + +// TestResourceTemplateResource defines the template resource of a TestResourceTemplate. +type TestResourceTemplateResource struct { + // spec is the desired state of KubeadmControlPlaneTemplateResource. + // +required + Spec TestResourceSpec `json:"spec"` +} + +// TestResourceTemplateList is a list of TestResourceTemplate. +// +kubebuilder:object:root=true +type TestResourceTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestResourceTemplate `json:"items"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=testresources,scope=Namespaced +// +kubebuilder:storageversion +// +kubebuilder:subresource:status + +// TestResource defines a test resource. +type TestResource struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec TestResourceSpec `json:"spec,omitempty"` +} + +// TestResourceSpec defines the resource spec. +type TestResourceSpec struct { + // mandatory field from the Cluster API contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // Mandatory field from the Cluster API contract - version support + + // +optional + Version string `json:"version,omitempty"` + + // Mandatory field from the Cluster API contract - machine support + + // +required + MachineTemplate TestResourceMachineTemplateSpec `json:"machineTemplate"` + + // General purpose fields to be used in different test scenario. + + // +optional + Omitable string `json:"omitable,omitempty"` +} + +// TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. +type TestResourceMachineTemplateSpec struct { + // +optional + ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty"` + + // +optional + InfrastructureRef TestContractVersionedObjectReference `json:"infrastructureRef"` +} + +// TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. +type TestContractVersionedObjectReference struct { + // +optional + Kind string `json:"kind"` + + // +optional + Name string `json:"name"` + + // +optional + APIGroup string `json:"apiGroup"` +} + +// TestResourceStatus defines the status of a TestResource. +type TestResourceStatus struct { + // mandatory field from the Cluster API contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // +optional + ReadyReplicas *int32 `json:"readyReplicas,omitempty"` + + // +optional + AvailableReplicas *int32 `json:"availableReplicas,omitempty"` + + // +optional + UpToDateReplicas *int32 `json:"upToDateReplicas,omitempty"` + + // Mandatory field from the Cluster API contract - version support + + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=256 + Version *string `json:"version,omitempty"` +} + +// TestResourceList is a list of TestResource. +// +kubebuilder:object:root=true +type TestResourceList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []TestResource `json:"items"` +} diff --git a/internal/topology/upgrade/test/t2/v1beta2/zz_generated.deepcopy.go b/internal/topology/upgrade/test/t2/v1beta2/zz_generated.deepcopy.go new file mode 100644 index 000000000000..9cd5c6caf87b --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta2/zz_generated.deepcopy.go @@ -0,0 +1,266 @@ +//go:build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1beta2 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestContractVersionedObjectReference) DeepCopyInto(out *TestContractVersionedObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestContractVersionedObjectReference. +func (in *TestContractVersionedObjectReference) DeepCopy() *TestContractVersionedObjectReference { + if in == nil { + return nil + } + out := new(TestContractVersionedObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResource) DeepCopyInto(out *TestResource) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResource. +func (in *TestResource) DeepCopy() *TestResource { + if in == nil { + return nil + } + out := new(TestResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResource) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceList) DeepCopyInto(out *TestResourceList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceList. +func (in *TestResourceList) DeepCopy() *TestResourceList { + if in == nil { + return nil + } + out := new(TestResourceList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceMachineTemplateSpec) DeepCopyInto(out *TestResourceMachineTemplateSpec) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.InfrastructureRef = in.InfrastructureRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceMachineTemplateSpec. +func (in *TestResourceMachineTemplateSpec) DeepCopy() *TestResourceMachineTemplateSpec { + if in == nil { + return nil + } + out := new(TestResourceMachineTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceSpec) DeepCopyInto(out *TestResourceSpec) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + in.MachineTemplate.DeepCopyInto(&out.MachineTemplate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceSpec. +func (in *TestResourceSpec) DeepCopy() *TestResourceSpec { + if in == nil { + return nil + } + out := new(TestResourceSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceStatus) DeepCopyInto(out *TestResourceStatus) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.ReadyReplicas != nil { + in, out := &in.ReadyReplicas, &out.ReadyReplicas + *out = new(int32) + **out = **in + } + if in.AvailableReplicas != nil { + in, out := &in.AvailableReplicas, &out.AvailableReplicas + *out = new(int32) + **out = **in + } + if in.UpToDateReplicas != nil { + in, out := &in.UpToDateReplicas, &out.UpToDateReplicas + *out = new(int32) + **out = **in + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceStatus. +func (in *TestResourceStatus) DeepCopy() *TestResourceStatus { + if in == nil { + return nil + } + out := new(TestResourceStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplate) DeepCopyInto(out *TestResourceTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplate. +func (in *TestResourceTemplate) DeepCopy() *TestResourceTemplate { + if in == nil { + return nil + } + out := new(TestResourceTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateList) DeepCopyInto(out *TestResourceTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TestResourceTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateList. +func (in *TestResourceTemplateList) DeepCopy() *TestResourceTemplateList { + if in == nil { + return nil + } + out := new(TestResourceTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TestResourceTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateResource) DeepCopyInto(out *TestResourceTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateResource. +func (in *TestResourceTemplateResource) DeepCopy() *TestResourceTemplateResource { + if in == nil { + return nil + } + out := new(TestResourceTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TestResourceTemplateSpec) DeepCopyInto(out *TestResourceTemplateSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResourceTemplateSpec. +func (in *TestResourceTemplateSpec) DeepCopy() *TestResourceTemplateSpec { + if in == nil { + return nil + } + out := new(TestResourceTemplateSpec) + in.DeepCopyInto(out) + return out +} diff --git a/internal/topology/upgrade/test/t2/webhook/webhook.go b/internal/topology/upgrade/test/t2/webhook/webhook.go new file mode 100644 index 000000000000..7b8319b520c7 --- /dev/null +++ b/internal/topology/upgrade/test/t2/webhook/webhook.go @@ -0,0 +1,90 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package webhook define webhooks for the v1beta2 API. +package webhook + +import ( + "context" + + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + testv1 "sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t2/v1beta2" +) + +// TestResourceTemplate defines CustomDefaulter and CustomValidator for a TestResourceTemplate. +type TestResourceTemplate struct{} + +var _ webhook.CustomDefaulter = &TestResourceTemplate{} +var _ webhook.CustomValidator = &TestResourceTemplate{} + +// Default a TestResourceTemplate. +func (webhook *TestResourceTemplate) Default(_ context.Context, obj runtime.Object) error { + r := obj.(*testv1.TestResourceTemplate) + if r.Annotations == nil { + r.Annotations = map[string]string{} + } + r.Annotations["default-t2"] = "" + return nil +} + +// ValidateCreate a TestResourceTemplate. +func (webhook *TestResourceTemplate) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateUpdate a TestResourceTemplate. +func (webhook *TestResourceTemplate) ValidateUpdate(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateDelete a TestResourceTemplate. +func (webhook *TestResourceTemplate) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// TestResource defines CustomDefaulter and CustomValidator for a TestResource. +type TestResource struct{} + +var _ webhook.CustomDefaulter = &TestResource{} +var _ webhook.CustomValidator = &TestResource{} + +// Default a TestResource. +func (webhook *TestResource) Default(_ context.Context, obj runtime.Object) error { + r := obj.(*testv1.TestResource) + if r.Annotations == nil { + r.Annotations = map[string]string{} + } + r.Annotations["default-t2"] = "" + return nil +} + +// ValidateCreate a TestResource. +func (webhook *TestResource) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateUpdate a TestResource. +func (webhook *TestResource) ValidateUpdate(_ context.Context, _, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} + +// ValidateDelete a TestResource. +func (webhook *TestResource) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + return nil, nil +} From c32ed91b9e7a8485529e04a6801c8ff7fe435203 Mon Sep 17 00:00:00 2001 From: fabriziopandini Date: Thu, 26 Jun 2025 16:19:21 +0200 Subject: [PATCH 2/4] Address feedback --- Makefile | 8 +- controllers/crdmigrator/crd_migrator_test.go | 8 +- .../upgrade/clusterctl_upgrade_test.go | 164 ++++++++++-------- .../test.cluster.x-k8s.io_testresources.yaml | 35 +--- ...luster.x-k8s.io_testresourcetemplates.yaml | 15 +- .../topology/upgrade/test/t1/v1beta1/types.go | 21 ++- .../test/t1/v1beta1/zz_generated.deepcopy.go | 1 - .../test.cluster.x-k8s.io_testresources.yaml | 30 ++-- ...luster.x-k8s.io_testresourcetemplates.yaml | 30 ++-- .../topology/upgrade/test/t2/v1beta1/doc.go | 1 - .../topology/upgrade/test/t2/v1beta1/types.go | 16 +- .../t2/v1beta1/zz_generated.conversion.go | 4 +- .../topology/upgrade/test/t2/v1beta2/types.go | 16 +- 13 files changed, 172 insertions(+), 177 deletions(-) diff --git a/Makefile b/Makefile index d7532f539444..e2f36eb7d00e 100644 --- a/Makefile +++ b/Makefile @@ -406,14 +406,14 @@ generate-go-deepcopy-core: $(CONTROLLER_GEN) ## Generate deepcopy go code for co paths=./api/ipam/... \ paths=./api/runtime/... \ paths=./api/runtime/hooks/... \ + paths=./cmd/clusterctl/... \ + paths=./controllers/crdmigrator/test/... \ paths=./internal/api/addons/... \ paths=./internal/api/core/... \ paths=./internal/runtime/test/... \ - paths=./cmd/clusterctl/... \ - paths=./util/test/builder/... \ paths=./internal/topology/upgrade/test/... \ - paths=./util/deprecated/v1beta1/test/builder/... \ - paths=./controllers/crdmigrator/test/... + paths=./util/test/builder/... \ + paths=./util/deprecated/v1beta1/test/builder/... .PHONY: generate-go-deepcopy-kubeadm-bootstrap generate-go-deepcopy-kubeadm-bootstrap: $(CONTROLLER_GEN) ## Generate deepcopy go code for kubeadm bootstrap diff --git a/controllers/crdmigrator/crd_migrator_test.go b/controllers/crdmigrator/crd_migrator_test.go index 51acf4d76c06..1000ac050eb7 100644 --- a/controllers/crdmigrator/crd_migrator_test.go +++ b/controllers/crdmigrator/crd_migrator_test.go @@ -167,7 +167,7 @@ func TestReconcile(t *testing.T) { }() t.Logf("T1: Install CRDs") - g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", "test", "t1", "crd"))).To(Succeed()) + g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "test", "t1", "crd"))).To(Succeed()) validateStoredVersions(t, g, crdObjectKey, "v1beta1") t.Logf("T1: Start Manager") @@ -208,7 +208,7 @@ func TestReconcile(t *testing.T) { stopManager(cancelManager, managerStopped) t.Logf("T2: Install CRDs") - g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", "test", "t2", "crd"))).To(Succeed()) + g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "test", "t2", "crd"))).To(Succeed()) validateStoredVersions(t, g, crdObjectKey, "v1beta1", "v1beta2") t.Logf("T2: Start Manager") @@ -242,7 +242,7 @@ func TestReconcile(t *testing.T) { stopManager(cancelManager, managerStopped) t.Logf("T3: Install CRDs") - g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", "test", "t3", "crd"))).To(Succeed()) + g.Expect(env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "test", "t3", "crd"))).To(Succeed()) // Stored versions didn't change. if skipCRDMigrationPhases.Has(StorageVersionMigrationPhase) { validateStoredVersions(t, g, crdObjectKey, "v1beta1", "v1beta2") @@ -281,7 +281,7 @@ func TestReconcile(t *testing.T) { stopManager(cancelManager, managerStopped) t.Logf("T4: Install CRDs") - err = env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "..", "..", "controllers", "crdmigrator", "test", "t4", "crd")) + err = env.ApplyCRDs(ctx, filepath.Join(path.Dir(filename), "test", "t4", "crd")) if skipCRDMigrationPhases.Has(StorageVersionMigrationPhase) { // If storage version migration was skipped before, we now cannot deploy CRDs that remove v1beta1. g.Expect(err).To(HaveOccurred()) diff --git a/internal/topology/upgrade/clusterctl_upgrade_test.go b/internal/topology/upgrade/clusterctl_upgrade_test.go index e1e19f233a06..e9e42f361538 100644 --- a/internal/topology/upgrade/clusterctl_upgrade_test.go +++ b/internal/topology/upgrade/clusterctl_upgrade_test.go @@ -60,15 +60,15 @@ import ( ) const ( - allowOmitableFields = true - doNotAllowOmitableFields = false + omittableFieldsMustBeSet = true + omittableFieldsMustNotBeSet = false ) // allObjs can be used to look at all the objects / how they change across generation while debugging. var allObjs map[string]map[string]map[int64]client.Object func addToAllObj(cluster, prefix string, obj client.Object) { - ref := fmt.Sprintf("%s - %s, %s", prefix, klog.KObj(obj), obj.GetName()) + ref := fmt.Sprintf("%s - %s", prefix, klog.KObj(obj)) if allObjs == nil { allObjs = make(map[string]map[string]map[int64]client.Object) } @@ -81,14 +81,23 @@ func addToAllObj(cluster, prefix string, obj client.Object) { allObjs[cluster][ref][obj.GetGeneration()] = obj } -// TestDropDefaulterRemoveUnknownOrOmitableFields validates effects of removing the DropDefaulterRemoveUnknownOrOmitableFields option +// TestDropDefaulterRemoveUnknownOrOmittableFields validates effects of removing the DropDefaulterRemoveUnknownOrOmitableFields option // from provider's defaulting webhooks. -// TL;DR this should cause a rollout only when rebasing. -func TestDropDefaulterRemoveUnknownOrOmitableFields(t *testing.T) { +// TL;DR if setting omittable fields in a patch, this should cause a rollout only when rebasing to a ClusterClass which uses provider objects with a new apiVersion. +// Details: +// Case 1: cluster1 created at t1 with v1beta references, rebased at t2 to v1beta2 references +// * t1: create cluster1 with clusterClasst1 => should be stable +// * t1 => t2: upgrade CRDs / webhooks +// * t2: force reconcile cluster1 => should be stable +// * t2: rebase cluster1 to clusterClasst2 => should roll out and then be stable +// Case 2: cluster2 created at t2 with v1beta references, rebased at t2 to v1beta2 references +// * t2: create cluster2 with clusterClasst1 => should be stable +// * t2: rebase cluster2 to clusterClasst2 => should roll out and then be stable +func TestDropDefaulterRemoveUnknownOrOmittableFields(t *testing.T) { utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true) g := NewWithT(t) - ns, err := env.CreateNamespace(ctx, "test-optional-required") + ns, err := env.CreateNamespace(ctx, "test-drop-defaulter-option") g.Expect(err).ToNot(HaveOccurred()) // T1 - mimic an existing environment with a provider using the DefaulterRemoveUnknownOrOmitableFields option in its own defaulting webhook. @@ -99,7 +108,7 @@ func TestDropDefaulterRemoveUnknownOrOmitableFields(t *testing.T) { ct1, t1CheckObj, t1CheckTemplate, t1webhookConfig, t1TemplateWebhookConfig := setupT1CRDAndWebHooks(g, ns) // create clusterClasst1 using v1beta1 references - // Notably, this cluster class implements patches adding omitable fields. + // Notably, this cluster class implements patches adding omittable fields. t.Log("create clusterClasst1") clusterClasst1 := createT1ClusterClass(g, ns, ct1) @@ -108,21 +117,21 @@ func TestDropDefaulterRemoveUnknownOrOmitableFields(t *testing.T) { cluster1 := createClusterWithClusterClass(g, ns.Name, "cluster1", clusterClasst1) // check cluster1 is stable (no infinite reconcile or unexpected template rotation). - // Also, checks that the omitable fields that are added by ClusterClass patches are dropped by the DefaulterRemoveUnknownOrOmitableFields. + // Also, checks that the omittable fields that are added by ClusterClass patches are dropped by the DefaulterRemoveUnknownOrOmitableFields. t.Log("check cluster1 is stable") var cluster1Refs map[clusterv1.ContractVersionedObjectReference]int64 g.Eventually(func() error { - cluster1Refs, err = getClusterTopologyReferences(cluster1, "v1beta1", checkOmitableField(doNotAllowOmitableFields)) // The defaulting webhook drops the omitable field when using v1beta1. + cluster1Refs, err = getClusterTopologyReferences(cluster1, "v1beta1", checkOmittableField(omittableFieldsMustNotBeSet)) // The defaulting webhook drops the omittable field when using v1beta1. if err != nil { return err } return nil }, 5*time.Second).Should(Succeed()) - assertClusterTopologyIsStable(g, cluster1Refs, ns.Name, "v1beta1") + assertClusterTopologyBecomesStable(g, cluster1Refs, ns.Name, "v1beta1") // T2 - mimic a clusterctl upgrade for a provider dropping DefaulterRemoveUnknownOrOmitableFields from its own defaulting webhook. - // setupT2CRDAndWebHooks setups CRD and web hooks at t2,. + // setupT2CRDAndWebHooks setups CRD and web hooks at t2. // At t2 we have a CRD in v1beta1 and v1beta2 with the conversion webhook, and the defaulting web hook without the DefaulterRemoveUnknownOrOmitableFields option. t.Log("setupT2CRDAndWebHooks") @@ -151,10 +160,10 @@ func TestDropDefaulterRemoveUnknownOrOmitableFields(t *testing.T) { // check cluster1 (created at t1, with a cluster class using v1beta1 references) is still stable after the clusterctl upgrade. t.Log("check cluster1 is still stable") - assertClusterTopologyIsStable(g, cluster1Refs, ns.Name, "v1beta2") + assertClusterTopologyBecomesStable(g, cluster1Refs, ns.Name, "v1beta2") // rebase cluster1 to a CC created at t2, using v1beta2. - // Also, checks that the omitable fields that are added by ClusterClass is now present. + // Also, checks that the omittable fields that are added by ClusterClass is now present. t.Log("rebase cluster1 to clusterClasst2, check is stable and rollout happens") cluster1New = cluster1New.DeepCopy() cluster1New.Spec.Topology.ClassRef.Name = clusterClasst2.Name @@ -163,24 +172,24 @@ func TestDropDefaulterRemoveUnknownOrOmitableFields(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) var cluster1RefsNew map[clusterv1.ContractVersionedObjectReference]int64 - g.Consistently(func() error { - cluster1RefsNew, err = getClusterTopologyReferences(cluster1, "v1beta2", checkOmitableField(allowOmitableFields)) // The defaulting webhook do not drop anymore the omitable field when using v1beta2. + g.Eventually(func() error { + cluster1RefsNew, err = getClusterTopologyReferences(cluster1, "v1beta2", checkOmittableField(omittableFieldsMustBeSet)) // The defaulting webhook does not drop the omittable field anymore when using v1beta2. if err != nil { return err } return nil }, 5*time.Second).Should(Succeed()) - assertRollout(g, cluster1, cluster1Refs, cluster1RefsNew) // The omitable field should trigger rollout. - assertClusterTopologyIsStable(g, cluster1RefsNew, ns.Name, "v1beta2") + assertRollout(g, cluster1, cluster1Refs, cluster1RefsNew) // The omittable field should trigger rollout. + assertClusterTopologyBecomesStable(g, cluster1RefsNew, ns.Name, "v1beta2") // create cluster2 using clusterClasst1 - // Also, checks that the omitable fields that are added by ClusterClass patches are dropped by the conversion webhook. + // Also, checks that the omittable fields that are added by ClusterClass patches are dropped by the conversion webhook. t.Log("create cluster2 using clusterClasst1") - cluster2 := createClusterWithClusterClass(g, ns.Name, "cluster3", clusterClasst1) + cluster2 := createClusterWithClusterClass(g, ns.Name, "cluster2", clusterClasst1) var cluster2Refs map[clusterv1.ContractVersionedObjectReference]int64 g.Eventually(func() error { - cluster2Refs, err = getClusterTopologyReferences(cluster2, "v1beta2", checkOmitableField(doNotAllowOmitableFields)) // The conversion webhook drops the omitable field when using v1beta1. + cluster2Refs, err = getClusterTopologyReferences(cluster2, "v1beta2", checkOmittableField(omittableFieldsMustNotBeSet)) // The conversion webhook drops the omittable field when using v1beta1. if err != nil { return err } @@ -188,10 +197,10 @@ func TestDropDefaulterRemoveUnknownOrOmitableFields(t *testing.T) { }, 5*time.Second).Should(Succeed()) t.Log("check cluster2 is stable") - assertClusterTopologyIsStable(g, cluster2Refs, ns.Name, "v1beta2") + assertClusterTopologyBecomesStable(g, cluster2Refs, ns.Name, "v1beta2") // rebase cluster2 to a CC created at t2, using v1beta2. - // Also, checks that the omitable fields that are added by ClusterClass is now present. + // Also, checks that the omittable fields that are added by ClusterClass is now present. t.Log("rebase cluster2 to clusterClasst2, check is stable and rollout happens") cluster2New := cluster2.DeepCopy() cluster2New.Spec.Topology.ClassRef.Name = clusterClasst2.Name @@ -200,15 +209,15 @@ func TestDropDefaulterRemoveUnknownOrOmitableFields(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) var cluster2RefsNew map[clusterv1.ContractVersionedObjectReference]int64 - g.Consistently(func() error { - cluster2RefsNew, err = getClusterTopologyReferences(cluster2, "v1beta2", checkOmitableField(allowOmitableFields)) // The defaulting webhook do not drop anymore the omitable field when using v1beta2. + g.Eventually(func() error { + cluster2RefsNew, err = getClusterTopologyReferences(cluster2, "v1beta2", checkOmittableField(omittableFieldsMustBeSet)) // The defaulting webhook do not drop anymore the omittable field when using v1beta2. if err != nil { return err } return nil }, 5*time.Second).Should(Succeed()) - assertRollout(g, cluster2, cluster2Refs, cluster2RefsNew) // The omitable field should trigger rollout. - assertClusterTopologyIsStable(g, cluster2RefsNew, ns.Name, "v1beta2") + assertRollout(g, cluster2, cluster2Refs, cluster2RefsNew) // The omittable field should trigger rollout. + assertClusterTopologyBecomesStable(g, cluster2RefsNew, ns.Name, "v1beta2") // Cleanup err = env.Delete(ctx, t2TemplateWebhookConfig) @@ -241,7 +250,7 @@ func createClusterWithClusterClass(g *WithT, namespace, name string, clusterClas return cluster } -// createT1ClusterClass creates a CC with v1beta1 references and a patch adding the omitable field. +// createT1ClusterClass creates a CC with v1beta1 references and a patch adding the omittable field. func createT1ClusterClass(g *WithT, ns *corev1.Namespace, ct1 client.Client) *clusterv1.ClusterClass { infrastructureClusterTemplate1 := &testt1v1beta1.TestResourceTemplate{ ObjectMeta: metav1.ObjectMeta{ @@ -279,7 +288,7 @@ func createT1ClusterClass(g *WithT, ns *corev1.Namespace, ct1 client.Client) *cl g.Expect(ct1.Create(ctx, bootstrapTemplate)).To(Succeed()) // Waits for the env client to get in cache object we created with client ct1. - waitForObjects(g, ct1.Scheme(), infrastructureClusterTemplate1, infrastructureClusterTemplate1, controlPlaneTemplate, infrastructureMachineTemplate1, bootstrapTemplate) + waitForObjects(g, ct1.Scheme(), infrastructureClusterTemplate1, controlPlaneTemplate, infrastructureMachineTemplate1, bootstrapTemplate) machineDeploymentClass1 := clusterv1.MachineDeploymentClass{ Class: "md-class1", @@ -302,10 +311,6 @@ func createT1ClusterClass(g *WithT, ns *corev1.Namespace, ct1 client.Client) *cl } clusterClass := &clusterv1.ClusterClass{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterClass", - APIVersion: clusterv1.GroupVersion.String(), - }, ObjectMeta: metav1.ObjectMeta{ Name: "cluster-class-t1", Namespace: ns.Name, @@ -331,7 +336,7 @@ func createT1ClusterClass(g *WithT, ns *corev1.Namespace, ct1 client.Client) *cl MachineInfrastructure: &clusterv1.ClusterClassTemplate{ Ref: &clusterv1.ClusterClassTemplateReference{ Kind: "TestResourceTemplate", - Name: infrastructureClusterTemplate1.Name, + Name: infrastructureMachineTemplate1.Name, APIVersion: testt1v1beta1.GroupVersion.String(), }, }, @@ -343,10 +348,10 @@ func createT1ClusterClass(g *WithT, ns *corev1.Namespace, ct1 client.Client) *cl }, Patches: []clusterv1.ClusterClassPatch{ { - // Add the omitable fields. + // Add the omittable fields. // Note: - // - At t1, omitable fields are then dropped by DefaulterRemoveUnknownOrOmitableFields options in the defaulter webhook - // - At t2, omitable fields are then dropped by the conversion webhook + // - At t1, omittable fields are then dropped by DefaulterRemoveUnknownOrOmitableFields options in the defaulter webhook + // - At t2, omittable fields are then dropped by the conversion webhook Name: "patch-t1", Definitions: []clusterv1.PatchDefinition{ { @@ -364,7 +369,7 @@ func createT1ClusterClass(g *WithT, ns *corev1.Namespace, ct1 client.Client) *cl JSONPatches: []clusterv1.JSONPatch{ { Op: "add", - Path: "/spec/template/spec/omitable", + Path: "/spec/template/spec/omittable", Value: &apiextensionsv1.JSON{Raw: []byte(`""`)}, }, }, @@ -391,7 +396,7 @@ func waitForObjects(g *WithT, scheme *runtime.Scheme, objs ...client.Object) { } } -// createT2ClusterClass creates a CC with v1beta2 references and a patch adding the omitable field. +// createT2ClusterClass creates a CC with v1beta2 references and a patch adding the omittable field. func createT2ClusterClass(g *WithT, ns *corev1.Namespace, ct2 client.Client) *clusterv1.ClusterClass { infrastructureClusterTemplate1 := &testt2v1beta2.TestResourceTemplate{ ObjectMeta: metav1.ObjectMeta{ @@ -430,7 +435,7 @@ func createT2ClusterClass(g *WithT, ns *corev1.Namespace, ct2 client.Client) *cl g.Expect(ct2.Create(ctx, bootstrapTemplate)).To(Succeed()) // Waits for the env client to get in cache object we created with client ct2. - waitForObjects(g, ct2.Scheme(), infrastructureClusterTemplate1, infrastructureClusterTemplate1, controlPlaneTemplate, infrastructureMachineTemplate1, bootstrapTemplate) + waitForObjects(g, ct2.Scheme(), infrastructureClusterTemplate1, controlPlaneTemplate, infrastructureMachineTemplate1, bootstrapTemplate) machineDeploymentClass1 := clusterv1.MachineDeploymentClass{ Class: "md-class1", @@ -453,10 +458,6 @@ func createT2ClusterClass(g *WithT, ns *corev1.Namespace, ct2 client.Client) *cl } clusterClass := &clusterv1.ClusterClass{ - TypeMeta: metav1.TypeMeta{ - Kind: "ClusterClass", - APIVersion: clusterv1.GroupVersion.String(), - }, ObjectMeta: metav1.ObjectMeta{ Name: "cluster-class-t2", Namespace: ns.Name, @@ -482,7 +483,7 @@ func createT2ClusterClass(g *WithT, ns *corev1.Namespace, ct2 client.Client) *cl MachineInfrastructure: &clusterv1.ClusterClassTemplate{ Ref: &clusterv1.ClusterClassTemplateReference{ Kind: "TestResourceTemplate", - Name: infrastructureClusterTemplate1.Name, + Name: infrastructureMachineTemplate1.Name, APIVersion: testt2v1beta2.GroupVersion.String(), }, }, @@ -494,8 +495,8 @@ func createT2ClusterClass(g *WithT, ns *corev1.Namespace, ct2 client.Client) *cl }, Patches: []clusterv1.ClusterClassPatch{ { - // Add the omitable fields. - // Note: the omitable fields are not dropped because the DefaulterRemoveUnknownOrOmitableFields options is not used anymore. + // Add the omittable fields. + // Note: the omittable fields are not dropped because the DefaulterRemoveUnknownOrOmitableFields options is not used anymore. Name: "patch-t2", Definitions: []clusterv1.PatchDefinition{ { @@ -513,7 +514,7 @@ func createT2ClusterClass(g *WithT, ns *corev1.Namespace, ct2 client.Client) *cl JSONPatches: []clusterv1.JSONPatch{ { Op: "add", - Path: "/spec/template/spec/omitable", + Path: "/spec/template/spec/omittable", Value: &apiextensionsv1.JSON{Raw: []byte(`""`)}, }, }, @@ -538,14 +539,14 @@ func getClusterTopologyReferences(cluster *clusterv1.Cluster, version string, ad if err := env.Get(ctx, client.ObjectKeyFromObject(cluster), actualCluster); err != nil { return nil, errors.Wrapf(err, "failed to get cluster %s", cluster.Name) } - if c := conditions.Get(actualCluster, clusterv1.ClusterTopologyReconciledCondition); c != nil && c.Status != metav1.ConditionTrue { + if c := conditions.Get(actualCluster, clusterv1.ClusterTopologyReconciledCondition); c == nil || c.Status != metav1.ConditionTrue { return nil, errors.Errorf("cluster %s topology is not reconciled", cluster.Name) } addToAllObj(cluster.Name, "cluster", actualCluster) refs[clusterv1.ContractVersionedObjectReference{ - APIGroup: actualCluster.GroupVersionKind().Group, - Kind: actualCluster.GroupVersionKind().Kind, + APIGroup: clusterv1.GroupVersion.Group, + Kind: "Cluster", Name: actualCluster.GetName(), }] = actualCluster.GetGeneration() @@ -589,11 +590,11 @@ func getClusterTopologyReferences(cluster *clusterv1.Cluster, version string, ad cpInfraRef, err := contract.ControlPlane().MachineTemplate().InfrastructureRef().Get(refObj) if err != nil { - return nil, errors.Wrap(err, "cluster controplPlane.spec.machineTemplate.infrastructureRef is not yet set") + return nil, errors.Wrap(err, "cluster controlPlane.spec.machineTemplate.infrastructureRef is not yet set") } refObj, err = getReferencedObject(ctx, env.GetClient(), cpInfraRef, version, actualCluster.Namespace) if err != nil { - return nil, errors.Wrap(err, "failed to get referenced controplPlane.spec.machineTemplate.infrastructureRef") + return nil, errors.Wrap(err, "failed to get referenced controlPlane.spec.machineTemplate.infrastructureRef") } addToAllObj(cluster.Name, "controlPlane.spec.machineTemplate.infrastructureRef", refObj) refs[clusterv1.ContractVersionedObjectReference{ @@ -601,6 +602,11 @@ func getClusterTopologyReferences(cluster *clusterv1.Cluster, version string, ad Kind: refObj.GetKind(), Name: refObj.GetName(), }] = refObj.GetGeneration() + if additionalChecks != nil { + if err := additionalChecks(refObj); err != nil { + return nil, errors.Wrap(err, "failed additional checks on controlPlane.spec.machineTemplate.infrastructureRef") + } + } machineDeployments := &clusterv1.MachineDeploymentList{} if err = env.List(ctx, machineDeployments, client.InNamespace(cluster.Namespace), client.MatchingLabels{ @@ -668,24 +674,30 @@ func getReferencedObject(ctx context.Context, c client.Client, ref *clusterv1.Co return refObj, nil } -func checkOmitableField(allowOmitable bool) func(obj *unstructured.Unstructured) error { +func checkOmittableField(mustBeSet bool) func(obj *unstructured.Unstructured) error { return func(obj *unstructured.Unstructured) error { switch obj.GetKind() { case "TestResource": - _, exists, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "omitable") + _, exists, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "omittable") if err != nil { return err } - if exists && !allowOmitable { - return errors.Errorf("expected to not contain omitable field") + if exists && !mustBeSet { + return errors.Errorf("expected to not contain omittable field") + } + if !exists && mustBeSet { + return errors.Errorf("expected to contain omittable field") } case "TestResourceTemplate": - _, exists, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "template", "spec", "omitable") + _, exists, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "template", "spec", "omittable") if err != nil { return err } - if exists && !allowOmitable { - return errors.Errorf("expected to not contain omitable field") + if exists && !mustBeSet { + return errors.Errorf("expected to not contain omittable field") + } + if !exists && mustBeSet { + return errors.Errorf("expected to contain omittable field") } } return nil @@ -699,8 +711,8 @@ func assertRollout(g *WithT, cluster *clusterv1.Cluster, refsBefore, refsAfter m g.Expect(err).To(Succeed()) clusterRef := clusterv1.ContractVersionedObjectReference{ - APIGroup: actualCluster.GroupVersionKind().Group, - Kind: actualCluster.GroupVersionKind().Kind, + APIGroup: clusterv1.GroupVersion.Group, + Kind: "Cluster", Name: actualCluster.GetName(), } g.Expect(refsAfter[clusterRef]).To(Equal(refsBefore[clusterRef]+1), "cluster unexpected change") // Cluster is expected to have an additional generation due to the rebase @@ -745,11 +757,11 @@ func assertRollout(g *WithT, cluster *clusterv1.Cluster, refsBefore, refsAfter m for _, md := range machineDeployments.Items { mdRef := clusterv1.ContractVersionedObjectReference{ - APIGroup: md.GroupVersionKind().GroupVersion().String(), - Kind: md.GroupVersionKind().Kind, + APIGroup: clusterv1.GroupVersion.Group, + Kind: "MachineDeployment", Name: md.GetName(), } - g.Expect(refsAfter[mdRef]).To(Equal(refsBefore[mdRef]), "machineDeployment "+md.Name+" unexpected change") + g.Expect(refsAfter[mdRef]).To(Equal(refsBefore[mdRef]+1), "machineDeployment "+md.Name+" unexpected change") // MachineDeployment is expected to have an additional generation due to rollout refObj, err = getReferencedObject(ctx, env.GetClient(), &md.Spec.Template.Spec.InfrastructureRef, "v1beta2", cluster.Namespace) g.Expect(err).To(Succeed()) @@ -782,8 +794,8 @@ func assertNoRollout(g *WithT, cluster *clusterv1.Cluster, refsBefore, refsAfter g.Expect(err).To(Succeed()) clusterRef := clusterv1.ContractVersionedObjectReference{ - APIGroup: actualCluster.GroupVersionKind().Group, - Kind: actualCluster.GroupVersionKind().Kind, + APIGroup: clusterv1.GroupVersion.Group, + Kind: "Cluster", Name: actualCluster.GetName(), } g.Expect(refsAfter[clusterRef]).To(Equal(refsBefore[clusterRef]+1), "cluster unexpected change") // Cluster is expected to have an additional generation due to the rebase @@ -827,8 +839,8 @@ func assertNoRollout(g *WithT, cluster *clusterv1.Cluster, refsBefore, refsAfter for _, md := range machineDeployments.Items { mdRef := clusterv1.ContractVersionedObjectReference{ - APIGroup: md.GroupVersionKind().GroupVersion().String(), - Kind: md.GroupVersionKind().Kind, + APIGroup: clusterv1.GroupVersion.Group, + Kind: "MachineDeployment", Name: md.GetName(), } g.Expect(refsAfter[mdRef]).To(Equal(refsBefore[mdRef]), "machineDeployment "+md.Name+" unexpected change") @@ -853,12 +865,15 @@ func assertNoRollout(g *WithT, cluster *clusterv1.Cluster, refsBefore, refsAfter } } -// assertClusterTopologyIsStable checks a cluster topology is stable ensuring all the objects included cluster, md and referenced template or referencedObjects do not changed/increased generation. -func assertClusterTopologyIsStable(g *WithT, refs map[clusterv1.ContractVersionedObjectReference]int64, namespace, version string) { +// assertClusterTopologyBecomesStable checks a cluster topology becomes stable ensuring all the objects included cluster, md and referenced template or referencedObjects do not changed/increased generation. +func assertClusterTopologyBecomesStable(g *WithT, refs map[clusterv1.ContractVersionedObjectReference]int64, namespace, version string) { g.Eventually(func(g Gomega) { for r, generation := range refs { obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(r.GroupKind().WithVersion(version)) + if r.Kind == "Cluster" || r.Kind == "MachineDeployment" { + obj.SetGroupVersionKind(clusterv1.GroupVersion.WithKind(r.Kind)) + } err := env.GetClient().Get(ctx, client.ObjectKey{Name: r.Name, Namespace: namespace}, obj) g.Expect(err).ToNot(HaveOccurred()) if generation != obj.GetGeneration() { @@ -872,6 +887,9 @@ func assertClusterTopologyIsStable(g *WithT, refs map[clusterv1.ContractVersione for r, generation := range refs { obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(r.GroupKind().WithVersion(version)) + if r.Kind == "Cluster" || r.Kind == "MachineDeployment" { + obj.SetGroupVersionKind(clusterv1.GroupVersion.WithKind(r.Kind)) + } err := env.GetClient().Get(ctx, client.ObjectKey{Name: r.Name, Namespace: namespace}, obj) g.Expect(err).ToNot(HaveOccurred()) g.Expect(obj.GetGeneration()).To(Equal(generation), "generation is not remaining stable for %s/%s, %s", r.Kind, r.GroupKind().WithVersion(version).GroupVersion().String(), r.Name) @@ -1023,7 +1041,7 @@ func setupT2CRDAndWebHooks(g *WithT, ns *corev1.Namespace, t1CheckObj *testt1v1b g.Expect(ct2.Create(ctx, t2TemplateObj, client.DryRunAll)).To(Succeed()) g.Expect(t2TemplateObj.Annotations).ToNot(HaveKey("default-t1")) g.Expect(t2TemplateObj.Annotations).To(HaveKey("default-t2")) - g.Expect(t2TemplateObj.Annotations).ToNot(HaveKey("conversion")) + g.Expect(t2TemplateObj.Annotations).ToNot(HaveKey("conversionTo")) }, 5*time.Second).Should(Succeed()) g.Eventually(ctx, func(g Gomega) { @@ -1036,7 +1054,7 @@ func setupT2CRDAndWebHooks(g *WithT, ns *corev1.Namespace, t1CheckObj *testt1v1b g.Expect(ct2.Create(ctx, t2Obj, client.DryRunAll)).To(Succeed()) g.Expect(t2Obj.Annotations).ToNot(HaveKey("default-t1")) g.Expect(t2Obj.Annotations).To(HaveKey("default-t2")) - g.Expect(t2Obj.Annotations).ToNot(HaveKey("conversion")) + g.Expect(t2Obj.Annotations).ToNot(HaveKey("conversionTo")) }, 5*time.Second).Should(Succeed()) // Test the conversion works diff --git a/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml index ecd710e8c767..7d6cefd22152 100644 --- a/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml +++ b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml @@ -40,13 +40,14 @@ spec: description: TestResourceSpec defines the resource spec. properties: machineTemplate: - description: TestResourceMachineTemplateSpec define the spec for machineTemplate - in a resource. + description: |- + TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. + Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. properties: infrastructureRef: - description: TestContractVersionedObjectReference is a reference - to a resource for which the version is inferred from contract - labels. + description: |- + TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). properties: apiGroup: type: string @@ -99,7 +100,7 @@ spec: type: object type: object type: object - omitable: + omittable: type: string replicas: format: int32 @@ -109,28 +110,6 @@ spec: required: - machineTemplate type: object - status: - description: TestResourceStatus defines the status of a TestResource. - properties: - availableReplicas: - format: int32 - type: integer - readyReplicas: - format: int32 - type: integer - replicas: - format: int32 - type: integer - upToDateReplicas: - format: int32 - type: integer - version: - maxLength: 256 - minLength: 1 - type: string - type: object type: object served: true storage: true - subresources: - status: {} diff --git a/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml index 81a48e43b5be..263a2ada684d 100644 --- a/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml +++ b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml @@ -44,16 +44,17 @@ spec: of a TestResourceTemplate. properties: spec: - description: spec is the desired state of KubeadmControlPlaneTemplateResource. + description: TestResourceSpec defines the resource spec. properties: machineTemplate: - description: TestResourceMachineTemplateSpec define the spec - for machineTemplate in a resource. + description: |- + TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. + Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. properties: infrastructureRef: - description: TestContractVersionedObjectReference is a - reference to a resource for which the version is inferred - from contract labels. + description: |- + TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). properties: apiGroup: type: string @@ -106,7 +107,7 @@ spec: type: object type: object type: object - omitable: + omittable: type: string replicas: format: int32 diff --git a/internal/topology/upgrade/test/t1/v1beta1/types.go b/internal/topology/upgrade/test/t1/v1beta1/types.go index 879a8d168622..e7c78d2188df 100644 --- a/internal/topology/upgrade/test/t1/v1beta1/types.go +++ b/internal/topology/upgrade/test/t1/v1beta1/types.go @@ -44,7 +44,6 @@ type TestResourceTemplateSpec struct { // TestResourceTemplateResource defines the template resource of a TestResourceTemplate. type TestResourceTemplateResource struct { - // spec is the desired state of KubeadmControlPlaneTemplateResource. // +required Spec TestResourceSpec `json:"spec"` } @@ -60,29 +59,29 @@ type TestResourceTemplateList struct { // +kubebuilder:object:root=true // +kubebuilder:resource:path=testresources,scope=Namespaced // +kubebuilder:storageversion -// +kubebuilder:subresource:status // TestResource defines a test resource. type TestResource struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec TestResourceSpec `json:"spec,omitempty"` - Status TestResourceStatus `json:"status,omitempty"` + Spec TestResourceSpec `json:"spec,omitempty"` } // TestResourceSpec defines the resource spec. -type TestResourceSpec struct { // NOTE: we are using testDefaulterT1 field to test if the defaulter works. - // mandatory field from the Cluster API contract - replicas support +type TestResourceSpec struct { + // mandatory field from the Cluster API control plane contract - replicas support // +optional Replicas *int32 `json:"replicas,omitempty"` - // Mandatory field from the Cluster API contract - version support + // Mandatory field from the Cluster API control plane contract - version support + // Note: field is not required because this CRD is also used for non - control plane cases. // +optional Version string `json:"version,omitempty"` - // Mandatory field from the Cluster API contract - machine support + // Mandatory field from the Cluster API control plane contract - machine support + // Note: field is not required because this CRD is also used for non - control plane cases. // +required MachineTemplate TestResourceMachineTemplateSpec `json:"machineTemplate"` @@ -90,10 +89,11 @@ type TestResourceSpec struct { // NOTE: we are using testDefaulterT1 field to te // General purpose fields to be used in different test scenario. // +optional - Omitable string `json:"omitable,omitempty"` + Omittable string `json:"omittable,omitempty"` } // TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. +// Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. type TestResourceMachineTemplateSpec struct { // +optional ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty"` @@ -103,6 +103,7 @@ type TestResourceMachineTemplateSpec struct { } // TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. +// Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). type TestContractVersionedObjectReference struct { // +optional Kind string `json:"kind"` @@ -133,8 +134,6 @@ type TestResourceStatus struct { // Mandatory field from the Cluster API contract - version support // +optional - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=256 Version *string `json:"version,omitempty"` } diff --git a/internal/topology/upgrade/test/t1/v1beta1/zz_generated.deepcopy.go b/internal/topology/upgrade/test/t1/v1beta1/zz_generated.deepcopy.go index c1923a658b70..3fb0faf7e0b5 100644 --- a/internal/topology/upgrade/test/t1/v1beta1/zz_generated.deepcopy.go +++ b/internal/topology/upgrade/test/t1/v1beta1/zz_generated.deepcopy.go @@ -45,7 +45,6 @@ func (in *TestResource) DeepCopyInto(out *TestResource) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TestResource. diff --git a/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml index b27135b8bfa3..d13631ef28d9 100644 --- a/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml +++ b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml @@ -40,13 +40,14 @@ spec: description: TestResourceSpec defines the resource spec. properties: machineTemplate: - description: TestResourceMachineTemplateSpec define the spec for machineTemplate - in a resource. + description: |- + TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. + Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. properties: infrastructureRef: - description: TestContractVersionedObjectReference is a reference - to a resource for which the version is inferred from contract - labels. + description: |- + TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). properties: apiGroup: type: string @@ -99,7 +100,7 @@ spec: type: object type: object type: object - omitable: + omittable: type: string replicas: format: int32 @@ -112,8 +113,6 @@ spec: type: object served: true storage: false - subresources: - status: {} - name: v1beta2 schema: openAPIV3Schema: @@ -140,13 +139,14 @@ spec: description: TestResourceSpec defines the resource spec. properties: machineTemplate: - description: TestResourceMachineTemplateSpec define the spec for machineTemplate - in a resource. + description: |- + TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. + Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. properties: infrastructureRef: - description: TestContractVersionedObjectReference is a reference - to a resource for which the version is inferred from contract - labels. + description: |- + TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). properties: apiGroup: type: string @@ -199,7 +199,7 @@ spec: type: object type: object type: object - omitable: + omittable: type: string replicas: format: int32 @@ -212,5 +212,3 @@ spec: type: object served: true storage: true - subresources: - status: {} diff --git a/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml index 0dedc69d266b..59982bed121a 100644 --- a/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml +++ b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml @@ -44,16 +44,17 @@ spec: of a TestResourceTemplate. properties: spec: - description: spec is the desired state of KubeadmControlPlaneTemplateResource. + description: TestResourceSpec defines the resource spec. properties: machineTemplate: - description: TestResourceMachineTemplateSpec define the spec - for machineTemplate in a resource. + description: |- + TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. + Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. properties: infrastructureRef: - description: TestContractVersionedObjectReference is a - reference to a resource for which the version is inferred - from contract labels. + description: |- + TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). properties: apiGroup: type: string @@ -106,7 +107,7 @@ spec: type: object type: object type: object - omitable: + omittable: type: string replicas: format: int32 @@ -155,16 +156,17 @@ spec: of a TestResourceTemplate. properties: spec: - description: spec is the desired state of KubeadmControlPlaneTemplateResource. + description: TestResourceSpec defines the resource spec. properties: machineTemplate: - description: TestResourceMachineTemplateSpec define the spec - for machineTemplate in a resource. + description: |- + TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. + Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. properties: infrastructureRef: - description: TestContractVersionedObjectReference is a - reference to a resource for which the version is inferred - from contract labels. + description: |- + TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). properties: apiGroup: type: string @@ -217,7 +219,7 @@ spec: type: object type: object type: object - omitable: + omittable: type: string replicas: format: int32 diff --git a/internal/topology/upgrade/test/t2/v1beta1/doc.go b/internal/topology/upgrade/test/t2/v1beta1/doc.go index 734a09dd5707..cb634c849c76 100644 --- a/internal/topology/upgrade/test/t2/v1beta1/doc.go +++ b/internal/topology/upgrade/test/t2/v1beta1/doc.go @@ -15,7 +15,6 @@ limitations under the License. */ // Package v1beta1 contains test types. -// +k8s:openapi-gen=true // +k8s:conversion-gen=sigs.k8s.io/cluster-api/internal/topology/upgrade/test/t2/v1beta2 // +kubebuilder:object:generate=true // +groupName=test.cluster.x-k8s.io diff --git a/internal/topology/upgrade/test/t2/v1beta1/types.go b/internal/topology/upgrade/test/t2/v1beta1/types.go index 46a961163b3f..f3be6e0fb9b0 100644 --- a/internal/topology/upgrade/test/t2/v1beta1/types.go +++ b/internal/topology/upgrade/test/t2/v1beta1/types.go @@ -40,7 +40,6 @@ type TestResourceTemplateSpec struct { // TestResourceTemplateResource defines the template resource of a TestResourceTemplate. type TestResourceTemplateResource struct { - // spec is the desired state of KubeadmControlPlaneTemplateResource. // +required Spec TestResourceSpec `json:"spec"` } @@ -55,7 +54,6 @@ type TestResourceTemplateList struct { // +kubebuilder:object:root=true // +kubebuilder:resource:path=testresources,scope=Namespaced -// +kubebuilder:subresource:status // TestResource defines a test resource. type TestResource struct { @@ -66,17 +64,19 @@ type TestResource struct { // TestResourceSpec defines the resource spec. type TestResourceSpec struct { - // mandatory field from the Cluster API contract - replicas support + // mandatory field from the Cluster API control plane contract - replicas support // +optional Replicas *int32 `json:"replicas,omitempty"` - // Mandatory field from the Cluster API contract - version support + // Mandatory field from the Cluster API control plane contract - version support + // Note: field is not required because this CRD is also used for non - control plane cases. // +optional Version string `json:"version,omitempty"` - // Mandatory field from the Cluster API contract - machine support + // Mandatory field from the Cluster API control plane contract - machine support + // Note: field is not required because this CRD is also used for non - control plane cases. // +required MachineTemplate TestResourceMachineTemplateSpec `json:"machineTemplate"` @@ -84,10 +84,11 @@ type TestResourceSpec struct { // General purpose fields to be used in different test scenario. // +optional - Omitable string `json:"omitable,omitempty"` + Omittable string `json:"omittable,omitempty"` } // TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. +// Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. type TestResourceMachineTemplateSpec struct { // +optional ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty"` @@ -97,6 +98,7 @@ type TestResourceMachineTemplateSpec struct { } // TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. +// Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). type TestContractVersionedObjectReference struct { // +optional Kind string `json:"kind"` @@ -127,8 +129,6 @@ type TestResourceStatus struct { // Mandatory field from the Cluster API contract - version support // +optional - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=256 Version *string `json:"version,omitempty"` } diff --git a/internal/topology/upgrade/test/t2/v1beta1/zz_generated.conversion.go b/internal/topology/upgrade/test/t2/v1beta1/zz_generated.conversion.go index 449f2a7abaa1..a1b371e29dfd 100644 --- a/internal/topology/upgrade/test/t2/v1beta1/zz_generated.conversion.go +++ b/internal/topology/upgrade/test/t2/v1beta1/zz_generated.conversion.go @@ -243,7 +243,7 @@ func autoConvert_v1beta1_TestResourceSpec_To_v1beta2_TestResourceSpec(in *TestRe if err := Convert_v1beta1_TestResourceMachineTemplateSpec_To_v1beta2_TestResourceMachineTemplateSpec(&in.MachineTemplate, &out.MachineTemplate, s); err != nil { return err } - out.Omitable = in.Omitable + out.Omittable = in.Omittable return nil } @@ -258,7 +258,7 @@ func autoConvert_v1beta2_TestResourceSpec_To_v1beta1_TestResourceSpec(in *v1beta if err := Convert_v1beta2_TestResourceMachineTemplateSpec_To_v1beta1_TestResourceMachineTemplateSpec(&in.MachineTemplate, &out.MachineTemplate, s); err != nil { return err } - out.Omitable = in.Omitable + out.Omittable = in.Omittable return nil } diff --git a/internal/topology/upgrade/test/t2/v1beta2/types.go b/internal/topology/upgrade/test/t2/v1beta2/types.go index be71a87508c9..54d3ede2ae50 100644 --- a/internal/topology/upgrade/test/t2/v1beta2/types.go +++ b/internal/topology/upgrade/test/t2/v1beta2/types.go @@ -44,7 +44,6 @@ type TestResourceTemplateSpec struct { // TestResourceTemplateResource defines the template resource of a TestResourceTemplate. type TestResourceTemplateResource struct { - // spec is the desired state of KubeadmControlPlaneTemplateResource. // +required Spec TestResourceSpec `json:"spec"` } @@ -60,7 +59,6 @@ type TestResourceTemplateList struct { // +kubebuilder:object:root=true // +kubebuilder:resource:path=testresources,scope=Namespaced // +kubebuilder:storageversion -// +kubebuilder:subresource:status // TestResource defines a test resource. type TestResource struct { @@ -71,17 +69,19 @@ type TestResource struct { // TestResourceSpec defines the resource spec. type TestResourceSpec struct { - // mandatory field from the Cluster API contract - replicas support + // mandatory field from the Cluster API control plane contract - replicas support // +optional Replicas *int32 `json:"replicas,omitempty"` - // Mandatory field from the Cluster API contract - version support + // Mandatory field from the Cluster API control plane contract - version support + // Note: field is not required because this CRD is also used for non - control plane cases. // +optional Version string `json:"version,omitempty"` - // Mandatory field from the Cluster API contract - machine support + // Mandatory field from the Cluster API control plane contract - machine support + // Note: field is not required because this CRD is also used for non - control plane cases. // +required MachineTemplate TestResourceMachineTemplateSpec `json:"machineTemplate"` @@ -89,10 +89,11 @@ type TestResourceSpec struct { // General purpose fields to be used in different test scenario. // +optional - Omitable string `json:"omitable,omitempty"` + Omittable string `json:"omittable,omitempty"` } // TestResourceMachineTemplateSpec define the spec for machineTemplate in a resource. +// Note: infrastructureRef field is not required because this CRD is also used for non - control plane cases. type TestResourceMachineTemplateSpec struct { // +optional ObjectMeta clusterv1.ObjectMeta `json:"metadata,omitempty"` @@ -102,6 +103,7 @@ type TestResourceMachineTemplateSpec struct { } // TestContractVersionedObjectReference is a reference to a resource for which the version is inferred from contract labels. +// Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). type TestContractVersionedObjectReference struct { // +optional Kind string `json:"kind"` @@ -132,8 +134,6 @@ type TestResourceStatus struct { // Mandatory field from the Cluster API contract - version support // +optional - // +kubebuilder:validation:MinLength=1 - // +kubebuilder:validation:MaxLength=256 Version *string `json:"version,omitempty"` } From d3adcffa1b31b6bde26ee8eff31d664b927ebfed Mon Sep 17 00:00:00 2001 From: fabriziopandini Date: Thu, 26 Jun 2025 17:14:23 +0200 Subject: [PATCH 3/4] Address feedback --- .../upgrade/clusterctl_upgrade_test.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/internal/topology/upgrade/clusterctl_upgrade_test.go b/internal/topology/upgrade/clusterctl_upgrade_test.go index e9e42f361538..0d483eb3ef91 100644 --- a/internal/topology/upgrade/clusterctl_upgrade_test.go +++ b/internal/topology/upgrade/clusterctl_upgrade_test.go @@ -539,7 +539,7 @@ func getClusterTopologyReferences(cluster *clusterv1.Cluster, version string, ad if err := env.Get(ctx, client.ObjectKeyFromObject(cluster), actualCluster); err != nil { return nil, errors.Wrapf(err, "failed to get cluster %s", cluster.Name) } - if c := conditions.Get(actualCluster, clusterv1.ClusterTopologyReconciledCondition); c == nil || c.Status != metav1.ConditionTrue { + if c := conditions.Get(actualCluster, clusterv1.ClusterTopologyReconciledCondition); c == nil || c.Status != metav1.ConditionTrue || c.ObservedGeneration != actualCluster.Generation { return nil, errors.Errorf("cluster %s topology is not reconciled", cluster.Name) } @@ -867,22 +867,6 @@ func assertNoRollout(g *WithT, cluster *clusterv1.Cluster, refsBefore, refsAfter // assertClusterTopologyBecomesStable checks a cluster topology becomes stable ensuring all the objects included cluster, md and referenced template or referencedObjects do not changed/increased generation. func assertClusterTopologyBecomesStable(g *WithT, refs map[clusterv1.ContractVersionedObjectReference]int64, namespace, version string) { - g.Eventually(func(g Gomega) { - for r, generation := range refs { - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(r.GroupKind().WithVersion(version)) - if r.Kind == "Cluster" || r.Kind == "MachineDeployment" { - obj.SetGroupVersionKind(clusterv1.GroupVersion.WithKind(r.Kind)) - } - err := env.GetClient().Get(ctx, client.ObjectKey{Name: r.Name, Namespace: namespace}, obj) - g.Expect(err).ToNot(HaveOccurred()) - if generation != obj.GetGeneration() { - refs[r] = obj.GetGeneration() - } - g.Expect(obj.GetGeneration()).To(Equal(generation), "generation is not getting stable for %s/%s, %s", r.Kind, r.GroupKind().WithVersion(version).GroupVersion().String(), r.Name) - } - }, 5*time.Second, 1*time.Second).Should(Succeed(), "Resource versions never became stable") - g.Consistently(func(g Gomega) { for r, generation := range refs { obj := &unstructured.Unstructured{} From 4af08913f90b4e787fd7cd1f2089227fb3c3d395 Mon Sep 17 00:00:00 2001 From: fabriziopandini Date: Fri, 27 Jun 2025 09:48:22 +0200 Subject: [PATCH 4/4] More feedback --- .../upgrade/clusterctl_upgrade_test.go | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/internal/topology/upgrade/clusterctl_upgrade_test.go b/internal/topology/upgrade/clusterctl_upgrade_test.go index 0d483eb3ef91..14ca8103b256 100644 --- a/internal/topology/upgrade/clusterctl_upgrade_test.go +++ b/internal/topology/upgrade/clusterctl_upgrade_test.go @@ -81,18 +81,18 @@ func addToAllObj(cluster, prefix string, obj client.Object) { allObjs[cluster][ref][obj.GetGeneration()] = obj } -// TestDropDefaulterRemoveUnknownOrOmittableFields validates effects of removing the DropDefaulterRemoveUnknownOrOmitableFields option +// TestDropDefaulterRemoveUnknownOrOmittableFields validates effects of removing the DropDefaulterRemoveUnknownOrOmitableFields option. // from provider's defaulting webhooks. // TL;DR if setting omittable fields in a patch, this should cause a rollout only when rebasing to a ClusterClass which uses provider objects with a new apiVersion. // Details: -// Case 1: cluster1 created at t1 with v1beta references, rebased at t2 to v1beta2 references -// * t1: create cluster1 with clusterClasst1 => should be stable -// * t1 => t2: upgrade CRDs / webhooks -// * t2: force reconcile cluster1 => should be stable -// * t2: rebase cluster1 to clusterClasst2 => should roll out and then be stable -// Case 2: cluster2 created at t2 with v1beta references, rebased at t2 to v1beta2 references -// * t2: create cluster2 with clusterClasst1 => should be stable -// * t2: rebase cluster2 to clusterClasst2 => should roll out and then be stable +// Case 1: cluster1 created at t1 with v1beta references, rebased at t2 to v1beta2 references. +// * t1: create cluster1 with clusterClasst1 => should be stable. +// * t1 => t2: upgrade CRDs / webhooks. +// * t2: force reconcile cluster1 => should be stable. +// * t2: rebase cluster1 to clusterClasst2 => should roll out and then be stable. +// Case 2: cluster2 created at t2 with v1beta references, rebased at t2 to v1beta2 references. +// * t2: create cluster2 with clusterClasst1 => should be stable. +// * t2: rebase cluster2 to clusterClasst2 => should roll out and then be stable. func TestDropDefaulterRemoveUnknownOrOmittableFields(t *testing.T) { utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true) g := NewWithT(t) @@ -621,8 +621,8 @@ func getClusterTopologyReferences(cluster *clusterv1.Cluster, version string, ad for _, md := range machineDeployments.Items { refs[clusterv1.ContractVersionedObjectReference{ - APIGroup: md.GroupVersionKind().Group, - Kind: md.GroupVersionKind().Kind, + APIGroup: clusterv1.GroupVersion.Group, + Kind: "MachineDeployment", Name: md.GetName(), }] = md.GetGeneration() addToAllObj(cluster.Name, "machineDeployment "+md.Name, &md) @@ -683,10 +683,10 @@ func checkOmittableField(mustBeSet bool) func(obj *unstructured.Unstructured) er return err } if exists && !mustBeSet { - return errors.Errorf("expected to not contain omittable field") + return errors.New("expected to not contain omittable field") } if !exists && mustBeSet { - return errors.Errorf("expected to contain omittable field") + return errors.New("expected to contain omittable field") } case "TestResourceTemplate": _, exists, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "template", "spec", "omittable") @@ -694,10 +694,10 @@ func checkOmittableField(mustBeSet bool) func(obj *unstructured.Unstructured) er return err } if exists && !mustBeSet { - return errors.Errorf("expected to not contain omittable field") + return errors.New("expected to not contain omittable field") } if !exists && mustBeSet { - return errors.Errorf("expected to contain omittable field") + return errors.New("expected to contain omittable field") } } return nil @@ -871,9 +871,6 @@ func assertClusterTopologyBecomesStable(g *WithT, refs map[clusterv1.ContractVer for r, generation := range refs { obj := &unstructured.Unstructured{} obj.SetGroupVersionKind(r.GroupKind().WithVersion(version)) - if r.Kind == "Cluster" || r.Kind == "MachineDeployment" { - obj.SetGroupVersionKind(clusterv1.GroupVersion.WithKind(r.Kind)) - } err := env.GetClient().Get(ctx, client.ObjectKey{Name: r.Name, Namespace: namespace}, obj) g.Expect(err).ToNot(HaveOccurred()) g.Expect(obj.GetGeneration()).To(Equal(generation), "generation is not remaining stable for %s/%s, %s", r.Kind, r.GroupKind().WithVersion(version).GroupVersion().String(), r.Name)