Repository: Azure/ARO-RP
Pull request #1889 (2022-01-04 18:14:36)

Geneva master resize
### Which issue this PR addresses:

Net new creation of ARO cluster master resize geneva action


### What this PR does / why we need it:

This work will allow us to leverage geneva actions to resize the compute assigned to ARO master nodes. 

### Test plan for issue:

Functionality can be tested through both direct calls to the RP as well as the pre-approved Geneva environment mock setup

### Is there any documentation that needs to be updated for this PR?

Documentation and SOP will be provided once process (and PR) has been solidified. 


jhoreman: This is blocked on https://github.com/Azure/ARO-RP/pull/1839
github-actions[bot]: Please rebase pull request.
bennerv: /azp run ci 
azure-pipelines[bot]: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
github-actions[bot]: Please rebase pull request.
jhoreman: /azp run e2e
azure-pipelines[bot]: <samp>
Pull request contains merge conflicts.<br>

</samp>

Patch:
diff --git a/docs/upstream-differences.md b/docs/upstream-differences.md
index 1dbc92a10..f42a9a2c3 100644
--- a/docs/upstream-differences.md
+++ b/docs/upstream-differences.md
@@ -88,3 +88,5 @@ Once installer fork is ready:
 1. After this point, you should be able to create a dev cluster using the RP and it should use the new release.
 1. `make discoverycache`.
     * This command requires a running cluster with the new version.
+1. The list of the hard-coded namespaces in `pkg/util/namespace/namespace.go` needs to be updated regularly as every
+   minor version of upstream OCP introduces a new namespace or two.
\ No newline at end of file
diff --git a/pkg/cluster/deployresources_resources.go b/pkg/cluster/deployresources_resources.go
index 14b716ba0..12db8fd0e 100644
--- a/pkg/cluster/deployresources_resources.go
+++ b/pkg/cluster/deployresources_resources.go
@@ -33,7 +33,7 @@ func (m *manager) networkBootstrapNIC(installConfig *installconfig.InstallConfig
 								},
 							},
 							Subnet: &mgmtnetwork.Subnet{
-								ID: to.StringPtr(m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID),
+								ID: &m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID,
 							},
 						},
 						Name: to.StringPtr("bootstrap-nic-ip-v4"),
@@ -67,7 +67,7 @@ func (m *manager) networkMasterNICs(installConfig *installconfig.InstallConfig)
 								},
 							},
 							Subnet: &mgmtnetwork.Subnet{
-								ID: to.StringPtr(m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID),
+								ID: &m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID,
 							},
 						},
 						Name: to.StringPtr("pipConfig"),
diff --git a/pkg/cluster/deploystorage.go b/pkg/cluster/deploystorage.go
index cb0e4a923..846a1f9ff 100644
--- a/pkg/cluster/deploystorage.go
+++ b/pkg/cluster/deploystorage.go
@@ -69,7 +69,7 @@ func (m *manager) ensureResourceGroup(ctx context.Context) error {
 
 	group := mgmtfeatures.ResourceGroup{
 		Location:  &m.doc.OpenShiftCluster.Location,
-		ManagedBy: to.StringPtr(m.doc.OpenShiftCluster.ID),
+		ManagedBy: &m.doc.OpenShiftCluster.ID,
 	}
 	if m.env.IsLocalDevelopmentMode() {
 		// grab tags so we do not accidently remove them on createOrUpdate, set purge tag to true for dev clusters
@@ -124,29 +124,30 @@ func (m *manager) ensureResourceGroup(ctx context.Context) error {
 	return m.env.EnsureARMResourceGroupRoleAssignment(ctx, m.fpAuthorizer, resourceGroup)
 }
 
-func (m *manager) deployStorageTemplate(ctx context.Context, installConfig *installconfig.InstallConfig) error {
+func (m *manager) deployStorageTemplate(ctx context.Context) error {
 	resourceGroup := stringutils.LastTokenByte(m.doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID, '/')
 	infraID := m.doc.OpenShiftCluster.Properties.InfraID
 
 	clusterStorageAccountName := "cluster" + m.doc.OpenShiftCluster.Properties.StorageSuffix
+	azureRegion := strings.ToLower(m.doc.OpenShiftCluster.Location) // Used in k8s object names, so must pass DNS-1123 validation
 
 	resources := []*arm.Resource{
-		m.storageAccount(clusterStorageAccountName, installConfig.Config.Azure.Region, true),
+		m.storageAccount(clusterStorageAccountName, azureRegion, true),
 		m.storageAccountBlobContainer(clusterStorageAccountName, "ignition"),
 		m.storageAccountBlobContainer(clusterStorageAccountName, "aro"),
-		m.storageAccount(m.doc.OpenShiftCluster.Properties.ImageRegistryStorageAccountName, installConfig.Config.Azure.Region, true),
+		m.storageAccount(m.doc.OpenShiftCluster.Properties.ImageRegistryStorageAccountName, azureRegion, true),
 		m.storageAccountBlobContainer(m.doc.OpenShiftCluster.Properties.ImageRegistryStorageAccountName, "image-registry"),
-		m.clusterNSG(infraID, installConfig.Config.Azure.Region),
+		m.clusterNSG(infraID, azureRegion),
 		m.clusterServicePrincipalRBAC(),
-		m.networkPrivateLinkService(installConfig),
-		m.networkPublicIPAddress(installConfig, infraID+"-pip-v4"),
-		m.networkInternalLoadBalancer(installConfig),
-		m.networkPublicLoadBalancer(installConfig),
+		m.networkPrivateLinkService(azureRegion),
+		m.networkPublicIPAddress(azureRegion, infraID+"-pip-v4"),
+		m.networkInternalLoadBalancer(azureRegion),
+		m.networkPublicLoadBalancer(azureRegion),
 	}
 
 	if m.doc.OpenShiftCluster.Properties.IngressProfiles[0].Visibility == api.VisibilityPublic {
 		resources = append(resources,
-			m.networkPublicIPAddress(installConfig, infraID+"-default-v4"),
+			m.networkPublicIPAddress(azureRegion, infraID+"-default-v4"),
 		)
 	}
 
diff --git a/pkg/cluster/deploystorage_resources.go b/pkg/cluster/deploystorage_resources.go
index 3a40f25a7..6dbc39d51 100644
--- a/pkg/cluster/deploystorage_resources.go
+++ b/pkg/cluster/deploystorage_resources.go
@@ -10,7 +10,6 @@ import (
 	mgmtauthorization "github.com/Azure/azure-sdk-for-go/services/preview/authorization/mgmt/2018-09-01-preview/authorization"
 	mgmtstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-06-01/storage"
 	"github.com/Azure/go-autorest/autorest/to"
-	"github.com/openshift/installer/pkg/asset/installconfig"
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/util/arm"
@@ -80,11 +79,11 @@ func (m *manager) clusterServicePrincipalRBAC() *arm.Resource {
 func (m *manager) storageAccount(name, region string, encrypted bool) *arm.Resource {
 	virtualNetworkRules := []mgmtstorage.VirtualNetworkRule{
 		{
-			VirtualNetworkResourceID: to.StringPtr(m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID),
+			VirtualNetworkResourceID: &m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID,
 			Action:                   mgmtstorage.Allow,
 		},
 		{
-			VirtualNetworkResourceID: to.StringPtr(m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].SubnetID),
+			VirtualNetworkResourceID: &m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].SubnetID,
 			Action:                   mgmtstorage.Allow,
 		},
 		{
@@ -177,7 +176,7 @@ func (m *manager) storageAccountBlobContainer(storageAccountName, name string) *
 	}
 }
 
-func (m *manager) networkPrivateLinkService(installConfig *installconfig.InstallConfig) *arm.Resource {
+func (m *manager) networkPrivateLinkService(azureRegion string) *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.PrivateLinkService{
 			PrivateLinkServiceProperties: &mgmtnetwork.PrivateLinkServiceProperties{
@@ -190,7 +189,7 @@ func (m *manager) networkPrivateLinkService(installConfig *installconfig.Install
 					{
 						PrivateLinkServiceIPConfigurationProperties: &mgmtnetwork.PrivateLinkServiceIPConfigurationProperties{
 							Subnet: &mgmtnetwork.Subnet{
-								ID: to.StringPtr(m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID),
+								ID: &m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID,
 							},
 						},
 						Name: to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID + "-pls-nic"),
@@ -209,7 +208,7 @@ func (m *manager) networkPrivateLinkService(installConfig *installconfig.Install
 			},
 			Name:     to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID + "-pls"),
 			Type:     to.StringPtr("Microsoft.Network/privateLinkServices"),
-			Location: &installConfig.Config.Azure.Region,
+			Location: &azureRegion,
 		},
 		APIVersion: azureclient.APIVersion("Microsoft.Network"),
 		DependsOn: []string{
@@ -245,7 +244,7 @@ func (m *manager) networkPrivateEndpoint() *arm.Resource {
 	}
 }
 
-func (m *manager) networkPublicIPAddress(installConfig *installconfig.InstallConfig, name string) *arm.Resource {
+func (m *manager) networkPublicIPAddress(azureRegion string, name string) *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.PublicIPAddress{
 			Sku: &mgmtnetwork.PublicIPAddressSku{
@@ -256,13 +255,13 @@ func (m *manager) networkPublicIPAddress(installConfig *installconfig.InstallCon
 			},
 			Name:     &name,
 			Type:     to.StringPtr("Microsoft.Network/publicIPAddresses"),
-			Location: &installConfig.Config.Azure.Region,
+			Location: &azureRegion,
 		},
 		APIVersion: azureclient.APIVersion("Microsoft.Network"),
 	}
 }
 
-func (m *manager) networkInternalLoadBalancer(installConfig *installconfig.InstallConfig) *arm.Resource {
+func (m *manager) networkInternalLoadBalancer(azureRegion string) *arm.Resource {
 	return &arm.Resource{
 		Resource: &mgmtnetwork.LoadBalancer{
 			Sku: &mgmtnetwork.LoadBalancerSku{
@@ -282,7 +281,7 @@ func (m *manager) networkInternalLoadBalancer(installConfig *installconfig.Insta
 				},
 				BackendAddressPools: &[]mgmtnetwork.BackendAddressPool{
 					{
-						Name: to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID),
+						Name: &m.doc.OpenShiftCluster.Properties.InfraID,
 					},
 					{
 						Name: to.StringPtr("ssh-0"),
@@ -429,13 +428,13 @@ func (m *manager) networkInternalLoadBalancer(installConfig *installconfig.Insta
 			},
 			Name:     to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID + "-internal"),
 			Type:     to.StringPtr("Microsoft.Network/loadBalancers"),
-			Location: &installConfig.Config.Azure.Region,
+			Location: &azureRegion,
 		},
 		APIVersion: azureclient.APIVersion("Microsoft.Network"),
 	}
 }
 
-func (m *manager) networkPublicLoadBalancer(installConfig *installconfig.InstallConfig) *arm.Resource {
+func (m *manager) networkPublicLoadBalancer(azureRegion string) *arm.Resource {
 	lb := &mgmtnetwork.LoadBalancer{
 		Sku: &mgmtnetwork.LoadBalancerSku{
 			Name: mgmtnetwork.LoadBalancerSkuNameStandard,
@@ -476,9 +475,9 @@ func (m *manager) networkPublicLoadBalancer(installConfig *installconfig.Install
 				},
 			},
 		},
-		Name:     to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID),
+		Name:     &m.doc.OpenShiftCluster.Properties.InfraID,
 		Type:     to.StringPtr("Microsoft.Network/loadBalancers"),
-		Location: &installConfig.Config.Azure.Region,
+		Location: &azureRegion,
 	}
 
 	if m.doc.OpenShiftCluster.Properties.APIServerProfile.Visibility == api.VisibilityPublic {
diff --git a/pkg/cluster/install.go b/pkg/cluster/install.go
index 4eb178d36..24c1579b4 100644
--- a/pkg/cluster/install.go
+++ b/pkg/cluster/install.go
@@ -150,9 +150,7 @@ func (m *manager) Install(ctx context.Context) error {
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.ensureResourceGroup)),
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.enableServiceEndpoints)),
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.setMasterSubnetPolicies)),
-			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(func(ctx context.Context) error {
-				return m.deployStorageTemplate(ctx, installConfig)
-			})),
+			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.deployStorageTemplate)),
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.updateAPIIPEarly)),
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.createOrUpdateRouterIPEarly)),
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.ensureGatewayCreate)),
diff --git a/pkg/deploy/devconfig.go b/pkg/deploy/devconfig.go
index 8a77665a6..3d1427f17 100644
--- a/pkg/deploy/devconfig.go
+++ b/pkg/deploy/devconfig.go
@@ -108,9 +108,9 @@ func DevConfig(_env env.Core) (*Config, error) {
 		Configuration: &Configuration{
 			ACRResourceID:                to.StringPtr("/subscriptions/" + _env.SubscriptionID() + "/resourceGroups/" + os.Getenv("USER") + "-global/providers/Microsoft.ContainerRegistry/registries/" + os.Getenv("USER") + "aro"),
 			AdminAPICABundle:             to.StringPtr(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca}))),
-			AdminAPIClientCertCommonName: to.StringPtr(clientCert.Subject.CommonName),
+			AdminAPIClientCertCommonName: &clientCert.Subject.CommonName,
 			ARMAPICABundle:               to.StringPtr(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca}))),
-			ARMAPIClientCertCommonName:   to.StringPtr(clientCert.Subject.CommonName),
+			ARMAPIClientCertCommonName:   &clientCert.Subject.CommonName,
 			ARMClientID:                  to.StringPtr(os.Getenv("AZURE_ARM_CLIENT_ID")),
 			AzureSecPackVSATenantId:      to.StringPtr(""),
 			ClusterMDMAccount:            to.StringPtr(version.DevClusterGenevaMetricsAccount),
diff --git a/pkg/deploy/generator/resources.go b/pkg/deploy/generator/resources.go
index 74ffbe8af..856575924 100644
--- a/pkg/deploy/generator/resources.go
+++ b/pkg/deploy/generator/resources.go
@@ -20,9 +20,9 @@ func (g *generator) actionGroup(name string, shortName string) *arm.Resource {
 		Resource: mgmtinsights.ActionGroupResource{
 			ActionGroup: &mgmtinsights.ActionGroup{
 				Enabled:        to.BoolPtr(true),
-				GroupShortName: to.StringPtr(shortName),
+				GroupShortName: &shortName,
 			},
-			Name:     to.StringPtr(name),
+			Name:     &name,
 			Type:     to.StringPtr("Microsoft.Insights/actionGroups"),
 			Location: to.StringPtr("Global"),
 		},
@@ -147,7 +147,7 @@ func (g *generator) keyVault(name string, accessPolicies *[]mgmtkeyvault.AccessP
 				TenantID:       &tenantUUIDHack,
 				AccessPolicies: accessPolicies,
 			},
-			Name:     to.StringPtr(name),
+			Name:     &name,
 			Type:     to.StringPtr("Microsoft.KeyVault/vaults"),
 			Location: to.StringPtr("[resourceGroup().location]"),
 		},
diff --git a/pkg/frontend/admin_openshiftcluster_startvm.go b/pkg/frontend/admin_openshiftcluster_startvm.go
new file mode 100644
index 000000000..be43f2383
--- /dev/null
+++ b/pkg/frontend/admin_openshiftcluster_startvm.go
@@ -0,0 +1,60 @@
+package frontend
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	"github.com/gorilla/mux"
+	"github.com/sirupsen/logrus"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
+	"github.com/Azure/ARO-RP/pkg/frontend/middleware"
+)
+
+func (f *frontend) postAdminOpenShiftClusterStartVM(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	log := ctx.Value(middleware.ContextKeyLog).(*logrus.Entry)
+	r.URL.Path = filepath.Dir(r.URL.Path)
+
+	err := f._postAdminOpenShiftClusterStartVM(ctx, r, log)
+
+	adminReply(log, w, nil, nil, err)
+}
+
+func (f *frontend) _postAdminOpenShiftClusterStartVM(ctx context.Context, r *http.Request, log *logrus.Entry) error {
+	vars := mux.Vars(r)
+
+	vmName := r.URL.Query().Get("vmName")
+	err := validateAdminVMName(vmName)
+	if err != nil {
+		return err
+	}
+
+	resourceID := strings.TrimPrefix(r.URL.Path, "/admin")
+
+	doc, err := f.dbOpenShiftClusters.Get(ctx, resourceID)
+	switch {
+	case cosmosdb.IsErrorStatusCode(err, http.StatusNotFound):
+		return api.NewCloudError(http.StatusNotFound, api.CloudErrorCodeResourceNotFound, "", "The Resource '%s/%s' under resource group '%s' was not found.", vars["resourceType"], vars["resourceName"], vars["resourceGroupName"])
+	case err != nil:
+		return err
+	}
+
+	subscriptionDoc, err := f.getSubscriptionDocument(ctx, doc.Key)
+	if err != nil {
+		return err
+	}
+
+	a, err := f.azureActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	if err != nil {
+		return err
+	}
+
+	return a.VMStartAndWait(ctx, vmName)
+}
diff --git a/pkg/frontend/admin_openshiftcluster_stopvm.go b/pkg/frontend/admin_openshiftcluster_stopvm.go
new file mode 100644
index 000000000..e2a013979
--- /dev/null
+++ b/pkg/frontend/admin_openshiftcluster_stopvm.go
@@ -0,0 +1,60 @@
+package frontend
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	"github.com/gorilla/mux"
+	"github.com/sirupsen/logrus"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
+	"github.com/Azure/ARO-RP/pkg/frontend/middleware"
+)
+
+func (f *frontend) postAdminOpenShiftClusterStopVM(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	log := ctx.Value(middleware.ContextKeyLog).(*logrus.Entry)
+	r.URL.Path = filepath.Dir(r.URL.Path)
+
+	err := f._postAdminOpenShiftClusterStopVM(ctx, r, log)
+
+	adminReply(log, w, nil, nil, err)
+}
+
+func (f *frontend) _postAdminOpenShiftClusterStopVM(ctx context.Context, r *http.Request, log *logrus.Entry) error {
+	vars := mux.Vars(r)
+
+	vmName := r.URL.Query().Get("vmName")
+	err := validateAdminVMName(vmName)
+	if err != nil {
+		return err
+	}
+
+	resourceID := strings.TrimPrefix(r.URL.Path, "/admin")
+
+	doc, err := f.dbOpenShiftClusters.Get(ctx, resourceID)
+	switch {
+	case cosmosdb.IsErrorStatusCode(err, http.StatusNotFound):
+		return api.NewCloudError(http.StatusNotFound, api.CloudErrorCodeResourceNotFound, "", "The Resource '%s/%s' under resource group '%s' was not found.", vars["resourceType"], vars["resourceName"], vars["resourceGroupName"])
+	case err != nil:
+		return err
+	}
+
+	subscriptionDoc, err := f.getSubscriptionDocument(ctx, doc.Key)
+	if err != nil {
+		return err
+	}
+
+	a, err := f.azureActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	if err != nil {
+		return err
+	}
+
+	return a.VMStopAndWait(ctx, vmName)
+}
diff --git a/pkg/frontend/admin_openshiftcluster_vmresize.go b/pkg/frontend/admin_openshiftcluster_vmresize.go
new file mode 100644
index 000000000..e04a7e08b
--- /dev/null
+++ b/pkg/frontend/admin_openshiftcluster_vmresize.go
@@ -0,0 +1,111 @@
+package frontend
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	"github.com/gorilla/mux"
+	"github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	kruntime "k8s.io/apimachinery/pkg/runtime"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
+	"github.com/Azure/ARO-RP/pkg/frontend/middleware"
+)
+
+func (f *frontend) postAdminOpenShiftClusterVMResize(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	log := ctx.Value(middleware.ContextKeyLog).(*logrus.Entry)
+	r.URL.Path = filepath.Dir(r.URL.Path)
+
+	err := f._postAdminOpenShiftClusterVMResize(ctx, r, log)
+
+	adminReply(log, w, nil, nil, err)
+}
+
+func (f *frontend) _postAdminOpenShiftClusterVMResize(ctx context.Context, r *http.Request, log *logrus.Entry) error {
+	vars := mux.Vars(r)
+
+	vmName := r.URL.Query().Get("vmName")
+	err := validateAdminVMName(vmName)
+	if err != nil {
+		return err
+	}
+
+	resourceID := strings.TrimPrefix(r.URL.Path, "/admin")
+
+	doc, err := f.dbOpenShiftClusters.Get(ctx, resourceID)
+	switch {
+	case cosmosdb.IsErrorStatusCode(err, http.StatusNotFound):
+		return api.NewCloudError(http.StatusNotFound, api.CloudErrorCodeResourceNotFound, "",
+			"The Resource '%s/%s' under resource group '%s' was not found.",
+			vars["resourceType"], vars["resourceName"], vars["resourceGroupName"])
+	case err != nil:
+		return err
+	}
+
+	subscriptionDoc, err := f.getSubscriptionDocument(ctx, doc.Key)
+	if err != nil {
+		return err
+	}
+
+	vmSize := r.URL.Query().Get("vmSize")
+	err = validateAdminVMSize(vmSize)
+	if err != nil {
+		return err
+	}
+
+	a, err := f.azureActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	if err != nil {
+		return err
+	}
+
+	k, err := f.kubeActionsFactory(log, f.env, doc.OpenShiftCluster)
+	if err != nil {
+		return err
+	}
+
+	nodeList, err := k.KubeList(ctx, "node", "")
+	if err != nil {
+		return err
+	}
+
+	var u unstructured.Unstructured
+	var nodes corev1.NodeList
+	if err = json.Unmarshal(nodeList, &u); err != nil {
+		return err
+	}
+
+	err = kruntime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &nodes)
+	if err != nil {
+		return err
+	}
+
+	nodeExists := false
+	for _, node := range nodes.Items {
+		if _, ok := node.ObjectMeta.Labels["node-role.kubernetes.io/master"]; !ok {
+			continue
+		}
+
+		if strings.EqualFold(vmName, node.ObjectMeta.Name) {
+			nodeExists = true
+			break
+		}
+	}
+
+	if !nodeExists {
+		return api.NewCloudError(http.StatusNotFound, api.CloudErrorCodeNotFound, "",
+			`"The master node '%s' under resource group '%s' was not found."`,
+			vmName, vars["resourceGroupName"])
+	}
+
+	return a.VMResize(ctx, vmName, vmSize)
+}
diff --git a/pkg/frontend/admin_openshiftcluster_vmsizelist.go b/pkg/frontend/admin_openshiftcluster_vmsizelist.go
new file mode 100644
index 000000000..5a712b363
--- /dev/null
+++ b/pkg/frontend/admin_openshiftcluster_vmsizelist.go
@@ -0,0 +1,78 @@
+package frontend
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
+	"github.com/gorilla/mux"
+	"github.com/sirupsen/logrus"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/database/cosmosdb"
+	"github.com/Azure/ARO-RP/pkg/frontend/middleware"
+)
+
+// /admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/skus
+func (f *frontend) getAdminOpenShiftClusterVMResizeOptions(w http.ResponseWriter, r *http.Request) {
+	ctx := r.Context()
+	log := ctx.Value(middleware.ContextKeyLog).(*logrus.Entry)
+	r.URL.Path = filepath.Dir(r.URL.Path)
+
+	b, err := f._getAdminOpenShiftClusterVMResizeOptions(ctx, r, log)
+
+	adminReply(log, w, nil, b, err)
+}
+
+func (f *frontend) _getAdminOpenShiftClusterVMResizeOptions(ctx context.Context, r *http.Request, log *logrus.Entry) ([]byte, error) {
+	vars := mux.Vars(r)
+
+	resourceID := strings.TrimPrefix(r.URL.Path, "/admin")
+
+	doc, err := f.dbOpenShiftClusters.Get(ctx, resourceID)
+	switch {
+	case cosmosdb.IsErrorStatusCode(err, http.StatusNotFound):
+		return nil, api.NewCloudError(http.StatusNotFound, api.CloudErrorCodeResourceNotFound, "",
+			"The Resource '%s/%s' under resource group '%s' was not found.",
+			vars["resourceType"], vars["resourceName"], vars["resourceGroupName"])
+	case err != nil:
+		return nil, err
+	}
+
+	subscriptionDoc, err := f.getSubscriptionDocument(ctx, doc.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	a, err := f.azureActionsFactory(log, f.env, doc.OpenShiftCluster, subscriptionDoc)
+	if err != nil {
+		return nil, err
+	}
+
+	skus, err := a.VMSizeList(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	return json.Marshal(f.filterVMSkus(skus))
+}
+
+func (f *frontend) filterVMSkus(skus []mgmtcompute.ResourceSku) []string {
+	filteredSkus := []string{}
+
+	for _, sku := range skus {
+		if sku.Restrictions != nil && len(*sku.Restrictions) == 0 {
+			if sku.Name != nil {
+				filteredSkus = append(filteredSkus, *sku.Name)
+			}
+		}
+	}
+
+	return filteredSkus
+}
diff --git a/pkg/frontend/adminactions/azureactions.go b/pkg/frontend/adminactions/azureactions.go
index d0e9bf737..e9ba404c9 100644
--- a/pkg/frontend/adminactions/azureactions.go
+++ b/pkg/frontend/adminactions/azureactions.go
@@ -5,8 +5,10 @@ package adminactions
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 
+	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
 	"github.com/sirupsen/logrus"
 
 	"github.com/Azure/ARO-RP/pkg/api"
@@ -23,6 +25,10 @@ type AzureActions interface {
 	ResourcesList(ctx context.Context) ([]byte, error)
 	NICReconcileFailedState(ctx context.Context, nicName string) error
 	VMRedeployAndWait(ctx context.Context, vmName string) error
+	VMStartAndWait(ctx context.Context, vmName string) error
+	VMStopAndWait(ctx context.Context, vmName string) error
+	VMSizeList(ctx context.Context) ([]mgmtcompute.ResourceSku, error)
+	VMResize(ctx context.Context, vmName string, vmSize string) error
 	VMSerialConsole(ctx context.Context, w http.ResponseWriter, log *logrus.Entry, vmName string) error
 }
 
@@ -32,6 +38,7 @@ type azureActions struct {
 	oc  *api.OpenShiftCluster
 
 	resources          features.ResourcesClient
+	resourceSkus       compute.ResourceSkusClient
 	virtualMachines    compute.VirtualMachinesClient
 	virtualNetworks    network.VirtualNetworksClient
 	diskEncryptionSets compute.DiskEncryptionSetsClient
@@ -55,6 +62,7 @@ func NewAzureActions(log *logrus.Entry, env env.Interface, oc *api.OpenShiftClus
 		oc:  oc,
 
 		resources:          features.NewResourcesClient(env.Environment(), subscriptionDoc.ID, fpAuth),
+		resourceSkus:       compute.NewResourceSkusClient(env.Environment(), subscriptionDoc.ID, fpAuth),
 		virtualMachines:    compute.NewVirtualMachinesClient(env.Environment(), subscriptionDoc.ID, fpAuth),
 		virtualNetworks:    network.NewVirtualNetworksClient(env.Environment(), subscriptionDoc.ID, fpAuth),
 		diskEncryptionSets: compute.NewDiskEncryptionSetsClient(env.Environment(), subscriptionDoc.ID, fpAuth),
@@ -68,3 +76,29 @@ func (a *azureActions) VMRedeployAndWait(ctx context.Context, vmName string) err
 	clusterRGName := stringutils.LastTokenByte(a.oc.Properties.ClusterProfile.ResourceGroupID, '/')
 	return a.virtualMachines.RedeployAndWait(ctx, clusterRGName, vmName)
 }
+
+func (a *azureActions) VMStartAndWait(ctx context.Context, vmName string) error {
+	clusterRGName := stringutils.LastTokenByte(a.oc.Properties.ClusterProfile.ResourceGroupID, '/')
+	return a.virtualMachines.StartAndWait(ctx, clusterRGName, vmName)
+}
+
+func (a *azureActions) VMStopAndWait(ctx context.Context, vmName string) error {
+	clusterRGName := stringutils.LastTokenByte(a.oc.Properties.ClusterProfile.ResourceGroupID, '/')
+	return a.virtualMachines.StopAndWait(ctx, clusterRGName, vmName)
+}
+
+func (a *azureActions) VMSizeList(ctx context.Context) ([]mgmtcompute.ResourceSku, error) {
+	filter := fmt.Sprintf("location eq '%s'", a.env.Location())
+	return a.resourceSkus.List(ctx, filter)
+}
+
+func (a *azureActions) VMResize(ctx context.Context, vmName string, size string) error {
+	clusterRGName := stringutils.LastTokenByte(a.oc.Properties.ClusterProfile.ResourceGroupID, '/')
+	vm, err := a.virtualMachines.Get(ctx, clusterRGName, vmName, mgmtcompute.InstanceView)
+	if err != nil {
+		return err
+	}
+
+	vm.HardwareProfile.VMSize = mgmtcompute.VirtualMachineSizeTypes(size)
+	return a.virtualMachines.CreateOrUpdateAndWait(ctx, clusterRGName, vmName, vm)
+}
diff --git a/pkg/frontend/frontend.go b/pkg/frontend/frontend.go
index e020eb84a..29d24f962 100644
--- a/pkg/frontend/frontend.go
+++ b/pkg/frontend/frontend.go
@@ -244,6 +244,18 @@ func (f *frontend) authenticatedRoutes(r *mux.Router) {
 
 	s.Methods(http.MethodPost).HandlerFunc(f.postAdminOpenShiftClusterRedeployVM).Name("postAdminOpenShiftClusterRedeployVM")
 
+	s = r.
+		Path("/admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/stopvm").
+		Subrouter()
+
+	s.Methods(http.MethodPost).HandlerFunc(f.postAdminOpenShiftClusterStopVM).Name("postAdminOpenShiftClusterStopVM")
+
+	s = r.
+		Path("/admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/startvm").
+		Subrouter()
+
+	s.Methods(http.MethodPost).HandlerFunc(f.postAdminOpenShiftClusterStartVM).Name("postAdminOpenShiftClusterStartVM")
+
 	s = r.
 		Path("/admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/upgrade").
 		Subrouter()
@@ -256,6 +268,18 @@ func (f *frontend) authenticatedRoutes(r *mux.Router) {
 
 	s.Methods(http.MethodGet).HandlerFunc(f.getAdminOpenShiftClusters).Name("getAdminOpenShiftClusters")
 
+	s = r.
+		Path("/admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/skus").
+		Subrouter()
+
+	s.Methods(http.MethodGet).HandlerFunc(f.getAdminOpenShiftClusterVMResizeOptions).Name("getAdminOpenShiftClusterVMResizeOptions")
+
+	s = r.
+		Path("/admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/resize").
+		Subrouter()
+
+	s.Methods(http.MethodPost).HandlerFunc(f.postAdminOpenShiftClusterVMResize).Name("postAdminOpenShiftClusterVMResize")
+
 	s = r.
 		Path("/admin/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}/reconcilefailednic").
 		Subrouter()
diff --git a/pkg/frontend/validate.go b/pkg/frontend/validate.go
index 71c4defb8..52d309710 100644
--- a/pkg/frontend/validate.go
+++ b/pkg/frontend/validate.go
@@ -77,7 +77,7 @@ func (f *frontend) validateOpenShiftUniqueKey(ctx context.Context, doc *api.Open
 var rxKubernetesString = regexp.MustCompile(`(?i)^[-a-z0-9.]{0,255}$`)
 
 func validateAdminKubernetesObjectsNonCustomer(method, groupKind, namespace, name string) error {
-	if !utilnamespace.IsOpenShift(namespace) {
+	if !utilnamespace.IsOpenShiftNamespace(namespace) {
 		return api.NewCloudError(http.StatusForbidden, api.CloudErrorCodeForbidden, "", "Access to the provided namespace '%s' is forbidden.", namespace)
 	}
 
@@ -127,6 +127,12 @@ func validateNetworkInterfaceName(nicName string) error {
 	if nicName == "" || !rxNetworkInterfaceName.MatchString(nicName) {
 		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The provided nicName '%s' is invalid.", nicName)
 	}
+	return nil
+}
 
+func validateAdminVMSize(vmSize string) error {
+	if vmSize == "" {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The provided vmSize '%s' is invalid.", vmSize)
+	}
 	return nil
 }
diff --git a/pkg/monitor/cluster/clusterversions.go b/pkg/monitor/cluster/clusterversions.go
index cf85a4961..804c50d99 100644
--- a/pkg/monitor/cluster/clusterversions.go
+++ b/pkg/monitor/cluster/clusterversions.go
@@ -38,14 +38,26 @@ func (mon *Monitor) emitClusterVersions(ctx context.Context) error {
 		availableRP = version.GitCommit
 	}
 
+	actualVersion := actualVersion(cv)
+	actualMinorVersion := ""
+	if len(actualVersion) > 0 {
+		parsedVersion, err := version.ParseVersion(actualVersion)
+		if err != nil {
+			return err
+		}
+		actualMinorVersion = parsedVersion.MinorVersion()
+	}
+
 	mon.emitGauge("cluster.versions", 1, map[string]string{
-		"actualVersion":                        actualVersion(cv),
+		"actualVersion":                        actualVersion,
 		"desiredVersion":                       desiredVersion(cv),
 		"provisionedByResourceProviderVersion": mon.oc.Properties.ProvisionedBy,              // last successful Put or Patch
 		"resourceProviderVersion":              version.GitCommit,                            // RP version currently running
 		"operatorVersion":                      operatorVersion,                              // operator version in the cluster
 		"availableVersion":                     availableVersion(cv, version.UpgradeStreams), // current available version for upgrade from stream
 		"availableRP":                          availableRP,                                  // current RP version available for document update, empty when none
+		"latestGaMinorVersion":                 version.InstallStream.Version.MinorVersion(), // Latest GA in ARO Minor version
+		"actualMinorVersion":                   actualMinorVersion,                           // Minor version, empty if actual version is not in expected form
 	})
 
 	return nil
diff --git a/pkg/monitor/cluster/daemonsetstatuses.go b/pkg/monitor/cluster/daemonsetstatuses.go
index f7869d2ed..9d1db8fdb 100644
--- a/pkg/monitor/cluster/daemonsetstatuses.go
+++ b/pkg/monitor/cluster/daemonsetstatuses.go
@@ -24,7 +24,7 @@ func (mon *Monitor) emitDaemonsetStatuses(ctx context.Context) error {
 		count += int64(len(dss.Items))
 
 		for _, ds := range dss.Items {
-			if !namespace.IsOpenShift(ds.Namespace) {
+			if !namespace.IsOpenShiftNamespace(ds.Namespace) {
 				continue
 			}
 
diff --git a/pkg/monitor/cluster/deploymentstatuses.go b/pkg/monitor/cluster/deploymentstatuses.go
index ed02e290b..771fe8989 100644
--- a/pkg/monitor/cluster/deploymentstatuses.go
+++ b/pkg/monitor/cluster/deploymentstatuses.go
@@ -24,7 +24,7 @@ func (mon *Monitor) emitDeploymentStatuses(ctx context.Context) error {
 		count += int64(len(ds.Items))
 
 		for _, d := range ds.Items {
-			if !namespace.IsOpenShift(d.Namespace) {
+			if !namespace.IsOpenShiftNamespace(d.Namespace) {
 				continue
 			}
 
diff --git a/pkg/monitor/cluster/jobconditions.go b/pkg/monitor/cluster/jobconditions.go
index f45104ca3..9941f9380 100644
--- a/pkg/monitor/cluster/jobconditions.go
+++ b/pkg/monitor/cluster/jobconditions.go
@@ -30,7 +30,7 @@ func (mon *Monitor) emitJobConditions(ctx context.Context) error {
 		count += int64(len(jobs.Items))
 
 		for _, job := range jobs.Items {
-			if !namespace.IsOpenShift(job.Namespace) {
+			if !namespace.IsOpenShiftNamespace(job.Namespace) {
 				continue
 			}
 
diff --git a/pkg/monitor/cluster/podconditions.go b/pkg/monitor/cluster/podconditions.go
index 119db6454..48956d19b 100644
--- a/pkg/monitor/cluster/podconditions.go
+++ b/pkg/monitor/cluster/podconditions.go
@@ -51,7 +51,7 @@ func (mon *Monitor) emitPodConditions(ctx context.Context) error {
 
 func (mon *Monitor) _emitPodConditions(ps *corev1.PodList) {
 	for _, p := range ps.Items {
-		if !namespace.IsOpenShift(p.Namespace) {
+		if !namespace.IsOpenShiftNamespace(p.Namespace) {
 			continue
 		}
 
@@ -89,7 +89,7 @@ func (mon *Monitor) _emitPodConditions(ps *corev1.PodList) {
 
 func (mon *Monitor) _emitPodContainerStatuses(ps *corev1.PodList) {
 	for _, p := range ps.Items {
-		if !namespace.IsOpenShift(p.Namespace) {
+		if !namespace.IsOpenShiftNamespace(p.Namespace) {
 			continue
 		}
 
@@ -126,7 +126,7 @@ func (mon *Monitor) _emitPodContainerStatuses(ps *corev1.PodList) {
 
 func (mon *Monitor) _emitPodContainerRestartCounter(ps *corev1.PodList) {
 	for _, p := range ps.Items {
-		if !namespace.IsOpenShiftSystemNamespace(p.Namespace) {
+		if !namespace.IsOpenShiftNamespace(p.Namespace) {
 			continue
 		}
 
diff --git a/pkg/monitor/cluster/prometheusalerts.go b/pkg/monitor/cluster/prometheusalerts.go
index c0208e981..8df500943 100644
--- a/pkg/monitor/cluster/prometheusalerts.go
+++ b/pkg/monitor/cluster/prometheusalerts.go
@@ -80,7 +80,7 @@ func (mon *Monitor) emitPrometheusAlerts(ctx context.Context) error {
 	mon.emitGauge("prometheus.alerts.count", int64(len(alerts)), nil)
 
 	for _, alert := range alerts {
-		if !namespace.IsOpenShift(string(alert.Labels["namespace"])) {
+		if !namespace.IsOpenShiftNamespace(string(alert.Labels["namespace"])) {
 			continue
 		}
 
diff --git a/pkg/monitor/cluster/replicasetstatuses.go b/pkg/monitor/cluster/replicasetstatuses.go
index fc5fc8dc2..1b9d30bbd 100644
--- a/pkg/monitor/cluster/replicasetstatuses.go
+++ b/pkg/monitor/cluster/replicasetstatuses.go
@@ -24,7 +24,7 @@ func (mon *Monitor) emitReplicasetStatuses(ctx context.Context) error {
 		count += int64(len(rss.Items))
 
 		for _, rs := range rss.Items {
-			if !namespace.IsOpenShift(rs.Namespace) {
+			if !namespace.IsOpenShiftNamespace(rs.Namespace) {
 				continue
 			}
 
diff --git a/pkg/monitor/cluster/statefulsetstatuses.go b/pkg/monitor/cluster/statefulsetstatuses.go
index d348eec7d..57a95db9c 100644
--- a/pkg/monitor/cluster/statefulsetstatuses.go
+++ b/pkg/monitor/cluster/statefulsetstatuses.go
@@ -24,7 +24,7 @@ func (mon *Monitor) emitStatefulsetStatuses(ctx context.Context) error {
 		count += int64(len(sss.Items))
 
 		for _, ss := range sss.Items {
-			if !namespace.IsOpenShift(ss.Namespace) {
+			if !namespace.IsOpenShiftNamespace(ss.Namespace) {
 				continue
 			}
 
diff --git a/pkg/operator/controllers/previewfeature/nsgflowlogs/nsgflowlogs.go b/pkg/operator/controllers/previewfeature/nsgflowlogs/nsgflowlogs.go
index dee1d4e70..1bd661eb8 100644
--- a/pkg/operator/controllers/previewfeature/nsgflowlogs/nsgflowlogs.go
+++ b/pkg/operator/controllers/previewfeature/nsgflowlogs/nsgflowlogs.go
@@ -78,21 +78,21 @@ func (n *nsgFlowLogsFeature) Enable(ctx context.Context, instance *aropreviewv1a
 func (n *nsgFlowLogsFeature) newFlowLog(instance *aropreviewv1alpha1.PreviewFeature, nsgID string) *mgmtnetwork.FlowLog {
 	// build a request as described here https://docs.microsoft.com/en-us/azure/network-watcher/network-watcher-nsg-flow-logging-rest#enable-network-security-group-flow-logs
 	return &mgmtnetwork.FlowLog{
-		Location: to.StringPtr(n.location),
+		Location: &n.location,
 		FlowLogPropertiesFormat: &mgmtnetwork.FlowLogPropertiesFormat{
-			TargetResourceID: to.StringPtr(nsgID),
+			TargetResourceID: &nsgID,
 			Enabled:          to.BoolPtr(true),
 			Format: &mgmtnetwork.FlowLogFormatParameters{
 				Type:    mgmtnetwork.JSON,
 				Version: to.Int32Ptr(int32(instance.Spec.NSGFlowLogs.Version)),
 			},
 			RetentionPolicy: &mgmtnetwork.RetentionPolicyParameters{
-				Days: to.Int32Ptr(instance.Spec.NSGFlowLogs.RetentionDays),
+				Days: &instance.Spec.NSGFlowLogs.RetentionDays,
 			},
-			StorageID: to.StringPtr(instance.Spec.NSGFlowLogs.StorageAccountResourceID),
+			StorageID: &instance.Spec.NSGFlowLogs.StorageAccountResourceID,
 			FlowAnalyticsConfiguration: &mgmtnetwork.TrafficAnalyticsProperties{
 				NetworkWatcherFlowAnalyticsConfiguration: &mgmtnetwork.TrafficAnalyticsConfigurationProperties{
-					WorkspaceID:              to.StringPtr(instance.Spec.NSGFlowLogs.TrafficAnalyticsLogAnalyticsWorkspaceID),
+					WorkspaceID:              &instance.Spec.NSGFlowLogs.TrafficAnalyticsLogAnalyticsWorkspaceID,
 					TrafficAnalyticsInterval: to.Int32Ptr(int32(instance.Spec.NSGFlowLogs.TrafficAnalyticsInterval.Truncate(time.Minute).Minutes())),
 				},
 			},
diff --git a/pkg/util/azureclient/mgmt/compute/virtualmachines_addons.go b/pkg/util/azureclient/mgmt/compute/virtualmachines_addons.go
index 8617d2cc3..5b6113860 100644
--- a/pkg/util/azureclient/mgmt/compute/virtualmachines_addons.go
+++ b/pkg/util/azureclient/mgmt/compute/virtualmachines_addons.go
@@ -7,6 +7,7 @@ import (
 	"context"
 
 	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
+	"github.com/Azure/go-autorest/autorest/to"
 )
 
 // VirtualMachinesClientAddons contains addons for VirtualMachinesClient
@@ -15,6 +16,7 @@ type VirtualMachinesClientAddons interface {
 	DeleteAndWait(ctx context.Context, resourceGroupName string, VMName string, forceDeletion *bool) error
 	RedeployAndWait(ctx context.Context, resourceGroupName string, VMName string) error
 	StartAndWait(ctx context.Context, resourceGroupName string, VMName string) error
+	StopAndWait(ctx context.Context, resourceGroupName string, VMName string) error
 	List(ctx context.Context, resourceGroupName string) (result []mgmtcompute.VirtualMachine, err error)
 }
 
@@ -54,6 +56,15 @@ func (c *virtualMachinesClient) StartAndWait(ctx context.Context, resourceGroupN
 	return future.WaitForCompletionRef(ctx, c.Client)
 }
 
+func (c *virtualMachinesClient) StopAndWait(ctx context.Context, resourceGroupName string, VMName string) error {
+	future, err := c.PowerOff(ctx, resourceGroupName, VMName, to.BoolPtr(false))
+	if err != nil {
+		return err
+	}
+
+	return future.WaitForCompletionRef(ctx, c.Client)
+}
+
 func (c *virtualMachinesClient) List(ctx context.Context, resourceGroupName string) (result []mgmtcompute.VirtualMachine, err error) {
 	page, err := c.VirtualMachinesClient.List(ctx, resourceGroupName)
 	if err != nil {
diff --git a/pkg/util/dns/dns.go b/pkg/util/dns/dns.go
index 6294b608a..d3f498f91 100644
--- a/pkg/util/dns/dns.go
+++ b/pkg/util/dns/dns.go
@@ -111,7 +111,7 @@ func (m *manager) CreateOrUpdateRouter(ctx context.Context, oc *api.OpenShiftClu
 			TTL: to.Int64Ptr(300),
 			ARecords: &[]mgmtdns.ARecord{
 				{
-					Ipv4Address: to.StringPtr(routerIP),
+					Ipv4Address: &routerIP,
 				},
 			},
 		},
@@ -158,7 +158,7 @@ func (m *manager) createOrUpdate(ctx context.Context, oc *api.OpenShiftCluster,
 	rs := mgmtdns.RecordSet{
 		RecordSetProperties: &mgmtdns.RecordSetProperties{
 			Metadata: map[string]*string{
-				resourceID: to.StringPtr(oc.ID),
+				resourceID: &oc.ID,
 			},
 			TTL: to.Int64Ptr(300),
 		},
@@ -167,7 +167,7 @@ func (m *manager) createOrUpdate(ctx context.Context, oc *api.OpenShiftCluster,
 	if ip != "" {
 		rs.ARecords = &[]mgmtdns.ARecord{
 			{
-				Ipv4Address: to.StringPtr(ip),
+				Ipv4Address: &ip,
 			},
 		}
 	}
diff --git a/pkg/util/mocks/adminactions/adminactions.go b/pkg/util/mocks/adminactions/adminactions.go
index f8c83279f..21501dc6a 100644
--- a/pkg/util/mocks/adminactions/adminactions.go
+++ b/pkg/util/mocks/adminactions/adminactions.go
@@ -9,6 +9,7 @@ import (
 	http "net/http"
 	reflect "reflect"
 
+	compute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
 	gomock "github.com/golang/mock/gomock"
 	logrus "github.com/sirupsen/logrus"
 	unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -175,6 +176,20 @@ func (mr *MockAzureActionsMockRecorder) VMRedeployAndWait(arg0, arg1 interface{}
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMRedeployAndWait", reflect.TypeOf((*MockAzureActions)(nil).VMRedeployAndWait), arg0, arg1)
 }
 
+// VMResize mocks base method.
+func (m *MockAzureActions) VMResize(arg0 context.Context, arg1, arg2 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "VMResize", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// VMResize indicates an expected call of VMResize.
+func (mr *MockAzureActionsMockRecorder) VMResize(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMResize", reflect.TypeOf((*MockAzureActions)(nil).VMResize), arg0, arg1, arg2)
+}
+
 // VMSerialConsole mocks base method.
 func (m *MockAzureActions) VMSerialConsole(arg0 context.Context, arg1 http.ResponseWriter, arg2 *logrus.Entry, arg3 string) error {
 	m.ctrl.T.Helper()
@@ -188,3 +203,46 @@ func (mr *MockAzureActionsMockRecorder) VMSerialConsole(arg0, arg1, arg2, arg3 i
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMSerialConsole", reflect.TypeOf((*MockAzureActions)(nil).VMSerialConsole), arg0, arg1, arg2, arg3)
 }
+
+// VMSizeList mocks base method.
+func (m *MockAzureActions) VMSizeList(arg0 context.Context) ([]compute.ResourceSku, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "VMSizeList", arg0)
+	ret0, _ := ret[0].([]compute.ResourceSku)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// VMSizeList indicates an expected call of VMSizeList.
+func (mr *MockAzureActionsMockRecorder) VMSizeList(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMSizeList", reflect.TypeOf((*MockAzureActions)(nil).VMSizeList), arg0)
+}
+
+// VMStartAndWait mocks base method.
+func (m *MockAzureActions) VMStartAndWait(arg0 context.Context, arg1 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "VMStartAndWait", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// VMStartAndWait indicates an expected call of VMStartAndWait.
+func (mr *MockAzureActionsMockRecorder) VMStartAndWait(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMStartAndWait", reflect.TypeOf((*MockAzureActions)(nil).VMStartAndWait), arg0, arg1)
+}
+
+// VMStopAndWait mocks base method.
+func (m *MockAzureActions) VMStopAndWait(arg0 context.Context, arg1 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "VMStopAndWait", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// VMStopAndWait indicates an expected call of VMStopAndWait.
+func (mr *MockAzureActionsMockRecorder) VMStopAndWait(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VMStopAndWait", reflect.TypeOf((*MockAzureActions)(nil).VMStopAndWait), arg0, arg1)
+}
diff --git a/pkg/util/mocks/azureclient/mgmt/compute/compute.go b/pkg/util/mocks/azureclient/mgmt/compute/compute.go
index f06be48d1..299207613 100644
--- a/pkg/util/mocks/azureclient/mgmt/compute/compute.go
+++ b/pkg/util/mocks/azureclient/mgmt/compute/compute.go
@@ -211,6 +211,20 @@ func (mr *MockVirtualMachinesClientMockRecorder) StartAndWait(arg0, arg1, arg2 i
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartAndWait", reflect.TypeOf((*MockVirtualMachinesClient)(nil).StartAndWait), arg0, arg1, arg2)
 }
 
+// StopAndWait mocks base method.
+func (m *MockVirtualMachinesClient) StopAndWait(arg0 context.Context, arg1, arg2 string) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "StopAndWait", arg0, arg1, arg2)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// StopAndWait indicates an expected call of StopAndWait.
+func (mr *MockVirtualMachinesClientMockRecorder) StopAndWait(arg0, arg1, arg2 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopAndWait", reflect.TypeOf((*MockVirtualMachinesClient)(nil).StopAndWait), arg0, arg1, arg2)
+}
+
 // MockUsageClient is a mock of UsageClient interface.
 type MockUsageClient struct {
 	ctrl     *gomock.Controller
diff --git a/pkg/util/namespace/namespace.go b/pkg/util/namespace/namespace.go
index fd7fd785c..3b9a98979 100644
--- a/pkg/util/namespace/namespace.go
+++ b/pkg/util/namespace/namespace.go
@@ -3,23 +3,74 @@ package namespace
 // Copyright (c) Microsoft Corporation.
 // Licensed under the Apache License 2.0.
 
-import (
-	"strings"
-)
+// IsOpenShiftNamespace returns true if ns is a namespace in the defined hardcoded map.
+// We should only add new namespaces into this hardcoded list but never delete
+// the existing ones in the namespace list to avoid backward compatibility issues.
+func IsOpenShiftNamespace(ns string) bool {
+	nsmap := map[string]struct{}{
+		// Allow validation against non-namespaced objects via Geneva actions
+		"": {},
 
-// IsOpenShift returns true if ns is an openshift managed namespace, including the default namespace
-func IsOpenShift(ns string) bool {
-	return ns == "" ||
-		ns == "default" ||
-		ns == "openshift" ||
-		strings.HasPrefix(ns, "kube-") ||
-		strings.HasPrefix(ns, "openshift-")
-}
+		// ARO specific namespaces
+		"openshift-azure-logging":            {},
+		"openshift-azure-operator":           {},
+		"openshift-managed-upgrade-operator": {},
 
-// IsOpenShiftSystemNamespace returns true if ns is an openshift managed namespace, without the default namespace
-func IsOpenShiftSystemNamespace(ns string) bool {
-	return ns == "" ||
-		ns == "openshift" ||
-		strings.HasPrefix(ns, "kube-") ||
-		strings.HasPrefix(ns, "openshift-")
+		// OCP namespaces
+		"openshift":                                        {},
+		"openshift-apiserver":                              {},
+		"openshift-apiserver-operator":                     {},
+		"openshift-authentication-operator":                {},
+		"openshift-cloud-controller-manager":               {},
+		"openshift-cloud-controller-manager-operator":      {},
+		"openshift-cloud-credential-operator":              {},
+		"openshift-cluster-csi-drivers":                    {},
+		"openshift-cluster-machine-approver":               {},
+		"openshift-cluster-node-tuning-operator":           {},
+		"openshift-cluster-samples-operator":               {},
+		"openshift-cluster-storage-operator":               {},
+		"openshift-config":                                 {},
+		"openshift-config-managed":                         {},
+		"openshift-config-operator":                        {},
+		"openshift-console":                                {},
+		"openshift-console-operator":                       {},
+		"openshift-console-user-settings":                  {},
+		"openshift-controller-manager":                     {},
+		"openshift-controller-manager-operator":            {},
+		"openshift-dns":                                    {},
+		"openshift-dns-operator":                           {},
+		"openshift-etcd":                                   {},
+		"openshift-etcd-operator":                          {},
+		"openshift-host-network":                           {},
+		"openshift-image-registry":                         {},
+		"openshift-ingress":                                {},
+		"openshift-ingress-canary":                         {},
+		"openshift-ingress-operator":                       {},
+		"openshift-insights":                               {},
+		"openshift-kni-infra":                              {},
+		"openshift-kube-apiserver":                         {},
+		"openshift-kube-apiserver-operator":                {},
+		"openshift-kube-controller-manager":                {},
+		"openshift-kube-controller-manager-operator":       {},
+		"openshift-kube-scheduler":                         {},
+		"openshift-kube-scheduler-operator":                {},
+		"openshift-kube-storage-version-migrator":          {},
+		"openshift-kube-storage-version-migrator-operator": {},
+		"openshift-machine-api":                            {},
+		"openshift-machine-config-operator":                {},
+		"openshift-marketplace":                            {},
+		"openshift-monitoring":                             {},
+		"openshift-multus":                                 {},
+		"openshift-network-diagnostics":                    {},
+		"openshift-network-operator":                       {},
+		"openshift-oauth-apiserver":                        {},
+		"openshift-openstack-infra":                        {},
+		"openshift-operators":                              {},
+		"openshift-ovirt-infra":                            {},
+		"openshift-sdn":                                    {},
+		"openshift-service-ca":                             {},
+		"openshift-service-ca-operator":                    {},
+	}
+	_, ok := nsmap[ns]
+	return ok
 }
diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go
index cccc8dd68..1820d022b 100644
--- a/pkg/util/version/version.go
+++ b/pkg/util/version/version.go
@@ -77,3 +77,7 @@ func (v *Version) Eq(w *Version) bool {
 	}
 	return true
 }
+
+func (v *Version) MinorVersion() string {
+	return fmt.Sprintf("%d.%d", v.V[0], v.V[1])
+}
diff --git a/python/az/aro/azext_aro/_validators.py b/python/az/aro/azext_aro/_validators.py
index a6e7d527e..fc76d41f1 100644
--- a/python/az/aro/azext_aro/_validators.py
+++ b/python/az/aro/azext_aro/_validators.py
@@ -11,6 +11,7 @@ from azure.cli.core.commands.client_factory import get_subscription_id
 from azure.cli.core.profiles import ResourceType
 from azure.cli.core.azclierror import CLIInternalError, InvalidArgumentValueError, \
     RequiredArgumentMissingError
+from azure.core.exceptions import ResourceNotFoundError
 from knack.log import get_logger
 from msrestazure.azure_exceptions import CloudError
 from msrestazure.tools import is_valid_resource_id
@@ -77,7 +78,7 @@ def validate_disk_encryption_set(cmd, namespace):
         except CloudError as err:
             raise InvalidArgumentValueError(
                 f"Invald --disk-encryption-set, error when getting '{namespace.disk_encryption_set}':"
-                f" {err.message}") from err
+                f" {str(err)}") from err
 
 
 def validate_domain(namespace):
@@ -153,11 +154,11 @@ def validate_subnet(key):
         try:
             client.subnets.get(parts['resource_group'],
                                parts['name'], parts['child_name_1'])
-        except CloudError as err:
-            if err.status_code == 404:
+        except Exception as err:
+            if isinstance(err, ResourceNotFoundError):
                 raise InvalidArgumentValueError(
-                    f"Invald --{key.replace('_', '-')}, error when getting '{subnet}': {err.message}") from err
-            raise CLIInternalError(err.message) from err
+                    f"Invald --{key.replace('_', '-')}, error when getting '{subnet}': {str(err)}") from err
+            raise CLIInternalError(f"Unexpected error when getting subnet '{subnet}': {str(err)}") from err
 
     return _validate_subnet
 


Test patch:
diff --git a/pkg/frontend/admin_openshiftcluster_kubernetesobjects_test.go b/pkg/frontend/admin_openshiftcluster_kubernetesobjects_test.go
index 31ec22fcd..bae3bbd56 100644
--- a/pkg/frontend/admin_openshiftcluster_kubernetesobjects_test.go
+++ b/pkg/frontend/admin_openshiftcluster_kubernetesobjects_test.go
@@ -45,7 +45,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			name:         "cluster exist in db - get",
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objKind:      "ConfigMap",
-			objNamespace: "openshift-project",
+			objNamespace: "openshift",
 			objName:      "config",
 			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 				k.EXPECT().
@@ -60,7 +60,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			name:         "cluster exist in db - list",
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objKind:      "ConfigMap",
-			objNamespace: "openshift-project",
+			objNamespace: "openshift",
 			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 				k.EXPECT().
 					KubeList(gomock.Any(), tt.objKind, tt.objNamespace).
@@ -73,7 +73,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			method:       http.MethodGet,
 			name:         "no groupKind provided",
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
-			objNamespace: "openshift-project",
+			objNamespace: "openshift",
 			objName:      "config",
 			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
@@ -85,7 +85,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			name:         "secret requested",
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objKind:      "Secret",
-			objNamespace: "openshift-project",
+			objNamespace: "openshift",
 			objName:      "config",
 			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
@@ -97,7 +97,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			name:         "cluster exist in db",
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objKind:      "ConfigMap",
-			objNamespace: "openshift-project",
+			objNamespace: "openshift",
 			objName:      "config",
 			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 				k.EXPECT().
@@ -110,7 +110,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			method:       http.MethodDelete,
 			name:         "no groupKind provided",
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
-			objNamespace: "openshift-project",
+			objNamespace: "openshift",
 			objName:      "config",
 			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
@@ -122,7 +122,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			name:         "no name provided",
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objKind:      "this",
-			objNamespace: "openshift-project",
+			objNamespace: "openshift",
 			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
 			wantStatusCode: http.StatusBadRequest,
@@ -133,7 +133,7 @@ func TestAdminKubernetesObjectsGetAndDelete(t *testing.T) {
 			name:         "secret requested",
 			resourceID:   fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName", mockSubID),
 			objKind:      "Secret",
-			objNamespace: "openshift-project",
+			objNamespace: "openshift",
 			objName:      "config",
 			mocks: func(tt *test, k *mock_adminactions.MockKubeActions) {
 			},
@@ -222,21 +222,21 @@ func TestValidateAdminKubernetesObjectsNonCustomer(t *testing.T) {
 		{
 			test:      "invalid groupKind",
 			groupKind: "$",
-			namespace: "openshift-ns",
+			namespace: "openshift",
 			name:      "Valid-NAME-01",
 			wantErr:   "400: InvalidParameter: : The provided groupKind '$' is invalid.",
 		},
 		{
 			test:      "forbidden groupKind",
 			groupKind: "Secret",
-			namespace: "openshift-ns",
+			namespace: "openshift",
 			name:      "Valid-NAME-01",
 			wantErr:   "403: Forbidden: : Access to secrets is forbidden.",
 		},
 		{
 			test:      "forbidden groupKind",
 			groupKind: "Anything.oauth.openshift.io",
-			namespace: "openshift-ns",
+			namespace: "openshift",
 			name:      "Valid-NAME-01",
 			wantErr:   "403: Forbidden: : Access to secrets is forbidden.",
 		},
@@ -266,7 +266,7 @@ func TestValidateAdminKubernetesObjectsNonCustomer(t *testing.T) {
 		},
 		{
 			test:      "empty groupKind",
-			namespace: "openshift-ns",
+			namespace: "openshift",
 			name:      "Valid-NAME-01",
 			wantErr:   "400: InvalidParameter: : The provided groupKind '' is invalid.",
 		},
@@ -275,12 +275,12 @@ func TestValidateAdminKubernetesObjectsNonCustomer(t *testing.T) {
 			groupKind: "Valid-kind.openshift.io",
 			namespace: "openshift-/",
 			name:      "Valid-NAME-01",
-			wantErr:   "400: InvalidParameter: : The provided namespace 'openshift-/' is invalid.",
+			wantErr:   "403: Forbidden: : Access to the provided namespace 'openshift-/' is forbidden.",
 		},
 		{
 			test:      "invalid name",
 			groupKind: "Valid-kind.openshift.io",
-			namespace: "openshift-ns",
+			namespace: "openshift",
 			name:      longName,
 			wantErr:   "400: InvalidParameter: : The provided name '" + longName + "' is invalid.",
 		},
@@ -288,14 +288,14 @@ func TestValidateAdminKubernetesObjectsNonCustomer(t *testing.T) {
 			test:      "post: empty name",
 			method:    http.MethodPost,
 			groupKind: "Valid-kind.openshift.io",
-			namespace: "openshift-ns",
+			namespace: "openshift",
 			wantErr:   "400: InvalidParameter: : The provided name '' is invalid.",
 		},
 		{
 			test:      "delete: empty name",
 			method:    http.MethodDelete,
 			groupKind: "Valid-kind.openshift.io",
-			namespace: "openshift-ns",
+			namespace: "openshift",
 			wantErr:   "400: InvalidParameter: : The provided name '' is invalid.",
 		},
 	} {
@@ -353,6 +353,10 @@ func TestAdminPostKubernetesObjects(t *testing.T) {
 			objInBody: &unstructured.Unstructured{
 				Object: map[string]interface{}{
 					"kind": "Secret",
+					"metadata": map[string]interface{}{
+						"namespace": "openshift",
+						"name":      "secret",
+					},
 				},
 			},
 			mocks:          func(tt *test, k *mock_adminactions.MockKubeActions) {},
diff --git a/pkg/frontend/admin_openshiftcluster_startvm_test.go b/pkg/frontend/admin_openshiftcluster_startvm_test.go
new file mode 100644
index 000000000..cc3553bd4
--- /dev/null
+++ b/pkg/frontend/admin_openshiftcluster_startvm_test.go
@@ -0,0 +1,110 @@
+package frontend
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/sirupsen/logrus"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/frontend/adminactions"
+	"github.com/Azure/ARO-RP/pkg/metrics/noop"
+	mock_adminactions "github.com/Azure/ARO-RP/pkg/util/mocks/adminactions"
+	testdatabase "github.com/Azure/ARO-RP/test/database"
+)
+
+func TestAdminStartVM(t *testing.T) {
+	mockSubID := "00000000-0000-0000-0000-000000000000"
+	mockTenantID := "00000000-0000-0000-0000-000000000000"
+
+	ctx := context.Background()
+
+	type test struct {
+		name           string
+		resourceID     string
+		fixture        func(*testdatabase.Fixture)
+		vmName         string
+		mocks          func(*test, *mock_adminactions.MockAzureActions)
+		wantStatusCode int
+		wantResponse   []byte
+		wantError      string
+	}
+
+	for _, tt := range []*test{
+		{
+			name:       "basic coverage",
+			vmName:     "aro-worker-australiasoutheast-7tcq7",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
+					Key: strings.ToLower(testdatabase.GetResourcePath(mockSubID, "resourceName")),
+					OpenShiftCluster: &api.OpenShiftCluster{
+						ID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+						Properties: api.OpenShiftClusterProperties{
+							ClusterProfile: api.ClusterProfile{
+								ResourceGroupID: fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster", mockSubID),
+							},
+						},
+					},
+				})
+
+				f.AddSubscriptionDocuments(&api.SubscriptionDocument{
+					ID: mockSubID,
+					Subscription: &api.Subscription{
+						State: api.SubscriptionStateRegistered,
+						Properties: &api.SubscriptionProperties{
+							TenantID: mockTenantID,
+						},
+					},
+				})
+			},
+			mocks: func(tt *test, a *mock_adminactions.MockAzureActions) {
+				a.EXPECT().VMStartAndWait(gomock.Any(), tt.vmName).Return(nil)
+			},
+			wantStatusCode: http.StatusOK,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			ti := newTestInfra(t).WithOpenShiftClusters().WithSubscriptions()
+			defer ti.done()
+
+			a := mock_adminactions.NewMockAzureActions(ti.controller)
+			tt.mocks(tt, a)
+
+			err := ti.buildFixtures(tt.fixture)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			f, err := NewFrontend(ctx, ti.audit, 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
+			}, nil)
+
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			go f.Run(ctx, nil, nil)
+
+			resp, b, err := ti.request(http.MethodPost,
+				fmt.Sprintf("https://server/admin%s/startvm?vmName=%s", tt.resourceID, tt.vmName),
+				nil, nil)
+			if err != nil {
+				t.Error(err)
+			}
+
+			err = validateResponse(resp, b, tt.wantStatusCode, tt.wantError, tt.wantResponse)
+			if err != nil {
+				t.Error(err)
+			}
+		})
+	}
+}
diff --git a/pkg/frontend/admin_openshiftcluster_stopvm_test.go b/pkg/frontend/admin_openshiftcluster_stopvm_test.go
new file mode 100644
index 000000000..4f7ac5970
--- /dev/null
+++ b/pkg/frontend/admin_openshiftcluster_stopvm_test.go
@@ -0,0 +1,110 @@
+package frontend
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/sirupsen/logrus"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/frontend/adminactions"
+	"github.com/Azure/ARO-RP/pkg/metrics/noop"
+	mock_adminactions "github.com/Azure/ARO-RP/pkg/util/mocks/adminactions"
+	testdatabase "github.com/Azure/ARO-RP/test/database"
+)
+
+func TestAdminStopVM(t *testing.T) {
+	mockSubID := "00000000-0000-0000-0000-000000000000"
+	mockTenantID := "00000000-0000-0000-0000-000000000000"
+
+	ctx := context.Background()
+
+	type test struct {
+		name           string
+		resourceID     string
+		fixture        func(*testdatabase.Fixture)
+		vmName         string
+		mocks          func(*test, *mock_adminactions.MockAzureActions)
+		wantStatusCode int
+		wantResponse   []byte
+		wantError      string
+	}
+
+	for _, tt := range []*test{
+		{
+			name:       "basic coverage",
+			vmName:     "aro-worker-australiasoutheast-7tcq7",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
+					Key: strings.ToLower(testdatabase.GetResourcePath(mockSubID, "resourceName")),
+					OpenShiftCluster: &api.OpenShiftCluster{
+						ID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+						Properties: api.OpenShiftClusterProperties{
+							ClusterProfile: api.ClusterProfile{
+								ResourceGroupID: fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster", mockSubID),
+							},
+						},
+					},
+				})
+
+				f.AddSubscriptionDocuments(&api.SubscriptionDocument{
+					ID: mockSubID,
+					Subscription: &api.Subscription{
+						State: api.SubscriptionStateRegistered,
+						Properties: &api.SubscriptionProperties{
+							TenantID: mockTenantID,
+						},
+					},
+				})
+			},
+			mocks: func(tt *test, a *mock_adminactions.MockAzureActions) {
+				a.EXPECT().VMStopAndWait(gomock.Any(), tt.vmName).Return(nil)
+			},
+			wantStatusCode: http.StatusOK,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			ti := newTestInfra(t).WithOpenShiftClusters().WithSubscriptions()
+			defer ti.done()
+
+			a := mock_adminactions.NewMockAzureActions(ti.controller)
+			tt.mocks(tt, a)
+
+			err := ti.buildFixtures(tt.fixture)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			f, err := NewFrontend(ctx, ti.audit, 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
+			}, nil)
+
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			go f.Run(ctx, nil, nil)
+
+			resp, b, err := ti.request(http.MethodPost,
+				fmt.Sprintf("https://server/admin%s/stopvm?vmName=%s", tt.resourceID, tt.vmName),
+				nil, nil)
+			if err != nil {
+				t.Error(err)
+			}
+
+			err = validateResponse(resp, b, tt.wantStatusCode, tt.wantError, tt.wantResponse)
+			if err != nil {
+				t.Error(err)
+			}
+		})
+	}
+}
diff --git a/pkg/frontend/admin_openshiftcluster_vmresize_test.go b/pkg/frontend/admin_openshiftcluster_vmresize_test.go
new file mode 100644
index 000000000..97897c6a8
--- /dev/null
+++ b/pkg/frontend/admin_openshiftcluster_vmresize_test.go
@@ -0,0 +1,240 @@
+package frontend
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/frontend/adminactions"
+	"github.com/Azure/ARO-RP/pkg/metrics/noop"
+	mock_adminactions "github.com/Azure/ARO-RP/pkg/util/mocks/adminactions"
+	testdatabase "github.com/Azure/ARO-RP/test/database"
+)
+
+func TestAdminVMResize(t *testing.T) {
+	mockSubID := "00000000-0000-0000-0000-000000000000"
+	mockTenantID := "00000000-0000-0000-0000-000000000000"
+	ctx := context.Background()
+
+	type test struct {
+		name           string
+		resourceID     string
+		vmName         string
+		vmSize         string
+		fixture        func(f *testdatabase.Fixture)
+		mocks          func(*test, *mock_adminactions.MockAzureActions, *mock_adminactions.MockKubeActions)
+		wantStatusCode int
+		wantResponse   []byte
+		wantError      string
+	}
+
+	for _, tt := range []*test{
+		{
+			name:       "basic coverage",
+			vmName:     "aro-fake-node-0",
+			vmSize:     "Standard_D8s_v3",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
+					Key: strings.ToLower(testdatabase.GetResourcePath(mockSubID, "resourceName")),
+					OpenShiftCluster: &api.OpenShiftCluster{
+						ID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+						Properties: api.OpenShiftClusterProperties{
+							ClusterProfile: api.ClusterProfile{
+								ResourceGroupID: fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster", mockSubID),
+							},
+						},
+					},
+				})
+				f.AddSubscriptionDocuments(&api.SubscriptionDocument{
+					ID: mockSubID,
+					Subscription: &api.Subscription{
+						State: api.SubscriptionStateRegistered,
+						Properties: &api.SubscriptionProperties{
+							TenantID: mockTenantID,
+						},
+					},
+				})
+			},
+			mocks: func(tt *test, a *mock_adminactions.MockAzureActions, k *mock_adminactions.MockKubeActions) {
+				node := corev1.Node{
+					TypeMeta: metav1.TypeMeta{
+						Kind: "node",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "aro-fake-node-0",
+
+						Labels: map[string]string{
+							"node-role.kubernetes.io/master": "true",
+						},
+					},
+				}
+				nodeList := corev1.NodeList{
+					TypeMeta: metav1.TypeMeta{
+						Kind: "List",
+					},
+					Items: []corev1.Node{node},
+				}
+				marsh, _ := json.Marshal(nodeList)
+				k.EXPECT().KubeList(gomock.Any(), "node", "").Return([]byte(marsh), nil)
+				a.EXPECT().VMResize(gomock.Any(), tt.vmName, tt.vmSize).Return(nil)
+			},
+			wantStatusCode: http.StatusOK,
+		},
+		{
+			name:       "cluster not found",
+			vmName:     "aro-fake-node-0",
+			vmSize:     "Standard_D8s_v3",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddSubscriptionDocuments(&api.SubscriptionDocument{
+					ID: mockSubID,
+					Subscription: &api.Subscription{
+						State: api.SubscriptionStateRegistered,
+						Properties: &api.SubscriptionProperties{
+							TenantID: mockTenantID,
+						},
+					},
+				})
+			},
+			mocks:          func(tt *test, a *mock_adminactions.MockAzureActions, k *mock_adminactions.MockKubeActions) {},
+			wantStatusCode: http.StatusNotFound,
+			wantError:      `404: ResourceNotFound: : The Resource 'openshiftclusters/resourcename' under resource group 'resourcegroup' was not found.`,
+		},
+		{
+			name:       "subscription doc not found",
+			vmName:     "aro-fake-node-0",
+			vmSize:     "Standard_D8s_v3",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
+					Key: strings.ToLower(testdatabase.GetResourcePath(mockSubID, "resourceName")),
+					OpenShiftCluster: &api.OpenShiftCluster{
+						ID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+						Properties: api.OpenShiftClusterProperties{
+							ClusterProfile: api.ClusterProfile{
+								ResourceGroupID: fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster", mockSubID),
+							},
+						},
+					},
+				})
+			},
+			mocks:          func(tt *test, a *mock_adminactions.MockAzureActions, k *mock_adminactions.MockKubeActions) {},
+			wantStatusCode: http.StatusBadRequest,
+			wantError:      fmt.Sprintf(`400: InvalidSubscriptionState: : Request is not allowed in unregistered subscription '%s'.`, mockSubID),
+		},
+		{
+			name:       "master node not found",
+			vmName:     "aro-fake-node-0",
+			vmSize:     "Standard_D8s_v3",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
+					Key: strings.ToLower(testdatabase.GetResourcePath(mockSubID, "resourceName")),
+					OpenShiftCluster: &api.OpenShiftCluster{
+						ID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+						Properties: api.OpenShiftClusterProperties{
+							ClusterProfile: api.ClusterProfile{
+								ResourceGroupID: fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster", mockSubID),
+							},
+						},
+					},
+				})
+				f.AddSubscriptionDocuments(&api.SubscriptionDocument{
+					ID: mockSubID,
+					Subscription: &api.Subscription{
+						State: api.SubscriptionStateRegistered,
+						Properties: &api.SubscriptionProperties{
+							TenantID: mockTenantID,
+						},
+					},
+				})
+			},
+			mocks: func(tt *test, a *mock_adminactions.MockAzureActions, k *mock_adminactions.MockKubeActions) {
+				node := corev1.Node{
+					TypeMeta: metav1.TypeMeta{
+						Kind: "node",
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "aro-fake-node-0",
+
+						Labels: map[string]string{
+							"node-role.kubernetes.io/worker": "true",
+						},
+					},
+				}
+				nodeList := corev1.NodeList{
+					TypeMeta: metav1.TypeMeta{
+						Kind: "List",
+					},
+					Items: []corev1.Node{node},
+				}
+				marsh, _ := json.Marshal(nodeList)
+				k.EXPECT().KubeList(gomock.Any(), "node", "").Return([]byte(marsh), nil)
+			},
+			wantStatusCode: http.StatusNotFound,
+			wantError:      `404: NotFound: : "The master node 'aro-fake-node-0' under resource group 'resourcegroup' was not found."`,
+		},
+		{
+			name:           "invalid vmname",
+			vmName:         "%26&ampersandvmname",
+			resourceID:     testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			vmSize:         "Standard_D8s_v3",
+			mocks:          func(tt *test, a *mock_adminactions.MockAzureActions, k *mock_adminactions.MockKubeActions) {},
+			wantStatusCode: http.StatusBadRequest,
+			wantError:      `400: InvalidParameter: : The provided vmName '&' is invalid.`,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			ti := newTestInfra(t).WithSubscriptions().WithOpenShiftClusters()
+			defer ti.done()
+
+			a := mock_adminactions.NewMockAzureActions(ti.controller)
+			k := mock_adminactions.NewMockKubeActions(ti.controller)
+			tt.mocks(tt, a, k)
+
+			err := ti.buildFixtures(tt.fixture)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			f, err := NewFrontend(ctx, ti.audit, ti.log, ti.env, ti.asyncOperationsDatabase, ti.openShiftClustersDatabase, ti.subscriptionsDatabase, api.APIs, &noop.Noop{}, nil,
+				func(e *logrus.Entry, i env.Interface, osc *api.OpenShiftCluster) (adminactions.KubeActions, error) {
+					return k, nil
+				}, func(*logrus.Entry, env.Interface, *api.OpenShiftCluster, *api.SubscriptionDocument) (adminactions.AzureActions, error) {
+					return a, nil
+				}, nil)
+
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			go f.Run(ctx, nil, nil)
+
+			resp, b, err := ti.request(http.MethodPost,
+				fmt.Sprintf("https://server/admin%s/resize?vmName=%s&vmSize=%s", tt.resourceID, tt.vmName, tt.vmSize),
+				nil, nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			err = validateResponse(resp, b, tt.wantStatusCode, tt.wantError, tt.wantResponse)
+			if err != nil {
+				t.Error(err)
+			}
+		})
+	}
+}
diff --git a/pkg/frontend/admin_openshiftcluster_vmsizelist_test.go b/pkg/frontend/admin_openshiftcluster_vmsizelist_test.go
new file mode 100644
index 000000000..4347e3436
--- /dev/null
+++ b/pkg/frontend/admin_openshiftcluster_vmsizelist_test.go
@@ -0,0 +1,162 @@
+package frontend
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+
+	mgmtcompute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2020-06-01/compute"
+	"github.com/Azure/go-autorest/autorest/to"
+	"github.com/golang/mock/gomock"
+	"github.com/sirupsen/logrus"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/frontend/adminactions"
+	"github.com/Azure/ARO-RP/pkg/metrics/noop"
+	mock_adminactions "github.com/Azure/ARO-RP/pkg/util/mocks/adminactions"
+	testdatabase "github.com/Azure/ARO-RP/test/database"
+)
+
+func TestAdminListVMSizeList(t *testing.T) {
+	mockSubID := "00000000-0000-0000-0000-000000000000"
+	mockTenantID := "00000000-0000-0000-0000-000000000000"
+	ctx := context.Background()
+
+	type test struct {
+		name           string
+		resourceID     string
+		fixture        func(f *testdatabase.Fixture)
+		mocks          func(*test, *mock_adminactions.MockAzureActions)
+		wantStatusCode int
+		wantResponse   []byte
+		wantError      string
+	}
+
+	for _, tt := range []*test{
+		{
+			name:       "happy path",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
+					Key: strings.ToLower(testdatabase.GetResourcePath(mockSubID, "resourceName")),
+					OpenShiftCluster: &api.OpenShiftCluster{
+						ID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+						Properties: api.OpenShiftClusterProperties{
+							ClusterProfile: api.ClusterProfile{
+								ResourceGroupID: fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster", mockSubID),
+							},
+						},
+					},
+				})
+				f.AddSubscriptionDocuments(&api.SubscriptionDocument{
+					ID: mockSubID,
+					Subscription: &api.Subscription{
+						State: api.SubscriptionStateRegistered,
+						Properties: &api.SubscriptionProperties{
+							TenantID: mockTenantID,
+						},
+					},
+				})
+			},
+			mocks: func(tt *test, a *mock_adminactions.MockAzureActions) {
+				a.EXPECT().
+					VMSizeList(gomock.Any()).
+					Return([]mgmtcompute.ResourceSku{
+						{
+							Name: to.StringPtr("Standard_D8s_v90"),
+							Restrictions: &[]mgmtcompute.ResourceSkuRestrictions{
+								{
+									Type: mgmtcompute.Location,
+								},
+							},
+						},
+						{
+							Name:         to.StringPtr("Standard_D8s_v9001"),
+							Restrictions: &[]mgmtcompute.ResourceSkuRestrictions{},
+						},
+					}, nil)
+			},
+			wantStatusCode: http.StatusOK,
+			wantResponse:   []byte(`["Standard_D8s_v9001"]` + "\n"),
+		},
+		{
+			name:       "cluster not found",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddSubscriptionDocuments(&api.SubscriptionDocument{
+					ID: mockSubID,
+					Subscription: &api.Subscription{
+						State: api.SubscriptionStateRegistered,
+						Properties: &api.SubscriptionProperties{
+							TenantID: mockTenantID,
+						},
+					},
+				})
+			},
+			mocks:          func(tt *test, a *mock_adminactions.MockAzureActions) {},
+			wantStatusCode: http.StatusNotFound,
+			wantError:      `404: ResourceNotFound: : The Resource 'openshiftclusters/resourcename' under resource group 'resourcegroup' was not found.`,
+		},
+		{
+			name:       "subscription doc not found",
+			resourceID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+			fixture: func(f *testdatabase.Fixture) {
+				f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{
+					Key: strings.ToLower(testdatabase.GetResourcePath(mockSubID, "resourceName")),
+					OpenShiftCluster: &api.OpenShiftCluster{
+						ID: testdatabase.GetResourcePath(mockSubID, "resourceName"),
+						Properties: api.OpenShiftClusterProperties{
+							ClusterProfile: api.ClusterProfile{
+								ResourceGroupID: fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster", mockSubID),
+							},
+						},
+					},
+				})
+			},
+			mocks:          func(tt *test, a *mock_adminactions.MockAzureActions) {},
+			wantStatusCode: http.StatusBadRequest,
+			wantError:      fmt.Sprintf(`400: InvalidSubscriptionState: : Request is not allowed in unregistered subscription '%s'.`, mockSubID),
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			ti := newTestInfra(t).WithSubscriptions().WithOpenShiftClusters()
+			defer ti.done()
+
+			a := mock_adminactions.NewMockAzureActions(ti.controller)
+			tt.mocks(tt, a)
+
+			err := ti.buildFixtures(tt.fixture)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			f, err := NewFrontend(ctx, ti.audit, 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
+			}, nil)
+
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			go f.Run(ctx, nil, nil)
+
+			resp, b, err := ti.request(http.MethodGet,
+				fmt.Sprintf("https://server/admin%s/skus", tt.resourceID),
+				nil, nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			err = validateResponse(resp, b, tt.wantStatusCode, tt.wantError, tt.wantResponse)
+			if err != nil {
+				t.Error(err)
+			}
+		})
+	}
+}
diff --git a/pkg/monitor/cluster/clusterversions_test.go b/pkg/monitor/cluster/clusterversions_test.go
index 23fd60838..9ebd186d1 100644
--- a/pkg/monitor/cluster/clusterversions_test.go
+++ b/pkg/monitor/cluster/clusterversions_test.go
@@ -44,6 +44,7 @@ func TestEmitClusterVersion(t *testing.T) {
 		wantProvisionedByResourceProviderVersion string
 		wantAvailableVersion                     string
 		wantAvailableRP                          string
+		wantActualMinorVersion                   string
 	}{
 		{
 			name: "without spec",
@@ -79,6 +80,7 @@ func TestEmitClusterVersion(t *testing.T) {
 			wantProvisionedByResourceProviderVersion: "",
 			wantAvailableVersion:                     "4.5.39",
 			wantAvailableRP:                          "unknown",
+			wantActualMinorVersion:                   "4.5",
 		},
 		{
 			name: "with spec",
@@ -103,6 +105,7 @@ func TestEmitClusterVersion(t *testing.T) {
 			wantDesiredVersion:                       "4.5.4",
 			wantProvisionedByResourceProviderVersion: "",
 			wantAvailableRP:                          "unknown",
+			wantActualMinorVersion:                   "",
 		},
 		{
 			name: "with ProvisionedBy",
@@ -158,6 +161,8 @@ func TestEmitClusterVersion(t *testing.T) {
 				"resourceProviderVersion":              "unknown",
 				"availableVersion":                     tt.wantAvailableVersion,
 				"availableRP":                          tt.wantAvailableRP,
+				"latestGaMinorVersion":                 version.InstallStream.Version.MinorVersion(),
+				"actualMinorVersion":                   tt.wantActualMinorVersion,
 			})
 
 			err := mon.emitClusterVersions(ctx)
diff --git a/pkg/monitor/cluster/podconditions_test.go b/pkg/monitor/cluster/podconditions_test.go
index e29b1f372..8574bffb4 100644
--- a/pkg/monitor/cluster/podconditions_test.go
+++ b/pkg/monitor/cluster/podconditions_test.go
@@ -235,7 +235,7 @@ func TestEmitPodContainerRestartCounter(t *testing.T) {
 		&corev1.Pod{ // #6 Multi-container pod
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "multi-container-pod",
-				Namespace: "openshift-test",
+				Namespace: "openshift",
 			},
 			Status: corev1.PodStatus{
 				ContainerStatuses: []corev1.ContainerStatus{
@@ -285,7 +285,7 @@ func TestEmitPodContainerRestartCounter(t *testing.T) {
 
 	m.EXPECT().EmitGauge("pod.restartcounter", int64(restartCounterThreshold*2), map[string]string{
 		"name":      "multi-container-pod",
-		"namespace": "openshift-test",
+		"namespace": "openshift",
 	})
 
 	ps, _ := cli.CoreV1().Pods("").List(context.Background(), metav1.ListOptions{})
diff --git a/pkg/util/dynamichelper/dynamichelper_test.go b/pkg/util/dynamichelper/dynamichelper_test.go
index b5e1791b1..ee14985fd 100644
--- a/pkg/util/dynamichelper/dynamichelper_test.go
+++ b/pkg/util/dynamichelper/dynamichelper_test.go
@@ -11,6 +11,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	kruntime "k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 
 	"github.com/Azure/ARO-RP/pkg/util/cmp"
 )
@@ -288,3 +289,76 @@ func TestMerge(t *testing.T) {
 		})
 	}
 }
+
+func TestMakeURLSegments(t *testing.T) {
+	for _, tt := range []struct {
+		gvr         *schema.GroupVersionResource
+		namespace   string
+		uname, name string
+		url         []string
+		want        []string
+	}{
+		{
+			uname: "Group is empty",
+			gvr: &schema.GroupVersionResource{
+				Group:    "",
+				Version:  "4.10",
+				Resource: "test-resource",
+			},
+			namespace: "openshift",
+			name:      "test-name-1",
+			want:      []string{"api", "4.10", "namespaces", "openshift", "test-resource", "test-name-1"},
+		},
+		{
+			uname: "Group is not empty",
+			gvr: &schema.GroupVersionResource{
+				Group:    "test-group",
+				Version:  "4.10",
+				Resource: "test-resource",
+			},
+			namespace: "openshift-apiserver",
+			name:      "test-name-2",
+			want:      []string{"apis", "test-group", "4.10", "namespaces", "openshift-apiserver", "test-resource", "test-name-2"},
+		},
+		{
+			uname: "Namespace is empty",
+			gvr: &schema.GroupVersionResource{
+				Group:    "test-group",
+				Version:  "4.10",
+				Resource: "test-resource",
+			},
+			namespace: "",
+			name:      "test-name-3",
+			want:      []string{"apis", "test-group", "4.10", "test-resource", "test-name-3"},
+		},
+		{
+			uname: "Namespace is not empty",
+			gvr: &schema.GroupVersionResource{
+				Group:    "test-group",
+				Version:  "4.10",
+				Resource: "test-resource",
+			},
+			namespace: "openshift-sdn",
+			name:      "test-name-3",
+			want:      []string{"apis", "test-group", "4.10", "namespaces", "openshift-sdn", "test-resource", "test-name-3"},
+		},
+		{
+			uname: "Name is empty",
+			gvr: &schema.GroupVersionResource{
+				Group:    "test-group",
+				Version:  "4.10",
+				Resource: "test-resource",
+			},
+			namespace: "openshift-ns",
+			name:      "",
+			want:      []string{"apis", "test-group", "4.10", "namespaces", "openshift-ns", "test-resource"},
+		},
+	} {
+		t.Run(tt.uname, func(t *testing.T) {
+			got := makeURLSegments(tt.gvr, tt.namespace, tt.name)
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Error(cmp.Diff(got, tt.want))
+			}
+		})
+	}
+}
diff --git a/pkg/util/namespace/namespace_test.go b/pkg/util/namespace/namespace_test.go
index ae2cd2729..23821b025 100644
--- a/pkg/util/namespace/namespace_test.go
+++ b/pkg/util/namespace/namespace_test.go
@@ -7,73 +7,62 @@ import (
 	"testing"
 )
 
-func TestIsOpenShift(t *testing.T) {
+func TestIsOpenShiftNamespace(t *testing.T) {
 	for _, tt := range []struct {
 		namespace string
 		want      bool
 	}{
 		{
-			want: true,
+			namespace: "",
+			want:      true,
 		},
 		{
-			namespace: "openshift-ns",
+			namespace: "openshift-apiserver",
 			want:      true,
 		},
 		{
-			namespace: "openshift",
+			namespace: "openshift-azure-operator",
 			want:      true,
 		},
 		{
-			namespace: "kube-ns",
+			namespace: "openshift-azure-logging",
 			want:      true,
 		},
 		{
-			namespace: "default",
-			want:      true,
+			namespace: "openshift-gitops",
+			want:      false,
 		},
 		{
-			namespace: "customer",
+			namespace: "openshift-authentication",
+			want:      false,
 		},
-	} {
-		t.Run(tt.namespace, func(t *testing.T) {
-			got := IsOpenShift(tt.namespace)
-			if tt.want != got {
-				t.Error(got)
-			}
-		})
-	}
-}
-
-func TestIsOpenShiftSystemNamespace(t *testing.T) {
-	for _, tt := range []struct {
-		namespace string
-		want      bool
-	}{
 		{
-			want: true,
+			namespace: "default",
+			want:      false,
 		},
 		{
-			namespace: "openshift-ns",
-			want:      true,
+			namespace: "customer",
+			want:      false,
 		},
 		{
-			namespace: "openshift",
-			want:      true,
+			namespace: "openshift-/",
+			want:      false,
 		},
 		{
-			namespace: "kube-ns",
-			want:      true,
+			namespace: "openshift-ns",
+			want:      false,
 		},
 		{
-			namespace: "default",
+			namespace: "kube-ns",
 			want:      false,
 		},
 		{
-			namespace: "customer",
+			namespace: "openshift-operator-lifecycle-manager",
+			want:      false,
 		},
 	} {
 		t.Run(tt.namespace, func(t *testing.T) {
-			got := IsOpenShiftSystemNamespace(tt.namespace)
+			got := IsOpenShiftNamespace(tt.namespace)
 			if tt.want != got {
 				t.Error(got)
 			}
diff --git a/test/e2e/adminapi_kubernetesobjects.go b/test/e2e/adminapi_kubernetesobjects.go
index e91302982..7f665e69a 100644
--- a/test/e2e/adminapi_kubernetesobjects.go
+++ b/test/e2e/adminapi_kubernetesobjects.go
@@ -26,7 +26,7 @@ var _ = Describe("[Admin API] Kubernetes objects action", func() {
 	const objName = "e2e-test-object"
 
 	When("in a standard openshift namespace", func() {
-		const namespace = "default"
+		const namespace = "openshift"
 
 		It("should be able to create, get, list, update and delete objects", func() {
 			defer func() {
diff --git a/test/e2e/operator.go b/test/e2e/operator.go
index 6cd90b4b8..331e615a6 100644
--- a/test/e2e/operator.go
+++ b/test/e2e/operator.go
@@ -367,7 +367,7 @@ var _ = Describe("ARO Operator - Azure Subnet Reconciler", func() {
 	// Creates an empty NSG that gets assigned to master/worker subnets.
 	createE2ENSG := func() {
 		testnsg = mgmtnetwork.SecurityGroup{
-			Location:                      to.StringPtr(location),
+			Location:                      &location,
 			Name:                          to.StringPtr(nsg),
 			Type:                          to.StringPtr("Microsoft.Network/networkSecurityGroups"),
 			SecurityGroupPropertiesFormat: &mgmtnetwork.SecurityGroupPropertiesFormat{},