diff --git a/.github/workflows/e2e-upgrade.yaml b/.github/workflows/e2e-upgrade.yaml new file mode 100644 index 00000000..4e832f6d --- /dev/null +++ b/.github/workflows/e2e-upgrade.yaml @@ -0,0 +1,37 @@ +name: End-to-end upgrade tests +on: + schedule: + # Weekly, matching the conformance suite's cadence. Offset to Tuesday to + # avoid contending for IONOS Cloud resources with conformance (Mondays). + - cron: "0 5 * * 2" + workflow_dispatch: {} +jobs: + e2e-upgrade: + runs-on: ubuntu-latest + environment: e2e + env: + IONOS_TOKEN: ${{ secrets.IONOS_TOKEN }} + IONOSCLOUD_MACHINE_IMAGE_ID: ${{ vars.IONOSCLOUD_MACHINE_IMAGE_ID }} + CONTROL_PLANE_ENDPOINT_LOCATION: ${{ vars.CONTROL_PLANE_ENDPOINT_LOCATION }} + IONOSCLOUD_CUBE_TEMPLATE_ID: ${{ vars.IONOSCLOUD_CUBE_TEMPLATE_ID }} + # Real IONOS Cloud cluster provisioning across multiple upgrade hops is very + # likely to exceed the Makefile's 2h default; override for this scheduled run. + GINKGO_TIMEOUT: "4h" + steps: + - uses: actions/checkout@v7 + + - name: Setup Go environment + uses: actions/setup-go@v7 + with: + go-version-file: "go.mod" + + - name: Run e2e upgrade tests + run: make test-e2e-upgrade + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + if: success() || failure() + with: + name: logs-upgrade + path: _artifacts + retention-days: 7 diff --git a/Makefile b/Makefile index 76950c6f..e05b606c 100644 --- a/Makefile +++ b/Makefile @@ -284,7 +284,10 @@ GINKGO_NODES ?= 1 GINKGO_FOCUS ?= GINKGO_TIMEOUT ?= 2h GINKGO_NOCOLOR ?= false -GINKGO_LABEL ?= "!Conformance" +# Exclude the "upgrade" label by default: the provider-upgrade suite is heavy +# (spins up its own kind management cluster and installs old providers per block) +# and is run separately via `make test-e2e-upgrade`. +GINKGO_LABEL ?= "!Conformance && !upgrade" GINKGO_SKIP ?= # To set multiple ginkgo skip flags, if any @@ -306,6 +309,16 @@ test-e2e: docker-build-e2e ## Run the end-to-end tests -e2e.artifacts-folder="$(ARTIFACTS)" -e2e.config="$(E2E_CONF_FILE)" \ -e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) -e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER) +.PHONY: test-e2e-upgrade +test-e2e-upgrade: docker-build-e2e ## Run only the provider-upgrade e2e tests (Label "upgrade") + CGO_ENABLED=1 go run github.com/onsi/ginkgo/v2/ginkgo -v --trace \ + -poll-progress-after=$(GINKGO_POLL_PROGRESS_AFTER) \ + -poll-progress-interval=$(GINKGO_POLL_PROGRESS_INTERVAL) --tags=e2e --fail-fast \ + --nodes=1 --label-filter="upgrade" --timeout=$(GINKGO_TIMEOUT) --no-color=$(GINKGO_NOCOLOR) \ + --output-dir="$(ARTIFACTS)" --junit-report="junit.e2e_upgrade.1.xml" $(GINKGO_ARGS) $(ROOT_DIR)/$(TEST_DIR)/e2e -- \ + -e2e.artifacts-folder="$(ARTIFACTS)" -e2e.config="$(E2E_CONF_FILE)" \ + -e2e.skip-resource-cleanup=$(SKIP_RESOURCE_CLEANUP) -e2e.use-existing-cluster=$(USE_EXISTING_CLUSTER) + .PHONY: ionosctl ionosctl: $(LOCALBIN) ## Download the latest release of ionosctl and add to the bin folder @if [ ! -f $(LOCALBIN)/ionosctl ]; then \ diff --git a/test/e2e/config/ionoscloud.yaml b/test/e2e/config/ionoscloud.yaml index 135a54da..ec3cdeea 100644 --- a/test/e2e/config/ionoscloud.yaml +++ b/test/e2e/config/ionoscloud.yaml @@ -14,6 +14,42 @@ providers: new: --metrics-addr=:8443 files: - sourcePath: "../data/shared/v1.13/metadata.yaml" + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/core-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/core-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/core-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/core-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" - name: kubeadm type: BootstrapProvider versions: @@ -26,6 +62,42 @@ providers: new: --metrics-addr=:8443 files: - sourcePath: "../data/shared/v1.13/metadata.yaml" + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/bootstrap-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/bootstrap-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/bootstrap-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/bootstrap-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" - name: kubeadm type: ControlPlaneProvider versions: @@ -38,6 +110,42 @@ providers: new: --metrics-addr=:8443 files: - sourcePath: "../data/shared/v1.13/metadata.yaml" + - name: "v1.12.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.12.10/control-plane-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.12/metadata.yaml" + - name: "v1.11.11" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.11.11/control-plane-components.yaml" + type: url + contract: v1beta2 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.11/metadata.yaml" + - name: "v1.10.10" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/control-plane-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.10/metadata.yaml" + - name: "v1.8.12" + value: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/control-plane-components.yaml" + type: url + contract: v1beta1 + replacements: + - old: --metrics-addr=127.0.0.1:8080 + new: --metrics-addr=:8443 + files: + - sourcePath: "../data/shared/v1.8/metadata.yaml" - name: in-cluster type: IPAMProvider versions: @@ -54,6 +162,22 @@ providers: - name: ionoscloud type: InfrastructureProvider versions: + - name: v0.6.3 + value: "https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/releases/download/v0.6.3/infrastructure-components.yaml" + type: url + contract: v1beta1 + files: + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" + - name: v0.7.0 + value: "https://github.com/ionos-cloud/cluster-api-provider-ionoscloud/releases/download/v0.7.0/infrastructure-components.yaml" + type: url + contract: v1beta1 + files: + # The published v0.7.0 metadata.yaml is missing its own "0.7" releaseSeries + # entry, so clusterctl can't resolve the version's contract and init fails. + # Override it with the repo metadata.yaml, which declares 0.7 as v1beta1. + - sourcePath: "../../../metadata.yaml" + - sourcePath: "../data/infrastructure-ionoscloud/cluster-template.yaml" - name: v0.8.99 value: "../../../config/default" replacements: @@ -73,6 +197,7 @@ variables: # This avoids building node images in the default case which improves the test duration significantly. # Workload version: must match the IONOS node image (IONOSCLOUD_MACHINE_IMAGE_ID). KUBERNETES_VERSION: "v1.34.11" + IP_FAMILY: "ipv4" # Management (kind) version: must be in CAPI's supported management range, v1.32-v1.36 for v1.13. KUBERNETES_VERSION_MANAGEMENT: "v1.33.7" CNI: "./data/cni/calico.yaml" @@ -98,6 +223,11 @@ intervals: default/wait-delete-cluster: [ "30m", "10s" ] default/wait-nodes-ready: ["30m", "10s"] default/wait-resource-versions-become-stable: ["5m", "10s"] + # Upgrade spec intervals; values match upstream's docker.yaml. A missing key yields nil, + # which Eventually treats as Gomega's ~1s default. + default/wait-resource-versions-remain-stable: ["2m", "5s"] + default/wait-control-plane-upgrade: ["15m", "10s"] + default/wait-machine-deployment-upgrade: ["10m", "10s"] scale/wait-cluster: ["30m", "10s"] scale/wait-control-plane: ["30m", "10s"] scale/wait-worker-nodes: ["30m", "10s"] diff --git a/test/e2e/helpers/clusterhealth.go b/test/e2e/helpers/clusterhealth.go new file mode 100644 index 00000000..934a526c --- /dev/null +++ b/test/e2e/helpers/clusterhealth.go @@ -0,0 +1,52 @@ +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 helpers + +import ( + "context" + + clusterv1beta1 "sigs.k8s.io/cluster-api/api/core/v1beta1" //nolint:staticcheck // only contract CAPI v1.10.x serves + "sigs.k8s.io/cluster-api/test/framework" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/gomega" +) + +// AssertV1Beta1ClusterAndMachinesHealthy is a PostUpgrade hook checking Cluster/Machine +// Status.Phase only — safe to use when the upgrade target doesn't serve v1beta2 yet. +func AssertV1Beta1ClusterAndMachinesHealthy(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, clusterName string) { + c := proxy.GetClient() + + cluster := &clusterv1beta1.Cluster{} + Expect(c.Get(ctx, runtimeclient.ObjectKey{Namespace: namespace, Name: clusterName}, cluster)).To(Succeed()) + Expect(cluster.Status.Phase).To(Equal(string(clusterv1beta1.ClusterPhaseProvisioned)), + "Cluster %s should be in the Provisioned phase", clusterName) + + machines := &clusterv1beta1.MachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace), + runtimeclient.MatchingLabels{clusterv1beta1.ClusterNameLabel: clusterName})).To(Succeed()) + Expect(machines.Items).NotTo(BeEmpty(), "expected at least one Machine for cluster %s", clusterName) + + for _, m := range machines.Items { + Expect(m.Status.Phase).To(Equal(string(clusterv1beta1.MachinePhaseRunning)), + "Machine %s should be in the Running phase", m.Name) + } + } +} diff --git a/test/e2e/helpers/conditions.go b/test/e2e/helpers/conditions.go new file mode 100644 index 00000000..5a41839b --- /dev/null +++ b/test/e2e/helpers/conditions.go @@ -0,0 +1,140 @@ +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 helpers + +import ( + "context" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clusterctlcluster "sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster" + "sigs.k8s.io/cluster-api/test/framework" + runtimeclient "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "github.com/ionos-cloud/cluster-api-provider-ionoscloud/api/v1alpha1" + + . "github.com/onsi/gomega" +) + +// AssertConditionsMigration returns a PostUpgrade hook that verifies CAPIC resources +// carry the v1beta2 status fields after upgrading from v0.6.x to v0.8+. +func AssertConditionsMigration(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, _ string) { + c := proxy.GetClient() + + clusters := &infrav1.IonosCloudClusterList{} + Expect(c.List(ctx, clusters, runtimeclient.InNamespace(namespace))).To(Succeed()) + Expect(clusters.Items).NotTo(BeEmpty(), + "expected at least one IonosCloudCluster in namespace %s", namespace) + + for i := range clusters.Items { + assertClusterConditionsMigrated(&clusters.Items[i]) + } + + machines := &infrav1.IonosCloudMachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace))).To(Succeed()) + Expect(machines.Items).NotTo(BeEmpty(), + "expected at least one IonosCloudMachine in namespace %s", namespace) + + for i := range machines.Items { + assertMachineConditionsMigrated(&machines.Items[i]) + } + } +} + +// AssertCAPIV1Beta2Migration returns a PostUpgrade hook that verifies CAPI core +// v1beta2 types are active and the Cluster is available after upgrading to v1.11+. +func AssertCAPIV1Beta2Migration(ctx context.Context) func(framework.ClusterProxy, string, string) { + return func(proxy framework.ClusterProxy, namespace, clusterName string) { + c := proxy.GetClient() + + // Assert the clusters.cluster.x-k8s.io CRD storage migration to v1beta2 has completed + // (status.storedVersions == [v1beta2], not just spec.versions[].storage). + framework.ValidateCRDMigration(ctx, proxy, namespace, clusterName, + func(crd apiextensionsv1.CustomResourceDefinition) bool { + return crd.Name == "clusters.cluster.x-k8s.io" + }, + clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName)) + + // Assert Cluster has Available=True. + framework.VerifyClusterAvailable(ctx, framework.VerifyClusterAvailableInput{ + Getter: proxy.GetClient(), + Name: clusterName, + Namespace: namespace, + }) + + // Assert no continuous reconcile loop (resource versions stable). + framework.ValidateResourceVersionStable(ctx, framework.ValidateResourceVersionStableInput{ + ClusterProxy: proxy, + Namespace: namespace, + OwnerGraphFilterFunction: clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName), + }) + + // Assert CAPIC resources also carry the v1beta2 fields (upgrade included CAPIC too). + clusters := &infrav1.IonosCloudClusterList{} + Expect(c.List(ctx, clusters, runtimeclient.InNamespace(namespace))).To(Succeed()) + for i := range clusters.Items { + assertClusterConditionsMigrated(&clusters.Items[i]) + } + + machines := &infrav1.IonosCloudMachineList{} + Expect(c.List(ctx, machines, runtimeclient.InNamespace(namespace))).To(Succeed()) + for i := range machines.Items { + assertMachineConditionsMigrated(&machines.Items[i]) + } + } +} + +func assertClusterConditionsMigrated(cluster *infrav1.IonosCloudCluster) { + assertConditionsMigrated("IonosCloudCluster", cluster.Name, + cluster.Status.Initialization.Provisioned, cluster.Status.Conditions) +} + +func assertMachineConditionsMigrated(machine *infrav1.IonosCloudMachine) { + assertConditionsMigrated("IonosCloudMachine", machine.Name, + machine.Status.Initialization.Provisioned, machine.Status.Conditions) +} + +// assertConditionsMigrated verifies the v1beta2 status shape shared by IonosCloudCluster +// and IonosCloudMachine after upgrading from v0.6.x/v0.7.x to v0.8+, including that any +// condition carried over from the old v1beta1 conditions API (empty Reason) was backfilled +// by api/v1alpha1.BackfillLegacyConditionReasons rather than rejected by the new schema. +func assertConditionsMigrated(kind, name string, provisioned *bool, conditions []metav1.Condition) { + // status.initialization.provisioned must be set and true. + Expect(provisioned).NotTo(BeNil(), + "%s %s: status.initialization.provisioned must be set after upgrade", kind, name) + Expect(*provisioned).To(BeTrue(), + "%s %s: status.initialization.provisioned must be true after upgrade", kind, name) + + // status.conditions (metav1.Condition) must be non-empty. + Expect(conditions).NotTo(BeEmpty(), + "%s %s: status.conditions must be non-empty after upgrade", kind, name) + hasTrue := false + for _, cond := range conditions { + if cond.Status == metav1.ConditionTrue { + hasTrue = true + } + Expect(cond.Reason).NotTo(BeEmpty(), + "%s %s: condition %q must carry a non-empty Reason after upgrade "+ + "(legacy v1beta1 conditions must be backfilled, see api/v1alpha1.BackfillLegacyConditionReasons)", + kind, name, cond.Type) + } + Expect(hasTrue).To(BeTrue(), + "%s %s: at least one metav1.Condition must have Status=True", kind, name) +} diff --git a/test/e2e/upgrade_test.go b/test/e2e/upgrade_test.go new file mode 100644 index 00000000..af7d4bd7 --- /dev/null +++ b/test/e2e/upgrade_test.go @@ -0,0 +1,139 @@ +//go:build e2e + +/* +Copyright 2024 IONOS Cloud. + +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 e2e + +import ( + capie2e "sigs.k8s.io/cluster-api/test/e2e" + "sigs.k8s.io/cluster-api/test/framework" + + "github.com/ionos-cloud/cluster-api-provider-ionoscloud/test/e2e/helpers" + + . "github.com/onsi/ginkgo/v2" +) + +// upgradeSpecBaseInput returns the ClusterctlUpgradeSpecInput fields shared by all +// upgrade test blocks. Each block overrides init/upgrade versions and PostUpgrade. +func upgradeSpecBaseInput() capie2e.ClusterctlUpgradeSpecInput { + return capie2e.ClusterctlUpgradeSpecInput{ + E2EConfig: e2eConfig, + ClusterctlConfigPath: clusterctlConfigPath, + BootstrapClusterProxy: bootstrapClusterProxy, + ArtifactFolder: artifactFolder, + SkipCleanup: skipCleanup, + UseKindForManagementCluster: true, + // Without this, the framework defaults to its own package-private initScheme(), + // which registers only the default CAPI schemes and not our infrav1 types — every + // PostUpgrade hook that lists IonosCloudCluster/IonosCloudMachine objects against + // this proxy would fail with "no kind registered for type v1alpha1...". + KindManagementClusterNewClusterProxyFunc: func(name, kubeconfigPath string) framework.ClusterProxy { + return framework.NewClusterProxy(name, kubeconfigPath, initScheme(), framework.WithMachineLogCollector(framework.DockerLogCollector{})) + }, + PostNamespaceCreated: cloudEnv.createCredentialsSecretPNC, + // InitWithKubernetesVersion is required by ClusterctlUpgradeSpec: it is the + // Kubernetes version of the secondary (kind) management cluster the old + // providers are installed into. Reuse the suite's configured version. + InitWithKubernetesVersion: e2eConfig.MustGetVariable(KubernetesVersion), + ControlPlaneMachineCount: new(int64(1)), + } +} + +var _ = Describe("Should stage a v1.8-created cluster through v1.10 (real v0.7.0 release), v1.11 (v1beta1-conditions migration) and v1.12 (full v1beta2 migration)", Label("upgrade", "staged"), func() { + capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { + in := upgradeSpecBaseInput() + in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.8.12/clusterctl-{OS}-{ARCH}" + in.InitWithCoreProvider = "cluster-api:v1.8.12" + // Pin bootstrap/control-plane to the same version as core. Without this they + // fall back to the latest matching the "*" contract (v1.12.10), which would + // mismatch the v1.8.12 core provider and fail clusterctl init. + in.InitWithBootstrapProviders = []string{"kubeadm:v1.8.12"} + in.InitWithControlPlaneProviders = []string{"kubeadm:v1.8.12"} + in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.6.3"} + in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ + { + // Real released hop: proves v0.6.3 (v1.8.12, v1beta1) -> v0.7.0 (v1.10.10, + // still pure v1beta1 — see docs/capi-upgrade-plan.md) survives before the + // pure-v1beta2 v0.8 cut is applied. + WithBinary: "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}", + CoreProvider: "cluster-api:v1.10.10", + BootstrapProviders: []string{"kubeadm:v1.10.10"}, + ControlPlaneProviders: []string{"kubeadm:v1.10.10"}, + InfrastructureProviders: []string{"ionoscloud:v0.7.0"}, + PostUpgrade: helpers.AssertV1Beta1ClusterAndMachinesHealthy(ctx), + }, + { + // This hop lands the pure-v1beta2 v0.8 provider (local build) on a + // v0.7.0-created cluster and proves both: the legacy v1beta1 conditions + // (empty Reason) are backfilled, not rejected by the new schema, and the + // CAPI core v1beta2 type migration itself completes cleanly. + CoreProvider: "cluster-api:v1.11.11", + BootstrapProviders: []string{"kubeadm:v1.11.11"}, + ControlPlaneProviders: []string{"kubeadm:v1.11.11"}, + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: func(proxy framework.ClusterProxy, namespace, clusterName string) { + helpers.AssertConditionsMigration(ctx)(proxy, namespace, clusterName) + helpers.AssertCAPIV1Beta2Migration(ctx)(proxy, namespace, clusterName) + }, + }, + { + // Extra hop bumping CAPI core to v1.12.10. The v0.8.99 provider is + // already built against v1.12.10, so this exercises the v1.11 -> v1.12 + // core upgrade and confirms the migrated conditions stay intact. + CoreProvider: "cluster-api:v1.12.10", + BootstrapProviders: []string{"kubeadm:v1.12.10"}, + ControlPlaneProviders: []string{"kubeadm:v1.12.10"}, + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + } + return in + }) +}) + +// Overlaps with the staged block's second hop on purpose: starting fresh at v1.10.10 +// isolates a real v1.10->v1.11 bug from one caused by the staged block's carried-over state. +var _ = Describe("Should handle CAPI core v1beta2 type migration when upgrading from v1.10 through v1.11 to v1.12", Label("upgrade", "isolated-hop"), func() { + capie2e.ClusterctlUpgradeSpec(ctx, func() capie2e.ClusterctlUpgradeSpecInput { + in := upgradeSpecBaseInput() + in.InitWithBinary = "https://github.com/kubernetes-sigs/cluster-api/releases/download/v1.10.10/clusterctl-{OS}-{ARCH}" + in.InitWithCoreProvider = "cluster-api:v1.10.10" + // Pin bootstrap/control-plane to match the v1.10.10 core provider (see block above). + in.InitWithBootstrapProviders = []string{"kubeadm:v1.10.10"} + in.InitWithControlPlaneProviders = []string{"kubeadm:v1.10.10"} + in.InitWithInfrastructureProviders = []string{"ionoscloud:v0.7.0"} + in.Upgrades = []capie2e.ClusterctlUpgradeSpecInputUpgrade{ + { + CoreProvider: "cluster-api:v1.11.11", + BootstrapProviders: []string{"kubeadm:v1.11.11"}, + ControlPlaneProviders: []string{"kubeadm:v1.11.11"}, + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + { + // Extra hop bumping CAPI core to v1.12.10 (the version v0.8.99 is built + // against) to validate the v1.11 -> v1.12 core upgrade. + CoreProvider: "cluster-api:v1.12.10", + BootstrapProviders: []string{"kubeadm:v1.12.10"}, + ControlPlaneProviders: []string{"kubeadm:v1.12.10"}, + InfrastructureProviders: []string{"ionoscloud:v0.8.99"}, + PostUpgrade: helpers.AssertCAPIV1Beta2Migration(ctx), + }, + } + return in + }) +})