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..e2f36eb7d00e 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 \ @@ -398,13 +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=./internal/topology/upgrade/test/... \ paths=./util/test/builder/... \ - paths=./util/deprecated/v1beta1/test/builder/... \ - paths=./controllers/crdmigrator/test/... + 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 @@ -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..1000ac050eb7 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), "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), "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), "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), "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..14ca8103b256 --- /dev/null +++ b/internal/topology/upgrade/clusterctl_upgrade_test.go @@ -0,0 +1,1173 @@ +/* +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 ( + 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", prefix, klog.KObj(obj)) + 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 +} + +// 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. +func TestDropDefaulterRemoveUnknownOrOmittableFields(t *testing.T) { + utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.ClusterTopology, true) + g := NewWithT(t) + + 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. + + // 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 omittable 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 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", checkOmittableField(omittableFieldsMustNotBeSet)) // The defaulting webhook drops the omittable field when using v1beta1. + if err != nil { + return err + } + return nil + }, 5*time.Second).Should(Succeed()) + 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. + // 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") + assertClusterTopologyBecomesStable(g, cluster1Refs, ns.Name, "v1beta2") + + // rebase cluster1 to a CC created at t2, using v1beta2. + // 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 + + err = env.Patch(ctx, cluster1New, client.MergeFrom(cluster1)) + g.Expect(err).ToNot(HaveOccurred()) + + var cluster1RefsNew map[clusterv1.ContractVersionedObjectReference]int64 + 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 omittable field should trigger rollout. + assertClusterTopologyBecomesStable(g, cluster1RefsNew, ns.Name, "v1beta2") + + // create cluster2 using clusterClasst1 + // 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, "cluster2", clusterClasst1) + + var cluster2Refs map[clusterv1.ContractVersionedObjectReference]int64 + g.Eventually(func() error { + cluster2Refs, err = getClusterTopologyReferences(cluster2, "v1beta2", checkOmittableField(omittableFieldsMustNotBeSet)) // The conversion webhook drops the omittable field when using v1beta1. + if err != nil { + return err + } + return nil + }, 5*time.Second).Should(Succeed()) + + t.Log("check cluster2 is stable") + assertClusterTopologyBecomesStable(g, cluster2Refs, ns.Name, "v1beta2") + + // rebase cluster2 to a CC created at t2, using v1beta2. + // 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 + + err = env.Patch(ctx, cluster2New, client.MergeFrom(cluster2)) + g.Expect(err).ToNot(HaveOccurred()) + + var cluster2RefsNew map[clusterv1.ContractVersionedObjectReference]int64 + 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 omittable field should trigger rollout. + assertClusterTopologyBecomesStable(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 omittable 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, 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{ + 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: infrastructureMachineTemplate1.Name, + APIVersion: testt1v1beta1.GroupVersion.String(), + }, + }, + }, + Workers: clusterv1.WorkersClass{ + MachineDeployments: []clusterv1.MachineDeploymentClass{ + machineDeploymentClass1, + }, + }, + Patches: []clusterv1.ClusterClassPatch{ + { + // Add the omittable fields. + // Note: + // - 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{ + { + 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/omittable", + 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 omittable 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, 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{ + 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: infrastructureMachineTemplate1.Name, + APIVersion: testt2v1beta2.GroupVersion.String(), + }, + }, + }, + Workers: clusterv1.WorkersClass{ + MachineDeployments: []clusterv1.MachineDeploymentClass{ + machineDeploymentClass1, + }, + }, + Patches: []clusterv1.ClusterClassPatch{ + { + // 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{ + { + 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/omittable", + 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 || c.ObservedGeneration != actualCluster.Generation { + return nil, errors.Errorf("cluster %s topology is not reconciled", cluster.Name) + } + + addToAllObj(cluster.Name, "cluster", actualCluster) + refs[clusterv1.ContractVersionedObjectReference{ + APIGroup: clusterv1.GroupVersion.Group, + Kind: "Cluster", + 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 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 controlPlane.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() + 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{ + 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: clusterv1.GroupVersion.Group, + Kind: "MachineDeployment", + 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 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", "omittable") + if err != nil { + return err + } + if exists && !mustBeSet { + return errors.New("expected to not contain omittable field") + } + if !exists && mustBeSet { + return errors.New("expected to contain omittable field") + } + case "TestResourceTemplate": + _, exists, err := unstructured.NestedFieldNoCopy(obj.Object, "spec", "template", "spec", "omittable") + if err != nil { + return err + } + if exists && !mustBeSet { + return errors.New("expected to not contain omittable field") + } + if !exists && mustBeSet { + return errors.New("expected to contain omittable 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: 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 + + 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: clusterv1.GroupVersion.Group, + Kind: "MachineDeployment", + Name: md.GetName(), + } + 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()) + 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: 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 + + 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: clusterv1.GroupVersion.Group, + Kind: "MachineDeployment", + 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") + } +} + +// 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.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("conversionTo")) + }, 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("conversionTo")) + }, 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..7d6cefd22152 --- /dev/null +++ b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresources.yaml @@ -0,0 +1,115 @@ +--- +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. + 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. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). + 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 + omittable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + type: object + served: true + storage: true 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..263a2ada684d --- /dev/null +++ b/internal/topology/upgrade/test/t1/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml @@ -0,0 +1,128 @@ +--- +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: TestResourceSpec defines the resource spec. + properties: + machineTemplate: + 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. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). + 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 + omittable: + 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..e7c78d2188df --- /dev/null +++ b/internal/topology/upgrade/test/t1/v1beta1/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 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 { + // +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 + +// 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 control plane contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // 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 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"` + + // General purpose fields to be used in different test scenario. + + // +optional + 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"` + + // +optional + InfrastructureRef TestContractVersionedObjectReference `json:"infrastructureRef"` +} + +// 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"` + + // +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 + 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..3fb0faf7e0b5 --- /dev/null +++ b/internal/topology/upgrade/test/t1/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/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..d13631ef28d9 --- /dev/null +++ b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresources.yaml @@ -0,0 +1,214 @@ +--- +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. + 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. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). + 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 + omittable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + type: object + served: true + storage: false + - 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. + 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. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). + 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 + omittable: + type: string + replicas: + format: int32 + type: integer + version: + type: string + required: + - machineTemplate + type: object + type: object + served: true + storage: true 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..59982bed121a --- /dev/null +++ b/internal/topology/upgrade/test/t2/crd/test.cluster.x-k8s.io_testresourcetemplates.yaml @@ -0,0 +1,240 @@ +--- +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: TestResourceSpec defines the resource spec. + properties: + machineTemplate: + 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. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). + 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 + omittable: + 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: TestResourceSpec defines the resource spec. + properties: + machineTemplate: + 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. + Note: fields are not required / do not have validation for sake of simplicity (not relevant for the test). + 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 + omittable: + 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..cb634c849c76 --- /dev/null +++ b/internal/topology/upgrade/test/t2/v1beta1/doc.go @@ -0,0 +1,21 @@ +/* +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: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..f3be6e0fb9b0 --- /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 { + // +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 + +// 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 control plane contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // 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 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"` + + // General purpose fields to be used in different test scenario. + + // +optional + 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"` + + // +optional + InfrastructureRef TestContractVersionedObjectReference `json:"infrastructureRef"` +} + +// 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"` + + // +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 + 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..a1b371e29dfd --- /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.Omittable = in.Omittable + 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.Omittable = in.Omittable + 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..54d3ede2ae50 --- /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 { + // +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 + +// 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 control plane contract - replicas support + + // +optional + Replicas *int32 `json:"replicas,omitempty"` + + // 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 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"` + + // General purpose fields to be used in different test scenario. + + // +optional + 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"` + + // +optional + InfrastructureRef TestContractVersionedObjectReference `json:"infrastructureRef"` +} + +// 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"` + + // +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 + 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 +}