diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/.gitignore b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..e660fd93d3196215552065b1e63bf6a2f393ed86
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/.gitignore
@@ -0,0 +1 @@
+bin/
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/Makefile b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..d9c503ed916d5e1367cf05c6c0a1a4a561078f6b
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/Makefile
@@ -0,0 +1,34 @@
+GO111MODULE = on
+export GO111MODULE
+GOFLAGS ?= -mod=vendor
+export GOFLAGS
+GOPROXY ?=
+export GOPROXY
+
+# ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary.
+ENVTEST_K8S_VERSION = 1.28
+
+PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
+ENVTEST = go run ${PROJECT_DIR}/vendor/sigs.k8s.io/controller-runtime/tools/setup-envtest
+GOLANGCI_LINT = go run ${PROJECT_DIR}/vendor/github.com/golangci/golangci-lint/cmd/golangci-lint
+
+.PHONY: all
+all: lint unit
+
+.PHONY: vendor
+vendor: ## Update vendor directory
+	go mod tidy
+	go mod vendor
+	go mod verify
+
+.PHONY: lint
+lint: ## Go lint your code
+	( GOLANGCI_LINT_CACHE=$(PROJECT_DIR)/.cache $(GOLANGCI_LINT) run --config ../.golangci.yaml --timeout 10m )
+
+.PHONY: unit
+unit: ## Run unit tests
+	KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path --bin-dir $(PROJECT_DIR)/bin)" ./hack/test.sh
+
+.PHONY: help
+help:
+	@grep -E '^[a-zA-Z/0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/cleanup.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/cleanup.go
new file mode 100644
index 0000000000000000000000000000000000000000..cb1b317177c1c80aa13b11b5cad87a3a30bcbf32
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/cleanup.go
@@ -0,0 +1,150 @@
+/*
+Copyright 2022 Red Hat, Inc.
+
+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 testutils
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/onsi/gomega"
+
+	corev1resourcebuilder "github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1"
+	corev1 "k8s.io/api/core/v1"
+	apimeta "k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	coreclient "k8s.io/client-go/kubernetes/typed/core/v1"
+	"k8s.io/client-go/rest"
+
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/envtest/komega"
+)
+
+// CleanupResources will clean up resources of the types of objects given in a particular namespace if given.
+// The namespace will then be removed once it has been emptied.
+// This utility is intended to be used in AfterEach blocks to clean up from a specific test.
+// It calls various gomega assertions so will fail a test if there are any errors.
+func CleanupResources(g gomega.Gomega, ctx context.Context, cfg *rest.Config, k8sClient client.Client, namespace string, objs ...client.Object) {
+	for _, obj := range objs {
+		cleanupResource(g, ctx, k8sClient, namespace, obj)
+	}
+
+	if namespace != "" {
+		removeNamespace(g, ctx, cfg, k8sClient, namespace)
+	}
+}
+
+// cleanupResource removes all of a particular resource within a namespace.
+func cleanupResource(g gomega.Gomega, ctx context.Context, k8sClient client.Client, namespace string, obj client.Object) {
+	removeFinalizersFromAll(g, ctx, k8sClient, namespace, obj)
+
+	g.Eventually(func() (client.ObjectList, error) {
+		if err := k8sClient.DeleteAllOf(ctx, obj, client.InNamespace(namespace)); err != nil {
+			return nil, fmt.Errorf("error deleting resource list: %w", err)
+		}
+
+		listObj := newListFromObject(g, k8sClient, obj)
+
+		return komega.ObjectList(listObj, client.InNamespace(namespace))()
+	}).Should(gomega.HaveField("Items", gomega.HaveLen(0)))
+}
+
+// removeFinalizersFromAll removes any finalizers from all of the objects of the given object kind,
+// in the namespace provided.
+func removeFinalizersFromAll(g gomega.Gomega, ctx context.Context, k8sClient client.Client, namespace string, obj client.Object) {
+	listObj := newListFromObject(g, k8sClient, obj)
+
+	g.Expect(k8sClient.List(ctx, listObj, client.InNamespace(namespace))).Should(gomega.Succeed())
+
+	listItems, err := apimeta.ExtractList(listObj)
+	g.Expect(err).ToNot(gomega.HaveOccurred())
+
+	for _, item := range listItems {
+		o, ok := item.(client.Object)
+		g.Expect(ok).To(gomega.BeTrue())
+
+		removeFinalizers(g, o)
+	}
+}
+
+// removeFinalizers removes all finalizers from the object given.
+// Finalizers must be removed one by one else the API server will reject the update.
+func removeFinalizers(g gomega.Gomega, obj client.Object) {
+	filter := func(finalizers []string, toRemove string) []string {
+		out := []string{}
+
+		for _, f := range finalizers {
+			if f != toRemove {
+				out = append(out, f)
+			}
+		}
+
+		return out
+	}
+
+	for _, finalizer := range obj.GetFinalizers() {
+		g.Eventually(komega.Update(obj, func() {
+			obj.SetFinalizers(filter(obj.GetFinalizers(), finalizer))
+		})).Should(gomega.Succeed())
+	}
+}
+
+// newListFromObject converts an individual object type into a list object type to allow the
+// the list to be checked for emptiness.
+func newListFromObject(g gomega.Gomega, k8sClient client.Client, obj client.Object) client.ObjectList {
+	objGVKs, _, err := k8sClient.Scheme().ObjectKinds(obj)
+	g.Expect(err).ToNot(gomega.HaveOccurred())
+	g.Expect(objGVKs).To(gomega.HaveLen(1))
+
+	listGVK := objGVKs[0]
+	listGVK.Kind = fmt.Sprintf("%sList", listGVK.Kind)
+
+	newObj, err := k8sClient.Scheme().New(listGVK)
+	g.Expect(err).ToNot(gomega.HaveOccurred())
+
+	listObj, ok := newObj.(client.ObjectList)
+	g.Expect(ok).To(gomega.BeTrue())
+
+	return listObj
+}
+
+// removeNamespace handles the namespace finalization act that is normally performed by the garbage collector
+// once it is happy that the namespace has no objects left within it.
+func removeNamespace(g gomega.Gomega, ctx context.Context, cfg *rest.Config, k8sClient client.Client, namespace string) {
+	coreClient, err := coreclient.NewForConfig(cfg)
+	g.Expect(err).ToNot(gomega.HaveOccurred())
+
+	nsBuilder := corev1resourcebuilder.Namespace().WithName(namespace)
+
+	// Delete the namespace
+	ns := nsBuilder.Build()
+	g.Expect(k8sClient.Delete(ctx, ns)).To(gomega.Succeed())
+
+	// Remove the finalizer
+	g.Eventually(func() error {
+		if err := komega.Get(ns)(); err != nil {
+			return fmt.Errorf("could not get namespace: %w", err)
+		}
+		ns.Spec.Finalizers = []corev1.FinalizerName{}
+
+		_, err := coreClient.Namespaces().Finalize(ctx, ns, metav1.UpdateOptions{})
+		if err != nil {
+			return fmt.Errorf("could not finalize namespace: %w", err)
+		}
+
+		return nil
+	}).Should(gomega.Succeed())
+}
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/conditions.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/conditions.go
new file mode 100644
index 0000000000000000000000000000000000000000..728b02cd8fb1d33d50cb179fe83aea8d0c57d5d0
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/conditions.go
@@ -0,0 +1,232 @@
+/*
+Copyright 2022 Red Hat, Inc.
+
+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 testutils
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/onsi/gomega"
+	"github.com/onsi/gomega/types"
+
+	configv1 "github.com/openshift/api/config/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// errActualTypeMismatchCondition is used when the type of the actual object does not match the expected type of Condition.
+var errActualTypeMismatchCondition = errors.New("actual should be of type Condition")
+
+// MatchConditions returns a custom matcher to check equality of a slice of metav1.Condtion.
+func MatchConditions(expected []metav1.Condition) types.GomegaMatcher {
+	return &matchConditions{
+		expected: expected,
+	}
+}
+
+type matchConditions struct {
+	expected []metav1.Condition
+}
+
+// Match checks for equality between the actual and expected objects.
+func (m matchConditions) Match(actual interface{}) (success bool, err error) {
+	elems := []interface{}{}
+	for _, condition := range m.expected {
+		elems = append(elems, MatchCondition(condition))
+	}
+
+	ok, err := gomega.ConsistOf(elems).Match(actual)
+	if !ok {
+		return false, wrap(err, "conditions did not match")
+	}
+
+	return true, nil
+}
+
+// FailureMessage is the message returned to the test when the actual and expected
+// objects do not match.
+func (m matchConditions) FailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("expected\n\t%#v\nto match\n\t%#v\n", actual, m.expected)
+}
+
+// NegatedFailureMessage is the negated version of the FailureMessage.
+func (m matchConditions) NegatedFailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("expected\n\t%#v\nto not match\n\t%#v\n", actual, m.expected)
+}
+
+// MatchCondition returns a custom matcher to check equality of metav1.Condition.
+func MatchCondition(expected metav1.Condition) types.GomegaMatcher {
+	return &matchCondition{
+		expected: expected,
+	}
+}
+
+type matchCondition struct {
+	expected metav1.Condition
+}
+
+// Match checks for equality between the actual and expected objects.
+//
+//nolint:dupl
+func (m matchCondition) Match(actual interface{}) (success bool, err error) {
+	actualCondition, ok := actual.(metav1.Condition)
+	if !ok {
+		return false, errActualTypeMismatchCondition
+	}
+
+	ok, err = gomega.Equal(m.expected.Type).Match(actualCondition.Type)
+	if !ok {
+		return false, wrap(err, "condition type does not match")
+	}
+
+	ok, err = gomega.Equal(m.expected.Status).Match(actualCondition.Status)
+	if !ok {
+		return false, wrap(err, "condition status does not match")
+	}
+
+	ok, err = gomega.Equal(m.expected.Reason).Match(actualCondition.Reason)
+	if !ok {
+		return false, wrap(err, "condition reason does not match")
+	}
+
+	ok, err = gomega.Equal(m.expected.Message).Match(actualCondition.Message)
+	if !ok {
+		return false, wrap(err, "condition message does not match")
+	}
+
+	return true, nil
+}
+
+// FailureMessage is the message returned to the test when the actual and expected
+// objects do not match.
+func (m matchCondition) FailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("expected\n\t%#v\nto match\n\t%#v\n", actual, m.expected)
+}
+
+// NegatedFailureMessage is the negated version of the FailureMessage.
+func (m matchCondition) NegatedFailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("expected\n\t%#v\nto not match\n\t%#v\n", actual, m.expected)
+}
+
+// errActualTypeMismatchClusterOperatorStatusCondition is used when the type of the actual object does not match
+// the expected type of ClusterOperatorStatusCondition.
+var errActualTypeMismatchClusterOperatorStatusCondition = errors.New("actual should be of type ClusterOperatorStatusCondition")
+
+// MatchClusterOperatorStatusConditions returns a custom matcher to check equality of configv1.ClusterOperatorStatusConditions.
+func MatchClusterOperatorStatusConditions(expected []configv1.ClusterOperatorStatusCondition) types.GomegaMatcher {
+	return &matchClusterOperatorConditions{
+		expected: expected,
+	}
+}
+
+type matchClusterOperatorConditions struct {
+	expected []configv1.ClusterOperatorStatusCondition
+}
+
+// Match checks for equality between the actual and expected objects.
+func (m matchClusterOperatorConditions) Match(actual interface{}) (success bool, err error) {
+	elems := []interface{}{}
+	for _, condition := range m.expected {
+		elems = append(elems, MatchClusterOperatorStatusCondition(condition))
+	}
+
+	ok, err := gomega.ConsistOf(elems).Match(actual)
+	if !ok {
+		return false, wrap(err, "conditions did not match")
+	}
+
+	return true, nil
+}
+
+// FailureMessage is the message returned to the test when the actual and expected
+// objects do not match.
+func (m matchClusterOperatorConditions) FailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("expected\n\t%#v\nto match\n\t%#v\n", actual, m.expected)
+}
+
+// NegatedFailureMessage is the negated version of the FailureMessage.
+func (m matchClusterOperatorConditions) NegatedFailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("expected\n\t%#v\nto not match\n\t%#v\n", actual, m.expected)
+}
+
+// MatchClusterOperatorStatusCondition returns a custom matcher to check equality of configv1.ClusterOperatorStatusCondition.
+func MatchClusterOperatorStatusCondition(expected configv1.ClusterOperatorStatusCondition) types.GomegaMatcher {
+	return &matchClusterOperatorCondition{
+		expected: expected,
+	}
+}
+
+type matchClusterOperatorCondition struct {
+	expected configv1.ClusterOperatorStatusCondition
+}
+
+// Match checks for equality between the actual and expected objects.
+//
+//nolint:dupl
+func (m matchClusterOperatorCondition) Match(actual interface{}) (success bool, err error) {
+	actualCondition, ok := actual.(configv1.ClusterOperatorStatusCondition)
+	if !ok {
+		return false, errActualTypeMismatchClusterOperatorStatusCondition
+	}
+
+	ok, err = gomega.Equal(m.expected.Type).Match(actualCondition.Type)
+	if !ok {
+		return false, wrap(err, "condition type does not match")
+	}
+
+	ok, err = gomega.Equal(m.expected.Status).Match(actualCondition.Status)
+	if !ok {
+		return false, wrap(err, "condition status does not match")
+	}
+
+	ok, err = gomega.Equal(m.expected.Reason).Match(actualCondition.Reason)
+	if !ok {
+		return false, wrap(err, "condition reason does not match")
+	}
+
+	ok, err = gomega.Equal(m.expected.Message).Match(actualCondition.Message)
+	if !ok {
+		return false, wrap(err, "condition message does not match")
+	}
+
+	return true, nil
+}
+
+// FailureMessage is the message returned to the test when the actual and expected
+// objects do not match.
+func (m matchClusterOperatorCondition) FailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("expected\n\t%#v\nto match\n\t%#v\n", actual, m.expected)
+}
+
+// NegatedFailureMessage is the negated version of the FailureMessage.
+func (m matchClusterOperatorCondition) NegatedFailureMessage(actual interface{}) (message string) {
+	return fmt.Sprintf("expected\n\t%#v\nto not match\n\t%#v\n", actual, m.expected)
+}
+
+// wrap wraps an error with the given message if the error isn't nil.
+func wrap(err error, msg string) error {
+	if err == nil {
+		return nil
+	}
+
+	if !strings.HasSuffix(msg, "%w") {
+		msg = fmt.Sprintf("%s: %%w", msg)
+	}
+
+	// We are expecting the passed messages to wrap the error, so skip linting.
+	return fmt.Errorf(msg, err) //nolint:goerr113
+}
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/logger.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/logger.go
new file mode 100644
index 0000000000000000000000000000000000000000..ea36f78319ebf97a925dfdb20384084648beaf81
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/logger.go
@@ -0,0 +1,116 @@
+/*
+Copyright 2022 Red Hat, Inc.
+
+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 testutils
+
+import (
+	"github.com/go-logr/logr"
+)
+
+// LogEntry captures the information about a particular log line.
+type LogEntry struct {
+	// Error is the error passed to an error log line.
+	Error error
+
+	// KeysAndValues are the keys and values that were logged with
+	// the log line.
+	KeysAndValues []interface{}
+
+	// Level is the level of the info log line.
+	Level int
+
+	// Messages is the message from the log line.
+	Message string
+}
+
+// TestLogger provides a logr.Logger and access to a list of log
+// entries logged via the logger.
+type TestLogger interface {
+	Entries() []LogEntry
+	Logger() logr.Logger
+}
+
+// NewTestLogger constructs a new TestLogger.
+func NewTestLogger() TestLogger {
+	l := &testLogger{
+		entries: &[]LogEntry{},
+	}
+	l.logger = logr.New(l)
+
+	return l
+}
+
+// testLogger is an implementation of the TestLogger interface.
+type testLogger struct {
+	entries       *[]LogEntry
+	keysAndValues []interface{}
+	logger        logr.Logger
+	runtimeInfo   logr.RuntimeInfo
+}
+
+// Logger provides the TestLoggers logr.Logger.
+func (t *testLogger) Logger() logr.Logger {
+	return t.logger
+}
+
+// Entries returns the previously logged log entries.
+func (t *testLogger) Entries() []LogEntry {
+	return *t.entries
+}
+
+// Init configures the logr.LogSink implementation.
+func (t *testLogger) Init(info logr.RuntimeInfo) {
+	t.runtimeInfo = info
+}
+
+// Enabled is used to determine whether an info log should be logged.
+func (t *testLogger) Enabled(_ int) bool {
+	// Always return true so that we capture every log line in the test.
+	return true
+}
+
+// Info accepts an info log line.
+func (t *testLogger) Info(level int, msg string, keysAndValues ...interface{}) {
+	*t.entries = append(*t.entries, LogEntry{
+		KeysAndValues: append(t.keysAndValues, keysAndValues...),
+		Level:         level,
+		Message:       msg,
+	})
+}
+
+// Error accepts an error log line.
+func (t *testLogger) Error(err error, msg string, keysAndValues ...interface{}) {
+	*t.entries = append(*t.entries, LogEntry{
+		Error:         err,
+		KeysAndValues: append(t.keysAndValues, keysAndValues...),
+		Message:       msg,
+	})
+}
+
+// WithValues creates a child logger with additional keys and values attached.
+func (t *testLogger) WithValues(keysAndValues ...interface{}) logr.LogSink {
+	return &testLogger{
+		runtimeInfo:   t.runtimeInfo,
+		logger:        t.logger,
+		entries:       t.entries,
+		keysAndValues: append(t.keysAndValues, keysAndValues...),
+	}
+}
+
+// WithName sets the name of the logger. This is not currently supported.
+func (t *testLogger) WithName(name string) logr.LogSink {
+	return t
+}
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/configmap.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/configmap.go
new file mode 100644
index 0000000000000000000000000000000000000000..c99ba7277f2a45e3b95439812e11d2263ea4b803
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/configmap.go
@@ -0,0 +1,92 @@
+/*
+Copyright 2023 Red Hat, Inc.
+
+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 v1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// ConfigMap creates a new ConfigMap builder.
+func ConfigMap() ConfigMapBuilder {
+	return ConfigMapBuilder{}
+}
+
+// ConfigMapBuilder is used to build out a ConfigMap object.
+type ConfigMapBuilder struct {
+	generateName string
+	name         string
+	namespace    string
+	labels       map[string]string
+	data         map[string]string
+}
+
+// Build builds a new ConfigMap based on the configuration provided.
+func (m ConfigMapBuilder) Build() *corev1.ConfigMap {
+	ConfigMap := &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName: m.generateName,
+			Name:         m.name,
+			Namespace:    m.namespace,
+			Labels:       m.labels,
+		},
+		Data: m.data,
+	}
+
+	return ConfigMap
+}
+
+// WithData sets the data for the ConfigMap builder.
+func (m ConfigMapBuilder) WithData(data map[string]string) ConfigMapBuilder {
+	m.data = data
+	return m
+}
+
+// WithGenerateName sets the generateName for the ConfigMap builder.
+func (m ConfigMapBuilder) WithGenerateName(generateName string) ConfigMapBuilder {
+	m.generateName = generateName
+	return m
+}
+
+// WithLabel sets the labels for the ConfigMap builder.
+func (m ConfigMapBuilder) WithLabel(key, value string) ConfigMapBuilder {
+	if m.labels == nil {
+		m.labels = make(map[string]string)
+	}
+
+	m.labels[key] = value
+
+	return m
+}
+
+// WithLabels sets the labels for the ConfigMap builder.
+func (m ConfigMapBuilder) WithLabels(labels map[string]string) ConfigMapBuilder {
+	m.labels = labels
+	return m
+}
+
+// WithName sets the name for the ConfigMap builder.
+func (m ConfigMapBuilder) WithName(name string) ConfigMapBuilder {
+	m.name = name
+	return m
+}
+
+// WithNamespace sets the namespace for the ConfigMap builder.
+func (m ConfigMapBuilder) WithNamespace(namespace string) ConfigMapBuilder {
+	m.namespace = namespace
+	return m
+}
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/namespace.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/namespace.go
new file mode 100644
index 0000000000000000000000000000000000000000..e2cd1f859c510496dd48fcd83dde23c0dedd1781
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/namespace.go
@@ -0,0 +1,55 @@
+/*
+Copyright 2022 Red Hat, Inc.
+
+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 v1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// Namespace creates a new namespace builder.
+func Namespace() NamespaceBuilder {
+	return NamespaceBuilder{}
+}
+
+// NamespaceBuilder is used to build out a namespace object.
+type NamespaceBuilder struct {
+	generateName string
+	name         string
+}
+
+// Build builds a new namespace based on the configuration provided.
+func (n NamespaceBuilder) Build() *corev1.Namespace {
+	return &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName: n.generateName,
+			Name:         n.name,
+		},
+	}
+}
+
+// WithGenerateName sets the generateName for the namespace builder.
+func (n NamespaceBuilder) WithGenerateName(generateName string) NamespaceBuilder {
+	n.generateName = generateName
+	return n
+}
+
+// WithName sets the name for the namespace builder.
+func (n NamespaceBuilder) WithName(name string) NamespaceBuilder {
+	n.name = name
+	return n
+}
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/node.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/node.go
new file mode 100644
index 0000000000000000000000000000000000000000..ff103b767880c7d0092a0e36131b80b6de893562
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/node.go
@@ -0,0 +1,121 @@
+/*
+Copyright 2022 Red Hat, Inc.
+
+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 v1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	masterNodeRoleLabel = "node-role.kubernetes.io/master"
+	workerNodeRoleLabel = "node-role.kubernetes.io/worker"
+)
+
+// Node creates a new node builder.
+func Node() NodeBuilder {
+	return NodeBuilder{}
+}
+
+// NodeBuilder is used to build out a node object.
+type NodeBuilder struct {
+	generateName string
+	name         string
+	labels       map[string]string
+	conditions   []corev1.NodeCondition
+}
+
+// Build builds a new node based on the configuration provided.
+func (m NodeBuilder) Build() *corev1.Node {
+	node := &corev1.Node{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName: m.generateName,
+			Name:         m.name,
+			Labels:       m.labels,
+		},
+		Status: corev1.NodeStatus{
+			Conditions: m.conditions,
+		},
+	}
+
+	return node
+}
+
+// AsWorker sets the worker role on the node labels for the node builder.
+func (m NodeBuilder) AsWorker() NodeBuilder {
+	return m.WithLabel(workerNodeRoleLabel, "")
+}
+
+// AsMaster sets the master role on the node labels for the node builder.
+func (m NodeBuilder) AsMaster() NodeBuilder {
+	return m.WithLabel(masterNodeRoleLabel, "")
+}
+
+// AsNotReady sets the node as ready for the node builder.
+func (m NodeBuilder) AsNotReady() NodeBuilder {
+	return m.WithConditions([]corev1.NodeCondition{
+		{
+			Type:   corev1.NodeReady,
+			Status: corev1.ConditionFalse,
+		},
+	})
+}
+
+// AsReady sets the node as ready for the node builder.
+func (m NodeBuilder) AsReady() NodeBuilder {
+	return m.WithConditions([]corev1.NodeCondition{
+		{
+			Type:   corev1.NodeReady,
+			Status: corev1.ConditionTrue,
+		},
+	})
+}
+
+// WithConditions sets the conditions for the node builder.
+func (m NodeBuilder) WithConditions(conditions []corev1.NodeCondition) NodeBuilder {
+	m.conditions = conditions
+	return m
+}
+
+// WithGenerateName sets the generateName for the node builder.
+func (m NodeBuilder) WithGenerateName(generateName string) NodeBuilder {
+	m.generateName = generateName
+	return m
+}
+
+// WithLabel sets the labels for the node builder.
+func (m NodeBuilder) WithLabel(key, value string) NodeBuilder {
+	if m.labels == nil {
+		m.labels = make(map[string]string)
+	}
+
+	m.labels[key] = value
+
+	return m
+}
+
+// WithLabels sets the labels for the node builder.
+func (m NodeBuilder) WithLabels(labels map[string]string) NodeBuilder {
+	m.labels = labels
+	return m
+}
+
+// WithName sets the name for the node builder.
+func (m NodeBuilder) WithName(name string) NodeBuilder {
+	m.name = name
+	return m
+}
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/secret.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/secret.go
new file mode 100644
index 0000000000000000000000000000000000000000..98d8ce04fb291552a340eb719ed9232b04e6681b
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/secret.go
@@ -0,0 +1,92 @@
+/*
+Copyright 2023 Red Hat, Inc.
+
+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 v1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// Secret creates a new Secret builder.
+func Secret() SecretBuilder {
+	return SecretBuilder{}
+}
+
+// SecretBuilder is used to build out a Secret object.
+type SecretBuilder struct {
+	generateName string
+	name         string
+	namespace    string
+	labels       map[string]string
+	data         map[string][]byte
+}
+
+// Build builds a new Secret based on the configuration provided.
+func (m SecretBuilder) Build() *corev1.Secret {
+	Secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName: m.generateName,
+			Name:         m.name,
+			Namespace:    m.namespace,
+			Labels:       m.labels,
+		},
+		Data: m.data,
+	}
+
+	return Secret
+}
+
+// WithData sets the data for the Secret builder.
+func (m SecretBuilder) WithData(data map[string][]byte) SecretBuilder {
+	m.data = data
+	return m
+}
+
+// WithGenerateName sets the generateName for the Secret builder.
+func (m SecretBuilder) WithGenerateName(generateName string) SecretBuilder {
+	m.generateName = generateName
+	return m
+}
+
+// WithLabel sets the labels for the Secret builder.
+func (m SecretBuilder) WithLabel(key, value string) SecretBuilder {
+	if m.labels == nil {
+		m.labels = make(map[string]string)
+	}
+
+	m.labels[key] = value
+
+	return m
+}
+
+// WithLabels sets the labels for the Secret builder.
+func (m SecretBuilder) WithLabels(labels map[string]string) SecretBuilder {
+	m.labels = labels
+	return m
+}
+
+// WithName sets the name for the Secret builder.
+func (m SecretBuilder) WithName(name string) SecretBuilder {
+	m.name = name
+	return m
+}
+
+// WithNamespace sets the namespace for the Secret builder.
+func (m SecretBuilder) WithNamespace(namespace string) SecretBuilder {
+	m.namespace = namespace
+	return m
+}
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/service.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..6ff97d8d27b38d110fc31aafc8271ca8fd6379ea
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1/service.go
@@ -0,0 +1,102 @@
+/*
+Copyright 2023 Red Hat, Inc.
+
+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 v1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// Service creates a new Service builder.
+func Service() ServiceBuilder {
+	return ServiceBuilder{}
+}
+
+// ServiceBuilder is used to build out a Service object.
+type ServiceBuilder struct {
+	generateName string
+	name         string
+	namespace    string
+	labels       map[string]string
+	selector     map[string]string
+	ports        []corev1.ServicePort
+}
+
+// Build builds a new Service based on the configuration provided.
+func (m ServiceBuilder) Build() *corev1.Service {
+	Service := &corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName: m.generateName,
+			Name:         m.name,
+			Namespace:    m.namespace,
+			Labels:       m.labels,
+		},
+		Spec: corev1.ServiceSpec{
+			Ports:    m.ports,
+			Selector: m.selector,
+		},
+	}
+
+	return Service
+}
+
+// WithGenerateName sets the generateName for the Service builder.
+func (m ServiceBuilder) WithGenerateName(generateName string) ServiceBuilder {
+	m.generateName = generateName
+	return m
+}
+
+// WithLabel sets the labels for the Service builder.
+func (m ServiceBuilder) WithLabel(key, value string) ServiceBuilder {
+	if m.labels == nil {
+		m.labels = make(map[string]string)
+	}
+
+	m.labels[key] = value
+
+	return m
+}
+
+// WithLabels sets the labels for the Service builder.
+func (m ServiceBuilder) WithLabels(labels map[string]string) ServiceBuilder {
+	m.labels = labels
+	return m
+}
+
+// WithName sets the name for the Service builder.
+func (m ServiceBuilder) WithName(name string) ServiceBuilder {
+	m.name = name
+	return m
+}
+
+// WithNamespace sets the namespace for the Service builder.
+func (m ServiceBuilder) WithNamespace(namespace string) ServiceBuilder {
+	m.namespace = namespace
+	return m
+}
+
+// WithPorts sets the ports for the Service builder.
+func (m ServiceBuilder) WithPorts(ports []corev1.ServicePort) ServiceBuilder {
+	m.ports = ports
+	return m
+}
+
+// WithSelector sets the selector for the Service builder.
+func (m ServiceBuilder) WithSelector(selector map[string]string) ServiceBuilder {
+	m.selector = selector
+	return m
+}
diff --git a/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/tools.go b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/tools.go
new file mode 100644
index 0000000000000000000000000000000000000000..3e758e892daaae549886fb33c5f232256d8e7b29
--- /dev/null
+++ b/vendor/github.com/openshift/cluster-api-actuator-pkg/testutils/tools.go
@@ -0,0 +1,13 @@
+//go:build tools
+// +build tools
+
+// Official workaround to track tool dependencies with go modules:
+// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
+
+package tools
+
+import (
+	_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
+	_ "github.com/onsi/ginkgo/v2/ginkgo"
+	_ "sigs.k8s.io/controller-runtime/tools/setup-envtest"
+)
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 19bdbec2c75d181a92bca5edf60440cc8af623e4..b89681e536a7c4922ebca2b35c2842371e4a5030 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -833,8 +833,10 @@ github.com/openshift/client-go/operator/applyconfigurations/internal
 github.com/openshift/client-go/operator/applyconfigurations/operator/v1
 # github.com/openshift/cluster-api-actuator-pkg/testutils v0.0.0-20240626103413-ddea9c7c0aca
 ## explicit; go 1.21
+github.com/openshift/cluster-api-actuator-pkg/testutils
 github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder
 github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/config/v1
+github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/core/v1
 github.com/openshift/cluster-api-actuator-pkg/testutils/resourcebuilder/machine/v1beta1
 # github.com/openshift/cluster-control-plane-machine-set-operator v0.0.0-20240909043600-373ac49835bf
 ## explicit; go 1.22.0