Repository: kubernetes-sigs/cluster-api-provider-openstack

Issue:
CI: e2e test that makes use of the clusterclass-dev template
We want an e2e test that makes use of the clusterclass-dev template.

_Originally posted by @lentzi90 in https://github.com/kubernetes-sigs/cluster-api-provider-openstack/pull/2101#pullrequestreview-2081765538_
            

Discussion:
@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@EmilienM: /remove-lifecycle stale@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@EmilienM: /remove-lifecycle stale@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lentzi90: /remove-lifecycle stale@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lentzi90: /remove-lifecycle stale@k8s-triage-robot: The Kubernetes project currently lacks enough contributors to adequately respond to all issues.

This bot triages un-triaged issues according to the following rules:
- After 90d of inactivity, `lifecycle/stale` is applied
- After 30d of inactivity since `lifecycle/stale` was applied, `lifecycle/rotten` is applied
- After 30d of inactivity since `lifecycle/rotten` was applied, the issue is closed

You can:
- Mark this issue as fresh with `/remove-lifecycle stale`
- Close this issue with `/close`
- Offer to help out with [Issue Triage][1]

Please send feedback to sig-contributor-experience at [kubernetes/community](https://github.com/kubernetes/community).

/lifecycle stale

[1]: https://www.kubernetes.dev/docs/guide/issue-triage/@lentzi90: /remove-lifecycle stale

Solution patch:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 6f6043ec..c3ba8214 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -1,44 +1,118 @@
-name: release
+# This code is borrowed from https://github.com/kubernetes-sigs/cluster-api/blob/main/.github/workflows/release.yaml
+name: Create Release
 
 on:
   push:
-    # Sequence of patterns matched against refs/tags
-    tags:
-    - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
+    branches:
+    - main
+    paths:
+    - 'releasenotes/*.md'
 
-permissions:
-  contents: write # Allow to create a release.
+permissions: {}
 
 jobs:
-  build:
+  push_release_tags:
+    permissions:
+      contents: write # Allow to push tags
+    runs-on: ubuntu-latest
+    outputs:
+      release_tag: ${{ steps.release-version.outputs.release_version }}
+    if: github.repository == 'kubernetes-sigs/cluster-api-provider-openstack'
+    steps:
+    - name: Checkout code
+      uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0
+      with:
+        fetch-depth: 0
+    - name: Get changed files
+      id: changed-files
+      uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
+    - name: Get release version
+      id: release-version
+      run: |
+        if [[ ${{ steps.changed-files.outputs.all_changed_files_count }} != 1 ]]; then
+          echo "1 release notes file should be changed to create a release tag, found ${{ steps.changed-files.outputs.all_changed_files_count }}"
+          exit 1
+        fi
+        for changed_file in ${{ steps.changed-files.outputs.all_changed_files }}; do
+          export RELEASE_VERSION=$(echo "${changed_file}" | grep -oP '(?<=/)[^/]+(?=\.md)')
+          echo "RELEASE_VERSION=${RELEASE_VERSION}" >> ${GITHUB_ENV}
+          echo "RELEASE_VERSION=${RELEASE_VERSION}" >> ${GITHUB_OUTPUT}
+          if [[ "${RELEASE_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
+            echo "Valid semver: ${RELEASE_VERSION}"
+          else
+            echo "Invalid semver: ${RELEASE_VERSION}"
+            exit 1
+          fi
+        done
+    - name: Determine the release branch to use
+      run: |
+        if [[ ${RELEASE_VERSION} =~ beta ]] || [[ ${RELEASE_VERSION} =~ alpha ]]; then
+          export RELEASE_BRANCH=main
+          echo "RELEASE_BRANCH=${RELEASE_BRANCH}" >> ${GITHUB_ENV}
+          echo "This is a beta or alpha release, will use release branch ${RELEASE_BRANCH}"
+        else
+          export RELEASE_BRANCH=release-$(echo ${RELEASE_VERSION} | sed -E 's/^v([0-9]+)\.([0-9]+)\..*$/\1.\2/')
+          echo "RELEASE_BRANCH=${RELEASE_BRANCH}" >> ${GITHUB_ENV}
+          echo "This is not a beta or alpha release, will use release branch ${RELEASE_BRANCH}"
+        fi
+    - name: Create or checkout release branch
+      run: |
+        if git show-ref --verify --quiet "refs/remotes/origin/${RELEASE_BRANCH}"; then
+          echo "Branch ${RELEASE_BRANCH} already exists"
+          git checkout "${RELEASE_BRANCH}"
+        else
+          git checkout -b "${RELEASE_BRANCH}"
+          git push origin "${RELEASE_BRANCH}"
+          echo "Created branch ${RELEASE_BRANCH}"
+        fi
+    - name: Validate tag does not already exist
+      run: |
+        if [[ -n "$(git tag -l "${RELEASE_VERSION}")" ]]; then
+          echo "Tag ${RELEASE_VERSION} already exists, exiting"
+          exit 1
+        fi
+    - name: Create Release Tag
+      run: |
+        git config user.name "${GITHUB_ACTOR}"
+        git config user.email "${GITHUB_ACTOR}@users.noreply.github.com"
+        git tag -a ${RELEASE_VERSION} -m ${RELEASE_VERSION}
+        git push origin ${RELEASE_VERSION}
+        echo "Created tags ${RELEASE_VERSION}"
+  release:
     name: create draft release
     runs-on: ubuntu-latest
+    needs: push_release_tags
+    permissions:
+      contents: write # Allow to create a release.
     steps:
-      - name: Set env
-        run:  echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV
-      - name: checkout code
-        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0
-        with:
-          fetch-depth: 0
-      - name: Calculate go version
-        run: echo "go_version=$(make go-version)" >> $GITHUB_ENV
-      - name: Set up Go
-        uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # tag=v6.0.0
-        with:
-          go-version: ${{ env.go_version }}
-      - name: generate release artifacts
-        run: |
-          make release
-      - name: generate release notes
-        # Ignore failures for release-notes generation so they could still get
-        # generated manually before publishing.
-        run: |
-          make generate-release-notes || echo "Failed to generate release notes" >> _releasenotes/${{ env.RELEASE_TAG }}.md
-        env:
-          GH_TOKEN: ${{ github.token }}
-      - name: Release
-        uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # tag=v2.4.1
-        with:
-          draft: true
-          files: out/*
-          body_path: _releasenotes/${{ env.RELEASE_TAG }}.md
+    - name: Set env
+      run: echo "RELEASE_TAG=${RELEASE_TAG}" >> ${GITHUB_ENV}
+      env:
+        RELEASE_TAG: ${{needs.push_release_tags.outputs.release_tag}}
+    - name: checkout code
+      uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # tag=v5.0.0
+      with:
+        fetch-depth: 0
+        ref: ${{ env.RELEASE_TAG }}
+    - name: Calculate go version
+      run: echo "go_version=$(make go-version)" >> ${GITHUB_ENV}
+    - name: Set up Go
+      uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # tag=v6.0.0
+      with:
+        go-version: ${{ env.go_version }}
+    - name: generate release artifacts
+      run: |
+        make release
+      env:
+        GH_TOKEN: ${{ github.token }}
+    - name: get release notes
+      run: |
+        curl -L "https://raw.githubusercontent.com/${{ github.repository }}/main/releasenotes/${{ env.RELEASE_TAG }}.md" \
+        -o "${{ env.RELEASE_TAG }}.md"
+    - name: Release
+      uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # tag=v2.4.1
+      with:
+        draft: true
+        files: out/*
+        body_path: ${{ env.RELEASE_TAG }}.md
+        tag_name: ${{ env.RELEASE_TAG }}
diff --git a/Makefile b/Makefile
index 6ccff05a..60d73b86 100644
--- a/Makefile
+++ b/Makefile
@@ -188,6 +188,7 @@ e2e-templates: $(addprefix $(E2E_NO_ARTIFACT_TEMPLATES_DIR)/, \
 		 cluster-template-multi-network.yaml \
 		 cluster-template-without-lb.yaml \
 		 cluster-template.yaml \
+		 cluster-template-topology.yaml \
 		 cluster-template-flatcar.yaml \
 		 cluster-template-k8s-upgrade.yaml \
 		 cluster-template-flatcar-sysext.yaml \
@@ -448,14 +449,13 @@ staging-manifests:
 ## --------------------------------------
 ##@ Release
 ## --------------------------------------
-
 ifneq (,$(findstring -,$(RELEASE_TAG)))
     PRE_RELEASE=true
 endif
 PREVIOUS_TAG ?= $(shell git tag -l | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$$" | sort -V | grep -B1 $(RELEASE_TAG) | head -n 1 2>/dev/null)
 ## set by Prow, ref name of the base branch, e.g., main
 RELEASE_DIR := out
-RELEASE_NOTES_DIR := _releasenotes
+RELEASE_NOTES_DIR := releasenotes
 
 .PHONY: $(RELEASE_DIR)
 $(RELEASE_DIR):
@@ -478,7 +478,7 @@ list-image:
 	gcloud container images list-tags $(STAGING_REGISTRY)/$(IMAGE) --filter="tags=('$(RELEASE_TAG)')" --format=json
 
 .PHONY: release
-release: $(RELEASE_NOTES) clean-release $(RELEASE_DIR)  ## Builds and push container images using the latest git tag for the commit.
+release: $(RELEASE_NOTES) $(RELEASE_DIR)  ## Builds and push container images using the latest git tag for the commit.
 	@if [ -z "${RELEASE_TAG}" ]; then echo "RELEASE_TAG is not set"; exit 1; fi
 	@if ! [ -z "$$(git status --porcelain)" ]; then echo "Your local git repository contains uncommitted changes, use git clean before proceeding."; fi
 	git checkout "${RELEASE_TAG}"
@@ -486,6 +486,7 @@ release: $(RELEASE_NOTES) clean-release $(RELEASE_DIR)  ## Builds and push conta
 	$(MAKE) manifest-modification REGISTRY=$(PROD_REGISTRY)
 	$(MAKE) release-manifests
 	$(MAKE) release-templates
+	$(MAKE) generate-release-notes
 
 .PHONY: manifest-modification
 manifest-modification: # Set the manifest images to the staging/production bucket.
diff --git a/templates/cluster-template-development.yaml b/templates/cluster-template-topology.yaml
similarity index 86%
rename from templates/cluster-template-development.yaml
rename to templates/cluster-template-topology.yaml
index df62668d..d83ca03e 100644
--- a/templates/cluster-template-development.yaml
+++ b/templates/cluster-template-topology.yaml
@@ -1,25 +1,20 @@
-apiVersion: cluster.x-k8s.io/v1beta1
+apiVersion: cluster.x-k8s.io/v1beta2
 kind: Cluster
 metadata:
   name: ${CLUSTER_NAME}
 spec:
   topology:
-    class: dev-test
-    version: ${KUBERNETES_VERSION}
+    classRef:
+      name: dev-test
     controlPlane:
       replicas: ${CONTROL_PLANE_MACHINE_COUNT}
-    workers:
-      machineDeployments:
-      - class: default-worker
-        name: md-0
-        replicas: ${WORKER_MACHINE_COUNT}
     variables:
     - name: identityRef
       value:
-        name: ${CLOUD_CONFIG_SECRET:=dev-test-cloud-config}
         cloudName: ${OPENSTACK_CLOUD:=capo-e2e}
-    - name: imageName
-      value: ${IMAGE_NAME:=flatcar_production}
+        name: ${CLOUD_CONFIG_SECRET:=dev-test-cloud-config}
+    - name: imageRef
+      value: ${IMAGE_REF:=node-image}
     - name: addImageVersion
       value: ${ADD_IMAGE_VERSION:=false}
     - name: injectIgnitionSysext
@@ -32,6 +27,12 @@ spec:
         spec:
           flavor: ${OPENSTACK_BASTION_FLAVOR:=m1.small}
           image:
-            filter:
+            imageRef:
               name: ${OPENSTACK_BASTION_IMAGE_NAME:=ubuntu-24.04}
           sshKeyName: ${OPENSTACK_SSH_KEY_NAME:=""}
+    version: ${KUBERNETES_VERSION}
+    workers:
+      machineDeployments:
+      - class: default-worker
+        name: md-0
+        replicas: ${WORKER_MACHINE_COUNT}
diff --git a/templates/clusterclass-dev-test.yaml b/templates/clusterclass-dev-test.yaml
index eff4f62a..220d4b06 100644
--- a/templates/clusterclass-dev-test.yaml
+++ b/templates/clusterclass-dev-test.yaml
@@ -5,7 +5,7 @@ metadata:
 spec:
   controlPlane:
     templateRef:
-      apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+      apiVersion: controlplane.cluster.x-k8s.io/v1beta2
       kind: KubeadmControlPlaneTemplate
       name: dev-test-control-plane
     machineInfrastructure:
@@ -23,7 +23,7 @@ spec:
     - class: default-worker
       bootstrap:
         templateRef:
-          apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+          apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
           kind: KubeadmConfigTemplate
           name: dev-test-default-worker-bootstraptemplate
       infrastructure:
@@ -53,16 +53,22 @@ spec:
         type: string
         description: |
           The base name of the OpenStack image that is used for creating the servers.
-          This will be combined with the k8s version to create the full name. E.g. imageName-v1.31.2.
-        default: "ubuntu-2404-kube"
   - name: addImageVersion
     required: false
     schema:
       openAPIV3Schema:
         type: boolean
         description: |
-          Add a suffix with the Kubernetes version to the imageName. E.g. imageName-v1.32.2.
+          Add a suffix with the Kubernetes version to the imageName or imageRef. E.g. imageName-v1.32.2.
         default: true
+  - name: imageRef
+    required: false
+    schema:
+      openAPIV3Schema:
+        type: string
+        description: |
+          The name of the ORC Image object that is used for creating the servers.
+        default: "node-image"
   - name: injectIgnitionSysext
     required: false
     schema:
@@ -109,6 +115,12 @@ spec:
                       name:
                         type: string
                         description: "Name of the image to use for bastion"
+                  imageRef:
+                    type: object
+                    properties:
+                      name:
+                        type: string
+                        description: "Name of the ORC Image to use for bastion"
               sshKeyName:
                 type: string
                 description: "SSH key pair name for bastion access"
@@ -120,8 +132,9 @@ spec:
             type: string
             description: "Availability zone for the bastion host"
   patches:
-  - name: image
-    description: "Sets the OpenStack image that is used for creating the servers."
+  - name: imageName
+    description: "Sets the OpenStack image that is used for creating the servers using a name filter."
+    enabledIf: "{{ if .imageName }}true{{ end }}"
     definitions:
     - selector:
         apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
@@ -129,11 +142,13 @@ spec:
         matchResources:
           controlPlane: true
       jsonPatches:
+      - op: remove
+        path: /spec/template/spec/image/imageRef
       - op: add
-        path: /spec/template/spec/image/filter/name
+        path: /spec/template/spec/image/filter
         valueFrom:
           template: |
-            {{ .imageName }}{{ if .addImageVersion }}-{{ .builtin.controlPlane.version }}{{ end }}
+            name: {{ .imageName }}{{ if .addImageVersion }}-{{ .builtin.controlPlane.version }}{{ end }}
     - selector:
         apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
         kind: OpenStackMachineTemplate
@@ -142,11 +157,41 @@ spec:
             names:
             - default-worker
       jsonPatches:
+      - op: remove
+        path: /spec/template/spec/image/imageRef
       - op: add
-        path: /spec/template/spec/image/filter/name
+        path: /spec/template/spec/image/filter
         valueFrom:
           template: |
-            {{ .imageName }}{{ if .addImageVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}
+            name: {{ .imageName }}{{ if .addImageVersion }}-{{ .builtin.machineDeployment.version }}{{ end }}
+  - name: imageRef
+    description: "Sets the OpenStack image that is used for creating the servers using an ORC Image reference."
+    enabledIf: "{{ if .imageRef }}true{{ end }}"
+    definitions:
+    - selector:
+        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+        kind: OpenStackMachineTemplate
+        matchResources:
+          controlPlane: true
+      jsonPatches:
+      - op: replace
+        path: /spec/template/spec/image/imageRef/name
+        valueFrom:
+          template: |
+            {{ .imageRef }}{{ if .addImageVersion }}-{{ .builtin.controlPlane.version }}{{ end }}
+    - selector:
+        apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
+        kind: OpenStackMachineTemplate
+        matchResources:
+          machineDeploymentClass:
+            names:
+            - default-worker
+      jsonPatches:
+      - op: replace
+        path: /spec/template/spec/image/imageRef/name
+        valueFrom:
+          template: |
+            {{ .imageRef }}{{ if .addImageVersion }}-{{ .builtin.controlPlane.version }}{{ end }}
   - name: identityRef
     description: "Sets the OpenStack identity reference."
     definitions:
@@ -193,7 +238,7 @@ spec:
     enabledIf: "{{ .injectIgnitionSysext }}"
     definitions:
     - selector:
-        apiVersion: controlplane.cluster.x-k8s.io/v1beta1
+        apiVersion: controlplane.cluster.x-k8s.io/v1beta2
         kind: KubeadmControlPlaneTemplate
         matchResources:
           controlPlane: true
@@ -211,12 +256,18 @@ spec:
               nodeRegistration:
                 name: $${COREOS_OPENSTACK_HOSTNAME}
                 kubeletExtraArgs:
-                  provider-id: openstack:///$${COREOS_OPENSTACK_INSTANCE_UUID}
+                - name: cloud-provider
+                  value: external
+                - name: provider-id
+                  value: openstack:///$${COREOS_OPENSTACK_INSTANCE_UUID}
             joinConfiguration:
               nodeRegistration:
                 name: $${COREOS_OPENSTACK_HOSTNAME}
                 kubeletExtraArgs:
-                  provider-id: openstack:///$${COREOS_OPENSTACK_INSTANCE_UUID}
+                - name: cloud-provider
+                  value: external
+                - name: provider-id
+                  value: openstack:///$${COREOS_OPENSTACK_INSTANCE_UUID}
             format: ignition
             ignition:
               containerLinuxConfig:
@@ -233,16 +284,16 @@ spec:
                         mode: 0644
                         contents:
                           remote:
-                            url: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-{{ $minor }}.conf
+                            url: https://extensions.flatcar.org/extensions/kubernetes/kubernetes-{{ $minor }}.conf
                       - path: /etc/sysupdate.d/noop.conf
                         mode: 0644
                         contents:
                           remote:
-                            url: https://github.com/flatcar/sysext-bakery/releases/download/latest/noop.conf
+                            url: https://extensions.flatcar.org/extensions/noop.conf
                       - path: /opt/extensions/kubernetes/kubernetes-{{ .builtin.controlPlane.version }}-x86-64.raw
                         contents:
                           remote:
-                            url: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-{{ .builtin.controlPlane.version }}-x86-64.raw
+                            url: https://extensions.flatcar.org/extensions/kubernetes-{{ .builtin.controlPlane.version }}-x86-64.raw
                   systemd:
                     units:
                       - name: systemd-sysupdate.service
@@ -277,7 +328,7 @@ spec:
                               [Service]
                               EnvironmentFile=/run/metadata/flatcar
     - selector:
-        apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
+        apiVersion: bootstrap.cluster.x-k8s.io/v1beta2
         kind: KubeadmConfigTemplate
         matchResources:
           machineDeploymentClass:
@@ -297,7 +348,10 @@ spec:
               nodeRegistration:
                 name: $${COREOS_OPENSTACK_HOSTNAME}
                 kubeletExtraArgs:
-                  provider-id: openstack:///$${COREOS_OPENSTACK_INSTANCE_UUID}
+                - name: cloud-provider
+                  value: external
+                - name: provider-id
+                  value: openstack:///$${COREOS_OPENSTACK_INSTANCE_UUID}
             format: ignition
             ignition:
               containerLinuxConfig:
@@ -314,16 +368,16 @@ spec:
                         mode: 0644
                         contents:
                           remote:
-                            url: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-{{ $minor }}.conf
+                            url: https://extensions.flatcar.org/extensions/kubernetes/kubernetes-{{ $minor }}.conf
                       - path: /etc/sysupdate.d/noop.conf
                         mode: 0644
                         contents:
                           remote:
-                            url: https://github.com/flatcar/sysext-bakery/releases/download/latest/noop.conf
+                            url: https://extensions.flatcar.org/extensions/noop.conf
                       - path: /opt/extensions/kubernetes/kubernetes-{{ .builtin.machineDeployment.version }}-x86-64.raw
                         contents:
                           remote:
-                            url: https://github.com/flatcar/sysext-bakery/releases/download/latest/kubernetes-{{ .builtin.machineDeployment.version }}-x86-64.raw
+                            url: https://extensions.flatcar.org/extensions/kubernetes-{{ .builtin.machineDeployment.version }}-x86-64.raw
                   systemd:
                     units:
                       - name: systemd-sysupdate.service
@@ -365,7 +419,6 @@ metadata:
 spec:
   template:
     spec:
-      files: []
       joinConfiguration:
         nodeRegistration:
           kubeletExtraArgs:
@@ -382,6 +435,11 @@ metadata:
 spec:
   template:
     spec:
+      rollout:
+        strategy:
+          type: RollingUpdate
+          rollingUpdate:
+            maxSurge: 1
       kubeadmConfigSpec:
         clusterConfiguration:
           controllerManager:
@@ -454,8 +512,8 @@ spec:
     spec:
       flavor: ${OPENSTACK_CONTROL_PLANE_MACHINE_FLAVOR:=m1.medium}
       image:
-        filter:
-          name: overridden-by-patch
+        imageRef:
+          name: node-image
       sshKeyName: ${OPENSTACK_SSH_KEY_NAME:=""}
 ---
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
@@ -467,6 +525,6 @@ spec:
     spec:
       flavor: ${OPENSTACK_NODE_MACHINE_FLAVOR:=m1.small}
       image:
-        filter:
-          name: overridden-by-patch
+        imageRef:
+          name: node-image
       sshKeyName: ${OPENSTACK_SSH_KEY_NAME:=""}



Test patch:
diff --git a/test/e2e/data/e2e_conf.yaml b/test/e2e/data/e2e_conf.yaml
index 9914f0eb..4e69ffe0 100644
--- a/test/e2e/data/e2e_conf.yaml
+++ b/test/e2e/data/e2e_conf.yaml
@@ -171,6 +171,7 @@ providers:
     - sourcePath: "./infrastructure-openstack-no-artifact/cluster-template.yaml"
     - sourcePath: "./infrastructure-openstack-no-artifact/cluster-template-without-lb.yaml"
     - sourcePath: "./infrastructure-openstack-no-artifact/cluster-template-cluster-identity.yaml"
+    - sourcePath: "../../../templates/clusterclass-dev-test.yaml"
     replacements:
     - old: gcr.io/k8s-staging-capi-openstack/capi-openstack-controller:dev
       new: gcr.io/k8s-staging-capi-openstack/capi-openstack-controller:e2e
@@ -218,6 +219,7 @@ variables:
   CNI: "../../data/cni/calico.yaml"
   CCM: "../../data/ccm/cloud-controller-manager.yaml"
   EXP_CLUSTER_RESOURCE_SET: "true"
+  IP_FAMILY: "ipv4"
   OPENSTACK_BASTION_IMAGE_NAME: "cirros-0.6.1-x86_64-disk"
   OPENSTACK_BASTION_IMAGE_URL: https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/cirros/2022-12-05/cirros-0.6.1-x86_64-disk.img
   OPENSTACK_BASTION_IMAGE_HASH: 0c839612eb3f2469420f2ccae990827f
@@ -246,6 +248,7 @@ variables:
   E2E_IMAGE_URL: "http://10.0.3.15/capo-e2e-image.tar"
   # The default user for SSH connections from bastion to machines
   SSH_USER_MACHINE: "ubuntu"
+  CLUSTER_TOPOLOGY: "true"
   EXP_KUBEADM_BOOTSTRAP_FORMAT_IGNITION: "true"
   # The Flatcar image produced by the image-builder
   OPENSTACK_FLATCAR_IMAGE_NAME: "flatcar-stable-4152.2.3-kube-v1.33.1"
diff --git a/test/e2e/data/kustomize/components/common/kustomization.yaml b/test/e2e/data/kustomize/components/common/kustomization.yaml
index 211dc0e4..61582df5 100644
--- a/test/e2e/data/kustomize/components/common/kustomization.yaml
+++ b/test/e2e/data/kustomize/components/common/kustomization.yaml
@@ -1,7 +1,7 @@
 apiVersion: kustomize.config.k8s.io/v1alpha1
 kind: Component
-resources:
-- images.yaml
+components:
+- ../images
 patches:
 # Set AZ and enable bastion
 - target:
diff --git a/test/e2e/data/kustomize/components/common/images.yaml b/test/e2e/data/kustomize/components/images/images.yaml
similarity index 100%
rename from test/e2e/data/kustomize/components/common/images.yaml
rename to test/e2e/data/kustomize/components/images/images.yaml
diff --git a/test/e2e/data/kustomize/components/images/kustomization.yaml b/test/e2e/data/kustomize/components/images/kustomization.yaml
new file mode 100644
index 00000000..daa515fd
--- /dev/null
+++ b/test/e2e/data/kustomize/components/images/kustomization.yaml
@@ -0,0 +1,4 @@
+apiVersion: kustomize.config.k8s.io/v1alpha1
+kind: Component
+resources:
+- images.yaml
diff --git a/test/e2e/data/kustomize/topology/cluster.yaml b/test/e2e/data/kustomize/topology/cluster.yaml
new file mode 100644
index 00000000..477715f2
--- /dev/null
+++ b/test/e2e/data/kustomize/topology/cluster.yaml
@@ -0,0 +1,40 @@
+# Rather than editing the cluster-template, which include already variables,
+# we simply create a new cluster-template with the correct variables for e2e.
+apiVersion: cluster.x-k8s.io/v1beta2
+kind: Cluster
+metadata:
+  name: ${CLUSTER_NAME}
+spec:
+  topology:
+    classRef:
+      name: dev-test
+    version: ${KUBERNETES_VERSION}
+    controlPlane:
+      replicas: ${CONTROL_PLANE_MACHINE_COUNT}
+    workers:
+      machineDeployments:
+      - class: default-worker
+        name: md-0
+        replicas: ${WORKER_MACHINE_COUNT}
+    variables:
+    - name: identityRef
+      value:
+        name: ${CLUSTER_NAME}-cloud-config
+        cloudName: ${OPENSTACK_CLOUD}
+    - name: imageRef
+      value: node-image
+    - name: addImageVersion
+      value: false
+    - name: injectIgnitionSysext
+      value: true
+    - name: allowedCIDRs
+      value: ${OPENSTACK_API_SERVER_ALLOWED_CIDRS:=[]}
+    - name: bastion
+      value:
+        enabled: ${OPENSTACK_BASTION_ENABLED:=false}
+        spec:
+          flavor: ${OPENSTACK_BASTION_FLAVOR:=m1.small}
+          image:
+            imageRef:
+              name: bastion-image
+          sshKeyName: ${OPENSTACK_SSH_KEY_NAME:=""}
diff --git a/test/e2e/data/kustomize/topology/kustomization.yaml b/test/e2e/data/kustomize/topology/kustomization.yaml
new file mode 100644
index 00000000..b2ad517c
--- /dev/null
+++ b/test/e2e/data/kustomize/topology/kustomization.yaml
@@ -0,0 +1,24 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+- cluster.yaml
+- secret.yaml
+
+components:
+- ../components/images
+- ../components/cluster-resource-sets
+
+patches:
+- target:
+    group: openstack.k-orc.cloud
+    version: v1alpha1
+    kind: Image
+    name: node-image
+  patch: |-
+    - op: replace
+      path: /spec/resource/name
+      value: ${FLATCAR_IMAGE_NAME}
+    - op: replace
+      path: /spec/resource/content/download/url
+      value: ${FLATCAR_IMAGE_URL}
diff --git a/test/e2e/data/kustomize/topology/secret.yaml b/test/e2e/data/kustomize/topology/secret.yaml
new file mode 100644
index 00000000..2d6641ad
--- /dev/null
+++ b/test/e2e/data/kustomize/topology/secret.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+data:
+  cacert: ${OPENSTACK_CLOUD_CACERT_B64}
+  clouds.yaml: ${OPENSTACK_CLOUD_YAML_B64}
+kind: Secret
+metadata:
+  labels:
+    clusterctl.cluster.x-k8s.io/move: "true"
+  name: ${CLUSTER_NAME}-cloud-config
diff --git a/test/e2e/shared/defaults.go b/test/e2e/shared/defaults.go
index e14af489..e8821ac8 100644
--- a/test/e2e/shared/defaults.go
+++ b/test/e2e/shared/defaults.go
@@ -52,6 +52,7 @@ const (
 	OpenStackNodeMachineFlavor = "OPENSTACK_NODE_MACHINE_FLAVOR"
 	SSHUserMachine             = "SSH_USER_MACHINE"
 	FlavorDefault              = ""
+	FlavorTopology             = "topology"
 	FlavorNoBastion            = "no-bastion"
 	FlavorWithoutLB            = "without-lb"
 	FlavorMultiNetwork         = "multi-network"
diff --git a/test/e2e/suites/e2e/clusterctl_upgrade_test.go b/test/e2e/suites/e2e/clusterctl_upgrade_test.go
index 1af541d4..30be6748 100644
--- a/test/e2e/suites/e2e/clusterctl_upgrade_test.go
+++ b/test/e2e/suites/e2e/clusterctl_upgrade_test.go
@@ -68,6 +68,7 @@ var _ = Describe("When testing clusterctl upgrades for CAPO (v0.11=>current) and
 			WorkloadFlavor:                    shared.FlavorCapiV1Beta1,
 			InitWithKubernetesVersion:         e2eCtx.E2EConfig.MustGetVariable(shared.KubernetesVersion),
 			InitWithRuntimeExtensionProviders: []string{"openstack-resource-controller:v1.0.2"},
+			UseKindForManagementCluster:       true,
 		}
 	})
 })
@@ -102,6 +103,7 @@ var _ = Describe("When testing clusterctl upgrades for CAPO (v0.12=>current) and
 			WorkloadFlavor:                    shared.FlavorCapiV1Beta1,
 			InitWithKubernetesVersion:         e2eCtx.E2EConfig.MustGetVariable(shared.KubernetesVersion),
 			InitWithRuntimeExtensionProviders: []string{"openstack-resource-controller:v1.0.2"},
+			UseKindForManagementCluster:       true,
 		}
 	})
 })
@@ -136,6 +138,7 @@ var _ = Describe("When testing clusterctl upgrades for CAPO (v0.13=>current) and
 			WorkloadFlavor:                    shared.FlavorDefault,
 			InitWithKubernetesVersion:         e2eCtx.E2EConfig.MustGetVariable(shared.KubernetesVersion),
 			InitWithRuntimeExtensionProviders: []string{"openstack-resource-controller:v1.0.2"},
+			UseKindForManagementCluster:       true,
 		}
 	})
 })
diff --git a/test/e2e/suites/e2e/quick_start_test.go b/test/e2e/suites/e2e/quick_start_test.go
new file mode 100644
index 00000000..1a8d3d37
--- /dev/null
+++ b/test/e2e/suites/e2e/quick_start_test.go
@@ -0,0 +1,43 @@
+//go:build e2e
+// +build e2e
+
+/*
+Copyright 2024 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 e2e
+
+import (
+	"context"
+
+	. "github.com/onsi/ginkgo/v2"
+	"k8s.io/utils/ptr"
+	capi_e2e "sigs.k8s.io/cluster-api/test/e2e"
+
+	"sigs.k8s.io/cluster-api-provider-openstack/test/e2e/shared"
+)
+
+var _ = Describe("When following the Cluster API quick-start with ClusterClass [PR-Blocking] [ClusterClass]", func() {
+	capi_e2e.QuickStartSpec(context.TODO(), func() capi_e2e.QuickStartSpecInput {
+		return capi_e2e.QuickStartSpecInput{
+			E2EConfig:             e2eCtx.E2EConfig,
+			ClusterctlConfigPath:  e2eCtx.Environment.ClusterctlConfigPath,
+			BootstrapClusterProxy: e2eCtx.Environment.BootstrapClusterProxy,
+			ArtifactFolder:        e2eCtx.Settings.ArtifactFolder,
+			SkipCleanup:           false,
+			Flavor:                ptr.To(shared.FlavorTopology),
+		}
+	})
+})