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

Issue:
E2e image sources hard coded
/kind bug

**What steps did you take and what happened:**

I noticed that we have hard coded a [few things](https://github.com/kubernetes-sigs/cluster-api-provider-openstack/blob/7f7f0f0fd51307bd9e87925212d6c672f625dfbd/test/e2e/shared/images.go#L45) with the introduction of ORC in the e2e tests. This unfortunately makes the tests much less flexible than they used to be. In theory, we could point to any OpenStack-based cloud, tune the e2e config to match and run the suite.
The images used could differ.

Now with ORC we have hard coded where the images should come from, while still allowing the exact name to be configurable.
This does not make sense to me. We should make the source also configurable.

I think/hope it is already possible to pre-create the images and ORC will just adopt them. This is good.

**What did you expect to happen:**

It should be possible to run the e2e test suite in different configurations without assumptions on what exact images would be used or where they can be found.

**Anything else you would like to add:**

I intend to work on this but I don't have an estimate on when I will have the time.


Hints from issue comments:
@mdbooth: This seems reasonable. Sounds like I refactored it too hard when doing this. We just need enough configuration in e2e data to generate the ORC images.

I suspect you'll want to replace the DownloadImage stuff with something more flexible. Note that we don't just create all images on every run. e.g. we only add the upgrade image when running the upgrade test, and the flatcar images when running the flatcar tests.

Just a thought: how about instead of generating these images in code we defined yamls for them. We'd still want to be able to define separate sets of images so we don't load all of them every time, but... maybe a kustomize module per image 'set'? If we put the path to each of these sets in the e2e config, the test could just load them from the specified directory. That way you'd be able to replace them with whatever you need in your environment. Would that work?@lentzi90: It does sound doable. We anyway specify the name of each image in the e2e config, so why not point to a kustomization that provides the image :slightly_smiling_face: 
I think I will slowly get started on this. Just want to get the CI somewhat stable first@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/Makefile b/Makefile
index 7dd58958..543a7677 100644
--- a/Makefile
+++ b/Makefile
@@ -570,7 +570,7 @@ clean-release-git: ## Restores the git files usually modified during a release
 	git restore ./*manager_image_patch.yaml ./*manager_pull_policy.yaml
 
 .PHONY: verify
-verify: verify-boilerplate verify-modules verify-gen verify-govulncheck
+verify: verify-boilerplate verify-modules verify-gen
 
 .PHONY: verify-boilerplate
 verify-boilerplate:



Test patch:
diff --git a/test/e2e/data/e2e_conf.yaml b/test/e2e/data/e2e_conf.yaml
index de7baafb..8d74e579 100644
--- a/test/e2e/data/e2e_conf.yaml
+++ b/test/e2e/data/e2e_conf.yaml
@@ -10,7 +10,7 @@ images:
 # Use local dev images built source tree;
 - name: gcr.io/k8s-staging-capi-openstack/capi-openstack-controller:e2e
   loadBehavior: mustLoad
-- name: quay.io/orc/openstack-resource-controller:v2.0.3
+- name: quay.io/orc/openstack-resource-controller:v2.2.0
   loadBehavior: tryLoad
 
 providers:
@@ -146,7 +146,7 @@ providers:
 - name: openstack-resource-controller
   type: RuntimeExtensionProvider # ORC isn't a provider but we fake it so it can be handled by the clusterctl machinery.
   versions:
-  - name: v2.0.99
+  - name: v2.2.0
     value: ../../../../cluster-api-provider-openstack/test/infrastructure/openstack-resource-controller/config/default
     contract: v1beta1
     files:
@@ -173,6 +173,9 @@ variables:
   CCM: "../../data/ccm/cloud-controller-manager.yaml"
   EXP_CLUSTER_RESOURCE_SET: "true"
   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
+  OPENSTACK_BASTION_IMAGE_HASH_ALGORITHM: "md5"
   OPENSTACK_BASTION_MACHINE_FLAVOR: "m1.tiny"
   OPENSTACK_BASTION_MACHINE_FLAVOR_ALT: "m1.tiny.alt"
   OPENSTACK_CLOUD: "capo-e2e"
@@ -184,7 +187,9 @@ variables:
   OPENSTACK_FAILURE_DOMAIN: "testaz1"
   OPENSTACK_FAILURE_DOMAIN_ALT: "testaz2"
   OPENSTACK_IMAGE_NAME: "ubuntu-2404-kube-v1.31.2"
+  OPENSTACK_IMAGE_URL: https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/ubuntu/2024-11-21/ubuntu-2404-kube-v1.31.2.img
   OPENSTACK_IMAGE_NAME_UPGRADE_FROM: "ubuntu-2204-kube-v1.30.1"
+  OPENSTACK_IMAGE_URL_UPGRADE_FROM: https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/ubuntu/2024-05-28/ubuntu-2204-kube-v1.30.1.img
   OPENSTACK_NODE_MACHINE_FLAVOR: "m1.small"
   OPENSTACK_SSH_KEY_NAME: "cluster-api-provider-openstack-sigs-k8s-io"
   # The default external network created by devstack
@@ -198,8 +203,10 @@ variables:
   EXP_KUBEADM_BOOTSTRAP_FORMAT_IGNITION: "true"
   # The Flatcar image produced by the image-builder
   OPENSTACK_FLATCAR_IMAGE_NAME: "flatcar-stable-4152.2.0-kube-v1.31.2"
+  OPENSTACK_FLATCAR_IMAGE_URL: "https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/flatcar/flatcar-stable-4152.2.0-kube-v1.31.2.img"
   # A plain Flatcar from the Flatcar releases server
   FLATCAR_IMAGE_NAME: "flatcar_production_openstack_image"
+  FLATCAR_IMAGE_URL: https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_openstack_image.img
 
 intervals:
   conformance/wait-control-plane: ["30m", "10s"]
diff --git a/test/e2e/data/kustomize/common-patches/images-without-ref/images.yaml b/test/e2e/data/kustomize/common-patches/images-without-ref/images.yaml
index 4c9ed531..9fc007bd 100644
--- a/test/e2e/data/kustomize/common-patches/images-without-ref/images.yaml
+++ b/test/e2e/data/kustomize/common-patches/images-without-ref/images.yaml
@@ -1,13 +1,20 @@
----
 apiVersion: openstack.k-orc.cloud/v1alpha1
 kind: Image
 metadata:
   name: node-image
+  labels:
+    clusterctl.cluster.x-k8s.io/move: ""
 spec:
-  import:
-    filter:
-      name: ${OPENSTACK_IMAGE_NAME}
-  managementPolicy: unmanaged
+  resource:
+    name: ${OPENSTACK_IMAGE_NAME}
+    content:
+      containerFormat: bare
+      diskFormat: qcow2
+      download:
+        url: ${OPENSTACK_IMAGE_URL}
+  managementPolicy: managed
+  managedOptions:
+    onDelete: detach
   cloudCredentialsRef:
     secretName: ${CLUSTER_NAME}-cloud-config
     cloudName: ${OPENSTACK_CLOUD}
@@ -16,11 +23,22 @@ apiVersion: openstack.k-orc.cloud/v1alpha1
 kind: Image
 metadata:
   name: bastion-image
+  labels:
+    clusterctl.cluster.x-k8s.io/move: ""
 spec:
-  import:
-    filter:
-      name: ${OPENSTACK_BASTION_IMAGE_NAME}
-  managementPolicy: unmanaged
+  resource:
+    name: ${OPENSTACK_BASTION_IMAGE_NAME}
+    content:
+      containerFormat: bare
+      diskFormat: qcow2
+      download:
+        url: ${OPENSTACK_BASTION_IMAGE_URL}
+        hash:
+          algorithm: ${OPENSTACK_BASTION_IMAGE_HASH_ALGORITHM}
+          value: ${OPENSTACK_BASTION_IMAGE_HASH}
+  managementPolicy: managed
+  managedOptions:
+    onDelete: detach
   cloudCredentialsRef:
     secretName: ${CLUSTER_NAME}-cloud-config
-    cloudName: ${OPENSTACK_CLOUD}
\ No newline at end of file
+    cloudName: ${OPENSTACK_CLOUD}
diff --git a/test/e2e/data/kustomize/common-patches/images/images.yaml b/test/e2e/data/kustomize/common-patches/images/images.yaml
index 4c9ed531..9fc007bd 100644
--- a/test/e2e/data/kustomize/common-patches/images/images.yaml
+++ b/test/e2e/data/kustomize/common-patches/images/images.yaml
@@ -1,13 +1,20 @@
----
 apiVersion: openstack.k-orc.cloud/v1alpha1
 kind: Image
 metadata:
   name: node-image
+  labels:
+    clusterctl.cluster.x-k8s.io/move: ""
 spec:
-  import:
-    filter:
-      name: ${OPENSTACK_IMAGE_NAME}
-  managementPolicy: unmanaged
+  resource:
+    name: ${OPENSTACK_IMAGE_NAME}
+    content:
+      containerFormat: bare
+      diskFormat: qcow2
+      download:
+        url: ${OPENSTACK_IMAGE_URL}
+  managementPolicy: managed
+  managedOptions:
+    onDelete: detach
   cloudCredentialsRef:
     secretName: ${CLUSTER_NAME}-cloud-config
     cloudName: ${OPENSTACK_CLOUD}
@@ -16,11 +23,22 @@ apiVersion: openstack.k-orc.cloud/v1alpha1
 kind: Image
 metadata:
   name: bastion-image
+  labels:
+    clusterctl.cluster.x-k8s.io/move: ""
 spec:
-  import:
-    filter:
-      name: ${OPENSTACK_BASTION_IMAGE_NAME}
-  managementPolicy: unmanaged
+  resource:
+    name: ${OPENSTACK_BASTION_IMAGE_NAME}
+    content:
+      containerFormat: bare
+      diskFormat: qcow2
+      download:
+        url: ${OPENSTACK_BASTION_IMAGE_URL}
+        hash:
+          algorithm: ${OPENSTACK_BASTION_IMAGE_HASH_ALGORITHM}
+          value: ${OPENSTACK_BASTION_IMAGE_HASH}
+  managementPolicy: managed
+  managedOptions:
+    onDelete: detach
   cloudCredentialsRef:
     secretName: ${CLUSTER_NAME}-cloud-config
-    cloudName: ${OPENSTACK_CLOUD}
\ No newline at end of file
+    cloudName: ${OPENSTACK_CLOUD}
diff --git a/test/e2e/data/kustomize/flatcar-sysext/kustomization.yaml b/test/e2e/data/kustomize/flatcar-sysext/kustomization.yaml
index 763bab52..3550c5f5 100644
--- a/test/e2e/data/kustomize/flatcar-sysext/kustomization.yaml
+++ b/test/e2e/data/kustomize/flatcar-sysext/kustomization.yaml
@@ -1,4 +1,3 @@
----
 resources:
 - ../../../../../kustomize/v1beta1/flatcar-sysext
 
@@ -17,5 +16,8 @@ patches:
     name: node-image
   patch: |-
     - op: replace
-      path: /spec/import/filter/name
+      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/flatcar/kustomization.yaml b/test/e2e/data/kustomize/flatcar/kustomization.yaml
index e7bbb7d8..674f00cc 100644
--- a/test/e2e/data/kustomize/flatcar/kustomization.yaml
+++ b/test/e2e/data/kustomize/flatcar/kustomization.yaml
@@ -19,5 +19,8 @@ patches:
     name: node-image
   patch: |-
     - op: replace
-      path: /spec/import/filter/name
+      path: /spec/resource/name
       value: ${OPENSTACK_FLATCAR_IMAGE_NAME}
+    - op: replace
+      path: /spec/resource/content/download/url
+      value: ${OPENSTACK_FLATCAR_IMAGE_URL}
diff --git a/test/e2e/data/kustomize/k8s-upgrade/kustomization.yaml b/test/e2e/data/kustomize/k8s-upgrade/kustomization.yaml
index 46e9bbe7..422629f5 100644
--- a/test/e2e/data/kustomize/k8s-upgrade/kustomization.yaml
+++ b/test/e2e/data/kustomize/k8s-upgrade/kustomization.yaml
@@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 resources:
 - ../default
+- upgrade-from-images.yaml
 - upgrade-from-template.yaml
 - upgrade-to-template.yaml
 
diff --git a/test/e2e/data/kustomize/k8s-upgrade/upgrade-from-images.yaml b/test/e2e/data/kustomize/k8s-upgrade/upgrade-from-images.yaml
new file mode 100644
index 00000000..cf6d2ea8
--- /dev/null
+++ b/test/e2e/data/kustomize/k8s-upgrade/upgrade-from-images.yaml
@@ -0,0 +1,20 @@
+apiVersion: openstack.k-orc.cloud/v1alpha1
+kind: Image
+metadata:
+  name: node-image-upgrade-from
+  labels:
+    clusterctl.cluster.x-k8s.io/move: ""
+spec:
+  resource:
+    name: ${OPENSTACK_IMAGE_NAME_UPGRADE_FROM}
+    content:
+      containerFormat: bare
+      diskFormat: qcow2
+      download:
+        url: ${OPENSTACK_IMAGE_URL_UPGRADE_FROM}
+  managementPolicy: managed
+  managedOptions:
+    onDelete: detach
+  cloudCredentialsRef:
+    secretName: ${CLUSTER_NAME}-cloud-config
+    cloudName: ${OPENSTACK_CLOUD}
diff --git a/test/e2e/data/kustomize/k8s-upgrade/upgrade-from-template.yaml b/test/e2e/data/kustomize/k8s-upgrade/upgrade-from-template.yaml
index d4920811..f818e8c1 100644
--- a/test/e2e/data/kustomize/k8s-upgrade/upgrade-from-template.yaml
+++ b/test/e2e/data/kustomize/k8s-upgrade/upgrade-from-template.yaml
@@ -1,4 +1,3 @@
----
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: OpenStackMachineTemplate
 metadata:
@@ -8,8 +7,8 @@ spec:
     spec:
       flavor: ${OPENSTACK_CONTROL_PLANE_MACHINE_FLAVOR}
       image:
-        filter:
-          name: ${OPENSTACK_IMAGE_NAME_UPGRADE_FROM}
+        imageRef:
+          name: node-image-upgrade-from
       sshKeyName: ${OPENSTACK_SSH_KEY_NAME}
       tags:
       - control-plane
@@ -23,8 +22,8 @@ spec:
     spec:
       flavor: ${OPENSTACK_NODE_MACHINE_FLAVOR}
       image:
-        filter:
-          name: ${OPENSTACK_IMAGE_NAME_UPGRADE_FROM}
+        imageRef:
+          name: node-image-upgrade-from
       sshKeyName: ${OPENSTACK_SSH_KEY_NAME}
       tags:
       - machine
diff --git a/test/e2e/data/kustomize/k8s-upgrade/upgrade-to-template.yaml b/test/e2e/data/kustomize/k8s-upgrade/upgrade-to-template.yaml
index 466cb793..777ee038 100644
--- a/test/e2e/data/kustomize/k8s-upgrade/upgrade-to-template.yaml
+++ b/test/e2e/data/kustomize/k8s-upgrade/upgrade-to-template.yaml
@@ -3,7 +3,6 @@
 # The normal templates has the cluster name as part of the OSMT name.
 # See CONTROL_PLANE_MACHINE_TEMPLATE_UPGRADE_TO and WORKERS_MACHINE_TEMPLATE_UPGRADE_TO
 # in the e2e config.
----
 apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
 kind: OpenStackMachineTemplate
 metadata:
@@ -15,8 +14,8 @@ spec:
     spec:
       flavor: ${OPENSTACK_CONTROL_PLANE_MACHINE_FLAVOR}
       image:
-        filter:
-          name: ${OPENSTACK_IMAGE_NAME}
+        imageRef:
+          name: node-image
       sshKeyName: ${OPENSTACK_SSH_KEY_NAME}
       tags:
       - control-plane
@@ -32,8 +31,8 @@ spec:
     spec:
       flavor: ${OPENSTACK_NODE_MACHINE_FLAVOR}
       image:
-        filter:
-          name: ${OPENSTACK_IMAGE_NAME}
+        imageRef:
+          name: node-image
       sshKeyName: ${OPENSTACK_SSH_KEY_NAME}
       tags:
       - machine
diff --git a/test/e2e/data/shared/openstack-resource-controller/metadata.yaml b/test/e2e/data/shared/openstack-resource-controller/metadata.yaml
index e26a837a..56d37f0b 100644
--- a/test/e2e/data/shared/openstack-resource-controller/metadata.yaml
+++ b/test/e2e/data/shared/openstack-resource-controller/metadata.yaml
@@ -8,3 +8,9 @@ releaseSeries:
 - major: 2
   minor: 0
   contract: v1beta1
+- major: 2
+  minor: 1
+  contract: v1beta1
+- major: 2
+  minor: 2
+  contract: v1beta1
diff --git a/test/e2e/shared/cluster.go b/test/e2e/shared/cluster.go
index 76958b39..6373eefd 100644
--- a/test/e2e/shared/cluster.go
+++ b/test/e2e/shared/cluster.go
@@ -105,8 +105,8 @@ func setupBootstrapCluster(config *clusterctl.E2EConfig, scheme *runtime.Scheme,
 }
 
 // initBootstrapCluster uses kind to create a cluster.
-func initBootstrapCluster(e2eCtx *E2EContext) {
-	clusterctl.InitManagementClusterAndWatchControllerLogs(context.TODO(), clusterctl.InitManagementClusterAndWatchControllerLogsInput{
+func initBootstrapCluster(ctx context.Context, e2eCtx *E2EContext) {
+	clusterctl.InitManagementClusterAndWatchControllerLogs(ctx, clusterctl.InitManagementClusterAndWatchControllerLogsInput{
 		ClusterProxy:              e2eCtx.Environment.BootstrapClusterProxy,
 		ClusterctlConfigPath:      e2eCtx.Environment.ClusterctlConfigPath,
 		InfrastructureProviders:   e2eCtx.E2EConfig.InfrastructureProviders(),
diff --git a/test/e2e/shared/images.go b/test/e2e/shared/images.go
index 739c7b7f..11041238 100644
--- a/test/e2e/shared/images.go
+++ b/test/e2e/shared/images.go
@@ -21,189 +21,19 @@ package shared
 
 import (
 	"context"
-	"encoding/base64"
-	neturl "net/url"
-	"slices"
-	"strings"
 
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
-	corev1 "k8s.io/api/core/v1"
-	meta "k8s.io/apimachinery/pkg/api/meta"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	orcv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/api/v1alpha1"
-	orcapplyconfigv1alpha1 "github.com/k-orc/openstack-resource-controller/v2/pkg/clients/applyconfiguration/api/v1alpha1"
-
-	"sigs.k8s.io/cluster-api-provider-openstack/internal/util/ssa"
-	"sigs.k8s.io/cluster-api-provider-openstack/pkg/scope"
 )
 
 const (
-	// The base URL of the CAPO staging artifacts bucket.
-	stagingArtifactBase = "https://storage.googleapis.com/artifacts.k8s-staging-capi-openstack.appspot.com/test/"
-
-	// The name of the credentials secret.
-	credentialsSecretName = "openstack-credentials" //nolint:gosec // these aren't hard-coded credentials
-
 	// A tag which will be added to all glances images created by these tests.
 	E2EImageTag = "capo-e2e"
 )
 
-type DownloadImage struct {
-	// The name of the ORC Image object
-	Name string
-	// An absolute URL to download the image from
-	URL string
-	// A path relative to the staging artifact repo to download the image from
-	ArtifactPath string
-	// A hash used to verify the downloaded image
-	Hash, HashAlgorithm string
-}
-
-func CoreImages(e2eCtx *E2EContext) []DownloadImage {
-	return []DownloadImage{
-		{
-			Name:         "cirros",
-			ArtifactPath: "cirros/2022-12-05/" + e2eCtx.E2EConfig.MustGetVariable("OPENSTACK_BASTION_IMAGE_NAME") + ".img",
-
-			// Specifying an image hash means we can't use
-			// web-download. This serves as an E2E test of image
-			// upload. We only do this with cirros because it's very
-			// small and it's going to involve data transfer between
-			// the test runner and wherever OpenStack is running
-			HashAlgorithm: string(orcv1alpha1.ImageHashAlgorithmMD5),
-			// From: https://download.cirros-cloud.net/0.6.1/MD5SUMS
-			Hash: "0c839612eb3f2469420f2ccae990827f",
-		},
-		{
-			Name:         "capo-default",
-			ArtifactPath: "ubuntu/2024-11-21/" + e2eCtx.E2EConfig.MustGetVariable("OPENSTACK_IMAGE_NAME") + ".img",
-		},
-	}
-}
-
-func ApplyCoreImagesPlus(ctx context.Context, e2eCtx *E2EContext, additionalImages ...DownloadImage) {
-	coreImages := CoreImages(e2eCtx)
-	allImages := slices.Concat(coreImages, additionalImages)
-
-	ApplyGlanceImages(ctx, e2eCtx, allImages)
-}
-
-func CreateGlanceCredentials(ctx context.Context, e2eCtx *E2EContext) {
-	k8sClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()
-
-	// Generate the credentials secret each image will reference
-	credentialsSecret := generateCredentialsSecret(e2eCtx)
-	Expect(k8sClient.Create(ctx, credentialsSecret)).To(Succeed(), "create openstack-credentials in default namespace")
-}
-
-func imageNames(images []DownloadImage) string {
-	names := make([]string, len(images))
-	for i := range images {
-		names[i] = images[i].Name
-	}
-	return strings.Join(names, ", ")
-}
-
-// ApplyGlanceImages creates ORC Images corresponding to the given set of DownloadImages.
-// Note that it does not wait for the images to become available.
-func ApplyGlanceImages(ctx context.Context, e2eCtx *E2EContext, images []DownloadImage) {
-	By("Applying glances images: " + imageNames(images))
-
-	k8sClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()
-
-	for _, image := range images {
-		// Infer the url if none was specified
-		url := image.URL
-		if url == "" {
-			url = stagingArtifactBase + image.ArtifactPath
-		}
-
-		// Infer the name to use for the glance image
-
-		// Use last part of url
-		u, err := neturl.Parse(url)
-		Expect(err).NotTo(HaveOccurred(), "parsing "+url)
-		d := strings.Split(u.Path, "/")
-		Expect(len(d)).To(BeNumerically(">", 1), "Not enough path elements in "+url)
-		glanceName := d[len(d)-1]
-
-		// Remove the type suffix
-		for _, suffix := range []string{".img", ".qcow2"} {
-			if strings.HasSuffix(glanceName, suffix) {
-				glanceName = glanceName[:len(glanceName)-len(suffix)]
-				continue
-			}
-		}
-
-		var imageHash *orcv1alpha1.ImageHash
-		if image.HashAlgorithm != "" && image.Hash != "" {
-			imageHash = &orcv1alpha1.ImageHash{
-				Algorithm: orcv1alpha1.ImageHashAlgorithm(image.HashAlgorithm),
-				Value:     image.Hash,
-			}
-		}
-
-		// Generate and create the image
-		orcImage, applyConfig := generateORCImage(e2eCtx, image.Name, glanceName, url, imageHash)
-		Logf("Ensuring glance image " + image.Name)
-		Expect(k8sClient.Patch(ctx, orcImage, ssa.ApplyConfigPatch(applyConfig), client.ForceOwnership, client.FieldOwner("capo-e2e"))).To(Succeed(), "ensure image "+image.Name)
-	}
-}
-
-// WaitForGlanceImagesAvailable waits for the ORC Images described by the given set of DownloadImages to become available.
-func WaitForGlanceImagesAvailable(ctx context.Context, e2eCtx *E2EContext, images []DownloadImage) {
-	names := imageNames(images)
-	By("Waiting for glance images to become available: " + names)
-
-	k8sClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()
-
-	Eventually(func(ctx context.Context) []string {
-		By("Polling images")
-
-		orcImageList := &orcv1alpha1.ImageList{}
-		Expect(k8sClient.List(ctx, orcImageList, client.InNamespace("default"))).To(Succeed())
-
-		var available []string
-		var notAvailable []string
-		for i := range images {
-			imageName := images[i].Name
-			image := func() *orcv1alpha1.Image {
-				for j := range orcImageList.Items {
-					item := &orcImageList.Items[j]
-					if item.Name == imageName {
-						return item
-					}
-				}
-				return nil
-			}()
-
-			Expect(image).ToNot(BeNil(), "Did not find "+imageName+" in image list")
-
-			availableCondition := meta.FindStatusCondition(image.Status.Conditions, orcv1alpha1.ConditionAvailable)
-			if availableCondition == nil || availableCondition.Status != metav1.ConditionTrue {
-				var msg string
-				if availableCondition == nil {
-					msg = "no status yet"
-				} else {
-					msg = availableCondition.Message
-				}
-				notAvailable = append(notAvailable, image.Name+": "+msg)
-			} else {
-				available = append(available, image.Name)
-			}
-		}
-		Logf("Available: " + strings.Join(available, ", "))
-		Logf("Not available: " + strings.Join(notAvailable, ", "))
-
-		return notAvailable
-	}, e2eCtx.E2EConfig.GetIntervals("default", "wait-image-create")...).WithContext(ctx).Should(BeEmpty(), "ORC Images are not available")
-
-	Logf("Glance images became available: " + names)
-}
-
 func DeleteAllORCImages(ctx context.Context, e2eCtx *E2EContext) {
 	k8sClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()
 
@@ -228,54 +58,3 @@ func DeleteAllORCImages(ctx context.Context, e2eCtx *E2EContext) {
 		return images
 	}, e2eCtx.E2EConfig.GetIntervals("default", "wait-image-delete")...).Should(BeEmpty(), "ORC Images were not deleted")
 }
-
-func generateCredentialsSecret(e2eCtx *E2EContext) *corev1.Secret {
-	// We run in Node1BeforeSuite, so OPENSTACK_CLOUD_YAML_B64 is not yet set
-	openStackCloudYAMLFile := e2eCtx.E2EConfig.MustGetVariable(OpenStackCloudYAMLFile)
-	caCertB64 := e2eCtx.E2EConfig.MustGetVariable(OpenStackCloudCACertB64)
-	caCert, err := base64.StdEncoding.DecodeString(caCertB64)
-	Expect(err).NotTo(HaveOccurred(), "base64 decode CA Cert: "+caCertB64)
-
-	cloudsYAML := getOpenStackCloudYAML(openStackCloudYAMLFile)
-	credentialsSecret := corev1.Secret{
-		StringData: map[string]string{
-			scope.CloudsSecretKey: string(cloudsYAML),
-			scope.CASecretKey:     string(caCert),
-		},
-	}
-	credentialsSecret.SetName(credentialsSecretName)
-	credentialsSecret.SetNamespace("default")
-
-	return &credentialsSecret
-}
-
-func generateORCImage(e2eCtx *E2EContext, name, glanceName, url string, downloadHash *orcv1alpha1.ImageHash) (*orcv1alpha1.Image, *orcapplyconfigv1alpha1.ImageApplyConfiguration) {
-	const imageNamespace = "default"
-
-	applyConfig := orcapplyconfigv1alpha1.Image(name, imageNamespace).
-		WithSpec(orcapplyconfigv1alpha1.ImageSpec().
-			WithResource(orcapplyconfigv1alpha1.ImageResourceSpec().
-				WithName(orcv1alpha1.OpenStackName(glanceName)).
-				WithTags(E2EImageTag).
-				WithContent(orcapplyconfigv1alpha1.ImageContent().
-					WithContainerFormat(orcv1alpha1.ImageContainerFormatBare).
-					WithDiskFormat(orcv1alpha1.ImageDiskFormatQCOW2).
-					WithDownload(orcapplyconfigv1alpha1.ImageContentSourceDownload().
-						WithURL(url)))).
-			WithCloudCredentialsRef(orcapplyconfigv1alpha1.CloudCredentialsReference().
-				WithSecretName(credentialsSecretName).
-				WithCloudName(e2eCtx.E2EConfig.MustGetVariable("OPENSTACK_CLOUD"))))
-
-	if downloadHash != nil {
-		applyConfig.Spec.Resource.Content.Download.
-			WithHash(orcapplyconfigv1alpha1.ImageHash().
-				WithAlgorithm(downloadHash.Algorithm).
-				WithValue(downloadHash.Value))
-	}
-
-	orcImage := &orcv1alpha1.Image{}
-	orcImage.Name = name
-	orcImage.Namespace = imageNamespace
-
-	return orcImage, applyConfig
-}
diff --git a/test/e2e/shared/openstack.go b/test/e2e/shared/openstack.go
index d3ee61ea..6cf9ea4e 100644
--- a/test/e2e/shared/openstack.go
+++ b/test/e2e/shared/openstack.go
@@ -139,6 +139,26 @@ func dumpOpenStack(_ context.Context, e2eCtx *E2EContext, bootstrapClusterProxyN
 	if err := dumpOpenStackVolumes(e2eCtx, logPath); err != nil {
 		_, _ = fmt.Fprintf(GinkgoWriter, "error dumping OpenStack volumes: %s\n", err)
 	}
+
+	if err := dumpOpenStackServers(e2eCtx, logPath); err != nil {
+		_, _ = fmt.Fprintf(GinkgoWriter, "error dumping OpenStack servers: %s\n", err)
+	}
+}
+
+// dumpOpenStackServers writes all OpenStack servers to servers.json under the logPath.
+func dumpOpenStackServers(e2eCtx *E2EContext, logPath string) error {
+	serversList, err := DumpOpenStackServers(e2eCtx, servers.ListOpts{})
+	if err != nil {
+		return err
+	}
+	serversJSON, err := json.MarshalIndent(serversList, "", "    ")
+	if err != nil {
+		return fmt.Errorf("error marshalling servers %v: %s", serversList, err)
+	}
+	if err := os.WriteFile(path.Join(logPath, "servers.json"), serversJSON, 0o600); err != nil {
+		return fmt.Errorf("error writing servers.json %s: %s", serversJSON, err)
+	}
+	return nil
 }
 
 // dumpOpenStackVolumes returns all OpenStack volumes to a file in the artifact folder.
diff --git a/test/e2e/shared/suite.go b/test/e2e/shared/suite.go
index d434adcb..5d48d950 100644
--- a/test/e2e/shared/suite.go
+++ b/test/e2e/shared/suite.go
@@ -155,10 +155,7 @@ func Node1BeforeSuite(ctx context.Context, e2eCtx *E2EContext) []byte {
 	e2eCtx.Environment.BootstrapClusterProvider, e2eCtx.Environment.BootstrapClusterProxy = setupBootstrapCluster(e2eCtx.E2EConfig, e2eCtx.Environment.Scheme, e2eCtx.Settings.UseExistingCluster)
 
 	Logf("Initializing the bootstrap cluster")
-	initBootstrapCluster(e2eCtx)
-
-	// Create credentials used by all glance images
-	CreateGlanceCredentials(ctx, e2eCtx)
+	initBootstrapCluster(ctx, e2eCtx)
 
 	conf := synchronizedBeforeTestSuiteConfig{
 		ArtifactFolder:          e2eCtx.Settings.ArtifactFolder,
diff --git a/test/e2e/suites/conformance/conformance_test.go b/test/e2e/suites/conformance/conformance_test.go
index 5af9998a..6f218349 100644
--- a/test/e2e/suites/conformance/conformance_test.go
+++ b/test/e2e/suites/conformance/conformance_test.go
@@ -49,8 +49,6 @@ var _ = Describe("conformance tests", func() {
 		Expect(e2eCtx.E2EConfig.Variables).To(HaveKey(shared.KubernetesVersion))
 		// Setup a Namespace where to host objects for this spec and create a watcher for the namespace events.
 		namespace = shared.SetupSpecNamespace(ctx, specName, e2eCtx)
-
-		shared.ApplyCoreImagesPlus(ctx, e2eCtx)
 	})
 
 	It(specName, func(ctx context.Context) {
diff --git a/test/e2e/suites/e2e/clusterctl_upgrade_test.go b/test/e2e/suites/e2e/clusterctl_upgrade_test.go
index 29ffb5a5..ec11037f 100644
--- a/test/e2e/suites/e2e/clusterctl_upgrade_test.go
+++ b/test/e2e/suites/e2e/clusterctl_upgrade_test.go
@@ -38,7 +38,6 @@ var (
 
 var _ = Describe("When testing clusterctl upgrades (v0.11=>current) [clusterctl-upgrade]", func() {
 	BeforeEach(func(ctx context.Context) {
-		shared.ApplyCoreImagesPlus(ctx, e2eCtx, upgradeImage)
 		setDownloadE2EImageEnvVar()
 		// Note: This gives the version without the 'v' prefix, so we need to add it below.
 		capoRelease011, err = clusterctl.ResolveRelease(ctx, "go://github.com/kubernetes-sigs/cluster-api-provider-openstack@v0.11")
@@ -72,7 +71,6 @@ var _ = Describe("When testing clusterctl upgrades (v0.11=>current) [clusterctl-
 
 var _ = Describe("When testing clusterctl upgrades (v0.12=>current) [clusterctl-upgrade]", func() {
 	BeforeEach(func(ctx context.Context) {
-		shared.ApplyCoreImagesPlus(ctx, e2eCtx, upgradeImage)
 		setDownloadE2EImageEnvVar()
 		// Note: This gives the version without the 'v' prefix, so we need to add it below.
 		capoRelease012, err = clusterctl.ResolveRelease(ctx, "go://github.com/kubernetes-sigs/cluster-api-provider-openstack@v0.12")
diff --git a/test/e2e/suites/e2e/e2e_suite_test.go b/test/e2e/suites/e2e/e2e_suite_test.go
index 2219dfe8..a4fb17da 100644
--- a/test/e2e/suites/e2e/e2e_suite_test.go
+++ b/test/e2e/suites/e2e/e2e_suite_test.go
@@ -39,9 +39,8 @@ import (
 )
 
 var (
-	e2eCtx       *shared.E2EContext
-	err          error
-	upgradeImage shared.DownloadImage
+	e2eCtx *shared.E2EContext
+	err    error
 )
 
 func init() {
@@ -97,11 +96,6 @@ var _ = SynchronizedBeforeSuite(func(ctx context.Context) []byte {
 	return data
 }, func(data []byte) {
 	shared.AllNodesBeforeSuite(e2eCtx, data)
-
-	upgradeImage = shared.DownloadImage{
-		Name:         "capo-upgrade-from",
-		ArtifactPath: "ubuntu/2024-05-28/" + e2eCtx.E2EConfig.MustGetVariable("OPENSTACK_IMAGE_NAME_UPGRADE_FROM") + ".img",
-	}
 })
 
 // CheckResourceCleanup checks if all resources created during the test are cleaned up by comparing the resources
diff --git a/test/e2e/suites/e2e/e2e_test.go b/test/e2e/suites/e2e/e2e_test.go
index 67c55daf..22c2a958 100644
--- a/test/e2e/suites/e2e/e2e_test.go
+++ b/test/e2e/suites/e2e/e2e_test.go
@@ -63,20 +63,6 @@ import (
 
 const specName = "e2e"
 
-// Additional images required for flatcar tests.
-func flatcarImages(e2eCtx *shared.E2EContext) []shared.DownloadImage {
-	return []shared.DownloadImage{
-		{
-			Name:         "capo-flatcar",
-			ArtifactPath: "flatcar/" + e2eCtx.E2EConfig.MustGetVariable("OPENSTACK_FLATCAR_IMAGE_NAME") + ".img",
-		},
-		{
-			Name: "flatcar-openstack",
-			URL:  "https://stable.release.flatcar-linux.net/amd64-usr/current/flatcar_production_openstack_image.img",
-		},
-	}
-}
-
 var _ = Describe("e2e tests [PR-Blocking]", func() {
 	var (
 		namespace        *corev1.Namespace
@@ -84,9 +70,6 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
 
 		// Cleanup functions which cannot run until after the cluster has been deleted
 		postClusterCleanup []func(context.Context)
-
-		// Images required for the current test in addition to the core images
-		additionalImages []shared.DownloadImage
 	)
 
 	createCluster := func(ctx context.Context, configCluster clusterctl.ConfigClusterInput, result *clusterctl.ApplyClusterTemplateAndWaitResult) {
@@ -119,12 +102,6 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
 		Expect(e2eCtx.E2EConfig).ToNot(BeNil(), "Invalid argument. e2eConfig can't be nil when calling %s spec", specName)
 		Expect(e2eCtx.E2EConfig.Variables).To(HaveKey(shared.KubernetesVersion))
 		postClusterCleanup = nil
-
-		additionalImages = nil
-	})
-
-	JustBeforeEach(func(ctx context.Context) {
-		shared.ApplyCoreImagesPlus(ctx, e2eCtx, additionalImages...)
 	})
 
 	Describe("Workload cluster (default)", func() {
@@ -388,10 +365,6 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
 	})
 
 	Describe("Workload cluster (flatcar)", func() {
-		BeforeEach(func() {
-			additionalImages = append(additionalImages, flatcarImages(e2eCtx)...)
-		})
-
 		It("should be creatable and deletable", func(ctx context.Context) {
 			// Flatcar default user is "core"
 			shared.SetEnvVar(shared.SSHUserMachine, "core", false)
@@ -437,10 +410,6 @@ var _ = Describe("e2e tests [PR-Blocking]", func() {
 	})
 
 	Describe("Workload cluster (flatcar-sysext)", func() {
-		BeforeEach(func() {
-			additionalImages = append(additionalImages, flatcarImages(e2eCtx)...)
-		})
-
 		It("should be creatable and deletable", func(ctx context.Context) {
 			// Flatcar default user is "core"
 			shared.SetEnvVar(shared.SSHUserMachine, "core", false)
diff --git a/test/e2e/suites/e2e/remediations_test.go b/test/e2e/suites/e2e/remediations_test.go
index 8f8c5bdf..d8df1765 100644
--- a/test/e2e/suites/e2e/remediations_test.go
+++ b/test/e2e/suites/e2e/remediations_test.go
@@ -31,10 +31,6 @@ import (
 
 var _ = Describe("When testing unhealthy machines remediation", func() {
 	Describe("When testing MachineDeployment remediation", func() {
-		BeforeEach(func(ctx context.Context) {
-			shared.ApplyCoreImagesPlus(ctx, e2eCtx)
-		})
-
 		capi_e2e.MachineDeploymentRemediationSpec(context.TODO(), func() capi_e2e.MachineDeploymentRemediationSpecInput {
 			return capi_e2e.MachineDeploymentRemediationSpecInput{
 				E2EConfig:             e2eCtx.E2EConfig,
diff --git a/test/e2e/suites/e2e/self_hosted_test.go b/test/e2e/suites/e2e/self_hosted_test.go
index 93946a54..3ad15110 100644
--- a/test/e2e/suites/e2e/self_hosted_test.go
+++ b/test/e2e/suites/e2e/self_hosted_test.go
@@ -29,9 +29,7 @@ import (
 )
 
 var _ = Describe("When testing Cluster API provider Openstack working on [self-hosted] clusters", func() {
-	BeforeEach(func(ctx context.Context) {
-		shared.ApplyCoreImagesPlus(ctx, e2eCtx, upgradeImage)
-
+	BeforeEach(func() {
 		setDownloadE2EImageEnvVar()
 	})
 
diff --git a/test/infrastructure/openstack-resource-controller/config/default/kustomization.yaml b/test/infrastructure/openstack-resource-controller/config/default/kustomization.yaml
index b042e3da..1e332598 100644
--- a/test/infrastructure/openstack-resource-controller/config/default/kustomization.yaml
+++ b/test/infrastructure/openstack-resource-controller/config/default/kustomization.yaml
@@ -8,4 +8,4 @@ labels:
     cluster.x-k8s.io/provider: "runtime-extension-openstack-resource-controller"
 
 resources:
-- https://github.com/k-orc/openstack-resource-controller/releases/download/v2.0.3/install.yaml
+- https://github.com/k-orc/openstack-resource-controller/releases/download/v2.2.0/install.yaml