Repository: Azure/ARO-RP
Pull request #1246 (2021-01-13 01:21:49)

Split k8s and az actions
### Which issue this PR addresses:

Fixes https://github.com/Azure/ARO-RP/issues/1061

### What this PR does / why we need it:

Currently, all admin actions only work if k8s clients can be created, even if they don't require the use of k8s clients. In clusters that failed to be created, this may not be the case. If they don't have a value for `PrivateEndpointIP`, restconfig cannot be created. Therefore, the k8s clients can't be created, either. This PR enables us to do things like list Azure resources even if the k8s clients can't be created, which would help debug some failed cluster creations.

### Test plan for issue:

Run existing unit and E2E tests

### Is there any documentation that needs to be updated for this PR?

No.

jim-minter: Aren't Azure geneva actions and k8s geneva actions completely disjoint?
Please could you see if you can separate them so that each is off a separate struct?

Patch:
diff --git a/cmd/aro/rp.go b/cmd/aro/rp.go
index 615e2312c..73bc7c15b 100644
--- a/cmd/aro/rp.go
+++ b/cmd/aro/rp.go
@@ -120,7 +120,7 @@ func rp(ctx context.Context, log *logrus.Entry) error {
 		return err
 	}
 
-	f, err := frontend.NewFrontend(ctx, log.WithField("component", "frontend"), _env, dbAsyncOperations, dbOpenShiftClusters, dbSubscriptions, api.APIs, m, feAead, adminactions.New)
+	f, err := frontend.NewFrontend(ctx, log.WithField("component", "frontend"), _env, dbAsyncOperations, dbOpenShiftClusters, dbSubscriptions, api.APIs, m, feAead, adminactions.NewKubeActions, adminactions.NewAzureActions)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/frontend/admin_openshiftcluster_kubernetesobjects.go b/pkg/frontend/admin_openshiftcluster_kubernetesobjects.go
index 1c25ab435..006181760 100644
--- a/pkg/frontend/admin_openshiftcluster_kubernetesobjects.go
+++ b/pkg/frontend/admin_openshiftcluster_kubernetesobjects.go
@@ -54,15 +54,15 @@ func (f *frontend) _getAdminKubernetesObjects(ctx context.Context, r *http.Reque
 		return nil, err
 	}
 
-	a, err := f.adminActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	k, err := f.kubeActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
 	if err != nil {
 		return nil, err
 	}
 
 	if name != "" {
-		return a.K8sGet(ctx, groupKind, namespace, name)
+		return k.KubeGet(ctx, groupKind, namespace, name)
 	}
-	return a.K8sList(ctx, groupKind, namespace)
+	return k.KubeList(ctx, groupKind, namespace)
 }
 
 func (f *frontend) deleteAdminKubernetesObjects(w http.ResponseWriter, r *http.Request) {
@@ -100,12 +100,12 @@ func (f *frontend) _deleteAdminKubernetesObjects(ctx context.Context, r *http.Re
 		return err
 	}
 
-	a, err := f.adminActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	k, err := f.kubeActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
 	if err != nil {
 		return err
 	}
 
-	return a.K8sDelete(ctx, groupKind, namespace, name)
+	return k.KubeDelete(ctx, groupKind, namespace, name)
 }
 
 func (f *frontend) postAdminKubernetesObjects(w http.ResponseWriter, r *http.Request) {
@@ -154,10 +154,10 @@ func (f *frontend) _postAdminKubernetesObjects(ctx context.Context, r *http.Requ
 		return err
 	}
 
-	a, err := f.adminActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	k, err := f.kubeActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
 	if err != nil {
 		return err
 	}
 
-	return a.K8sCreateOrUpdate(ctx, obj)
+	return k.KubeCreateOrUpdate(ctx, obj)
 }
diff --git a/pkg/frontend/admin_openshiftcluster_redeployvm.go b/pkg/frontend/admin_openshiftcluster_redeployvm.go
index 3a3a91bb2..1e6fe6a68 100644
--- a/pkg/frontend/admin_openshiftcluster_redeployvm.go
+++ b/pkg/frontend/admin_openshiftcluster_redeployvm.go
@@ -51,7 +51,7 @@ func (f *frontend) _postAdminOpenShiftClusterRedeployVM(ctx context.Context, r *
 		return err
 	}
 
-	a, err := f.adminActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	a, err := f.azureActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/frontend/admin_openshiftcluster_resources_list.go b/pkg/frontend/admin_openshiftcluster_resources_list.go
index 262f98073..a3b0490be 100644
--- a/pkg/frontend/admin_openshiftcluster_resources_list.go
+++ b/pkg/frontend/admin_openshiftcluster_resources_list.go
@@ -47,7 +47,7 @@ func (f *frontend) _listAdminOpenShiftClusterResources(
 		return nil, err
 	}
 
-	a, err := f.adminActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	a, err := f.azureActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/frontend/admin_openshiftcluster_serialconsole.go b/pkg/frontend/admin_openshiftcluster_serialconsole.go
index 6fa5bc115..3f9a6a016 100644
--- a/pkg/frontend/admin_openshiftcluster_serialconsole.go
+++ b/pkg/frontend/admin_openshiftcluster_serialconsole.go
@@ -51,7 +51,7 @@ func (f *frontend) _getAdminOpenShiftClusterSerialConsole(ctx context.Context, w
 		return err
 	}
 
-	a, err := f.adminActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	a, err := f.azureActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/frontend/admin_openshiftcluster_upgrade.go b/pkg/frontend/admin_openshiftcluster_upgrade.go
index adc7ea8de..ed995c2f3 100644
--- a/pkg/frontend/admin_openshiftcluster_upgrade.go
+++ b/pkg/frontend/admin_openshiftcluster_upgrade.go
@@ -47,10 +47,10 @@ func (f *frontend) _postAdminOpenShiftUpgrade(ctx context.Context, r *http.Reque
 		return err
 	}
 
-	a, err := f.adminActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	k, err := f.kubeActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
 	if err != nil {
 		return err
 	}
 
-	return a.Upgrade(ctx, upgradeY)
+	return k.Upgrade(ctx, upgradeY)
 }
diff --git a/pkg/frontend/adminactions/adminactions.go b/pkg/frontend/adminactions/adminactions.go
deleted file mode 100644
index 80fbd66c1..000000000
--- a/pkg/frontend/adminactions/adminactions.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package adminactions
-
-// Copyright (c) Microsoft Corporation.
-// Licensed under the Apache License 2.0.
-
-import (
-	"context"
-	"net/http"
-
-	configclient "github.com/openshift/client-go/config/clientset/versioned"
-	"github.com/sirupsen/logrus"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/client-go/kubernetes"
-
-	"github.com/Azure/ARO-RP/pkg/api"
-	"github.com/Azure/ARO-RP/pkg/env"
-	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/compute"
-	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/features"
-	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/network"
-	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/storage"
-	"github.com/Azure/ARO-RP/pkg/util/dynamichelper"
-	"github.com/Azure/ARO-RP/pkg/util/restconfig"
-	"github.com/Azure/ARO-RP/pkg/util/stringutils"
-)
-
-// Interface for adminactions
-type Interface interface {
-	K8sGet(ctx context.Context, groupKind, namespace, name string) ([]byte, error)
-	K8sList(ctx context.Context, groupKind, namespace string) ([]byte, error)
-	K8sCreateOrUpdate(ctx context.Context, obj *unstructured.Unstructured) error
-	K8sDelete(ctx context.Context, groupKind, namespace, name string) error
-	ResourcesList(ctx context.Context) ([]byte, error)
-	Upgrade(ctx context.Context, upgradeY bool) error
-	VMRedeployAndWait(ctx context.Context, vmName string) error
-	VMSerialConsole(ctx context.Context, w http.ResponseWriter,
-		log *logrus.Entry, vmName string) error
-}
-
-type adminactions struct {
-	log *logrus.Entry
-	env env.Interface
-	oc  *api.OpenShiftCluster
-	dh  dynamichelper.Interface
-
-	kubernetescli kubernetes.Interface
-	configcli     configclient.Interface
-
-	resources       features.ResourcesClient
-	virtualMachines compute.VirtualMachinesClient
-	virtualNetworks network.VirtualNetworksClient
-	routeTables     network.RouteTablesClient
-	storageAccounts storage.AccountsClient
-}
-
-// New returns an adminactions Interface
-func New(log *logrus.Entry, env env.Interface, oc *api.OpenShiftCluster,
-	subscriptionDoc *api.SubscriptionDocument) (Interface, error) {
-
-	restConfig, err := restconfig.RestConfig(env, oc)
-	if err != nil {
-		return nil, err
-	}
-
-	dh, err := dynamichelper.New(log, restConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	kubernetescli, err := kubernetes.NewForConfig(restConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	configcli, err := configclient.NewForConfig(restConfig)
-	if err != nil {
-		return nil, err
-	}
-
-	fpAuth, err := env.FPAuthorizer(subscriptionDoc.Subscription.Properties.TenantID,
-		env.Environment().ResourceManagerEndpoint)
-	if err != nil {
-		return nil, err
-	}
-
-	return &adminactions{
-		log: log,
-		env: env,
-		oc:  oc,
-		dh:  dh,
-
-		kubernetescli: kubernetescli,
-		configcli:     configcli,
-
-		resources:       features.NewResourcesClient(env.Environment(), subscriptionDoc.ID, fpAuth),
-		virtualMachines: compute.NewVirtualMachinesClient(env.Environment(), subscriptionDoc.ID, fpAuth),
-		virtualNetworks: network.NewVirtualNetworksClient(env.Environment(), subscriptionDoc.ID, fpAuth),
-		routeTables:     network.NewRouteTablesClient(env.Environment(), subscriptionDoc.ID, fpAuth),
-		storageAccounts: storage.NewAccountsClient(env.Environment(), subscriptionDoc.ID, fpAuth),
-	}, nil
-}
-
-func (a *adminactions) K8sGet(ctx context.Context, groupKind, namespace, name string) ([]byte, error) {
-	un, err := a.dh.Get(ctx, groupKind, namespace, name)
-	if err != nil {
-		return nil, err
-	}
-	return un.MarshalJSON()
-}
-
-func (a *adminactions) K8sList(ctx context.Context, groupKind, namespace string) ([]byte, error) {
-	ul, err := a.dh.List(ctx, groupKind, namespace)
-	if err != nil {
-		return nil, err
-	}
-	return ul.MarshalJSON()
-}
-
-func (a *adminactions) K8sCreateOrUpdate(ctx context.Context, obj *unstructured.Unstructured) error {
-	return a.dh.CreateOrUpdate(ctx, obj)
-}
-
-func (a *adminactions) K8sDelete(ctx context.Context, groupKind, namespace, name string) error {
-	return a.dh.Delete(ctx, groupKind, namespace, name)
-}
-
-func (a *adminactions) VMRedeployAndWait(ctx context.Context, vmName string) error {
-	clusterRGName := stringutils.LastTokenByte(a.oc.Properties.ClusterProfile.ResourceGroupID, '/')
-	return a.virtualMachines.RedeployAndWait(ctx, clusterRGName, vmName)
-}
diff --git a/pkg/frontend/adminactions/azureactions.go b/pkg/frontend/adminactions/azureactions.go
new file mode 100644
index 000000000..7d0e3df6a
--- /dev/null
+++ b/pkg/frontend/adminactions/azureactions.go
@@ -0,0 +1,66 @@
+package adminactions
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"net/http"
+
+	"github.com/sirupsen/logrus"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/compute"
+	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/features"
+	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/network"
+	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/storage"
+	"github.com/Azure/ARO-RP/pkg/util/stringutils"
+)
+
+// AzureActions contains those actions which rely solely on Azure clients, not using any k8s clients
+type AzureActions interface {
+	ResourcesList(ctx context.Context) ([]byte, error)
+	VMRedeployAndWait(ctx context.Context, vmName string) error
+	VMSerialConsole(ctx context.Context, w http.ResponseWriter, log *logrus.Entry, vmName string) error
+}
+
+type azureActions struct {
+	log *logrus.Entry
+	env env.Interface
+	oc  *api.OpenShiftCluster
+
+	resources       features.ResourcesClient
+	virtualMachines compute.VirtualMachinesClient
+	virtualNetworks network.VirtualNetworksClient
+	routeTables     network.RouteTablesClient
+	storageAccounts storage.AccountsClient
+}
+
+// NewAzureActions returns an azureActions
+func NewAzureActions(log *logrus.Entry, env env.Interface, oc *api.OpenShiftCluster,
+	subscriptionDoc *api.SubscriptionDocument) (AzureActions, error) {
+
+	fpAuth, err := env.FPAuthorizer(subscriptionDoc.Subscription.Properties.TenantID,
+		env.Environment().ResourceManagerEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return &azureActions{
+		log: log,
+		env: env,
+		oc:  oc,
+
+		resources:       features.NewResourcesClient(env.Environment(), subscriptionDoc.ID, fpAuth),
+		virtualMachines: compute.NewVirtualMachinesClient(env.Environment(), subscriptionDoc.ID, fpAuth),
+		virtualNetworks: network.NewVirtualNetworksClient(env.Environment(), subscriptionDoc.ID, fpAuth),
+		routeTables:     network.NewRouteTablesClient(env.Environment(), subscriptionDoc.ID, fpAuth),
+		storageAccounts: storage.NewAccountsClient(env.Environment(), subscriptionDoc.ID, fpAuth),
+	}, nil
+}
+
+func (a *azureActions) VMRedeployAndWait(ctx context.Context, vmName string) error {
+	clusterRGName := stringutils.LastTokenByte(a.oc.Properties.ClusterProfile.ResourceGroupID, '/')
+	return a.virtualMachines.RedeployAndWait(ctx, clusterRGName, vmName)
+}
diff --git a/pkg/frontend/adminactions/generate.go b/pkg/frontend/adminactions/generate.go
index eb9b8c6f5..0462f2241 100644
--- a/pkg/frontend/adminactions/generate.go
+++ b/pkg/frontend/adminactions/generate.go
@@ -4,5 +4,5 @@ package adminactions
 // Licensed under the Apache License 2.0.
 
 //go:generate rm -rf ../../util/mocks/$GOPACKAGE
-//go:generate go run ../../../vendor/github.com/golang/mock/mockgen -destination=../../util/mocks/$GOPACKAGE/$GOPACKAGE.go github.com/Azure/ARO-RP/pkg/frontend/$GOPACKAGE Interface
+//go:generate go run ../../../vendor/github.com/golang/mock/mockgen -destination=../../util/mocks/$GOPACKAGE/$GOPACKAGE.go github.com/Azure/ARO-RP/pkg/frontend/$GOPACKAGE KubeActions,AzureActions
 //go:generate go run ../../../vendor/golang.org/x/tools/cmd/goimports -local=github.com/Azure/ARO-RP -e -w ../../util/mocks/$GOPACKAGE/$GOPACKAGE.go
diff --git a/pkg/frontend/adminactions/kubeactions.go b/pkg/frontend/adminactions/kubeactions.go
new file mode 100644
index 000000000..cfe9ca893
--- /dev/null
+++ b/pkg/frontend/adminactions/kubeactions.go
@@ -0,0 +1,95 @@
+package adminactions
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+
+	configclient "github.com/openshift/client-go/config/clientset/versioned"
+	"github.com/sirupsen/logrus"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/network"
+	"github.com/Azure/ARO-RP/pkg/util/dynamichelper"
+	"github.com/Azure/ARO-RP/pkg/util/restconfig"
+)
+
+// KubeActions are those that involve k8s objects, and thus depend upon k8s clients being createable
+type KubeActions interface {
+	KubeGet(ctx context.Context, groupKind, namespace, name string) ([]byte, error)
+	KubeList(ctx context.Context, groupKind, namespace string) ([]byte, error)
+	KubeCreateOrUpdate(ctx context.Context, obj *unstructured.Unstructured) error
+	KubeDelete(ctx context.Context, groupKind, namespace, name string) error
+	Upgrade(ctx context.Context, upgradeY bool) error
+}
+
+type kubeActions struct {
+	log       *logrus.Entry
+	oc        *api.OpenShiftCluster
+	dh        dynamichelper.Interface
+	configcli configclient.Interface
+
+	virtualNetworks network.VirtualNetworksClient
+}
+
+// NewKubeActions returns a kubeActions
+func NewKubeActions(log *logrus.Entry, env env.Interface, oc *api.OpenShiftCluster,
+	subscriptionDoc *api.SubscriptionDocument) (KubeActions, error) {
+
+	restConfig, err := restconfig.RestConfig(env, oc)
+	if err != nil {
+		return nil, err
+	}
+
+	dh, err := dynamichelper.New(log, restConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	configcli, err := configclient.NewForConfig(restConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	fpAuth, err := env.FPAuthorizer(subscriptionDoc.Subscription.Properties.TenantID,
+		env.Environment().ResourceManagerEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return &kubeActions{
+		log:       log,
+		oc:        oc,
+		dh:        dh,
+		configcli: configcli,
+
+		virtualNetworks: network.NewVirtualNetworksClient(env.Environment(), subscriptionDoc.ID, fpAuth),
+	}, nil
+}
+
+func (k *kubeActions) KubeGet(ctx context.Context, groupKind, namespace, name string) ([]byte, error) {
+	un, err := k.dh.Get(ctx, groupKind, namespace, name)
+	if err != nil {
+		return nil, err
+	}
+	return un.MarshalJSON()
+}
+
+func (k *kubeActions) KubeList(ctx context.Context, groupKind, namespace string) ([]byte, error) {
+	ul, err := k.dh.List(ctx, groupKind, namespace)
+	if err != nil {
+		return nil, err
+	}
+	return ul.MarshalJSON()
+}
+
+func (k *kubeActions) KubeCreateOrUpdate(ctx context.Context, obj *unstructured.Unstructured) error {
+	return k.dh.CreateOrUpdate(ctx, obj)
+}
+
+func (k *kubeActions) KubeDelete(ctx context.Context, groupKind, namespace, name string) error {
+	return k.dh.Delete(ctx, groupKind, namespace, name)
+}
diff --git a/pkg/frontend/adminactions/resources_list.go b/pkg/frontend/adminactions/resources_list.go
index eac6269dd..7bb6e6ca6 100644
--- a/pkg/frontend/adminactions/resources_list.go
+++ b/pkg/frontend/adminactions/resources_list.go
@@ -17,7 +17,7 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/subnet"
 )
 
-func (a *adminactions) ResourcesList(ctx context.Context) ([]byte, error) {
+func (a *azureActions) ResourcesList(ctx context.Context) ([]byte, error) {
 	clusterRGName := stringutils.LastTokenByte(a.oc.Properties.ClusterProfile.ResourceGroupID, '/')
 
 	resources, err := a.resources.ListByResourceGroup(ctx, clusterRGName, "", "", nil)
@@ -59,7 +59,7 @@ func (a *adminactions) ResourcesList(ctx context.Context) ([]byte, error) {
 	return json.Marshal(armResources)
 }
 
-func (a *adminactions) appendAzureNetworkResources(ctx context.Context, armResources []arm.Resource) ([]arm.Resource, error) {
+func (a *azureActions) appendAzureNetworkResources(ctx context.Context, armResources []arm.Resource) ([]arm.Resource, error) {
 	vNetID, _, err := subnet.Split(a.oc.Properties.MasterProfile.SubnetID)
 	if err != nil {
 		return nil, err
diff --git a/pkg/frontend/adminactions/upgrade.go b/pkg/frontend/adminactions/upgrade.go
index bd78fe588..28d158943 100644
--- a/pkg/frontend/adminactions/upgrade.go
+++ b/pkg/frontend/adminactions/upgrade.go
@@ -22,13 +22,13 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/version"
 )
 
-func (a *adminactions) Upgrade(ctx context.Context, upgradeY bool) error {
-	err := preUpgradeChecks(ctx, a.oc, a.virtualNetworks)
+func (k *kubeActions) Upgrade(ctx context.Context, upgradeY bool) error {
+	err := preUpgradeChecks(ctx, k.oc, k.virtualNetworks)
 	if err != nil {
 		return err
 	}
 
-	return upgrade(ctx, a.log, a.configcli, version.Streams, upgradeY)
+	return upgrade(ctx, k.log, k.configcli, version.Streams, upgradeY)
 }
 
 func upgrade(ctx context.Context, log *logrus.Entry, configcli configclient.Interface, streams []*version.Stream, upgradeY bool) error {
diff --git a/pkg/frontend/adminactions/serialtool.go b/pkg/frontend/adminactions/vmserialconsole.go
similarity index 97%
rename from pkg/frontend/adminactions/serialtool.go
rename to pkg/frontend/adminactions/vmserialconsole.go
index 07c9601e9..f236417b7 100644
--- a/pkg/frontend/adminactions/serialtool.go
+++ b/pkg/frontend/adminactions/vmserialconsole.go
@@ -21,7 +21,7 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/stringutils"
 )
 
-func (a *adminactions) VMSerialConsole(ctx context.Context, w http.ResponseWriter,
+func (a *azureActions) VMSerialConsole(ctx context.Context, w http.ResponseWriter,
 	log *logrus.Entry, vmName string) error {
 
 	clusterRGName := stringutils.LastTokenByte(a.oc.Properties.ClusterProfile.ResourceGroupID, '/')
diff --git a/pkg/frontend/frontend.go b/pkg/frontend/frontend.go
index 0d680c224..ec3791b19 100644
--- a/pkg/frontend/frontend.go
+++ b/pkg/frontend/frontend.go
@@ -37,8 +37,11 @@ func (err statusCodeError) Error() string {
 	return fmt.Sprintf("%d", err)
 }
 
-type adminActionsFactory func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
-	*api.SubscriptionDocument) (adminactions.Interface, error)
+type kubeActionsFactory func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
+	*api.SubscriptionDocument) (adminactions.KubeActions, error)
+
+type azureActionsFactory func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
+	*api.SubscriptionDocument) (adminactions.AzureActions, error)
 
 type frontend struct {
 	baseLog *logrus.Entry
@@ -53,7 +56,8 @@ type frontend struct {
 	aead encryption.AEAD
 
 	ocEnricher          clusterdata.OpenShiftClusterEnricher
-	adminActionsFactory adminActionsFactory
+	kubeActionsFactory  kubeActionsFactory
+	azureActionsFactory azureActionsFactory
 
 	l net.Listener
 	s *http.Server
@@ -79,7 +83,8 @@ func NewFrontend(ctx context.Context,
 	apis map[string]*api.Version,
 	m metrics.Interface,
 	aead encryption.AEAD,
-	adminActionsFactory adminActionsFactory) (Runnable, error) {
+	kubeActionsFactory kubeActionsFactory,
+	azureActionsFactory azureActionsFactory) (Runnable, error) {
 	f := &frontend{
 		baseLog:             baseLog,
 		env:                 _env,
@@ -89,7 +94,8 @@ func NewFrontend(ctx context.Context,
 		apis:                apis,
 		m:                   m,
 		aead:                aead,
-		adminActionsFactory: adminActionsFactory,
+		kubeActionsFactory:  kubeActionsFactory,
+		azureActionsFactory: azureActionsFactory,
 
 		ocEnricher: clusterdata.NewBestEffortEnricher(baseLog, _env, m),
 
diff --git a/pkg/util/mocks/adminactions/adminactions.go b/pkg/util/mocks/adminactions/adminactions.go
index e48e5dc2d..3bb3a628b 100644
--- a/pkg/util/mocks/adminactions/adminactions.go
+++ b/pkg/util/mocks/adminactions/adminactions.go
@@ -1,5 +1,5 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/Azure/ARO-RP/pkg/frontend/adminactions (interfaces: Interface)
+// Source: github.com/Azure/ARO-RP/pkg/frontend/adminactions (interfaces: KubeActions,AzureActions)
 
 // Package mock_adminactions is a generated GoMock package.
 package mock_adminactions
@@ -14,118 +14,141 @@ import (
 	unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
-// MockInterface is a mock of Interface interface
-type MockInterface struct {
+// MockKubeActions is a mock of KubeActions interface
+type MockKubeActions struct {
 	ctrl     *gomock.Controller
-	recorder *MockInterfaceMockRecorder
+	recorder *MockKubeActionsMockRecorder
 }
 
-// MockInterfaceMockRecorder is the mock recorder for MockInterface
-type MockInterfaceMockRecorder struct {
-	mock *MockInterface
+// MockKubeActionsMockRecorder is the mock recorder for MockKubeActions
+type MockKubeActionsMockRecorder struct {
+	mock *MockKubeActions
 }
 
-// NewMockInterface creates a new mock instance
-func NewMockInterface(ctrl *gomock.Controller) *MockInterface {
-	mock := &MockInterface{ctrl: ctrl}
-	mock.recorder = &MockInterfaceMockRecorder{mock}
+// NewMockKubeActions creates a new mock instance
+func NewMockKubeActions(ctrl *gomock.Controller) *MockKubeActions {
+	mock := &MockKubeActions{ctrl: ctrl}
+	mock.recorder = &MockKubeActionsMockRecorder{mock}
 	return mock
 }
 
 // EXPECT returns an object that allows the caller to indicate expected use
-func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
+func (m *MockKubeActions) EXPECT() *MockKubeActionsMockRecorder {
 	return m.recorder
 }
 
-// K8sCreateOrUpdate mocks base method
-func (m *MockInterface) K8sCreateOrUpdate(arg0 context.Context, arg1 *unstructured.Unstructured) error {
+// KubeCreateOrUpdate mocks base method
+func (m *MockKubeActions) KubeCreateOrUpdate(arg0 context.Context, arg1 *unstructured.Unstructured) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "K8sCreateOrUpdate", arg0, arg1)
+	ret := m.ctrl.Call(m, "KubeCreateOrUpdate", arg0, arg1)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// K8sCreateOrUpdate indicates an expected call of K8sCreateOrUpdate
-func (mr *MockInterfaceMockRecorder) K8sCreateOrUpdate(arg0, arg1 interface{}) *gomock.Call {
+// KubeCreateOrUpdate indicates an expected call of KubeCreateOrUpdate
+func (mr *MockKubeActionsMockRecorder) KubeCreateOrUpdate(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "K8sCreateOrUpdate", reflect.TypeOf((*MockInterface)(nil).K8sCreateOrUpdate), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KubeCreateOrUpdate", reflect.TypeOf((*MockKubeActions)(nil).KubeCreateOrUpdate), arg0, arg1)
 }
 
-// K8sDelete mocks base method
-func (m *MockInterface) K8sDelete(arg0 context.Context, arg1, arg2, arg3 string) error {
+// KubeDelete mocks base method
+func (m *MockKubeActions) KubeDelete(arg0 context.Context, arg1, arg2, arg3 string) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "K8sDelete", arg0, arg1, arg2, arg3)
+	ret := m.ctrl.Call(m, "KubeDelete", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].(error)
 	return ret0
 }
 
-// K8sDelete indicates an expected call of K8sDelete
-func (mr *MockInterfaceMockRecorder) K8sDelete(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+// KubeDelete indicates an expected call of KubeDelete
+func (mr *MockKubeActionsMockRecorder) KubeDelete(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "K8sDelete", reflect.TypeOf((*MockInterface)(nil).K8sDelete), arg0, arg1, arg2, arg3)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KubeDelete", reflect.TypeOf((*MockKubeActions)(nil).KubeDelete), arg0, arg1, arg2, arg3)
 }
 
-// K8sGet mocks base method
-func (m *MockInterface) K8sGet(arg0 context.Context, arg1, arg2, arg3 string) ([]byte, error) {
+// KubeGet mocks base method
+func (m *MockKubeActions) KubeGet(arg0 context.Context, arg1, arg2, arg3 string) ([]byte, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "K8sGet", arg0, arg1, arg2, arg3)
+	ret := m.ctrl.Call(m, "KubeGet", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].([]byte)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// K8sGet indicates an expected call of K8sGet
-func (mr *MockInterfaceMockRecorder) K8sGet(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+// KubeGet indicates an expected call of KubeGet
+func (mr *MockKubeActionsMockRecorder) KubeGet(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "K8sGet", reflect.TypeOf((*MockInterface)(nil).K8sGet), arg0, arg1, arg2, arg3)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KubeGet", reflect.TypeOf((*MockKubeActions)(nil).KubeGet), arg0, arg1, arg2, arg3)
 }
 
-// K8sList mocks base method
-func (m *MockInterface) K8sList(arg0 context.Context, arg1, arg2 string) ([]byte, error) {
+// KubeList mocks base method
+func (m *MockKubeActions) KubeList(arg0 context.Context, arg1, arg2 string) ([]byte, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "K8sList", arg0, arg1, arg2)
+	ret := m.ctrl.Call(m, "KubeList", arg0, arg1, arg2)
 	ret0, _ := ret[0].([]byte)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
-// K8sList indicates an expected call of K8sList
-func (mr *MockInterfaceMockRecorder) K8sList(arg0, arg1, arg2 interface{}) *gomock.Call {
+// KubeList indicates an expected call of KubeList
+func (mr *MockKubeActionsMockRecorder) KubeList(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "K8sList", reflect.TypeOf((*MockInterface)(nil).K8sList), arg0, arg1, arg2)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KubeList", reflect.TypeOf((*MockKubeActions)(nil).KubeList), arg0, arg1, arg2)
 }
 
-// ResourcesList mocks base method
-func (m *MockInterface) ResourcesList(arg0 context.Context) ([]byte, error) {
+// Upgrade mocks base method
+func (m *MockKubeActions) Upgrade(arg0 context.Context, arg1 bool) error {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ResourcesList", arg0)
-	ret0, _ := ret[0].([]byte)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
+	ret := m.ctrl.Call(m, "Upgrade", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
 }
 
-// ResourcesList indicates an expected call of ResourcesList
-func (mr *MockInterfaceMockRecorder) ResourcesList(arg0 interface{}) *gomock.Call {
+// Upgrade indicates an expected call of Upgrade
+func (mr *MockKubeActionsMockRecorder) Upgrade(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourcesList", reflect.TypeOf((*MockInterface)(nil).ResourcesList), arg0)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upgrade", reflect.TypeOf((*MockKubeActions)(nil).Upgrade), arg0, arg1)
 }
 
-// Upgrade mocks base method
-func (m *MockInterface) Upgrade(arg0 context.Context, arg1 bool) error {
+// MockAzureActions is a mock of AzureActions interface
+type MockAzureActions struct {
+	ctrl     *gomock.Controller
+	recorder *MockAzureActionsMockRecorder
+}
+
+// MockAzureActionsMockRecorder is the mock recorder for MockAzureActions
+type MockAzureActionsMockRecorder struct {
+	mock *MockAzureActions
+}
+
+// NewMockAzureActions creates a new mock instance
+func NewMockAzureActions(ctrl *gomock.Controller) *MockAzureActions {
+	mock := &MockAzureActions{ctrl: ctrl}
+	mock.recorder = &MockAzureActionsMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockAzureActions) EXPECT() *MockAzureActionsMockRecorder {
+	return m.recorder
+}
+
+// ResourcesList mocks base method
+func (m *MockAzureActions) ResourcesList(arg0 context.Context) ([]byte, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Upgrade", arg0, arg1)
-	ret0, _ := ret[0].(error)
-	return ret0
+	ret := m.ctrl.Call(m, "ResourcesList", arg0)
+	ret0, _ := ret[0].([]byte)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
 }
 
-// Upgrade indicates an expected call of Upgrade
-func (mr *MockInterfaceMockRecorder) Upgrade(arg0, arg1 interface{}) *gomock.Call {
+// ResourcesList indicates an expected call of ResourcesList
+func (mr *MockAzureActionsMockRecorder) ResourcesList(arg0 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upgrade", reflect.TypeOf((*MockInterface)(nil).Upgrade), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourcesList", reflect.TypeOf((*MockAzureActions)(nil).ResourcesList), arg0)
 }
 
 // VMRedeployAndWait mocks base method
-func (m *MockInterface) VMRedeployAndWait(arg0 context.Context, arg1 string) error {
+func (m *MockAzureActions) VMRedeployAndWait(arg0 context.Context, arg1 string) error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "VMRedeployAndWait", arg0, arg1)
 	ret0, _ := ret[0].(error)
@@ -133,13 +156,13 @@ func (m *MockInterface) VMRedeployAndWait(arg0 context.Context, arg1 string) err
 }
 
 // VMRedeployAndWait indicates an expected call of VMRedeployAndWait
-func (mr *MockInterfaceMockRecorder) VMRedeployAndWait(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockAzureActionsMockRecorder) VMRedeployAndWait(arg0, arg1 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMRedeployAndWait", reflect.TypeOf((*MockInterface)(nil).VMRedeployAndWait), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMRedeployAndWait", reflect.TypeOf((*MockAzureActions)(nil).VMRedeployAndWait), arg0, arg1)
 }
 
 // VMSerialConsole mocks base method
-func (m *MockInterface) VMSerialConsole(arg0 context.Context, arg1 http.ResponseWriter, arg2 *logrus.Entry, arg3 string) error {
+func (m *MockAzureActions) VMSerialConsole(arg0 context.Context, arg1 http.ResponseWriter, arg2 *logrus.Entry, arg3 string) error {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "VMSerialConsole", arg0, arg1, arg2, arg3)
 	ret0, _ := ret[0].(error)
@@ -147,7 +170,7 @@ func (m *MockInterface) VMSerialConsole(arg0 context.Context, arg1 http.Response
 }
 
 // VMSerialConsole indicates an expected call of VMSerialConsole
-func (mr *MockInterfaceMockRecorder) VMSerialConsole(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+func (mr *MockAzureActionsMockRecorder) VMSerialConsole(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMSerialConsole", reflect.TypeOf((*MockInterface)(nil).VMSerialConsole), arg0, arg1, arg2, arg3)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMSerialConsole", reflect.TypeOf((*MockAzureActions)(nil).VMSerialConsole), arg0, arg1, arg2, arg3)
 }


Test patch:
diff --git a/pkg/frontend/admin_openshiftcluster_kubernetesobjects_test.go b/pkg/frontend/admin_openshiftcluster_kubernetesobjects_test.go
index 520e23479..fb6d3d57a 100644
--- a/pkg/frontend/admin_openshiftcluster_kubernetesobjects_test.go
+++ b/pkg/frontend/admin_openshiftcluster_kubernetesobjects_test.go
@@ -32,7 +32,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 		objKind        string
 		objNamespace   string
 		objName        string
-		mocks          func(*test, *mock_adminactions.MockInterface)
+		mocks          func(*test, *mock_adminactions.MockKubeActions)
 		method         string
 		wantStatusCode int
 		wantResponse   []byte
@@ -47,9 +47,9 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			objKind:      "ConfigMap",
 			objNamespace: "openshift-project",
 			objName:      "config",
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
-				a.EXPECT().
-					K8sGet(gomock.Any(), tt.objKind, tt.objNamespace, tt.objName).
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
+				k.EXPECT().
+					KubeGet(gomock.Any(), tt.objKind, tt.objNamespace, tt.objName).
 					Return([]byte(`{"Kind": "test"}`), nil)
 
 			},
@@ -62,9 +62,9 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objKind:      "ConfigMap",
 			objNamespace: "openshift-project",
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
-				a.EXPECT().
-					K8sList(gomock.Any(), tt.objKind, tt.objNamespace).
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
+				k.EXPECT().
+					KubeList(gomock.Any(), tt.objKind, tt.objNamespace).
 					Return([]byte(`{"Kind": "test"}`), nil)
 
 			},
@@ -77,7 +77,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objNamespace: "openshift-project",
 			objName:      "config",
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
 			wantStatusCode: http.StatusBadRequest,
 			wantError:      "400: InvalidParameter: : The provided groupKind '' is invalid.",
@@ -89,7 +89,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			objKind:      "Secret",
 			objNamespace: "openshift-project",
 			objName:      "config",
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
 			wantStatusCode: http.StatusForbidden,
 			wantError:      "403: Forbidden: : Access to secrets is forbidden.",
@@ -101,9 +101,9 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			objKind:      "ConfigMap",
 			objNamespace: "openshift-project",
 			objName:      "config",
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
-				a.EXPECT().
-					K8sDelete(gomock.Any(), tt.objKind, tt.objNamespace, tt.objName).
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
+				k.EXPECT().
+					KubeDelete(gomock.Any(), tt.objKind, tt.objNamespace, tt.objName).
 					Return(nil)
 
 			},
@@ -115,7 +115,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objNamespace: "openshift-project",
 			objName:      "config",
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
 			wantStatusCode: http.StatusBadRequest,
 			wantError:      "400: InvalidParameter: : The provided groupKind '' is invalid.",
@@ -126,7 +126,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objKind:      "this",
 			objNamespace: "openshift-project",
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
 			wantStatusCode: http.StatusBadRequest,
 			wantError:      "400: InvalidParameter: : The provided name '' is invalid.",
@@ -138,7 +138,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			objKind:      "Secret",
 			objNamespace: "openshift-project",
 			objName:      "config",
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
 			wantStatusCode: http.StatusForbidden,
 			wantError:      "403: Forbidden: : Access to secrets is forbidden.",
@@ -148,8 +148,8 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			ti := newTestInfra(t).WithOpenShiftClusters().WithSubscriptions()
 			defer ti.done()
 
-			a := mock_adminactions.NewMockInterface(ti.controller)
-			tt.mocks(tt, a)
+			k := mock_adminactions.NewMockKubeActions(ti.controller)
+			tt.mocks(tt, k)
 
 			ti.fixture.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
 				Key: strings.ToLower(tt.resourceID),
@@ -175,9 +175,9 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			}
 
 			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
-				*api.SubscriptionDocument) (adminactions.Interface, error) {
-				return a, nil
-			})
+				*api.SubscriptionDocument) (adminactions.KubeActions, error) {
+				return k, nil
+			}, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -301,7 +301,7 @@ func TestAdminPostKubernetesObjects(t *testing.T) {
 	type test struct {
 		name           string
 		resourceID     string
-		mocks          func(*test, *mock_adminactions.MockInterface)
+		mocks          func(*test, *mock_adminactions.MockKubeActions)
 		wantStatusCode int
 		objInBody      *unstructured.Unstructured
 		wantError      string
@@ -320,8 +320,8 @@ func TestAdminPostKubernetesObjects(t *testing.T) {
 					},
 				},
 			},
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
-				a.EXPECT().K8sCreateOrUpdate(gomock.Any(), tt.objInBody).
+			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
+				k.EXPECT().KubeCreateOrUpdate(gomock.Any(), tt.objInBody).
 					Return(nil)
 			},
 			wantStatusCode: http.StatusOK,
@@ -334,7 +334,7 @@ func TestAdminPostKubernetesObjects(t *testing.T) {
 					"kind": "Secret",
 				},
 			},
-			mocks:          func(tt *test, a *mock_adminactions.MockInterface) {},
+			mocks:          func(tt *test, k *mock_adminactions.MockKubeActions) {},
 			wantStatusCode: http.StatusForbidden,
 			wantError:      "403: Forbidden: : Access to secrets is forbidden.",
 		},
@@ -343,8 +343,8 @@ func TestAdminPostKubernetesObjects(t *testing.T) {
 			ti := newTestInfra(t).WithOpenShiftClusters().WithSubscriptions()
 			defer ti.done()
 
-			a := mock_adminactions.NewMockInterface(ti.controller)
-			tt.mocks(tt, a)
+			k := mock_adminactions.NewMockKubeActions(ti.controller)
+			tt.mocks(tt, k)
 
 			ti.fixture.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
 				Key: strings.ToLower(tt.resourceID),
@@ -370,9 +370,9 @@ func TestAdminPostKubernetesObjects(t *testing.T) {
 			}
 
 			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
-				*api.SubscriptionDocument) (adminactions.Interface, error) {
-				return a, nil
-			})
+				*api.SubscriptionDocument) (adminactions.KubeActions, error) {
+				return k, nil
+			}, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/frontend/admin_openshiftcluster_list_test.go b/pkg/frontend/admin_openshiftcluster_list_test.go
index c2781e0f6..5ac1e614e 100644
--- a/pkg/frontend/admin_openshiftcluster_list_test.go
+++ b/pkg/frontend/admin_openshiftcluster_list_test.go
@@ -118,7 +118,7 @@ func TestAdminListOpenShiftCluster(t *testing.T) {
 				ti.openShiftClustersClient.SetError(tt.throwsError)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, aead, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, aead, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/frontend/admin_openshiftcluster_redeployvm_test.go b/pkg/frontend/admin_openshiftcluster_redeployvm_test.go
index a5dc1a734..df7190057 100644
--- a/pkg/frontend/admin_openshiftcluster_redeployvm_test.go
+++ b/pkg/frontend/admin_openshiftcluster_redeployvm_test.go
@@ -32,7 +32,7 @@ func TestAdminRedeployVM(t *testing.T) {
 		resourceID     string
 		fixture        func(*testdatabase.Fixture)
 		vmName         string
-		mocks          func(*test, *mock_adminactions.MockInterface)
+		mocks          func(*test, *mock_adminactions.MockAzureActions)
 		wantStatusCode int
 		wantResponse   []byte
 		wantError      string
@@ -66,7 +66,7 @@ func TestAdminRedeployVM(t *testing.T) {
 					},
 				})
 			},
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
+			mocks: func(tt *test, a *mock_adminactions.MockAzureActions) {
 				a.EXPECT().VMRedeployAndWait(gomock.Any(), tt.vmName).Return(nil)
 			},
 			wantStatusCode: http.StatusOK,
@@ -76,7 +76,7 @@ func TestAdminRedeployVM(t *testing.T) {
 			ti := newTestInfra(t).WithOpenShiftClusters().WithSubscriptions()
 			defer ti.done()
 
-			a := mock_adminactions.NewMockInterface(ti.controller)
+			a := mock_adminactions.NewMockAzureActions(ti.controller)
 			tt.mocks(tt, a)
 
 			err := ti.buildFixtures(tt.fixture)
@@ -84,8 +84,8 @@ func TestAdminRedeployVM(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
-				*api.SubscriptionDocument) (adminactions.Interface, error) {
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil, func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
+				*api.SubscriptionDocument) (adminactions.AzureActions, error) {
 				return a, nil
 			})
 
diff --git a/pkg/frontend/admin_openshiftcluster_resources_list_test.go b/pkg/frontend/admin_openshiftcluster_resources_list_test.go
index 9ba1f7ec9..fbcd7bcf2 100644
--- a/pkg/frontend/admin_openshiftcluster_resources_list_test.go
+++ b/pkg/frontend/admin_openshiftcluster_resources_list_test.go
@@ -30,7 +30,7 @@ func TestAdminListResourcesList(t *testing.T) {
 		name           string
 		resourceID     string
 		fixture        func(f *testdatabase.Fixture)
-		mocks          func(*test, *mock_adminactions.MockInterface)
+		mocks          func(*test, *mock_adminactions.MockAzureActions)
 		wantStatusCode int
 		wantResponse   []byte
 		wantError      string
@@ -65,7 +65,7 @@ func TestAdminListResourcesList(t *testing.T) {
 					},
 				})
 			},
-			mocks: func(tt *test, a *mock_adminactions.MockInterface) {
+			mocks: func(tt *test, a *mock_adminactions.MockAzureActions) {
 
 				a.EXPECT().
 					ResourcesList(gomock.Any()).
@@ -80,7 +80,7 @@ func TestAdminListResourcesList(t *testing.T) {
 			ti := newTestInfra(t).WithSubscriptions().WithOpenShiftClusters()
 			defer ti.done()
 
-			a := mock_adminactions.NewMockInterface(ti.controller)
+			a := mock_adminactions.NewMockAzureActions(ti.controller)
 			tt.mocks(tt, a)
 
 			err := ti.buildFixtures(tt.fixture)
@@ -88,8 +88,8 @@ func TestAdminListResourcesList(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
-				*api.SubscriptionDocument) (adminactions.Interface, error) {
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil, func(*logrus.Entry, env.Interface, *api.OpenShiftCluster,
+				*api.SubscriptionDocument) (adminactions.AzureActions, error) {
 				return a, nil
 			})
 
diff --git a/pkg/frontend/adminactions/resources_list_test.go b/pkg/frontend/adminactions/resources_list_test.go
index d7f80474e..b26cae8b9 100644
--- a/pkg/frontend/adminactions/resources_list_test.go
+++ b/pkg/frontend/adminactions/resources_list_test.go
@@ -143,7 +143,7 @@ func TestResourcesList(t *testing.T) {
 			routeTables := mock_network.NewMockRouteTablesClient(controller)
 			tt.mocks(tt, resources, virtualMachines, virtualNetworks, routeTables)
 
-			a := adminactions{
+			a := azureActions{
 				log: logrus.NewEntry(logrus.StandardLogger()),
 				env: env,
 				oc: &api.OpenShiftCluster{
diff --git a/pkg/frontend/adminactions/upgrade_test.go b/pkg/frontend/adminactions/upgrade_test.go
index 1ab153108..953a75b65 100644
--- a/pkg/frontend/adminactions/upgrade_test.go
+++ b/pkg/frontend/adminactions/upgrade_test.go
@@ -173,12 +173,12 @@ func TestUpgradeCluster(t *testing.T) {
 				return false, nil, nil
 			})
 
-			a := &adminactions{
+			k := &kubeActions{
 				log:       logrus.NewEntry(logrus.StandardLogger()),
 				configcli: tt.fakecli,
 			}
 
-			err := upgrade(ctx, a.log, a.configcli, []*version.Stream{stream43, stream44, stream45}, tt.upgradeY)
+			err := upgrade(ctx, k.log, k.configcli, []*version.Stream{stream43, stream44, stream45}, tt.upgradeY)
 			if err != nil && err.Error() != tt.wantErr ||
 				err == nil && tt.wantErr != "" {
 				t.Error(err)
@@ -188,7 +188,7 @@ func TestUpgradeCluster(t *testing.T) {
 				t.Fatal(updated)
 			}
 
-			cv, err := a.configcli.ConfigV1().ClusterVersions().Get(ctx, "version", metav1.GetOptions{})
+			cv, err := k.configcli.ConfigV1().ClusterVersions().Get(ctx, "version", metav1.GetOptions{})
 			if err != nil {
 				t.Error(err)
 			}
@@ -252,7 +252,7 @@ func TestCheckCustomDNS(t *testing.T) {
 				tt.mocks(virtualNetworks)
 			}
 
-			a := &adminactions{
+			k := &kubeActions{
 				log:             logrus.NewEntry(logrus.StandardLogger()),
 				virtualNetworks: virtualNetworks,
 			}
@@ -265,7 +265,7 @@ func TestCheckCustomDNS(t *testing.T) {
 				},
 			}
 
-			err := checkCustomDNS(ctx, oc, a.virtualNetworks)
+			err := checkCustomDNS(ctx, oc, k.virtualNetworks)
 			if err != nil && err.Error() != tt.wantErr ||
 				err == nil && tt.wantErr != "" {
 				t.Error(err)
diff --git a/pkg/frontend/asyncoperationresult_get_test.go b/pkg/frontend/asyncoperationresult_get_test.go
index 514d89466..247e323ef 100644
--- a/pkg/frontend/asyncoperationresult_get_test.go
+++ b/pkg/frontend/asyncoperationresult_get_test.go
@@ -122,7 +122,7 @@ func TestGetAsyncOperationResult(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/frontend/asyncoperationsstatus_get_test.go b/pkg/frontend/asyncoperationsstatus_get_test.go
index 2854bc964..9d09a073e 100644
--- a/pkg/frontend/asyncoperationsstatus_get_test.go
+++ b/pkg/frontend/asyncoperationsstatus_get_test.go
@@ -160,7 +160,7 @@ func TestGetAsyncOperationsStatus(t *testing.T) {
 				ti.asyncOperationsClient.SetError(tt.dbError)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/frontend/openshiftcluster_delete_test.go b/pkg/frontend/openshiftcluster_delete_test.go
index 0f3095d3d..14063735a 100644
--- a/pkg/frontend/openshiftcluster_delete_test.go
+++ b/pkg/frontend/openshiftcluster_delete_test.go
@@ -114,7 +114,7 @@ func TestDeleteOpenShiftCluster(t *testing.T) {
 				ti.subscriptionsClient.SetError(tt.dbError)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/frontend/openshiftcluster_get_test.go b/pkg/frontend/openshiftcluster_get_test.go
index 617b592ca..52d68e079 100644
--- a/pkg/frontend/openshiftcluster_get_test.go
+++ b/pkg/frontend/openshiftcluster_get_test.go
@@ -92,7 +92,7 @@ func TestGetOpenShiftCluster(t *testing.T) {
 				ti.openShiftClustersClient.SetError(tt.dbError)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/frontend/openshiftcluster_list_test.go b/pkg/frontend/openshiftcluster_list_test.go
index 5217e52ee..21f55f582 100644
--- a/pkg/frontend/openshiftcluster_list_test.go
+++ b/pkg/frontend/openshiftcluster_list_test.go
@@ -193,7 +193,7 @@ func TestListOpenShiftCluster(t *testing.T) {
 
 					aead := testdatabase.NewFakeAEAD()
 
-					f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, aead, nil)
+					f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, aead, nil, nil)
 					if err != nil {
 						t.Fatal(err)
 					}
diff --git a/pkg/frontend/openshiftcluster_putorpatch_test.go b/pkg/frontend/openshiftcluster_putorpatch_test.go
index 3f8e80274..013356996 100644
--- a/pkg/frontend/openshiftcluster_putorpatch_test.go
+++ b/pkg/frontend/openshiftcluster_putorpatch_test.go
@@ -203,7 +203,7 @@ func TestPutOrPatchOpenShiftClusterAdminAPI(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, apis, &noop.Noop{}, nil, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, apis, &noop.Noop{}, nil, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -872,7 +872,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, apis, &noop.Noop{}, nil, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, apis, &noop.Noop{}, nil, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/frontend/openshiftclustercredentials_post_test.go b/pkg/frontend/openshiftclustercredentials_post_test.go
index 8524e33ec..dcda300d8 100644
--- a/pkg/frontend/openshiftclustercredentials_post_test.go
+++ b/pkg/frontend/openshiftclustercredentials_post_test.go
@@ -266,7 +266,7 @@ func TestPostOpenShiftClusterCredentials(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, apis, &noop.Noop{}, nil, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, apis, &noop.Noop{}, nil, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}
diff --git a/pkg/frontend/security_test.go b/pkg/frontend/security_test.go
index e42cd4451..ada6d2de5 100644
--- a/pkg/frontend/security_test.go
+++ b/pkg/frontend/security_test.go
@@ -68,7 +68,7 @@ func TestSecurity(t *testing.T) {
 	pool := x509.NewCertPool()
 	pool.AddCert(servercerts[0])
 
-	f, err := NewFrontend(ctx, logrus.NewEntry(logrus.StandardLogger()), _env, nil, nil, nil, api.APIs, &noop.Noop{}, nil, nil)
+	f, err := NewFrontend(ctx, logrus.NewEntry(logrus.StandardLogger()), _env, nil, nil, nil, api.APIs, &noop.Noop{}, nil, nil, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/pkg/frontend/subscriptions_put_test.go b/pkg/frontend/subscriptions_put_test.go
index 532a422cb..d6d495db5 100644
--- a/pkg/frontend/subscriptions_put_test.go
+++ b/pkg/frontend/subscriptions_put_test.go
@@ -244,7 +244,7 @@ func TestPutSubscription(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil)
+			f, err := NewFrontend(ctx, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil, nil, nil)
 			if err != nil {
 				t.Fatal(err)
 			}