Repository: Azure/ARO-RP
Pull request #2120 (2022-05-06 13:20:59)

improve Python unit testing and refactor _validators.py
### Which issue this PR addresses:

<!--
Please include a link to the ADO work item as well as any GitHub issues.

Usage: `Fixes #<GitHub issue number>`, or `Fixes (paste link of issue)`.
-->
Improvement of Python code testing and refactor of _validators.py (application of Guard clauses, give some names to magic numbers, etc).

### What this PR does / why we need it:
Before this PR there was no execution of any Python unit tests (even though we have a prompt saying "Python Unit tests", we were just executing the Python linter). 

Now we have a lot more Python code covered by unit tests.

This PR simplifies the code in python/az/aro/azext_aro/_validators.py applying Guard clauses, avoiding having nested ifs and complex boolean conditions in order to improve the understanding of the code at first sight.

This PR also splits the Python test folder in:
- unit tests (no external dependencies) and 
- integration tests (depends on Microsoft testing libraries and always fails -> this is why we did not execute Python tests before this PR). Now that we split tests in unit vs integrations we can safely execute unit tests without depending on MSFT things and without the tests failing all the time.

This PR adds Unit Tests for all the functions in _validators.py (it uses pytest.mark.parametrize for a cleaner data-oriented tests).

Lastly, it includes directives in the Makefile to execute python UNIT TESTS (no integration). -> this could be done directly in the Makefile but I decided to write a simple script because 

1) the Python unit tests are invoked in two recipes of the Makefile: 
    - one to just run the unit test (unit-test-python) 
    - and another one to also run the linter (test-python)

(there are two different recipes in the Makefile because when programming, you just one to make your tests go green without worrying about the linter, and just before committing the code, execute the linter to make sure there are no linting errors. To sum up: it is more convenient to execute N times 
```
make unit-test-python
```
and just once 
```
make test-python
``` 
at the end, all that for a faster feedback loop).

2) IMHO it's better to have a big independent script and a clean Makefile rather than a bloated Makefile. If in the future we change how to execute the unit tests, we will touch a single file with that responsibility, the script, and NO the whole Makefile.

<!--
Include a brief summary of what the PR is intended to accomplish and how the PR
does it. (2-3 sentences)
-->

### Is there any documentation that needs to be updated for this PR?

<!--
- If yes and the docs are in GitHub, include doc updates in the PR.
- If yes and the docs are not in GitHub (i.e. ADO wiki), include a link to the
  docs.
- If no, explain why (e.g. "tech debt cleanup, N/A").
-->
N/A. One could argue that the new Unit tests also serve as executable documentation.


Patch:
diff --git a/.github/workflows/golint.yml b/.github/workflows/golint.yml
index 4bbea0171..c2ccc6c2b 100644
--- a/.github/workflows/golint.yml
+++ b/.github/workflows/golint.yml
@@ -1,4 +1,4 @@
-name: golangci-lint
+name: golang-lint
 on:
   push:
     tags:
@@ -13,7 +13,7 @@ permissions:
   # pull-requests: read
 jobs:
   golangci:
-    name: lint
+    name: golangci-lint
     runs-on: ubuntu-latest
     steps:
       - run: |
@@ -47,3 +47,12 @@ jobs:
 
           # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
           # skip-build-cache: true
+  validate-go:
+    name: validate-go
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/setup-go@v3
+        with:
+          go-version: 1.17
+      - uses: actions/checkout@v3
+      - run: make validate-go-action
diff --git a/.pipelines/ci.yml b/.pipelines/ci.yml
index 9e39902ef..39d209b21 100644
--- a/.pipelines/ci.yml
+++ b/.pipelines/ci.yml
@@ -46,12 +46,6 @@ jobs:
       [[ -z "$(git status -s)" ]]
     displayName: ⚙️ Run Golang code generate
 
-  - script: |
-      set -xe
-      make validate-go
-      [[ -z "$(git status -s)" ]]
-    displayName: 🕵️ Validate Golang code
-
   - script: |
       set -xe
       make build-all
diff --git a/.pipelines/templates/template-job-deploy-azure-env-tag.yml b/.pipelines/templates/template-job-deploy-azure-env-tag.yml
index 0253857cb..b1b2a681c 100644
--- a/.pipelines/templates/template-job-deploy-azure-env-tag.yml
+++ b/.pipelines/templates/template-job-deploy-azure-env-tag.yml
@@ -87,7 +87,17 @@ jobs:
     - script: |
         # Pass variables between tasks: https://medium.com/microsoftazure/how-to-pass-variables-in-azure-pipelines-yaml-tasks-5c81c5d31763
         echo "##vso[task.setvariable variable=REGION]${{ location }}"
-        CLUSTER="v4-e2e-V$BUILD_BUILDID-${{ location }}"
+        # TODO: Remove this hack after AvailabilitySet name too long bug is fixed.
+        LOCATION=${{ location }}
+        NONZONAL_REGIONS="australiacentral australiacentral2 australiasoutheast brazilsoutheast canadaeast japanwest northcentralus norwaywest southindia switzerlandwest uaenorth ukwest westcentralus westus"
+        if echo $NONZONAL_REGIONS | grep -wq $LOCATION
+        then
+            CLUSTER=$(head -c 19 <<< "v4-e2e-V$BUILD_BUILDID-$LOCATION")
+        else
+            CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
+        fi
+        # TODO: Uncomment next line after above hack is removed.
+        # CLUSTER="v4-e2e-V$BUILD_BUILDID-${{ location }}"
         echo "##vso[task.setvariable variable=CLUSTER]$CLUSTER"
         CLUSTER_RESOURCEGROUP="v4-e2e-V$BUILD_BUILDID-${{ location }}"
         echo "##vso[task.setvariable variable=CLUSTER_RESOURCEGROUP]$CLUSTER_RESOURCEGROUP"
diff --git a/.pipelines/templates/template-job-deploy-azure-env.yml b/.pipelines/templates/template-job-deploy-azure-env.yml
index d380c45b3..56289d4ed 100644
--- a/.pipelines/templates/template-job-deploy-azure-env.yml
+++ b/.pipelines/templates/template-job-deploy-azure-env.yml
@@ -83,7 +83,17 @@ jobs:
     - script: |
         # Pass variables between tasks: https://medium.com/microsoftazure/how-to-pass-variables-in-azure-pipelines-yaml-tasks-5c81c5d31763
         echo "##vso[task.setvariable variable=REGION]${{ location }}"
-        CLUSTER="v4-e2e-V$BUILD_BUILDID-${{ location }}"
+        # TODO: Remove this hack after AvailabilitySet name too long bug is fixed.
+        LOCATION=${{ location }}
+        NONZONAL_REGIONS="australiacentral australiacentral2 australiasoutheast brazilsoutheast canadaeast japanwest northcentralus norwaywest southindia switzerlandwest uaenorth ukwest westcentralus westus"
+        if echo $NONZONAL_REGIONS | grep -wq $LOCATION
+        then
+            CLUSTER=$(head -c 19 <<< "v4-e2e-V$BUILD_BUILDID-$LOCATION")
+        else
+            CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
+        fi
+        # TODO: Uncomment next line after above hack is removed.
+        # CLUSTER="v4-e2e-V$BUILD_BUILDID-${{ location }}"
         echo "##vso[task.setvariable variable=CLUSTER]$CLUSTER"
         CLUSTER_RESOURCEGROUP="v4-e2e-V$BUILD_BUILDID-${{ location }}"
         echo "##vso[task.setvariable variable=CLUSTER_RESOURCEGROUP]$CLUSTER_RESOURCEGROUP"
diff --git a/Makefile b/Makefile
index 531d0c3f7..c278dd1bb 100644
--- a/Makefile
+++ b/Makefile
@@ -158,6 +158,13 @@ validate-go:
 	go vet ./...
 	go test -tags e2e -run ^$$ ./test/e2e/...
 
+validate-go-action:
+	go run ./hack/licenses -validate -ignored-go vendor,pkg/client,.git -ignored-python python/client,vendor,.git
+	go run ./hack/validate-imports cmd hack pkg test
+	@[ -z "$$(ls pkg/util/*.go 2>/dev/null)" ] || (echo error: go files are not allowed in pkg/util, use a subpackage; exit 1)
+	@[ -z "$$(find -name "*:*")" ] || (echo error: filenames with colons are not allowed on Windows, please rename; exit 1)
+	@sha256sum --quiet -c .sha256sum || (echo error: client library is stale, please run make client; exit 1)
+
 validate-fips:
 	hack/fips/validate-fips.sh
 
@@ -165,7 +172,7 @@ unit-test-go:
 	go run ./vendor/gotest.tools/gotestsum/main.go --format pkgname --junitfile report.xml -- -tags=aro,containers_image_openpgp -coverprofile=cover.out ./...
 
 lint-go:
-	go run ./vendor/github.com/golangci/golangci-lint/cmd/golangci-lint run
+	hack/lint-go.sh
 
 lint-admin-portal:
 	docker build -f Dockerfile.portal_lint . -t linter
@@ -174,13 +181,21 @@ lint-admin-portal:
 test-python: pyenv az
 	. pyenv/bin/activate && \
 		azdev linter && \
-		azdev style 
+		azdev style && \
+		hack/unit-test-python.sh
+
+
+unit-test-python:
+	hack/unit-test-python.sh
 
 admin.kubeconfig:
 	hack/get-admin-kubeconfig.sh /subscriptions/${AZURE_SUBSCRIPTION_ID}/resourceGroups/${RESOURCEGROUP}/providers/Microsoft.RedHatOpenShift/openShiftClusters/${CLUSTER} >admin.kubeconfig
 
+aks.kubeconfig:
+	hack/get-admin-aks-kubeconfig.sh
+
 vendor:
 	# See comments in the script for background on why we need it
 	hack/update-go-module-dependencies.sh
 
-.PHONY: admin.kubeconfig aro az clean client deploy dev-config.yaml discoverycache generate image-aro image-aro-multistage image-fluentbit image-proxy lint-go runlocal-rp proxy publish-image-aro publish-image-aro-multistage publish-image-fluentbit publish-image-proxy secrets secrets-update e2e.test tunnel test-e2e test-go test-python vendor build-all validate-go  unit-test-go coverage-go validate-fips
+.PHONY: admin.kubeconfig aks.kubeconfig aro az clean client deploy dev-config.yaml discoverycache generate image-aro image-aro-multistage image-fluentbit image-proxy lint-go runlocal-rp proxy publish-image-aro publish-image-aro-multistage publish-image-fluentbit publish-image-proxy secrets secrets-update e2e.test tunnel test-e2e test-go test-python vendor build-all validate-go  unit-test-go coverage-go validate-fips
diff --git a/deploy/aks-development.json b/deploy/aks-development.json
new file mode 100644
index 000000000..f64a96a89
--- /dev/null
+++ b/deploy/aks-development.json
@@ -0,0 +1,645 @@
+{
+    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+    "contentVersion": "1.0.0.0",
+    "parameters": {
+        "adminObjectId": {
+            "type": "string",
+            "metadata": {
+                "description": "The AAD admin group's object ID."
+            }
+        },
+        "aksClusterName": {
+            "type": "string",
+            "defaultValue": "aro-aks-cluster",
+            "metadata": {
+                "description": "The name of the AKS Managed Cluster resource."
+            }
+        },
+        "keyvaultPrefix": {
+            "type": "string",
+            "defaultValue": "[concat(take(resourceGroup().name, 16), '-dev')]",
+            "maxLength": 20
+        },
+        "dnsPrefix": {
+            "type": "string",
+            "defaultValue": "[parameters('aksClusterName')]",
+            "metadata": {
+                "description": "Optional DNS prefix to use with hosted Kubernetes API server FQDN."
+            }
+        },
+        "availabilityZones": {
+            "defaultValue": [
+                "1",
+                "2",
+                "3"
+            ],
+            "type": "array",
+            "metadata": {
+                "description": "Specifies the availability zones for the system node pool."
+            }
+        },
+        "agentCount": {
+            "defaultValue": 3,
+            "type": "int",
+            "metadata": {
+                "description": "The number of agent nodes for the cluster."
+            }
+        },
+        "agentMinCount": {
+            "defaultValue": 3,
+            "type": "int",
+            "metadata": {
+                "description": "The min number of agent nodes."
+            }
+        },
+        "agentMaxCount": {
+            "defaultValue": 10,
+            "type": "int",
+            "metadata": {
+                "description": "The max number of agent nodes."
+            }
+        },
+        "agentVMSize": {
+            "defaultValue": "Standard_D2s_v3",
+            "type": "string",
+            "metadata": {
+                "description": "The size of the Virtual Machine."
+            }
+        },
+        "osDiskSizeGB": {
+            "defaultValue": 32,
+            "minValue": 0,
+            "maxValue": 1023,
+            "type": "int",
+            "metadata": {
+                "description": "Disk size (in GB) to provision for each of the agent pool nodes. This value ranges from 0 to 1023. Specifying 0 will apply the default disk size for that agentVMSize."
+            }
+        },
+        "maxPods": {
+            "defaultValue": 100,
+            "type": "int",
+            "metadata": {
+                "description": "Maximum number of pods that can run on a node."
+            }
+        },
+        "linuxAdminUsername": {
+            "type": "string",
+            "defaultValue": "cloud-user",
+            "metadata": {
+                "description": "User name for the Linux Virtual Machines."
+            }
+        },
+        "sshRSAPublicKey": {
+            "type": "string",
+            "metadata": {
+                "description": "Configure all linux machines with the SSH RSA public key string. Your key should include three parts, for example 'ssh-rsa AAAAB...snip...UcyupgH azureuser@linuxvm'"
+            }
+        },
+        "kubernetesVersion": {
+            "defaultValue": "1.23.5",
+            "type": "string",
+            "metadata": {
+                "description": "The version of Kubernetes."
+            }
+        },
+        "networkPlugin": {
+            "allowedValues": [
+                "azure",
+                "kubenet"
+            ],
+            "defaultValue": "azure",
+            "type": "string",
+            "metadata": {
+                "description": "Network plugin used for building Kubernetes network."
+            }
+        },
+        "enableRBAC": {
+            "defaultValue": true,
+            "type": "bool",
+            "metadata": {
+                "description": "boolean flag to turn on and off of RBAC"
+            }
+        },
+        "vnetName": {
+            "type": "string",
+            "defaultValue": "aks-net",
+            "metadata": {
+                "description": "Name of the VNET that will contain the AKS cluster and related resources."
+            }
+        },
+        "vnetAddressPrefix": {
+            "type": "string",
+            "defaultValue": "10.128.0.0/9",
+            "metadata": {
+                "description": "VNET address prefix"
+            }
+        },
+        "vnetSubnetName": {
+            "type": "string",
+            "defaultValue": "ClusterSubnet",
+            "metadata": {
+                "description": "Subnet name that will contain the App Service Environment"
+            }
+        },
+        "subnetPrefix": {
+            "type": "string",
+            "defaultValue": "10.128.64.0/18",
+            "metadata": {
+                "description": "Subnet address prefix"
+            }
+        },
+        "podSubnetName": {
+            "defaultValue": "PodSubnet",
+            "type": "string",
+            "metadata": {
+                "description": "Specifies the name of the subnet hosting the pods of the AKS cluster."
+            }
+        },
+        "podSubnetPrefix": {
+            "defaultValue": "10.129.0.0/16",
+            "type": "string",
+            "metadata": {
+                "description": "Specifies the address prefix of the subnet hosting the pods of the AKS cluster."
+            }
+        },
+        "gatewaySubnetName": {
+            "type": "string",
+            "defaultValue": "GatewaySubnet",
+            "metadata": {
+                "description": "Subnet name that will contain the App Service Environment"
+            }
+        },
+        "gatewaySubnetPrefix": {
+            "type": "string",
+            "defaultValue": "10.128.0.0/24",
+            "metadata": {
+                "description": "Subnet address prefix"
+            }
+        },
+        "serviceCidr": {
+            "type": "string",
+            "defaultValue": "10.130.0.0/16",
+            "metadata": {
+                "description": "A CIDR notation IP range from which to assign service cluster IPs."
+            }
+        },
+        "dnsServiceIP": {
+            "type": "string",
+            "defaultValue": "10.130.0.10",
+            "metadata": {
+                "description": "Containers DNS server IP address."
+            }
+        },
+        "dockerBridgeCidr": {
+            "type": "string",
+            "defaultValue": "172.17.0.1/16",
+            "metadata": {
+                "description": "A CIDR notation IP for Docker bridge."
+            }
+        },
+        "nodePoolEnableAutoScaling": {
+            "defaultValue": true,
+            "type": "bool",
+            "metadata": {
+                "description": "boolean flag to turn on and off nodepool autoscaling"
+            }
+        },
+        "autoScalerProfileScanInterval": {
+            "type": "string",
+            "defaultValue": "10s",
+            "metadata": {
+                "description": "Specifies the scan interval of the auto-scaler of the AKS cluster."
+            }
+        },
+        "autoScalerProfileScaleDownDelayAfterAdd": {
+            "type": "string",
+            "defaultValue": "10m",
+            "metadata": {
+                "description": "Specifies the scale down delay after add of the auto-scaler of the AKS cluster."
+            }
+        },
+        "autoScalerProfileScaleDownDelayAfterDelete": {
+            "type": "string",
+            "defaultValue": "20s",
+            "metadata": {
+                "description": "Specifies the scale down delay after delete of the auto-scaler of the AKS cluster."
+            }
+        },
+        "autoScalerProfileScaleDownDelayAfterFailure": {
+            "type": "string",
+            "defaultValue": "3m",
+            "metadata": {
+                "description": "Specifies scale down delay after failure of the auto-scaler of the AKS cluster."
+            }
+        },
+        "autoScalerProfileScaleDownUnneededTime": {
+            "type": "string",
+            "defaultValue": "10m",
+            "metadata": {
+                "description": "Specifies the scale down unneeded time of the auto-scaler of the AKS cluster."
+            }
+        },
+        "autoScalerProfileScaleDownUnreadyTime": {
+            "type": "string",
+            "defaultValue": "20m",
+            "metadata": {
+                "description": "Specifies the scale down unready time of the auto-scaler of the AKS cluster."
+            }
+        },
+        "autoScalerProfileUtilizationThreshold": {
+            "type": "string",
+            "defaultValue": "0.5",
+            "metadata": {
+                "description": "Specifies the utilization threshold of the auto-scaler of the AKS cluster."
+            }
+        },
+        "autoScalerProfileMaxGracefulTerminationSec": {
+            "type": "string",
+            "defaultValue": "600",
+            "metadata": {
+                "description": "Specifies the max graceful termination time interval in seconds for the auto-scaler of the AKS cluster."
+            }
+        },
+        "vpnCACertificate": {
+            "type": "string",
+            "defaultValue": ""
+        }
+    },
+    "variables": {
+        "aksNsgId": "[resourceId('Microsoft.Network/networkSecurityGroups', 'aks-nsg')]",
+        "aksPodNsgId": "[resourceId('Microsoft.Network/networkSecurityGroups', 'aks-pod-nsg')]",
+        "aksClusterId": "[resourceId('Microsoft.ContainerService/managedClusters', parameters('aksClusterName'))]",
+        "vnetId": "[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetName'))]",
+        "vnetSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('vnetSubnetName'))]",
+        "podSubnetId": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('podSubnetName'))]",
+        "networkContributorRoleId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions/', '4d97b98b-1d4f-4787-a291-c67834d212e7')]",
+        "aksClusterUserDefinedManagedIdentityName": "[concat(parameters('aksClusterName'), '-msi')]",
+        "aksClusterUserDefinedManagedIdentityId": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', variables('aksClusterUserDefinedManagedIdentityName'))]",
+        "aksNetworkContributorRoleAssignmentName": "[guid(variables('aksClusterUserDefinedManagedIdentityId'), variables('networkContributorRoleId'), variables('vnetSubnetId'))]"
+    },
+    "resources": [
+        {
+            "properties": {},
+            "name": "aks-nsg",
+            "type": "Microsoft.Network/networkSecurityGroups",
+            "location": "[resourceGroup().location]",
+            "apiVersion": "2021-04-01"
+        },
+        {
+            "properties": {},
+            "name": "aks-pod-nsg",
+            "type": "Microsoft.Network/networkSecurityGroups",
+            "location": "[resourceGroup().location]",
+            "apiVersion": "2021-04-01"
+        },
+        {
+            "type": "Microsoft.Network/virtualNetworks",
+            "apiVersion": "2022-01-01",
+            "location": "[resourceGroup().location]",
+            "name": "[parameters('vnetName')]",
+            "dependsOn": [
+                "[variables('aksNsgId')]",
+                "[variables('aksPodNsgId')]"
+            ],
+            "properties": {
+                "addressSpace": {
+                    "addressPrefixes": [
+                        "[parameters('vnetAddressPrefix')]"
+                    ]
+                },
+                "subnets": [
+                    {
+                        "name": "[parameters('vnetSubnetName')]",
+                        "properties": {
+                            "addressPrefix": "[parameters('subnetPrefix')]",
+                            "networkSecurityGroup": {
+                                "id": "[variables('aksNsgId')]"
+                            },
+                            "serviceEndpoints": [
+                                {
+                                    "service": "Microsoft.AzureCosmosDB"
+                                },
+                                {
+                                    "service": "Microsoft.ContainerRegistry"
+                                },
+                                {
+                                    "service": "Microsoft.Storage"
+                                },
+                                {
+                                    "service": "Microsoft.KeyVault"
+                                }
+                            ]
+                        }
+                    },
+                    {
+                        "name": "[parameters('podSubnetName')]",
+                        "properties": {
+                            "addressPrefix": "[parameters('podSubnetPrefix')]",
+                            "networkSecurityGroup": {
+                                "id": "[variables('aksPodNsgId')]"
+                            },
+                            "delegations": [
+                                {
+                                    "name": "AKS",
+                                    "properties": {
+                                        "serviceName": "Microsoft.ContainerService/managedClusters"
+                                    }
+                                }
+                            ]
+                        }
+                    },
+                    {
+                        "name": "[parameters('gatewaySubnetName')]",
+                        "properties": {
+                            "addressPrefix": "[parameters('gatewaySubnetPrefix')]"
+                        }
+                    }
+                ]
+            }
+        },
+        {
+            "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings",
+            "name": "[concat(parameters('vnetName'), '/peering-rp-pe-vnet-001')]",
+            "apiVersion": "2021-12-01",
+            "location": "[resourceGroup().location]",
+            "dependsOn": [
+                "[variables('vnetId')]"
+            ],
+            "properties": {
+                "allowVirtualNetworkAccess": true,
+                "allowForwardedTraffic": true,
+                "allowGatewayTransit": false,
+                "useRemoteGateways": false,
+                "remoteVirtualNetwork": {
+                    "id": "[resourceId('Microsoft.Network/virtualNetworks', 'rp-pe-vnet-001')]"
+                }
+            }
+        },
+        {
+            "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings",
+            "name": "[concat('rp-pe-vnet-001/peering-', parameters('vnetName'))]",
+            "apiVersion": "2021-12-01",
+            "location": "[resourceGroup().location]",
+            "dependsOn": [
+                "[variables('vnetId')]"
+            ],
+            "properties": {
+                "allowVirtualNetworkAccess": true,
+                "allowForwardedTraffic": true,
+                "allowGatewayTransit": false,
+                "useRemoteGateways": false,
+                "remoteVirtualNetwork": {
+                    "id": "[variables('vnetId')]"
+                }
+            }
+        },
+        {
+            "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings",
+            "name": "[concat(parameters('vnetName'), '/peering-rp-vnet')]",
+            "apiVersion": "2021-12-01",
+            "location": "[resourceGroup().location]",
+            "dependsOn": [
+                "[variables('vnetId')]"
+            ],
+            "properties": {
+                "allowVirtualNetworkAccess": true,
+                "allowForwardedTraffic": true,
+                "allowGatewayTransit": false,
+                "useRemoteGateways": false,
+                "remoteVirtualNetwork": {
+                    "id": "[resourceId('Microsoft.Network/virtualNetworks', 'rp-vnet')]"
+                }
+            }
+        },
+        {
+            "type": "Microsoft.Network/virtualNetworks/virtualNetworkPeerings",
+            "name": "[concat('rp-vnet/peering-', parameters('vnetName'))]",
+            "apiVersion": "2021-12-01",
+            "location": "[resourceGroup().location]",
+            "dependsOn": [
+                "[variables('vnetId')]"
+            ],
+            "properties": {
+                "allowVirtualNetworkAccess": true,
+                "allowForwardedTraffic": true,
+                "allowGatewayTransit": false,
+                "useRemoteGateways": false,
+                "remoteVirtualNetwork": {
+                    "id": "[variables('vnetId')]"
+                }
+            }
+        },
+        {
+            "name": "aks-vpn-pip",
+            "type": "Microsoft.Network/publicIPAddresses",
+            "location": "[resourceGroup().location]",
+            "apiVersion": "2020-08-01",
+            "sku": {
+                "name": "Basic"
+            },
+            "properties": {
+                "publicIPAllocationMethod": "Dynamic"
+            }
+        },
+        {
+            "type": "Microsoft.ManagedIdentity/userAssignedIdentities",
+            "apiVersion": "2018-11-30",
+            "name": "[variables('aksClusterUserDefinedManagedIdentityName')]",
+            "location": "[resourceGroup().location]"
+        },
+        {
+            "type": "Microsoft.Authorization/roleAssignments",
+            "apiVersion": "2020-10-01-preview",
+            "name": "[variables('aksNetworkContributorRoleAssignmentName')]",
+            "scope": "[variables('vnetSubnetId')]",
+            "dependsOn": [
+                "[variables('aksClusterUserDefinedManagedIdentityId')]",
+                "[variables('vnetId')]"
+            ],
+            "properties": {
+                "roleDefinitionId": "[variables('networkContributorRoleId')]",
+                "principalId": "[reference(variables('aksClusterUserDefinedManagedIdentityName')).principalId]",
+                "principalType": "ServicePrincipal"
+            }
+        },
+        {
+            "apiVersion": "2022-03-01",
+            "type": "Microsoft.ContainerService/managedClusters",
+            "location": "[resourceGroup().location]",
+            "name": "[parameters('aksClusterName')]",
+            "dependsOn": [
+                "[variables('vnetId')]",
+                "[variables('aksClusterUserDefinedManagedIdentityId')]"
+            ],
+            "identity": {
+                "type": "UserAssigned",
+                "userAssignedIdentities": {
+                    "[variables('aksClusterUserDefinedManagedIdentityId')]": {}
+                }
+            },
+            "properties": {
+                "nodeResourceGroup": "[concat(resourceGroup().name, '-aks')]",
+                "apiServerAccessProfile": {
+                    "enablePrivateCluster": true
+                },
+                "addonProfiles": {
+                    "azureKeyvaultSecretsProvider": {
+                        "enabled": true
+                    }
+                },
+                "kubernetesVersion": "[parameters('kubernetesVersion')]",
+                "enableRBAC": "[parameters('enableRBAC')]",
+                "dnsPrefix": "[parameters('dnsPrefix')]",
+                "linuxProfile": {
+                    "adminUsername": "[parameters('linuxAdminUsername')]",
+                    "ssh": {
+                        "publicKeys": [
+                            {
+                                "keyData": "[parameters('sshRSAPublicKey')]"
+                            }
+                        ]
+                    }
+                },
+                "agentPoolProfiles": [
+                    {
+                        "name": "systempool",
+                        "osType": "Linux",
+                        "mode": "System",
+                        "orchestratorVersion": "[parameters('kubernetesVersion')]",
+                        "enableAutoScaling": "[parameters('nodePoolEnableAutoScaling')]",
+                        "enableEncryptionAtHost": true,
+                        "enableFIPS": true,
+                        "OSDiskType": "Ephemeral",
+                        "osDiskSizeGB": "[parameters('osDiskSizeGB')]",
+                        "count": "[parameters('agentCount')]",
+                        "minCount": "[parameters('agentMinCount')]",
+                        "maxCount": "[parameters('agentMaxCount')]",
+                        "vmSize": "[parameters('agentVMSize')]",
+                        "vnetSubnetID": "[variables('vnetSubnetID')]",
+                        "podSubnetID": "[variables('podSubnetId')]",
+                        "maxPods": "[parameters('maxPods')]",
+                        "availabilityZones": "[parameters('availabilityZones')]"
+                    }
+                ],
+                "networkProfile": {
+                    "networkPlugin": "[parameters('networkPlugin')]",
+                    "serviceCidr": "[parameters('serviceCidr')]",
+                    "dnsServiceIP": "[parameters('dnsServiceIP')]",
+                    "dockerBridgeCidr": "[parameters('dockerBridgeCidr')]"
+                },
+                "aadProfile": {
+                    "managed": true,
+                    "enableAzureRBAC": true
+                },
+                "autoScalerProfile": {
+                    "scan-interval": "[parameters('autoScalerProfileScanInterval')]",
+                    "scale-down-delay-after-add": "[parameters('autoScalerProfileScaleDownDelayAfterAdd')]",
+                    "scale-down-delay-after-delete": "[parameters('autoScalerProfileScaleDownDelayAfterDelete')]",
+                    "scale-down-delay-after-failure": "[parameters('autoScalerProfileScaleDownDelayAfterFailure')]",
+                    "scale-down-unneeded-time": "[parameters('autoScalerProfileScaleDownUnneededTime')]",
+                    "scale-down-unready-time": "[parameters('autoScalerProfileScaleDownUnreadyTime')]",
+                    "scale-down-utilization-threshold": "[parameters('autoScalerProfileUtilizationThreshold')]",
+                    "max-graceful-termination-sec": "[parameters('autoScalerProfileMaxGracefulTerminationSec')]"
+                }
+            }
+        },
+        {
+            "apiVersion": "2019-09-01",
+            "type": "Microsoft.KeyVault/vaults",
+            "name": "[concat(parameters('keyvaultPrefix'), '-aks')]",
+            "location": "[resourceGroup().location]",
+            "dependsOn": [
+                "[variables('aksClusterId')]"
+            ],
+            "properties": {
+                "tenantId": "[subscription().tenantId]",
+                "sku": {
+                    "family": "A",
+                    "name": "standard"
+                },
+                "accessPolicies": [
+                    {
+                        "tenantId": "[subscription().tenantId]",
+                        "objectId": "[reference(variables('aksClusterId'), '2020-12-01', 'Full').properties.addonProfiles.azureKeyvaultSecretsProvider.identity.objectId]",
+                        "permissions": {
+                            "secrets": [
+                                "get",
+                                "list"
+                            ],
+                            "certificates": [
+                                "get"
+                            ]
+                        }
+                    },
+                    {
+                        "tenantId": "[subscription().tenantId]",
+                        "objectId": "[parameters('adminObjectId')]",
+                        "permissions": {
+                            "secrets": [
+                                "get",
+                                "set",
+                                "list"
+                            ],
+                            "certificates": [
+                                "delete",
+                                "get",
+                                "import",
+                                "list"
+                            ]
+                        }
+                    }
+                ],
+                "enableSoftDelete": true
+            }
+        },
+        {
+            "type": "Microsoft.Network/virtualNetworkGateways",
+            "name": "aks-vpn",
+            "apiVersion": "2020-08-01",
+            "location": "[resourceGroup().location]",
+            "dependsOn": [
+                "[resourceId('Microsoft.Network/publicIPAddresses', 'aks-vpn-pip')]",
+                "[variables('vnetId')]",
+                "[parameters('aksClusterName')]"
+            ],
+            "properties": {
+                "ipConfigurations": [
+                    {
+                        "properties": {
+                            "subnet": {
+                                "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('vnetName'), parameters('gatewaySubnetName'))]"
+                            },
+                            "publicIPAddress": {
+                                "id": "[resourceId('Microsoft.Network/publicIPAddresses', 'aks-vpn-pip')]"
+                            }
+                        },
+                        "name": "default"
+                    }
+                ],
+                "vpnType": "RouteBased",
+                "sku": {
+                    "name": "VpnGw1",
+                    "tier": "VpnGw1"
+                },
+                "vpnClientConfiguration": {
+                    "vpnClientAddressPool": {
+                        "addressPrefixes": [
+                            "192.168.254.0/24"
+                        ]
+                    },
+                    "vpnClientRootCertificates": [
+                        {
+                            "properties": {
+                                "publicCertData": "[parameters('vpnCACertificate')]"
+                            },
+                            "name": "aks-vpn-ca"
+                        }
+                    ],
+                    "vpnClientProtocols": [
+                        "OpenVPN"
+                    ]
+                }
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/docs/deploy-development-rp.md b/docs/deploy-development-rp.md
index 2251b02da..b7a8c356f 100644
--- a/docs/deploy-development-rp.md
+++ b/docs/deploy-development-rp.md
@@ -99,13 +99,13 @@
    OR use the create utility:
 
    ```bash
-   CLUSTER=cluster go run ./hack/cluster create
+   CLUSTER=<cluster-name> go run ./hack/cluster create
    ```
 
    Later the cluster can be deleted as follows:
 
    ```bash
-   CLUSTER=cluster go run ./hack/cluster delete
+   CLUSTER=<cluster-name> go run ./hack/cluster delete
    ```
 
    [1]: https://docs.microsoft.com/en-us/azure/openshift/tutorial-create-cluster
@@ -184,7 +184,7 @@
   curl -X GET -k "https://localhost:8443/admin/subscriptions/$AZURE_SUBSCRIPTION_ID/resourceGroups/$RESOURCEGROUP/providers/Microsoft.RedHatOpenShift/openShiftClusters/$CLUSTER/kubernetespodlogs?podname=$POD&namespace=$NAMESPACE&container=$CONTAINER"
   ```
 
-## Debugging
+## Debugging OpenShift Cluster
 
 * SSH to the bootstrap node:
 > __NOTE:__ If you have a password-based `sudo` command, you must first authenticate before running `sudo` in the background
@@ -224,6 +224,42 @@
    CLUSTER=cluster hack/ssh-agent.sh bootstrap # the bootstrap node used to provision cluster
    ```
 
+# Debugging AKS Cluster
+
+* Connect to the VPN:
+
+To access the cluster for oc / kubectl or SSH'ing into the cluster you need to connect to the VPN first.
+> __NOTE:__ If you have a password-based `sudo` command, you must first authenticate before running `sudo` in the background
+  ```bash
+  sudo openvpn secrets/vpn-aks-$LOCATION.ovpn &
+  ```
+
+* Access the cluster via API (oc / kubectl):
+
+  ```bash
+  make aks.kubeconfig
+  export KUBECONFIG=aks.kubeconfig
+
+  $ oc get nodes
+  NAME                                 STATUS   ROLES   AGE   VERSION
+  aks-systempool-99744725-vmss000000   Ready    agent   9h    v1.23.5
+  aks-systempool-99744725-vmss000001   Ready    agent   9h    v1.23.5
+  aks-systempool-99744725-vmss000002   Ready    agent   9h    v1.23.5
+  ```
+
+* "SSH" into a cluster node:
+
+  * Run the ssh-aks.sh script, specifying the cluster name and the node number of the VM you are trying to ssh to.
+  ```
+  hack/ssk-aks.sh aro-aks-cluster 0 # The first VM node in 'aro-aks-cluster'
+  hack/ssk-aks.sh aro-aks-cluster 1 # The second VM node in 'aro-aks-cluster'
+  hack/ssk-aks.sh aro-aks-cluster 2 # The third VM node in 'aro-aks-cluster'
+  ```
+
+* Access via Azure Portal
+
+Due to the fact that the AKS cluster is private, you need to be connected to the VPN in order to view certain AKS cluster properties, because the UI interrogates k8s via the VPN. 
+
 ### Metrics
 
 To run fake metrics socket:
diff --git a/go.mod b/go.mod
index 6b7df2129..384990006 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/containers/image/v5 v5.21.0
 	github.com/coreos/go-oidc v2.2.1+incompatible
 	github.com/coreos/go-systemd/v22 v22.3.2
-	github.com/coreos/ignition/v2 v2.13.0
+	github.com/coreos/ignition/v2 v2.14.0
 	github.com/coreos/stream-metadata-go v0.2.0
 	github.com/davecgh/go-spew v1.1.1
 	github.com/form3tech-oss/jwt-go v3.2.5+incompatible
diff --git a/go.sum b/go.sum
index 03ddbb7ea..5133f6553 100644
--- a/go.sum
+++ b/go.sum
@@ -488,6 +488,7 @@ github.com/baidubce/bce-sdk-go v0.9.81/go.mod h1:zbYJMQwE4IZuyrJiFO8tO8NbtYiKTFT
 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA=
 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=
 github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
+github.com/beevik/etree v1.1.1-0.20200718192613-4a2f8b9d084c/go.mod h1:0yGO2rna3S9DkITDWHY1bMtcY4IJ4w+4S+EooZUR0bE=
 github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg=
 github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -703,8 +704,8 @@ github.com/coreos/ignition/v2 v2.3.0/go.mod h1:85dmM/CERMZXNrJsXqtNLIxR/dn8G9qlL
 github.com/coreos/ignition/v2 v2.7.0/go.mod h1:3CjaRpg51hmJzPjarbzB0RvSZbLkNOczxKJobTl6nOY=
 github.com/coreos/ignition/v2 v2.9.0/go.mod h1:A5lFFzA2/zvZQPVEvI1lR5WPLWRb7KZ7Q1QOeUMtcAc=
 github.com/coreos/ignition/v2 v2.11.0/go.mod h1:uFhfdmeUfzT/8MqBvazzrLdzR3DvMCWR78GUYFRwPrs=
-github.com/coreos/ignition/v2 v2.13.0 h1:1ouW+d0nOuPUbLjxxOCnC+dWQxynr8Mt5exqJoCD7b4=
-github.com/coreos/ignition/v2 v2.13.0/go.mod h1:HO1HWYWcvAIbHu6xewoKxPGBTyZ32FLwGIuipw5d63o=
+github.com/coreos/ignition/v2 v2.14.0 h1:KfkCCnA6AK0kts/1zxzzNH5lDMCQN9sqqGcGs+RJVX4=
+github.com/coreos/ignition/v2 v2.14.0/go.mod h1:wxc4qdYEIHLygzWbVVEuoD7lQGTZmMgX0VjAPYBbeEQ=
 github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
 github.com/coreos/stream-metadata-go v0.1.3 h1:i/mUQBANlYirNAU4ybH6x/SCuc/Rh6uiopph6XZhROk=
@@ -2784,6 +2785,7 @@ github.com/vmware/govmomi v0.24.0/go.mod h1:Y+Wq4lst78L85Ge/F8+ORXIWiKYqaro1vhAu
 github.com/vmware/govmomi v0.27.4 h1:5kY8TAkhB20lsjzrjE073eRb8+HixBI29PVMG5lxq6I=
 github.com/vmware/govmomi v0.27.4/go.mod h1:daTuJEcQosNMXYJOeku0qdBJP9SOLLWB3Mqz8THtv6o=
 github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk=
+github.com/vmware/vmw-guestinfo v0.0.0-20220317130741-510905f0efa3/go.mod h1:CSBTxrhePCm0cmXNKDGeu+6bOQzpaEklfCqEpn89JWk=
 github.com/vmware/vmw-ovflib v0.0.0-20170608004843-1f217b9dc714/go.mod h1:jiPk45kn7klhByRvUq5i2vo1RtHKBHj+iWGFpxbXuuI=
 github.com/waigani/diffparser v0.0.0-20190828052634-7391f219313d/go.mod h1:BzSc3WEF8R+lCaP5iGFRxd5kIXy4JKOZAwNe1w0cdc0=
 github.com/weaveworks/common v0.0.0-20210419092856-009d1eebd624/go.mod h1:ykzWac1LtVfOxdCK+jD754at1Ws9dKCwFeUzkFBffPs=
diff --git a/hack/devtools/deploy-shared-env.sh b/hack/devtools/deploy-shared-env.sh
index 16b01252b..3e7ad463f 100644
--- a/hack/devtools/deploy-shared-env.sh
+++ b/hack/devtools/deploy-shared-env.sh
@@ -68,6 +68,18 @@ deploy_env_dev() {
             "vpnCACertificate=$(base64 -w0 <secrets/vpn-ca.crt)" >/dev/null
 }
 
+deploy_aks_dev() {
+    echo "########## Deploying aks-development in RG $RESOURCEGROUP ##########"
+    az deployment group create \
+        -g "$RESOURCEGROUP" \
+        -n aks-development \
+        --template-file deploy/aks-development.json \
+        --parameters \
+            "adminObjectId=$ADMIN_OBJECT_ID" \
+            "sshRSAPublicKey=$(<secrets/proxy_id_rsa.pub)" \
+            "vpnCACertificate=$(base64 -w0 <secrets/vpn-ca.crt)" >/dev/null
+}
+
 deploy_env_dev_override() {
     echo "########## Deploying env-development in RG $RESOURCEGROUP ##########"
     az deployment group create \
diff --git a/hack/get-admin-aks-kubeconfig.sh b/hack/get-admin-aks-kubeconfig.sh
new file mode 100755
index 000000000..6a3a136d5
--- /dev/null
+++ b/hack/get-admin-aks-kubeconfig.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+if [ -z "$RESOURCEGROUP" ]; then
+    echo "the environment variable RESOURCEGROUP is required" >&2
+    exit 1
+fi
+
+[ -f aks.kubeconfig ] && rm -f aks.kubeconfig
+
+if az aks get-credentials --admin -g "$RESOURCEGROUP" -n aro-aks-cluster --public-fqdn -f aks.kubeconfig 2>/dev/null; then
+    chmod 600 aks.kubeconfig
+else
+    echo "Error generating AKS kubeconfig"
+    exit 1
+fi
diff --git a/hack/licenses/licenses.go b/hack/licenses/licenses.go
deleted file mode 100644
index 72152ad92..000000000
--- a/hack/licenses/licenses.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package main
-
-// Copyright (c) Microsoft Corporation.
-// Licensed under the Apache License 2.0.
-
-import (
-	"bytes"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-)
-
-var (
-	goLicense = []byte(`// Copyright (c) Microsoft Corporation.
-// Licensed under the Apache License 2.0.`)
-
-	pythonLicense = []byte(`# Copyright (c) Microsoft Corporation.
-# Licensed under the Apache License 2.0.`)
-)
-
-func applyGoLicense() error {
-	return filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-
-		switch path {
-		case "pkg/client", "vendor", ".git":
-			return filepath.SkipDir
-		}
-
-		if !strings.HasSuffix(path, ".go") {
-			return nil
-		}
-
-		b, err := ioutil.ReadFile(path)
-		if err != nil {
-			return err
-		}
-
-		if bytes.Contains(b, []byte("DO NOT EDIT.")) {
-			return nil
-		}
-
-		if !bytes.Contains(b, goLicense) {
-			i := bytes.Index(b, []byte("package "))
-			i += bytes.Index(b[i:], []byte("\n"))
-
-			var bb []byte
-			bb = append(bb, b[:i]...)
-			bb = append(bb, []byte("\n\n")...)
-			bb = append(bb, goLicense...)
-			bb = append(bb, b[i:]...)
-
-			err = ioutil.WriteFile(path, bb, 0666)
-			if err != nil {
-				return err
-			}
-		}
-
-		return nil
-	})
-}
-
-func applyPythonLicense() error {
-	return filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-
-		if strings.HasPrefix(path, "pyenv") {
-			return filepath.SkipDir
-		}
-
-		switch path {
-		case "python/client", "vendor":
-			return filepath.SkipDir
-		}
-
-		if !strings.HasSuffix(path, ".py") {
-			return nil
-		}
-
-		b, err := ioutil.ReadFile(path)
-		if err != nil {
-			return err
-		}
-
-		if !bytes.Contains(b, pythonLicense) {
-			var bb []byte
-
-			if bytes.HasPrefix(b, []byte("#!")) {
-				i := bytes.Index(b, []byte("\n"))
-
-				bb = append(bb, b[:i]...)
-				bb = append(bb, []byte("\n\n")...)
-				bb = append(bb, pythonLicense...)
-				bb = append(bb, b[i:]...)
-			} else {
-				bb = append(bb, pythonLicense...)
-				bb = append(bb, []byte("\n\n")...)
-				bb = append(bb, b...)
-			}
-
-			err = ioutil.WriteFile(path, bb, 0666)
-			if err != nil {
-				return err
-			}
-		}
-
-		return nil
-	})
-}
-
-func run() error {
-	err := applyGoLicense()
-	if err != nil {
-		return err
-	}
-
-	return applyPythonLicense()
-}
-
-func main() {
-	if err := run(); err != nil {
-		panic(err)
-	}
-}
diff --git a/hack/licenses/main.go b/hack/licenses/main.go
new file mode 100644
index 000000000..e4e9243c6
--- /dev/null
+++ b/hack/licenses/main.go
@@ -0,0 +1,252 @@
+package main
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"bytes"
+	"errors"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+var (
+	goLicense = []byte(`// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.`)
+
+	pythonLicense = []byte(`# Copyright (c) Microsoft Corporation.
+# Licensed under the Apache License 2.0.`)
+)
+
+func main() {
+	ignoredGo := flag.String("ignored-go", "", "comma separated list of ignored directories")
+	ignoredPython := flag.String("ignored-python", "", "comma separated list of ignored directories")
+	dirs := flag.String("dirs", ".", "comma separated list of dirs to parse")
+	validateOnly := flag.Bool("validate", false, "only validates and does not modify (for CI)")
+	flag.Parse()
+
+	if *validateOnly {
+		err := validateLicenses(*ignoredGo, *ignoredPython, *dirs)
+		if err != nil {
+			os.Exit(1)
+		}
+		os.Exit(0)
+	} else {
+		if err := run(); err != nil {
+			log.Fatalf("error while checking for licences. err: %s ", err)
+		}
+	}
+}
+
+func run() error {
+	err := applyGoLicense()
+	if err != nil {
+		return err
+	}
+
+	return applyPythonLicense()
+}
+
+func applyGoLicense() error {
+	return filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		switch path {
+		case "pkg/client", "vendor", ".git":
+			return filepath.SkipDir
+		}
+
+		if !strings.HasSuffix(path, ".go") {
+			return nil
+		}
+
+		b, err := ioutil.ReadFile(path)
+		if err != nil {
+			return err
+		}
+
+		if bytes.Contains(b, []byte("DO NOT EDIT.")) {
+			return nil
+		}
+
+		if !bytes.Contains(b, goLicense) {
+			i := bytes.Index(b, []byte("package "))
+			i += bytes.Index(b[i:], []byte("\n"))
+
+			var bb []byte
+			bb = append(bb, b[:i]...)
+			bb = append(bb, []byte("\n\n")...)
+			bb = append(bb, goLicense...)
+			bb = append(bb, b[i:]...)
+
+			err = ioutil.WriteFile(path, bb, 0666)
+			if err != nil {
+				return err
+			}
+		}
+
+		return nil
+	})
+}
+
+func applyPythonLicense() error {
+	return filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		if strings.HasPrefix(path, "pyenv") {
+			return filepath.SkipDir
+		}
+
+		switch path {
+		case "python/client", "vendor":
+			return filepath.SkipDir
+		}
+
+		if !strings.HasSuffix(path, ".py") {
+			return nil
+		}
+
+		b, err := ioutil.ReadFile(path)
+		if err != nil {
+			return err
+		}
+
+		if !bytes.Contains(b, pythonLicense) {
+			var bb []byte
+
+			if bytes.HasPrefix(b, []byte("#!")) {
+				i := bytes.Index(b, []byte("\n"))
+
+				bb = append(bb, b[:i]...)
+				bb = append(bb, []byte("\n\n")...)
+				bb = append(bb, pythonLicense...)
+				bb = append(bb, b[i:]...)
+			} else {
+				bb = append(bb, pythonLicense...)
+				bb = append(bb, []byte("\n\n")...)
+				bb = append(bb, b...)
+			}
+
+			err = ioutil.WriteFile(path, bb, 0666)
+			if err != nil {
+				return err
+			}
+		}
+
+		return nil
+	})
+}
+
+//returns the lists of files that don't have a license but should
+func validateGoLicenses(ignored map[string]bool, dirs []string) []string {
+	unlicensedFiles := make([]string, 0)
+	for _, dir := range dirs {
+		filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+
+			if ignored[path] {
+				return filepath.SkipDir
+			}
+
+			if !strings.HasSuffix(path, ".go") {
+				return nil
+			}
+
+			b, err := ioutil.ReadFile(path)
+			if err != nil {
+				return err
+			}
+
+			if bytes.Contains(b, []byte("DO NOT EDIT.")) {
+				return nil
+			}
+
+			if !bytes.Contains(b, goLicense) {
+				unlicensedFiles = append(unlicensedFiles, path)
+			}
+			return nil
+		})
+	}
+
+	return unlicensedFiles
+}
+
+//returns the lists of files that don't have a license but should
+func validatePythonLicenses(ignored map[string]bool, dirs []string) []string {
+	unlicensedFiles := make([]string, 0)
+
+	for _, dir := range dirs {
+		filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+
+			if strings.HasPrefix(path, "pyenv") {
+				return filepath.SkipDir
+			}
+
+			if ignored[path] {
+				return filepath.SkipDir
+			}
+
+			if !strings.HasSuffix(path, ".py") {
+				return nil
+			}
+
+			b, err := ioutil.ReadFile(path)
+			if err != nil {
+				return err
+			}
+
+			if !bytes.Contains(b, pythonLicense) {
+				unlicensedFiles = append(unlicensedFiles, path)
+			}
+			return nil
+		})
+	}
+
+	return unlicensedFiles
+}
+
+func parseIgnored(commaSeparated string) map[string]bool {
+	ignored := strings.Split(commaSeparated, ",")
+
+	result := make(map[string]bool)
+	for _, v := range ignored {
+		result[v] = true
+	}
+	return result
+}
+
+func validateLicenses(ignoredGo, ignoredPython, dirs string) error {
+	ignoredMapGo := parseIgnored(ignoredGo)
+	ignoredMapPython := parseIgnored(ignoredPython)
+
+	unlicensedGo := validateGoLicenses(ignoredMapGo, strings.Split(dirs, ","))
+
+	unlicensedPython := validatePythonLicenses(ignoredMapPython, strings.Split(dirs, ","))
+
+	for _, v := range unlicensedGo {
+		fmt.Printf("%s does not have a license\n", v)
+	}
+
+	for _, v := range unlicensedPython {
+		fmt.Printf("%s does not have a license\n", v)
+	}
+
+	if len(unlicensedGo) > 0 || len(unlicensedPython) > 0 {
+		return errors.New("validation failed")
+	}
+	return nil
+}
diff --git a/hack/lint-go.sh b/hack/lint-go.sh
new file mode 100755
index 000000000..7a7987900
--- /dev/null
+++ b/hack/lint-go.sh
@@ -0,0 +1,18 @@
+#!/bin/bash -e
+
+# lint-go.sh is used to run Go linting. This shell script is invoked by the Makefile.
+# It is not used in our pipelines as we have a github action that does the Go linting, so this script
+# is intended to be used locally when developing.
+
+# What does this script?
+# It will try to execute the golangci-lint binary to check if there are any linter errors.
+# In case the binary that is trying to execute does not exist,
+# the script will print a message with instructions to download the binary and will exit with a non-zero exit code.
+
+if ! command -v golangci-lint &> /dev/null
+then
+    echo "ERROR: golangci-lint could not be found, Go linting aborted. To install it visit https://golangci-lint.run/usage/install/#local-installation"
+    exit 1
+else
+    golangci-lint run
+fi
diff --git a/hack/ssh-aks.sh b/hack/ssh-aks.sh
new file mode 100755
index 000000000..34facc2e2
--- /dev/null
+++ b/hack/ssh-aks.sh
@@ -0,0 +1,54 @@
+#!/bin/bash -e
+
+# ssh-aks.sh allows you to ssh into the specifed AKS clusters worker nodes
+
+usage() {
+	echo -e "usage: ${0} <cluster-name> <worker-node-index>\n" >&2
+	echo "       Examples: ${0} aks-test 0        # the first VM node in 'aks-test'" >&2
+	echo "                 ${0} aro-aks-cluster 1 # The second VM node in 'aro-aks-cluster'" >&2
+	exit 1
+}
+
+if [ "$#" -ne 2 ]; then
+	usage
+fi
+
+if [ -z "$RESOURCEGROUP" ]; then
+	echo "RESOURCEGROUP env variable must be set"
+	exit 1
+fi
+
+NODE_RESOURCEGROUP=$( az aks list -g "$RESOURCEGROUP" --query "[?contains(name, '$1')].nodeResourceGroup" -o tsv )
+if [[ $(grep -c . <<<"$NODE_RESOURCEGROUP") -ne 1 ]]; then
+	echo "Cluster with pattern "${1}" not found in resource group ${RESOURCEGROUP}"
+	echo "The following AKS clusters are available:"
+	az aks list -g $RESOURCEGROUP -otable
+	exit 1
+fi
+
+echo "Node RG:   ${NODE_RESOURCEGROUP}"
+
+NODE_VMSS=$( az vmss list -g "$NODE_RESOURCEGROUP" --query "[?contains(name, 'system')].name" -otsv )
+if [[ $(grep -c . <<<"$NODE_VMSS") -ne 1 ]]; then
+	echo "VMSS not found in the node resource group ${NODE_RESOURCEGROUP}"
+	usage
+fi
+
+echo "Node VMSS: ${NODE_VMSS}"
+
+IPs=$( az vmss nic list -g "$NODE_RESOURCEGROUP" --vmss-name "$NODE_VMSS" --query '[?items[].ipConfigurations[].primary == true].ipConfigurations[0].privateIpAddress' )
+if [ -z "$IPs" ]; then
+	echo "Primary IPs not found for VMs in ${NODE_VMSS}"
+	usage
+fi
+
+IP_COUNT=$( echo ${IPs} | jq -r ". | length" )
+echo "Found ${IP_COUNT} worker node IPs"
+
+IP=$( echo "$IPs" | jq -r ".[${2}]" )
+if [ -z "$IP" ]; then
+	echo "IP not found for worker index ${2}"
+	usage
+fi
+
+ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i secrets/proxy_id_rsa -l cloud-user "$IP"
diff --git a/hack/validate-imports/allowed-import-names.yaml b/hack/validate-imports/allowed-import-names.yaml
index 7f3ffa503..12e6b51b0 100644
--- a/hack/validate-imports/allowed-import-names.yaml
+++ b/hack/validate-imports/allowed-import-names.yaml
@@ -1,4 +1,7 @@
 allowedImportNames:
+  github.com/Azure/ARO-RP/pkg/api/validate:
+    - ""
+    - apiValidate
   github.com/Azure/ARO-RP/pkg/frontend/middleware:
     - ""
     - frontendmiddleware
diff --git a/pkg/api/validate/openshiftcluster.go b/pkg/api/validate/openshiftcluster.go
new file mode 100644
index 000000000..efc934026
--- /dev/null
+++ b/pkg/api/validate/openshiftcluster.go
@@ -0,0 +1,53 @@
+package validate
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"strings"
+)
+
+// TODO: remove the VM name validation after https://bugzilla.redhat.com/show_bug.cgi?id=2093044 is resolved
+
+// Max length of a ARO cluster name is determined by the length of the generated availability set
+// in the machine-api provider.  It varies per region, but to remain consistent across non-zonal regions
+// we hardcode this to 19, which is the max for all non-zonal regions.
+//
+// the generated availability set must be <= 80 characters and can be calculated below
+// <cluster-name>-XXXXX_<cluster-name>-XXXXX-worker-<region>-as
+//   XXXXX:          represents the 5 digit cluster infraID
+//   <cluster-name>: the name of the cluster
+//   <region>:       the short-name of the region
+const MaxClusterNameLength = 19
+
+// nonZonalRegions are defined by the Compute List SKUs API not returning zones within the VM objects
+//
+// This can be queried for a location by running `az vm list-skus -l <region> --all --zone`
+// If the object is empty that means the location does not support VMs deployed into
+// availability zones
+var nonZonalRegions = map[string]bool{
+	"australiacentral":   true,
+	"australiacentral2":  true,
+	"australiasoutheast": true,
+	"brazilsoutheast":    true,
+	"canadaeast":         true,
+	"japanwest":          true,
+	"northcentralus":     true,
+	"norwaywest":         true,
+	"southindia":         true,
+	"switzerlandwest":    true,
+	"uaenorth":           true,
+	"ukwest":             true,
+	"westcentralus":      true,
+	"westus":             true,
+}
+
+// OpenShiftClusterNameLength validates that the name does not exceed the maximumLength
+// which is in place for non-zonal regions due to https://bugzilla.redhat.com/show_bug.cgi?id=2093044
+func OpenShiftClusterNameLength(name, location string) bool {
+	if nonZonalRegions[strings.ToLower(location)] && len(name) > MaxClusterNameLength {
+		return false
+	}
+
+	return true
+}
diff --git a/pkg/cluster/deployresources.go b/pkg/cluster/deployresources.go
index 505dc4ecb..da9a5e948 100644
--- a/pkg/cluster/deployresources.go
+++ b/pkg/cluster/deployresources.go
@@ -68,16 +68,25 @@ func (m *manager) deployResourceTemplate(ctx context.Context) error {
 
 // zones configures how master nodes are distributed across availability zones. In regions where the number of zones matches
 // the number of nodes, it's one node per zone. In regions where there are no zones, all the nodes are in the same place.
-// Anything else (e.g. 2-zone regions) is currently unsupported.
+// Valid zone values are nil, 1, 2, and 3. Greater than 3 zones is not supported.
 func zones(installConfig *installconfig.InstallConfig) (zones *[]string, err error) {
 	zoneCount := len(installConfig.Config.ControlPlane.Platform.Azure.Zones)
 	replicas := int(*installConfig.Config.ControlPlane.Replicas)
-	if reflect.DeepEqual(installConfig.Config.ControlPlane.Platform.Azure.Zones, []string{""}) {
-		// []string{""} indicates that there are no Azure Zones, so "zones" return value will be nil
-	} else if zoneCount == replicas {
-		zones = &[]string{"[copyIndex(1)]"}
+	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 {
-		err = fmt.Errorf("cluster creation with %d zone(s) and %d replica(s) is unimplemented", zoneCount, replicas)
+		zones = &[]string{"[copyIndex(1)]"}
 	}
+
 	return
 }
diff --git a/pkg/cluster/gatewayprivateendpoint.go b/pkg/cluster/gatewayprivateendpoint.go
index bd9c859ef..bd7d96786 100644
--- a/pkg/cluster/gatewayprivateendpoint.go
+++ b/pkg/cluster/gatewayprivateendpoint.go
@@ -33,7 +33,7 @@ func (m *manager) ensureGatewayUpgrade(ctx context.Context) error {
 		ContentVersion: "1.0.0.0",
 		Resources:      []*arm.Resource{m.networkPrivateEndpoint()},
 	}
-	err = arm.DeployTemplate(ctx, m.log, m.deployments, resourceGroup, "storage", t, nil)
+	err = arm.DeployTemplate(ctx, m.log, m.deployments, resourceGroup, "gatewayprivateendpoint", t, nil)
 	if err != nil {
 		m.log.Print(err)
 		return nil
diff --git a/pkg/deploy/bindata.go b/pkg/deploy/bindata.go
index 50c7c9377..ed058f525 100644
--- a/pkg/deploy/bindata.go
+++ b/pkg/deploy/bindata.go
@@ -1,5 +1,6 @@
 // Code generated for package deploy by go-bindata DO NOT EDIT. (@generated)
 // sources:
+// aks-development.json
 // cluster-development-predeploy.json
 // databases-development.json
 // env-development.json
@@ -96,6 +97,26 @@ func (fi bindataFileInfo) Sys() interface{} {
 	return nil
 }
 
+var _aksDevelopmentJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x3c\x6b\x6f\xdb\xba\x92\xdf\xef\xaf\x20\xb2\x0b\xb8\x05\x22\xc7\x76\x7a\xdb\xa6\x9f\xd6\x8d\xdb\x9e\xa0\xb7\xad\x37\x4e\x03\xec\x16\x07\x17\x8c\x38\xb6\x79\x23\x91\x02\x49\x39\x71\x0f\xfa\xdf\x17\xa4\x28\x59\x0f\xea\xe1\x57\xda\xb3\xf7\xa4\x5f\x6a\x89\xe2\x3c\x39\x33\x9c\xe1\xf0\x8f\xbf\x21\x84\xd0\xc9\x7f\x4a\x7f\x09\x21\x3e\x79\x83\x4e\x96\x4a\x45\xf2\xcd\xd9\x59\xf2\xa4\x1f\x62\x86\x17\x10\x02\x53\x7d\xfc\x3d\x16\xd0\xf7\x79\x68\xdf\xc9\xb3\xd1\x60\x78\xe1\x0d\x5e\x78\x83\xe1\x19\x81\x28\xe0\x6b\x3d\xee\x06\xc2\x28\xc0\x0a\xfa\xff\x92\x9c\xfd\xc7\xc9\x69\x02\xc1\xe7\x4c\x01\x53\xb7\x20\x24\xe5\x4c\x03\x1a\xf6\x07\xfa\x5f\x3a\x20\xc2\x02\x87\xa0\x40\xc8\x93\x37\x28\x41\xcb\x3c\xc7\x24\xa4\xec\xcb\xdd\xbf\xc0\x57\x57\xa4\xf0\xca\xbc\x56\xeb\x08\xf4\x6c\x52\x09\xca\x16\x76\xb2\xec\x6d\x08\x0a\x13\xac\x70\xe5\x3b\xf3\x96\x80\xf4\x05\x8d\x94\x45\xe8\x66\x09\x68\x3c\x9e\x20\x03\x11\x2d\x04\x8f\xa3\x9e\x44\xdc\x80\x46\x57\x93\xfe\x49\x61\x86\x1f\xd9\xaf\x1f\xa7\x39\x6c\xef\xe5\x65\x10\x4b\x05\xe2\x33\x0e\x61\x5b\x74\x09\xcc\x71\x1c\xa8\x5b\x1c\xc4\x66\x14\x16\xdc\xc3\xf7\xd2\xf3\x93\x29\xf7\xa6\x8e\xe1\x10\x10\x9f\x23\xa5\x29\xfd\x38\x43\x9f\x8c\x70\x09\xb2\x28\x23\x01\x92\xc7\xc2\x87\x4e\xa4\xde\xc3\x7a\xa5\xb1\x9d\x0a\x98\xd3\xc7\x7d\x49\xfd\xe6\x73\xe6\x63\xf5\x4c\xe1\x7b\x78\x96\xe2\xf1\x41\xcb\xe0\xd9\xf3\xbe\xc6\xfb\x14\x0d\x5f\x3e\x3f\x45\x3d\x8f\xc0\xaa\xf7\xfc\xf7\x0a\x2f\xf0\xe3\x3f\x80\x2d\xd4\xf2\xe4\x0d\x1a\x0d\x9c\x08\x13\x26\x0f\x84\xeb\x46\x57\x9f\xf5\x8a\x12\x77\x61\xb6\x8d\x94\xbe\x98\xff\xe1\x00\x4d\x3e\xcf\x50\x64\xb0\x45\x8a\xa3\x58\x02\x7a\xa0\x6a\x89\x96\x5c\x2a\x20\xe8\x63\x7c\x07\x82\x81\x02\x89\xc6\xd3\x2b\x24\x41\xac\x40\xa0\xf7\xff\x3d\xf9\xdc\x4d\x4b\x57\x98\x06\xf8\x8e\x06\x54\xad\xff\x97\x33\x90\x55\x8e\x94\x68\xfe\x56\xc5\x7c\x58\x22\xd4\x3c\x1c\xb9\x1e\x9e\x17\x71\xfa\xfd\xd4\xcd\x7c\x2c\x04\x5e\xef\xc5\xbd\x59\x04\x3e\x9d\x53\x90\x46\xc3\xf3\x54\xa2\xef\x9a\x4c\x34\xe7\xc2\xbc\x92\x6b\xa9\x20\x44\x8c\x13\x40\x11\xe7\x41\x37\xae\x2d\x80\xa9\x4b\x1e\x33\xd5\xca\xae\xf3\x1a\x12\x29\x53\xfb\x2f\xe2\x38\xbc\x03\xa1\x97\xb1\x41\xc8\x10\xb1\xa1\xcc\x9a\x8a\xee\x04\x7d\xa2\xec\xe7\xd3\xa4\x0d\xae\x93\xae\x2d\xe8\xc0\x8f\xdd\xe8\x18\x0e\x8e\x49\x08\x7e\xdc\x97\x90\xdb\x4f\x33\xfa\xdd\xe1\x3a\xca\x56\x68\xa6\x30\x23\x58\x90\x7f\x4e\x46\xf2\x9f\xab\xf3\x32\xea\x07\x76\x8c\x92\x7e\xcf\x5c\xc7\x2d\x15\x2a\xc6\x01\xfa\x84\xfd\x25\x65\xdd\xbc\x05\x97\x13\x2a\xef\x35\x65\x1f\xde\xb6\xab\xda\xa8\x8c\x2e\x65\xe9\xbb\xb2\xf4\x42\xfc\xb8\x91\xec\xe8\x58\x4a\xaa\x91\x4f\x78\xf0\x8c\x32\xf4\xe1\xed\x73\x6d\x95\x23\xc1\x57\x54\x87\x33\x66\xf5\x01\xf6\x97\x29\x87\x12\xc1\x6b\xcb\x62\xa5\x8f\x6e\x96\x54\xa2\x95\x46\x14\x09\xcc\x16\x7a\xc5\x0a\x1e\xa2\x81\x9e\x47\xe3\xdd\x47\x89\xf1\x5a\x53\xb6\x40\x03\xf4\x40\x83\x00\xe1\x28\x0a\xd6\x66\x3e\xcb\x1f\x44\x32\x34\x92\xf5\x8e\x15\xca\xe9\x4c\x27\x41\x84\xf8\x71\xca\x49\xbb\xc5\x1f\x0e\x8e\xb5\x4e\x3e\xe1\x47\x1a\xc6\x61\x6e\x9d\x44\x9c\xc8\x84\x1a\x1f\x33\x24\x62\x86\x38\x43\xd8\xb0\xae\x13\x4d\x01\x65\xf1\xe3\x58\x87\x6d\x5f\x25\x08\x76\x80\xc8\xcb\x0f\x78\x4c\xbc\x58\xee\x19\x74\x69\x74\x92\xa8\x2b\xb5\xcf\xff\xd0\xa8\x96\x57\x50\x37\xeb\x20\xe5\xf2\x7a\x36\x9e\xc6\x77\x01\xf5\x3f\xc2\xfa\xa8\xb1\xf0\x25\x67\x73\xba\x88\x05\x20\x1c\x04\xc8\xf0\x17\x85\x16\xd9\x24\x12\xd1\xc4\xcc\x66\xbf\xa1\xeb\xd9\x18\x45\x06\x27\x74\x0f\x6b\x94\xc0\xee\xa3\xff\xe1\xb1\x48\x1e\x2c\x79\x1c\x10\x44\x99\x1f\xc4\x04\x90\x5a\x0a\x00\x14\x61\xa1\xe4\x69\xb2\x6c\x1e\x71\x18\x05\x80\x7a\x52\x2e\x3d\x21\x31\x1a\x8f\xc7\xe3\xb7\xfd\x7e\x5f\x32\x1a\xf5\xfb\xfd\xaf\xfe\x3a\x8e\x16\xbf\x21\xb3\xfb\xd0\x02\xf9\x2f\x83\xcd\x2a\xec\x75\x0a\x52\xb3\x40\x69\xb3\xf3\x68\xb1\xab\xc3\xfe\xe8\xbc\xff\xf7\x23\x9b\xd3\x55\x82\x8d\x56\xfe\x4d\x2c\xd7\x49\x0b\x18\xa8\x07\x2e\xee\xa7\x41\xbc\xa0\x0e\x6a\x70\x10\xf0\x07\x20\x86\x1a\xe9\x0e\xdc\x0c\x27\x5d\x71\x9a\x66\x16\x03\xd5\x1c\xad\x55\xf6\x28\x8e\xd9\x0e\xc9\xad\xcf\x09\xbd\x28\x32\x04\xeb\x48\x98\x18\xbd\xb9\x8b\x69\x40\xb4\xb1\xcc\xc5\xc2\x96\x37\x9d\xf8\x08\x0c\xdf\x05\x70\xfd\x76\x7c\xd9\xaa\x12\x4a\xc4\x50\x43\xe0\x1d\xe7\xc1\x5e\xe4\xe9\x09\x00\x33\x34\x0f\xf0\x42\xfb\x02\x15\x8b\xc4\xfa\x31\x82\xf8\x7c\xae\x15\xc4\xe0\xd8\x81\xa2\x15\x03\x75\x90\x5d\xe7\xbd\xf4\xb4\x1a\xec\x25\xb5\xdc\x4e\xf3\xf6\xf3\xbb\x9b\xc4\xba\x1b\xaf\xe6\x73\xa6\x30\x65\xd9\x26\xd4\x46\xac\x86\x62\x01\x01\xd6\xfb\x9b\x74\x03\xd8\x6d\x49\x68\xc2\xc7\x84\x08\x90\x07\xda\xe0\x0d\x07\xfd\xe1\xe8\x75\x7f\xd0\x1f\x9c\x5d\xec\xc5\x06\x43\x3a\x4e\x50\xb3\xdb\xb9\xae\x04\xcd\xe2\xbb\x03\xc9\xd3\x6e\x4f\x93\x09\xf7\xdb\x5f\x99\x29\x12\x87\x56\x23\xd0\x28\x42\x33\x10\x2b\xea\x03\x7a\xc7\x56\x54\x70\x16\x02\x53\x9d\xdc\x9b\x99\xfc\xa0\x02\x7c\xf9\xa2\x3f\x38\x1b\xbe\x3e\x04\xcd\xdb\x0b\x31\xe2\xa4\x49\x86\x65\x94\xa7\xe9\xf0\x23\xda\xd2\xe2\xfe\x38\x9f\x0d\x4a\x98\x6f\xd2\x0b\xda\xa6\xea\x47\x26\x28\xcb\x25\x8b\xb6\xd9\x59\x66\xb4\xd7\x89\xd3\x29\xb0\x0b\xb3\xe2\x86\x2f\x9f\x8c\x03\x45\xa1\x1e\x89\x17\x0b\xac\xe0\x01\xaf\x0f\xb7\x9e\x3f\xe4\x27\xfc\x75\xd7\x73\x81\xee\x83\xdb\xe5\xd1\x8b\x9f\xb3\xaa\x65\xc2\x8c\x4b\x4a\xc4\x21\xa8\x39\x1f\xd4\xe8\xfc\x56\xd4\x8c\xd1\xe5\xd5\xe4\x1a\x31\xae\xb0\x7e\x84\xae\xa6\xc9\x26\x33\xd9\x63\x3e\x2c\xa9\xbf\xd4\xb1\x05\x96\x92\x2e\x18\xb2\x24\x64\x8e\xf7\x6a\xda\xcd\xcd\x12\x26\xad\x2a\x5c\x4d\x0f\x46\xfb\x70\xb0\xef\x3e\x45\xeb\x2a\x08\x69\x32\xa6\x36\x11\x7a\x35\x4d\x25\xdb\x8d\x30\xee\xdf\x83\x78\x2b\x28\x59\x1c\x46\xb0\xaf\x46\xfd\xe1\x2b\x4d\xdc\x11\x04\xab\x63\xdf\x89\x41\x18\xdd\x19\x8c\xbb\xed\x1a\x38\x81\x29\xe7\xc1\x3b\x13\xf5\x8e\x63\xc5\x67\x3e\x0e\x34\x2d\xbf\x72\xf0\xab\xb1\x36\x49\x14\x1c\x2b\x2e\x2d\xc2\x1d\xa8\xc5\x96\x3e\x10\x53\xc1\xe7\x34\x80\x99\x8f\xd9\x15\x53\x20\x56\x38\xd8\x5f\x75\xe5\x01\x5d\x90\xf4\x31\x43\xd4\xa2\x96\xa5\x8f\x62\xc5\x3d\x69\x08\xd8\xd5\xeb\xb8\x58\x10\xc0\x84\x3f\xb0\x09\x04\x78\x3d\x9e\x2b\x10\x63\xb2\x75\x41\xad\xca\x8c\xf0\xb0\xcc\x08\x00\x11\xfe\xc0\x10\xd1\x58\x22\x3c\x37\x3b\x03\x42\x9e\x98\x33\x13\x08\x40\xed\xed\xa8\x47\x87\xd6\x14\x17\x73\x88\x41\xf5\x89\xf9\xf3\x1e\xd3\x40\x6f\xfb\xf7\x64\xd0\xf9\xa1\x94\xa7\x86\x37\xf3\x04\xcd\x27\x61\xce\x57\xc6\x00\x08\x90\x1b\xba\x7f\x84\x77\xb4\x55\x15\x5b\x24\x91\xa2\xe1\x53\xb1\x45\x00\x26\xeb\x43\x70\x65\x74\x3c\xae\x18\x1c\x8f\xce\x94\xaf\x8a\x06\xf4\xbb\xf1\xe4\x37\x4b\x01\x72\xc9\x83\xbd\x0d\xf0\xa0\x9a\xaa\xdc\x83\x29\xf1\x06\x43\x93\xa9\x35\x28\x1e\x93\x25\x9f\xf0\xe3\x07\x81\x7d\x98\xc7\xc1\x0d\x88\x90\x32\x03\x7b\x06\xfe\xbe\x7c\x79\x39\xd8\x2f\xb0\x2c\xf2\x25\xc4\x8f\x68\x61\xf1\x44\x6a\x83\x68\xa2\x32\x99\xfb\xa6\x3a\xb8\xf6\x39\x23\x9b\x7a\xec\x01\x98\xb6\x8a\xd8\xe5\xf8\x12\x84\xa2\x73\xea\xe3\xfd\xdd\xd2\x06\x66\x02\xcf\xc2\x3a\x59\x61\x41\x75\x74\x58\x3e\x80\x73\x2f\x3f\xcb\x85\x39\x7b\x73\xf2\x2d\x4d\xcc\x5d\x91\x67\xbd\x4f\xd4\x17\x5c\xf2\xb9\xea\xdb\x1c\xed\x99\xcd\xbf\xce\xc0\x8f\x05\x55\x6b\x73\x7c\x43\xf6\x4e\x51\xcf\xa4\x14\xe5\xa2\x78\x3a\x42\xcf\x3c\xe5\xe4\x20\x93\x47\x9c\xb8\x01\xd8\xe4\x57\x23\x84\x6c\x13\x61\xb7\x37\x67\xc9\xa1\x27\x62\xbf\xd5\x40\x9a\x4e\x7c\x14\x61\xae\x18\xa8\x4e\xf4\xac\x92\x1a\x90\xfd\x59\x06\x92\x26\x73\x5d\xd3\x27\x9b\xd7\x5d\x80\x9c\x25\xb9\x8d\x7a\x60\xd5\xe7\x9b\xbc\x45\x19\x95\x2c\xc9\xf3\x04\x98\x14\x92\x69\x65\x44\xac\x66\x68\x31\x0a\x7a\x17\x2b\x2e\xae\x79\x00\x16\x2b\x19\xdf\x65\x0b\xfb\xda\x89\xe1\x38\x56\x4b\x2e\xac\xd9\x3b\x13\x3c\x80\x09\xcc\x29\xa3\xfa\xa7\x3c\xd3\x1a\xf6\x82\x5c\xbc\xba\xbb\x78\x7d\xe7\x0d\xc9\x8b\xb9\xf7\xe2\xd5\xeb\x57\x1e\x1e\x5d\x0c\x3d\xff\xe5\xab\xd7\xe7\x2f\xc8\x68\x38\x82\x57\xb5\xaa\xf7\x55\xea\x78\x72\x4e\x19\x10\x7b\xde\xea\x8a\x00\x53\x54\xad\x6d\x32\x28\x3b\xfb\xd4\xa4\x64\xa7\xa8\xe7\x85\x92\xee\x00\xa5\x51\x3a\xa5\xb1\x67\xb1\x04\x31\x36\x69\x82\xec\x21\x05\x2d\xa4\xcc\x38\xe4\x51\x6b\x26\xad\x2c\x26\x6d\x48\x9c\x92\x4a\x00\x86\xc0\x54\xc6\x91\x45\x4c\xc9\xb3\x2d\x61\x5e\x11\xcd\xa6\xdc\x47\x75\x7a\x51\x1a\x96\x5f\x52\x06\xe7\x82\x55\xcc\x4a\x11\x85\x62\x5a\xc9\x00\x47\x82\x47\xda\x40\x27\xb6\xf3\x47\xc9\x00\xdb\x82\xf4\x89\xb5\x82\x75\xe9\xcd\x8e\x26\xaf\xfc\x79\xc0\x7d\x9c\x7a\xad\x6f\xe5\x93\x73\xe9\xcb\xca\xa9\x34\x1c\xd1\xdc\x59\xcc\xd1\x60\x34\x4c\x0e\x72\x9e\xb8\x5c\xd0\xce\xd4\x5a\xb3\xfc\xa7\xa7\xb8\x1e\xe1\x92\x59\x6b\x05\x3a\xf2\x06\x43\x0d\xf4\x20\x24\xa5\xac\xfe\xe6\x36\xa1\x95\xf1\x04\x22\x60\x44\x7e\x61\xee\xc2\xf0\xb7\xe2\x82\x33\x8e\xb9\x3a\x8b\x6b\x68\xea\xc6\x7b\xe9\xea\x49\xff\xca\xd5\xe3\xa2\xea\x54\xe7\xb5\x69\xbb\x59\x84\xfd\x6a\x9c\x53\x1e\x95\x24\x95\x6b\xca\xdc\x1b\x54\xcb\xcc\x29\x94\x0a\x2b\x18\x67\x98\x57\x9e\xfe\x70\xf0\xc1\x7a\xb1\x5a\x0c\xdc\x04\xa0\x46\xe1\xe5\x1d\x9d\x8b\xf9\xd9\x0c\x2d\xcc\x2c\x8c\xc5\xa5\xfa\x68\x11\x6c\xbe\xf6\xd6\x0c\x14\xe5\x1c\x6e\x61\x95\xb6\xa2\x60\xbe\xa4\x89\x33\xaa\x51\xb4\xc6\xef\x1d\xec\x2f\x4c\x6d\xd3\xda\xef\x18\x89\x38\x65\x0d\x32\xc9\xff\xb5\xa3\x9c\x9f\xbb\x68\x02\xc6\xdf\x63\x01\x97\x5c\x86\x5c\x4e\xde\x36\xe3\xde\x01\xff\x7d\xb1\xc9\x82\xd8\x6b\x58\x50\xa9\xc4\xfa\xa7\x63\x34\x53\x5c\xe0\x05\xfc\x74\x3c\x3e\xc2\xfa\x56\x6f\x80\x3a\x20\xd2\x38\xa2\x6a\x11\x9a\xbf\xab\x21\x6b\x5b\x93\x50\x0a\x7d\x9f\xc8\x22\x94\x2a\xb8\x4f\x6f\x14\x6a\x5d\x4a\xf9\xaf\xcd\x2e\x10\x08\x60\x61\xfc\xe7\x41\x4d\x42\x2a\xab\xf1\xc7\x59\x0b\x67\xb2\x4f\xb6\x90\x4e\xe1\x3b\xab\xd5\x69\x70\xdc\x7d\xe3\xda\xae\xf1\xa8\x55\xeb\xdb\x47\xfc\xa4\x75\x51\xa9\xab\x3f\xd9\xda\x70\x54\xb6\x1b\xb5\xb4\x86\x0d\x95\xa7\xbf\xb7\x27\x83\x76\x8d\x4b\x4b\xbf\xa7\x00\x82\xb2\x45\x25\x5c\x65\x0d\x3b\xd2\xfc\xd6\xbc\x77\x16\x25\x33\x78\x22\xf2\x22\xf0\xf4\x3b\x6f\x30\x18\x3a\x62\x4e\x47\xd8\x3d\x1c\x1d\x2c\x02\xde\x22\xa2\x5d\xd9\x0d\xde\xbe\x11\x6a\x10\xf0\x87\xdb\x02\x37\xc7\xbe\x0f\x52\x3a\x6b\xa5\x9b\x4f\xde\x73\xf1\x80\x05\x01\x72\x23\xf0\x7c\x4e\xfd\x96\xe1\xf6\x90\xc7\x8d\xc0\x4c\x52\x75\xf2\x06\xcd\x71\x20\x5d\xa3\x63\x09\xd7\x10\x72\x05\xf6\x0b\xd9\x30\x56\x98\x81\x45\xe4\xeb\xa3\x6c\xba\x5b\x16\xab\x57\x55\x89\x6a\x28\xfd\xcb\x69\x7a\x09\xeb\x4c\xbf\xbb\xa5\xe5\xd0\x5f\x9a\x8e\xfe\xfc\x9a\xde\xce\x42\xf4\x4b\xea\x6e\x27\x2b\xad\x5f\xfc\x65\x9e\xff\xff\x29\xed\x2e\xe6\x39\xd5\x85\x3f\x81\x6e\xa7\xe8\xfe\x65\x90\xff\x0d\x75\xfb\x58\x06\x39\x9f\xa2\x5e\x45\xcc\x8b\x68\xd4\x3d\x45\x9d\x34\x3b\x5d\x4d\x6d\x1a\x11\x8e\x95\x9e\x1e\x78\x83\xd7\x0e\x65\x95\xf7\xb1\x5b\x3b\x52\xa2\xde\x62\x49\xfd\x52\x5d\x79\x4b\x45\xcb\x48\x0c\x52\x74\x3f\x81\x5a\x72\x23\x94\xc9\x9a\xe1\xb0\x02\x60\x27\x4b\xd1\xad\xf8\xd4\xc6\xa8\xe1\x6b\x6f\x38\xf4\xce\x2b\xe5\xfe\xcc\x92\xec\x54\xba\xaa\xc8\xa7\xa3\x54\x77\x62\x44\xb5\x02\xb9\x29\x89\xb5\x97\x14\x06\xde\x70\xe0\x0d\x86\x5e\x24\x60\x45\xe1\xa1\x23\x1f\xba\x94\xe2\x1c\x6c\x90\x3e\x8f\x2a\xd3\x15\x2b\x68\x7b\x96\x1d\x3a\xd5\xf9\x5a\x8b\x12\x07\x32\xb9\xc5\x6a\xf0\x55\xc5\x2c\xd5\x97\x18\x9d\x18\x46\x82\x32\x9f\x46\x38\xc8\x8a\xb2\x73\x10\xc0\x7c\xd8\xb6\xd6\x69\x5d\x5e\x3f\x37\x61\x33\xc0\x1b\xab\x74\x36\x53\x34\x4d\x9f\x6f\xbf\x8e\x5d\x25\xad\x73\x87\xa1\xaa\xaa\x79\x6b\xba\xea\x78\x45\xb1\xd6\x5b\x3a\x76\xf2\xe6\xed\x85\xb1\x8e\xca\xdc\xa8\xa3\xd4\x0e\x75\x6b\x68\xca\xe6\xaf\x39\xe3\xe9\x42\xac\xc6\xb8\xd6\xba\xe0\xdd\x08\x79\x83\xfe\xa8\xe6\xb5\x7e\xec\xe5\x8c\x18\x27\x70\x9d\x17\x7e\x3e\x3c\x74\xdf\x14\xd3\xf3\xf0\xbd\xac\x11\x10\x8e\xe8\xcc\x74\x09\x24\xa1\x93\x3d\x87\x56\xcf\x87\xa4\x4f\x74\x2a\xe8\x0a\x2b\xb0\x5c\xb0\x01\x54\x95\x50\x17\x3c\x42\x38\xb3\x50\x1a\xd8\x6d\xda\x68\x3f\xda\x2b\x75\x66\xe0\x0b\x50\x1a\xb7\x15\x25\x50\xed\x47\x70\x20\x48\xea\x70\x42\xce\x54\xa3\x0b\x53\x57\xaf\x74\x71\x15\x55\x46\xd4\xf0\xb8\xd0\x5b\x5b\x9c\x62\xf3\xaa\xe6\xdb\xfc\x2d\x3d\xc5\x4f\xb3\x37\x35\x5f\x9a\xbe\xf0\x56\x79\xe2\xd2\x35\x01\x45\x18\xd5\x9b\x04\xea\xb3\xca\x27\x52\x2e\x9b\x45\x13\xa5\xbd\xfa\xed\x95\x87\x0e\x65\x92\x7b\x58\x4f\x92\xe3\x8b\xa5\xe2\x6d\xf1\x5e\x80\xf6\x82\x49\xed\x5b\x77\x26\xbf\x9b\xf6\x98\x7b\x28\xa6\x9c\x07\x39\x5d\xdf\xb9\x26\x9e\xdc\xcf\x13\x55\xfb\x4b\x0a\xa3\xb9\x4c\xbd\x9a\xb9\x55\xa1\x69\x68\xc8\x49\xe2\xfe\xcc\xc4\x8d\x93\x0a\x7f\x09\x52\x09\xac\xb8\xd8\x73\x1d\x64\x73\x82\xa3\xeb\xa6\x38\x63\x6d\x7f\x4e\xa7\x99\xdf\x31\x5f\xac\xcd\x09\xb7\xb1\xfa\x8d\x4b\x55\xbb\xc1\x2b\x7d\xf7\xfe\x6a\x3a\x6b\x1f\xfb\x65\x36\xa1\xf2\x3e\x65\xf5\xbb\x68\x09\x21\x08\xdc\x22\x99\xc2\x6d\x2f\x45\x5a\xf3\x2f\x5b\xc8\xf3\xed\x8d\x3e\x25\x67\x9e\x5d\xc4\xd4\xf2\x79\xb8\xb9\xdb\xc8\x31\x43\x7a\xf3\x51\xdb\x24\x9b\x8b\x85\x5c\x93\xd8\xb7\x2d\x93\xac\x42\x7b\xa9\x8f\x63\x8a\xe4\xf2\x96\xb6\x09\x36\x21\xf6\xa4\x21\xfc\x9e\xb4\x15\xc1\xb2\x33\x9b\x95\x59\x72\xc7\x39\xdb\x39\x62\xaf\x90\x29\x52\x63\x9f\xb7\x7c\xed\xba\x7c\xac\xc4\x95\xf2\x88\x5a\x93\xe6\xa8\xa1\x39\x4c\x53\x7a\x65\x46\x9b\x6b\x28\x5f\xad\x51\x5a\xa2\xf9\xb7\x4d\x5e\xa1\xd0\x21\x5a\x32\xd5\x9b\x77\x0d\x33\x94\x1a\x2d\x2b\x5e\x30\x7b\xd9\x34\x47\xb5\xa7\xb1\x34\x4f\x69\x80\x3b\xb5\xe2\x32\xf4\x98\xb4\x72\xd2\x06\xf6\x8d\xb6\x25\xb5\x8a\x3a\xf0\xb1\xa1\x42\xf7\xa0\xaa\xdc\x47\x50\x8f\x8a\xf4\x31\xf3\xe8\xa6\xf7\xaf\xa4\x6a\x4d\x9d\x82\x4d\x42\xd6\xdf\x78\x84\x3f\x30\xcf\x34\x1c\x79\xa6\xe1\xc8\xc3\x84\x74\x02\xe1\xe8\xc4\xdb\x1e\x18\x49\x9b\xd4\xb6\x87\x97\xf4\xb7\x6d\x0f\x72\x9e\xf5\x7d\x6d\x0f\xd3\xf6\x8c\x75\x03\x9a\x36\x2a\x79\x8a\xba\x76\x73\x9d\xba\xb0\xba\x42\x32\xcd\x3f\xdb\x03\xca\xfa\x9a\x3a\xc2\xd9\xb4\xd6\x78\x2a\xd7\xfd\xd3\x02\xd0\xd5\x33\xd4\x00\x30\xc4\x8f\x5e\xda\xa8\xe2\xe5\x1a\x55\x3c\x69\x5a\x6a\x5a\x80\xd5\x76\xe3\x1c\x28\xef\x5a\x49\xdd\x5d\x78\x83\x8b\x4e\xa9\x83\xf4\x0c\xd7\x99\xd9\x1f\x6d\x55\x11\x2b\xde\x53\x9a\x9c\xa4\x77\x6e\x0f\x8f\x5f\x07\xc8\xf7\xa3\xec\x9d\x9a\x52\xc0\x30\x53\x8e\x2e\x87\x67\xcf\xfb\xe9\x3b\xe7\x3e\xa9\x2e\x83\x6c\x5e\xce\x71\x48\x83\xb5\x39\xd8\x54\xa7\x63\x59\x90\x6e\x2f\x20\xec\xe8\x36\x92\xed\x36\x0f\xa8\x4f\x77\xdb\x1c\xec\x4a\x70\x36\x01\xdf\xdc\x22\xdc\x9a\x7a\x4b\xba\x04\x7a\x49\x7a\x75\xe4\x0d\x86\xbd\x53\xd4\x7b\x1f\x07\x41\xef\x79\x7f\x23\x99\x7e\x61\x77\xdf\x6f\xda\xc4\xf7\xd3\x34\x4e\x3f\x45\xa3\x39\x50\xd3\xeb\x4f\x4a\x7b\x6a\xad\xe5\xb8\x92\x4c\x40\x75\x3a\xde\x76\xb2\xa8\x5c\x1a\xe2\x1c\x16\x50\xd9\x72\x62\xd2\x11\x6a\x15\x66\xf0\x37\x2d\x6b\x5b\xa0\xd6\x0c\xb2\xf6\xed\xa1\x8e\x9a\x1d\x56\xc9\x0a\xd6\x36\x7f\x91\x75\x5b\x98\xfe\xb3\xa5\x2f\x7f\x5d\x25\xb1\x61\x4f\x07\xec\x3a\xd2\x4a\xc3\x88\x8b\x43\x91\xbb\xad\x82\x76\xda\xc0\x24\x91\xf2\x8c\xcf\x55\x76\x31\x41\x25\x54\x3e\x64\xb1\x3e\xab\xe7\xd6\xb8\x59\x5b\x37\xdd\xb5\x84\x79\x1c\x3f\xdb\x7c\x10\xa2\x52\xb5\x4d\x3b\x47\x6d\xfd\x77\xab\x92\x92\x6b\x60\x73\xb5\x61\x2f\x4f\x4f\xa3\xf4\x3a\xce\x96\x63\xcc\x4d\xa9\xc8\x2d\x4e\xbf\x26\xdd\x20\x5b\x9d\xdd\x3e\x66\xef\xa7\xe3\xa0\xef\xbe\x67\xc2\x4b\xda\x70\x40\x52\xbb\xe8\xd9\x8e\xd9\xd9\x06\xa2\xb2\x95\x69\x7b\xbd\xf7\xc9\x96\xac\x22\x96\xe6\xfa\xae\x79\xac\xe0\x2d\x96\xee\x8a\x52\x63\x1c\x99\x22\x74\x1b\xb1\x0f\x0f\xae\xab\xe2\xcd\x28\x45\x4d\x75\x23\x1d\xd5\x29\x90\x5c\x45\xec\x32\xa0\x26\x03\x98\x5b\x14\xf5\x98\x64\xe3\xd3\xbe\x2f\xce\xab\x77\x00\x15\xbe\xd8\xa6\xbd\xcc\x7c\x30\xbc\x18\xf5\x87\x2f\x5f\xf7\x47\x7f\x7f\x91\x5c\x4d\xb6\x6d\x86\xbd\x86\x3d\x19\xea\xd7\x9c\xab\xcb\xae\x9e\xb2\x65\x75\x6f\xd9\x85\x60\xd7\x8a\x86\xee\xac\x3e\x94\x6f\x27\xd8\xbb\x5f\xa3\x7c\x38\xc7\xc7\xdb\x9e\xac\xaf\x89\x39\x36\xec\x9c\x0a\xae\xb8\xcf\x83\x96\xce\xc1\x2f\x11\xb0\xdb\xe9\xe7\xce\xfd\x81\x75\xfe\xf8\x6f\xc9\xf8\x1f\xff\x17\x00\x00\xff\xff\x2d\x27\xde\x1d\x6d\x65\x00\x00")
+
+func aksDevelopmentJsonBytes() ([]byte, error) {
+	return bindataRead(
+		_aksDevelopmentJson,
+		"aks-development.json",
+	)
+}
+
+func aksDevelopmentJson() (*asset, error) {
+	bytes, err := aksDevelopmentJsonBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	info := bindataFileInfo{name: "aks-development.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
+	a := &asset{bytes: bytes, info: info}
+	return a, nil
+}
+
 var _clusterDevelopmentPredeployJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xec\x58\xdf\x8f\xa3\x36\x10\x7e\xdf\xbf\x02\xd1\x4a\x24\x52\xf8\x91\xe8\x5a\x75\xf7\x6d\xd5\xf6\xaa\xd5\xa9\xd7\x55\x73\xdd\x3e\x44\x79\x70\xec\x49\xd6\x07\xd8\xc8\x36\xb9\x72\x55\xfe\xf7\xca\x10\x48\x00\x43\xb2\x34\xb9\x56\xab\xbb\x93\x56\xc2\x8c\xc7\x33\xdf\xcc\xf7\x79\xc2\xdf\x37\x96\x65\x59\xf6\xb7\x12\x3f\x43\x8c\xec\x3b\xcb\x7e\x56\x2a\x91\x77\xbe\x5f\xac\x78\x31\x62\x68\x03\x31\x30\xe5\xa1\xcf\xa9\x00\x0f\xf3\x78\xff\x4e\xfa\xb3\x60\xfa\x9d\x1b\x4c\xdd\x60\xea\x13\x48\x22\x9e\x69\xbb\x0f\x10\x27\x11\x52\xe0\x7d\x94\x9c\x7d\x63\x4f\x8a\x13\x30\x67\x0a\x98\x7a\x02\x21\x29\x67\xfa\xa0\xa9\x17\xe8\xff\xa5\x41\x82\x04\x8a\x41\x81\x90\xf6\x9d\x55\x84\x55\x6c\xa4\xb5\xe7\x7c\x4d\x65\x09\x68\x17\x2b\xce\xa3\xfd\xfe\xea\x1d\x81\x35\x4a\x23\xf5\x84\xa2\x54\xdb\xac\x51\x24\xa1\xb2\xd8\x4d\x8e\x1c\x47\xa9\x54\x20\xde\xa3\x18\xba\x4f\x90\x4a\x50\xb6\xb1\xfb\x1c\xcc\x41\x6c\x29\x86\x47\x41\x19\xa6\x09\x8a\x1e\xc8\x30\x77\xeb\xe4\x52\x9e\xc2\xed\xf0\xa4\x62\xa4\x73\xba\x27\x44\x80\x94\x8f\x02\xd6\xf4\xaf\x61\x8e\x04\x4f\x15\xc8\xee\xbd\x48\x08\x94\x9d\x28\xde\x62\x69\x74\xbd\x65\xa0\x2e\x10\xe1\x27\x2e\xc2\xe1\xa9\xde\x1c\xb9\xb3\x05\x48\x9e\x0a\x9c\xe7\xbb\xa8\x6c\x1a\xae\x12\xc1\x13\x10\x8a\x1a\x50\xc9\xdf\xa3\x22\x92\x79\x82\x70\xbb\x78\x4d\xab\x22\xde\xc6\x81\x2d\xe3\xc5\x81\x55\x23\xa7\x05\x9b\x33\x5e\xda\xc6\xbd\xcb\xd6\xea\xae\xb6\xb2\x6b\x94\x8d\x15\xed\x66\x13\xd8\xba\xfa\x94\x66\x59\x4b\x10\x7f\xa5\x58\x70\xc9\xd7\xca\x7b\x0f\x4a\xc3\xef\x6f\xa9\x50\x29\x8a\xf6\x8f\xb2\xb9\x31\xe2\x18\xa9\xbd\x5e\x2c\x4a\x90\x7f\x11\x3c\x4d\x46\x63\xaf\x7c\xb9\x6c\xee\xc2\x9c\x11\x5a\x6d\x3b\x86\x00\x53\x9d\x73\xc3\x1c\x25\xf4\x48\x96\x66\xc1\x2c\x70\x83\x1f\xdc\x60\x6a\xec\x9a\x17\xd6\xb4\x62\x41\x3d\x8e\x62\xb9\x85\x7f\x17\xae\x0b\xcc\x19\x46\x6a\x54\x4b\xe5\x20\x5f\xce\x78\x62\x39\xae\x50\x86\xdc\xba\x91\xcf\x43\xf8\x80\x56\x11\x5c\x08\xf5\x6b\xc2\x88\x1a\x24\xad\xa3\x69\xd0\xac\x36\x14\x87\x72\xe4\x49\x77\x13\x8c\x92\x5a\xde\x94\x8c\x9c\x5e\xf4\x9c\x89\x75\x6e\x79\x8c\x41\x15\x75\x42\x1b\x9d\x3a\x4b\xa3\x68\x20\xf7\xca\x1e\x71\x4a\x12\xfa\xce\xc4\xea\x8d\xa8\x80\xed\x45\x4d\xd3\xa0\xab\x2f\xd3\x15\x03\xd5\x6a\xa0\xce\x56\x68\xc9\x7d\x02\x8c\xc8\xdf\x98\x51\xc5\x4e\xd5\xa0\x11\x8c\x33\xb1\xaa\xdc\x3b\xca\x7f\xad\xa2\xd6\x4e\x5a\x0e\x63\xd3\xd5\x59\x62\xb8\xee\xbe\xb2\xe4\x34\x4b\x0a\xd8\x5e\x0d\x4b\xca\x60\x6a\x6c\x39\xaf\x36\xa5\x5e\xbc\x4a\x6e\x55\x0d\x72\x1c\x53\x31\x48\x9f\x55\xfb\x77\x90\x3d\xe9\xd1\xd5\xdf\xea\xbf\x17\xba\x52\x4f\x11\x5e\x01\x43\x4c\x3d\x14\x54\x94\xe9\x4a\x62\x41\x13\xed\x69\x34\xf6\xca\x77\xc6\x62\xc9\x30\xed\xa6\xf6\x1a\xc5\x34\xca\xb4\xcf\xfb\x2e\x22\x96\x68\x49\x85\x18\x41\x82\xb4\xc7\xc8\x9d\xe1\x54\x84\xb1\x96\x1e\x1e\x51\x5c\xa4\xb4\x58\x1a\xac\x80\xe9\x2e\x21\x6f\xb9\xf8\x89\xca\xf0\x67\x86\x45\x96\xec\xa1\x53\x22\x85\xce\x1d\x8f\xa9\xd8\xc0\xa3\xe0\x0a\xf0\x91\x79\xaf\x2a\xfc\xcb\x41\x71\x7a\xeb\x06\xb7\x17\x9a\x70\x42\x95\x23\xfe\xfb\xdc\x84\xb9\x1d\x42\x36\xa7\x9f\x35\xe4\x6f\x82\xdb\xef\x07\xcf\x8c\x65\x3f\x4f\x2c\xc7\x3f\xb0\xb0\xd9\x8e\xda\x83\x26\x20\xa1\x32\x74\xa1\xc2\xdf\x0d\x21\x33\x90\xff\x34\x17\xfc\x10\xb2\xff\xc7\x64\x5f\x15\x6c\xa0\xc2\x3e\xd4\xa4\xad\x91\x66\xe3\x22\x29\xb1\x6e\x49\xd8\x19\xcd\x42\x09\x30\x45\xf3\x8e\x30\xb1\x7e\x0f\xf8\x3c\x93\x0a\xe2\x7b\x29\xe9\x86\x01\xe9\xff\x25\x71\x72\x74\xc0\x8a\x6e\xe1\x1d\x98\x8f\xcc\x4d\x0a\x08\xf2\x8c\x3b\x8d\x2c\xc3\x68\x70\x09\xd0\x3a\xd2\xaa\x8e\x0c\x21\xfb\x43\x44\xfb\x63\xd7\x20\x80\x61\x18\x9d\x15\x40\xde\x9c\x1d\x51\x0c\x64\xc8\xc4\x72\x0e\xad\xa6\xef\xd9\xb7\x69\x14\x39\x63\xef\x50\x03\x2f\x8f\x97\xfe\x49\xd5\xf3\xbe\x3f\x0d\xf9\xbe\x6c\xa0\x39\x37\x44\x69\x1a\x8e\x0d\x1c\xfe\x91\xc7\x49\xaa\xc0\x27\x35\x0d\x9e\x1b\xc6\x98\xff\xea\x37\xfa\x74\x76\x2d\x26\x5f\xa1\x27\x06\x88\xc0\xb9\x42\x8e\x08\x19\x32\xa1\xf8\x8d\xdb\xf8\x8b\x0c\x2c\xed\x09\xc0\xc8\xe7\x1e\x71\x19\x3a\xf2\x54\x0e\xf8\xea\x23\xe0\xd2\xc1\x29\xb1\xe8\x66\xc1\xcb\xaf\xcf\x9c\x79\x85\x38\xcc\xa6\x6e\xf0\xa6\x2e\x0e\xa5\xe4\x7b\x47\x5f\x7f\x7b\xf3\x48\x40\xc4\x54\x6a\x46\x98\xb1\xae\x19\xe7\x37\x70\xdf\x97\xc2\xca\x72\xd3\xfa\x7a\x67\x34\xfb\x24\x50\xa2\x6f\x8b\x33\x4c\x53\x56\x1a\xf7\xda\xb6\xbf\x39\x96\xff\x76\xe6\xab\xa0\xb5\xba\xfc\x22\xb3\xde\x45\x04\xe7\xf2\x7d\xd5\x2d\x30\x37\xc5\xf3\xee\xe6\x9f\x00\x00\x00\xff\xff\x60\x27\x3a\x8d\xec\x19\x00\x00")
 
 func clusterDevelopmentPredeployJsonBytes() ([]byte, error) {
@@ -548,6 +569,7 @@ func AssetNames() []string {
 
 // _bindata is a table, holding each asset generator, mapped to its name.
 var _bindata = map[string]func() (*asset, error){
+	"aks-development.json":                         aksDevelopmentJson,
 	"cluster-development-predeploy.json":           clusterDevelopmentPredeployJson,
 	"databases-development.json":                   databasesDevelopmentJson,
 	"env-development.json":                         envDevelopmentJson,
@@ -611,6 +633,7 @@ type bintree struct {
 }
 
 var _bintree = &bintree{nil, map[string]*bintree{
+	"aks-development.json":                         {aksDevelopmentJson, map[string]*bintree{}},
 	"cluster-development-predeploy.json":           {clusterDevelopmentPredeployJson, map[string]*bintree{}},
 	"databases-development.json":                   {databasesDevelopmentJson, map[string]*bintree{}},
 	"env-development.json":                         {envDevelopmentJson, map[string]*bintree{}},
diff --git a/pkg/operator/controllers/machinehealthcheck/bindata.go b/pkg/operator/controllers/machinehealthcheck/bindata.go
deleted file mode 100644
index e3e0183a5..000000000
--- a/pkg/operator/controllers/machinehealthcheck/bindata.go
+++ /dev/null
@@ -1,267 +0,0 @@
-// Code generated for package machinehealthcheck by go-bindata DO NOT EDIT. (@generated)
-// sources:
-// staticresources/machinehealthcheck.yaml
-// staticresources/mhcremediationalert.yaml
-package machinehealthcheck
-
-import (
-	"bytes"
-	"compress/gzip"
-	"fmt"
-	"io"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-	"time"
-)
-
-func bindataRead(data []byte, name string) ([]byte, error) {
-	gz, err := gzip.NewReader(bytes.NewBuffer(data))
-	if err != nil {
-		return nil, fmt.Errorf("Read %q: %v", name, err)
-	}
-
-	var buf bytes.Buffer
-	_, err = io.Copy(&buf, gz)
-	clErr := gz.Close()
-
-	if err != nil {
-		return nil, fmt.Errorf("Read %q: %v", name, err)
-	}
-	if clErr != nil {
-		return nil, err
-	}
-
-	return buf.Bytes(), nil
-}
-
-type asset struct {
-	bytes []byte
-	info  os.FileInfo
-}
-
-type bindataFileInfo struct {
-	name    string
-	size    int64
-	mode    os.FileMode
-	modTime time.Time
-}
-
-// Name return file name
-func (fi bindataFileInfo) Name() string {
-	return fi.name
-}
-
-// Size return file size
-func (fi bindataFileInfo) Size() int64 {
-	return fi.size
-}
-
-// Mode return file mode
-func (fi bindataFileInfo) Mode() os.FileMode {
-	return fi.mode
-}
-
-// Mode return file modify time
-func (fi bindataFileInfo) ModTime() time.Time {
-	return fi.modTime
-}
-
-// IsDir return file whether a directory
-func (fi bindataFileInfo) IsDir() bool {
-	return fi.mode&os.ModeDir != 0
-}
-
-// Sys return file is sys mode
-func (fi bindataFileInfo) Sys() interface{} {
-	return nil
-}
-
-var _machinehealthcheckYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x9c\x90\xb1\x6e\xf3\x30\x0c\x84\x77\x3f\x05\xa1\xdd\xf9\x13\xfc\x9b\xd6\x20\x45\x3b\xb4\x43\x81\x74\x67\x65\x06\x16\x2c\x51\x82\x48\xa7\xf1\xdb\x17\x8a\xe2\x74\x28\xba\x64\x93\x4e\xc7\xef\x4e\xc4\xec\x3f\xa8\x88\x4f\x6c\x21\xa2\x1b\x3d\xd3\x26\x65\x62\x19\xfd\x49\x37\x3e\xfd\x3b\xef\x3e\x49\x71\xd7\x4d\x9e\x07\x0b\xaf\xcd\xf2\x4c\x18\x74\xdc\x8f\xe4\xa6\x2e\x92\xe2\x80\x8a\xb6\x03\x60\x8c\x64\x01\x4b\xea\x6f\xac\xf1\x6a\x74\x57\x63\x7b\x96\x8c\x8e\x2c\xdc\x33\x56\x67\x8f\xd9\x77\x92\xc9\x55\x8e\x50\x20\xa7\xa9\xd4\x33\x40\x44\x75\xe3\xe1\x92\x0b\x49\x2d\x2a\x4d\xed\x61\xa2\xe5\x8f\xd2\x2e\xcc\xa2\x54\x2a\xf3\xce\x2f\x29\xd0\x75\x10\x6a\x78\xc1\x8a\x87\xb7\xa4\x2f\x7c\x53\xcf\x18\x66\xba\xc1\x2b\xde\x78\x3e\x15\x34\x3f\xf7\x88\x15\x6a\x1e\x49\x17\xd2\x5f\xd9\x87\x8b\x17\x95\x0e\x60\xe6\xb6\xa6\x65\x9f\x78\xf0\xba\x7e\xb1\x07\x5d\x32\x59\x30\xef\x84\xc3\xd2\x62\xd5\x47\x4a\xb3\x5a\x30\xff\xb7\x5b\x69\x9a\x28\xea\x2c\x16\xcc\x13\x06\x21\xf3\xc8\xe4\x91\x27\x4e\x5f\x5c\xd5\x88\x97\xe3\xda\xc7\x82\xd9\x99\xee\x3b\x00\x00\xff\xff\x38\x97\xb5\xaf\x23\x02\x00\x00")
-
-func machinehealthcheckYamlBytes() ([]byte, error) {
-	return bindataRead(
-		_machinehealthcheckYaml,
-		"machinehealthcheck.yaml",
-	)
-}
-
-func machinehealthcheckYaml() (*asset, error) {
-	bytes, err := machinehealthcheckYamlBytes()
-	if err != nil {
-		return nil, err
-	}
-
-	info := bindataFileInfo{name: "machinehealthcheck.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
-	a := &asset{bytes: bytes, info: info}
-	return a, nil
-}
-
-var _mhcremediationalertYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x74\x91\x4d\x8b\x14\x4d\x10\x84\xef\xfd\x2b\xe2\xf8\xbe\x87\x5e\x5d\x0f\x22\x75\x10\x44\x84\xbd\x2c\xc8\x08\x5e\x44\x86\x9c\xea\x70\xaa\x98\xfa\x68\x32\xb3\x67\xdd\x7f\x2f\xdd\xd3\xea\x5e\xcc\x63\x55\x3e\x11\x4f\x51\x32\xe7\xaf\x54\xcb\xbd\x05\xd4\xde\xb2\x77\xcd\xed\x7c\x17\xbb\xb2\xdb\x5d\xec\xf5\xd5\xf5\x7e\xb8\xe4\x36\x05\x7c\xd6\x5e\xe9\x89\x8b\x1d\x96\xc2\xa1\xd2\x65\x12\x97\x30\x00\x4d\x2a\x03\x6a\x8a\xa3\xb2\x72\xca\xe2\xb9\xb7\x51\x0a\xd5\xf7\x5b\x9b\x25\x32\xa0\xcf\x6c\x96\xf2\x0f\x1f\xab\xc4\x94\x1b\x47\x99\xf3\x00\x14\x39\xb1\xd8\x1a\x05\xcc\x7f\x7a\x02\x2e\xef\x6c\x3b\xd3\x5e\x18\xb0\x05\x8e\xba\x14\xda\x60\x33\xe3\xba\x7f\xd6\xbe\xcc\x1b\x39\xee\x1a\xa6\x1c\xff\xa5\x02\x6c\xf4\xad\x68\xbc\x05\x06\x7c\x39\x7c\x7a\xbc\xe9\x3c\x50\x8a\xa7\x8f\x89\xf1\x72\xf8\x8b\x1f\xc4\xf9\x90\xcf\x69\xa3\x00\xfe\x9c\x35\x20\xb7\xa8\x14\xe3\x7f\x55\xe6\x7c\xdc\x9f\x93\x36\x3e\xae\xfc\xf1\x45\xff\xd1\x96\x18\x69\x76\xf4\xee\x52\xf0\xed\xed\xeb\xfa\xfd\x7f\xbc\xc7\xfd\x9e\xf8\xa1\xb5\xee\xdb\xe6\xae\xb6\xce\x23\xcd\xe4\xcc\x80\xa7\xae\x17\x2a\x5a\x9f\x68\x48\x72\x25\x4e\x64\xc3\xef\x7c\x4e\x78\x83\xae\xa8\x5d\x09\xcf\x95\x86\xdc\xe0\x89\x28\x62\x8e\xd4\x17\x85\xa7\x6c\xa8\xf2\x8c\xdc\xa6\x1c\xc5\x09\x69\x58\x9a\xb9\x9c\x0a\xb7\x82\xd2\x65\x82\x2e\xad\xe5\x76\x46\xbf\xf1\xb1\x2c\xe6\xd4\xdd\xe8\xe5\x1f\xad\x63\xbc\x52\xb3\x3f\x07\x3c\x89\xae\xd8\xf0\x2b\x00\x00\xff\xff\x9e\xca\xa1\xb4\x4d\x02\x00\x00")
-
-func mhcremediationalertYamlBytes() ([]byte, error) {
-	return bindataRead(
-		_mhcremediationalertYaml,
-		"mhcremediationalert.yaml",
-	)
-}
-
-func mhcremediationalertYaml() (*asset, error) {
-	bytes, err := mhcremediationalertYamlBytes()
-	if err != nil {
-		return nil, err
-	}
-
-	info := bindataFileInfo{name: "mhcremediationalert.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)}
-	a := &asset{bytes: bytes, info: info}
-	return a, nil
-}
-
-// Asset loads and returns the asset for the given name.
-// It returns an error if the asset could not be found or
-// could not be loaded.
-func Asset(name string) ([]byte, error) {
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
-	if f, ok := _bindata[cannonicalName]; ok {
-		a, err := f()
-		if err != nil {
-			return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
-		}
-		return a.bytes, nil
-	}
-	return nil, fmt.Errorf("Asset %s not found", name)
-}
-
-// MustAsset is like Asset but panics when Asset would return an error.
-// It simplifies safe initialization of global variables.
-func MustAsset(name string) []byte {
-	a, err := Asset(name)
-	if err != nil {
-		panic("asset: Asset(" + name + "): " + err.Error())
-	}
-
-	return a
-}
-
-// AssetInfo loads and returns the asset info for the given name.
-// It returns an error if the asset could not be found or
-// could not be loaded.
-func AssetInfo(name string) (os.FileInfo, error) {
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
-	if f, ok := _bindata[cannonicalName]; ok {
-		a, err := f()
-		if err != nil {
-			return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
-		}
-		return a.info, nil
-	}
-	return nil, fmt.Errorf("AssetInfo %s not found", name)
-}
-
-// AssetNames returns the names of the assets.
-func AssetNames() []string {
-	names := make([]string, 0, len(_bindata))
-	for name := range _bindata {
-		names = append(names, name)
-	}
-	return names
-}
-
-// _bindata is a table, holding each asset generator, mapped to its name.
-var _bindata = map[string]func() (*asset, error){
-	"machinehealthcheck.yaml":  machinehealthcheckYaml,
-	"mhcremediationalert.yaml": mhcremediationalertYaml,
-}
-
-// AssetDir returns the file names below a certain
-// directory embedded in the file by go-bindata.
-// For example if you run go-bindata on data/... and data contains the
-// following hierarchy:
-//     data/
-//       foo.txt
-//       img/
-//         a.png
-//         b.png
-// then AssetDir("data") would return []string{"foo.txt", "img"}
-// AssetDir("data/img") would return []string{"a.png", "b.png"}
-// AssetDir("foo.txt") and AssetDir("notexist") would return an error
-// AssetDir("") will return []string{"data"}.
-func AssetDir(name string) ([]string, error) {
-	node := _bintree
-	if len(name) != 0 {
-		cannonicalName := strings.Replace(name, "\\", "/", -1)
-		pathList := strings.Split(cannonicalName, "/")
-		for _, p := range pathList {
-			node = node.Children[p]
-			if node == nil {
-				return nil, fmt.Errorf("Asset %s not found", name)
-			}
-		}
-	}
-	if node.Func != nil {
-		return nil, fmt.Errorf("Asset %s not found", name)
-	}
-	rv := make([]string, 0, len(node.Children))
-	for childName := range node.Children {
-		rv = append(rv, childName)
-	}
-	return rv, nil
-}
-
-type bintree struct {
-	Func     func() (*asset, error)
-	Children map[string]*bintree
-}
-
-var _bintree = &bintree{nil, map[string]*bintree{
-	"machinehealthcheck.yaml":  {machinehealthcheckYaml, map[string]*bintree{}},
-	"mhcremediationalert.yaml": {mhcremediationalertYaml, map[string]*bintree{}},
-}}
-
-// RestoreAsset restores an asset under the given directory
-func RestoreAsset(dir, name string) error {
-	data, err := Asset(name)
-	if err != nil {
-		return err
-	}
-	info, err := AssetInfo(name)
-	if err != nil {
-		return err
-	}
-	err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
-	if err != nil {
-		return err
-	}
-	err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
-	if err != nil {
-		return err
-	}
-	err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
-// RestoreAssets restores an asset under the given directory recursively
-func RestoreAssets(dir, name string) error {
-	children, err := AssetDir(name)
-	// File
-	if err != nil {
-		return RestoreAsset(dir, name)
-	}
-	// Dir
-	for _, child := range children {
-		err = RestoreAssets(dir, filepath.Join(name, child))
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func _filePath(dir, name string) string {
-	cannonicalName := strings.Replace(name, "\\", "/", -1)
-	return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
-}
diff --git a/pkg/operator/controllers/machinehealthcheck/generate.go b/pkg/operator/controllers/machinehealthcheck/generate.go
deleted file mode 100644
index 9fd716667..000000000
--- a/pkg/operator/controllers/machinehealthcheck/generate.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package machinehealthcheck
-
-// Copyright (c) Microsoft Corporation.
-// Licensed under the Apache License 2.0.
-
-//go:generate go run ../../../../vendor/github.com/go-bindata/go-bindata/go-bindata -nometadata -pkg $GOPACKAGE -prefix staticresources staticresources/...
-//go:generate gofmt -s -l -w bindata.go
diff --git a/pkg/operator/controllers/machinehealthcheck/machinehealthcheck_controller.go b/pkg/operator/controllers/machinehealthcheck/machinehealthcheck_controller.go
index f52461a9d..76b7b1652 100644
--- a/pkg/operator/controllers/machinehealthcheck/machinehealthcheck_controller.go
+++ b/pkg/operator/controllers/machinehealthcheck/machinehealthcheck_controller.go
@@ -5,13 +5,19 @@ package machinehealthcheck
 
 import (
 	"context"
+	_ "embed"
+	"strings"
 	"time"
 
 	machinev1beta1 "github.com/openshift/api/machine/v1beta1"
+	monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	kruntime "k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes/scheme"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 
 	arov1alpha1 "github.com/Azure/ARO-RP/pkg/operator/apis/aro.openshift.io/v1alpha1"
@@ -19,6 +25,12 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/dynamichelper"
 )
 
+//go:embed staticresources/machinehealthcheck.yaml
+var machinehealthcheckYaml []byte
+
+//go:embed staticresources/mhcremediationalert.yaml
+var mhcremediationalertYaml []byte
+
 const (
 	ControllerName string = "MachineHealthCheck"
 	managed        string = "aro.machinehealthcheck.managed"
@@ -62,15 +74,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.
 
 	var resources []kruntime.Object
 
-	// this loop prevents us from hard coding resource strings
-	// and ensures all static resources are accounted for.
-	for _, assetName := range AssetNames() {
-		b, err := Asset(assetName)
-		if err != nil {
-			return reconcile.Result{}, err
-		}
-
-		resource, _, err := scheme.Codecs.UniversalDeserializer().Decode(b, nil, nil)
+	for _, asset := range [][]byte{machinehealthcheckYaml, mhcremediationalertYaml} {
+		resource, _, err := scheme.Codecs.UniversalDeserializer().Decode(asset, nil, nil)
 		if err != nil {
 			return reconcile.Result{}, err
 		}
@@ -101,8 +106,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.
 
 // SetupWithManager will manage only our MHC resource with our specific controller name
 func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
+	aroClusterPredicate := predicate.NewPredicateFuncs(func(o client.Object) bool {
+		return strings.EqualFold(arov1alpha1.SingletonClusterName, o.GetName())
+	})
+
 	return ctrl.NewControllerManagedBy(mgr).
-		For(&machinev1beta1.MachineHealthCheck{}).
+		For(&arov1alpha1.Cluster{}, builder.WithPredicates(aroClusterPredicate)).
 		Named(ControllerName).
+		Owns(&machinev1beta1.MachineHealthCheck{}).
+		Owns(&monitoringv1.PrometheusRule{}).
 		Complete(r)
 }
diff --git a/pkg/util/namespace/namespace.go b/pkg/util/namespace/namespace.go
index 3b9a98979..dbb4952b1 100644
--- a/pkg/util/namespace/namespace.go
+++ b/pkg/util/namespace/namespace.go
@@ -66,6 +66,7 @@ func IsOpenShiftNamespace(ns string) bool {
 		"openshift-oauth-apiserver":                        {},
 		"openshift-openstack-infra":                        {},
 		"openshift-operators":                              {},
+		"openshift-operator-lifecycle-manager":             {},
 		"openshift-ovirt-infra":                            {},
 		"openshift-sdn":                                    {},
 		"openshift-service-ca":                             {},
diff --git a/python/az/aro/azext_aro/_validators.py b/python/az/aro/azext_aro/_validators.py
index fc76d41f1..10764b9f1 100644
--- a/python/az/aro/azext_aro/_validators.py
+++ b/python/az/aro/azext_aro/_validators.py
@@ -24,70 +24,76 @@ logger = get_logger(__name__)
 def validate_cidr(key):
     def _validate_cidr(namespace):
         cidr = getattr(namespace, key)
-        if cidr is not None:
-            try:
-                ipaddress.IPv4Network(cidr)
-            except ValueError as e:
-                raise InvalidArgumentValueError(f"Invalid --{key.replace('_', '-')} '{cidr}'.") from e
+        if cidr is None:
+            return
+        try:
+            ipaddress.IPv4Network(cidr)
+        except ValueError as e:
+            raise InvalidArgumentValueError(f"Invalid --{key.replace('_', '-')} '{cidr}'.") from e
 
     return _validate_cidr
 
 
 def validate_client_id(namespace):
-    if namespace.client_id is not None:
-        try:
-            uuid.UUID(namespace.client_id)
-        except ValueError as e:
-            raise InvalidArgumentValueError(f"Invalid --client-id '{namespace.client_id}'.") from e
+    if namespace.client_id is None:
+        return
+    try:
+        uuid.UUID(namespace.client_id)
+    except ValueError as e:
+        raise InvalidArgumentValueError(f"Invalid --client-id '{namespace.client_id}'.") from e
 
-        if namespace.client_secret is None or not str(namespace.client_secret):
-            raise RequiredArgumentMissingError('Must specify --client-secret with --client-id.')
+    if namespace.client_secret is None or not str(namespace.client_secret):
+        raise RequiredArgumentMissingError('Must specify --client-secret with --client-id.')
 
 
 def validate_client_secret(isCreate):
     def _validate_client_secret(namespace):
-        if isCreate and namespace.client_secret is not None:
-            if namespace.client_id is None or not str(namespace.client_id):
-                raise RequiredArgumentMissingError('Must specify --client-id with --client-secret.')
+        if not isCreate or namespace.client_secret is None:
+            return
+        if namespace.client_id is None or not str(namespace.client_id):
+            raise RequiredArgumentMissingError('Must specify --client-id with --client-secret.')
 
     return _validate_client_secret
 
 
 def validate_cluster_resource_group(cmd, namespace):
-    if namespace.cluster_resource_group is not None:
-        client = get_mgmt_service_client(
-            cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)
+    if namespace.cluster_resource_group is None:
+        return
+    client = get_mgmt_service_client(
+        cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES)
 
-        if client.resource_groups.check_existence(namespace.cluster_resource_group):
-            raise InvalidArgumentValueError(
-                f"Invalid --cluster-resource-group '{namespace.cluster_resource_group}':"
-                " resource group must not exist.")
+    if client.resource_groups.check_existence(namespace.cluster_resource_group):
+        raise InvalidArgumentValueError(
+            f"Invalid --cluster-resource-group '{namespace.cluster_resource_group}':"
+            " resource group must not exist.")
 
 
 def validate_disk_encryption_set(cmd, namespace):
-    if namespace.disk_encryption_set is not None:
-        if not is_valid_resource_id(namespace.disk_encryption_set):
-            raise InvalidArgumentValueError(
-                f"Invalid --disk-encryption-set '{namespace.disk_encryption_set}', has to be a resource ID.")
-
-        desid = parse_resource_id(namespace.disk_encryption_set)
-        compute_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_COMPUTE)
-        try:
-            compute_client.disk_encryption_sets.get(resource_group_name=desid['resource_group'],
-                                                    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" {str(err)}") from err
+    if namespace.disk_encryption_set is None:
+        return
+    if not is_valid_resource_id(namespace.disk_encryption_set):
+        raise InvalidArgumentValueError(
+            f"Invalid --disk-encryption-set '{namespace.disk_encryption_set}', has to be a resource ID.")
+
+    desid = parse_resource_id(namespace.disk_encryption_set)
+    compute_client = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_COMPUTE)
+    try:
+        compute_client.disk_encryption_sets.get(resource_group_name=desid['resource_group'],
+                                                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" {str(err)}") from err
 
 
 def validate_domain(namespace):
-    if namespace.domain is not None:
-        if not re.match(r'^' +
-                        r'([a-z0-9]|[a-z0-9][-a-z0-9]{0,61}[a-z0-9])' +
-                        r'(\.([a-z0-9]|[a-z0-9][-a-z0-9]{0,61}[a-z0-9]))*' +
-                        r'$', namespace.domain):
-            raise InvalidArgumentValueError(f"Invalid --domain '{namespace.domain}'.")
+    if namespace.domain is None:
+        return
+    if not re.match(r'^' +
+                    r'([a-z0-9]|[a-z0-9][-a-z0-9]{0,61}[a-z0-9])' +
+                    r'(\.([a-z0-9]|[a-z0-9][-a-z0-9]{0,61}[a-z0-9]))*' +
+                    r'$', namespace.domain):
+        raise InvalidArgumentValueError(f"Invalid --domain '{namespace.domain}'.")
 
 
 def validate_pull_secret(namespace):
@@ -97,20 +103,23 @@ def validate_pull_secret(namespace):
             "Red Hat or from certified partners."
 
         logger.warning(warning)
+        return
 
-    else:
-        try:
-            if not isinstance(json.loads(namespace.pull_secret), dict):
-                raise Exception()
-        except Exception as e:
-            raise InvalidArgumentValueError("Invalid --pull-secret.") from e
+    try:
+        if not isinstance(json.loads(namespace.pull_secret), dict):
+            raise Exception()
+    except Exception as e:
+        raise InvalidArgumentValueError("Invalid --pull-secret.") from e
 
 
 def validate_sdn(namespace):
-    if namespace.software_defined_network is not None:
-        if namespace.software_defined_network not in ['OVNKubernetes', 'OpenshiftSDN']:
-            raise InvalidArgumentValueError(
-                f"Invalid --software-defined-network '{namespace.software_defined_network}'.")
+    if namespace.software_defined_network is None:
+        return
+
+    target_values = ['OVNKubernetes', 'OpenshiftSDN']
+    if namespace.software_defined_network not in target_values:
+        raise InvalidArgumentValueError(
+            f"Invalid --software-defined-network '{namespace.software_defined_network}'.")
 
 
 def validate_subnet(key):
@@ -132,15 +141,18 @@ def validate_subnet(key):
             raise InvalidArgumentValueError(
                 f"--{key.replace('_', '-')} subscription '{parts['subscription']}' must equal cluster subscription.")
 
-        if parts['namespace'].lower() != 'microsoft.network':
+        expected_namespace = 'microsoft.network'
+        if parts['namespace'].lower() != expected_namespace:
             raise InvalidArgumentValueError(
                 f"--{key.replace('_', '-')} namespace '{parts['namespace']}' must equal Microsoft.Network.")
 
-        if parts['type'].lower() != 'virtualnetworks':
+        expected_type = 'virtualnetworks'
+        if parts['type'].lower() != expected_type:
             raise InvalidArgumentValueError(
                 f"--{key.replace('_', '-')} type '{parts['type']}' must equal virtualNetworks.")
 
-        if parts['last_child_num'] != 1:
+        expected_last_child_num = 1
+        if parts['last_child_num'] != expected_last_child_num:
             raise InvalidArgumentValueError(f"--{key.replace('_', '-')} '{subnet}' must have one child.")
 
         if 'child_namespace_1' in parts:
@@ -185,10 +197,13 @@ def validate_subnets(master_subnet, worker_subnet):
 def validate_visibility(key):
     def _validate_visibility(namespace):
         visibility = getattr(namespace, key)
-        if visibility is not None:
-            visibility = visibility.capitalize()
-            if visibility not in ['Private', 'Public']:
-                raise InvalidArgumentValueError(f"Invalid --{key.replace('_', '-')} '{visibility}'.")
+        if visibility is None:
+            return
+        visibility = visibility.capitalize()
+
+        possible_visibilities = ['Private', 'Public']
+        if visibility not in possible_visibilities:
+            raise InvalidArgumentValueError(f"Invalid --{key.replace('_', '-')} '{visibility}'.")
 
     return _validate_visibility
 
@@ -196,15 +211,16 @@ def validate_visibility(key):
 def validate_vnet(cmd, namespace):
     validate_vnet_resource_group_name(namespace)
 
-    if namespace.vnet:
-        if not is_valid_resource_id(namespace.vnet):
-            namespace.vnet = resource_id(
-                subscription=get_subscription_id(cmd.cli_ctx),
-                resource_group=namespace.vnet_resource_group_name,
-                namespace='Microsoft.Network',
-                type='virtualNetworks',
-                name=namespace.vnet,
-            )
+    if not namespace.vnet:
+        return
+    if not is_valid_resource_id(namespace.vnet):
+        namespace.vnet = resource_id(
+            subscription=get_subscription_id(cmd.cli_ctx),
+            resource_group=namespace.vnet_resource_group_name,
+            namespace='Microsoft.Network',
+            type='virtualNetworks',
+            name=namespace.vnet,
+        )
 
 
 def validate_vnet_resource_group_name(namespace):
@@ -213,18 +229,27 @@ def validate_vnet_resource_group_name(namespace):
 
 
 def validate_worker_count(namespace):
-    if namespace.worker_count:
-        if namespace.worker_count < 3:
-            raise InvalidArgumentValueError('--worker-count must be greater than or equal to 3.')
+    if not namespace.worker_count:
+        return
+
+    minimum_workers_count = 3
+    if namespace.worker_count < minimum_workers_count:
+        raise InvalidArgumentValueError('--worker-count must be greater than or equal to ' + str(minimum_workers_count))
 
 
 def validate_worker_vm_disk_size_gb(namespace):
-    if namespace.worker_vm_disk_size_gb:
-        if namespace.worker_vm_disk_size_gb < 128:
-            raise InvalidArgumentValueError('--worker-vm-disk-size-gb must be greater than or equal to 128.')
+    if not namespace.worker_vm_disk_size_gb:
+        return
+
+    minimum_worker_vm_disk_size_gb = 128
+    if namespace.worker_vm_disk_size_gb < minimum_worker_vm_disk_size_gb:
+        error_msg = '--worker-vm-disk-size-gb must be greater than or equal to ' + str(minimum_worker_vm_disk_size_gb)
+
+        raise InvalidArgumentValueError(error_msg)
 
 
 def validate_refresh_cluster_credentials(namespace):
-    if namespace.refresh_cluster_credentials:
-        if namespace.client_secret is not None or namespace.client_id is not None:
-            raise RequiredArgumentMissingError('--client-id and --client-secret must be not set with --refresh-credentials.')  # pylint: disable=line-too-long
+    if not namespace.refresh_cluster_credentials:
+        return
+    if namespace.client_secret is not None or namespace.client_id is not None:
+        raise RequiredArgumentMissingError('--client-id and --client-secret must be not set with --refresh-credentials.')  # pylint: disable=line-too-long
diff --git a/vendor/github.com/coreos/ignition/v2/config/shared/errors/errors.go b/vendor/github.com/coreos/ignition/v2/config/shared/errors/errors.go
index 7761280d0..492fd7e6c 100644
--- a/vendor/github.com/coreos/ignition/v2/config/shared/errors/errors.go
+++ b/vendor/github.com/coreos/ignition/v2/config/shared/errors/errors.go
@@ -62,6 +62,7 @@ var (
 	ErrClevisCustomWithOthers    = errors.New("cannot use custom clevis config with tpm2, tang, or threshold")
 	ErrTangThumbprintRequired    = errors.New("thumbprint is required")
 	ErrFileIllegalMode           = errors.New("illegal file mode")
+	ErrModeSpecialBits           = errors.New("setuid/setgid/sticky bits are not supported in spec versions older than 3.4.0")
 	ErrBothIDAndNameSet          = errors.New("cannot set both id and name")
 	ErrLabelTooLong              = errors.New("partition labels may not exceed 36 characters")
 	ErrDoesntMatchGUIDRegex      = errors.New("doesn't match the form \"01234567-89AB-CDEF-EDCB-A98765432101\"")
@@ -99,6 +100,7 @@ var (
 	ErrEngineConfiguration             = errors.New("engine incorrectly configured")
 
 	// AWS S3 specific errors
+	ErrInvalidS3ARN             = errors.New("invalid S3 ARN format")
 	ErrInvalidS3ObjectVersionId = errors.New("invalid S3 object VersionId")
 
 	// Obsolete errors, left here for ABI compatibility
diff --git a/vendor/github.com/coreos/ignition/v2/config/v3_2/types/directory.go b/vendor/github.com/coreos/ignition/v2/config/v3_2/types/directory.go
index f6f068455..b01a6bf9d 100644
--- a/vendor/github.com/coreos/ignition/v2/config/v3_2/types/directory.go
+++ b/vendor/github.com/coreos/ignition/v2/config/v3_2/types/directory.go
@@ -22,5 +22,6 @@ import (
 func (d Directory) Validate(c path.ContextPath) (r report.Report) {
 	r.Merge(d.Node.Validate(c))
 	r.AddOnError(c.Append("mode"), validateMode(d.Mode))
+	r.AddOnWarn(c.Append("mode"), validateModeSpecialBits(d.Mode))
 	return
 }
diff --git a/vendor/github.com/coreos/ignition/v2/config/v3_2/types/file.go b/vendor/github.com/coreos/ignition/v2/config/v3_2/types/file.go
index 9b71bb26a..4e7566bd3 100644
--- a/vendor/github.com/coreos/ignition/v2/config/v3_2/types/file.go
+++ b/vendor/github.com/coreos/ignition/v2/config/v3_2/types/file.go
@@ -25,6 +25,7 @@ import (
 func (f File) Validate(c path.ContextPath) (r report.Report) {
 	r.Merge(f.Node.Validate(c))
 	r.AddOnError(c.Append("mode"), validateMode(f.Mode))
+	r.AddOnWarn(c.Append("mode"), validateModeSpecialBits(f.Mode))
 	r.AddOnError(c.Append("overwrite"), f.validateOverwrite())
 	return
 }
diff --git a/vendor/github.com/coreos/ignition/v2/config/v3_2/types/mode.go b/vendor/github.com/coreos/ignition/v2/config/v3_2/types/mode.go
index 9eb7573d8..ad3e51c22 100644
--- a/vendor/github.com/coreos/ignition/v2/config/v3_2/types/mode.go
+++ b/vendor/github.com/coreos/ignition/v2/config/v3_2/types/mode.go
@@ -24,3 +24,13 @@ func validateMode(m *int) error {
 	}
 	return nil
 }
+
+func validateModeSpecialBits(m *int) error {
+	if m != nil {
+		mode := uint32(*m)
+		if mode&07000 != 0 {
+			return errors.ErrModeSpecialBits
+		}
+	}
+	return nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 191810da7..bcafd9a5e 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -355,7 +355,7 @@ github.com/coreos/ignition/config/shared/errors
 github.com/coreos/ignition/config/shared/validations
 github.com/coreos/ignition/config/v2_2/types
 github.com/coreos/ignition/config/validate/report
-# github.com/coreos/ignition/v2 v2.13.0
+# github.com/coreos/ignition/v2 v2.14.0
 ## explicit; go 1.15
 github.com/coreos/ignition/v2/config/shared/errors
 github.com/coreos/ignition/v2/config/shared/validations


Test patch:
diff --git a/.pipelines/e2e-with-billing.yml b/.pipelines/e2e-with-billing.yml
index 45257cf42..0a18c3e87 100644
--- a/.pipelines/e2e-with-billing.yml
+++ b/.pipelines/e2e-with-billing.yml
@@ -48,7 +48,16 @@ stages:
     - script: |
         # Pass variables between tasks: https://medium.com/microsoftazure/how-to-pass-variables-in-azure-pipelines-yaml-tasks-5c81c5d31763
         echo "##vso[task.setvariable variable=REGION]$LOCATION"
-        CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
+        # TODO: Remove this hack after AvailabilitySet name too long bug is fixed.
+        NONZONAL_REGIONS="australiacentral australiacentral2 australiasoutheast brazilsoutheast canadaeast japanwest northcentralus norwaywest southindia switzerlandwest uaenorth ukwest westcentralus westus"
+        if echo $NONZONAL_REGIONS | grep -wq $LOCATION
+        then
+            CLUSTER=$(head -c 19 <<< "v4-e2e-V$BUILD_BUILDID-$LOCATION")
+        else
+            CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
+        fi
+        # TODO: Uncomment next line after above hack is removed.
+        # CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
         echo "##vso[task.setvariable variable=CLUSTER]$CLUSTER"
         CLUSTER_RESOURCEGROUP="v4-e2e-V$BUILD_BUILDID-$LOCATION"
         echo "##vso[task.setvariable variable=CLUSTER_RESOURCEGROUP]$CLUSTER_RESOURCEGROUP"
diff --git a/.pipelines/templates/template-prod-e2e-steps.yml b/.pipelines/templates/template-prod-e2e-steps.yml
index 64b31668c..18f82dde4 100644
--- a/.pipelines/templates/template-prod-e2e-steps.yml
+++ b/.pipelines/templates/template-prod-e2e-steps.yml
@@ -35,6 +35,14 @@ steps:
       export AZURE_FP_CLIENT_ID=f1dd0a37-89c6-4e07-bcd1-ffd3d43d8875
       export AZURE_FP_SERVICE_PRINCIPAL_ID=50c17c64-bc11-4fdd-a339-0ecd396bf911
     fi
+    # TODO: Remove this after AvailabilitySet name too long bug is fixed.
+    NONZONAL_REGIONS="australiacentral australiacentral2 australiasoutheast brazilsoutheast canadaeast japanwest northcentralus norwaywest southindia switzerlandwest uaenorth ukwest westcentralus westus"
+    if echo $NONZONAL_REGIONS | grep -wq $LOCATION
+    then
+        CLUSTER=$(head -c 19 <<< "v4-e2e-V$BUILD_BUILDID-$LOCATION")
+    else
+        CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
+    fi
     # TODO: remove --env RESOURCEGROUP after next RP deployment.
 
     # TODO: e2e.test arguments need to move inside the container somehow.  Maybe
@@ -50,7 +58,7 @@ steps:
       --env AZURE_SUBSCRIPTION_ID \
       --env LOCATION \
       --env RESOURCEGROUP="v4-e2e-V$BUILD_BUILDID-$LOCATION" \
-      --env CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION" \
+      --env CLUSTER="$CLUSTER" \
       --entrypoint e2e.test \
       "$IMAGE:$VERSION" \
       -test.timeout 180m -test.v -ginkgo.v
diff --git a/.pipelines/templates/template-rp-and-billing-e2e.yml b/.pipelines/templates/template-rp-and-billing-e2e.yml
index 27c729bcf..7a89ff6f8 100644
--- a/.pipelines/templates/template-rp-and-billing-e2e.yml
+++ b/.pipelines/templates/template-rp-and-billing-e2e.yml
@@ -43,7 +43,18 @@ stages:
     - script: |
         # Pass variables between tasks: https://medium.com/microsoftazure/how-to-pass-variables-in-azure-pipelines-yaml-tasks-5c81c5d31763
         echo "##vso[task.setvariable variable=REGION]${{ parameters.location }}"
-        CLUSTER="v4-e2e-V$BUILD_BUILDID-${{ parameters.location }}"
+
+        # TODO: Remove this hack after AvailabilitySet name too long bug is fixed.
+        LOCATION=${{ parameters.location }}
+        NONZONAL_REGIONS="australiacentral australiacentral2 australiasoutheast brazilsoutheast canadaeast japanwest northcentralus norwaywest southindia switzerlandwest uaenorth ukwest westcentralus westus"
+        if echo $NONZONAL_REGIONS | grep -wq $LOCATION
+        then
+            CLUSTER=$(head -c 19 <<< "v4-e2e-V$BUILD_BUILDID-$LOCATION")
+        else
+            CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
+        fi
+        # TODO: Uncomment next line after above hack is removed.
+        # CLUSTER="v4-e2e-V$BUILD_BUILDID-${{ parameters.location }}"
         echo "##vso[task.setvariable variable=CLUSTER]$CLUSTER"
         CLUSTER_RESOURCEGROUP="v4-e2e-V$BUILD_BUILDID-${{ parameters.location }}"
         echo "##vso[task.setvariable variable=CLUSTER_RESOURCEGROUP]$CLUSTER_RESOURCEGROUP"
diff --git a/hack/e2e/run-rp-and-e2e.sh b/hack/e2e/run-rp-and-e2e.sh
index 81aaff899..7d2638bf8 100755
--- a/hack/e2e/run-rp-and-e2e.sh
+++ b/hack/e2e/run-rp-and-e2e.sh
@@ -72,18 +72,28 @@ clean_e2e_db(){
 
 
 # if LOCAL_E2E is set, set the value with the local test names
-# If it it not set, it defaults to the build ID 
-if [ -z "${LOCAL_E2E}" ] ; then 
-    export CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
+# If it it not set, it defaults to the build ID
+if [ -z "${LOCAL_E2E}" ] ; then
+    # TODO: Remove this hack after AvailabilitySet name too long bug is fixed.
+    NONZONAL_REGIONS="australiacentral australiacentral2 australiasoutheast brazilsoutheast canadaeast japanwest northcentralus norwaywest southindia switzerlandwest uaenorth ukwest westcentralus westus"
+
+    if echo $NONZONAL_REGIONS | grep -wq $LOCATION
+    then
+        export CLUSTER=$(head -c 19 <<< "v4-e2e-V$BUILD_BUILDID-$LOCATION")
+    else
+        export CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
+    fi
+    # TODO: uncomment after above hack is removed.
+    # export CLUSTER="v4-e2e-V$BUILD_BUILDID-$LOCATION"
     export DATABASE_NAME="v4-e2e-V$BUILD_BUILDID-$LOCATION"
 fi
 
-if [ -z "${CLUSTER}" ] ; then 
+if [ -z "${CLUSTER}" ] ; then
     echo "CLUSTER is not set , aborting"
     exit 1
 fi
 
-if [ -z "${DATABASE_NAME}" ] ; then 
+if [ -z "${DATABASE_NAME}" ] ; then
     echo "DATABASE_NAME is not set , aborting"
     exit 1
 fi
diff --git a/hack/unit-test-python.sh b/hack/unit-test-python.sh
new file mode 100755
index 000000000..5d4668ceb
--- /dev/null
+++ b/hack/unit-test-python.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# This script executes Python unit tests. It first tries to activate the virtual environment, 
+# but if it does not exist it will be created and then activated.
+# When executing "pytest" command, it ignores integration test folder as 
+# this script is intended to just execute unit tests. 
+
+. pyenv/bin/activate || python3 -m venv pyenv && . pyenv/bin/activate
+pip install pytest
+cd python
+pytest --ignore=az/aro/azext_aro/tests/latest/integration
diff --git a/pkg/api/v20191231preview/openshiftcluster_validatestatic.go b/pkg/api/v20191231preview/openshiftcluster_validatestatic.go
index 1bde17ad1..7ec17f8ee 100644
--- a/pkg/api/v20191231preview/openshiftcluster_validatestatic.go
+++ b/pkg/api/v20191231preview/openshiftcluster_validatestatic.go
@@ -71,6 +71,13 @@ func (sv *openShiftClusterStaticValidator) validate(oc *OpenShiftCluster, isCrea
 		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)
 }
 
diff --git a/pkg/api/v20191231preview/openshiftcluster_validatestatic_test.go b/pkg/api/v20191231preview/openshiftcluster_validatestatic_test.go
index 71a948652..a095d7d6a 100644
--- a/pkg/api/v20191231preview/openshiftcluster_validatestatic_test.go
+++ b/pkg/api/v20191231preview/openshiftcluster_validatestatic_test.go
@@ -10,15 +10,19 @@ import (
 	"testing"
 
 	"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
@@ -34,15 +38,18 @@ const (
 
 var (
 	subscriptionID = "00000000-0000-0000-0000-000000000000"
-	id             = fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/microsoft.redhatopenshift/openshiftclusters/resourceName", subscriptionID)
 )
 
-func validOpenShiftCluster() *OpenShiftCluster {
+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 {
 	oc := &OpenShiftCluster{
-		ID:       id,
-		Name:     "resourceName",
+		ID:       getResourceID(name),
+		Name:     name,
 		Type:     "Microsoft.RedHatOpenShift/OpenShiftClusters",
-		Location: "location",
+		Location: location,
 		Tags: Tags{
 			"key": "value",
 		},
@@ -100,22 +107,31 @@ 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:            "location",
+					location:            *tt.location,
 					domain:              "location.aroapp.io",
 					requireD2sV3Workers: tt.requireD2sV3Workers,
-					resourceID:          id,
+					resourceID:          getResourceID(*tt.clusterName),
 					r: azure.Resource{
 						SubscriptionID: subscriptionID,
 						ResourceGroup:  "resourceGroup",
 						Provider:       "Microsoft.RedHatOpenShift",
 						ResourceType:   "openshiftClusters",
-						ResourceName:   "resourceName",
+						ResourceName:   *tt.clusterName,
 					},
 				}
 
 				validOCForTest := func() *OpenShiftCluster {
-					oc := validOpenShiftCluster()
+					oc := validOpenShiftCluster(*tt.clusterName, *tt.location)
 					if tt.current != nil {
 						tt.current(oc)
 					}
@@ -160,7 +176,11 @@ func runTests(t *testing.T, mode testMode, tests []*validateTest) {
 }
 
 func TestOpenShiftClusterStaticValidate(t *testing.T) {
-	tests := []*validateTest{
+	clusterName19 := "19characters-aaaaaa"
+	clusterName30 := "thisis30characterslong-aaaaaa"
+	nonZonalRegion := "australiasoutheast"
+
+	commonTests := []*validateTest{
 		{
 			name: "valid",
 		},
@@ -192,10 +212,38 @@ func TestOpenShiftClusterStaticValidate(t *testing.T) {
 			},
 			wantErr: "400: InvalidParameter: location: The provided location 'invalid' is invalid.",
 		},
+		{
+			name:        "valid - zonal regions can exceed max cluster name length",
+			clusterName: &clusterName30,
+		},
 	}
 
-	runTests(t, testModeCreate, tests)
-	runTests(t, testModeUpdate, tests)
+	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) {
diff --git a/pkg/api/v20200430/openshiftcluster_validatestatic.go b/pkg/api/v20200430/openshiftcluster_validatestatic.go
index c38b26902..8349aac9f 100644
--- a/pkg/api/v20200430/openshiftcluster_validatestatic.go
+++ b/pkg/api/v20200430/openshiftcluster_validatestatic.go
@@ -71,6 +71,13 @@ func (sv *openShiftClusterStaticValidator) validate(oc *OpenShiftCluster, isCrea
 		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)
 }
 
diff --git a/pkg/api/v20200430/openshiftcluster_validatestatic_test.go b/pkg/api/v20200430/openshiftcluster_validatestatic_test.go
index bdd6c077e..372af5b9c 100644
--- a/pkg/api/v20200430/openshiftcluster_validatestatic_test.go
+++ b/pkg/api/v20200430/openshiftcluster_validatestatic_test.go
@@ -10,15 +10,19 @@ import (
 	"testing"
 
 	"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
@@ -34,15 +38,18 @@ const (
 
 var (
 	subscriptionID = "00000000-0000-0000-0000-000000000000"
-	id             = fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/microsoft.redhatopenshift/openshiftclusters/resourceName", subscriptionID)
 )
 
-func validOpenShiftCluster() *OpenShiftCluster {
+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 {
 	oc := &OpenShiftCluster{
-		ID:       id,
-		Name:     "resourceName",
+		ID:       getResourceID(name),
+		Name:     name,
 		Type:     "Microsoft.RedHatOpenShift/OpenShiftClusters",
-		Location: "location",
+		Location: location,
 		Tags: Tags{
 			"key": "value",
 		},
@@ -100,22 +107,31 @@ 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:            "location",
+					location:            *tt.location,
 					domain:              "location.aroapp.io",
 					requireD2sV3Workers: tt.requireD2sV3Workers,
-					resourceID:          id,
+					resourceID:          getResourceID(*tt.clusterName),
 					r: azure.Resource{
 						SubscriptionID: subscriptionID,
 						ResourceGroup:  "resourceGroup",
 						Provider:       "Microsoft.RedHatOpenShift",
 						ResourceType:   "openshiftClusters",
-						ResourceName:   "resourceName",
+						ResourceName:   *tt.clusterName,
 					},
 				}
 
 				validOCForTest := func() *OpenShiftCluster {
-					oc := validOpenShiftCluster()
+					oc := validOpenShiftCluster(*tt.clusterName, *tt.location)
 					if tt.current != nil {
 						tt.current(oc)
 					}
@@ -160,7 +176,11 @@ func runTests(t *testing.T, mode testMode, tests []*validateTest) {
 }
 
 func TestOpenShiftClusterStaticValidate(t *testing.T) {
-	tests := []*validateTest{
+	clusterName19 := "19characters-aaaaaa"
+	clusterName30 := "thisis30characterslong-aaaaaa"
+	nonZonalRegion := "australiasoutheast"
+
+	commonTests := []*validateTest{
 		{
 			name: "valid",
 		},
@@ -192,10 +212,38 @@ func TestOpenShiftClusterStaticValidate(t *testing.T) {
 			},
 			wantErr: "400: InvalidParameter: location: The provided location 'invalid' is invalid.",
 		},
+		{
+			name:        "valid - zonal regions can exceed max cluster name length",
+			clusterName: &clusterName30,
+		},
 	}
 
-	runTests(t, testModeCreate, tests)
-	runTests(t, testModeUpdate, tests)
+	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) {
diff --git a/pkg/api/v20210901preview/openshiftcluster_validatestatic.go b/pkg/api/v20210901preview/openshiftcluster_validatestatic.go
index b73c6837e..324dc97ce 100644
--- a/pkg/api/v20210901preview/openshiftcluster_validatestatic.go
+++ b/pkg/api/v20210901preview/openshiftcluster_validatestatic.go
@@ -71,6 +71,13 @@ func (sv *openShiftClusterStaticValidator) validate(oc *OpenShiftCluster, isCrea
 		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)
 }
 
diff --git a/pkg/api/v20210901preview/openshiftcluster_validatestatic_test.go b/pkg/api/v20210901preview/openshiftcluster_validatestatic_test.go
index 5788b4d5f..4c497086c 100644
--- a/pkg/api/v20210901preview/openshiftcluster_validatestatic_test.go
+++ b/pkg/api/v20210901preview/openshiftcluster_validatestatic_test.go
@@ -11,15 +11,19 @@ import (
 	"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
@@ -35,20 +39,23 @@ const (
 
 var (
 	subscriptionID = "00000000-0000-0000-0000-000000000000"
-	id             = fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/microsoft.redhatopenshift/openshiftclusters/resourceName", subscriptionID)
 )
 
-func validOpenShiftCluster() *OpenShiftCluster {
+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:       id,
-		Name:     "resourceName",
+		ID:       getResourceID(name),
+		Name:     name,
 		Type:     "Microsoft.RedHatOpenShift/OpenShiftClusters",
-		Location: "location",
+		Location: location,
 		Tags: Tags{
 			"key": "value",
 		},
@@ -117,22 +124,31 @@ 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:            "location",
+					location:            *tt.location,
 					domain:              "location.aroapp.io",
 					requireD2sV3Workers: tt.requireD2sV3Workers,
-					resourceID:          id,
+					resourceID:          getResourceID(*tt.clusterName),
 					r: azure.Resource{
 						SubscriptionID: subscriptionID,
 						ResourceGroup:  "resourceGroup",
 						Provider:       "Microsoft.RedHatOpenShift",
 						ResourceType:   "openshiftClusters",
-						ResourceName:   "resourceName",
+						ResourceName:   *tt.clusterName,
 					},
 				}
 
 				validOCForTest := func() *OpenShiftCluster {
-					oc := validOpenShiftCluster()
+					oc := validOpenShiftCluster(*tt.clusterName, *tt.location)
 					if tt.current != nil {
 						tt.current(oc)
 					}
@@ -177,7 +193,11 @@ func runTests(t *testing.T, mode testMode, tests []*validateTest) {
 }
 
 func TestOpenShiftClusterStaticValidate(t *testing.T) {
-	tests := []*validateTest{
+	clusterName19 := "19characters-aaaaaa"
+	clusterName30 := "thisis30characterslong-aaaaaa"
+	nonZonalRegion := "australiasoutheast"
+
+	commonTests := []*validateTest{
 		{
 			name: "valid",
 		},
@@ -209,10 +229,38 @@ func TestOpenShiftClusterStaticValidate(t *testing.T) {
 			},
 			wantErr: "400: InvalidParameter: location: The provided location 'invalid' is invalid.",
 		},
+		{
+			name:        "valid - zonal regions can exceed max cluster name length",
+			clusterName: &clusterName30,
+		},
 	}
 
-	runTests(t, testModeCreate, tests)
-	runTests(t, testModeUpdate, tests)
+	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) {
diff --git a/pkg/api/v20220401/openshiftcluster_validatestatic.go b/pkg/api/v20220401/openshiftcluster_validatestatic.go
index 6e10a5cf4..db0b44832 100644
--- a/pkg/api/v20220401/openshiftcluster_validatestatic.go
+++ b/pkg/api/v20220401/openshiftcluster_validatestatic.go
@@ -71,6 +71,13 @@ func (sv *openShiftClusterStaticValidator) validate(oc *OpenShiftCluster, isCrea
 		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)
 }
 
diff --git a/pkg/api/v20220401/openshiftcluster_validatestatic_test.go b/pkg/api/v20220401/openshiftcluster_validatestatic_test.go
index f8dbe9c91..3f2741bf5 100644
--- a/pkg/api/v20220401/openshiftcluster_validatestatic_test.go
+++ b/pkg/api/v20220401/openshiftcluster_validatestatic_test.go
@@ -11,15 +11,19 @@ import (
 	"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
@@ -35,20 +39,23 @@ const (
 
 var (
 	subscriptionID = "00000000-0000-0000-0000-000000000000"
-	id             = fmt.Sprintf("/subscriptions/%s/resourcegroups/resourceGroup/providers/microsoft.redhatopenshift/openshiftclusters/resourceName", subscriptionID)
 )
 
-func validOpenShiftCluster() *OpenShiftCluster {
+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:       id,
-		Name:     "resourceName",
+		ID:       getResourceID(name),
+		Name:     name,
 		Type:     "Microsoft.RedHatOpenShift/OpenShiftClusters",
-		Location: "location",
+		Location: location,
 		Tags: Tags{
 			"key": "value",
 		},
@@ -117,22 +124,31 @@ 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:            "location",
+					location:            *tt.location,
 					domain:              "location.aroapp.io",
 					requireD2sV3Workers: tt.requireD2sV3Workers,
-					resourceID:          id,
+					resourceID:          getResourceID(*tt.clusterName),
 					r: azure.Resource{
 						SubscriptionID: subscriptionID,
 						ResourceGroup:  "resourceGroup",
 						Provider:       "Microsoft.RedHatOpenShift",
 						ResourceType:   "openshiftClusters",
-						ResourceName:   "resourceName",
+						ResourceName:   *tt.clusterName,
 					},
 				}
 
 				validOCForTest := func() *OpenShiftCluster {
-					oc := validOpenShiftCluster()
+					oc := validOpenShiftCluster(*tt.clusterName, *tt.location)
 					if tt.current != nil {
 						tt.current(oc)
 					}
@@ -177,7 +193,11 @@ func runTests(t *testing.T, mode testMode, tests []*validateTest) {
 }
 
 func TestOpenShiftClusterStaticValidate(t *testing.T) {
-	tests := []*validateTest{
+	clusterName19 := "19characters-aaaaaa"
+	clusterName30 := "thisis30characterslong-aaaaaa"
+	nonZonalRegion := "australiasoutheast"
+
+	commonTests := []*validateTest{
 		{
 			name: "valid",
 		},
@@ -209,10 +229,38 @@ func TestOpenShiftClusterStaticValidate(t *testing.T) {
 			},
 			wantErr: "400: InvalidParameter: location: The provided location 'invalid' is invalid.",
 		},
+		{
+			name:        "valid - zonal regions can exceed max cluster name length",
+			clusterName: &clusterName30,
+		},
 	}
 
-	runTests(t, testModeCreate, tests)
-	runTests(t, testModeUpdate, tests)
+	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) {
diff --git a/pkg/api/validate/openshiftcluster_test.go b/pkg/api/validate/openshiftcluster_test.go
new file mode 100644
index 000000000..6095782dc
--- /dev/null
+++ b/pkg/api/validate/openshiftcluster_test.go
@@ -0,0 +1,56 @@
+package validate
+
+import (
+	"testing"
+)
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+func TestOpenShiftClusterName(t *testing.T) {
+	clusterName19 := "19characters-aaaaaa"
+	clusterName30 := "thisis30characterslong-aaaaaa"
+
+	for _, tt := range []struct {
+		name          string
+		clusterName   string
+		location      string
+		desiredResult bool
+	}{
+		{
+			name:          "valid - zoned region > maxLength",
+			clusterName:   clusterName30,
+			location:      "eastus",
+			desiredResult: true,
+		},
+		{
+			name:          "valid - zoned region <= maxLength",
+			clusterName:   clusterName19,
+			location:      "eastus",
+			desiredResult: true,
+		},
+		{
+			name:          "valid - non-zoned region <= maxLength",
+			clusterName:   clusterName19,
+			location:      "australiasoutheast",
+			desiredResult: true,
+		},
+		{
+			name:        "invalid - non-zoned region > maxLength",
+			clusterName: clusterName30,
+			location:    "australiasoutheast",
+		},
+		{
+			name:        "invalid - non-zoned region > maxLength",
+			clusterName: clusterName30,
+			location:    "WESTCENTRALUS",
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			isValid := OpenShiftClusterNameLength(tt.clusterName, tt.location)
+			if isValid != tt.desiredResult {
+				t.Errorf("Got %v, wanted %v, for cluster name '%s' in region '%s'", isValid, tt.desiredResult, tt.clusterName, tt.location)
+			}
+		})
+	}
+}
diff --git a/pkg/cluster/deployresources_test.go b/pkg/cluster/deployresources_test.go
index fad3a5a21..ba26eac02 100644
--- a/pkg/cluster/deployresources_test.go
+++ b/pkg/cluster/deployresources_test.go
@@ -15,34 +15,69 @@ import (
 
 func TestZones(t *testing.T) {
 	for _, tt := range []struct {
-		name    string
-		zones   []string
-		want    *[]string
-		wantErr string
+		name       string
+		zones      []string
+		region     string
+		replicas   int64
+		wantMaster *[]string
+		wantWorker *[]string
+		wantErr    string
 	}{
 		{
-			name:  "no zones, 3 replicas",
-			zones: []string{""},
+			name:       "no zones, 3 replicas",
+			zones:      []string{""},
+			wantMaster: nil,
 		},
 		{
-			name:    "1 zone, 3 replicas",
-			zones:   []string{"1"},
-			wantErr: "cluster creation with 1 zone(s) and 3 replica(s) is unimplemented",
+			name:  "1 zone, 3 replicas",
+			zones: []string{"1"},
+			wantMaster: &[]string{
+				"1",
+			},
+			wantWorker: &[]string{"1"},
 		},
 		{
-			name:    "2 zones, 3 replicas",
-			zones:   []string{"1", "2"},
-			wantErr: "cluster creation with 2 zone(s) and 3 replica(s) is unimplemented",
+			name:  "2 zones, 3 replicas",
+			zones: []string{"1", "2"},
+			wantMaster: &[]string{
+				"1",
+				"2",
+			},
+			wantWorker: &[]string{
+				"1",
+				"2",
+			},
 		},
 		{
-			name:  "3 zones, 3 replicas",
-			zones: []string{"1", "2", "3"},
-			want: &[]string{
-				"[copyIndex(1)]",
-			},
+			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",
+			zones:   []string{"1", "2", "3", "4"},
+			wantErr: "cluster creation with 4 zone(s) and 3 replica(s) is unsupported",
+		},
+		{
+			name:     "4 zones, 4 replicas",
+			zones:    []string{"1", "2", "3", "4"},
+			replicas: 4,
+			wantErr:  "cluster creation with 4 zone(s) and 4 replica(s) is unsupported",
 		},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
+			if tt.replicas != 4 {
+				tt.replicas = 3
+			}
+
 			zones, err := zones(&installconfig.InstallConfig{
 				Config: &types.InstallConfig{
 					ControlPlane: &types.MachinePool{
@@ -51,7 +86,15 @@ func TestZones(t *testing.T) {
 								Zones: tt.zones,
 							},
 						},
-						Replicas: to.Int64Ptr(3),
+						Replicas: to.Int64Ptr(tt.replicas),
+					},
+					Platform: types.Platform{
+						Azure: &azuretypes.Platform{
+							Region: tt.region,
+							DefaultMachinePlatform: &azuretypes.MachinePool{
+								Zones: tt.zones,
+							},
+						},
 					},
 				},
 			})
@@ -59,8 +102,11 @@ func TestZones(t *testing.T) {
 				err == nil && tt.wantErr != "" {
 				t.Error(err)
 			}
-			if !reflect.DeepEqual(tt.want, zones) {
-				t.Error(zones)
+			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/util/namespace/namespace_test.go b/pkg/util/namespace/namespace_test.go
index 23821b025..daa43858e 100644
--- a/pkg/util/namespace/namespace_test.go
+++ b/pkg/util/namespace/namespace_test.go
@@ -58,7 +58,7 @@ func TestIsOpenShiftNamespace(t *testing.T) {
 		},
 		{
 			namespace: "openshift-operator-lifecycle-manager",
-			want:      false,
+			want:      true,
 		},
 	} {
 		t.Run(tt.namespace, func(t *testing.T) {
diff --git a/python/az/aro/azext_aro/tests/latest/integration/__init__.py b/python/az/aro/azext_aro/tests/latest/integration/__init__.py
new file mode 100644
index 000000000..1778c9802
--- /dev/null
+++ b/python/az/aro/azext_aro/tests/latest/integration/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the Apache License 2.0.
diff --git a/python/az/aro/azext_aro/tests/latest/test_aro_scenario.py b/python/az/aro/azext_aro/tests/latest/integration/test_aro_scenario.py
similarity index 98%
rename from python/az/aro/azext_aro/tests/latest/test_aro_scenario.py
rename to python/az/aro/azext_aro/tests/latest/integration/test_aro_scenario.py
index 5b6ef75ac..91c93b12a 100644
--- a/python/az/aro/azext_aro/tests/latest/test_aro_scenario.py
+++ b/python/az/aro/azext_aro/tests/latest/integration/test_aro_scenario.py
@@ -2,8 +2,7 @@
 # Licensed under the Apache License 2.0.
 
 import os
-import unittest
-import mock
+from unittest import mock
 
 from azure_devtools.scenario_tests import AllowLargeResponse
 from azure.cli.testsdk import ResourceGroupPreparer
diff --git a/python/az/aro/azext_aro/tests/latest/unit/__init__.py b/python/az/aro/azext_aro/tests/latest/unit/__init__.py
new file mode 100644
index 000000000..1778c9802
--- /dev/null
+++ b/python/az/aro/azext_aro/tests/latest/unit/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the Apache License 2.0.
diff --git a/python/az/aro/azext_aro/tests/latest/test_aro_helpers.py b/python/az/aro/azext_aro/tests/latest/unit/test_aro_helpers.py
similarity index 97%
rename from python/az/aro/azext_aro/tests/latest/test_aro_helpers.py
rename to python/az/aro/azext_aro/tests/latest/unit/test_aro_helpers.py
index becbc954a..d351d02e6 100644
--- a/python/az/aro/azext_aro/tests/latest/test_aro_helpers.py
+++ b/python/az/aro/azext_aro/tests/latest/unit/test_aro_helpers.py
@@ -2,7 +2,7 @@
 # Licensed under the Apache License 2.0.
 
 import unittest
-import mock
+from unittest import mock
 from azext_aro.custom import generate_random_id
 
 
diff --git a/python/az/aro/azext_aro/tests/latest/unit/test_validators.py b/python/az/aro/azext_aro/tests/latest/unit/test_validators.py
new file mode 100644
index 000000000..b257a8a58
--- /dev/null
+++ b/python/az/aro/azext_aro/tests/latest/unit/test_validators.py
@@ -0,0 +1,819 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the Apache License 2.0.
+
+from unittest.mock import Mock, patch
+from azext_aro._validators import (
+    validate_cidr, validate_client_id, validate_client_secret, validate_cluster_resource_group,
+    validate_disk_encryption_set, validate_domain, validate_pull_secret, validate_sdn, validate_subnet, validate_subnets,
+    validate_visibility, validate_vnet_resource_group_name, validate_worker_count, validate_worker_vm_disk_size_gb, validate_refresh_cluster_credentials
+)
+from azure.cli.core.azclierror import (
+    InvalidArgumentValueError, RequiredArgumentMissingError, RequiredArgumentMissingError, CLIInternalError
+)
+from azure.core.exceptions import ResourceNotFoundError
+import pytest
+
+
+test_validate_cidr_data = [
+    (
+        "should not raise exception when valid IPv4 address",
+        Mock(key='192.168.0.0/28'),
+        "key",
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError when non valid IPv4 address due to being a simple string",
+        Mock(key='this is an invalid network'),
+        "key",
+        InvalidArgumentValueError),
+    (
+        "should raise InvalidArgumentValueError when non valid IPv4 address due to invalid network ID",
+        Mock(key='192.168.0.0.0.0/28'),
+        "key",
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError when non valid IPv4 address due to invalid range of 2888",
+        Mock(key='192.168.0.0/2888'),
+        "key",
+        InvalidArgumentValueError
+    ),
+    (
+        "should not raise exception when IPv4 address is None",
+        Mock(key=None),
+        "key",
+        None
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, dummyclass, attribute_to_get_from_object, expected_exception",
+    test_validate_cidr_data,
+    ids=[i[0] for i in test_validate_cidr_data]
+)
+def test_validate_cidr(test_description, dummyclass, attribute_to_get_from_object, expected_exception):
+    validate_cidr_fn = validate_cidr(attribute_to_get_from_object)
+    if expected_exception is None:
+        validate_cidr_fn(dummyclass)
+    else:
+        with pytest.raises(expected_exception):
+            validate_cidr_fn(dummyclass)
+
+
+test_validate_client_id_data = [
+    (
+        "should not raise any Exception when namespace.client_id is None",
+        Mock(client_id=None),
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError when it can not create a UUID from namespace.client_id",
+        Mock(client_id="invalid_client_id"),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise RequiredArgumentMissingError when can not crate a string representation from namespace.client_secret because is None",
+        Mock(client_id="12345678123456781234567812345678", client_secret=None),
+        RequiredArgumentMissingError
+    ),
+    (
+        "should raise RequiredArgumentMissingError when can not crate a string representation from namespace.client_secret because it is an empty string",
+        Mock(client_id="12345678123456781234567812345678", client_secret=""),
+        RequiredArgumentMissingError
+    ),
+    (
+        "should not raise any exception when namespace.client_id is a valid input for creating a UUID and namespace.client_secret has a valid str representation",
+        Mock(client_id="12345678123456781234567812345678", client_secret="12345"),
+        None
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, namespace, expected_exception",
+    test_validate_client_id_data,
+    ids=[i[0] for i in test_validate_client_id_data]
+)
+def test_validate_client_id(test_description, namespace, expected_exception):
+    if expected_exception is None:
+        validate_client_id(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_client_id(namespace)
+
+
+test_validate_client_secret_data = [
+    (
+        "should not raise any exception when isCreate is false",
+        False,
+        Mock(client_id=None),
+        None
+    ),
+    (
+        "should not raise any exception when namespace.client_secret is None",
+        True,
+        Mock(client_secret=None),
+        None
+    ),
+    (
+        "should raise RequiredArgumentMissingError exception when namespace.client_id is None and client_secret is not None",
+        True,
+        Mock(client_id=None, client_secret="123"),
+        RequiredArgumentMissingError
+    ),
+    (
+        "should raise RequiredArgumentMissingError exception when can not crate a string representation from namespace.client_id because it is empty",
+        True,
+        Mock(client_id="", client_secret="123"),
+        RequiredArgumentMissingError
+    ),
+    (
+        "should raise RequiredArgumentMissingError exception when can not crate a string representation from namespace.client_id because it is None",
+        True,
+        Mock(client_id=None, client_secret="123"),
+        RequiredArgumentMissingError
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, isCreate, namespace, expected_exception",
+    test_validate_client_secret_data,
+    ids=[i[0] for i in test_validate_client_secret_data]
+)
+def test_validate_client_secret(test_description, isCreate, namespace, expected_exception):
+    validate_client_secret_fn = validate_client_secret(isCreate)
+    if expected_exception is None:
+        validate_client_secret_fn(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_client_secret_fn(namespace)
+
+
+test_validate_cluster_resource_group_data = [
+    (
+        "should not raise any exception when namespace.cluster_resource_group is None",
+        None,
+        None,
+        Mock(cluster_resource_group=None),
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.cluster_resource_group is not None and resource group exists in the client returned by get_mgmt_service_client",
+        Mock(**{"resource_groups.check_existence.return_value": True}),
+        Mock(cli_ctx=None),
+        Mock(cluster_resource_group="some_resource_group"),
+        InvalidArgumentValueError
+    ),
+    (
+        "should not raise any exception when namespace.cluster_resource_group is not None and resource group does not exists in the client returned by get_mgmt_service_client",
+        Mock(**{"resource_groups.check_existence.return_value": False}),
+        Mock(cli_ctx=None),
+        Mock(cluster_resource_group="some_resource_group"),
+        None
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, client_mock, cmd_mock, namespace, expected_exception",
+    test_validate_cluster_resource_group_data,
+    ids=[i[0] for i in test_validate_cluster_resource_group_data]
+)
+@patch('azext_aro._validators.get_mgmt_service_client')
+def test_validate_cluster_resource_group(get_mgmt_service_client_mock, test_description, client_mock, cmd_mock, namespace, expected_exception):
+    get_mgmt_service_client_mock.return_value = client_mock
+    if expected_exception is None:
+        validate_cluster_resource_group(cmd_mock, namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_cluster_resource_group(cmd_mock, namespace)
+
+
+test_validate_disk_encryption_set_data = [
+    (
+        "should not raise any exception when namespace.disk_encryption_set is None",
+        None,
+        Mock(disk_encryption_set=None),
+        None,
+        None,
+        None,
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.disk_encryption_set is not None and is_valid_resource_id(namespace.disk_encryption_set) returns False",
+        None,
+        Mock(disk_encryption_set="something different than None"),
+        False,
+        None,
+        InvalidArgumentValueError,
+        None
+    ),
+    (
+        "should not raise any exception when compute_client.disk_encryption_sets.get() not raises CludError exception",
+        Mock(cli_ctx=None),
+        Mock(disk_encryption_set="something different than None"),
+        True,
+        Mock(),
+        None,
+        {"resource_group": None, "name": None}
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, cmd_mock, namespace, is_valid_resource_id_return_value, compute_client_mock, expected_exception, parse_resource_id_mock_return_value",
+    test_validate_disk_encryption_set_data,
+    ids=[i[0] for i in test_validate_disk_encryption_set_data]
+)
+@patch('azext_aro._validators.get_mgmt_service_client')
+@patch('azext_aro._validators.parse_resource_id')
+@patch('azext_aro._validators.is_valid_resource_id')
+def test_validate_disk_encryption_set(
+    # Mocks:
+    is_valid_resource_id_mock,
+    parse_resource_id_mock,
+    get_mgmt_service_client_mock,
+
+    # Test cases parameters:
+    test_description, cmd_mock, namespace, is_valid_resource_id_return_value,
+    compute_client_mock, expected_exception, parse_resource_id_mock_return_value
+):
+    is_valid_resource_id_mock.return_value = is_valid_resource_id_return_value
+    parse_resource_id_mock.return_value = parse_resource_id_mock_return_value
+
+    if compute_client_mock is not None:
+        compute_client_mock.get.return_value = None
+        get_mgmt_service_client_mock.return_value = compute_client_mock
+
+    if expected_exception is None:
+        validate_disk_encryption_set(cmd_mock, namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_disk_encryption_set(cmd_mock, namespace)
+
+
+test_validate_domain_data = [
+    (
+        "should not raise any exception when namespace.domain is None",
+        Mock(domain=None),
+        None
+    ),
+    (
+        "should not raise any exception when namespace.domain has '-'",
+        Mock(domain="my-domain.com"),
+        None
+    ),
+    (
+        "should not raise any exception when namespace.domain is some.more.than.expected",
+        Mock(domain="some.more.than.expected"),
+        None
+    ),
+    (
+        "should not raise any exception when namespace.domain is azure.microsoft.com",
+        Mock(domain="azure.microsoft.com"),
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.domain ends with '.'",
+        Mock(domain="google."),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.domain has '_'",
+        Mock(domain="my_domain.com"),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.domain has is google..com",
+        Mock(domain="google..com"),
+        InvalidArgumentValueError
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, namespace, expected_exception",
+    test_validate_domain_data,
+    ids=[i[0] for i in test_validate_domain_data]
+)
+def test_validate_domain(test_description, namespace, expected_exception):
+    if expected_exception is None:
+        validate_domain(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_domain(namespace)
+
+
+test_validate_pull_secret_data = [
+    (
+        "should not raise any exception when namespace.pull_secret is None",
+        Mock(pull_secret=None),
+        None
+    ),
+    (
+        "should not raise any exception when namespace.pull_secret is a valid JSON",
+        Mock(pull_secret='{"key":"value"}'),
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.pull_secret is not a valid JSON beacuse is an empty string",
+        Mock(pull_secret=""),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.pull_secret is not a valid JSON because missing value",
+        Mock(pull_secret='{"key": }'),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.pull_secret is not a valid JSON because is a simple string",
+        Mock(pull_secret='a simple string'),
+        InvalidArgumentValueError
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, namespace, expected_exception",
+    test_validate_pull_secret_data,
+    ids=[i[0] for i in test_validate_pull_secret_data]
+)
+def test_validate_pull_secret(test_description, namespace, expected_exception):
+    if expected_exception is None:
+        validate_pull_secret(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_pull_secret(namespace)
+
+
+test_validate_sdn_data = [
+    (
+        "should not raise any exception when namespace.software_defined_network is None",
+        Mock(software_defined_network=None),
+        None
+    ),
+    (
+        "should not raise any exception when namespace.software_defined_network is OVNKubernetes",
+        Mock(software_defined_network="OVNKubernetes"),
+        None
+    ),
+    (
+        "should not raise any exception when namespace.software_defined_network is OpenshiftSDN",
+        Mock(software_defined_network="OpenshiftSDN"),
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.software_defined_network is 'uknown', not one of the target values",
+        Mock(software_defined_network="uknown"),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when namespace.software_defined_network is an empty string",
+        Mock(software_defined_network=""),
+        InvalidArgumentValueError
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, namespace, expected_exception",
+    test_validate_sdn_data,
+    ids=[i[0] for i in test_validate_sdn_data]
+)
+def test_validate_sdn(test_description, namespace, expected_exception):
+    if expected_exception is None:
+        validate_sdn(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_sdn(namespace)
+
+
+test_validate_subnet_data = [
+    (
+        "should raise RequiredArgumentMissingError exception when subnet is not a valid resource id and namespace.vnet is False",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        False,
+        None,
+        None,
+        None,
+        None,
+        RequiredArgumentMissingError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when parts subscription is different than cluster_subscription",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {"subscription": "expected"},
+        "different than expected",
+        None,
+        Mock(cli_ctx=None),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when parts namespace is different than expected namespace",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {"subscription": "subscription", "namespace": "something.different"},
+        "subscription",
+        None,
+        Mock(cli_ctx=None),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when parts type is different than expected_parts",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {
+            "subscription": "subscription",
+            "namespace": "MICROSOFT.NETWORK",
+            "type": "this_should_be_virtualnetworks"
+        },
+        "subscription",
+        None,
+        Mock(cli_ctx=None),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when subnet childs is different than expected childs",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {
+            "subscription": "subscription",
+            "namespace": "MICROSOFT.NETWORK",
+            "type": "virtualnetworks",
+            "last_child_num": 0
+        },
+        "subscription",
+        None,
+        Mock(cli_ctx=None),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when subnet has child namespace",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {
+            "subscription": "subscription",
+            "namespace": "MICROSOFT.NETWORK",
+            "type": "virtualnetworks",
+            "last_child_num": 1,
+            "child_namespace_1": "something"
+        },
+        "subscription",
+        None,
+        Mock(cli_ctx=None),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when child type subnet do not equal subnets",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {
+            "subscription": "subscription",
+            "namespace": "MICROSOFT.NETWORK",
+            "type": "virtualnetworks",
+            "last_child_num": 1,
+            "child_type_1": "this_should_be_subnets"
+        },
+        "subscription",
+        None,
+        Mock(cli_ctx=None),
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when client.subnets.get raises CLIInternalError because client.subnets.get() raises Exception exception",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {
+            "subscription": "subscription",
+            "namespace": "MICROSOFT.NETWORK",
+            "type": "virtualnetworks",
+            "last_child_num": 1,
+            "child_type_1": "subnets"
+        },
+        "subscription",
+        Mock(**{"subnets.get.side_effect": Exception}),
+        Mock(cli_ctx=None),
+        CLIInternalError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when client.subnets.get() raises ResourceNotFoundError exception",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {
+            "subscription": "subscription",
+            "namespace": "MICROSOFT.NETWORK",
+            "type": "virtualnetworks",
+            "last_child_num": 1,
+            "child_type_1": "subnets",
+            "resource_group": None,
+            "name": None,
+            "child_name_1": None
+        },
+        "subscription",
+        Mock(**{"subnets.get.side_effect": ResourceNotFoundError("")}),
+        Mock(cli_ctx=None),
+        InvalidArgumentValueError
+    ),
+    (
+        "should not raise any exception",
+        Mock(key='192.168.0.0/28', vnet=False),
+        'key',
+        True,
+        {
+            "subscription": "subscription",
+            "namespace": "MICROSOFT.NETWORK",
+            "type": "virtualnetworks",
+            "last_child_num": 1,
+            "child_type_1": "subnets",
+            "resource_group": None,
+            "name": None,
+            "child_name_1": None
+        },
+        "subscription",
+        Mock(**{"subnets.get.return_value": None}),
+        Mock(cli_ctx=None),
+        None
+    )
+]
+
+
+@ pytest.mark.parametrize(
+    "test_description, namespace, key, is_valid_resource_id_mock_return_value, parse_resource_id_mock_return_value, get_subscription_id_mock_return_value, get_mgmt_service_client_mock_return_value, cmd, expected_exception",
+    test_validate_subnet_data,
+    ids=[i[0] for i in test_validate_subnet_data]
+)
+@ patch('azext_aro._validators.get_mgmt_service_client')
+@ patch('azext_aro._validators.get_subscription_id')
+@ patch('azext_aro._validators.parse_resource_id')
+@ patch('azext_aro._validators.is_valid_resource_id')
+def test_validate_subnet(
+    # Mocked functions:
+    is_valid_resource_id_mock, parse_resource_id_mock, get_subscription_id_mock, get_mgmt_service_client_mock,
+
+    # Test cases parameters:
+    test_description, namespace, key, is_valid_resource_id_mock_return_value,
+    parse_resource_id_mock_return_value, get_subscription_id_mock_return_value,
+    get_mgmt_service_client_mock_return_value, cmd, expected_exception
+):
+    is_valid_resource_id_mock.return_value = is_valid_resource_id_mock_return_value
+    parse_resource_id_mock.return_value = parse_resource_id_mock_return_value
+    get_subscription_id_mock.return_value = get_subscription_id_mock_return_value
+    get_mgmt_service_client_mock.return_value = get_mgmt_service_client_mock_return_value
+
+    validate_subnet_fn = validate_subnet(key)
+
+    if expected_exception is None:
+        validate_subnet_fn(cmd, namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_subnet_fn(cmd, namespace)
+
+
+test_validate_subnets_data = [
+    (
+        "should raise InvalidArgumentValueError exception when resource group of master_parts is different than resource_group of worker_parts",
+        [
+            {"resource_group": "a"},
+            {"resource_group": "b"}
+        ],
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when name of master_parts is different than name of worker_parts",
+        [
+            {"resource_group": "equal", "name": "something"},
+            {"resource_group": "equal", "name": "different"}
+        ],
+        InvalidArgumentValueError
+    ),
+    (
+        "should raise InvalidArgumentValueError exception when name of master_parts is different than name of worker_parts",
+        [
+            {"resource_group": "equal", "name": "something", "child_name_1": "should_not_be_equal"},
+            {"resource_group": "equal", "name": "something", "child_name_1": "should_not_be_equal"}
+        ],
+        InvalidArgumentValueError
+    ),
+    (
+        "should not raise any exception",
+        [
+            {"resource_group": "equal", "name": "something", "child_name_1": "a"},
+            {"resource_group": "equal", "name": "something", "child_name_1": "z"}
+        ],
+        None
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, parse_resource_id_mock_tc, expected_exception",
+    test_validate_subnets_data,
+    ids=[i[0] for i in test_validate_subnets_data]
+)
+@patch('azext_aro._validators.parse_resource_id')
+def test_validate_subnets(parse_resource_id_mock, test_description, parse_resource_id_mock_tc, expected_exception):
+    parse_resource_id_mock.side_effect = parse_resource_id_mock_tc
+
+    if expected_exception is None:
+        validate_subnets(None, None)
+    else:
+        with pytest.raises(expected_exception):
+            validate_subnets(None, None)
+
+
+test_validate_visibility_data = [
+    (
+        "should not raise any exception",
+        "key",
+        Mock(key=None),
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError exception because visibility is not one of the expected values",
+        "key",
+        Mock(key="super_private"),
+        InvalidArgumentValueError
+    ),
+    (
+        "should not raise any exception because visibility is private",
+        "key",
+        Mock(key="private"),
+        None
+    ),
+    (
+        "should not raise any exception because visibility is PRIVATE",
+        "key",
+        Mock(key="PRIVATE"),
+        None
+    ),
+    (
+        "should not raise any exception because visibility is PUBLIC",
+        "key",
+        Mock(key="PUBLIC"),
+        None
+    ),
+    (
+        "should not raise any exception because visibility is public",
+        "key",
+        Mock(key="public"),
+        None
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, key, namespace, expected_exception",
+    test_validate_visibility_data,
+    ids=[i[0] for i in test_validate_visibility_data]
+)
+def test_validate_visibility(test_description, key, namespace, expected_exception):
+    validate_visibility_fn = validate_visibility(key)
+
+    if expected_exception is None:
+        validate_visibility_fn(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_visibility_fn(namespace)
+
+
+test_validate_vnet_resource_group_name_data = [
+    (
+        "should not copy namespace.resource_group_name to namespace.vnet_resource_group_name because namespace.resource_group_name already has a value",
+        Mock(vnet_resource_group_name="hello", resource_group_name="this_will_not_be_copied"),
+        "hello"
+    ),
+    (
+        "should copy resource_group_name field to vnet_resource_group_name because namespace.vnet_resource_group_name is None",
+        Mock(vnet_resource_group_name=None, resource_group_name="will_copy_this"),
+        "will_copy_this"
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, namespace, expected_namespace_vnet_resource_group_name",
+    test_validate_vnet_resource_group_name_data,
+    ids=[i[0] for i in test_validate_vnet_resource_group_name_data]
+)
+def test_validate_vnet_resource_group_name(test_description, namespace, expected_namespace_vnet_resource_group_name):
+    validate_vnet_resource_group_name(namespace)
+    assert(namespace.vnet_resource_group_name ==
+           expected_namespace_vnet_resource_group_name)
+
+
+test_validate_worker_count_data = [
+    (
+        "should not raise any Exception because worker count of namespace is None",
+        Mock(worker_count=None),
+        None
+    ),
+    (
+        "should not raise any Exception because worker count of namespace is 3",
+        Mock(worker_count=3),
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError Exception because worker count of namespace is less than minimum workers count",
+        Mock(worker_count=2),
+        InvalidArgumentValueError
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, namespace, expected_exception",
+    test_validate_worker_count_data,
+    ids=[i[0] for i in test_validate_worker_count_data]
+)
+def test_validate_worker_count(test_description, namespace, expected_exception):
+    if expected_exception is None:
+        validate_worker_count(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_worker_count(namespace)
+
+
+test_validate_worker_vm_disk_size_gb_data = [
+    (
+        "should not raise any Exception because worker_vm_disk_size_gb of namespace is None",
+        Mock(worker_vm_disk_size_gb=None),
+        None
+    ),
+    (
+        "should raise InvalidArgumentValueError Exception because worker_vm_disk_size_gb of namespace is less than minimum_worker_vm_disk_size_gb",
+        Mock(worker_vm_disk_size_gb=2),
+        InvalidArgumentValueError
+    ),
+    (
+        "should not raise any Exception because worker_vm_disk_size_gb of namespace is equal than minimum_worker_vm_disk_size_gb",
+        Mock(worker_vm_disk_size_gb=128),
+        None
+    ),
+    (
+        "should not raise any Exception because worker_vm_disk_size_gb of namespace is greater than minimum_worker_vm_disk_size_gb",
+        Mock(worker_vm_disk_size_gb=220),
+        None
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, namespace, expected_exception",
+    test_validate_worker_vm_disk_size_gb_data,
+    ids=[i[0] for i in test_validate_worker_vm_disk_size_gb_data]
+)
+def test_validate_worker_vm_disk_size_gb(test_description, namespace, expected_exception):
+    if expected_exception is None:
+        validate_worker_vm_disk_size_gb(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_worker_vm_disk_size_gb(namespace)
+
+
+test_validate_refresh_cluster_credentials_data = [
+    (
+        "should not raise any Exception because namespace.refresh_cluster_credentials is none",
+        Mock(refresh_cluster_credentials=None),
+        None
+    ),
+    (
+        "should raise RequiredArgumentMissingError Exception because namespace.client_secret is not None",
+        Mock(client_secret="secret_123"),
+        RequiredArgumentMissingError
+    ),
+    (
+        "should raise RequiredArgumentMissingError Exception because namespace.client_id is not None",
+        Mock(client_id="client_id_456"),
+        RequiredArgumentMissingError
+    ),
+    (
+        "should not raise any Exception because namespace.client_secret is None and namespace.client_id is None",
+        Mock(client_secret=None, client_id=None),
+        None
+    )
+]
+
+
+@pytest.mark.parametrize(
+    "test_description, namespace, expected_exception",
+    test_validate_refresh_cluster_credentials_data,
+    ids=[i[0] for i in test_validate_refresh_cluster_credentials_data]
+)
+def test_validate_refresh_cluster_credentials(test_description, namespace, expected_exception):
+    if expected_exception is None:
+        validate_refresh_cluster_credentials(namespace)
+    else:
+        with pytest.raises(expected_exception):
+            validate_refresh_cluster_credentials(namespace)