Repository: Azure/ARO-RP
Pull request #2089 (2022-04-26 21:49:47)

Added unit test for the EnsureDeleted function of dynamichelper
### Which issue this PR addresses:
To improve unit test coverage for dynamichelper. This ADO task can be separated to multiple PRs for each function in the dynamichelper as suggested. This PR is for the function `EnsureDeleted`

### What this PR does / why we need it:
The dynamichelper is used in multiple important parts of our codebase and need better unit test coverage

### Is there any documentation that needs to be updated for this PR?
No, this PR is mainly to meet the requirement in the ADO task [13525478 - Improve unit test coverage for dynamichelper](https://msazure.visualstudio.com/AzureRedHatOpenShift/_boards/board/t/Skippy/Stories/?workitem=13525478)

hawkowl: /azp run e2e
azure-pipelines[bot]: <samp>
Azure Pipelines successfully started running 1 pipeline(s).<br>

</samp>
github-actions[bot]: Please rebase pull request.

Patch:
diff --git a/.sha256sum b/.sha256sum
index d3c249a31..ada3d593a 100644
--- a/.sha256sum
+++ b/.sha256sum
@@ -1,3 +1,4 @@
 468fa0da0a50d50640ec57843ad288af343128b39f5bf23e76e4e336580883d4  swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2020-04-30/redhatopenshift.json
 c323c84befa5ea11da50a2407050abed6540ea01e796720bc2241604ce80567c  swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/preview/2021-09-01-preview/redhatopenshift.json
 e4e522e41855de71c0318db31cbd96c8713e7a74e7c81911bb494f0b194b3f43  swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-04-01/redhatopenshift.json
+895ec8758158ab788088bc92eb2a2c4aa7a448bc53192aea4cb38efa033fdd2d  swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/redhatopenshift.json
diff --git a/Makefile b/Makefile
index 26884be97..cd29e2836 100644
--- a/Makefile
+++ b/Makefile
@@ -56,7 +56,7 @@ clean:
 	find -type d -name 'gomock_reflect_[0-9]*' -exec rm -rf {} \+ 2>/dev/null
 
 client: generate
-	hack/build-client.sh "${AUTOREST_IMAGE}" 2020-04-30 2021-09-01-preview 2022-04-01
+	hack/build-client.sh "${AUTOREST_IMAGE}" 2020-04-30 2021-09-01-preview 2022-04-01 2022-09-04
 
 # TODO: hard coding dev-config.yaml is clunky; it is also probably convenient to
 # override COMMIT.
diff --git a/cmd/aro/operator.go b/cmd/aro/operator.go
index 83a3f8fb0..0b02c219f 100644
--- a/cmd/aro/operator.go
+++ b/cmd/aro/operator.go
@@ -12,6 +12,7 @@ import (
 	consoleclient "github.com/openshift/client-go/console/clientset/versioned"
 	imageregistryclient "github.com/openshift/client-go/imageregistry/clientset/versioned"
 	machineclient "github.com/openshift/client-go/machine/clientset/versioned"
+	operatorclient "github.com/openshift/client-go/operator/clientset/versioned"
 	securityclient "github.com/openshift/client-go/security/clientset/versioned"
 	mcoclient "github.com/openshift/machine-config-operator/pkg/generated/clientset/versioned"
 	"github.com/sirupsen/logrus"
@@ -108,6 +109,10 @@ func operator(ctx context.Context, log *logrus.Entry) error {
 	if err != nil {
 		return err
 	}
+	operatorcli, err := operatorclient.NewForConfig(restConfig)
+	if err != nil {
+		return err
+	}
 	// TODO (NE): dh is sometimes passed, sometimes created later. Can we standardize?
 	dh, err := dynamichelper.New(log, restConfig)
 	if err != nil {
@@ -225,7 +230,7 @@ func operator(ctx context.Context, log *logrus.Entry) error {
 
 	if err = (checker.NewReconciler(
 		log.WithField("controller", checker.ControllerName),
-		arocli, kubernetescli, maocli, role)).SetupWithManager(mgr); err != nil {
+		arocli, kubernetescli, maocli, operatorcli, configcli, role)).SetupWithManager(mgr); err != nil {
 		return fmt.Errorf("unable to create controller %s: %v", checker.ControllerName, err)
 	}
 
diff --git a/cmd/aro/rp.go b/cmd/aro/rp.go
index 0512f20c7..e557628f4 100644
--- a/cmd/aro/rp.go
+++ b/cmd/aro/rp.go
@@ -20,6 +20,7 @@ import (
 	_ "github.com/Azure/ARO-RP/pkg/api/v20200430"
 	_ "github.com/Azure/ARO-RP/pkg/api/v20210901preview"
 	_ "github.com/Azure/ARO-RP/pkg/api/v20220401"
+	_ "github.com/Azure/ARO-RP/pkg/api/v20220904"
 	"github.com/Azure/ARO-RP/pkg/backend"
 	"github.com/Azure/ARO-RP/pkg/database"
 	"github.com/Azure/ARO-RP/pkg/env"
diff --git a/go.mod b/go.mod
index 697d94f43..09dbb0d44 100644
--- a/go.mod
+++ b/go.mod
@@ -44,6 +44,7 @@ require (
 	github.com/openshift/client-go v0.0.0-20211209144617-7385dd6338e3
 	github.com/openshift/console-operator v0.0.0-20220407014945-45d37e70e0c2
 	github.com/openshift/hive v1.1.16
+	github.com/openshift/hive/apis v0.0.0
 	github.com/openshift/installer v0.16.1
 	github.com/openshift/library-go v0.0.0-20220405134141-226b07263a02
 	github.com/openshift/machine-config-operator v3.11.0+incompatible
@@ -259,7 +260,6 @@ require (
 	github.com/openshift/cluster-api-provider-ibmcloud v0.0.0-20211008100740-4d7907adbd6b // indirect
 	github.com/openshift/cluster-api-provider-libvirt v0.2.1-0.20191219173431-2336783d4603 // indirect
 	github.com/openshift/cluster-api-provider-ovirt v0.1.1-0.20211111151530-06177b773958 // indirect
-	github.com/openshift/hive/apis v0.0.0 // indirect
 	github.com/ovirt/go-ovirt v0.0.0-20210308100159-ac0bcbc88d7c // indirect
 	github.com/pascaldekloe/name v0.0.0-20180628100202-0fd16699aae1 // indirect
 	github.com/pborman/uuid v1.2.1 // indirect
diff --git a/hack/validate-imports/allowed-import-names.yaml b/hack/validate-imports/allowed-import-names.yaml
index 994645e34..87a5a7a54 100644
--- a/hack/validate-imports/allowed-import-names.yaml
+++ b/hack/validate-imports/allowed-import-names.yaml
@@ -108,3 +108,7 @@ allowedImportNames:
     - ctrl
   github.com/openshift/hive/pkg/client/clientset/versioned:
     - hiveclient
+  github.com/openshift/hive/pkg/client/clientset/versioned/fake:
+    - hivefake
+  github.com/openshift/hive/apis/hive/v1:
+    - hivev1
diff --git a/pkg/api/admin/openshiftcluster.go b/pkg/api/admin/openshiftcluster.go
index 8115f4b5b..ad8b79fdb 100644
--- a/pkg/api/admin/openshiftcluster.go
+++ b/pkg/api/admin/openshiftcluster.go
@@ -54,6 +54,7 @@ type OpenShiftClusterProperties struct {
 	RegistryProfiles                []RegistryProfile       `json:"registryProfiles,omitempty"`
 	ImageRegistryStorageAccountName string                  `json:"imageRegistryStorageAccountName,omitempty"`
 	InfraID                         string                  `json:"infraId,omitempty"`
+	HiveProfile                     HiveProfile             `json:"hiveProfile,omitempty"`
 }
 
 // ProvisioningState represents a provisioning state.
@@ -295,3 +296,7 @@ type SystemData struct {
 	LastModifiedByType CreatedByType `json:"lastModifiedByType,omitempty"`
 	LastModifiedAt     *time.Time    `json:"lastModifiedAt,omitempty"`
 }
+
+type HiveProfile struct {
+	Namespace string `json:"namespace,omitempty"`
+}
diff --git a/pkg/api/admin/openshiftcluster_convert.go b/pkg/api/admin/openshiftcluster_convert.go
index 15949e5e7..714da31cc 100644
--- a/pkg/api/admin/openshiftcluster_convert.go
+++ b/pkg/api/admin/openshiftcluster_convert.go
@@ -122,6 +122,9 @@ func (c *openShiftClusterConverter) ToExternal(oc *api.OpenShiftCluster) interfa
 		}
 	}
 
+	out.Properties.HiveProfile = HiveProfile{
+		Namespace: oc.Properties.HiveProfile.Namespace,
+	}
 	out.SystemData = SystemData{
 		CreatedBy:          oc.SystemData.CreatedBy,
 		CreatedAt:          oc.SystemData.CreatedAt,
@@ -169,6 +172,7 @@ func (c *openShiftClusterConverter) ToInternal(_oc interface{}, out *api.OpenShi
 	}
 	out.Properties.ArchitectureVersion = api.ArchitectureVersion(oc.Properties.ArchitectureVersion)
 	out.Properties.InfraID = oc.Properties.InfraID
+	out.Properties.HiveProfile.Namespace = oc.Properties.HiveProfile.Namespace
 	out.Properties.ProvisioningState = api.ProvisioningState(oc.Properties.ProvisioningState)
 	out.Properties.LastProvisioningState = api.ProvisioningState(oc.Properties.LastProvisioningState)
 	out.Properties.FailedProvisioningState = api.ProvisioningState(oc.Properties.FailedProvisioningState)
diff --git a/pkg/api/openshiftcluster.go b/pkg/api/openshiftcluster.go
index 79f1f6473..dac969e6e 100644
--- a/pkg/api/openshiftcluster.go
+++ b/pkg/api/openshiftcluster.go
@@ -136,6 +136,7 @@ type OpenShiftClusterProperties struct {
 
 	InfraID string      `json:"infraId,omitempty"`
 	SSHKey  SecureBytes `json:"sshKey,omitempty"`
+
 	// AdminKubeconfig is installer generated kubeconfig. It is 10 year config,
 	// and should never be returned to the user.
 	AdminKubeconfig SecureBytes `json:"adminKubeconfig,omitempty"`
@@ -150,6 +151,8 @@ type OpenShiftClusterProperties struct {
 	UserAdminKubeconfig SecureBytes `json:"userAdminKubeconfig,omitempty"`
 
 	RegistryProfiles []*RegistryProfile `json:"registryProfiles,omitempty"`
+
+	HiveProfile HiveProfile `json:"hiveProfile,omitempty"`
 }
 
 // ProvisioningState represents a provisioning state
@@ -406,3 +409,10 @@ const (
 	// ArchitectureVersionV2: 4.5: 1 load balancer, 1 NSG
 	ArchitectureVersionV2
 )
+
+// HiveProfile represents the hive related data of a cluster
+type HiveProfile struct {
+	MissingFields
+
+	Namespace string `json:"namespace,omitempty"`
+}
diff --git a/pkg/api/v20220904/generate.go b/pkg/api/v20220904/generate.go
new file mode 100644
index 000000000..7f3265594
--- /dev/null
+++ b/pkg/api/v20220904/generate.go
@@ -0,0 +1,6 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+//go:generate go run ../../../hack/swagger github.com/Azure/ARO-RP/pkg/api/v20220904 ../../../swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04
diff --git a/pkg/api/v20220904/openshiftcluster.go b/pkg/api/v20220904/openshiftcluster.go
new file mode 100644
index 000000000..113eedb66
--- /dev/null
+++ b/pkg/api/v20220904/openshiftcluster.go
@@ -0,0 +1,248 @@
+package v20220904
+
+import "time"
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+// OpenShiftClusterList represents a list of OpenShift clusters.
+type OpenShiftClusterList struct {
+	// The list of OpenShift clusters.
+	OpenShiftClusters []*OpenShiftCluster `json:"value"`
+
+	// The link used to get the next page of operations.
+	NextLink string `json:"nextLink,omitempty"`
+}
+
+// OpenShiftCluster represents an Azure Red Hat OpenShift cluster.
+type OpenShiftCluster struct {
+	// The resource ID.
+	ID string `json:"id,omitempty" mutable:"case"`
+
+	// The resource name.
+	Name string `json:"name,omitempty" mutable:"case"`
+
+	// The resource type.
+	Type string `json:"type,omitempty" mutable:"case"`
+
+	// The resource location.
+	Location string `json:"location,omitempty"`
+
+	// SystemData - The system metadata relating to this resource
+	SystemData *SystemData `json:"systemData,omitempty"`
+
+	// The resource tags.
+	Tags Tags `json:"tags,omitempty" mutable:"true"`
+
+	// The cluster properties.
+	Properties OpenShiftClusterProperties `json:"properties,omitempty"`
+}
+
+// Tags represents an OpenShift cluster's tags.
+type Tags map[string]string
+
+// OpenShiftClusterProperties represents an OpenShift cluster's properties.
+type OpenShiftClusterProperties struct {
+	// The cluster provisioning state.
+	ProvisioningState ProvisioningState `json:"provisioningState,omitempty"`
+
+	// The cluster profile.
+	ClusterProfile ClusterProfile `json:"clusterProfile,omitempty"`
+
+	// The console profile.
+	ConsoleProfile ConsoleProfile `json:"consoleProfile,omitempty"`
+
+	// The cluster service principal profile.
+	ServicePrincipalProfile ServicePrincipalProfile `json:"servicePrincipalProfile,omitempty"`
+
+	// The cluster network profile.
+	NetworkProfile NetworkProfile `json:"networkProfile,omitempty"`
+
+	// The cluster master profile.
+	MasterProfile MasterProfile `json:"masterProfile,omitempty"`
+
+	// The cluster worker profiles.
+	WorkerProfiles []WorkerProfile `json:"workerProfiles,omitempty"`
+
+	// The cluster API server profile.
+	APIServerProfile APIServerProfile `json:"apiserverProfile,omitempty"`
+
+	// The cluster ingress profiles.
+	IngressProfiles []IngressProfile `json:"ingressProfiles,omitempty"`
+}
+
+// ProvisioningState represents a provisioning state.
+type ProvisioningState string
+
+// ProvisioningState constants.
+const (
+	ProvisioningStateCreating      ProvisioningState = "Creating"
+	ProvisioningStateUpdating      ProvisioningState = "Updating"
+	ProvisioningStateAdminUpdating ProvisioningState = "AdminUpdating"
+	ProvisioningStateDeleting      ProvisioningState = "Deleting"
+	ProvisioningStateSucceeded     ProvisioningState = "Succeeded"
+	ProvisioningStateFailed        ProvisioningState = "Failed"
+)
+
+// FipsValidatedModules determines if FIPS is used.
+type FipsValidatedModules string
+
+// FipsValidatedModules constants.
+const (
+	FipsValidatedModulesEnabled  FipsValidatedModules = "Enabled"
+	FipsValidatedModulesDisabled FipsValidatedModules = "Disabled"
+)
+
+// ClusterProfile represents a cluster profile.
+type ClusterProfile struct {
+	// The pull secret for the cluster.
+	PullSecret string `json:"pullSecret,omitempty"`
+
+	// The domain for the cluster.
+	Domain string `json:"domain,omitempty"`
+
+	// The version of the cluster.
+	Version string `json:"version,omitempty"`
+
+	// The ID of the cluster resource group.
+	ResourceGroupID string `json:"resourceGroupId,omitempty"`
+
+	// If FIPS validated crypto modules are used
+	FipsValidatedModules FipsValidatedModules `json:"fipsValidatedModules,omitempty"`
+}
+
+// ConsoleProfile represents a console profile.
+type ConsoleProfile struct {
+	// The URL to access the cluster console.
+	URL string `json:"url,omitempty"`
+}
+
+// ServicePrincipalProfile represents a service principal profile.
+type ServicePrincipalProfile struct {
+	// The client ID used for the cluster.
+	ClientID string `json:"clientId,omitempty" mutable:"true"`
+
+	// The client secret used for the cluster.
+	ClientSecret string `json:"clientSecret,omitempty" mutable:"true"`
+}
+
+// NetworkProfile represents a network profile.
+type NetworkProfile struct {
+	// The CIDR used for OpenShift/Kubernetes Pods.
+	PodCIDR string `json:"podCidr,omitempty"`
+
+	// The CIDR used for OpenShift/Kubernetes Services.
+	ServiceCIDR string `json:"serviceCidr,omitempty"`
+}
+
+// EncryptionAtHost represents encryption at host state
+type EncryptionAtHost string
+
+// EncryptionAtHost constants
+const (
+	EncryptionAtHostEnabled  EncryptionAtHost = "Enabled"
+	EncryptionAtHostDisabled EncryptionAtHost = "Disabled"
+)
+
+// MasterProfile represents a master profile.
+type MasterProfile struct {
+	// The size of the master VMs.
+	VMSize VMSize `json:"vmSize,omitempty"`
+
+	// The Azure resource ID of the master subnet.
+	SubnetID string `json:"subnetId,omitempty"`
+
+	// Whether master virtual machines are encrypted at host.
+	EncryptionAtHost EncryptionAtHost `json:"encryptionAtHost,omitempty"`
+
+	// The resource ID of an associated DiskEncryptionSet, if applicable.
+	DiskEncryptionSetID string `json:"diskEncryptionSetId,omitempty"`
+}
+
+// VM size availability varies by region.
+// If a node contains insufficient compute resources (memory, cpu, etc.), pods might fail to run correctly.
+// For more details on restricted VM sizes, see: https://docs.microsoft.com/en-us/azure/openshift/support-policies-v4#supported-virtual-machine-sizes
+type VMSize string
+
+// WorkerProfile represents a worker profile.
+type WorkerProfile struct {
+	// The worker profile name.
+	Name string `json:"name,omitempty"`
+
+	// The size of the worker VMs.
+	VMSize VMSize `json:"vmSize,omitempty"`
+
+	// The disk size of the worker VMs.
+	DiskSizeGB int `json:"diskSizeGB,omitempty"`
+
+	// The Azure resource ID of the worker subnet.
+	SubnetID string `json:"subnetId,omitempty"`
+
+	// The number of worker VMs.
+	Count int `json:"count,omitempty"`
+
+	// Whether master virtual machines are encrypted at host.
+	EncryptionAtHost EncryptionAtHost `json:"encryptionAtHost,omitempty"`
+
+	// The resource ID of an associated DiskEncryptionSet, if applicable.
+	DiskEncryptionSetID string `json:"diskEncryptionSetId,omitempty"`
+}
+
+// APIServerProfile represents an API server profile.
+type APIServerProfile struct {
+	// API server visibility.
+	Visibility Visibility `json:"visibility,omitempty"`
+
+	// The URL to access the cluster API server.
+	URL string `json:"url,omitempty"`
+
+	// The IP of the cluster API server.
+	IP string `json:"ip,omitempty"`
+}
+
+// Visibility represents visibility.
+type Visibility string
+
+// Visibility constants
+const (
+	VisibilityPublic  Visibility = "Public"
+	VisibilityPrivate Visibility = "Private"
+)
+
+// IngressProfile represents an ingress profile.
+type IngressProfile struct {
+	// The ingress profile name.
+	Name string `json:"name,omitempty"`
+
+	// Ingress visibility.
+	Visibility Visibility `json:"visibility,omitempty"`
+
+	// The IP of the ingress.
+	IP string `json:"ip,omitempty"`
+}
+
+// CreatedByType by defines user type, which executed the request
+type CreatedByType string
+
+const (
+	CreatedByTypeApplication     CreatedByType = "Application"
+	CreatedByTypeKey             CreatedByType = "Key"
+	CreatedByTypeManagedIdentity CreatedByType = "ManagedIdentity"
+	CreatedByTypeUser            CreatedByType = "User"
+)
+
+// SystemData metadata pertaining to creation and last modification of the resource.
+type SystemData struct {
+	// The identity that created the resource.
+	CreatedBy string `json:"createdBy,omitempty"`
+	// The type of identity that created the resource. Possible values include: 'User', 'Application', 'ManagedIdentity', 'Key'
+	CreatedByType CreatedByType `json:"createdByType,omitempty"`
+	// The timestamp of resource creation (UTC).
+	CreatedAt *time.Time `json:"createdAt,omitempty"`
+	// The identity that last modified the resource.
+	LastModifiedBy string `json:"lastModifiedBy,omitempty"`
+	// The type of identity that last modified the resource. Possible values include: 'User', 'Application', 'ManagedIdentity', 'Key'
+	LastModifiedByType CreatedByType `json:"lastModifiedByType,omitempty"`
+	// The type of identity that last modified the resource.
+	LastModifiedAt *time.Time `json:"lastModifiedAt,omitempty"`
+}
diff --git a/pkg/api/v20220904/openshiftcluster_convert.go b/pkg/api/v20220904/openshiftcluster_convert.go
new file mode 100644
index 000000000..6e7c7f105
--- /dev/null
+++ b/pkg/api/v20220904/openshiftcluster_convert.go
@@ -0,0 +1,183 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"github.com/Azure/ARO-RP/pkg/api"
+)
+
+type openShiftClusterConverter struct{}
+
+// ToExternal returns a new external representation of the internal object,
+// reading from the subset of the internal object's fields that appear in the
+// external representation.  ToExternal does not modify its argument; there is
+// no pointer aliasing between the passed and returned objects
+func (c *openShiftClusterConverter) ToExternal(oc *api.OpenShiftCluster) interface{} {
+	out := &OpenShiftCluster{
+		ID:       oc.ID,
+		Name:     oc.Name,
+		Type:     oc.Type,
+		Location: oc.Location,
+		Properties: OpenShiftClusterProperties{
+			ProvisioningState: ProvisioningState(oc.Properties.ProvisioningState),
+			ClusterProfile: ClusterProfile{
+				PullSecret:           string(oc.Properties.ClusterProfile.PullSecret),
+				Domain:               oc.Properties.ClusterProfile.Domain,
+				Version:              oc.Properties.ClusterProfile.Version,
+				ResourceGroupID:      oc.Properties.ClusterProfile.ResourceGroupID,
+				FipsValidatedModules: FipsValidatedModules(oc.Properties.ClusterProfile.FipsValidatedModules),
+			},
+			ConsoleProfile: ConsoleProfile{
+				URL: oc.Properties.ConsoleProfile.URL,
+			},
+			ServicePrincipalProfile: ServicePrincipalProfile{
+				ClientID:     oc.Properties.ServicePrincipalProfile.ClientID,
+				ClientSecret: string(oc.Properties.ServicePrincipalProfile.ClientSecret),
+			},
+			NetworkProfile: NetworkProfile{
+				PodCIDR:     oc.Properties.NetworkProfile.PodCIDR,
+				ServiceCIDR: oc.Properties.NetworkProfile.ServiceCIDR,
+			},
+			MasterProfile: MasterProfile{
+				VMSize:              VMSize(oc.Properties.MasterProfile.VMSize),
+				SubnetID:            oc.Properties.MasterProfile.SubnetID,
+				EncryptionAtHost:    EncryptionAtHost(oc.Properties.MasterProfile.EncryptionAtHost),
+				DiskEncryptionSetID: oc.Properties.MasterProfile.DiskEncryptionSetID,
+			},
+			APIServerProfile: APIServerProfile{
+				Visibility: Visibility(oc.Properties.APIServerProfile.Visibility),
+				URL:        oc.Properties.APIServerProfile.URL,
+				IP:         oc.Properties.APIServerProfile.IP,
+			},
+		},
+	}
+
+	if oc.Properties.WorkerProfiles != nil {
+		out.Properties.WorkerProfiles = make([]WorkerProfile, 0, len(oc.Properties.WorkerProfiles))
+		for _, p := range oc.Properties.WorkerProfiles {
+			out.Properties.WorkerProfiles = append(out.Properties.WorkerProfiles, WorkerProfile{
+				Name:                p.Name,
+				VMSize:              VMSize(p.VMSize),
+				DiskSizeGB:          p.DiskSizeGB,
+				SubnetID:            p.SubnetID,
+				Count:               p.Count,
+				EncryptionAtHost:    EncryptionAtHost(p.EncryptionAtHost),
+				DiskEncryptionSetID: p.DiskEncryptionSetID,
+			})
+		}
+	}
+
+	if oc.Properties.IngressProfiles != nil {
+		out.Properties.IngressProfiles = make([]IngressProfile, 0, len(oc.Properties.IngressProfiles))
+		for _, p := range oc.Properties.IngressProfiles {
+			out.Properties.IngressProfiles = append(out.Properties.IngressProfiles, IngressProfile{
+				Name:       p.Name,
+				Visibility: Visibility(p.Visibility),
+				IP:         p.IP,
+			})
+		}
+	}
+
+	if oc.Tags != nil {
+		out.Tags = make(map[string]string, len(oc.Tags))
+		for k, v := range oc.Tags {
+			out.Tags[k] = v
+		}
+	}
+
+	out.SystemData = &SystemData{
+		CreatedBy:          oc.SystemData.CreatedBy,
+		CreatedAt:          oc.SystemData.CreatedAt,
+		CreatedByType:      CreatedByType(oc.SystemData.CreatedByType),
+		LastModifiedBy:     oc.SystemData.LastModifiedBy,
+		LastModifiedAt:     oc.SystemData.LastModifiedAt,
+		LastModifiedByType: CreatedByType(oc.SystemData.LastModifiedByType),
+	}
+
+	return out
+}
+
+// ToExternalList returns a slice of external representations of the internal
+// objects
+func (c *openShiftClusterConverter) ToExternalList(ocs []*api.OpenShiftCluster, nextLink string) interface{} {
+	l := &OpenShiftClusterList{
+		OpenShiftClusters: make([]*OpenShiftCluster, 0, len(ocs)),
+		NextLink:          nextLink,
+	}
+
+	for _, oc := range ocs {
+		l.OpenShiftClusters = append(l.OpenShiftClusters, c.ToExternal(oc).(*OpenShiftCluster))
+	}
+
+	return l
+}
+
+// ToInternal overwrites in place a pre-existing internal object, setting (only)
+// all mapped fields from the external representation. ToInternal modifies its
+// argument; there is no pointer aliasing between the passed and returned
+// objects
+func (c *openShiftClusterConverter) ToInternal(_oc interface{}, out *api.OpenShiftCluster) {
+	oc := _oc.(*OpenShiftCluster)
+
+	out.ID = oc.ID
+	out.Name = oc.Name
+	out.Type = oc.Type
+	out.Location = oc.Location
+	out.Tags = nil
+	if oc.Tags != nil {
+		out.Tags = make(map[string]string, len(oc.Tags))
+		for k, v := range oc.Tags {
+			out.Tags[k] = v
+		}
+	}
+	out.Properties.ProvisioningState = api.ProvisioningState(oc.Properties.ProvisioningState)
+	out.Properties.ClusterProfile.PullSecret = api.SecureString(oc.Properties.ClusterProfile.PullSecret)
+	out.Properties.ClusterProfile.Domain = oc.Properties.ClusterProfile.Domain
+	out.Properties.ClusterProfile.Version = oc.Properties.ClusterProfile.Version
+	out.Properties.ClusterProfile.ResourceGroupID = oc.Properties.ClusterProfile.ResourceGroupID
+	out.Properties.ConsoleProfile.URL = oc.Properties.ConsoleProfile.URL
+	out.Properties.ClusterProfile.FipsValidatedModules = api.FipsValidatedModules(oc.Properties.ClusterProfile.FipsValidatedModules)
+	out.Properties.ServicePrincipalProfile.ClientID = oc.Properties.ServicePrincipalProfile.ClientID
+	out.Properties.ServicePrincipalProfile.ClientSecret = api.SecureString(oc.Properties.ServicePrincipalProfile.ClientSecret)
+	out.Properties.NetworkProfile.PodCIDR = oc.Properties.NetworkProfile.PodCIDR
+	out.Properties.NetworkProfile.ServiceCIDR = oc.Properties.NetworkProfile.ServiceCIDR
+	out.Properties.MasterProfile.VMSize = api.VMSize(oc.Properties.MasterProfile.VMSize)
+	out.Properties.MasterProfile.SubnetID = oc.Properties.MasterProfile.SubnetID
+	out.Properties.MasterProfile.EncryptionAtHost = api.EncryptionAtHost(oc.Properties.MasterProfile.EncryptionAtHost)
+	out.Properties.MasterProfile.DiskEncryptionSetID = oc.Properties.MasterProfile.DiskEncryptionSetID
+	out.Properties.WorkerProfiles = nil
+	if oc.Properties.WorkerProfiles != nil {
+		out.Properties.WorkerProfiles = make([]api.WorkerProfile, len(oc.Properties.WorkerProfiles))
+		for i := range oc.Properties.WorkerProfiles {
+			out.Properties.WorkerProfiles[i].Name = oc.Properties.WorkerProfiles[i].Name
+			out.Properties.WorkerProfiles[i].VMSize = api.VMSize(oc.Properties.WorkerProfiles[i].VMSize)
+			out.Properties.WorkerProfiles[i].DiskSizeGB = oc.Properties.WorkerProfiles[i].DiskSizeGB
+			out.Properties.WorkerProfiles[i].SubnetID = oc.Properties.WorkerProfiles[i].SubnetID
+			out.Properties.WorkerProfiles[i].Count = oc.Properties.WorkerProfiles[i].Count
+			out.Properties.WorkerProfiles[i].EncryptionAtHost = api.EncryptionAtHost(oc.Properties.WorkerProfiles[i].EncryptionAtHost)
+			out.Properties.WorkerProfiles[i].DiskEncryptionSetID = oc.Properties.WorkerProfiles[i].DiskEncryptionSetID
+		}
+	}
+	out.Properties.APIServerProfile.Visibility = api.Visibility(oc.Properties.APIServerProfile.Visibility)
+	out.Properties.APIServerProfile.URL = oc.Properties.APIServerProfile.URL
+	out.Properties.APIServerProfile.IP = oc.Properties.APIServerProfile.IP
+	out.Properties.IngressProfiles = nil
+	if oc.Properties.IngressProfiles != nil {
+		out.Properties.IngressProfiles = make([]api.IngressProfile, len(oc.Properties.IngressProfiles))
+		for i := range oc.Properties.IngressProfiles {
+			out.Properties.IngressProfiles[i].Name = oc.Properties.IngressProfiles[i].Name
+			out.Properties.IngressProfiles[i].Visibility = api.Visibility(oc.Properties.IngressProfiles[i].Visibility)
+			out.Properties.IngressProfiles[i].IP = oc.Properties.IngressProfiles[i].IP
+		}
+	}
+
+	out.SystemData = api.SystemData{
+		CreatedBy:          oc.SystemData.CreatedBy,
+		CreatedAt:          oc.SystemData.CreatedAt,
+		CreatedByType:      api.CreatedByType(oc.SystemData.CreatedByType),
+		LastModifiedBy:     oc.SystemData.LastModifiedBy,
+		LastModifiedAt:     oc.SystemData.LastModifiedAt,
+		LastModifiedByType: api.CreatedByType(oc.SystemData.CreatedByType),
+	}
+}
diff --git a/pkg/api/v20220904/openshiftcluster_example.go b/pkg/api/v20220904/openshiftcluster_example.go
new file mode 100644
index 000000000..d239d90bc
--- /dev/null
+++ b/pkg/api/v20220904/openshiftcluster_example.go
@@ -0,0 +1,63 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"github.com/Azure/ARO-RP/pkg/api"
+)
+
+func exampleOpenShiftCluster() *OpenShiftCluster {
+	doc := api.ExampleOpenShiftClusterDocument()
+	return (&openShiftClusterConverter{}).ToExternal(doc.OpenShiftCluster).(*OpenShiftCluster)
+}
+
+// ExampleOpenShiftClusterPatchParameter returns an example OpenShiftCluster
+// object that an end-user might send to create a cluster in a PATCH request
+func ExampleOpenShiftClusterPatchParameter() interface{} {
+	oc := ExampleOpenShiftClusterPutParameter().(*OpenShiftCluster)
+	oc.Location = ""
+	oc.SystemData = nil
+
+	return oc
+}
+
+// ExampleOpenShiftClusterPutParameter returns an example OpenShiftCluster
+// object that an end-user might send to create a cluster in a PUT request
+func ExampleOpenShiftClusterPutParameter() interface{} {
+	oc := exampleOpenShiftCluster()
+	oc.ID = ""
+	oc.Name = ""
+	oc.Type = ""
+	oc.Properties.ProvisioningState = ""
+	oc.Properties.ClusterProfile.Version = ""
+	oc.Properties.ClusterProfile.FipsValidatedModules = FipsValidatedModules(FipsValidatedModulesEnabled)
+	oc.Properties.ConsoleProfile.URL = ""
+	oc.Properties.APIServerProfile.URL = ""
+	oc.Properties.APIServerProfile.IP = ""
+	oc.Properties.IngressProfiles[0].IP = ""
+	oc.Properties.MasterProfile.EncryptionAtHost = EncryptionAtHost(EncryptionAtHostEnabled)
+	oc.SystemData = nil
+
+	return oc
+}
+
+// ExampleOpenShiftClusterResponse returns an example OpenShiftCluster object
+// that the RP might return to an end-user
+func ExampleOpenShiftClusterResponse() interface{} {
+	oc := exampleOpenShiftCluster()
+	oc.Properties.ClusterProfile.PullSecret = ""
+	oc.Properties.ServicePrincipalProfile.ClientSecret = ""
+
+	return oc
+}
+
+// ExampleOpenShiftClusterListResponse returns an example OpenShiftClusterList
+// object that the RP might return to an end-user
+func ExampleOpenShiftClusterListResponse() interface{} {
+	return &OpenShiftClusterList{
+		OpenShiftClusters: []*OpenShiftCluster{
+			ExampleOpenShiftClusterResponse().(*OpenShiftCluster),
+		},
+	}
+}
diff --git a/pkg/api/v20220904/openshiftcluster_marshal.go b/pkg/api/v20220904/openshiftcluster_marshal.go
new file mode 100644
index 000000000..7e9ee2ff4
--- /dev/null
+++ b/pkg/api/v20220904/openshiftcluster_marshal.go
@@ -0,0 +1,20 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"encoding/json"
+)
+
+// UnmarshalJSON unmarshals tags.  We override this to ensure that PATCH
+// behaviour overwrites an existing tags map rather than endlessly adding to it
+func (t *Tags) UnmarshalJSON(b []byte) error {
+	var m map[string]string
+	err := json.Unmarshal(b, &m)
+	if err != nil {
+		return err
+	}
+	*t = m
+	return nil
+}
diff --git a/pkg/api/v20220904/openshiftclusteradminkubeconfig.go b/pkg/api/v20220904/openshiftclusteradminkubeconfig.go
new file mode 100644
index 000000000..e5f4e9b89
--- /dev/null
+++ b/pkg/api/v20220904/openshiftclusteradminkubeconfig.go
@@ -0,0 +1,10 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+// OpenShiftClusterAdminKubeconfig represents an OpenShift cluster's admin kubeconfig.
+type OpenShiftClusterAdminKubeconfig struct {
+	// The base64-encoded kubeconfig file.
+	Kubeconfig []byte `json:"kubeconfig,omitempty"`
+}
diff --git a/pkg/api/v20220904/openshiftclusteradminkubeconfig_convert.go b/pkg/api/v20220904/openshiftclusteradminkubeconfig_convert.go
new file mode 100644
index 000000000..92ebe0d2f
--- /dev/null
+++ b/pkg/api/v20220904/openshiftclusteradminkubeconfig_convert.go
@@ -0,0 +1,21 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"github.com/Azure/ARO-RP/pkg/api"
+)
+
+type openShiftClusterAdminKubeconfigConverter struct{}
+
+// openShiftClusterAdminKubeconfigConverter returns a new external representation
+// of the internal object, reading from the subset of the internal object's
+// fields that appear in the external representation.  ToExternal does not
+// modify its argument; there is no pointer aliasing between the passed and
+// returned objects.
+func (*openShiftClusterAdminKubeconfigConverter) ToExternal(oc *api.OpenShiftCluster) interface{} {
+	return &OpenShiftClusterAdminKubeconfig{
+		Kubeconfig: oc.Properties.UserAdminKubeconfig,
+	}
+}
diff --git a/pkg/api/v20220904/openshiftclusteradminkubeconfig_example.go b/pkg/api/v20220904/openshiftclusteradminkubeconfig_example.go
new file mode 100644
index 000000000..4f518128f
--- /dev/null
+++ b/pkg/api/v20220904/openshiftclusteradminkubeconfig_example.go
@@ -0,0 +1,12 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+// ExampleOpenShiftClusterAdminKubeconfigResponse returns an example
+// OpenShiftClusterAdminKubeconfig object that the RP might return to an end-user
+func ExampleOpenShiftClusterAdminKubeconfigResponse() interface{} {
+	return &OpenShiftClusterAdminKubeconfig{
+		Kubeconfig: []byte("{}"),
+	}
+}
diff --git a/pkg/api/v20220904/openshiftclustercredentials.go b/pkg/api/v20220904/openshiftclustercredentials.go
new file mode 100644
index 000000000..35d2f8964
--- /dev/null
+++ b/pkg/api/v20220904/openshiftclustercredentials.go
@@ -0,0 +1,13 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+// OpenShiftClusterCredentials represents an OpenShift cluster's credentials.
+type OpenShiftClusterCredentials struct {
+	// The username for the kubeadmin user.
+	KubeadminUsername string `json:"kubeadminUsername,omitempty"`
+
+	// The password for the kubeadmin user.
+	KubeadminPassword string `json:"kubeadminPassword,omitempty"`
+}
diff --git a/pkg/api/v20220904/openshiftclustercredentials_convert.go b/pkg/api/v20220904/openshiftclustercredentials_convert.go
new file mode 100644
index 000000000..477cd1f74
--- /dev/null
+++ b/pkg/api/v20220904/openshiftclustercredentials_convert.go
@@ -0,0 +1,24 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"github.com/Azure/ARO-RP/pkg/api"
+)
+
+type openShiftClusterCredentialsConverter struct{}
+
+// OpenShiftClusterCredentialsToExternal returns a new external representation
+// of the internal object, reading from the subset of the internal object's
+// fields that appear in the external representation.  ToExternal does not
+// modify its argument; there is no pointer aliasing between the passed and
+// returned objects.
+func (*openShiftClusterCredentialsConverter) ToExternal(oc *api.OpenShiftCluster) interface{} {
+	out := &OpenShiftClusterCredentials{
+		KubeadminUsername: "kubeadmin",
+		KubeadminPassword: string(oc.Properties.KubeadminPassword),
+	}
+
+	return out
+}
diff --git a/pkg/api/v20220904/openshiftclustercredentials_example.go b/pkg/api/v20220904/openshiftclustercredentials_example.go
new file mode 100644
index 000000000..4a9a75ac6
--- /dev/null
+++ b/pkg/api/v20220904/openshiftclustercredentials_example.go
@@ -0,0 +1,13 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+// ExampleOpenShiftClusterCredentialsResponse returns an example
+// OpenShiftClusterCredentials object that the RP might return to an end-user
+func ExampleOpenShiftClusterCredentialsResponse() interface{} {
+	return &OpenShiftClusterCredentials{
+		KubeadminUsername: "kubeadmin",
+		KubeadminPassword: "password",
+	}
+}
diff --git a/pkg/api/v20220904/register.go b/pkg/api/v20220904/register.go
new file mode 100644
index 000000000..289fc5f28
--- /dev/null
+++ b/pkg/api/v20220904/register.go
@@ -0,0 +1,38 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"github.com/Azure/ARO-RP/pkg/api"
+)
+
+// APIVersion contains a version string as it will be used by clients
+const APIVersion = "2022-09-04"
+
+const (
+	resourceProviderNamespace = "Microsoft.RedHatOpenShift"
+	resourceType              = "openShiftClusters"
+)
+
+func init() {
+	api.APIs[APIVersion] = &api.Version{
+		OpenShiftClusterConverter: func() api.OpenShiftClusterConverter {
+			return &openShiftClusterConverter{}
+		},
+		OpenShiftClusterStaticValidator: func(location, domain string, requireD2sV3Workers bool, resourceID string) api.OpenShiftClusterStaticValidator {
+			return &openShiftClusterStaticValidator{
+				location:            location,
+				domain:              domain,
+				requireD2sV3Workers: requireD2sV3Workers,
+				resourceID:          resourceID,
+			}
+		},
+		OpenShiftClusterCredentialsConverter: func() api.OpenShiftClusterCredentialsConverter {
+			return &openShiftClusterCredentialsConverter{}
+		},
+		OpenShiftClusterAdminKubeconfigConverter: func() api.OpenShiftClusterAdminKubeconfigConverter {
+			return &openShiftClusterAdminKubeconfigConverter{}
+		},
+	}
+}
diff --git a/pkg/api/validate/dynamic/dynamic.go b/pkg/api/validate/dynamic/dynamic.go
index b4af9d54e..a7b80144d 100644
--- a/pkg/api/validate/dynamic/dynamic.go
+++ b/pkg/api/validate/dynamic/dynamic.go
@@ -389,6 +389,11 @@ func (dv *dynamic) ValidateSubnets(ctx context.Context, oc *api.OpenShiftCluster
 			}
 		}
 
+		if ss.SubnetPropertiesFormat == nil ||
+			ss.SubnetPropertiesFormat.ProvisioningState != mgmtnetwork.Succeeded {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, s.Path, "The provided subnet '%s' is not in a Succeeded state", s.ID)
+		}
+
 		_, net, err := net.ParseCIDR(*ss.AddressPrefix)
 		if err != nil {
 			return err
diff --git a/pkg/backend/openshiftcluster.go b/pkg/backend/openshiftcluster.go
index b31cf0430..77905b97a 100644
--- a/pkg/backend/openshiftcluster.go
+++ b/pkg/backend/openshiftcluster.go
@@ -13,11 +13,13 @@ import (
 	"github.com/Azure/go-autorest/autorest/azure"
 	"github.com/Azure/go-autorest/autorest/to"
 	"github.com/sirupsen/logrus"
+	"k8s.io/client-go/rest"
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/cluster"
 	"github.com/Azure/ARO-RP/pkg/database"
 	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/hive"
 	"github.com/Azure/ARO-RP/pkg/util/billing"
 	"github.com/Azure/ARO-RP/pkg/util/encryption"
 	utillog "github.com/Azure/ARO-RP/pkg/util/log"
@@ -27,7 +29,7 @@ import (
 type openShiftClusterBackend struct {
 	*backend
 
-	newManager func(context.Context, *logrus.Entry, env.Interface, database.OpenShiftClusters, database.Gateway, encryption.AEAD, billing.Manager, *api.OpenShiftClusterDocument, *api.SubscriptionDocument) (cluster.Interface, error)
+	newManager func(context.Context, *logrus.Entry, env.Interface, database.OpenShiftClusters, database.Gateway, encryption.AEAD, billing.Manager, *api.OpenShiftClusterDocument, *api.SubscriptionDocument, *rest.Config) (cluster.Interface, error)
 }
 
 func newOpenShiftClusterBackend(b *backend) *openShiftClusterBackend {
@@ -100,7 +102,12 @@ func (ocb *openShiftClusterBackend) handle(ctx context.Context, log *logrus.Entr
 		return err
 	}
 
-	m, err := ocb.newManager(ctx, log, ocb.env, ocb.dbOpenShiftClusters, ocb.dbGateway, ocb.aead, ocb.billing, doc, subscriptionDoc)
+	hiveRestConfig, err := hive.HiveRestConfig()
+	if err != nil {
+		log.Error(err) // Don't fail because of hive
+	}
+
+	m, err := ocb.newManager(ctx, log, ocb.env, ocb.dbOpenShiftClusters, ocb.dbGateway, ocb.aead, ocb.billing, doc, subscriptionDoc, hiveRestConfig)
 	if err != nil {
 		return ocb.endLease(ctx, log, stop, doc, api.ProvisioningStateFailed, err)
 	}
diff --git a/pkg/bootstraplogging/bootstraplogging.go b/pkg/bootstraplogging/bootstraplogging.go
index 544154239..115aa8ec4 100644
--- a/pkg/bootstraplogging/bootstraplogging.go
+++ b/pkg/bootstraplogging/bootstraplogging.go
@@ -15,8 +15,8 @@ import (
 
 // GetConfig prepares a bootstraplogging.Config object based on
 // the environment
-func GetConfig(env env.Interface, doc *api.OpenShiftClusterDocument) (*bootstraplogging.Config, error) {
-	r, err := azure.ParseResourceID(doc.OpenShiftCluster.ID)
+func GetConfig(env env.Interface, oc *api.OpenShiftCluster) (*bootstraplogging.Config, error) {
+	r, err := azure.ParseResourceID(oc.ID)
 	if err != nil {
 		return nil, err
 	}
@@ -41,7 +41,7 @@ func GetConfig(env env.Interface, doc *api.OpenShiftClusterDocument) (*bootstrap
 		Environment:       env.ClusterGenevaLoggingEnvironment(),
 		ConfigVersion:     env.ClusterGenevaLoggingConfigVersion(),
 		Region:            env.Location(),
-		ResourceID:        doc.OpenShiftCluster.ID,
+		ResourceID:        oc.ID,
 		SubscriptionID:    r.SubscriptionID,
 		ResourceName:      r.ResourceName,
 		ResourceGroupName: r.ResourceGroup,
diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go
index 6f8773f50..0b1a76416 100644
--- a/pkg/cluster/cluster.go
+++ b/pkg/cluster/cluster.go
@@ -17,11 +17,13 @@ import (
 	"github.com/sirupsen/logrus"
 	extensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
 	"k8s.io/client-go/kubernetes"
+	"k8s.io/client-go/rest"
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/cluster/graph"
 	"github.com/Azure/ARO-RP/pkg/database"
 	"github.com/Azure/ARO-RP/pkg/env"
+	"github.com/Azure/ARO-RP/pkg/hive"
 	aroclient "github.com/Azure/ARO-RP/pkg/operator/clientset/versioned"
 	"github.com/Azure/ARO-RP/pkg/operator/deploy"
 	"github.com/Azure/ARO-RP/pkg/util/azureclient/graphrbac"
@@ -92,12 +94,14 @@ type manager struct {
 	arocli           aroclient.Interface
 	imageregistrycli imageregistryclient.Interface
 
+	hiveClusterManager hive.ClusterManager
+
 	aroOperatorDeployer deploy.Operator
 }
 
 // New returns a cluster manager
 func New(ctx context.Context, log *logrus.Entry, _env env.Interface, db database.OpenShiftClusters, dbGateway database.Gateway, aead encryption.AEAD,
-	billing billing.Manager, doc *api.OpenShiftClusterDocument, subscriptionDoc *api.SubscriptionDocument) (Interface, error) {
+	billing billing.Manager, doc *api.OpenShiftClusterDocument, subscriptionDoc *api.SubscriptionDocument, hiveConfig *rest.Config) (Interface, error) {
 	r, err := azure.ParseResourceID(doc.OpenShiftCluster.ID)
 	if err != nil {
 		return nil, err
@@ -120,6 +124,15 @@ func New(ctx context.Context, log *logrus.Entry, _env env.Interface, db database
 
 	storage := storage.NewManager(_env, r.SubscriptionID, fpAuthorizer)
 
+	// TODO: always set hive once we have it everywhere in prod and dev
+	var hr hive.ClusterManager
+	if hiveConfig != nil {
+		hr, err = hive.NewClusterManagerFromConfig(hiveConfig)
+		if err != nil {
+			return nil, err
+		}
+	}
+
 	return &manager{
 		log:               log,
 		env:               _env,
@@ -153,5 +166,7 @@ func New(ctx context.Context, log *logrus.Entry, _env env.Interface, db database
 		storage: storage,
 		subnet:  subnet.NewManager(_env.Environment(), r.SubscriptionID, fpAuthorizer),
 		graph:   graph.NewManager(log, aead, storage),
+
+		hiveClusterManager: hr,
 	}, nil
 }
diff --git a/pkg/cluster/delete.go b/pkg/cluster/delete.go
index 8c58c0a51..c1d14e7e9 100644
--- a/pkg/cluster/delete.go
+++ b/pkg/cluster/delete.go
@@ -465,5 +465,12 @@ func (m *manager) Delete(ctx context.Context) error {
 		}
 	}
 
+	// Don't fail the deletion because of hive
+	// This should change when/if we start using hive for cluster deletion
+	err = m.deregisterClusterFromHive(ctx)
+	if err != nil {
+		m.log.Info(err)
+	}
+
 	return m.billing.Delete(ctx, m.doc)
 }
diff --git a/pkg/cluster/deregisterFromHive.go b/pkg/cluster/deregisterFromHive.go
new file mode 100644
index 000000000..530b989e3
--- /dev/null
+++ b/pkg/cluster/deregisterFromHive.go
@@ -0,0 +1,23 @@
+package cluster
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"errors"
+)
+
+func (m *manager) deregisterClusterFromHive(ctx context.Context) error {
+	if m.hiveClusterManager == nil {
+		return errors.New("no hive cluster manager, skipping")
+	}
+	namespace := m.doc.OpenShiftCluster.Properties.HiveProfile.Namespace
+
+	if namespace == "" {
+		m.log.Info("no hive namespace name in cluster document, skipping hive deregistration")
+		return nil
+	}
+
+	return m.hiveClusterManager.Delete(ctx, namespace)
+}
diff --git a/pkg/cluster/install.go b/pkg/cluster/install.go
index 10ccf010f..26bfdae10 100644
--- a/pkg/cluster/install.go
+++ b/pkg/cluster/install.go
@@ -19,6 +19,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 
 	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/hive"
 	"github.com/Azure/ARO-RP/pkg/installer"
 	aroclient "github.com/Azure/ARO-RP/pkg/operator/clientset/versioned"
 	"github.com/Azure/ARO-RP/pkg/operator/deploy"
@@ -122,12 +123,16 @@ func (m *manager) Update(ctx context.Context) error {
 
 // callInstaller initialises and calls the Installer code. This will later be replaced with a call into Hive.
 func (m *manager) callInstaller(ctx context.Context) error {
-	i := installer.NewInstaller(m.log, m.env, m.doc, m.subscriptionDoc, m.fpAuthorizer, m.deployments, m.graph)
+	i := installer.NewInstaller(m.log, m.env, m.doc.ID, m.doc.OpenShiftCluster, m.subscriptionDoc.Subscription, m.fpAuthorizer, m.deployments, m.graph)
 	return i.Install(ctx)
 }
 
 // Install installs an ARO cluster
 func (m *manager) Install(ctx context.Context) error {
+	var (
+		workloadCluster *hive.WorkloadCluster
+	)
+
 	steps := map[api.InstallPhase][]steps.Step{
 		api.InstallPhaseBootstrap: {
 			steps.AuthorizationRefreshingAction(m.fpAuthorizer, steps.Action(m.validateResources)),
@@ -184,6 +189,25 @@ func (m *manager) Install(ctx context.Context) error {
 			steps.Action(m.configureIngressCertificate),
 			steps.Condition(m.ingressControllerReady, 30*time.Minute, true),
 			steps.Action(m.configureDefaultStorageClass),
+			steps.Action(func(ctx context.Context) error {
+				var err error
+				workloadCluster, err = m.collectDataForHive(ctx)
+				if err != nil {
+					m.hiveClusterManager = nil
+					m.log.Infof("collectDataForHive: %s", err)
+				}
+				return nil // don't fail because of hive
+			}),
+			steps.Action(func(ctx context.Context) error {
+				err := m.registerWithHive(ctx, workloadCluster)
+				if err != nil {
+					m.hiveClusterManager = nil
+					m.log.Infof("registerWithHive: %s", err)
+				}
+				return nil // don't fail because of hive
+			}),
+			steps.Condition(m.verifyRegistration, 2*time.Minute, false), // don't fail because of hive
+
 			steps.Action(m.finishInstallation),
 		},
 	}
diff --git a/pkg/cluster/registerWithHive.go b/pkg/cluster/registerWithHive.go
new file mode 100644
index 000000000..71d147272
--- /dev/null
+++ b/pkg/cluster/registerWithHive.go
@@ -0,0 +1,99 @@
+package cluster
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+
+	"github.com/openshift/installer/pkg/asset/installconfig/azure"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/hive"
+	"github.com/Azure/ARO-RP/pkg/util/stringutils"
+)
+
+func (m *manager) collectDataForHive(ctx context.Context) (*hive.WorkloadCluster, error) {
+	m.log.Info("collecting registration data from the new cluster")
+	if m.hiveClusterManager == nil {
+		return nil, errors.New("no hive cluster manager, skipping")
+	}
+
+	// TODO: When hive support first party principles we'll need to send both first party and cluster service principles
+	clusterSP := azure.Credentials{
+		TenantID:       m.subscriptionDoc.Subscription.Properties.TenantID,
+		SubscriptionID: m.subscriptionDoc.ID,
+		ClientID:       m.doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientID,
+		ClientSecret:   string(m.doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientSecret),
+	}
+
+	clusterSPBytes, err := json.Marshal(clusterSP)
+	if err != nil {
+		return nil, err
+	}
+
+	resourceGroupName := stringutils.LastTokenByte(m.doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID, '/')
+
+	hiveWorkloadCluster := &hive.WorkloadCluster{
+		SubscriptionID:    m.subscriptionDoc.ID,
+		ClusterName:       m.doc.OpenShiftCluster.Name,
+		ResourceGroupName: resourceGroupName,
+		Location:          m.doc.OpenShiftCluster.Location,
+		InfraID:           m.doc.OpenShiftCluster.Properties.InfraID,
+		ClusterID:         m.doc.ID,
+		KubeConfig:        string(m.doc.OpenShiftCluster.Properties.AROServiceKubeconfig),
+		ServicePrincipal:  string(clusterSPBytes),
+	}
+
+	return hiveWorkloadCluster, nil
+}
+
+func (m *manager) registerWithHive(ctx context.Context, workloadCluster *hive.WorkloadCluster) error {
+	m.log.Info("registering with hive")
+	if m.hiveClusterManager == nil {
+		return errors.New("no hive cluster manager, skipping")
+	}
+
+	cd, err := m.hiveClusterManager.Register(ctx, workloadCluster)
+	if err != nil {
+		return err
+	}
+
+	m.doc, err = m.db.PatchWithLease(ctx, m.doc.Key, func(doc *api.OpenShiftClusterDocument) error {
+		doc.OpenShiftCluster.Properties.HiveProfile = api.HiveProfile{
+			Namespace: cd.Namespace,
+		}
+		return nil
+	})
+
+	return err
+}
+
+// verifyRegistration is being run in steps.Condition with fail=false
+// as we don't want to fail on Hive errors during the post-installation
+// cluster adoption.
+// We must return true in case of successful adoption.
+// Returning false means that we want to continue waiting for adoption.
+// Returning non-nil error means that we give up waiting.
+func (m *manager) verifyRegistration(ctx context.Context) (bool, error) {
+	m.log.Info("verifying cluster registration in hive")
+	if m.hiveClusterManager == nil {
+		return false, errors.New("no hive cluster manager, skipping")
+	}
+
+	isConnected, reason, err := m.hiveClusterManager.IsConnected(ctx, m.doc.OpenShiftCluster.Properties.HiveProfile.Namespace)
+	if err != nil {
+		m.log.Infof("error getting hive registration status: %s", err)
+		return false, nil
+	}
+
+	if !isConnected {
+		m.log.Infof("hive is not able to connect to cluster %s", reason)
+		return false, nil
+	}
+
+	m.log.Info("cluster adopted successfully")
+	return true, nil
+}
diff --git a/pkg/hive/manager.go b/pkg/hive/manager.go
index 30c39e725..c70dc6c50 100644
--- a/pkg/hive/manager.go
+++ b/pkg/hive/manager.go
@@ -4,36 +4,120 @@ package hive
 // Licensed under the Apache License 2.0.
 
 import (
+	"context"
+	"fmt"
+
+	"github.com/gofrs/uuid"
+	hivev1 "github.com/openshift/hive/apis/hive/v1"
 	hiveclient "github.com/openshift/hive/pkg/client/clientset/versioned"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
 	"k8s.io/client-go/rest"
+	"k8s.io/client-go/util/retry"
 )
 
-type Interface interface {
+type ClusterManager interface {
+	// We need to keep cluster SP updated with Hive, so we not only need to be able to create,
+	// but also be able to update resources.
+	// See relevant work item: https://msazure.visualstudio.com/AzureRedHatOpenShift/_workitems/edit/14895480
+	// We might be able to do do this by using dynamic client.
+	// Something similar to this: https://github.com/Azure/ARO-RP/pull/2145#discussion_r897915283
+	// TODO: Replace Register with CreateOrUpdate and remove the comment above
+	Register(ctx context.Context, workloadCluster *WorkloadCluster) (*hivev1.ClusterDeployment, error)
+	Delete(ctx context.Context, namespace string) error
+	IsConnected(ctx context.Context, namespace string) (bool, string, error)
+}
+
+// WorkloadCluster represents all data in hive pertaining to a single ARO cluster
+type WorkloadCluster struct {
+	SubscriptionID    string `json:"subscription,omitempty"`
+	ClusterName       string `json:"name,omitempty"`
+	ResourceGroupName string `json:"resourceGroup,omitempty"`
+	Location          string `json:"location,omitempty"`
+	InfraID           string `json:"infraId,omitempty"`
+	ClusterID         string `json:"clusterID,omitempty"`
+	KubeConfig        string `json:"kubeconfig,omitempty"`
+	ServicePrincipal  string `json:"serviceprincipal,omitempty"`
 }
 
-type client struct {
+type clusterManager struct {
 	hiveClientset *hiveclient.Clientset
 	kubernetescli *kubernetes.Clientset
 }
 
-func New(hiveClientset *hiveclient.Clientset, kubernetescli *kubernetes.Clientset) Interface {
-	return &client{
+func NewClusterManagerFromConfig(restConfig *rest.Config) (ClusterManager, error) {
+	hiveclientset, err := hiveclient.NewForConfig(restConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	kubernetescli, err := kubernetes.NewForConfig(restConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	return newClusterManager(hiveclientset, kubernetescli), nil
+}
+
+func newClusterManager(hiveClientset *hiveclient.Clientset, kubernetescli *kubernetes.Clientset) ClusterManager {
+	return &clusterManager{
 		hiveClientset: hiveClientset,
 		kubernetescli: kubernetescli,
 	}
 }
 
-func NewFromConfig(restConfig *rest.Config) (Interface, error) {
-	hiveclientset, err := hiveclient.NewForConfig(restConfig)
+func (hr *clusterManager) Register(ctx context.Context, workloadCluster *WorkloadCluster) (*hivev1.ClusterDeployment, error) {
+	var namespace string
+
+	err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+		namespace = "aro-" + uuid.Must(uuid.NewV4()).String()
+		csn := ClusterNamespace(namespace)
+		_, err := hr.kubernetescli.CoreV1().Namespaces().Create(ctx, csn, metav1.CreateOptions{})
+		return err
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	kubesecret := KubeAdminSecret(namespace, []byte(workloadCluster.KubeConfig))
+
+	_, err = hr.kubernetescli.CoreV1().Secrets(namespace).Create(ctx, kubesecret, metav1.CreateOptions{})
 	if err != nil {
 		return nil, err
 	}
 
-	hiveKubernetescli, err := kubernetes.NewForConfig(restConfig)
+	spsecret := ServicePrincipalSecret(namespace, []byte(workloadCluster.ServicePrincipal))
+	_, err = hr.kubernetescli.CoreV1().Secrets(namespace).Create(ctx, spsecret, metav1.CreateOptions{})
 	if err != nil {
 		return nil, err
 	}
 
-	return New(hiveclientset, hiveKubernetescli), nil
+	cds := ClusterDeployment(namespace, workloadCluster.ClusterName, workloadCluster.ClusterID, workloadCluster.InfraID, workloadCluster.Location)
+	return hr.hiveClientset.HiveV1().ClusterDeployments(namespace).Create(ctx, cds, metav1.CreateOptions{})
+}
+
+func (hr *clusterManager) Delete(ctx context.Context, namespace string) error {
+	// Just deleting the namespace for now
+	return hr.kubernetescli.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{})
+}
+
+func (hr *clusterManager) IsConnected(ctx context.Context, namespace string) (bool, string, error) {
+	cd, err := hr.hiveClientset.HiveV1().ClusterDeployments(namespace).Get(ctx, clusterDeploymentName, metav1.GetOptions{})
+	if err != nil {
+		return false, "", err
+	}
+
+	// Looking for the UnreachableCondition in the list of conditions
+	// the order is not stable, but the condition is expected to be present
+	for _, condition := range cd.Status.Conditions {
+		if condition.Type == hivev1.UnreachableCondition {
+			//checking for false, meaning not unreachable, so is reachable
+			isReachable := condition.Status != corev1.ConditionTrue
+			return isReachable, condition.Message, nil
+		}
+	}
+
+	// we should never arrive here (famous last words)
+	return false, "", fmt.Errorf("could not find UnreachableCondition")
 }
diff --git a/pkg/hive/resources.go b/pkg/hive/resources.go
new file mode 100644
index 000000000..d92b17f55
--- /dev/null
+++ b/pkg/hive/resources.go
@@ -0,0 +1,84 @@
+package hive
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	hivev1 "github.com/openshift/hive/apis/hive/v1"
+	"github.com/openshift/hive/apis/hive/v1/azure"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	clusterDeploymentName      = "cluster"
+	kubesecretName             = "admin-kube-secret"
+	servicePrincipalSecretname = "serviceprincipal-secret"
+)
+
+func ClusterNamespace(namespace string) *corev1.Namespace {
+	return &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: namespace,
+		},
+	}
+}
+
+func KubeAdminSecret(namespace string, kubeConfig []byte) *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      kubesecretName,
+			Namespace: namespace,
+		},
+		Data: map[string][]byte{
+			"kubeconfig": kubeConfig,
+		},
+		Type: corev1.SecretTypeOpaque,
+	}
+}
+
+func ServicePrincipalSecret(namespace string, secret []byte) *corev1.Secret {
+	return &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      servicePrincipalSecretname,
+			Namespace: namespace,
+		},
+		Data: map[string][]byte{
+			"osServicePrincipal.json": secret,
+		},
+		Type: corev1.SecretTypeOpaque,
+	}
+}
+
+func ClusterDeployment(namespace string, clusterName string, clusterID string, infraID string, location string) *hivev1.ClusterDeployment {
+	return &hivev1.ClusterDeployment{
+
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      clusterDeploymentName,
+			Namespace: namespace,
+		},
+		Spec: hivev1.ClusterDeploymentSpec{
+			BaseDomain:  "",
+			ClusterName: clusterName,
+			Installed:   true,
+			ClusterMetadata: &hivev1.ClusterMetadata{
+				AdminKubeconfigSecretRef: corev1.LocalObjectReference{
+					Name: kubesecretName,
+				},
+				ClusterID: clusterID,
+				InfraID:   infraID,
+			},
+			Platform: hivev1.Platform{
+				Azure: &azure.Platform{
+					BaseDomainResourceGroupName: "",
+					Region:                      location,
+					CredentialsSecretRef: corev1.LocalObjectReference{
+						Name: servicePrincipalSecretname,
+					},
+				},
+			},
+			PreserveOnDelete: true,
+			ManageDNS:        false,
+		},
+	}
+}
diff --git a/pkg/installer/custominstallconfig.go b/pkg/installer/custominstallconfig.go
index f038dda24..cbb97ebc4 100644
--- a/pkg/installer/custominstallconfig.go
+++ b/pkg/installer/custominstallconfig.go
@@ -23,11 +23,11 @@ import (
 // generation, etc.
 func (m *manager) applyInstallConfigCustomisations(ctx context.Context, installConfig *installconfig.InstallConfig, image *releaseimage.Image) (graph.Graph, error) {
 	clusterID := &installconfig.ClusterID{
-		UUID:    m.doc.ID,
-		InfraID: m.doc.OpenShiftCluster.Properties.InfraID,
+		UUID:    m.clusterUUID,
+		InfraID: m.oc.Properties.InfraID,
 	}
 
-	bootstrapLoggingConfig, err := bootstraplogging.GetConfig(m.env, m.doc)
+	bootstrapLoggingConfig, err := bootstraplogging.GetConfig(m.env, m.oc)
 	if err != nil {
 		return nil, err
 	}
@@ -39,19 +39,19 @@ func (m *manager) applyInstallConfigCustomisations(ctx context.Context, installC
 	}
 
 	imageRegistryConfig := &bootkube.AROImageRegistryConfig{
-		AccountName:   m.doc.OpenShiftCluster.Properties.ImageRegistryStorageAccountName,
+		AccountName:   m.oc.Properties.ImageRegistryStorageAccountName,
 		ContainerName: "image-registry",
 		HTTPSecret:    hex.EncodeToString(httpSecret),
 	}
 
 	dnsConfig := &bootkube.ARODNSConfig{
-		APIIntIP:  m.doc.OpenShiftCluster.Properties.APIServerProfile.IntIP,
-		IngressIP: m.doc.OpenShiftCluster.Properties.IngressProfiles[0].IP,
+		APIIntIP:  m.oc.Properties.APIServerProfile.IntIP,
+		IngressIP: m.oc.Properties.IngressProfiles[0].IP,
 	}
 
-	if m.doc.OpenShiftCluster.Properties.NetworkProfile.GatewayPrivateEndpointIP != "" {
-		dnsConfig.GatewayPrivateEndpointIP = m.doc.OpenShiftCluster.Properties.NetworkProfile.GatewayPrivateEndpointIP
-		dnsConfig.GatewayDomains = append(m.env.GatewayDomains(), m.doc.OpenShiftCluster.Properties.ImageRegistryStorageAccountName+".blob."+m.env.Environment().StorageEndpointSuffix)
+	if m.oc.Properties.NetworkProfile.GatewayPrivateEndpointIP != "" {
+		dnsConfig.GatewayPrivateEndpointIP = m.oc.Properties.NetworkProfile.GatewayPrivateEndpointIP
+		dnsConfig.GatewayDomains = append(m.env.GatewayDomains(), m.oc.Properties.ImageRegistryStorageAccountName+".blob."+m.env.Environment().StorageEndpointSuffix)
 	}
 
 	g := graph.Graph{}
@@ -66,7 +66,7 @@ func (m *manager) applyInstallConfigCustomisations(ctx context.Context, installC
 	}
 
 	// Handle MTU3900 feature flag
-	if m.doc.OpenShiftCluster.Properties.NetworkProfile.MTUSize == api.MTU3900 {
+	if m.oc.Properties.NetworkProfile.MTUSize == api.MTU3900 {
 		m.log.Printf("applying feature flag %s", api.FeatureFlagMTU3900)
 		if err = m.overrideEthernetMTU(g); err != nil {
 			return nil, err
diff --git a/pkg/installer/deployresources.go b/pkg/installer/deployresources.go
index f07538bf0..ce921e852 100644
--- a/pkg/installer/deployresources.go
+++ b/pkg/installer/deployresources.go
@@ -17,8 +17,8 @@ import (
 )
 
 func (m *manager) deployResourceTemplate(ctx context.Context) error {
-	resourceGroup := stringutils.LastTokenByte(m.doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID, '/')
-	account := "cluster" + m.doc.OpenShiftCluster.Properties.StorageSuffix
+	resourceGroup := stringutils.LastTokenByte(m.oc.Properties.ClusterProfile.ResourceGroupID, '/')
+	account := "cluster" + m.oc.Properties.StorageSuffix
 
 	pg, err := m.graph.LoadPersisted(ctx, resourceGroup, account)
 	if err != nil {
@@ -55,8 +55,8 @@ func (m *manager) deployResourceTemplate(ctx context.Context) error {
 	return arm.DeployTemplate(ctx, m.log, m.deployments, resourceGroup, "resources", t, map[string]interface{}{
 		"sas": map[string]interface{}{
 			"value": map[string]interface{}{
-				"signedStart":         m.doc.OpenShiftCluster.Properties.Install.Now.Format(time.RFC3339),
-				"signedExpiry":        m.doc.OpenShiftCluster.Properties.Install.Now.Add(24 * time.Hour).Format(time.RFC3339),
+				"signedStart":         m.oc.Properties.Install.Now.Format(time.RFC3339),
+				"signedExpiry":        m.oc.Properties.Install.Now.Add(24 * time.Hour).Format(time.RFC3339),
 				"signedPermission":    "rl",
 				"signedResourceTypes": "o",
 				"signedServices":      "b",
@@ -72,16 +72,11 @@ func (m *manager) deployResourceTemplate(ctx context.Context) error {
 func zones(installConfig *installconfig.InstallConfig) (zones *[]string, err error) {
 	zoneCount := len(installConfig.Config.ControlPlane.Platform.Azure.Zones)
 	replicas := int(*installConfig.Config.ControlPlane.Replicas)
-	region := installConfig.Config.Azure.Region
 
 	if zoneCount > replicas || replicas > 3 {
 		err = fmt.Errorf("cluster creation with %d zone(s) and %d replica(s) is unsupported", zoneCount, replicas)
 	} else if reflect.DeepEqual(installConfig.Config.ControlPlane.Platform.Azure.Zones, []string{""}) {
 		return
-	} else if region == "centraluseuap" {
-		// hack - centraluseuap has no compute available in zone 1, deployment must occur in zone 2 only.
-		zones = &[]string{"2"}
-		installConfig.Config.Azure.DefaultMachinePlatform.Zones = []string{"2"}
 	} else if zoneCount <= 2 {
 		zones = &installConfig.Config.ControlPlane.Platform.Azure.Zones
 	} else {
diff --git a/pkg/installer/deployresources_resources.go b/pkg/installer/deployresources_resources.go
index 71864133c..e90478919 100644
--- a/pkg/installer/deployresources_resources.go
+++ b/pkg/installer/deployresources_resources.go
@@ -26,21 +26,21 @@ func (m *manager) networkBootstrapNIC(installConfig *installconfig.InstallConfig
 						InterfaceIPConfigurationPropertiesFormat: &mgmtnetwork.InterfaceIPConfigurationPropertiesFormat{
 							LoadBalancerBackendAddressPools: &[]mgmtnetwork.BackendAddressPool{
 								{
-									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s', '%[1]s')]", m.doc.OpenShiftCluster.Properties.InfraID)),
+									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s', '%[1]s')]", m.oc.Properties.InfraID)),
 								},
 								{
-									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s-internal', '%[1]s')]", m.doc.OpenShiftCluster.Properties.InfraID)),
+									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s-internal', '%[1]s')]", m.oc.Properties.InfraID)),
 								},
 							},
 							Subnet: &mgmtnetwork.Subnet{
-								ID: &m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID,
+								ID: &m.oc.Properties.MasterProfile.SubnetID,
 							},
 						},
 						Name: to.StringPtr("bootstrap-nic-ip-v4"),
 					},
 				},
 			},
-			Name:     to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID + "-bootstrap-nic"),
+			Name:     to.StringPtr(m.oc.Properties.InfraID + "-bootstrap-nic"),
 			Type:     to.StringPtr("Microsoft.Network/networkInterfaces"),
 			Location: &installConfig.Config.Azure.Region,
 		},
@@ -57,24 +57,24 @@ func (m *manager) networkMasterNICs(installConfig *installconfig.InstallConfig)
 						InterfaceIPConfigurationPropertiesFormat: &mgmtnetwork.InterfaceIPConfigurationPropertiesFormat{
 							LoadBalancerBackendAddressPools: &[]mgmtnetwork.BackendAddressPool{
 								{
-									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s', '%[1]s')]", m.doc.OpenShiftCluster.Properties.InfraID)),
+									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s', '%[1]s')]", m.oc.Properties.InfraID)),
 								},
 								{
-									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s-internal', '%[1]s')]", m.doc.OpenShiftCluster.Properties.InfraID)),
+									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s-internal', '%[1]s')]", m.oc.Properties.InfraID)),
 								},
 								{
-									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s-internal', concat('ssh-', copyIndex()))]", m.doc.OpenShiftCluster.Properties.InfraID)),
+									ID: to.StringPtr(fmt.Sprintf("[resourceId('Microsoft.Network/loadBalancers/backendAddressPools', '%s-internal', concat('ssh-', copyIndex()))]", m.oc.Properties.InfraID)),
 								},
 							},
 							Subnet: &mgmtnetwork.Subnet{
-								ID: &m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID,
+								ID: &m.oc.Properties.MasterProfile.SubnetID,
 							},
 						},
 						Name: to.StringPtr("pipConfig"),
 					},
 				},
 			},
-			Name:     to.StringPtr(fmt.Sprintf("[concat('%s-master', copyIndex(), '-nic')]", m.doc.OpenShiftCluster.Properties.InfraID)),
+			Name:     to.StringPtr(fmt.Sprintf("[concat('%s-master', copyIndex(), '-nic')]", m.oc.Properties.InfraID)),
 			Type:     to.StringPtr("Microsoft.Network/networkInterfaces"),
 			Location: &installConfig.Config.Azure.Region,
 		},
@@ -88,10 +88,10 @@ func (m *manager) networkMasterNICs(installConfig *installconfig.InstallConfig)
 
 func (m *manager) computeBootstrapVM(installConfig *installconfig.InstallConfig) *arm.Resource {
 	var customData string
-	if m.doc.OpenShiftCluster.Properties.NetworkProfile.GatewayPrivateEndpointIP != "" {
-		customData = `[base64(concat('{"ignition":{"version":"3.2.0","proxy":{"httpsProxy":"http://` + m.doc.OpenShiftCluster.Properties.NetworkProfile.GatewayPrivateEndpointIP + `"},"config":{"replace":{"source":"https://cluster` + m.doc.OpenShiftCluster.Properties.StorageSuffix + `.blob.` + m.env.Environment().StorageEndpointSuffix + `/ignition/bootstrap.ign?', listAccountSas(resourceId('Microsoft.Storage/storageAccounts', 'cluster` + m.doc.OpenShiftCluster.Properties.StorageSuffix + `'), '2019-04-01', parameters('sas')).accountSasToken, '"}}}}'))]`
+	if m.oc.Properties.NetworkProfile.GatewayPrivateEndpointIP != "" {
+		customData = `[base64(concat('{"ignition":{"version":"3.2.0","proxy":{"httpsProxy":"http://` + m.oc.Properties.NetworkProfile.GatewayPrivateEndpointIP + `"},"config":{"replace":{"source":"https://cluster` + m.oc.Properties.StorageSuffix + `.blob.` + m.env.Environment().StorageEndpointSuffix + `/ignition/bootstrap.ign?', listAccountSas(resourceId('Microsoft.Storage/storageAccounts', 'cluster` + m.oc.Properties.StorageSuffix + `'), '2019-04-01', parameters('sas')).accountSasToken, '"}}}}'))]`
 	} else {
-		customData = `[base64(concat('{"ignition":{"version":"3.2.0","config":{"replace":{"source":"https://cluster` + m.doc.OpenShiftCluster.Properties.StorageSuffix + `.blob.` + m.env.Environment().StorageEndpointSuffix + `/ignition/bootstrap.ign?', listAccountSas(resourceId('Microsoft.Storage/storageAccounts', 'cluster` + m.doc.OpenShiftCluster.Properties.StorageSuffix + `'), '2019-04-01', parameters('sas')).accountSasToken, '"}}}}'))]`
+		customData = `[base64(concat('{"ignition":{"version":"3.2.0","config":{"replace":{"source":"https://cluster` + m.oc.Properties.StorageSuffix + `.blob.` + m.env.Environment().StorageEndpointSuffix + `/ignition/bootstrap.ign?', listAccountSas(resourceId('Microsoft.Storage/storageAccounts', 'cluster` + m.oc.Properties.StorageSuffix + `'), '2019-04-01', parameters('sas')).accountSasToken, '"}}}}'))]`
 	}
 
 	vm := &mgmtcompute.VirtualMachine{
@@ -107,7 +107,7 @@ func (m *manager) computeBootstrapVM(installConfig *installconfig.InstallConfig)
 					Version:   &installConfig.Config.Azure.Image.Version,
 				},
 				OsDisk: &mgmtcompute.OSDisk{
-					Name:         to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID + "-bootstrap_OSDisk"),
+					Name:         to.StringPtr(m.oc.Properties.InfraID + "-bootstrap_OSDisk"),
 					Caching:      mgmtcompute.CachingTypesReadWrite,
 					CreateOption: mgmtcompute.DiskCreateOptionTypesFromImage,
 					DiskSizeGB:   to.Int32Ptr(100),
@@ -117,7 +117,7 @@ func (m *manager) computeBootstrapVM(installConfig *installconfig.InstallConfig)
 				},
 			},
 			OsProfile: &mgmtcompute.OSProfile{
-				ComputerName:  to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID + "-bootstrap-vm"),
+				ComputerName:  to.StringPtr(m.oc.Properties.InfraID + "-bootstrap-vm"),
 				AdminUsername: to.StringPtr("core"),
 				AdminPassword: to.StringPtr("NotActuallyApplied!"),
 				CustomData:    &customData,
@@ -128,18 +128,18 @@ func (m *manager) computeBootstrapVM(installConfig *installconfig.InstallConfig)
 			NetworkProfile: &mgmtcompute.NetworkProfile{
 				NetworkInterfaces: &[]mgmtcompute.NetworkInterfaceReference{
 					{
-						ID: to.StringPtr("[resourceId('Microsoft.Network/networkInterfaces', '" + m.doc.OpenShiftCluster.Properties.InfraID + "-bootstrap-nic')]"),
+						ID: to.StringPtr("[resourceId('Microsoft.Network/networkInterfaces', '" + m.oc.Properties.InfraID + "-bootstrap-nic')]"),
 					},
 				},
 			},
 			DiagnosticsProfile: &mgmtcompute.DiagnosticsProfile{
 				BootDiagnostics: &mgmtcompute.BootDiagnostics{
 					Enabled:    to.BoolPtr(true),
-					StorageURI: to.StringPtr("https://cluster" + m.doc.OpenShiftCluster.Properties.StorageSuffix + ".blob." + m.env.Environment().StorageEndpointSuffix + "/"),
+					StorageURI: to.StringPtr("https://cluster" + m.oc.Properties.StorageSuffix + ".blob." + m.env.Environment().StorageEndpointSuffix + "/"),
 				},
 			},
 		},
-		Name:     to.StringPtr(m.doc.OpenShiftCluster.Properties.InfraID + "-bootstrap"),
+		Name:     to.StringPtr(m.oc.Properties.InfraID + "-bootstrap"),
 		Type:     to.StringPtr("Microsoft.Compute/virtualMachines"),
 		Location: &installConfig.Config.Azure.Region,
 	}
@@ -159,7 +159,7 @@ func (m *manager) computeBootstrapVM(installConfig *installconfig.InstallConfig)
 		Resource:   vm,
 		APIVersion: azureclient.APIVersion("Microsoft.Compute"),
 		DependsOn: []string{
-			"Microsoft.Network/networkInterfaces/" + m.doc.OpenShiftCluster.Properties.InfraID + "-bootstrap-nic",
+			"Microsoft.Network/networkInterfaces/" + m.oc.Properties.InfraID + "-bootstrap-nic",
 		},
 	}
 }
@@ -178,7 +178,7 @@ func (m *manager) computeMasterVMs(installConfig *installconfig.InstallConfig, z
 					Version:   &installConfig.Config.Azure.Image.Version,
 				},
 				OsDisk: &mgmtcompute.OSDisk{
-					Name:         to.StringPtr("[concat('" + m.doc.OpenShiftCluster.Properties.InfraID + "-master-', copyIndex(), '_OSDisk')]"),
+					Name:         to.StringPtr("[concat('" + m.oc.Properties.InfraID + "-master-', copyIndex(), '_OSDisk')]"),
 					Caching:      mgmtcompute.CachingTypesReadOnly,
 					CreateOption: mgmtcompute.DiskCreateOptionTypesFromImage,
 					DiskSizeGB:   &installConfig.Config.ControlPlane.Platform.Azure.OSDisk.DiskSizeGB,
@@ -188,7 +188,7 @@ func (m *manager) computeMasterVMs(installConfig *installconfig.InstallConfig, z
 				},
 			},
 			OsProfile: &mgmtcompute.OSProfile{
-				ComputerName:  to.StringPtr("[concat('" + m.doc.OpenShiftCluster.Properties.InfraID + "-master-', copyIndex())]"),
+				ComputerName:  to.StringPtr("[concat('" + m.oc.Properties.InfraID + "-master-', copyIndex())]"),
 				AdminUsername: to.StringPtr("core"),
 				AdminPassword: to.StringPtr("NotActuallyApplied!"),
 				CustomData:    to.StringPtr(base64.StdEncoding.EncodeToString(machineMaster.File.Data)),
@@ -199,19 +199,19 @@ func (m *manager) computeMasterVMs(installConfig *installconfig.InstallConfig, z
 			NetworkProfile: &mgmtcompute.NetworkProfile{
 				NetworkInterfaces: &[]mgmtcompute.NetworkInterfaceReference{
 					{
-						ID: to.StringPtr("[resourceId('Microsoft.Network/networkInterfaces', concat('" + m.doc.OpenShiftCluster.Properties.InfraID + "-master', copyIndex(), '-nic'))]"),
+						ID: to.StringPtr("[resourceId('Microsoft.Network/networkInterfaces', concat('" + m.oc.Properties.InfraID + "-master', copyIndex(), '-nic'))]"),
 					},
 				},
 			},
 			DiagnosticsProfile: &mgmtcompute.DiagnosticsProfile{
 				BootDiagnostics: &mgmtcompute.BootDiagnostics{
 					Enabled:    to.BoolPtr(true),
-					StorageURI: to.StringPtr("https://cluster" + m.doc.OpenShiftCluster.Properties.StorageSuffix + ".blob." + m.env.Environment().StorageEndpointSuffix + "/"),
+					StorageURI: to.StringPtr("https://cluster" + m.oc.Properties.StorageSuffix + ".blob." + m.env.Environment().StorageEndpointSuffix + "/"),
 				},
 			},
 		},
 		Zones:    zones,
-		Name:     to.StringPtr("[concat('" + m.doc.OpenShiftCluster.Properties.InfraID + "-master-', copyIndex())]"),
+		Name:     to.StringPtr("[concat('" + m.oc.Properties.InfraID + "-master-', copyIndex())]"),
 		Type:     to.StringPtr("Microsoft.Compute/virtualMachines"),
 		Location: &installConfig.Config.Azure.Region,
 	}
@@ -236,7 +236,7 @@ func (m *manager) computeMasterVMs(installConfig *installconfig.InstallConfig, z
 			Count: int(*installConfig.Config.ControlPlane.Replicas),
 		},
 		DependsOn: []string{
-			"[concat('Microsoft.Network/networkInterfaces/" + m.doc.OpenShiftCluster.Properties.InfraID + "-master', copyIndex(), '-nic')]",
+			"[concat('Microsoft.Network/networkInterfaces/" + m.oc.Properties.InfraID + "-master', copyIndex(), '-nic')]",
 		},
 	}
 }
diff --git a/pkg/installer/generateconfig.go b/pkg/installer/generateconfig.go
index 39aaf6da6..d8b0d6c3d 100644
--- a/pkg/installer/generateconfig.go
+++ b/pkg/installer/generateconfig.go
@@ -32,9 +32,9 @@ import (
 )
 
 func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.InstallConfig, *releaseimage.Image, error) {
-	resourceGroup := stringutils.LastTokenByte(m.doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID, '/')
+	resourceGroup := stringutils.LastTokenByte(m.oc.Properties.ClusterProfile.ResourceGroupID, '/')
 
-	pullSecret, err := pullsecret.Build(m.doc.OpenShiftCluster, string(m.doc.OpenShiftCluster.Properties.ClusterProfile.PullSecret))
+	pullSecret, err := pullsecret.Build(m.oc, string(m.oc.Properties.ClusterProfile.PullSecret))
 	if err != nil {
 		return nil, nil, err
 	}
@@ -46,17 +46,17 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 		}
 	}
 
-	r, err := azure.ParseResourceID(m.doc.OpenShiftCluster.ID)
+	r, err := azure.ParseResourceID(m.oc.ID)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	_, masterSubnetName, err := subnet.Split(m.doc.OpenShiftCluster.Properties.MasterProfile.SubnetID)
+	_, masterSubnetName, err := subnet.Split(m.oc.Properties.MasterProfile.SubnetID)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	vnetID, workerSubnetName, err := subnet.Split(m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].SubnetID)
+	vnetID, workerSubnetName, err := subnet.Split(m.oc.Properties.WorkerProfiles[0].SubnetID)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -66,7 +66,7 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 		return nil, nil, err
 	}
 
-	privateKey, err := x509.ParsePKCS1PrivateKey(m.doc.OpenShiftCluster.Properties.SSHKey)
+	privateKey, err := x509.ParsePKCS1PrivateKey(m.oc.Properties.SSHKey)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -76,12 +76,12 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 		return nil, nil, err
 	}
 
-	domain := m.doc.OpenShiftCluster.Properties.ClusterProfile.Domain
+	domain := m.oc.Properties.ClusterProfile.Domain
 	if !strings.ContainsRune(domain, '.') {
 		domain += "." + m.env.Domain()
 	}
 
-	masterSKU, err := m.env.VMSku(string(m.doc.OpenShiftCluster.Properties.MasterProfile.VMSize))
+	masterSKU, err := m.env.VMSku(string(m.oc.Properties.MasterProfile.VMSize))
 	if err != nil {
 		return nil, nil, err
 	}
@@ -90,7 +90,7 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 		masterZones = []string{""}
 	}
 
-	workerSKU, err := m.env.VMSku(string(m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].VMSize))
+	workerSKU, err := m.env.VMSku(string(m.oc.Properties.WorkerProfiles[0].VMSize))
 	if err != nil {
 		return nil, nil, err
 	}
@@ -99,9 +99,15 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 		workerZones = []string{""}
 	}
 
+	// Standard_D8s_v3 is only available in one zone in centraluseuap, so we need a non-zonal install in that region
+	if strings.EqualFold(m.oc.Location, "centraluseuap") {
+		workerZones = []string{""}
+		masterZones = []string{""}
+	}
+
 	SoftwareDefinedNetwork := string(api.SoftwareDefinedNetworkOpenShiftSDN)
-	if m.doc.OpenShiftCluster.Properties.NetworkProfile.SoftwareDefinedNetwork != "" {
-		SoftwareDefinedNetwork = string(m.doc.OpenShiftCluster.Properties.NetworkProfile.SoftwareDefinedNetwork)
+	if m.oc.Properties.NetworkProfile.SoftwareDefinedNetwork != "" {
+		SoftwareDefinedNetwork = string(m.oc.Properties.NetworkProfile.SoftwareDefinedNetwork)
 	}
 
 	installConfig := &installconfig.InstallConfig{
@@ -123,12 +129,12 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 				NetworkType: SoftwareDefinedNetwork,
 				ClusterNetwork: []types.ClusterNetworkEntry{
 					{
-						CIDR:       *ipnet.MustParseCIDR(m.doc.OpenShiftCluster.Properties.NetworkProfile.PodCIDR),
+						CIDR:       *ipnet.MustParseCIDR(m.oc.Properties.NetworkProfile.PodCIDR),
 						HostPrefix: 23,
 					},
 				},
 				ServiceNetwork: []ipnet.IPNet{
-					*ipnet.MustParseCIDR(m.doc.OpenShiftCluster.Properties.NetworkProfile.ServiceCIDR),
+					*ipnet.MustParseCIDR(m.oc.Properties.NetworkProfile.ServiceCIDR),
 				},
 			},
 			ControlPlane: &types.MachinePool{
@@ -137,10 +143,10 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 				Platform: types.MachinePoolPlatform{
 					Azure: &azuretypes.MachinePool{
 						Zones:            masterZones,
-						InstanceType:     string(m.doc.OpenShiftCluster.Properties.MasterProfile.VMSize),
-						EncryptionAtHost: m.doc.OpenShiftCluster.Properties.MasterProfile.EncryptionAtHost == api.EncryptionAtHostEnabled,
+						InstanceType:     string(m.oc.Properties.MasterProfile.VMSize),
+						EncryptionAtHost: m.oc.Properties.MasterProfile.EncryptionAtHost == api.EncryptionAtHostEnabled,
 						OSDisk: azuretypes.OSDisk{
-							DiskEncryptionSetID: m.doc.OpenShiftCluster.Properties.MasterProfile.DiskEncryptionSetID,
+							DiskEncryptionSetID: m.oc.Properties.MasterProfile.DiskEncryptionSetID,
 							DiskSizeGB:          1024,
 						},
 					},
@@ -150,16 +156,16 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 			},
 			Compute: []types.MachinePool{
 				{
-					Name:     m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].Name,
-					Replicas: to.Int64Ptr(int64(m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].Count)),
+					Name:     m.oc.Properties.WorkerProfiles[0].Name,
+					Replicas: to.Int64Ptr(int64(m.oc.Properties.WorkerProfiles[0].Count)),
 					Platform: types.MachinePoolPlatform{
 						Azure: &azuretypes.MachinePool{
 							Zones:            workerZones,
-							InstanceType:     string(m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].VMSize),
-							EncryptionAtHost: m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].EncryptionAtHost == api.EncryptionAtHostEnabled,
+							InstanceType:     string(m.oc.Properties.WorkerProfiles[0].VMSize),
+							EncryptionAtHost: m.oc.Properties.WorkerProfiles[0].EncryptionAtHost == api.EncryptionAtHostEnabled,
 							OSDisk: azuretypes.OSDisk{
-								DiskEncryptionSetID: m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].DiskEncryptionSetID,
-								DiskSizeGB:          int32(m.doc.OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB),
+								DiskEncryptionSetID: m.oc.Properties.WorkerProfiles[0].DiskEncryptionSetID,
+								DiskSizeGB:          int32(m.oc.Properties.WorkerProfiles[0].DiskSizeGB),
 							},
 						},
 					},
@@ -169,7 +175,7 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 			},
 			Platform: types.Platform{
 				Azure: &azuretypes.Platform{
-					Region:                   strings.ToLower(m.doc.OpenShiftCluster.Location), // Used in k8s object names, so must pass DNS-1123 validation
+					Region:                   strings.ToLower(m.oc.Location), // Used in k8s object names, so must pass DNS-1123 validation
 					NetworkResourceGroupName: vnetr.ResourceGroup,
 					VirtualNetwork:           vnetr.ResourceName,
 					ControlPlaneSubnet:       masterSubnetName,
@@ -180,7 +186,7 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 				},
 			},
 			PullSecret: pullSecret,
-			FIPS:       m.doc.OpenShiftCluster.Properties.ClusterProfile.FipsValidatedModules == api.FipsValidatedModulesEnabled,
+			FIPS:       m.oc.Properties.ClusterProfile.FipsValidatedModules == api.FipsValidatedModulesEnabled,
 			ImageContentSources: []types.ImageContentSource{
 				{
 					Source: "quay.io/openshift-release-dev/ocp-release",
@@ -207,15 +213,15 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 			azuretypes.CloudEnvironment(m.env.Environment().Name),
 			m.env.Environment().ResourceManagerEndpoint,
 			&icazure.Credentials{
-				TenantID:       m.subscriptionDoc.Subscription.Properties.TenantID,
-				ClientID:       m.doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientID,
-				ClientSecret:   string(m.doc.OpenShiftCluster.Properties.ServicePrincipalProfile.ClientSecret),
+				TenantID:       m.sub.Properties.TenantID,
+				ClientID:       m.oc.Properties.ServicePrincipalProfile.ClientID,
+				ClientSecret:   string(m.oc.Properties.ServicePrincipalProfile.ClientSecret),
 				SubscriptionID: r.SubscriptionID,
 			},
 		),
 	}
 
-	if m.doc.OpenShiftCluster.Properties.IngressProfiles[0].Visibility == api.VisibilityPrivate {
+	if m.oc.Properties.IngressProfiles[0].Visibility == api.VisibilityPrivate {
 		installConfig.Config.Publish = types.InternalPublishingStrategy
 	}
 
@@ -225,10 +231,10 @@ func (m *manager) generateInstallConfig(ctx context.Context) (*installconfig.Ins
 	}
 
 	image := &releaseimage.Image{}
-	if m.doc.OpenShiftCluster.Properties.ClusterProfile.Version == version.InstallStream.Version.String() {
+	if m.oc.Properties.ClusterProfile.Version == version.InstallStream.Version.String() {
 		image.PullSpec = version.InstallStream.PullSpec
 	} else {
-		return nil, nil, fmt.Errorf("unimplemented version %q", m.doc.OpenShiftCluster.Properties.ClusterProfile.Version)
+		return nil, nil, fmt.Errorf("unimplemented version %q", m.oc.Properties.ClusterProfile.Version)
 	}
 
 	err = validation.ValidateInstallConfig(installConfig.Config).ToAggregate()
diff --git a/pkg/installer/graph.go b/pkg/installer/graph.go
index e49f21c2e..84bd4effc 100644
--- a/pkg/installer/graph.go
+++ b/pkg/installer/graph.go
@@ -11,8 +11,8 @@ import (
 )
 
 func (m *manager) persistGraph(ctx context.Context, g graph.Graph) error {
-	resourceGroup := stringutils.LastTokenByte(m.doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID, '/')
-	clusterStorageAccountName := "cluster" + m.doc.OpenShiftCluster.Properties.StorageSuffix
+	resourceGroup := stringutils.LastTokenByte(m.oc.Properties.ClusterProfile.ResourceGroupID, '/')
+	clusterStorageAccountName := "cluster" + m.oc.Properties.StorageSuffix
 
 	exists, err := m.graph.Exists(ctx, resourceGroup, clusterStorageAccountName)
 	if err != nil || exists {
diff --git a/pkg/installer/install.go b/pkg/installer/install.go
index fb11fb9d3..d2bf3b808 100644
--- a/pkg/installer/install.go
+++ b/pkg/installer/install.go
@@ -55,8 +55,8 @@ func (m *manager) Install(ctx context.Context) error {
 // initializeKubernetesClients initializes clients using the Installer-generated
 // kubeconfig.
 func (m *manager) initializeKubernetesClients(ctx context.Context) error {
-	resourceGroup := stringutils.LastTokenByte(m.doc.OpenShiftCluster.Properties.ClusterProfile.ResourceGroupID, '/')
-	account := "cluster" + m.doc.OpenShiftCluster.Properties.StorageSuffix
+	resourceGroup := stringutils.LastTokenByte(m.oc.Properties.ClusterProfile.ResourceGroupID, '/')
+	account := "cluster" + m.oc.Properties.StorageSuffix
 
 	// Load the installer's generated kubeconfig from the graph
 	pg, err := m.graph.LoadPersisted(ctx, resourceGroup, account)
@@ -74,7 +74,7 @@ func (m *manager) initializeKubernetesClients(ctx context.Context) error {
 	// k8s.io/client-go/transport/cache.go, k8s caches our transport, and it
 	// can't tell if data in the restconfig.Dial closure has changed.  We don't
 	// want it to cache a transport that can never work.
-	if m.doc.OpenShiftCluster.Properties.NetworkProfile.APIServerPrivateEndpointIP == "" {
+	if m.oc.Properties.NetworkProfile.APIServerPrivateEndpointIP == "" {
 		return errors.New("privateEndpointIP is empty")
 	}
 
@@ -87,7 +87,7 @@ func (m *manager) initializeKubernetesClients(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-	r.Dial = restconfig.DialContext(m.env, m.doc.OpenShiftCluster)
+	r.Dial = restconfig.DialContext(m.env, m.oc)
 
 	m.kubernetescli, err = kubernetes.NewForConfig(r)
 	return err
diff --git a/pkg/installer/manager.go b/pkg/installer/manager.go
index 2eb63d137..b9b2a0780 100644
--- a/pkg/installer/manager.go
+++ b/pkg/installer/manager.go
@@ -20,9 +20,13 @@ type manager struct {
 	log *logrus.Entry
 	env env.Interface
 
-	doc             *api.OpenShiftClusterDocument
-	subscriptionDoc *api.SubscriptionDocument
-	fpAuthorizer    refreshable.Authorizer
+	// clusterUUID is the UUID of the OpenShiftClusterDocument that contained
+	// this OpenShiftCluster. It should be used where a unique ID for this
+	// cluster is required.
+	clusterUUID  string
+	oc           *api.OpenShiftCluster
+	sub          *api.Subscription
+	fpAuthorizer refreshable.Authorizer
 
 	deployments features.DeploymentsClient
 
@@ -35,14 +39,15 @@ type Interface interface {
 	Install(ctx context.Context) error
 }
 
-func NewInstaller(log *logrus.Entry, _env env.Interface, doc *api.OpenShiftClusterDocument, subscriptionDoc *api.SubscriptionDocument, fpAuthorizer refreshable.Authorizer, deployments features.DeploymentsClient, g graph.Manager) Interface {
+func NewInstaller(log *logrus.Entry, _env env.Interface, clusterUUID string, oc *api.OpenShiftCluster, subscription *api.Subscription, fpAuthorizer refreshable.Authorizer, deployments features.DeploymentsClient, g graph.Manager) Interface {
 	return &manager{
-		log:             log,
-		env:             _env,
-		doc:             doc,
-		subscriptionDoc: subscriptionDoc,
-		fpAuthorizer:    fpAuthorizer,
-		deployments:     deployments,
-		graph:           g,
+		log:          log,
+		env:          _env,
+		clusterUUID:  clusterUUID,
+		oc:           oc,
+		sub:          subscription,
+		fpAuthorizer: fpAuthorizer,
+		deployments:  deployments,
+		graph:        g,
 	}
 }
diff --git a/pkg/operator/apis/aro.openshift.io/v1alpha1/cluster_types.go b/pkg/operator/apis/aro.openshift.io/v1alpha1/cluster_types.go
index 17e82c1b2..8c25725c1 100644
--- a/pkg/operator/apis/aro.openshift.io/v1alpha1/cluster_types.go
+++ b/pkg/operator/apis/aro.openshift.io/v1alpha1/cluster_types.go
@@ -24,17 +24,34 @@ const (
 	ServicePrincipalValid       = "ServicePrincipalValid"
 
 	ManagedUpgradeOperatorStatus = "ManagedUpgradeOperatorStatus"
+
+	// advisor checks
+	DefaultIngressCertificate = "DefaultIngressCertificate"
+	DefaultClusterDNS         = "DefaultClusterDNS"
 )
 
 // AllConditionTypes is a operator conditions currently in use, any condition not in this list is not
 // added to the operator.status.conditions list
 func AllConditionTypes() []string {
-	return []string{InternetReachableFromMaster, InternetReachableFromWorker, MachineValid, ServicePrincipalValid, ManagedUpgradeOperatorStatus}
+	return []string{
+		InternetReachableFromMaster,
+		InternetReachableFromWorker,
+		MachineValid,
+		ServicePrincipalValid,
+		ManagedUpgradeOperatorStatus,
+		DefaultIngressCertificate,
+		DefaultClusterDNS,
+	}
 }
 
 // ClusterChecksTypes represents checks performed on the cluster to verify basic functionality
 func ClusterChecksTypes() []string {
-	return []string{InternetReachableFromMaster, InternetReachableFromWorker, MachineValid, ServicePrincipalValid}
+	return []string{
+		InternetReachableFromMaster,
+		InternetReachableFromWorker,
+		MachineValid,
+		ServicePrincipalValid,
+	}
 }
 
 type GenevaLoggingSpec struct {
diff --git a/pkg/operator/controllers/checker/checker_controller.go b/pkg/operator/controllers/checker/checker_controller.go
index c40f431d0..4dad8cae8 100644
--- a/pkg/operator/controllers/checker/checker_controller.go
+++ b/pkg/operator/controllers/checker/checker_controller.go
@@ -7,7 +7,9 @@ import (
 	"context"
 	"time"
 
+	configclient "github.com/openshift/client-go/config/clientset/versioned"
 	machineclient "github.com/openshift/client-go/machine/clientset/versioned"
+	operatorclient "github.com/openshift/client-go/operator/clientset/versioned"
 	"github.com/sirupsen/logrus"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
@@ -37,12 +39,14 @@ type Reconciler struct {
 	arocli   aroclient.Interface
 }
 
-func NewReconciler(log *logrus.Entry, arocli aroclient.Interface, kubernetescli kubernetes.Interface, maocli machineclient.Interface, role string) *Reconciler {
+func NewReconciler(log *logrus.Entry, arocli aroclient.Interface, kubernetescli kubernetes.Interface, machinecli machineclient.Interface, operatorcli operatorclient.Interface, configcli configclient.Interface, role string) *Reconciler {
 	checkers := []Checker{NewInternetChecker(log, arocli, role)}
 
 	if role == operator.RoleMaster {
 		checkers = append(checkers,
-			NewServicePrincipalChecker(log, arocli, kubernetescli, maocli, role),
+			NewServicePrincipalChecker(log, arocli, kubernetescli, machinecli, role),
+			NewIngressCertificateChecker(log, arocli, operatorcli, configcli, role),
+			NewClusterDNSChecker(log, arocli, operatorcli, role),
 		)
 	}
 
diff --git a/pkg/operator/controllers/checker/clusterdns.go b/pkg/operator/controllers/checker/clusterdns.go
new file mode 100644
index 000000000..b6e8b4b96
--- /dev/null
+++ b/pkg/operator/controllers/checker/clusterdns.go
@@ -0,0 +1,89 @@
+// Implements a check that provides detail on openshift-dns-operator
+// configurations.
+//
+// Included checks are:
+//  - existence of custom DNS entries
+//  - malformed zone forwarding configuration
+
+package checker
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	operatorv1 "github.com/openshift/api/operator/v1"
+	operatorclient "github.com/openshift/client-go/operator/clientset/versioned"
+	"github.com/sirupsen/logrus"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	arov1alpha1 "github.com/Azure/ARO-RP/pkg/operator/apis/aro.openshift.io/v1alpha1"
+	aroclient "github.com/Azure/ARO-RP/pkg/operator/clientset/versioned"
+	"github.com/Azure/ARO-RP/pkg/util/conditions"
+)
+
+type ClusterDNSChecker struct {
+	log *logrus.Entry
+
+	arocli      aroclient.Interface
+	operatorcli operatorclient.Interface
+
+	role string
+}
+
+func NewClusterDNSChecker(log *logrus.Entry, arocli aroclient.Interface, operatorcli operatorclient.Interface, role string) *ClusterDNSChecker {
+	return &ClusterDNSChecker{
+		log:         log,
+		arocli:      arocli,
+		operatorcli: operatorcli,
+	}
+}
+
+func (r *ClusterDNSChecker) Name() string {
+	return "ClusterDNSChecker"
+}
+
+func (r *ClusterDNSChecker) Check(ctx context.Context) error {
+	cond := &operatorv1.OperatorCondition{
+		Type:    arov1alpha1.DefaultClusterDNS,
+		Status:  operatorv1.ConditionUnknown,
+		Message: "",
+		Reason:  "CheckDone",
+	}
+
+	dns, err := r.operatorcli.OperatorV1().DNSes().Get(ctx, "default", metav1.GetOptions{})
+	if err != nil {
+		cond.Message = err.Error()
+		cond.Reason = "CheckFailed"
+		return conditions.SetCondition(ctx, r.arocli, cond, r.role)
+	}
+
+	var upstreams []string
+	for _, s := range dns.Spec.Servers {
+		for _, z := range s.Zones {
+			if z == "." {
+				// If "." is set as a zone, bail out and warn about the
+				// malformed config, as this will prevent CoreDNS from rolling
+				// out
+				cond.Message = `Malformed config: "." in zones`
+				cond.Status = operatorv1.ConditionFalse
+				return conditions.SetCondition(ctx, r.arocli, cond, r.role)
+			}
+		}
+
+		upstreams = append(upstreams, s.ForwardPlugin.Upstreams...)
+	}
+
+	if len(upstreams) > 0 {
+		cond.Status = operatorv1.ConditionFalse
+		cond.Message = fmt.Sprintf("Custom upstream DNS servers in use: %s", strings.Join(upstreams, ", "))
+	} else {
+		cond.Status = operatorv1.ConditionTrue
+		cond.Message = "No in-cluster upstream DNS servers"
+	}
+
+	return conditions.SetCondition(ctx, r.arocli, cond, r.role)
+}
diff --git a/pkg/operator/controllers/checker/ingresscert.go b/pkg/operator/controllers/checker/ingresscert.go
new file mode 100644
index 000000000..fae96c126
--- /dev/null
+++ b/pkg/operator/controllers/checker/ingresscert.go
@@ -0,0 +1,81 @@
+// Implements a check that provides detail on potentially faulty or customised
+// IngressController configurations on the default controller.
+//
+// Included checks are:
+//  - existence of custom ingress certificate
+//  - existence of default ingresscontroller
+
+package checker
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+
+	operatorv1 "github.com/openshift/api/operator/v1"
+	configclient "github.com/openshift/client-go/config/clientset/versioned"
+	operatorclient "github.com/openshift/client-go/operator/clientset/versioned"
+	"github.com/sirupsen/logrus"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	arov1alpha1 "github.com/Azure/ARO-RP/pkg/operator/apis/aro.openshift.io/v1alpha1"
+	aroclient "github.com/Azure/ARO-RP/pkg/operator/clientset/versioned"
+	"github.com/Azure/ARO-RP/pkg/util/conditions"
+)
+
+type IngressCertificateChecker struct {
+	arocli      aroclient.Interface
+	operatorcli operatorclient.Interface
+	configcli   configclient.Interface
+
+	role string
+}
+
+func NewIngressCertificateChecker(log *logrus.Entry, arocli aroclient.Interface, operatorcli operatorclient.Interface, configcli configclient.Interface, role string) *IngressCertificateChecker {
+	return &IngressCertificateChecker{
+		arocli:      arocli,
+		operatorcli: operatorcli,
+		configcli:   configcli,
+	}
+}
+
+func (r *IngressCertificateChecker) Name() string {
+	return "IngressCertificateChecker"
+}
+
+func (r *IngressCertificateChecker) Check(ctx context.Context) error {
+	cond := &operatorv1.OperatorCondition{
+		Type:    arov1alpha1.DefaultIngressCertificate,
+		Status:  operatorv1.ConditionUnknown,
+		Message: "",
+		Reason:  "CheckDone",
+	}
+
+	cv, err := r.configcli.ConfigV1().ClusterVersions().Get(ctx, "version", metav1.GetOptions{})
+	if err != nil {
+		cond.Message = err.Error()
+		cond.Reason = "CheckFailed"
+		return conditions.SetCondition(ctx, r.arocli, cond, r.role)
+	}
+
+	ingress, err := r.operatorcli.OperatorV1().IngressControllers("openshift-ingress-operator").Get(ctx, "default", metav1.GetOptions{})
+	if err != nil {
+		cond.Message = err.Error()
+		cond.Reason = "CheckFailed"
+		return conditions.SetCondition(ctx, r.arocli, cond, r.role)
+	}
+
+	if ingress.Spec.DefaultCertificate == nil {
+		cond.Status = operatorv1.ConditionFalse
+		cond.Message = "Ingress has no certificate yet"
+	} else if ingress.Spec.DefaultCertificate.Name != string(cv.Spec.ClusterID)+"-ingress" {
+		cond.Status = operatorv1.ConditionFalse
+		cond.Message = "Custom ingress certificate in use: " + ingress.Spec.DefaultCertificate.Name
+	} else {
+		cond.Status = operatorv1.ConditionTrue
+		cond.Message = "Default ingress certificate in use"
+	}
+
+	return conditions.SetCondition(ctx, r.arocli, cond, r.role)
+}
diff --git a/pkg/swagger/generator.go b/pkg/swagger/generator.go
index bd7b9f3f4..d1034fb9a 100644
--- a/pkg/swagger/generator.go
+++ b/pkg/swagger/generator.go
@@ -10,11 +10,13 @@ import (
 	v20200430 "github.com/Azure/ARO-RP/pkg/api/v20200430"
 	"github.com/Azure/ARO-RP/pkg/api/v20210901preview"
 	v20220401 "github.com/Azure/ARO-RP/pkg/api/v20220401"
+	v20220904 "github.com/Azure/ARO-RP/pkg/api/v20220904"
 )
 
 const apiv20200430Path = "github.com/Azure/ARO-RP/pkg/api/v20200430"
 const apiv20210901previewPath = "github.com/Azure/ARO-RP/pkg/api/v20210901preview"
 const apiv20220401Path = "github.com/Azure/ARO-RP/pkg/api/v20220401"
+const apiv20220904Path = "github.com/Azure/ARO-RP/pkg/api/v20220904"
 
 type generator struct {
 	exampleOpenShiftClusterPutParameter            func() interface{}
@@ -69,6 +71,22 @@ var apis = map[string]*generator{
 		exampleOpenShiftClusterAdminKubeconfigResponse: v20220401.ExampleOpenShiftClusterAdminKubeconfigResponse,
 		exampleOperationListResponse:                   api.ExampleOperationListResponse,
 
+		xmsEnum:            []string{"EncryptionAtHost", "FipsValidatedModules", "SoftwareDefinedNetwork", "Visibility"},
+		xmsSecretList:      []string{"kubeconfig", "kubeadminPassword"},
+		xmsIdentifiers:     []string{},
+		commonTypesVersion: "v2",
+		systemData:         true,
+		kubeConfig:         true,
+	},
+	apiv20220904Path: {
+		exampleOpenShiftClusterPutParameter:            v20220904.ExampleOpenShiftClusterPutParameter,
+		exampleOpenShiftClusterPatchParameter:          v20220904.ExampleOpenShiftClusterPatchParameter,
+		exampleOpenShiftClusterResponse:                v20220904.ExampleOpenShiftClusterResponse,
+		exampleOpenShiftClusterCredentialsResponse:     v20220904.ExampleOpenShiftClusterCredentialsResponse,
+		exampleOpenShiftClusterListResponse:            v20220904.ExampleOpenShiftClusterListResponse,
+		exampleOpenShiftClusterAdminKubeconfigResponse: v20220904.ExampleOpenShiftClusterAdminKubeconfigResponse,
+		exampleOperationListResponse:                   api.ExampleOperationListResponse,
+
 		xmsEnum:            []string{"EncryptionAtHost", "FipsValidatedModules", "SoftwareDefinedNetwork", "Visibility"},
 		xmsSecretList:      []string{"kubeconfig", "kubeadminPassword"},
 		xmsIdentifiers:     []string{},
diff --git a/pkg/util/dynamichelper/dynamichelper.go b/pkg/util/dynamichelper/dynamichelper.go
index c9c213eac..8892fe028 100644
--- a/pkg/util/dynamichelper/dynamichelper.go
+++ b/pkg/util/dynamichelper/dynamichelper.go
@@ -39,7 +39,7 @@ type dynamicHelper struct {
 	GVRResolver
 
 	log     *logrus.Entry
-	restcli *rest.RESTClient
+	restcli rest.Interface
 }
 
 func New(log *logrus.Entry, restconfig *rest.Config) (Interface, error) {
diff --git a/python/az/aro/azext_aro/_validators.py b/python/az/aro/azext_aro/_validators.py
index 10764b9f1..a17fa76f9 100644
--- a/python/az/aro/azext_aro/_validators.py
+++ b/python/az/aro/azext_aro/_validators.py
@@ -82,7 +82,7 @@ def validate_disk_encryption_set(cmd, namespace):
                                                 disk_encryption_set_name=desid['name'])
     except CloudError as err:
         raise InvalidArgumentValueError(
-            f"Invald --disk-encryption-set, error when getting '{namespace.disk_encryption_set}':"
+            f"Invalid --disk-encryption-set, error when getting '{namespace.disk_encryption_set}':"
             f" {str(err)}") from err
 
 
@@ -169,7 +169,7 @@ def validate_subnet(key):
         except Exception as err:
             if isinstance(err, ResourceNotFoundError):
                 raise InvalidArgumentValueError(
-                    f"Invald --{key.replace('_', '-')}, error when getting '{subnet}': {str(err)}") from err
+                    f"Invalid --{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
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_CreateOrUpdate.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_CreateOrUpdate.json
new file mode 100644
index 000000000..b930710d3
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_CreateOrUpdate.json
@@ -0,0 +1,180 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04",
+    "subscriptionId": "subscriptionId",
+    "resourceGroupName": "resourceGroup",
+    "resourceName": "resourceName",
+    "parameters": {
+      "location": "location",
+      "tags": {
+        "key": "value"
+      },
+      "properties": {
+        "clusterProfile": {
+          "pullSecret": "{\"auths\":{\"registry.connect.redhat.com\":{\"auth\":\"\"},\"registry.redhat.io\":{\"auth\":\"\"}}}",
+          "domain": "cluster.location.aroapp.io",
+          "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup",
+          "fipsValidatedModules": "Enabled"
+        },
+        "consoleProfile": {},
+        "servicePrincipalProfile": {
+          "clientId": "clientId",
+          "clientSecret": "clientSecret"
+        },
+        "networkProfile": {
+          "podCidr": "10.128.0.0/14",
+          "serviceCidr": "172.30.0.0/16"
+        },
+        "masterProfile": {
+          "vmSize": "Standard_D8s_v3",
+          "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master",
+          "encryptionAtHost": "Enabled"
+        },
+        "workerProfiles": [
+          {
+            "name": "worker",
+            "vmSize": "Standard_D2s_v3",
+            "diskSizeGB": 128,
+            "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+            "count": 3
+          }
+        ],
+        "apiserverProfile": {
+          "visibility": "Public"
+        },
+        "ingressProfiles": [
+          {
+            "name": "default",
+            "visibility": "Public"
+          }
+        ]
+      }
+    }
+  },
+  "responses": {
+    "200": {
+      "body": {
+        "id": "/subscriptions/subscriptionId/resourceGroups/resourceGroup/providers/Microsoft.RedHatOpenShift/OpenShiftClusters/resourceName",
+        "name": "resourceName",
+        "type": "Microsoft.RedHatOpenShift/OpenShiftClusters",
+        "location": "location",
+        "systemData": {
+          "createdBy": "string",
+          "createdByType": "Application",
+          "createdAt": "2020-02-03T01:01:01.1075056Z",
+          "lastModifiedBy": "string",
+          "lastModifiedByType": "Application",
+          "lastModifiedAt": "2020-02-03T01:01:01.1075056Z"
+        },
+        "tags": {
+          "key": "value"
+        },
+        "properties": {
+          "provisioningState": "Succeeded",
+          "clusterProfile": {
+            "domain": "cluster.location.aroapp.io",
+            "version": "4.3.0",
+            "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup"
+          },
+          "consoleProfile": {
+            "url": "https://console-openshift-console.apps.cluster.location.aroapp.io/"
+          },
+          "servicePrincipalProfile": {
+            "clientId": "clientId"
+          },
+          "networkProfile": {
+            "podCidr": "10.128.0.0/14",
+            "serviceCidr": "172.30.0.0/16"
+          },
+          "masterProfile": {
+            "vmSize": "Standard_D8s_v3",
+            "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master"
+          },
+          "workerProfiles": [
+            {
+              "name": "worker",
+              "vmSize": "Standard_D2s_v3",
+              "diskSizeGB": 128,
+              "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+              "count": 3
+            }
+          ],
+          "apiserverProfile": {
+            "visibility": "Public",
+            "url": "https://api.cluster.location.aroapp.io:6443/",
+            "ip": "1.2.3.4"
+          },
+          "ingressProfiles": [
+            {
+              "name": "default",
+              "visibility": "Public",
+              "ip": "1.2.3.4"
+            }
+          ]
+        }
+      }
+    },
+    "201": {
+      "body": {
+        "id": "/subscriptions/subscriptionId/resourceGroups/resourceGroup/providers/Microsoft.RedHatOpenShift/OpenShiftClusters/resourceName",
+        "name": "resourceName",
+        "type": "Microsoft.RedHatOpenShift/OpenShiftClusters",
+        "location": "location",
+        "systemData": {
+          "createdBy": "string",
+          "createdByType": "Application",
+          "createdAt": "2020-02-03T01:01:01.1075056Z",
+          "lastModifiedBy": "string",
+          "lastModifiedByType": "Application",
+          "lastModifiedAt": "2020-02-03T01:01:01.1075056Z"
+        },
+        "tags": {
+          "key": "value"
+        },
+        "properties": {
+          "provisioningState": "Succeeded",
+          "clusterProfile": {
+            "domain": "cluster.location.aroapp.io",
+            "version": "4.3.0",
+            "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup"
+          },
+          "consoleProfile": {
+            "url": "https://console-openshift-console.apps.cluster.location.aroapp.io/"
+          },
+          "servicePrincipalProfile": {
+            "clientId": "clientId"
+          },
+          "networkProfile": {
+            "podCidr": "10.128.0.0/14",
+            "serviceCidr": "172.30.0.0/16"
+          },
+          "masterProfile": {
+            "vmSize": "Standard_D8s_v3",
+            "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master"
+          },
+          "workerProfiles": [
+            {
+              "name": "worker",
+              "vmSize": "Standard_D2s_v3",
+              "diskSizeGB": 128,
+              "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+              "count": 3
+            }
+          ],
+          "apiserverProfile": {
+            "visibility": "Public",
+            "url": "https://api.cluster.location.aroapp.io:6443/",
+            "ip": "1.2.3.4"
+          },
+          "ingressProfiles": [
+            {
+              "name": "default",
+              "visibility": "Public",
+              "ip": "1.2.3.4"
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Delete.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Delete.json
new file mode 100644
index 000000000..f0a504d0c
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Delete.json
@@ -0,0 +1,12 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04",
+    "subscriptionId": "subscriptionId",
+    "resourceGroupName": "resourceGroup",
+    "resourceName": "resourceName"
+  },
+  "responses": {
+    "202": {},
+    "204": {}
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Get.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Get.json
new file mode 100644
index 000000000..e6a384ed6
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Get.json
@@ -0,0 +1,72 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04",
+    "subscriptionId": "subscriptionId",
+    "resourceGroupName": "resourceGroup",
+    "resourceName": "resourceName"
+  },
+  "responses": {
+    "200": {
+      "body": {
+        "id": "/subscriptions/subscriptionId/resourceGroups/resourceGroup/providers/Microsoft.RedHatOpenShift/OpenShiftClusters/resourceName",
+        "name": "resourceName",
+        "type": "Microsoft.RedHatOpenShift/OpenShiftClusters",
+        "location": "location",
+        "systemData": {
+          "createdBy": "string",
+          "createdByType": "Application",
+          "createdAt": "2020-02-03T01:01:01.1075056Z",
+          "lastModifiedBy": "string",
+          "lastModifiedByType": "Application",
+          "lastModifiedAt": "2020-02-03T01:01:01.1075056Z"
+        },
+        "tags": {
+          "key": "value"
+        },
+        "properties": {
+          "provisioningState": "Succeeded",
+          "clusterProfile": {
+            "domain": "cluster.location.aroapp.io",
+            "version": "4.3.0",
+            "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup"
+          },
+          "consoleProfile": {
+            "url": "https://console-openshift-console.apps.cluster.location.aroapp.io/"
+          },
+          "servicePrincipalProfile": {
+            "clientId": "clientId"
+          },
+          "networkProfile": {
+            "podCidr": "10.128.0.0/14",
+            "serviceCidr": "172.30.0.0/16"
+          },
+          "masterProfile": {
+            "vmSize": "Standard_D8s_v3",
+            "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master"
+          },
+          "workerProfiles": [
+            {
+              "name": "worker",
+              "vmSize": "Standard_D2s_v3",
+              "diskSizeGB": 128,
+              "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+              "count": 3
+            }
+          ],
+          "apiserverProfile": {
+            "visibility": "Public",
+            "url": "https://api.cluster.location.aroapp.io:6443/",
+            "ip": "1.2.3.4"
+          },
+          "ingressProfiles": [
+            {
+              "name": "default",
+              "visibility": "Public",
+              "ip": "1.2.3.4"
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_List.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_List.json
new file mode 100644
index 000000000..604366426
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_List.json
@@ -0,0 +1,74 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04",
+    "subscriptionId": "subscriptionId"
+  },
+  "responses": {
+    "200": {
+      "body": {
+        "value": [
+          {
+            "id": "/subscriptions/subscriptionId/resourceGroups/resourceGroup/providers/Microsoft.RedHatOpenShift/OpenShiftClusters/resourceName",
+            "name": "resourceName",
+            "type": "Microsoft.RedHatOpenShift/OpenShiftClusters",
+            "location": "location",
+            "systemData": {
+              "createdBy": "string",
+              "createdByType": "Application",
+              "createdAt": "2020-02-03T01:01:01.1075056Z",
+              "lastModifiedBy": "string",
+              "lastModifiedByType": "Application",
+              "lastModifiedAt": "2020-02-03T01:01:01.1075056Z"
+            },
+            "tags": {
+              "key": "value"
+            },
+            "properties": {
+              "provisioningState": "Succeeded",
+              "clusterProfile": {
+                "domain": "cluster.location.aroapp.io",
+                "version": "4.3.0",
+                "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup"
+              },
+              "consoleProfile": {
+                "url": "https://console-openshift-console.apps.cluster.location.aroapp.io/"
+              },
+              "servicePrincipalProfile": {
+                "clientId": "clientId"
+              },
+              "networkProfile": {
+                "podCidr": "10.128.0.0/14",
+                "serviceCidr": "172.30.0.0/16"
+              },
+              "masterProfile": {
+                "vmSize": "Standard_D8s_v3",
+                "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master"
+              },
+              "workerProfiles": [
+                {
+                  "name": "worker",
+                  "vmSize": "Standard_D2s_v3",
+                  "diskSizeGB": 128,
+                  "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+                  "count": 3
+                }
+              ],
+              "apiserverProfile": {
+                "visibility": "Public",
+                "url": "https://api.cluster.location.aroapp.io:6443/",
+                "ip": "1.2.3.4"
+              },
+              "ingressProfiles": [
+                {
+                  "name": "default",
+                  "visibility": "Public",
+                  "ip": "1.2.3.4"
+                }
+              ]
+            }
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListAdminCredentials.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListAdminCredentials.json
new file mode 100644
index 000000000..6d66ba584
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListAdminCredentials.json
@@ -0,0 +1,15 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04",
+    "subscriptionId": "subscriptionId",
+    "resourceGroupName": "resourceGroup",
+    "resourceName": "resourceName"
+  },
+  "responses": {
+    "200": {
+      "body": {
+        "kubeconfig": "e30="
+      }
+    }
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListByResourceGroup.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListByResourceGroup.json
new file mode 100644
index 000000000..731b6a42c
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListByResourceGroup.json
@@ -0,0 +1,75 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04",
+    "subscriptionId": "subscriptionId",
+    "resourceGroupName": "resourceGroup"
+  },
+  "responses": {
+    "200": {
+      "body": {
+        "value": [
+          {
+            "id": "/subscriptions/subscriptionId/resourceGroups/resourceGroup/providers/Microsoft.RedHatOpenShift/OpenShiftClusters/resourceName",
+            "name": "resourceName",
+            "type": "Microsoft.RedHatOpenShift/OpenShiftClusters",
+            "location": "location",
+            "systemData": {
+              "createdBy": "string",
+              "createdByType": "Application",
+              "createdAt": "2020-02-03T01:01:01.1075056Z",
+              "lastModifiedBy": "string",
+              "lastModifiedByType": "Application",
+              "lastModifiedAt": "2020-02-03T01:01:01.1075056Z"
+            },
+            "tags": {
+              "key": "value"
+            },
+            "properties": {
+              "provisioningState": "Succeeded",
+              "clusterProfile": {
+                "domain": "cluster.location.aroapp.io",
+                "version": "4.3.0",
+                "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup"
+              },
+              "consoleProfile": {
+                "url": "https://console-openshift-console.apps.cluster.location.aroapp.io/"
+              },
+              "servicePrincipalProfile": {
+                "clientId": "clientId"
+              },
+              "networkProfile": {
+                "podCidr": "10.128.0.0/14",
+                "serviceCidr": "172.30.0.0/16"
+              },
+              "masterProfile": {
+                "vmSize": "Standard_D8s_v3",
+                "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master"
+              },
+              "workerProfiles": [
+                {
+                  "name": "worker",
+                  "vmSize": "Standard_D2s_v3",
+                  "diskSizeGB": 128,
+                  "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+                  "count": 3
+                }
+              ],
+              "apiserverProfile": {
+                "visibility": "Public",
+                "url": "https://api.cluster.location.aroapp.io:6443/",
+                "ip": "1.2.3.4"
+              },
+              "ingressProfiles": [
+                {
+                  "name": "default",
+                  "visibility": "Public",
+                  "ip": "1.2.3.4"
+                }
+              ]
+            }
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListCredentials.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListCredentials.json
new file mode 100644
index 000000000..b14349983
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_ListCredentials.json
@@ -0,0 +1,16 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04",
+    "subscriptionId": "subscriptionId",
+    "resourceGroupName": "resourceGroup",
+    "resourceName": "resourceName"
+  },
+  "responses": {
+    "200": {
+      "body": {
+        "kubeadminUsername": "kubeadmin",
+        "kubeadminPassword": "password"
+      }
+    }
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Update.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Update.json
new file mode 100644
index 000000000..05b026dbc
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/OpenShiftClusters_Update.json
@@ -0,0 +1,179 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04",
+    "subscriptionId": "subscriptionId",
+    "resourceGroupName": "resourceGroup",
+    "resourceName": "resourceName",
+    "parameters": {
+      "tags": {
+        "key": "value"
+      },
+      "properties": {
+        "clusterProfile": {
+          "pullSecret": "{\"auths\":{\"registry.connect.redhat.com\":{\"auth\":\"\"},\"registry.redhat.io\":{\"auth\":\"\"}}}",
+          "domain": "cluster.location.aroapp.io",
+          "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup",
+          "fipsValidatedModules": "Enabled"
+        },
+        "consoleProfile": {},
+        "servicePrincipalProfile": {
+          "clientId": "clientId",
+          "clientSecret": "clientSecret"
+        },
+        "networkProfile": {
+          "podCidr": "10.128.0.0/14",
+          "serviceCidr": "172.30.0.0/16"
+        },
+        "masterProfile": {
+          "vmSize": "Standard_D8s_v3",
+          "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master",
+          "encryptionAtHost": "Enabled"
+        },
+        "workerProfiles": [
+          {
+            "name": "worker",
+            "vmSize": "Standard_D2s_v3",
+            "diskSizeGB": 128,
+            "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+            "count": 3
+          }
+        ],
+        "apiserverProfile": {
+          "visibility": "Public"
+        },
+        "ingressProfiles": [
+          {
+            "name": "default",
+            "visibility": "Public"
+          }
+        ]
+      }
+    }
+  },
+  "responses": {
+    "200": {
+      "body": {
+        "id": "/subscriptions/subscriptionId/resourceGroups/resourceGroup/providers/Microsoft.RedHatOpenShift/OpenShiftClusters/resourceName",
+        "name": "resourceName",
+        "type": "Microsoft.RedHatOpenShift/OpenShiftClusters",
+        "location": "location",
+        "systemData": {
+          "createdBy": "string",
+          "createdByType": "Application",
+          "createdAt": "2020-02-03T01:01:01.1075056Z",
+          "lastModifiedBy": "string",
+          "lastModifiedByType": "Application",
+          "lastModifiedAt": "2020-02-03T01:01:01.1075056Z"
+        },
+        "tags": {
+          "key": "value"
+        },
+        "properties": {
+          "provisioningState": "Succeeded",
+          "clusterProfile": {
+            "domain": "cluster.location.aroapp.io",
+            "version": "4.3.0",
+            "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup"
+          },
+          "consoleProfile": {
+            "url": "https://console-openshift-console.apps.cluster.location.aroapp.io/"
+          },
+          "servicePrincipalProfile": {
+            "clientId": "clientId"
+          },
+          "networkProfile": {
+            "podCidr": "10.128.0.0/14",
+            "serviceCidr": "172.30.0.0/16"
+          },
+          "masterProfile": {
+            "vmSize": "Standard_D8s_v3",
+            "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master"
+          },
+          "workerProfiles": [
+            {
+              "name": "worker",
+              "vmSize": "Standard_D2s_v3",
+              "diskSizeGB": 128,
+              "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+              "count": 3
+            }
+          ],
+          "apiserverProfile": {
+            "visibility": "Public",
+            "url": "https://api.cluster.location.aroapp.io:6443/",
+            "ip": "1.2.3.4"
+          },
+          "ingressProfiles": [
+            {
+              "name": "default",
+              "visibility": "Public",
+              "ip": "1.2.3.4"
+            }
+          ]
+        }
+      }
+    },
+    "201": {
+      "body": {
+        "id": "/subscriptions/subscriptionId/resourceGroups/resourceGroup/providers/Microsoft.RedHatOpenShift/OpenShiftClusters/resourceName",
+        "name": "resourceName",
+        "type": "Microsoft.RedHatOpenShift/OpenShiftClusters",
+        "location": "location",
+        "systemData": {
+          "createdBy": "string",
+          "createdByType": "Application",
+          "createdAt": "2020-02-03T01:01:01.1075056Z",
+          "lastModifiedBy": "string",
+          "lastModifiedByType": "Application",
+          "lastModifiedAt": "2020-02-03T01:01:01.1075056Z"
+        },
+        "tags": {
+          "key": "value"
+        },
+        "properties": {
+          "provisioningState": "Succeeded",
+          "clusterProfile": {
+            "domain": "cluster.location.aroapp.io",
+            "version": "4.3.0",
+            "resourceGroupId": "/subscriptions/subscriptionId/resourceGroups/clusterResourceGroup"
+          },
+          "consoleProfile": {
+            "url": "https://console-openshift-console.apps.cluster.location.aroapp.io/"
+          },
+          "servicePrincipalProfile": {
+            "clientId": "clientId"
+          },
+          "networkProfile": {
+            "podCidr": "10.128.0.0/14",
+            "serviceCidr": "172.30.0.0/16"
+          },
+          "masterProfile": {
+            "vmSize": "Standard_D8s_v3",
+            "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master"
+          },
+          "workerProfiles": [
+            {
+              "name": "worker",
+              "vmSize": "Standard_D2s_v3",
+              "diskSizeGB": 128,
+              "subnetId": "/subscriptions/subscriptionId/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker",
+              "count": 3
+            }
+          ],
+          "apiserverProfile": {
+            "visibility": "Public",
+            "url": "https://api.cluster.location.aroapp.io:6443/",
+            "ip": "1.2.3.4"
+          },
+          "ingressProfiles": [
+            {
+              "name": "default",
+              "visibility": "Public",
+              "ip": "1.2.3.4"
+            }
+          ]
+        }
+      }
+    }
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/Operations_List.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/Operations_List.json
new file mode 100644
index 000000000..39e48e8ee
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/examples/Operations_List.json
@@ -0,0 +1,21 @@
+{
+  "parameters": {
+    "api-version": "2022-09-04"
+  },
+  "responses": {
+    "200": {
+      "body": {
+        "value": [
+          {
+            "name": "Microsoft.RedHatOpenShift/openShiftClusters/read",
+            "display": {
+              "provider": "Azure Red Hat OpenShift",
+              "resource": "openShiftClusters",
+              "operation": "Read OpenShift cluster"
+            }
+          }
+        ]
+      }
+    }
+  }
+}
diff --git a/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/redhatopenshift.json b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/redhatopenshift.json
new file mode 100644
index 000000000..481c4827c
--- /dev/null
+++ b/swagger/redhatopenshift/resource-manager/Microsoft.RedHatOpenShift/stable/2022-09-04/redhatopenshift.json
@@ -0,0 +1,913 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "title": "Azure Red Hat OpenShift Client",
+    "description": "Rest API for Azure Red Hat OpenShift 4",
+    "version": "2022-09-04"
+  },
+  "host": "management.azure.com",
+  "schemes": [
+    "https"
+  ],
+  "consumes": [
+    "application/json"
+  ],
+  "produces": [
+    "application/json"
+  ],
+  "paths": {
+    "/providers/Microsoft.RedHatOpenShift/operations": {
+      "get": {
+        "tags": [
+          "Operations"
+        ],
+        "summary": "Lists all of the available RP operations.",
+        "description": "The operation returns the RP operations.",
+        "operationId": "Operations_List",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "schema": {
+              "$ref": "#/definitions/OperationList"
+            }
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-examples": {
+          "Lists all of the available RP operations.": {
+            "$ref": "./examples/Operations_List.json"
+          }
+        },
+        "x-ms-pageable": {
+          "nextLinkName": "nextLink"
+        }
+      }
+    },
+    "/subscriptions/{subscriptionId}/providers/Microsoft.RedHatOpenShift/openShiftClusters": {
+      "get": {
+        "tags": [
+          "OpenShiftClusters"
+        ],
+        "summary": "Lists OpenShift clusters in the specified subscription.",
+        "description": "The operation returns properties of each OpenShift cluster.",
+        "operationId": "OpenShiftClusters_List",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/SubscriptionIdParameter"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftClusterList"
+            }
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-examples": {
+          "Lists OpenShift clusters in the specified subscription.": {
+            "$ref": "./examples/OpenShiftClusters_List.json"
+          }
+        },
+        "x-ms-pageable": {
+          "nextLinkName": "nextLink"
+        }
+      }
+    },
+    "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.RedHatOpenShift/openShiftClusters": {
+      "get": {
+        "tags": [
+          "OpenShiftClusters"
+        ],
+        "summary": "Lists OpenShift clusters in the specified subscription and resource group.",
+        "description": "The operation returns properties of each OpenShift cluster.",
+        "operationId": "OpenShiftClusters_ListByResourceGroup",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/SubscriptionIdParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ResourceGroupNameParameter"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftClusterList"
+            }
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-examples": {
+          "Lists OpenShift clusters in the specified subscription and resource group.": {
+            "$ref": "./examples/OpenShiftClusters_ListByResourceGroup.json"
+          }
+        },
+        "x-ms-pageable": {
+          "nextLinkName": "nextLink"
+        }
+      }
+    },
+    "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.RedHatOpenShift/openShiftClusters/{resourceName}": {
+      "get": {
+        "tags": [
+          "OpenShiftClusters"
+        ],
+        "summary": "Gets a OpenShift cluster with the specified subscription, resource group and resource name.",
+        "description": "The operation returns properties of a OpenShift cluster.",
+        "operationId": "OpenShiftClusters_Get",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/SubscriptionIdParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ResourceGroupNameParameter"
+          },
+          {
+            "name": "resourceName",
+            "in": "path",
+            "description": "The name of the OpenShift cluster resource.",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftCluster"
+            }
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-examples": {
+          "Gets a OpenShift cluster with the specified subscription, resource group and resource name.": {
+            "$ref": "./examples/OpenShiftClusters_Get.json"
+          }
+        }
+      },
+      "put": {
+        "tags": [
+          "OpenShiftClusters"
+        ],
+        "summary": "Creates or updates a OpenShift cluster with the specified subscription, resource group and resource name.",
+        "description": "The operation returns properties of a OpenShift cluster.",
+        "operationId": "OpenShiftClusters_CreateOrUpdate",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/SubscriptionIdParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ResourceGroupNameParameter"
+          },
+          {
+            "name": "resourceName",
+            "in": "path",
+            "description": "The name of the OpenShift cluster resource.",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "parameters",
+            "in": "body",
+            "description": "The OpenShift cluster resource.",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/OpenShiftCluster"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftCluster"
+            }
+          },
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftCluster"
+            }
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-long-running-operation": true,
+        "x-ms-examples": {
+          "Creates or updates a OpenShift cluster with the specified subscription, resource group and resource name.": {
+            "$ref": "./examples/OpenShiftClusters_CreateOrUpdate.json"
+          }
+        }
+      },
+      "delete": {
+        "tags": [
+          "OpenShiftClusters"
+        ],
+        "summary": "Deletes a OpenShift cluster with the specified subscription, resource group and resource name.",
+        "description": "The operation returns nothing.",
+        "operationId": "OpenShiftClusters_Delete",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/SubscriptionIdParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ResourceGroupNameParameter"
+          },
+          {
+            "name": "resourceName",
+            "in": "path",
+            "description": "The name of the OpenShift cluster resource.",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "202": {
+            "description": "Accepted"
+          },
+          "204": {
+            "description": "No Content"
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-long-running-operation": true,
+        "x-ms-examples": {
+          "Deletes a OpenShift cluster with the specified subscription, resource group and resource name.": {
+            "$ref": "./examples/OpenShiftClusters_Delete.json"
+          }
+        }
+      },
+      "patch": {
+        "tags": [
+          "OpenShiftClusters"
+        ],
+        "summary": "Creates or updates a OpenShift cluster with the specified subscription, resource group and resource name.",
+        "description": "The operation returns properties of a OpenShift cluster.",
+        "operationId": "OpenShiftClusters_Update",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/SubscriptionIdParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ResourceGroupNameParameter"
+          },
+          {
+            "name": "resourceName",
+            "in": "path",
+            "description": "The name of the OpenShift cluster resource.",
+            "required": true,
+            "type": "string"
+          },
+          {
+            "name": "parameters",
+            "in": "body",
+            "description": "The OpenShift cluster resource.",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/OpenShiftClusterUpdate"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftCluster"
+            }
+          },
+          "201": {
+            "description": "Created",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftCluster"
+            }
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-long-running-operation": true,
+        "x-ms-examples": {
+          "Creates or updates a OpenShift cluster with the specified subscription, resource group and resource name.": {
+            "$ref": "./examples/OpenShiftClusters_Update.json"
+          }
+        }
+      }
+    },
+    "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.RedHatOpenShift/openShiftClusters/{resourceName}/listAdminCredentials": {
+      "post": {
+        "tags": [
+          "OpenShiftClusters"
+        ],
+        "summary": "Lists admin kubeconfig of an OpenShift cluster with the specified subscription, resource group and resource name.",
+        "description": "The operation returns the admin kubeconfig.",
+        "operationId": "OpenShiftClusters_ListAdminCredentials",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/SubscriptionIdParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ResourceGroupNameParameter"
+          },
+          {
+            "name": "resourceName",
+            "in": "path",
+            "description": "The name of the OpenShift cluster resource.",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftClusterAdminKubeconfig"
+            }
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-examples": {
+          "Lists admin kubeconfig of an OpenShift cluster with the specified subscription, resource group and resource name.": {
+            "$ref": "./examples/OpenShiftClusters_ListAdminCredentials.json"
+          }
+        }
+      }
+    },
+    "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.RedHatOpenShift/openShiftClusters/{resourceName}/listCredentials": {
+      "post": {
+        "tags": [
+          "OpenShiftClusters"
+        ],
+        "summary": "Lists credentials of an OpenShift cluster with the specified subscription, resource group and resource name.",
+        "description": "The operation returns the credentials.",
+        "operationId": "OpenShiftClusters_ListCredentials",
+        "parameters": [
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ApiVersionParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/SubscriptionIdParameter"
+          },
+          {
+            "$ref": "../../../../../common-types/resource-management/v2/types.json#/parameters/ResourceGroupNameParameter"
+          },
+          {
+            "name": "resourceName",
+            "in": "path",
+            "description": "The name of the OpenShift cluster resource.",
+            "required": true,
+            "type": "string"
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "schema": {
+              "$ref": "#/definitions/OpenShiftClusterCredentials"
+            }
+          },
+          "default": {
+            "description": "Error response describing why the operation failed.  If the resource doesn't exist, 404 (Not Found) is returned.  If any of the input parameters is wrong, 400 (Bad Request) is returned.",
+            "schema": {
+              "$ref": "#/definitions/CloudError"
+            }
+          }
+        },
+        "x-ms-examples": {
+          "Lists credentials of an OpenShift cluster with the specified subscription, resource group and resource name.": {
+            "$ref": "./examples/OpenShiftClusters_ListCredentials.json"
+          }
+        }
+      }
+    }
+  },
+  "definitions": {
+    "APIServerProfile": {
+      "description": "APIServerProfile represents an API server profile.",
+      "type": "object",
+      "properties": {
+        "visibility": {
+          "$ref": "#/definitions/Visibility",
+          "description": "API server visibility."
+        },
+        "url": {
+          "description": "The URL to access the cluster API server.",
+          "type": "string"
+        },
+        "ip": {
+          "description": "The IP of the cluster API server.",
+          "type": "string"
+        }
+      }
+    },
+    "CloudError": {
+      "description": "CloudError represents a cloud error.",
+      "type": "object",
+      "properties": {
+        "error": {
+          "$ref": "#/definitions/CloudErrorBody",
+          "description": "An error response from the service."
+        }
+      }
+    },
+    "CloudErrorBody": {
+      "description": "CloudErrorBody represents the body of a cloud error.",
+      "type": "object",
+      "properties": {
+        "code": {
+          "description": "An identifier for the error. Codes are invariant and are intended to be consumed programmatically.",
+          "type": "string"
+        },
+        "message": {
+          "description": "A message describing the error, intended to be suitable for display in a user interface.",
+          "type": "string"
+        },
+        "target": {
+          "description": "The target of the particular error. For example, the name of the property in error.",
+          "type": "string"
+        },
+        "details": {
+          "description": "A list of additional details about the error.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/CloudErrorBody"
+          },
+          "x-ms-identifiers": []
+        }
+      }
+    },
+    "ClusterProfile": {
+      "description": "ClusterProfile represents a cluster profile.",
+      "type": "object",
+      "properties": {
+        "pullSecret": {
+          "description": "The pull secret for the cluster.",
+          "type": "string"
+        },
+        "domain": {
+          "description": "The domain for the cluster.",
+          "type": "string"
+        },
+        "version": {
+          "description": "The version of the cluster.",
+          "type": "string"
+        },
+        "resourceGroupId": {
+          "description": "The ID of the cluster resource group.",
+          "type": "string"
+        },
+        "fipsValidatedModules": {
+          "$ref": "#/definitions/FipsValidatedModules",
+          "description": "If FIPS validated crypto modules are used"
+        }
+      }
+    },
+    "ConsoleProfile": {
+      "description": "ConsoleProfile represents a console profile.",
+      "type": "object",
+      "properties": {
+        "url": {
+          "description": "The URL to access the cluster console.",
+          "type": "string"
+        }
+      }
+    },
+    "Display": {
+      "description": "Display represents the display details of an operation.",
+      "type": "object",
+      "properties": {
+        "provider": {
+          "description": "Friendly name of the resource provider.",
+          "type": "string"
+        },
+        "resource": {
+          "description": "Resource type on which the operation is performed.",
+          "type": "string"
+        },
+        "operation": {
+          "description": "Operation type: read, write, delete, listKeys/action, etc.",
+          "type": "string"
+        },
+        "description": {
+          "description": "Friendly name of the operation.",
+          "type": "string"
+        }
+      }
+    },
+    "EncryptionAtHost": {
+      "description": "EncryptionAtHost represents encryption at host state",
+      "enum": [
+        "Disabled",
+        "Enabled"
+      ],
+      "type": "string",
+      "x-ms-enum": {
+        "name": "EncryptionAtHost",
+        "modelAsString": true
+      }
+    },
+    "FipsValidatedModules": {
+      "description": "FipsValidatedModules determines if FIPS is used.",
+      "enum": [
+        "Disabled",
+        "Enabled"
+      ],
+      "type": "string",
+      "x-ms-enum": {
+        "name": "FipsValidatedModules",
+        "modelAsString": true
+      }
+    },
+    "IngressProfile": {
+      "description": "IngressProfile represents an ingress profile.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "description": "The ingress profile name.",
+          "type": "string"
+        },
+        "visibility": {
+          "$ref": "#/definitions/Visibility",
+          "description": "Ingress visibility."
+        },
+        "ip": {
+          "description": "The IP of the ingress.",
+          "type": "string"
+        }
+      }
+    },
+    "MasterProfile": {
+      "description": "MasterProfile represents a master profile.",
+      "type": "object",
+      "properties": {
+        "vmSize": {
+          "$ref": "#/definitions/VMSize",
+          "description": "The size of the master VMs."
+        },
+        "subnetId": {
+          "description": "The Azure resource ID of the master subnet.",
+          "type": "string"
+        },
+        "encryptionAtHost": {
+          "$ref": "#/definitions/EncryptionAtHost",
+          "description": "Whether master virtual machines are encrypted at host."
+        },
+        "diskEncryptionSetId": {
+          "description": "The resource ID of an associated DiskEncryptionSet, if applicable.",
+          "type": "string"
+        }
+      }
+    },
+    "NetworkProfile": {
+      "description": "NetworkProfile represents a network profile.",
+      "type": "object",
+      "properties": {
+        "podCidr": {
+          "description": "The CIDR used for OpenShift/Kubernetes Pods.",
+          "type": "string"
+        },
+        "serviceCidr": {
+          "description": "The CIDR used for OpenShift/Kubernetes Services.",
+          "type": "string"
+        }
+      }
+    },
+    "OpenShiftCluster": {
+      "description": "OpenShiftCluster represents an Azure Red Hat OpenShift cluster.",
+      "type": "object",
+      "allOf": [
+        {
+          "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/TrackedResource"
+        }
+      ],
+      "properties": {
+        "properties": {
+          "$ref": "#/definitions/OpenShiftClusterProperties",
+          "description": "The cluster properties.",
+          "x-ms-client-flatten": true
+        },
+        "systemData": {
+          "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/systemData",
+          "description": "The system meta data relating to this resource.",
+          "readOnly": true
+        }
+      }
+    },
+    "OpenShiftClusterAdminKubeconfig": {
+      "description": "OpenShiftClusterAdminKubeconfig represents an OpenShift cluster's admin kubeconfig.",
+      "type": "object",
+      "properties": {
+        "kubeconfig": {
+          "description": "The base64-encoded kubeconfig file.",
+          "type": "string",
+          "x-ms-secret": true
+        }
+      }
+    },
+    "OpenShiftClusterCredentials": {
+      "description": "OpenShiftClusterCredentials represents an OpenShift cluster's credentials.",
+      "type": "object",
+      "properties": {
+        "kubeadminUsername": {
+          "description": "The username for the kubeadmin user.",
+          "type": "string"
+        },
+        "kubeadminPassword": {
+          "description": "The password for the kubeadmin user.",
+          "type": "string",
+          "x-ms-secret": true
+        }
+      }
+    },
+    "OpenShiftClusterList": {
+      "description": "OpenShiftClusterList represents a list of OpenShift clusters.",
+      "type": "object",
+      "properties": {
+        "value": {
+          "description": "The list of OpenShift clusters.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/OpenShiftCluster"
+          },
+          "x-ms-identifiers": []
+        },
+        "nextLink": {
+          "description": "The link used to get the next page of operations.",
+          "type": "string"
+        }
+      }
+    },
+    "OpenShiftClusterProperties": {
+      "description": "OpenShiftClusterProperties represents an OpenShift cluster's properties.",
+      "type": "object",
+      "properties": {
+        "provisioningState": {
+          "$ref": "#/definitions/ProvisioningState",
+          "description": "The cluster provisioning state."
+        },
+        "clusterProfile": {
+          "$ref": "#/definitions/ClusterProfile",
+          "description": "The cluster profile."
+        },
+        "consoleProfile": {
+          "$ref": "#/definitions/ConsoleProfile",
+          "description": "The console profile."
+        },
+        "servicePrincipalProfile": {
+          "$ref": "#/definitions/ServicePrincipalProfile",
+          "description": "The cluster service principal profile."
+        },
+        "networkProfile": {
+          "$ref": "#/definitions/NetworkProfile",
+          "description": "The cluster network profile."
+        },
+        "masterProfile": {
+          "$ref": "#/definitions/MasterProfile",
+          "description": "The cluster master profile."
+        },
+        "workerProfiles": {
+          "description": "The cluster worker profiles.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/WorkerProfile"
+          },
+          "x-ms-identifiers": []
+        },
+        "apiserverProfile": {
+          "$ref": "#/definitions/APIServerProfile",
+          "description": "The cluster API server profile."
+        },
+        "ingressProfiles": {
+          "description": "The cluster ingress profiles.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/IngressProfile"
+          },
+          "x-ms-identifiers": []
+        }
+      }
+    },
+    "OpenShiftClusterUpdate": {
+      "description": "OpenShiftCluster represents an Azure Red Hat OpenShift cluster.",
+      "type": "object",
+      "properties": {
+        "tags": {
+          "$ref": "#/definitions/Tags",
+          "description": "The resource tags."
+        },
+        "properties": {
+          "$ref": "#/definitions/OpenShiftClusterProperties",
+          "description": "The cluster properties.",
+          "x-ms-client-flatten": true
+        },
+        "systemData": {
+          "$ref": "../../../../../common-types/resource-management/v2/types.json#/definitions/systemData",
+          "description": "The system meta data relating to this resource.",
+          "readOnly": true
+        }
+      }
+    },
+    "Operation": {
+      "description": "Operation represents an RP operation.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "description": "Operation name: {provider}/{resource}/{operation}.",
+          "type": "string"
+        },
+        "display": {
+          "$ref": "#/definitions/Display",
+          "description": "The object that describes the operation."
+        },
+        "origin": {
+          "description": "Sources of requests to this operation.  Comma separated list with valid values user or system, e.g. \"user,system\".",
+          "type": "string"
+        }
+      }
+    },
+    "OperationList": {
+      "description": "OperationList represents an RP operation list.",
+      "type": "object",
+      "properties": {
+        "value": {
+          "description": "List of operations supported by the resource provider.",
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/Operation"
+          },
+          "x-ms-identifiers": []
+        },
+        "nextLink": {
+          "description": "The link used to get the next page of operations.",
+          "type": "string"
+        }
+      }
+    },
+    "ProvisioningState": {
+      "description": "ProvisioningState represents a provisioning state.",
+      "enum": [
+        "AdminUpdating",
+        "Creating",
+        "Deleting",
+        "Failed",
+        "Succeeded",
+        "Updating"
+      ],
+      "type": "string"
+    },
+    "ServicePrincipalProfile": {
+      "description": "ServicePrincipalProfile represents a service principal profile.",
+      "type": "object",
+      "properties": {
+        "clientId": {
+          "description": "The client ID used for the cluster.",
+          "type": "string"
+        },
+        "clientSecret": {
+          "description": "The client secret used for the cluster.",
+          "type": "string"
+        }
+      }
+    },
+    "Tags": {
+      "description": "Tags represents an OpenShift cluster's tags.",
+      "type": "object",
+      "additionalProperties": {
+        "type": "string"
+      }
+    },
+    "VMSize": {
+      "description": "VM size availability varies by region.\nIf a node contains insufficient compute resources (memory, cpu, etc.), pods might fail to run correctly.\nFor more details on restricted VM sizes, see: https://docs.microsoft.com/en-us/azure/openshift/support-policies-v4#supported-virtual-machine-sizes",
+      "type": "string"
+    },
+    "Visibility": {
+      "description": "Visibility represents visibility.",
+      "enum": [
+        "Private",
+        "Public"
+      ],
+      "type": "string",
+      "x-ms-enum": {
+        "name": "Visibility",
+        "modelAsString": true
+      }
+    },
+    "WorkerProfile": {
+      "description": "WorkerProfile represents a worker profile.",
+      "type": "object",
+      "properties": {
+        "name": {
+          "description": "The worker profile name.",
+          "type": "string"
+        },
+        "vmSize": {
+          "$ref": "#/definitions/VMSize",
+          "description": "The size of the worker VMs."
+        },
+        "diskSizeGB": {
+          "description": "The disk size of the worker VMs.",
+          "type": "integer"
+        },
+        "subnetId": {
+          "description": "The Azure resource ID of the worker subnet.",
+          "type": "string"
+        },
+        "count": {
+          "description": "The number of worker VMs.",
+          "type": "integer"
+        },
+        "encryptionAtHost": {
+          "$ref": "#/definitions/EncryptionAtHost",
+          "description": "Whether master virtual machines are encrypted at host."
+        },
+        "diskEncryptionSetId": {
+          "description": "The resource ID of an associated DiskEncryptionSet, if applicable.",
+          "type": "string"
+        }
+      }
+    }
+  },
+  "securityDefinitions": {
+    "azure_auth": {
+      "type": "oauth2",
+      "description": "Azure Active Directory OAuth2 Flow",
+      "flow": "implicit",
+      "authorizationUrl": "https://login.microsoftonline.com/common/oauth2/authorize",
+      "scopes": {
+        "user_impersonation": "impersonate your user account"
+      }
+    }
+  },
+  "security": [
+    {
+      "azure_auth": [
+        "user_impersonation"
+      ]
+    }
+  ]
+}


Test patch:
diff --git a/pkg/api/v20220904/openshiftcluster_validatestatic.go b/pkg/api/v20220904/openshiftcluster_validatestatic.go
new file mode 100644
index 000000000..14ff66d6b
--- /dev/null
+++ b/pkg/api/v20220904/openshiftcluster_validatestatic.go
@@ -0,0 +1,364 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/Azure/go-autorest/autorest/azure"
+	"github.com/gofrs/uuid"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	"github.com/Azure/ARO-RP/pkg/api/validate"
+	"github.com/Azure/ARO-RP/pkg/util/immutable"
+	"github.com/Azure/ARO-RP/pkg/util/pullsecret"
+	"github.com/Azure/ARO-RP/pkg/util/subnet"
+	"github.com/Azure/ARO-RP/pkg/util/version"
+)
+
+type openShiftClusterStaticValidator struct {
+	location            string
+	domain              string
+	requireD2sV3Workers bool
+	resourceID          string
+
+	r azure.Resource
+}
+
+// Validate validates an OpenShift cluster
+func (sv *openShiftClusterStaticValidator) Static(_oc interface{}, _current *api.OpenShiftCluster) error {
+	oc := _oc.(*OpenShiftCluster)
+
+	var current *OpenShiftCluster
+	if _current != nil {
+		current = (&openShiftClusterConverter{}).ToExternal(_current).(*OpenShiftCluster)
+	}
+
+	var err error
+	sv.r, err = azure.ParseResourceID(sv.resourceID)
+	if err != nil {
+		return err
+	}
+
+	err = sv.validate(oc, current == nil)
+	if err != nil {
+		return err
+	}
+
+	if current == nil {
+		return nil
+	}
+
+	return sv.validateDelta(oc, current)
+}
+
+func (sv *openShiftClusterStaticValidator) validate(oc *OpenShiftCluster, isCreate bool) error {
+	if !strings.EqualFold(oc.ID, sv.resourceID) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeMismatchingResourceID, "id", "The provided resource ID '%s' did not match the name in the Url '%s'.", oc.ID, sv.resourceID)
+	}
+	if !strings.EqualFold(oc.Name, sv.r.ResourceName) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeMismatchingResourceName, "name", "The provided resource name '%s' did not match the name in the Url '%s'.", oc.Name, sv.r.ResourceName)
+	}
+	if !strings.EqualFold(oc.Type, resourceProviderNamespace+"/"+resourceType) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeMismatchingResourceType, "type", "The provided resource type '%s' did not match the name in the Url '%s'.", oc.Type, resourceProviderNamespace+"/"+resourceType)
+	}
+	if !strings.EqualFold(oc.Location, sv.location) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "location", "The provided location '%s' is invalid.", oc.Location)
+	}
+
+	// TODO: remove the VM name validation after https://bugzilla.redhat.com/show_bug.cgi?id=2093044 is resolved
+	if isCreate {
+		if !validate.OpenShiftClusterNameLength(oc.Name, oc.Location) {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "name", "The provided cluster name '%s' exceeds the maximum cluster name length of '%d'.", oc.Name, validate.MaxClusterNameLength)
+		}
+	}
+
+	return sv.validateProperties("properties", &oc.Properties, isCreate)
+}
+
+func (sv *openShiftClusterStaticValidator) validateProperties(path string, p *OpenShiftClusterProperties, isCreate bool) error {
+	switch p.ProvisioningState {
+	case ProvisioningStateCreating, ProvisioningStateUpdating,
+		ProvisioningStateAdminUpdating, ProvisioningStateDeleting,
+		ProvisioningStateSucceeded, ProvisioningStateFailed:
+	default:
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".provisioningState", "The provided provisioning state '%s' is invalid.", p.ProvisioningState)
+	}
+	if err := sv.validateClusterProfile(path+".clusterProfile", &p.ClusterProfile, isCreate); err != nil {
+		return err
+	}
+	if err := sv.validateConsoleProfile(path+".consoleProfile", &p.ConsoleProfile); err != nil {
+		return err
+	}
+	if err := sv.validateServicePrincipalProfile(path+".servicePrincipalProfile", &p.ServicePrincipalProfile); err != nil {
+		return err
+	}
+	if err := sv.validateNetworkProfile(path+".networkProfile", &p.NetworkProfile); err != nil {
+		return err
+	}
+	if err := sv.validateMasterProfile(path+".masterProfile", &p.MasterProfile); err != nil {
+		return err
+	}
+	if err := sv.validateAPIServerProfile(path+".apiserverProfile", &p.APIServerProfile); err != nil {
+		return err
+	}
+
+	if isCreate {
+		if len(p.WorkerProfiles) != 1 {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".workerProfiles", "There should be exactly one worker profile.")
+		}
+		if err := sv.validateWorkerProfile(path+".workerProfiles['"+p.WorkerProfiles[0].Name+"']", &p.WorkerProfiles[0], &p.MasterProfile); err != nil {
+			return err
+		}
+
+		if len(p.IngressProfiles) != 1 {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ingressProfiles", "There should be exactly one ingress profile.")
+		}
+		if err := sv.validateIngressProfile(path+".ingressProfiles['"+p.IngressProfiles[0].Name+"']", &p.IngressProfiles[0]); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateClusterProfile(path string, cp *ClusterProfile, isCreate bool) error {
+	if pullsecret.Validate(cp.PullSecret) != nil {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".pullSecret", "The provided pull secret is invalid.")
+	}
+	if isCreate {
+		if !validate.RxDomainName.MatchString(cp.Domain) {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".domain", "The provided domain '%s' is invalid.", cp.Domain)
+		}
+	} else {
+		// We currently do not allow domains with a digit as a first charecter,
+		// for new clusters, but we already have some existing clusters with
+		// domains like this and we need to allow customers to update them.
+		if !validate.RxDomainNameRFC1123.MatchString(cp.Domain) {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".domain", "The provided domain '%s' is invalid.", cp.Domain)
+		}
+	}
+	// domain ends .aroapp.io, but doesn't end .<rp-location>.aroapp.io
+	if strings.HasSuffix(cp.Domain, "."+strings.SplitN(sv.domain, ".", 2)[1]) &&
+		!strings.HasSuffix(cp.Domain, "."+sv.domain) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".domain", "The provided domain '%s' is invalid.", cp.Domain)
+	}
+	// domain is of form multiple.names.<rp-location>.aroapp.io
+	if strings.HasSuffix(cp.Domain, "."+sv.domain) &&
+		strings.ContainsRune(strings.TrimSuffix(cp.Domain, "."+sv.domain), '.') {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".domain", "The provided domain '%s' is invalid.", cp.Domain)
+	}
+
+	if isCreate && cp.Version != version.InstallStream.Version.String() {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".version", "The provided version '%s' is invalid.", cp.Version)
+	}
+
+	if !validate.RxResourceGroupID.MatchString(cp.ResourceGroupID) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".resourceGroupId", "The provided resource group '%s' is invalid.", cp.ResourceGroupID)
+	}
+	if strings.Split(cp.ResourceGroupID, "/")[2] != sv.r.SubscriptionID {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".resourceGroupId", "The provided resource group '%s' is invalid: must be in same subscription as cluster.", cp.ResourceGroupID)
+	}
+	if strings.EqualFold(cp.ResourceGroupID, fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", sv.r.SubscriptionID, sv.r.ResourceGroup)) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".resourceGroupId", "The provided resource group '%s' is invalid: must be different from resourceGroup of the OpenShift cluster object.", cp.ResourceGroupID)
+	}
+
+	switch cp.FipsValidatedModules {
+	case FipsValidatedModulesDisabled, FipsValidatedModulesEnabled:
+	default:
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".fipsValidatedModules", "The provided value '%s' is invalid.", cp.FipsValidatedModules)
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateConsoleProfile(path string, cp *ConsoleProfile) error {
+	if cp.URL != "" {
+		if _, err := url.Parse(cp.URL); err != nil {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".url", "The provided console URL '%s' is invalid.", cp.URL)
+		}
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateServicePrincipalProfile(path string, spp *ServicePrincipalProfile) error {
+	_, err := uuid.FromString(spp.ClientID)
+	if err != nil {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".clientId", "The provided client ID '%s' is invalid.", spp.ClientID)
+	}
+	if spp.ClientSecret == "" {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".clientSecret", "The provided client secret is invalid.")
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateNetworkProfile(path string, np *NetworkProfile) error {
+	_, pod, err := net.ParseCIDR(np.PodCIDR)
+	if err != nil {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".podCidr", "The provided pod CIDR '%s' is invalid: '%s'.", np.PodCIDR, err)
+	}
+	if pod.IP.To4() == nil {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".podCidr", "The provided pod CIDR '%s' is invalid: must be IPv4.", np.PodCIDR)
+	}
+	{
+		ones, _ := pod.Mask.Size()
+		if ones > 18 {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".podCidr", "The provided vnet CIDR '%s' is invalid: must be /18 or larger.", np.PodCIDR)
+		}
+	}
+	_, service, err := net.ParseCIDR(np.ServiceCIDR)
+	if err != nil {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".serviceCidr", "The provided service CIDR '%s' is invalid: '%s'.", np.ServiceCIDR, err)
+	}
+	if service.IP.To4() == nil {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".serviceCidr", "The provided service CIDR '%s' is invalid: must be IPv4.", np.ServiceCIDR)
+	}
+	{
+		ones, _ := service.Mask.Size()
+		if ones > 22 {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".serviceCidr", "The provided vnet CIDR '%s' is invalid: must be /22 or larger.", np.ServiceCIDR)
+		}
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateMasterProfile(path string, mp *MasterProfile) error {
+	if !validate.VMSizeIsValid(api.VMSize(mp.VMSize), sv.requireD2sV3Workers, true) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".vmSize", "The provided master VM size '%s' is invalid.", mp.VMSize)
+	}
+	if !validate.RxSubnetID.MatchString(mp.SubnetID) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", "The provided master VM subnet '%s' is invalid.", mp.SubnetID)
+	}
+	sr, err := azure.ParseResourceID(mp.SubnetID)
+	if err != nil {
+		return err
+	}
+	if sr.SubscriptionID != sv.r.SubscriptionID {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", "The provided master VM subnet '%s' is invalid: must be in same subscription as cluster.", mp.SubnetID)
+	}
+	switch mp.EncryptionAtHost {
+	case EncryptionAtHostDisabled, EncryptionAtHostEnabled:
+	default:
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".encryptionAtHost", "The provided value '%s' is invalid.", mp.EncryptionAtHost)
+	}
+	if mp.DiskEncryptionSetID != "" {
+		if !validate.RxDiskEncryptionSetID.MatchString(mp.DiskEncryptionSetID) {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".diskEncryptionSetId", "The provided master disk encryption set '%s' is invalid.", mp.DiskEncryptionSetID)
+		}
+		desr, err := azure.ParseResourceID(mp.DiskEncryptionSetID)
+		if err != nil {
+			return err
+		}
+		if desr.SubscriptionID != sv.r.SubscriptionID {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".diskEncryptionSetId", "The provided master disk encryption set '%s' is invalid: must be in same subscription as cluster.", mp.DiskEncryptionSetID)
+		}
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateWorkerProfile(path string, wp *WorkerProfile, mp *MasterProfile) error {
+	if wp.Name != "worker" {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".name", "The provided worker name '%s' is invalid.", wp.Name)
+	}
+	if !validate.VMSizeIsValid(api.VMSize(wp.VMSize), sv.requireD2sV3Workers, false) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".vmSize", "The provided worker VM size '%s' is invalid.", wp.VMSize)
+	}
+	if !validate.DiskSizeIsValid(wp.DiskSizeGB) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".diskSizeGB", "The provided worker disk size '%d' is invalid.", wp.DiskSizeGB)
+	}
+	if !validate.RxSubnetID.MatchString(wp.SubnetID) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", "The provided worker VM subnet '%s' is invalid.", wp.SubnetID)
+	}
+	switch wp.EncryptionAtHost {
+	case EncryptionAtHostDisabled, EncryptionAtHostEnabled:
+	default:
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".encryptionAtHost", "The provided value '%s' is invalid.", wp.EncryptionAtHost)
+	}
+	workerVnetID, _, err := subnet.Split(wp.SubnetID)
+	if err != nil {
+		return err
+	}
+	masterVnetID, _, err := subnet.Split(mp.SubnetID)
+	if err != nil {
+		return err
+	}
+	if !strings.EqualFold(masterVnetID, workerVnetID) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", "The provided worker VM subnet '%s' is invalid: must be in the same vnet as master VM subnet '%s'.", wp.SubnetID, mp.SubnetID)
+	}
+	if strings.EqualFold(mp.SubnetID, wp.SubnetID) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", "The provided worker VM subnet '%s' is invalid: must be different to master VM subnet '%s'.", wp.SubnetID, mp.SubnetID)
+	}
+	if wp.Count < 2 || wp.Count > 50 {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".count", "The provided worker count '%d' is invalid.", wp.Count)
+	}
+	if !strings.EqualFold(mp.DiskEncryptionSetID, wp.DiskEncryptionSetID) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", "The provided worker disk encryption set '%s' is invalid: must be the same as master disk encryption set '%s'.", wp.DiskEncryptionSetID, mp.DiskEncryptionSetID)
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateAPIServerProfile(path string, ap *APIServerProfile) error {
+	switch ap.Visibility {
+	case VisibilityPublic, VisibilityPrivate:
+	default:
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".visibility", "The provided visibility '%s' is invalid.", ap.Visibility)
+	}
+	if ap.URL != "" {
+		if _, err := url.Parse(ap.URL); err != nil {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".url", "The provided URL '%s' is invalid.", ap.URL)
+		}
+	}
+	if ap.IP != "" {
+		ip := net.ParseIP(ap.IP)
+		if ip == nil {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ip", "The provided IP '%s' is invalid.", ap.IP)
+		}
+		if ip.To4() == nil {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ip", "The provided IP '%s' is invalid: must be IPv4.", ap.IP)
+		}
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateIngressProfile(path string, p *IngressProfile) error {
+	if p.Name != "default" {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".name", "The provided ingress name '%s' is invalid.", p.Name)
+	}
+	switch p.Visibility {
+	case VisibilityPublic, VisibilityPrivate:
+	default:
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".visibility", "The provided visibility '%s' is invalid.", p.Visibility)
+	}
+	if p.IP != "" {
+		ip := net.ParseIP(p.IP)
+		if ip == nil {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ip", "The provided IP '%s' is invalid.", p.IP)
+		}
+		if ip.To4() == nil {
+			return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ip", "The provided IP '%s' is invalid: must be IPv4.", p.IP)
+		}
+	}
+
+	return nil
+}
+
+func (sv *openShiftClusterStaticValidator) validateDelta(oc, current *OpenShiftCluster) error {
+	err := immutable.Validate("", oc, current)
+	if err != nil {
+		err := err.(*immutable.ValidationError)
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodePropertyChangeNotAllowed, err.Target, err.Message)
+	}
+
+	return nil
+}
diff --git a/pkg/api/v20220904/openshiftcluster_validatestatic_test.go b/pkg/api/v20220904/openshiftcluster_validatestatic_test.go
new file mode 100644
index 000000000..4edd8443f
--- /dev/null
+++ b/pkg/api/v20220904/openshiftcluster_validatestatic_test.go
@@ -0,0 +1,986 @@
+package v20220904
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/Azure/go-autorest/autorest/azure"
+	"github.com/Azure/go-autorest/autorest/to"
+	"github.com/gofrs/uuid"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	apiValidate "github.com/Azure/ARO-RP/pkg/api/validate"
+	"github.com/Azure/ARO-RP/pkg/util/version"
+	"github.com/Azure/ARO-RP/test/validate"
+)
+
+type validateTest struct {
+	name                string
+	clusterName         *string
+	location            *string
+	current             func(oc *OpenShiftCluster)
+	modify              func(oc *OpenShiftCluster)
+	requireD2sV3Workers bool
+	wantErr             string
+}
+
+type testMode string
+
+const (
+	testModeCreate testMode = "Create"
+	testModeUpdate testMode = "Update"
+)
+
+var (
+	subscriptionID = "00000000-0000-0000-0000-000000000000"
+)
+
+func getResourceID(clusterName string) string {
+	return fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/microsoft.redhatopenshift/openshiftclusters/%s", subscriptionID, clusterName)
+}
+
+func validOpenShiftCluster(name, location string) *OpenShiftCluster {
+	timestamp, err := time.Parse(time.RFC3339, "2021-01-23T12:34:54.0000000Z")
+	if err != nil {
+		panic(err)
+	}
+
+	oc := &OpenShiftCluster{
+		ID:       getResourceID(name),
+		Name:     name,
+		Type:     "Microsoft.RedHatOpenShift/OpenShiftClusters",
+		Location: location,
+		Tags: Tags{
+			"key": "value",
+		},
+		SystemData: &SystemData{
+			CreatedBy:          "00000000-0000-0000-0000-000000000000",
+			CreatedByType:      CreatedByTypeApplication,
+			CreatedAt:          &timestamp,
+			LastModifiedBy:     "00000000-0000-0000-0000-000000000000",
+			LastModifiedByType: CreatedByTypeApplication,
+			LastModifiedAt:     &timestamp,
+		},
+		Properties: OpenShiftClusterProperties{
+			ProvisioningState: ProvisioningStateSucceeded,
+			ClusterProfile: ClusterProfile{
+				PullSecret:           `{"auths":{"registry.connect.redhat.com":{"auth":""},"registry.redhat.io":{"auth":""}}}`,
+				Domain:               "cluster.location.aroapp.io",
+				Version:              version.InstallStream.Version.String(),
+				ResourceGroupID:      fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster", subscriptionID),
+				FipsValidatedModules: FipsValidatedModulesDisabled,
+			},
+			ConsoleProfile: ConsoleProfile{
+				URL: "https://console-openshift-console.apps.cluster.location.aroapp.io/",
+			},
+			ServicePrincipalProfile: ServicePrincipalProfile{
+				ClientSecret: "clientSecret",
+				ClientID:     "11111111-1111-1111-1111-111111111111",
+			},
+			NetworkProfile: NetworkProfile{
+				PodCIDR:     "10.128.0.0/14",
+				ServiceCIDR: "172.30.0.0/16",
+			},
+			MasterProfile: MasterProfile{
+				VMSize:           "Standard_D8s_v3",
+				EncryptionAtHost: EncryptionAtHostDisabled,
+				SubnetID:         fmt.Sprintf("/subscriptions/%s/resourceGroups/vnet/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/master", subscriptionID),
+			},
+			WorkerProfiles: []WorkerProfile{
+				{
+					Name:             "worker",
+					VMSize:           "Standard_D4s_v3",
+					EncryptionAtHost: EncryptionAtHostDisabled,
+					DiskSizeGB:       128,
+					SubnetID:         fmt.Sprintf("/subscriptions/%s/resourceGroups/vnet/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/worker", subscriptionID),
+					Count:            3,
+				},
+			},
+			APIServerProfile: APIServerProfile{
+				Visibility: VisibilityPublic,
+				URL:        "https://api.cluster.location.aroapp.io:6443/",
+				IP:         "1.2.3.4",
+			},
+			IngressProfiles: []IngressProfile{
+				{
+					Name:       "default",
+					Visibility: VisibilityPublic,
+					IP:         "1.2.3.4",
+				},
+			},
+		},
+	}
+
+	return oc
+}
+
+func runTests(t *testing.T, mode testMode, tests []*validateTest) {
+	t.Run(string(mode), func(t *testing.T) {
+		for _, tt := range tests {
+			t.Run(tt.name, func(t *testing.T) {
+				// default values if not set
+				if tt.location == nil {
+					tt.location = to.StringPtr("location")
+				}
+
+				if tt.clusterName == nil {
+					tt.clusterName = to.StringPtr("resourceName")
+				}
+
+				v := &openShiftClusterStaticValidator{
+					location:            *tt.location,
+					domain:              "location.aroapp.io",
+					requireD2sV3Workers: tt.requireD2sV3Workers,
+					resourceID:          getResourceID(*tt.clusterName),
+					r: azure.Resource{
+						SubscriptionID: subscriptionID,
+						ResourceGroup:  "resourceGroup",
+						Provider:       "Microsoft.RedHatOpenShift",
+						ResourceType:   "openshiftClusters",
+						ResourceName:   *tt.clusterName,
+					},
+				}
+
+				validOCForTest := func() *OpenShiftCluster {
+					oc := validOpenShiftCluster(*tt.clusterName, *tt.location)
+					if tt.current != nil {
+						tt.current(oc)
+					}
+					return oc
+				}
+
+				oc := validOCForTest()
+				if tt.modify != nil {
+					tt.modify(oc)
+				}
+
+				var current *api.OpenShiftCluster
+				if mode == testModeUpdate {
+					current = &api.OpenShiftCluster{}
+					(&openShiftClusterConverter{}).ToInternal(validOCForTest(), current)
+				}
+
+				err := v.Static(oc, current)
+				if err == nil {
+					if tt.wantErr != "" {
+						t.Error(err)
+					}
+				} else {
+					if err.Error() != tt.wantErr {
+						t.Error(err)
+					}
+
+					cloudErr := err.(*api.CloudError)
+
+					if cloudErr.StatusCode != http.StatusBadRequest {
+						t.Error(cloudErr.StatusCode)
+					}
+					if cloudErr.Target == "" {
+						t.Error("target is required")
+					}
+
+					validate.CloudError(t, err)
+				}
+			})
+		}
+	})
+}
+
+func TestOpenShiftClusterStaticValidate(t *testing.T) {
+	clusterName19 := "19characters-aaaaaa"
+	clusterName30 := "thisis30characterslong-aaaaaa"
+	nonZonalRegion := "australiasoutheast"
+
+	commonTests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "id wrong",
+			modify: func(oc *OpenShiftCluster) {
+				oc.ID = "wrong"
+			},
+			wantErr: "400: MismatchingResourceID: id: The provided resource ID 'wrong' did not match the name in the Url '/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourceGroup/providers/microsoft.redhatopenshift/openshiftclusters/resourceName'.",
+		},
+		{
+			name: "name wrong",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Name = "wrong"
+			},
+			wantErr: "400: MismatchingResourceName: name: The provided resource name 'wrong' did not match the name in the Url 'resourceName'.",
+		},
+		{
+			name: "type wrong",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Type = "wrong"
+			},
+			wantErr: "400: MismatchingResourceType: type: The provided resource type 'wrong' did not match the name in the Url 'Microsoft.RedHatOpenShift/openShiftClusters'.",
+		},
+		{
+			name: "location invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Location = "invalid"
+			},
+			wantErr: "400: InvalidParameter: location: The provided location 'invalid' is invalid.",
+		},
+		{
+			name:        "valid - zonal regions can exceed max cluster name length",
+			clusterName: &clusterName30,
+		},
+	}
+
+	createTests := []*validateTest{
+		{
+			name:        "invalid - non-zonal regions cannot exceed max cluster name length on cluster create",
+			clusterName: &clusterName30,
+			location:    &nonZonalRegion,
+			wantErr:     fmt.Sprintf("400: InvalidParameter: name: The provided cluster name '%s' exceeds the maximum cluster name length of '%d'.", clusterName30, apiValidate.MaxClusterNameLength),
+		},
+		{
+			name:        "valid - non-zonal region less than max cluster name length",
+			clusterName: &clusterName19,
+			location:    &nonZonalRegion,
+		},
+	}
+
+	updateTests := []*validateTest{
+		{
+			name:        "valid - existing cluster names > max cluster name length still work on cluster update",
+			clusterName: &clusterName30,
+			location:    &nonZonalRegion,
+		},
+	}
+
+	runTests(t, testModeCreate, commonTests)
+	runTests(t, testModeUpdate, commonTests)
+	runTests(t, testModeCreate, createTests)
+	runTests(t, testModeUpdate, updateTests)
+}
+
+func TestOpenShiftClusterStaticValidateProperties(t *testing.T) {
+	commonTests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "provisioningState invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ProvisioningState = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.provisioningState: The provided provisioning state 'invalid' is invalid.",
+		},
+	}
+	createTests := []*validateTest{
+		{
+			name: "no workerProfiles invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles = nil
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles: There should be exactly one worker profile.",
+		},
+		{
+			name: "multiple workerProfiles invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles = []WorkerProfile{{}, {}}
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles: There should be exactly one worker profile.",
+		},
+	}
+
+	runTests(t, testModeCreate, createTests)
+	runTests(t, testModeCreate, commonTests)
+	runTests(t, testModeUpdate, commonTests)
+}
+
+func TestOpenShiftClusterStaticValidateClusterProfile(t *testing.T) {
+	commonTests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "pull secret not a map",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.PullSecret = "1"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.pullSecret: The provided pull secret is invalid.",
+		},
+		{
+			name: "pull secret invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.PullSecret = "{"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.pullSecret: The provided pull secret is invalid.",
+		},
+		{
+			name: "empty domain invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.Domain = ""
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.domain: The provided domain '' is invalid.",
+		},
+		{
+			name: "upper case domain invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.Domain = "BAD"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.domain: The provided domain 'BAD' is invalid.",
+		},
+		{
+			name: "domain invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.Domain = "!"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.domain: The provided domain '!' is invalid.",
+		},
+		{
+			name: "wrong location managed domain invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.Domain = "cluster.wronglocation.aroapp.io"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.domain: The provided domain 'cluster.wronglocation.aroapp.io' is invalid.",
+		},
+		{
+			name: "double part managed domain invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.Domain = "foo.bar.location.aroapp.io"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.domain: The provided domain 'foo.bar.location.aroapp.io' is invalid.",
+		},
+		{
+			name: "resourceGroupId invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.ResourceGroupID = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.resourceGroupId: The provided resource group 'invalid' is invalid.",
+		},
+		{
+			name: "cluster resource group subscriptionId not matching cluster subscriptionId",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.ResourceGroupID = "/subscriptions/7a3036d1-60a1-4605-8a41-44955e050804/resourcegroups/test-cluster"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.resourceGroupId: The provided resource group '/subscriptions/7a3036d1-60a1-4605-8a41-44955e050804/resourcegroups/test-cluster' is invalid: must be in same subscription as cluster.",
+		},
+		{
+			name: "cluster resourceGroup and external resourceGroup equal",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.ResourceGroupID = "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourceGroup"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.resourceGroupId: The provided resource group '/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourceGroup' is invalid: must be different from resourceGroup of the OpenShift cluster object.",
+		},
+		{
+			name: "fips validated modules invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.FipsValidatedModules = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.fipsValidatedModules: The provided value 'invalid' is invalid.",
+		},
+		{
+			name: "fips validated modules empty",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.FipsValidatedModules = ""
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.fipsValidatedModules: The provided value '' is invalid.",
+		},
+	}
+
+	createTests := []*validateTest{
+		{
+			name: "empty pull secret valid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.PullSecret = ""
+			},
+		},
+		{
+			name: "version invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.Version = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.version: The provided version 'invalid' is invalid.",
+		},
+		{
+			name: "leading digit domain invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.Domain = "4k7f9clk"
+			},
+			wantErr: "400: InvalidParameter: properties.clusterProfile.domain: The provided domain '4k7f9clk' is invalid.",
+		},
+	}
+
+	updateTests := []*validateTest{
+		{
+			name: "leading digit domain valid",
+			current: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.Domain = "4k7f9clk"
+			},
+		},
+	}
+
+	runTests(t, testModeCreate, createTests)
+	runTests(t, testModeCreate, commonTests)
+	runTests(t, testModeUpdate, updateTests)
+	runTests(t, testModeUpdate, commonTests)
+}
+
+func TestOpenShiftClusterStaticValidateConsoleProfile(t *testing.T) {
+	commonTests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "console url invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ConsoleProfile.URL = "\x00"
+			},
+			wantErr: "400: InvalidParameter: properties.consoleProfile.url: The provided console URL '\x00' is invalid.",
+		},
+	}
+
+	createTests := []*validateTest{
+		{
+			name: "empty console url valid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ConsoleProfile.URL = ""
+			},
+		},
+	}
+
+	runTests(t, testModeCreate, createTests)
+	runTests(t, testModeCreate, commonTests)
+	runTests(t, testModeUpdate, commonTests)
+}
+
+func TestOpenShiftClusterStaticValidateServicePrincipalProfile(t *testing.T) {
+	tests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "clientID invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ServicePrincipalProfile.ClientID = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.servicePrincipalProfile.clientId: The provided client ID 'invalid' is invalid.",
+		},
+		{
+			name: "empty clientSecret invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ServicePrincipalProfile.ClientSecret = ""
+			},
+			wantErr: "400: InvalidParameter: properties.servicePrincipalProfile.clientSecret: The provided client secret is invalid.",
+		},
+	}
+
+	runTests(t, testModeCreate, tests)
+	runTests(t, testModeUpdate, tests)
+}
+
+func TestOpenShiftClusterStaticValidateNetworkProfile(t *testing.T) {
+	tests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "podCidr invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.NetworkProfile.PodCIDR = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.networkProfile.podCidr: The provided pod CIDR 'invalid' is invalid: 'invalid CIDR address: invalid'.",
+		},
+		{
+			name: "ipv6 podCidr invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.NetworkProfile.PodCIDR = "::0/0"
+			},
+			wantErr: "400: InvalidParameter: properties.networkProfile.podCidr: The provided pod CIDR '::0/0' is invalid: must be IPv4.",
+		},
+		{
+			name: "serviceCidr invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.NetworkProfile.ServiceCIDR = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.networkProfile.serviceCidr: The provided service CIDR 'invalid' is invalid: 'invalid CIDR address: invalid'.",
+		},
+		{
+			name: "ipv6 serviceCidr invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.NetworkProfile.ServiceCIDR = "::0/0"
+			},
+			wantErr: "400: InvalidParameter: properties.networkProfile.serviceCidr: The provided service CIDR '::0/0' is invalid: must be IPv4.",
+		},
+		{
+			name: "podCidr too small",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.NetworkProfile.PodCIDR = "10.0.0.0/19"
+			},
+			wantErr: "400: InvalidParameter: properties.networkProfile.podCidr: The provided vnet CIDR '10.0.0.0/19' is invalid: must be /18 or larger.",
+		},
+		{
+			name: "serviceCidr too small",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.NetworkProfile.ServiceCIDR = "10.0.0.0/23"
+			},
+			wantErr: "400: InvalidParameter: properties.networkProfile.serviceCidr: The provided vnet CIDR '10.0.0.0/23' is invalid: must be /22 or larger.",
+		},
+	}
+
+	runTests(t, testModeCreate, tests)
+	runTests(t, testModeUpdate, tests)
+}
+
+func TestOpenShiftClusterStaticValidateMasterProfile(t *testing.T) {
+	tests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "vmSize unsupported",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.VMSize = "Standard_D2s_v3"
+			},
+			wantErr: "400: InvalidParameter: properties.masterProfile.vmSize: The provided master VM size 'Standard_D2s_v3' is invalid.",
+		},
+		{
+			name: "subnetId invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.SubnetID = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.masterProfile.subnetId: The provided master VM subnet 'invalid' is invalid.",
+		},
+		{
+			name: "subnet subscriptionId not matching cluster subscriptionId",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.SubnetID = "/subscriptions/7a3036d1-60a1-4605-8a41-44955e050804/resourcegroups/test-vnet/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/master"
+			},
+			wantErr: "400: InvalidParameter: properties.masterProfile.subnetId: The provided master VM subnet '/subscriptions/7a3036d1-60a1-4605-8a41-44955e050804/resourcegroups/test-vnet/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/master' is invalid: must be in same subscription as cluster.",
+		},
+		{
+			name: "disk encryption set is invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.DiskEncryptionSetID = "invalid"
+				oc.Properties.WorkerProfiles[0].DiskEncryptionSetID = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.masterProfile.diskEncryptionSetId: The provided master disk encryption set 'invalid' is invalid.",
+		},
+		{
+			name: "disk encryption set not matching cluster subscriptionId",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.DiskEncryptionSetID = "/subscriptions/7a3036d1-60a1-4605-8a41-44955e050804/resourceGroups/fakeRG/providers/Microsoft.Compute/diskEncryptionSets/fakeDES1"
+			},
+			wantErr: "400: InvalidParameter: properties.masterProfile.diskEncryptionSetId: The provided master disk encryption set '/subscriptions/7a3036d1-60a1-4605-8a41-44955e050804/resourceGroups/fakeRG/providers/Microsoft.Compute/diskEncryptionSets/fakeDES1' is invalid: must be in same subscription as cluster.",
+		},
+		{
+			name: "encryption at host invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.EncryptionAtHost = "Banana"
+			},
+			wantErr: "400: InvalidParameter: properties.masterProfile.encryptionAtHost: The provided value 'Banana' is invalid.",
+		},
+		{
+			name: "encryption at host empty",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.EncryptionAtHost = ""
+			},
+			wantErr: "400: InvalidParameter: properties.masterProfile.encryptionAtHost: The provided value '' is invalid.",
+		},
+	}
+
+	createTests := []*validateTest{
+		{
+			name: "disk encryption set is valid",
+			modify: func(oc *OpenShiftCluster) {
+				desID := fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster/providers/Microsoft.Compute/diskEncryptionSets/test-disk-encryption-set", subscriptionID)
+				oc.Properties.MasterProfile.DiskEncryptionSetID = desID
+				oc.Properties.WorkerProfiles[0].DiskEncryptionSetID = desID
+			},
+		},
+	}
+
+	runTests(t, testModeCreate, createTests)
+	runTests(t, testModeCreate, tests)
+	runTests(t, testModeUpdate, tests)
+}
+
+func TestOpenShiftClusterStaticValidateWorkerProfile(t *testing.T) {
+	tests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "name invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].Name = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['invalid'].name: The provided worker name 'invalid' is invalid.",
+		},
+		{
+			name: "vmSize invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].VMSize = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].vmSize: The provided worker VM size 'invalid' is invalid.",
+		},
+		{
+			name: "vmSize too small (prod)",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].VMSize = "Standard_D2s_v3"
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].vmSize: The provided worker VM size 'Standard_D2s_v3' is invalid.",
+		},
+		{
+			name: "vmSize too big (dev)",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].VMSize = "Standard_D4s_v3"
+			},
+			requireD2sV3Workers: true,
+			wantErr:             "400: InvalidParameter: properties.workerProfiles['worker'].vmSize: The provided worker VM size 'Standard_D4s_v3' is invalid.",
+		},
+		{
+			name: "disk too small",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].DiskSizeGB = 127
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].diskSizeGB: The provided worker disk size '127' is invalid.",
+		},
+		{
+			name: "subnetId invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].SubnetID = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].subnetId: The provided worker VM subnet 'invalid' is invalid.",
+		},
+		{
+			name: "master and worker subnets not in same vnet",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].SubnetID = fmt.Sprintf("/subscriptions/%s/resourceGroups/vnet/providers/Microsoft.Network/virtualNetworks/different-vnet/subnets/worker", subscriptionID)
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].subnetId: The provided worker VM subnet '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/vnet/providers/Microsoft.Network/virtualNetworks/different-vnet/subnets/worker' is invalid: must be in the same vnet as master VM subnet '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/vnet/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/master'.",
+		},
+		{
+			name: "master and worker subnets not different",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].SubnetID = oc.Properties.MasterProfile.SubnetID
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].subnetId: The provided worker VM subnet '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/vnet/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/master' is invalid: must be different to master VM subnet '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/vnet/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/master'.",
+		},
+		{
+			name: "count too small",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].Count = 1
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].count: The provided worker count '1' is invalid.",
+		},
+		{
+			name: "count too big",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].Count = 51
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].count: The provided worker count '51' is invalid.",
+		},
+		{
+			name: "disk encryption set not matching master disk encryption set",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.DiskEncryptionSetID = fmt.Sprintf("/subscriptions/%s/resourceGroups/test-cluster/providers/Microsoft.Compute/diskEncryptionSets/test-disk-encryption-set", subscriptionID)
+				oc.Properties.WorkerProfiles[0].DiskEncryptionSetID = "/subscriptions/7a3036d1-60a1-4605-8a41-44955e050804/resourceGroups/fakeRG/providers/Microsoft.Compute/diskEncryptionSets/fakeDES1"
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].subnetId: The provided worker disk encryption set '/subscriptions/7a3036d1-60a1-4605-8a41-44955e050804/resourceGroups/fakeRG/providers/Microsoft.Compute/diskEncryptionSets/fakeDES1' is invalid: must be the same as master disk encryption set '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-cluster/providers/Microsoft.Compute/diskEncryptionSets/test-disk-encryption-set'.",
+		},
+		{
+			name: "encryption at host invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].EncryptionAtHost = "Banana"
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].encryptionAtHost: The provided value 'Banana' is invalid.",
+		},
+		{
+			name: "encryption at host empty",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].EncryptionAtHost = ""
+			},
+			wantErr: "400: InvalidParameter: properties.workerProfiles['worker'].encryptionAtHost: The provided value '' is invalid.",
+		},
+	}
+
+	// We do not perform this validation on update
+	runTests(t, testModeCreate, tests)
+}
+
+func TestOpenShiftClusterStaticValidateAPIServerProfile(t *testing.T) {
+	commonTests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "visibility invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.APIServerProfile.Visibility = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.apiserverProfile.visibility: The provided visibility 'invalid' is invalid.",
+		},
+		{
+			name: "url invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.APIServerProfile.URL = "\x00"
+			},
+			wantErr: "400: InvalidParameter: properties.apiserverProfile.url: The provided URL '\x00' is invalid.",
+		},
+		{
+			name: "ip invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.APIServerProfile.IP = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.apiserverProfile.ip: The provided IP 'invalid' is invalid.",
+		},
+		{
+			name: "ipv6 ip invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.APIServerProfile.IP = "::"
+			},
+			wantErr: "400: InvalidParameter: properties.apiserverProfile.ip: The provided IP '::' is invalid: must be IPv4.",
+		},
+	}
+
+	createTests := []*validateTest{
+		{
+			name: "empty url valid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.APIServerProfile.URL = ""
+			},
+		},
+		{
+			name: "empty ip valid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.APIServerProfile.IP = ""
+			},
+		},
+	}
+
+	runTests(t, testModeCreate, createTests)
+	runTests(t, testModeCreate, commonTests)
+	runTests(t, testModeUpdate, commonTests)
+}
+
+func TestOpenShiftClusterStaticValidateIngressProfile(t *testing.T) {
+	tests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name: "name invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.IngressProfiles[0].Name = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.ingressProfiles['invalid'].name: The provided ingress name 'invalid' is invalid.",
+		},
+		{
+			name: "visibility invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.IngressProfiles[0].Visibility = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.ingressProfiles['default'].visibility: The provided visibility 'invalid' is invalid.",
+		},
+		{
+			name: "ip invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.IngressProfiles[0].IP = "invalid"
+			},
+			wantErr: "400: InvalidParameter: properties.ingressProfiles['default'].ip: The provided IP 'invalid' is invalid.",
+		},
+		{
+			name: "ipv6 ip invalid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.IngressProfiles[0].IP = "::"
+			},
+			wantErr: "400: InvalidParameter: properties.ingressProfiles['default'].ip: The provided IP '::' is invalid: must be IPv4.",
+		},
+		{
+			name: "empty ip valid",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.IngressProfiles[0].IP = ""
+			},
+		},
+	}
+
+	// we don't validate this on update as all fields are immutable and will
+	// be validated with "mutable" flag
+	runTests(t, testModeCreate, tests)
+}
+
+func TestOpenShiftClusterStaticValidateDelta(t *testing.T) {
+	tests := []*validateTest{
+		{
+			name: "valid",
+		},
+		{
+			name:   "valid id case change",
+			modify: func(oc *OpenShiftCluster) { oc.ID = strings.ToUpper(oc.ID) },
+		},
+		{
+			name:   "valid name case change",
+			modify: func(oc *OpenShiftCluster) { oc.Name = strings.ToUpper(oc.Name) },
+		},
+		{
+			name:   "valid type case change",
+			modify: func(oc *OpenShiftCluster) { oc.Type = strings.ToUpper(oc.Type) },
+		},
+		{
+			name:    "location change",
+			modify:  func(oc *OpenShiftCluster) { oc.Location = strings.ToUpper(oc.Location) },
+			wantErr: "400: PropertyChangeNotAllowed: location: Changing property 'location' is not allowed.",
+		},
+		{
+			name:   "valid tags change",
+			modify: func(oc *OpenShiftCluster) { oc.Tags = Tags{"new": "value"} },
+		},
+		{
+			name:    "provisioningState change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.ProvisioningState = ProvisioningStateFailed },
+			wantErr: "400: PropertyChangeNotAllowed: properties.provisioningState: Changing property 'properties.provisioningState' is not allowed.",
+		},
+		{
+			name:    "console url change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.ConsoleProfile.URL = "invalid" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.consoleProfile.url: Changing property 'properties.consoleProfile.url' is not allowed.",
+		},
+		{
+			name:    "pull secret change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.ClusterProfile.PullSecret = `{"auths":{}}` },
+			wantErr: "400: PropertyChangeNotAllowed: properties.clusterProfile.pullSecret: Changing property 'properties.clusterProfile.pullSecret' is not allowed.",
+		},
+		{
+			name:    "domain change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.ClusterProfile.Domain = "invalid" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.clusterProfile.domain: Changing property 'properties.clusterProfile.domain' is not allowed.",
+		},
+		{
+			name:    "version change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.ClusterProfile.Version = "4.3.999" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.clusterProfile.version: Changing property 'properties.clusterProfile.version' is not allowed.",
+		},
+		{
+			name: "resource group change",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ClusterProfile.ResourceGroupID = oc.Properties.ClusterProfile.ResourceGroupID[:strings.LastIndexByte(oc.Properties.ClusterProfile.ResourceGroupID, '/')] + "/changed"
+			},
+			wantErr: "400: PropertyChangeNotAllowed: properties.clusterProfile.resourceGroupId: Changing property 'properties.clusterProfile.resourceGroupId' is not allowed.",
+		},
+		{
+			name: "apiServer private change",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.APIServerProfile.Visibility = VisibilityPrivate
+			},
+			wantErr: "400: PropertyChangeNotAllowed: properties.apiserverProfile.visibility: Changing property 'properties.apiserverProfile.visibility' is not allowed.",
+		},
+		{
+			name:    "apiServer url change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.APIServerProfile.URL = "invalid" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.apiserverProfile.url: Changing property 'properties.apiserverProfile.url' is not allowed.",
+		},
+		{
+			name:    "apiServer ip change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.APIServerProfile.IP = "2.3.4.5" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.apiserverProfile.ip: Changing property 'properties.apiserverProfile.ip' is not allowed.",
+		},
+		{
+			name: "ingress private change",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.IngressProfiles[0].Visibility = VisibilityPrivate
+			},
+			wantErr: "400: PropertyChangeNotAllowed: properties.ingressProfiles['default'].visibility: Changing property 'properties.ingressProfiles['default'].visibility' is not allowed.",
+		},
+		{
+			name:    "ingress ip change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.IngressProfiles[0].IP = "2.3.4.5" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.ingressProfiles['default'].ip: Changing property 'properties.ingressProfiles['default'].ip' is not allowed.",
+		},
+		{
+			name: "clientId change",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.ServicePrincipalProfile.ClientID = uuid.Must(uuid.NewV4()).String()
+			},
+		},
+		{
+			name:   "clientSecret change",
+			modify: func(oc *OpenShiftCluster) { oc.Properties.ServicePrincipalProfile.ClientSecret = "invalid" },
+		},
+		{
+			name:    "podCidr change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.NetworkProfile.PodCIDR = "0.0.0.0/0" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.networkProfile.podCidr: Changing property 'properties.networkProfile.podCidr' is not allowed.",
+		},
+		{
+			name:    "serviceCidr change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.NetworkProfile.ServiceCIDR = "0.0.0.0/0" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.networkProfile.serviceCidr: Changing property 'properties.networkProfile.serviceCidr' is not allowed.",
+		},
+		{
+			name: "master subnetId change",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.MasterProfile.SubnetID = oc.Properties.MasterProfile.SubnetID[:strings.LastIndexByte(oc.Properties.MasterProfile.SubnetID, '/')] + "/changed"
+			},
+			wantErr: "400: PropertyChangeNotAllowed: properties.masterProfile.subnetId: Changing property 'properties.masterProfile.subnetId' is not allowed.",
+		},
+		{
+			name:    "worker name change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.WorkerProfiles[0].Name = "new-name" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.workerProfiles['new-name'].name: Changing property 'properties.workerProfiles['new-name'].name' is not allowed.",
+		},
+		{
+			name:    "worker vmSize change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.WorkerProfiles[0].VMSize = "Standard_D8s_v3" },
+			wantErr: "400: PropertyChangeNotAllowed: properties.workerProfiles['worker'].vmSize: Changing property 'properties.workerProfiles['worker'].vmSize' is not allowed.",
+		},
+		{
+			name:    "worker diskSizeGB change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.WorkerProfiles[0].DiskSizeGB++ },
+			wantErr: "400: PropertyChangeNotAllowed: properties.workerProfiles['worker'].diskSizeGB: Changing property 'properties.workerProfiles['worker'].diskSizeGB' is not allowed.",
+		},
+		{
+			name: "worker subnetId change",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles[0].SubnetID = oc.Properties.WorkerProfiles[0].SubnetID[:strings.LastIndexByte(oc.Properties.WorkerProfiles[0].SubnetID, '/')] + "/changed"
+			},
+			wantErr: "400: PropertyChangeNotAllowed: properties.workerProfiles['worker'].subnetId: Changing property 'properties.workerProfiles['worker'].subnetId' is not allowed.",
+		},
+		{
+			name:    "workerProfiles count change",
+			modify:  func(oc *OpenShiftCluster) { oc.Properties.WorkerProfiles[0].Count++ },
+			wantErr: "400: PropertyChangeNotAllowed: properties.workerProfiles['worker'].count: Changing property 'properties.workerProfiles['worker'].count' is not allowed.",
+		},
+		{
+			name: "number of workerProfiles changes",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles = []WorkerProfile{{}, {}}
+			},
+			wantErr: "400: PropertyChangeNotAllowed: properties.workerProfiles: Changing property 'properties.workerProfiles' is not allowed.",
+		},
+		{
+			name: "workerProfiles set to nil",
+			modify: func(oc *OpenShiftCluster) {
+				oc.Properties.WorkerProfiles = nil
+			},
+			wantErr: "400: PropertyChangeNotAllowed: properties.workerProfiles: Changing property 'properties.workerProfiles' is not allowed.",
+		},
+		{
+			name: "systemData set to empty",
+			modify: func(oc *OpenShiftCluster) {
+				oc.SystemData = &SystemData{}
+			},
+			wantErr: "400: PropertyChangeNotAllowed: systemData.createdBy: Changing property 'systemData.createdBy' is not allowed.",
+		},
+		{
+			name: "systemData LastUpdated changed",
+			modify: func(oc *OpenShiftCluster) {
+				oc.SystemData.LastModifiedBy = "Bob"
+			},
+			wantErr: "400: PropertyChangeNotAllowed: systemData.lastModifiedBy: Changing property 'systemData.lastModifiedBy' is not allowed.",
+		},
+	}
+
+	runTests(t, testModeUpdate, tests)
+}
diff --git a/pkg/api/validate/dynamic/dynamic_test.go b/pkg/api/validate/dynamic/dynamic_test.go
index 750be20e9..f5310d53b 100644
--- a/pkg/api/validate/dynamic/dynamic_test.go
+++ b/pkg/api/validate/dynamic/dynamic_test.go
@@ -598,7 +598,7 @@ func TestValidateSubnets(t *testing.T) {
 			},
 		},
 		{
-			name: "fail: subnet doe not exist on vnet",
+			name: "fail: subnet does not exist on vnet",
 			vnetMocks: func(vnetClient *mock_network.MockVirtualNetworksClient, vnet mgmtnetwork.VirtualNetwork) {
 				vnet.Subnets = nil
 				vnetClient.EXPECT().
@@ -607,6 +607,25 @@ func TestValidateSubnets(t *testing.T) {
 			},
 			wantErr: "400: InvalidLinkedVNet: : The provided subnet '" + masterSubnet + "' could not be found.",
 		},
+		{
+			name: "pass: subnet provisioning state is succeeded",
+			vnetMocks: func(vnetClient *mock_network.MockVirtualNetworksClient, vnet mgmtnetwork.VirtualNetwork) {
+				(*vnet.Subnets)[0].ProvisioningState = mgmtnetwork.Succeeded
+				vnetClient.EXPECT().
+					Get(gomock.Any(), resourceGroupName, vnetName, "").
+					Return(vnet, nil)
+			},
+		},
+		{
+			name: "fail: subnet provisioning state is not succeeded",
+			vnetMocks: func(vnetClient *mock_network.MockVirtualNetworksClient, vnet mgmtnetwork.VirtualNetwork) {
+				(*vnet.Subnets)[0].ProvisioningState = mgmtnetwork.Failed
+				vnetClient.EXPECT().
+					Get(gomock.Any(), resourceGroupName, vnetName, "").
+					Return(vnet, nil)
+			},
+			wantErr: "400: InvalidLinkedVNet: properties.masterProfile.subnetId: The provided subnet '" + masterSubnet + "' is not in a Succeeded state",
+		},
 		{
 			name: "fail: provisioning state creating: subnet has NSG",
 			modifyOC: func(oc *api.OpenShiftCluster) {
@@ -684,6 +703,7 @@ func TestValidateSubnets(t *testing.T) {
 								NetworkSecurityGroup: &mgmtnetwork.SecurityGroup{
 									ID: &masterNSGv1,
 								},
+								ProvisioningState: mgmtnetwork.Succeeded,
 							},
 						},
 					},
diff --git a/pkg/backend/openshiftcluster_test.go b/pkg/backend/openshiftcluster_test.go
index 4baa146a4..98663b6c7 100644
--- a/pkg/backend/openshiftcluster_test.go
+++ b/pkg/backend/openshiftcluster_test.go
@@ -12,6 +12,7 @@ import (
 
 	"github.com/golang/mock/gomock"
 	"github.com/sirupsen/logrus"
+	"k8s.io/client-go/rest"
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/cluster"
@@ -292,7 +293,7 @@ func TestBackendTry(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			createManager := func(context.Context, *logrus.Entry, env.Interface, database.OpenShiftClusters, database.Gateway, encryption.AEAD, billing.Manager, *api.OpenShiftClusterDocument, *api.SubscriptionDocument) (cluster.Interface, error) {
+			createManager := func(context.Context, *logrus.Entry, env.Interface, database.OpenShiftClusters, database.Gateway, encryption.AEAD, billing.Manager, *api.OpenShiftClusterDocument, *api.SubscriptionDocument, *rest.Config) (cluster.Interface, error) {
 				return manager, nil
 			}
 
diff --git a/pkg/installer/deployresources_test.go b/pkg/installer/deployresources_test.go
index 0b7224d25..29aec39b0 100644
--- a/pkg/installer/deployresources_test.go
+++ b/pkg/installer/deployresources_test.go
@@ -20,7 +20,6 @@ func TestZones(t *testing.T) {
 		region     string
 		replicas   int64
 		wantMaster *[]string
-		wantWorker *[]string
 		wantErr    string
 	}{
 		{
@@ -34,7 +33,6 @@ func TestZones(t *testing.T) {
 			wantMaster: &[]string{
 				"1",
 			},
-			wantWorker: &[]string{"1"},
 		},
 		{
 			name:  "2 zones, 3 replicas",
@@ -43,23 +41,11 @@ func TestZones(t *testing.T) {
 				"1",
 				"2",
 			},
-			wantWorker: &[]string{
-				"1",
-				"2",
-			},
-		},
-		{
-			name:       "centraluseuap",
-			zones:      []string{"1", "2"},
-			region:     "centraluseuap",
-			wantMaster: &[]string{"2"},
-			wantWorker: &[]string{"2"},
 		},
 		{
 			name:       "3 zones, 3 replicas",
 			zones:      []string{"1", "2", "3"},
 			wantMaster: &[]string{"[copyIndex(1)]"},
-			wantWorker: &[]string{"[copyIndex(1)]"},
 		},
 		{
 			name:    "4 zones, 3 replicas",
@@ -105,9 +91,6 @@ func TestZones(t *testing.T) {
 			if !reflect.DeepEqual(tt.wantMaster, zones) {
 				t.Errorf("Expected master %v, got master %v", tt.wantMaster, zones)
 			}
-			if !reflect.DeepEqual(tt.wantWorker, zones) {
-				t.Errorf("Expected worker %v, got worker %v", tt.wantWorker, zones)
-			}
 		})
 	}
 }
diff --git a/pkg/operator/controllers/checker/clusterdns_test.go b/pkg/operator/controllers/checker/clusterdns_test.go
new file mode 100644
index 000000000..2ac515560
--- /dev/null
+++ b/pkg/operator/controllers/checker/clusterdns_test.go
@@ -0,0 +1,193 @@
+package checker
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/go-test/deep"
+	operatorv1 "github.com/openshift/api/operator/v1"
+	operatorfake "github.com/openshift/client-go/operator/clientset/versioned/fake"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	arov1alpha1 "github.com/Azure/ARO-RP/pkg/operator/apis/aro.openshift.io/v1alpha1"
+	arofake "github.com/Azure/ARO-RP/pkg/operator/clientset/versioned/fake"
+)
+
+func TestDefaultClusterDNS(t *testing.T) {
+	ctx := context.Background()
+
+	for _, tt := range []struct {
+		name          string
+		aroCluster    *arov1alpha1.Cluster
+		dns           *operatorv1.DNS
+		expectedState operatorv1.OperatorCondition
+	}{
+		{
+			name: "run: has default DNS",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			dns: &operatorv1.DNS{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "default",
+				},
+				Spec: operatorv1.DNSSpec{},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultClusterDNS,
+				Status:  operatorv1.ConditionTrue,
+				Reason:  "CheckDone",
+				Message: "No in-cluster upstream DNS servers",
+			},
+		},
+		{
+			name: "run: has changed DNS",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			dns: &operatorv1.DNS{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "default",
+				},
+				Spec: operatorv1.DNSSpec{
+					Servers: []operatorv1.Server{
+						{
+							Name:  "test-server",
+							Zones: []string{"example.com"},
+							ForwardPlugin: operatorv1.ForwardPlugin{
+								Upstreams: []string{
+									"1.2.3.4", "5.6.7.8",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultClusterDNS,
+				Status:  operatorv1.ConditionFalse,
+				Reason:  "CheckDone",
+				Message: "Custom upstream DNS servers in use: 1.2.3.4, 5.6.7.8",
+			},
+		},
+		{
+			name: "run: malformed, servers but no forwardplugin",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			dns: &operatorv1.DNS{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "default",
+				},
+				Spec: operatorv1.DNSSpec{
+					Servers: []operatorv1.Server{
+						{
+							Name:  "test-server",
+							Zones: []string{"example.com"},
+						},
+					},
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultClusterDNS,
+				Status:  operatorv1.ConditionTrue,
+				Reason:  "CheckDone",
+				Message: "No in-cluster upstream DNS servers",
+			},
+		},
+		{
+			name: "fail: malformed, using . for zones",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			dns: &operatorv1.DNS{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "default",
+				},
+				Spec: operatorv1.DNSSpec{
+					Servers: []operatorv1.Server{
+						{
+							Name:  "test-server",
+							Zones: []string{"."},
+							ForwardPlugin: operatorv1.ForwardPlugin{
+								Upstreams: []string{
+									"1.2.3.4", "5.6.7.8",
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultClusterDNS,
+				Status:  operatorv1.ConditionFalse,
+				Reason:  "CheckDone",
+				Message: `Malformed config: "." in zones`,
+			},
+		},
+		{
+			name: "fail: no DNS",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultClusterDNS,
+				Status:  operatorv1.ConditionUnknown,
+				Reason:  "CheckFailed",
+				Message: `dnses.operator.openshift.io "default" not found`,
+			},
+		},
+	} {
+		arocli := arofake.NewSimpleClientset()
+		operatorcli := operatorfake.NewSimpleClientset()
+
+		if tt.aroCluster != nil {
+			arocli = arofake.NewSimpleClientset(tt.aroCluster)
+		}
+		if tt.dns != nil {
+			operatorcli = operatorfake.NewSimpleClientset(tt.dns)
+		}
+
+		sp := NewClusterDNSChecker(nil, arocli, operatorcli, "")
+
+		t.Run(tt.name, func(t *testing.T) {
+			err := sp.Check(ctx)
+
+			if err != nil {
+				t.Error(err)
+			}
+
+			cluster, err := arocli.AroV1alpha1().Clusters().Get(ctx, arov1alpha1.SingletonClusterName, metav1.GetOptions{})
+			if err != nil {
+				t.Error(err)
+			}
+
+			conds := []operatorv1.OperatorCondition{}
+
+			// nil out time
+			for _, c := range cluster.Status.Conditions {
+				c.LastTransitionTime = metav1.NewTime(time.Time{})
+				conds = append(conds, c)
+			}
+
+			errs := deep.Equal(conds, []operatorv1.OperatorCondition{tt.expectedState})
+			for _, err := range errs {
+				t.Error(err)
+			}
+		})
+	}
+}
diff --git a/pkg/operator/controllers/checker/ingresscert_test.go b/pkg/operator/controllers/checker/ingresscert_test.go
new file mode 100644
index 000000000..7601650ce
--- /dev/null
+++ b/pkg/operator/controllers/checker/ingresscert_test.go
@@ -0,0 +1,221 @@
+package checker
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/go-test/deep"
+	configv1 "github.com/openshift/api/config/v1"
+	operatorv1 "github.com/openshift/api/operator/v1"
+	configfake "github.com/openshift/client-go/config/clientset/versioned/fake"
+	operatorfake "github.com/openshift/client-go/operator/clientset/versioned/fake"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	arov1alpha1 "github.com/Azure/ARO-RP/pkg/operator/apis/aro.openshift.io/v1alpha1"
+	arofake "github.com/Azure/ARO-RP/pkg/operator/clientset/versioned/fake"
+)
+
+func TestDefaultIngressCertificate(t *testing.T) {
+	ctx := context.Background()
+
+	for _, tt := range []struct {
+		name              string
+		aroCluster        *arov1alpha1.Cluster
+		clusterVersion    *configv1.ClusterVersion
+		ingressController *operatorv1.IngressController
+		expectedState     operatorv1.OperatorCondition
+	}{
+		{
+			name: "run: has default certificate",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			clusterVersion: &configv1.ClusterVersion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "version",
+				},
+				Spec: configv1.ClusterVersionSpec{
+					ClusterID: "00000000-0000-0000-0000-000000000001",
+				},
+			},
+			ingressController: &operatorv1.IngressController{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "default",
+					Namespace: "openshift-ingress-operator",
+				},
+				Spec: operatorv1.IngressControllerSpec{
+					DefaultCertificate: &corev1.LocalObjectReference{
+						Name: "00000000-0000-0000-0000-000000000001-ingress",
+					},
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultIngressCertificate,
+				Status:  operatorv1.ConditionTrue,
+				Reason:  "CheckDone",
+				Message: "Default ingress certificate in use",
+			},
+		},
+		{
+			name: "run: does not have default certificate",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			clusterVersion: &configv1.ClusterVersion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "version",
+				},
+				Spec: configv1.ClusterVersionSpec{
+					ClusterID: "00000000-0000-0000-0000-000000000001",
+				},
+			},
+			ingressController: &operatorv1.IngressController{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "default",
+					Namespace: "openshift-ingress-operator",
+				},
+				Spec: operatorv1.IngressControllerSpec{
+					DefaultCertificate: &corev1.LocalObjectReference{
+						Name: "fancy-custom-cert",
+					},
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultIngressCertificate,
+				Status:  operatorv1.ConditionFalse,
+				Reason:  "CheckDone",
+				Message: "Custom ingress certificate in use: fancy-custom-cert",
+			},
+		},
+		{
+			name: "run: does not have default certificate defined",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			clusterVersion: &configv1.ClusterVersion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "version",
+				},
+				Spec: configv1.ClusterVersionSpec{
+					ClusterID: "00000000-0000-0000-0000-000000000001",
+				},
+			},
+			ingressController: &operatorv1.IngressController{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "default",
+					Namespace: "openshift-ingress-operator",
+				},
+				Spec: operatorv1.IngressControllerSpec{
+					DefaultCertificate: nil,
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultIngressCertificate,
+				Status:  operatorv1.ConditionFalse,
+				Reason:  "CheckDone",
+				Message: "Ingress has no certificate yet",
+			},
+		},
+		{
+			name: "fail: no ingress controller",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			clusterVersion: &configv1.ClusterVersion{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "version",
+				},
+				Spec: configv1.ClusterVersionSpec{
+					ClusterID: "00000000-0000-0000-0000-000000000001",
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultIngressCertificate,
+				Status:  operatorv1.ConditionUnknown,
+				Reason:  "CheckFailed",
+				Message: `ingresscontrollers.operator.openshift.io "default" not found`,
+			},
+		},
+
+		{
+			name: "fail: no clusterversion",
+			aroCluster: &arov1alpha1.Cluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: arov1alpha1.SingletonClusterName,
+				},
+			},
+			ingressController: &operatorv1.IngressController{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "default",
+					Namespace: "openshift-ingress-operator",
+				},
+				Spec: operatorv1.IngressControllerSpec{
+					DefaultCertificate: &corev1.LocalObjectReference{
+						Name: "fancy-custom-cert",
+					},
+				},
+			},
+			expectedState: operatorv1.OperatorCondition{
+				Type:    arov1alpha1.DefaultIngressCertificate,
+				Status:  operatorv1.ConditionUnknown,
+				Reason:  "CheckFailed",
+				Message: `clusterversions.config.openshift.io "version" not found`,
+			},
+		},
+	} {
+		arocli := arofake.NewSimpleClientset()
+		configcli := configfake.NewSimpleClientset()
+		operatorcli := operatorfake.NewSimpleClientset()
+
+		if tt.aroCluster != nil {
+			arocli = arofake.NewSimpleClientset(tt.aroCluster)
+		}
+		if tt.clusterVersion != nil {
+			configcli = configfake.NewSimpleClientset(tt.clusterVersion)
+		}
+		if tt.ingressController != nil {
+			operatorcli = operatorfake.NewSimpleClientset(tt.ingressController)
+		}
+
+		sp := NewIngressCertificateChecker(nil, arocli, operatorcli, configcli, "")
+
+		t.Run(tt.name, func(t *testing.T) {
+			err := sp.Check(ctx)
+
+			if err != nil {
+				t.Error(err)
+			}
+
+			cluster, err := arocli.AroV1alpha1().Clusters().Get(ctx, arov1alpha1.SingletonClusterName, metav1.GetOptions{})
+			if err != nil {
+				t.Error(err)
+			}
+
+			conds := []operatorv1.OperatorCondition{}
+
+			// nil out time
+			for _, c := range cluster.Status.Conditions {
+				c.LastTransitionTime = metav1.NewTime(time.Time{})
+				conds = append(conds, c)
+			}
+
+			errs := deep.Equal(conds, []operatorv1.OperatorCondition{tt.expectedState})
+			for _, err := range errs {
+				t.Error(err)
+			}
+		})
+	}
+}
diff --git a/pkg/util/dynamichelper/dynamichelper_test.go b/pkg/util/dynamichelper/dynamichelper_test.go
index b7103d823..2da2c4bde 100644
--- a/pkg/util/dynamichelper/dynamichelper_test.go
+++ b/pkg/util/dynamichelper/dynamichelper_test.go
@@ -4,6 +4,8 @@ package dynamichelper
 // Licensed under the Apache License 2.0.
 
 import (
+	"context"
+	"net/http"
 	"reflect"
 	"testing"
 	"time"
@@ -18,11 +20,73 @@ import (
 	kruntime "k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/intstr"
+	"k8s.io/cli-runtime/pkg/resource"
+	"k8s.io/client-go/rest/fake"
 
 	arov1alpha1 "github.com/Azure/ARO-RP/pkg/operator/apis/aro.openshift.io/v1alpha1"
 	"github.com/Azure/ARO-RP/pkg/util/cmp"
 )
 
+type mockGVRResolver struct{}
+
+func (gvr mockGVRResolver) Refresh() error {
+	return nil
+}
+
+func (gvr mockGVRResolver) Resolve(groupKind, optionalVersion string) (*schema.GroupVersionResource, error) {
+	return &schema.GroupVersionResource{Group: "metal3.io", Version: "v1alpha1", Resource: "configmap"}, nil
+}
+
+func TestEsureDeleted(t *testing.T) {
+	ctx := context.Background()
+
+	mockGVRResolver := mockGVRResolver{}
+
+	mockRestCLI := &fake.RESTClient{
+		GroupVersion:         schema.GroupVersion{Group: "testgroup", Version: "v1"},
+		NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
+		Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
+			switch req.Method {
+			case "DELETE":
+				switch req.URL.Path {
+				case "/apis/metal3.io/v1alpha1/namespaces/test-ns-1/configmap/test-name-1":
+					return &http.Response{StatusCode: http.StatusNotFound}, nil
+				case "/apis/metal3.io/v1alpha1/namespaces/test-ns-2/configmap/test-name-2":
+					return &http.Response{StatusCode: http.StatusInternalServerError}, nil
+				case "/apis/metal3.io/v1alpha1/namespaces/test-ns-3/configmap/test-name-3":
+					return &http.Response{StatusCode: http.StatusOK}, nil
+				default:
+					t.Fatalf("unexpected path: %#v\n%#v", req.URL, req)
+					return nil, nil
+				}
+			default:
+				t.Fatalf("unexpected request: %s %#v\n%#v", req.Method, req.URL, req)
+				return nil, nil
+			}
+		}),
+	}
+
+	dh := &dynamicHelper{
+		GVRResolver: mockGVRResolver,
+		restcli:     mockRestCLI,
+	}
+
+	err := dh.EnsureDeleted(ctx, "configmap", "test-ns-1", "test-name-1")
+	if err != nil {
+		t.Errorf("no error should be bounced for status not found, but got: %v", err)
+	}
+
+	err = dh.EnsureDeleted(ctx, "configmap", "test-ns-2", "test-name-2")
+	if err == nil {
+		t.Errorf("function should handle failure response (non-404) correctly")
+	}
+
+	err = dh.EnsureDeleted(ctx, "configmap", "test-ns-3", "test-name-3")
+	if err != nil {
+		t.Errorf("function should handle success response correctly")
+	}
+}
+
 func TestMerge(t *testing.T) {
 	serviceInternalTrafficPolicy := corev1.ServiceInternalTrafficPolicyCluster