Repository: Azure/ARO-RP
Pull request #1234 (2020-12-18 16:31:41)

refactor dynamic validator to be operator compatible
Duplicate of #1232 with changes suggested by @ihcsim in [this comment](https://github.com/Azure/ARO-RP/pull/1232#discussion_r545273592)

### Which issue this PR addresses:

Fixes our ability to consume dynamic validation in context of Cluster/Operator.
Suggestion for #1230
### What this PR does / why we need it:

Introduced 2 flavors of dynamic validator:
Full - all validators. Intended to run in the context of RP.
Slim - Validators intended to be running in the context of Operator.

It would need more refactoring so could consume the data from the validator. But might be good start.
### Test plan for issue:

unit tests
### Is there any documentation that needs to be updated for this PR?

no


Patch:
diff --git a/pkg/api/validate/openshiftcluster_validatedynamic.go b/pkg/api/validate/openshiftcluster_validatedynamic.go
index 0b86e8794..45ff83651 100644
--- a/pkg/api/validate/openshiftcluster_validatedynamic.go
+++ b/pkg/api/validate/openshiftcluster_validatedynamic.go
@@ -23,6 +23,7 @@ import (
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	"github.com/Azure/ARO-RP/pkg/env"
+	arooperatorapi "github.com/Azure/ARO-RP/pkg/operator/apis/aro.openshift.io/v1alpha1"
 	"github.com/Azure/ARO-RP/pkg/util/aad"
 	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/authorization"
 	"github.com/Azure/ARO-RP/pkg/util/azureclient/mgmt/features"
@@ -32,49 +33,99 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/subnet"
 )
 
-// OpenShiftClusterDynamicValidator is the dynamic validator interface
-type OpenShiftClusterDynamicValidator interface {
+// OpenShiftClusterFullDynamicValidator is the dynamic validator interface
+// for the RP execution contexts
+type OpenShiftClusterFullDynamicValidator interface {
 	Dynamic(context.Context) error
 }
 
-// NewOpenShiftClusterDynamicValidator creates a new OpenShiftClusterDynamicValidator
-func NewOpenShiftClusterDynamicValidator(log *logrus.Entry, env env.Interface, oc *api.OpenShiftCluster, subscriptionDoc *api.SubscriptionDocument, fpAuthorizer refreshable.Authorizer) OpenShiftClusterDynamicValidator {
-	return &openShiftClusterDynamicValidator{
-		log: log,
+// OpenShiftClusterSlimDynamicValidator is the dynamic validator interface
+// for the cluster execution context
+type OpenShiftClusterSlimDynamicValidator interface {
+	Dynamic(context.Context) error
+}
+
+// NewOpenShiftClusterFullDynamicValidator creates a new OpenShiftClusterFullDynamicValidator
+func NewOpenShiftClusterFullDynamicValidator(log *logrus.Entry, env env.Interface, oc *api.OpenShiftCluster, subscriptionDoc *api.SubscriptionDocument, fpAuthorizer refreshable.Authorizer) OpenShiftClusterFullDynamicValidator {
+	return &openShiftClusterFullDynamicValidator{
 		env: env,
 
 		oc:              oc,
 		subscriptionDoc: subscriptionDoc,
 		fpAuthorizer:    fpAuthorizer,
 
-		fpPermissions: authorization.NewPermissionsClient(env.Environment(), subscriptionDoc.ID, fpAuthorizer),
+		log: log,
 	}
 }
 
-type azureClaim struct {
-	Roles []string `json:"roles,omitempty"`
-}
-
-func (*azureClaim) Valid() error {
-	return fmt.Errorf("unimplemented")
+// NewOpenShiftClusterSlimDynamicValidator creates a new OpenShiftClusterSlimDynamicValidator
+func NewOpenShiftClusterSlimDynamicValidator(log *logrus.Entry, clientID string, clientSecret api.SecureString, subscriptionID string, tenantID string, clusterSpec *arooperatorapi.ClusterSpec) OpenShiftClusterSlimDynamicValidator {
+	spp := &api.ServicePrincipalProfile{
+		ClientID:     clientID,
+		ClientSecret: clientSecret,
+	}
+	return &openShiftClusterSlimDynamicValidator{
+		log:            log,
+		spp:            spp,
+		tenantID:       tenantID,
+		subscriptionID: subscriptionID,
+		clusterSpec:    clusterSpec,
+	}
 }
 
-type openShiftClusterDynamicValidator struct {
-	log *logrus.Entry
+// openShiftClusterFullDynamicValidator is dynamic validator used inside RP context
+// and can operate within cluster and firstParty service principals.
+// It includes openShiftClusterSlimDynamicValidatora
+type openShiftClusterFullDynamicValidator struct {
 	env env.Interface
 
 	oc              *api.OpenShiftCluster
 	subscriptionDoc *api.SubscriptionDocument
 	fpAuthorizer    refreshable.Authorizer
 
-	fpPermissions     authorization.PermissionsClient
-	spPermissions     authorization.PermissionsClient
-	spProviders       features.ProvidersClient
-	spVirtualNetworks network.VirtualNetworksClient
+	log *logrus.Entry
+}
+
+// openShiftClusterSlimDynamicValidator is dynamic validator used inside clusters
+// context and can operate ONLY with cluster service principal scope
+type openShiftClusterSlimDynamicValidator struct {
+	log *logrus.Entry
+
+	spp             *api.ServicePrincipalProfile
+	clusterSpec     *arooperatorapi.ClusterSpec
+	tenantID        string
+	subscriptionID  string
+	environmentName string
+}
+
+// Dynamic validates an OpenShift cluster in the context of cluster
+func (dv *openShiftClusterSlimDynamicValidator) Dynamic(ctx context.Context) error {
+	azureEnv, err := azure.EnvironmentFromName(dv.environmentName)
+	if err != nil {
+		return err
+	}
+	spAuthorizer, err := validateServicePrincipalProfile(ctx, dv.log, dv.spp, dv.tenantID, azureEnv.ResourceManagerEndpoint)
+	if err != nil {
+		return err
+	}
+
+	spPermissions := authorization.NewPermissionsClient(&azureEnv, dv.subscriptionID, spAuthorizer)
+
+	vnetr, err := azure.ParseResourceID(dv.clusterSpec.VNetID)
+	if err != nil {
+		return err
+	}
+
+	err = validateVnetPermissions(ctx, dv.log, spAuthorizer, spPermissions, dv.clusterSpec.VNetID, &vnetr, api.CloudErrorCodeInvalidServicePrincipalPermissions, "provided service principal")
+	if err != nil {
+		return err
+	}
+
+	return nil
 }
 
 // Dynamic validates an OpenShift cluster
-func (dv *openShiftClusterDynamicValidator) Dynamic(ctx context.Context) error {
+func (dv *openShiftClusterFullDynamicValidator) Dynamic(ctx context.Context) error {
 	// TODO: Dynamic() should work on an enriched oc (in update), and it
 	// currently doesn't.  One sticking point is handling subnet overlap
 	// calculations.
@@ -84,14 +135,15 @@ func (dv *openShiftClusterDynamicValidator) Dynamic(ctx context.Context) error {
 		return err
 	}
 
-	spAuthorizer, err := validateServicePrincipalProfile(ctx, dv.log, dv.env, dv.oc, dv.subscriptionDoc)
+	fpPermissions := authorization.NewPermissionsClient(dv.env.Environment(), dv.subscriptionDoc.ID, dv.fpAuthorizer)
+	spAuthorizer, err := validateServicePrincipalProfile(ctx, dv.log, &dv.oc.Properties.ServicePrincipalProfile, dv.subscriptionDoc.Subscription.Properties.TenantID, dv.env.Environment().ResourceManagerEndpoint)
 	if err != nil {
 		return err
 	}
 
-	dv.spPermissions = authorization.NewPermissionsClient(dv.env.Environment(), r.SubscriptionID, spAuthorizer)
-	dv.spProviders = features.NewProvidersClient(dv.env.Environment(), r.SubscriptionID, spAuthorizer)
-	dv.spVirtualNetworks = network.NewVirtualNetworksClient(dv.env.Environment(), r.SubscriptionID, spAuthorizer)
+	spPermissions := authorization.NewPermissionsClient(dv.env.Environment(), r.SubscriptionID, spAuthorizer)
+	spProviders := features.NewProvidersClient(dv.env.Environment(), r.SubscriptionID, spAuthorizer)
+	spVirtualNetworks := network.NewVirtualNetworksClient(dv.env.Environment(), r.SubscriptionID, spAuthorizer)
 
 	vnetID, _, err := subnet.Split(dv.oc.Properties.MasterProfile.SubnetID)
 	if err != nil {
@@ -103,38 +155,38 @@ func (dv *openShiftClusterDynamicValidator) Dynamic(ctx context.Context) error {
 		return err
 	}
 
-	err = dv.validateVnetPermissions(ctx, spAuthorizer, dv.spPermissions, vnetID, &vnetr, api.CloudErrorCodeInvalidServicePrincipalPermissions, "provided service principal")
+	err = validateVnetPermissions(ctx, dv.log, spAuthorizer, spPermissions, vnetID, &vnetr, api.CloudErrorCodeInvalidServicePrincipalPermissions, "provided service principal")
 	if err != nil {
 		return err
 	}
 
-	err = dv.validateVnetPermissions(ctx, dv.fpAuthorizer, dv.fpPermissions, vnetID, &vnetr, api.CloudErrorCodeInvalidResourceProviderPermissions, "resource provider")
+	err = validateVnetPermissions(ctx, dv.log, dv.fpAuthorizer, fpPermissions, vnetID, &vnetr, api.CloudErrorCodeInvalidResourceProviderPermissions, "resource provider")
 	if err != nil {
 		return err
 	}
 
 	// Get after validating permissions
-	vnet, err := dv.spVirtualNetworks.Get(ctx, vnetr.ResourceGroup, vnetr.ResourceName, "")
+	vnet, err := spVirtualNetworks.Get(ctx, vnetr.ResourceGroup, vnetr.ResourceName, "")
 	if err != nil {
 		return err
 	}
 
-	err = dv.validateRouteTablePermissions(ctx, spAuthorizer, dv.spPermissions, &vnet, api.CloudErrorCodeInvalidServicePrincipalPermissions, "provided service principal")
+	err = validateRouteTablePermissions(ctx, dv.log, dv.oc, spAuthorizer, spPermissions, &vnet, api.CloudErrorCodeInvalidServicePrincipalPermissions, "provided service principal")
 	if err != nil {
 		return err
 	}
 
-	err = dv.validateRouteTablePermissions(ctx, dv.fpAuthorizer, dv.fpPermissions, &vnet, api.CloudErrorCodeInvalidResourceProviderPermissions, "resource provider")
+	err = validateRouteTablePermissions(ctx, dv.log, dv.oc, dv.fpAuthorizer, fpPermissions, &vnet, api.CloudErrorCodeInvalidResourceProviderPermissions, "resource provider")
 	if err != nil {
 		return err
 	}
 
-	err = dv.validateVnet(ctx, &vnet)
+	err = validateVnet(ctx, dv.log, dv.oc, &vnet)
 	if err != nil {
 		return err
 	}
 
-	err = dv.validateProviders(ctx)
+	err = validateProviders(ctx, dv.log, spProviders)
 	if err != nil {
 		return err
 	}
@@ -142,10 +194,18 @@ func (dv *openShiftClusterDynamicValidator) Dynamic(ctx context.Context) error {
 	return nil
 }
 
-func validateServicePrincipalProfile(ctx context.Context, log *logrus.Entry, env env.Interface, oc *api.OpenShiftCluster, sub *api.SubscriptionDocument) (refreshable.Authorizer, error) {
+type azureClaim struct {
+	Roles []string `json:"roles,omitempty"`
+}
+
+func (*azureClaim) Valid() error {
+	return fmt.Errorf("unimplemented")
+}
+
+func validateServicePrincipalProfile(ctx context.Context, log *logrus.Entry, spp *api.ServicePrincipalProfile, tenantID, resource string) (refreshable.Authorizer, error) {
 	log.Print("validateServicePrincipalProfile")
 
-	token, err := aad.GetToken(ctx, log, oc, sub, env.Environment().ResourceManagerEndpoint)
+	token, err := aad.GetToken(ctx, log, spp, tenantID, resource)
 	if err != nil {
 		return nil, err
 	}
@@ -166,10 +226,10 @@ func validateServicePrincipalProfile(ctx context.Context, log *logrus.Entry, env
 	return refreshable.NewAuthorizer(token), nil
 }
 
-func (dv *openShiftClusterDynamicValidator) validateVnetPermissions(ctx context.Context, authorizer refreshable.Authorizer, client authorization.PermissionsClient, vnetID string, vnetr *azure.Resource, code, typ string) error {
-	dv.log.Printf("validateVnetPermissions (%s)", typ)
+func validateVnetPermissions(ctx context.Context, log *logrus.Entry, authorizer refreshable.Authorizer, client authorization.PermissionsClient, vnetID string, vnetr *azure.Resource, code, typ string) error {
+	log.Printf("validateVnetPermissions (%s)", typ)
 
-	err := validateActions(ctx, dv.log, vnetr, []string{
+	err := validateActions(ctx, log, vnetr, []string{
 		"Microsoft.Network/virtualNetworks/join/action",
 		"Microsoft.Network/virtualNetworks/read",
 		"Microsoft.Network/virtualNetworks/write",
@@ -187,14 +247,14 @@ func (dv *openShiftClusterDynamicValidator) validateVnetPermissions(ctx context.
 	return err
 }
 
-func (dv *openShiftClusterDynamicValidator) validateRouteTablePermissions(ctx context.Context, authorizer refreshable.Authorizer, client authorization.PermissionsClient, vnet *mgmtnetwork.VirtualNetwork, code, typ string) error {
-	err := dv.validateRouteTablePermissionsSubnet(ctx, authorizer, client, vnet, dv.oc.Properties.MasterProfile.SubnetID, "properties.masterProfile.subnetId", code, typ)
+func validateRouteTablePermissions(ctx context.Context, log *logrus.Entry, oc *api.OpenShiftCluster, authorizer refreshable.Authorizer, client authorization.PermissionsClient, vnet *mgmtnetwork.VirtualNetwork, code, typ string) error {
+	err := validateRouteTablePermissionsSubnet(ctx, log, authorizer, client, vnet, oc.Properties.MasterProfile.SubnetID, "properties.masterProfile.subnetId", code, typ)
 	if err != nil {
 		return err
 	}
 
-	for i, s := range dv.oc.Properties.WorkerProfiles {
-		err := dv.validateRouteTablePermissionsSubnet(ctx, authorizer, client, vnet, s.SubnetID, "properties.workerProfiles["+strconv.Itoa(i)+"].subnetId", code, typ)
+	for i, s := range oc.Properties.WorkerProfiles {
+		err := validateRouteTablePermissionsSubnet(ctx, log, authorizer, client, vnet, s.SubnetID, "properties.workerProfiles["+strconv.Itoa(i)+"].subnetId", code, typ)
 		if err != nil {
 			return err
 		}
@@ -202,8 +262,8 @@ func (dv *openShiftClusterDynamicValidator) validateRouteTablePermissions(ctx co
 	return nil
 }
 
-func (dv *openShiftClusterDynamicValidator) validateRouteTablePermissionsSubnet(ctx context.Context, authorizer refreshable.Authorizer, client authorization.PermissionsClient, vnet *mgmtnetwork.VirtualNetwork, subnetID, path, code, typ string) error {
-	dv.log.Printf("validateRouteTablePermissionsSubnet(%s, %s)", typ, path)
+func validateRouteTablePermissionsSubnet(ctx context.Context, log *logrus.Entry, authorizer refreshable.Authorizer, client authorization.PermissionsClient, vnet *mgmtnetwork.VirtualNetwork, subnetID, path, code, typ string) error {
+	log.Printf("validateRouteTablePermissionsSubnet(%s, %s)", typ, path)
 
 	var s *mgmtnetwork.Subnet
 	for _, ss := range *vnet.Subnets {
@@ -225,7 +285,7 @@ func (dv *openShiftClusterDynamicValidator) validateRouteTablePermissionsSubnet(
 		return err
 	}
 
-	err = validateActions(ctx, dv.log, &rtr, []string{
+	err = validateActions(ctx, log, &rtr, []string{
 		"Microsoft.Network/routeTables/join/action",
 		"Microsoft.Network/routeTables/read",
 		"Microsoft.Network/routeTables/write",
@@ -240,8 +300,8 @@ func (dv *openShiftClusterDynamicValidator) validateRouteTablePermissionsSubnet(
 	return err
 }
 
-func (dv *openShiftClusterDynamicValidator) validateSubnet(ctx context.Context, vnet *mgmtnetwork.VirtualNetwork, path, subnetID string) (*net.IPNet, error) {
-	dv.log.Printf("validateSubnet (%s)", path)
+func validateSubnet(ctx context.Context, log *logrus.Entry, oc *api.OpenShiftCluster, vnet *mgmtnetwork.VirtualNetwork, path, subnetID string) (*net.IPNet, error) {
+	log.Printf("validateSubnet (%s)", path)
 
 	var s *mgmtnetwork.Subnet
 	if vnet.Subnets != nil {
@@ -256,7 +316,7 @@ func (dv *openShiftClusterDynamicValidator) validateSubnet(ctx context.Context,
 		return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, path, "The provided subnet '%s' could not be found.", subnetID)
 	}
 
-	if strings.EqualFold(dv.oc.Properties.MasterProfile.SubnetID, subnetID) {
+	if strings.EqualFold(oc.Properties.MasterProfile.SubnetID, subnetID) {
 		if s.PrivateLinkServiceNetworkPolicies == nil ||
 			!strings.EqualFold(*s.PrivateLinkServiceNetworkPolicies, "Disabled") {
 			return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, path, "The provided subnet '%s' is invalid: must have privateLinkServiceNetworkPolicies disabled.", subnetID)
@@ -277,14 +337,14 @@ func (dv *openShiftClusterDynamicValidator) validateSubnet(ctx context.Context,
 		return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, path, "The provided subnet '%s' is invalid: must have Microsoft.ContainerRegistry serviceEndpoint.", subnetID)
 	}
 
-	if dv.oc.Properties.ProvisioningState == api.ProvisioningStateCreating {
+	if oc.Properties.ProvisioningState == api.ProvisioningStateCreating {
 		if s.SubnetPropertiesFormat != nil &&
 			s.SubnetPropertiesFormat.NetworkSecurityGroup != nil {
 			return nil, api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, path, "The provided subnet '%s' is invalid: must not have a network security group attached.", subnetID)
 		}
 
 	} else {
-		nsgID, err := subnet.NetworkSecurityGroupID(dv.oc, *s.ID)
+		nsgID, err := subnet.NetworkSecurityGroupID(oc, *s.ID)
 		if err != nil {
 			return nil, err
 		}
@@ -312,18 +372,18 @@ func (dv *openShiftClusterDynamicValidator) validateSubnet(ctx context.Context,
 
 // validateVnet checks that the vnet does not have custom dns servers set
 // and the subnets do not overlap with cluster pod/service CIDR blocks
-func (dv *openShiftClusterDynamicValidator) validateVnet(ctx context.Context, vnet *mgmtnetwork.VirtualNetwork) error {
-	dv.log.Print("validateVnet")
+func validateVnet(ctx context.Context, log *logrus.Entry, oc *api.OpenShiftCluster, vnet *mgmtnetwork.VirtualNetwork) error {
+	log.Print("validateVnet")
 	var err error
 
-	if !strings.EqualFold(*vnet.Location, dv.oc.Location) {
-		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, "", "The vnet location '%s' must match the cluster location '%s'.", *vnet.Location, dv.oc.Location)
+	if !strings.EqualFold(*vnet.Location, oc.Location) {
+		return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidLinkedVNet, "", "The vnet location '%s' must match the cluster location '%s'.", *vnet.Location, oc.Location)
 	}
 
 	// unique names of subnets from all node pools
 	var subnets []string
 	var CIDRArray []*net.IPNet
-	for i, subnet := range dv.oc.Properties.WorkerProfiles {
+	for i, subnet := range oc.Properties.WorkerProfiles {
 		exists := false
 		for _, s := range subnets {
 			if strings.EqualFold(strings.ToLower(subnet.SubnetID), strings.ToLower(s)) {
@@ -333,24 +393,24 @@ func (dv *openShiftClusterDynamicValidator) validateVnet(ctx context.Context, vn
 		}
 		if !exists {
 			subnets = append(subnets, subnet.SubnetID)
-			c, err := dv.validateSubnet(ctx, vnet, "properties.workerProfiles["+strconv.Itoa(i)+"].subnetId", subnet.SubnetID)
+			c, err := validateSubnet(ctx, log, oc, vnet, "properties.workerProfiles["+strconv.Itoa(i)+"].subnetId", subnet.SubnetID)
 			if err != nil {
 				return err
 			}
 			CIDRArray = append(CIDRArray, c)
 		}
 	}
-	masterSubnetCIDR, err := dv.validateSubnet(ctx, vnet, "properties.masterProfile.subnetId", dv.oc.Properties.MasterProfile.SubnetID)
+	masterSubnetCIDR, err := validateSubnet(ctx, log, oc, vnet, "properties.masterProfile.subnetId", oc.Properties.MasterProfile.SubnetID)
 	if err != nil {
 		return err
 	}
 
-	_, podCIDR, err := net.ParseCIDR(dv.oc.Properties.NetworkProfile.PodCIDR)
+	_, podCIDR, err := net.ParseCIDR(oc.Properties.NetworkProfile.PodCIDR)
 	if err != nil {
 		return err
 	}
 
-	_, serviceCIDR, err := net.ParseCIDR(dv.oc.Properties.NetworkProfile.ServiceCIDR)
+	_, serviceCIDR, err := net.ParseCIDR(oc.Properties.NetworkProfile.ServiceCIDR)
 	if err != nil {
 		return err
 	}
@@ -371,10 +431,10 @@ func (dv *openShiftClusterDynamicValidator) validateVnet(ctx context.Context, vn
 	return nil
 }
 
-func (dv *openShiftClusterDynamicValidator) validateProviders(ctx context.Context) error {
-	dv.log.Print("validateProviders")
+func validateProviders(ctx context.Context, log *logrus.Entry, spProviders features.ProvidersClient) error {
+	log.Print("validateProviders")
 
-	providers, err := dv.spProviders.List(ctx, nil, "")
+	providers, err := spProviders.List(ctx, nil, "")
 	if err != nil {
 		return err
 	}
diff --git a/pkg/api/validate/quota.go b/pkg/api/validate/quota.go
index d90f72cf2..c1fad9519 100644
--- a/pkg/api/validate/quota.go
+++ b/pkg/api/validate/quota.go
@@ -101,7 +101,7 @@ func NewAzureQuotaValidator(ctx context.Context, log *logrus.Entry, env env.Inte
 		return nil, err
 	}
 
-	spAuthorizer, err := validateServicePrincipalProfile(ctx, log, env, oc, subscriptionDoc)
+	spAuthorizer, err := validateServicePrincipalProfile(ctx, log, &oc.Properties.ServicePrincipalProfile, subscriptionDoc.Subscription.Properties.TenantID, env.Environment().ResourceManagerEndpoint)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/cluster/deploystorage.go b/pkg/cluster/deploystorage.go
index 375933e37..47b10499f 100644
--- a/pkg/cluster/deploystorage.go
+++ b/pkg/cluster/deploystorage.go
@@ -47,7 +47,7 @@ func (m *manager) clusterSPObjectID(ctx context.Context) (string, error) {
 	var clusterSPObjectID string
 	spp := &m.doc.OpenShiftCluster.Properties.ServicePrincipalProfile
 
-	token, err := aad.GetToken(ctx, m.log, m.doc.OpenShiftCluster, m.subscriptionDoc, m.env.Environment().GraphEndpoint)
+	token, err := aad.GetToken(ctx, m.log, &m.doc.OpenShiftCluster.Properties.ServicePrincipalProfile, m.subscriptionDoc.Subscription.Properties.TenantID, m.env.Environment().GraphEndpoint)
 	if err != nil {
 		return "", err
 	}
diff --git a/pkg/cluster/validate.go b/pkg/cluster/validate.go
index ce6ba088f..7b8f6dc11 100644
--- a/pkg/cluster/validate.go
+++ b/pkg/cluster/validate.go
@@ -11,7 +11,7 @@ import (
 )
 
 func (m *manager) validateResources(ctx context.Context) error {
-	ocDynamicValidator := validate.NewOpenShiftClusterDynamicValidator(
+	ocDynamicValidator := validate.NewOpenShiftClusterFullDynamicValidator(
 		m.log, m.env, m.doc.OpenShiftCluster, m.subscriptionDoc, m.fpAuthorizer,
 	)
 	return ocDynamicValidator.Dynamic(ctx)
diff --git a/pkg/operator/apis/aro.openshift.io/v1alpha1/cluster_types.go b/pkg/operator/apis/aro.openshift.io/v1alpha1/cluster_types.go
index f75d491c1..eb312a71c 100644
--- a/pkg/operator/apis/aro.openshift.io/v1alpha1/cluster_types.go
+++ b/pkg/operator/apis/aro.openshift.io/v1alpha1/cluster_types.go
@@ -36,11 +36,12 @@ type ClusterSpec struct {
 	ResourceID      string              `json:"resourceId,omitempty"`
 	ACRName         string              `json:"acrName,omitempty"`
 	ACRDomain       string              `json:"acrDomain,omitempty"`
+	AZEnvironment   string              `json:"azEnvironment,omitempty"`
 	Location        string              `json:"location,omitempty"`
 	GenevaLogging   GenevaLoggingSpec   `json:"genevaLogging,omitempty"`
 	InternetChecker InternetCheckerSpec `json:"internetChecker,omitempty"`
-
-	Features FeaturesSpec `json:"features,omitempty"`
+	VNetID          string              `json:"vnetId,omitempty"`
+	Features        FeaturesSpec        `json:"features,omitempty"`
 }
 
 // FeaturesSpec defines ARO operator feature gates
diff --git a/pkg/operator/deploy/bindata.go b/pkg/operator/deploy/bindata.go
index ce0110e90..7325d7dcf 100644
--- a/pkg/operator/deploy/bindata.go
+++ b/pkg/operator/deploy/bindata.go
@@ -86,7 +86,7 @@ func (fi bindataFileInfo) Sys() interface{} {
 	return nil
 }
 
-var _aroOpenshiftIo_clustersYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x58\xcd\x6e\xe3\xc8\x11\xbe\xeb\x29\x0a\xce\xc1\x87\x58\xf4\x0c\xf6\x92\xe8\x66\x78\x76\x03\x23\x9b\x5d\x63\x6c\xcc\x65\xbd\x87\x62\xb3\x44\x55\xdc\xec\x66\xaa\x8b\xf6\x68\x82\xbc\x7b\x50\x4d\x52\xa2\x64\x6a\xe4\x04\x58\x5e\x04\x56\x77\xd7\x7f\x7d\xfd\x89\x8b\xe5\x72\xb9\xc0\x96\xbf\x90\x24\x8e\x61\x05\xd8\x32\x7d\x55\x0a\xf6\x96\x8a\xe7\xbf\xa4\x82\xe3\xf5\xcb\xc7\x92\x14\x3f\x2e\x9e\x39\x54\x2b\xb8\xed\x92\xc6\xe6\x33\xa5\xd8\x89\xa3\x4f\xb4\xe6\xc0\xca\x31\x2c\x1a\x52\xac\x50\x71\xb5\x00\xc0\x10\xa2\xa2\x89\x93\xbd\x02\xb8\x18\x54\xa2\xf7\x24\xcb\x9a\x42\xf1\xdc\x95\x54\x76\xec\x2b\x92\x6c\x61\xb4\xff\xf2\xa1\xf8\xa1\xf8\xb0\x00\x70\x42\xf9\xf8\x23\x37\x94\x14\x9b\x76\x05\xa1\xf3\x7e\x01\x10\xb0\xa1\x15\x38\xdf\x25\x25\x49\x05\x4a\x2c\x62\x4b\x21\x6d\x78\xad\x05\xc7\x45\x6a\xc9\x99\xcd\x5a\x62\xd7\xae\xe0\xcd\x7a\xaf\x61\x70\x6b\x08\xa9\x57\x96\x25\x9e\x93\xfe\x7d\x2a\xfd\x99\x93\xe6\x95\xd6\x77\x82\x7e\x6f\x3a\x0b\x13\x87\xba\xf3\x28\x3b\xf1\x02\x20\xb9\xd8\xd2\x54\x6b\xea\x4a\x19\xf2\x35\xd8\x4d\x8a\xda\xa5\x15\xfc\xfb\x3f\x0b\x80\x17\xf4\x5c\xe5\x68\xfb\x45\x73\xf7\xe6\xfe\xee\xcb\x0f\x0f\x6e\x43\x0d\xf6\x42\x80\x8a\x92\x13\x6e\xf3\xbe\x51\x39\x70\x02\xdd\x10\xf4\x3b\x61\x1d\x25\xbf\x8e\x2e\xc2\xcd\xfd\xdd\x70\xba\x95\xd8\x92\x28\x8f\x1e\xd8\x33\xa9\xfc\x4e\x76\x64\xe7\xd2\x1c\xe9\xf7\x40\x65\xb5\xa6\xde\xe0\x50\x31\xaa\x20\xf5\xa6\xe3\x1a\x74\xc3\x09\x84\x5a\xa1\x44\xa1\xaf\xfe\x44\x2d\xd8\x16\x0c\x10\xcb\x7f\x92\xd3\x02\x1e\x48\x4c\x09\xa4\x4d\xec\x7c\x65\x0d\xf2\x42\xa2\x20\xe4\x62\x1d\xf8\xdb\x4e\x73\x02\x8d\xd9\xa4\x47\xa5\xa1\x14\xe3\xc3\x41\x49\x02\x7a\x4b\x61\x47\x57\x80\xa1\x82\x06\xb7\x20\x64\x36\xa0\x0b\x13\x6d\x79\x4b\x2a\xe0\x1f\x51\x08\x38\xac\xe3\x0a\x36\xaa\x6d\x5a\x5d\x5f\xd7\xac\x63\xaf\xbb\xd8\x34\x5d\x60\xdd\x5e\xe7\x8e\xe5\xb2\xd3\x28\xe9\xba\xa2\x17\xf2\xd7\x89\xeb\x25\x8a\xdb\xb0\x92\xd3\x4e\xe8\x1a\x5b\x5e\x66\xc7\x43\x6e\xf5\xa2\xa9\xfe\xb4\x2b\xf4\xe5\xc4\x53\xdd\x5a\x43\x24\x15\x0e\xf5\x4e\x9c\x7b\xef\x64\xde\xad\x07\xad\xba\x38\x1c\xeb\xfd\xdf\xa7\xd7\x44\x96\x95\xcf\x3f\x3e\x3c\xc2\x68\x34\x97\xe0\x30\xe7\x39\xdb\xfb\x63\x69\x9f\x78\x4b\x14\x87\x35\x49\x5f\xb8\xb5\xc4\x26\x6b\xa4\x50\xb5\x91\x83\x0e\x9d\xc4\x14\x0e\x93\x9e\xba\xb2\x61\xb5\x4a\xff\xab\xa3\xa4\x56\x9f\x02\x6e\xf3\xc4\x43\x49\xd0\xb5\x15\x2a\x55\x05\xdc\x05\xb8\xc5\x86\xfc\x2d\x26\xfa\xc3\xd3\x6e\x19\x4e\x4b\x4b\xe9\xf9\xc4\x4f\x81\xea\x70\x63\x9f\xad\x9d\x78\x84\x92\xd9\x0a\x0d\x13\xf8\xd0\x92\x3b\x98\x8c\x8a\x12\x8b\x75\xaf\xa2\x92\xf5\xfc\x14\x5d\x4e\xcf\x62\x9e\x47\x27\x9f\x62\x83\x1c\x0e\xc5\x27\xc2\x18\x4e\xfc\x62\x78\xf8\xde\xfd\x6b\x42\xcb\x60\x3a\x3e\x70\x10\xd9\x4f\xc3\xa6\x83\xd0\x6e\x3e\xff\x6a\xc0\x24\xa8\x51\x46\x35\x50\xdb\x48\x1e\xa9\x3a\x15\x5c\x5e\x33\xd4\x48\x4a\x41\xef\x25\x36\xa4\x1b\xea\x66\x76\x8d\xfe\x97\x31\x7a\xc2\x30\x1b\xdb\x51\xa5\xec\xa9\x29\xd0\x0b\xfe\x1c\xeb\x9a\x43\x7d\xac\xf5\x7b\x5e\xb9\x18\xd6\x5c\xcf\x20\xe1\xee\x30\xaa\xe1\xcc\x0a\x2e\x7f\xfb\xb0\xfc\xeb\xef\x7f\x2e\xfa\x9f\xcb\x93\x9e\xcf\x64\xde\x9e\x26\x06\xd6\x68\x4b\x7f\xbb\x7d\xf8\x31\xbc\xb0\xc4\xd0\x50\xd0\x39\x9b\x14\xba\x66\x4e\xbe\x84\x4f\x8c\x75\x88\x49\xd9\xa5\x7b\x89\xd5\xec\x9e\xc7\x63\xa4\x3c\xeb\xdd\xc9\xb4\xf6\x18\x4b\x7a\xbb\x21\xf7\x4c\xf2\xbf\x24\xb6\x13\x3f\x5b\x5e\x56\x6a\x66\x17\xce\xe4\x6f\x5c\x46\x11\xdc\xbe\xd7\x7f\x1f\xdd\xe4\x6e\x7d\x87\xa5\x11\x4c\xef\xaa\xef\x4e\xc9\xc8\x80\xee\x3e\x8d\x97\xf0\xcd\x37\x9b\x89\xfd\xf1\xfe\x4e\xa4\x09\x33\x78\x87\xfd\x79\x20\xea\xe9\xc2\x39\x28\xca\xbb\x0e\xc0\x28\x96\xc9\xd0\xfe\xff\x42\x23\x17\x43\xc5\x13\x0a\x77\xca\xf8\x6e\xdb\x70\x5d\x91\x66\x3b\xa3\x18\x38\x24\xc5\xe0\x28\x15\x47\x6a\x4e\xb4\xc1\x81\xf6\x8b\xbd\x9e\xfd\x1d\xd6\xd3\x08\x8b\x2c\x57\xf6\x80\x58\x5c\xa6\x3e\xd6\x63\x63\xf6\x4c\x5c\x45\x21\x3b\xb3\x23\xbb\xd0\x90\xdb\x60\xe0\xd4\xe4\x86\x0f\x15\x55\xc6\x3b\xec\x3e\x4b\x54\xc1\xeb\x86\x82\x25\x74\x46\x69\x45\x8a\xec\xd3\xce\x89\xbd\x5b\x66\xc3\x2e\x45\x84\x56\x38\x0a\xc3\x73\x88\xaf\x01\xa2\xc0\x6b\xa6\x3c\x79\xad\x6d\xfd\x71\x2b\xe7\x36\x88\x80\xde\xef\x73\x97\xd5\x43\xcd\x2f\x14\xc0\xa8\x41\x01\x4f\x61\x1a\xcf\xc0\xa2\x4a\x02\xac\x2a\x9a\x03\x05\x8d\x40\x5f\x5b\xcf\x8e\xd5\x6f\x7b\xba\xb5\x9d\xd4\x1e\x74\x83\x6a\xc1\x4a\xca\x34\xca\xc5\xa6\x8d\x21\x67\xdb\xe5\x64\x95\xb1\x9b\x83\x14\x41\xdd\x64\x0a\x81\x21\x33\x02\x96\x9e\x99\xc4\x44\x07\xda\x73\x2e\x33\xdd\xb0\xcb\x31\x93\x8d\x68\x27\x67\x54\x4e\x72\x98\x0a\xf8\x35\x38\x1a\x7a\xba\xba\xca\x4d\xdd\x10\x06\x33\x92\x53\xb2\xef\x0f\x87\x01\x7a\x0e\x32\xa3\xd3\x8a\x5b\x53\x05\x28\x25\xab\xa0\xb0\xdf\xc2\x12\xd8\x76\xbb\xd8\x50\x82\x16\x45\xc7\x81\xbd\xb9\xbf\xeb\xb9\xe4\x06\xfb\x31\x4a\xd8\xcc\x29\x2d\xd1\x3d\xbf\xa2\x54\x69\x99\x77\xaf\xa3\xf4\x6f\x96\x3b\x54\x2e\xd9\xb3\xe6\x54\x3b\x92\x30\x74\xc8\xb6\x0f\x3b\xdb\x9b\x8b\x7d\xe7\x41\x71\xf1\xf6\xfa\xfc\x0e\xd6\x02\x78\x4c\xfa\x28\x18\x12\x8f\x7f\x9c\xe6\x21\x76\x1d\xa5\x41\x5d\x81\xd1\xb4\xa5\xf2\x6c\x64\x67\x81\xb8\xa1\x94\xb0\x3e\x61\xe1\xcc\x59\x21\x4c\xf3\x17\xed\x29\x68\xf9\x9c\x4f\x18\xbe\x1c\x0d\x27\x42\x0c\xb4\x7c\x8d\x52\x5d\xed\xc9\xe6\xac\x62\x38\xfa\x67\xb2\x03\x67\x54\xaa\xa3\x6c\xed\xdd\x61\x97\x68\xb7\xd0\x89\x50\xd0\x01\x7b\xe7\xe0\xc4\x9e\x3b\x9d\xf1\x2a\x43\x06\x87\x5c\x79\x36\x8d\x9d\xb6\x9d\x5e\x41\xea\xdc\x06\x30\x65\x9f\x3d\x87\x53\x8e\xda\x9f\x63\xa7\x1e\x6a\x43\xd2\xe1\xa8\xf5\x17\x07\x48\x5d\xd3\xa0\xf0\xb7\xdc\xfe\xae\x77\x71\x40\x87\xec\xfc\x09\x3f\xcf\x14\xe4\xed\xf5\xf2\xee\xa3\x79\xf9\x7c\x25\xf7\x30\xfe\xb8\x6d\x69\xbc\x30\xed\xf0\x2e\xdd\xbb\x39\xce\xa1\x1e\xb3\xca\x89\x3d\x76\xe8\xfd\xd6\x46\x7f\x2c\x78\x05\xd6\x01\x06\xac\x69\x13\x45\xa1\xdd\x48\xfe\xcf\x31\x85\xc8\x6c\xec\x94\xd6\x01\x3d\x39\x54\x6c\xfd\x30\xdc\x96\x9c\x21\x1f\x9e\x2e\xb0\x0c\x36\x33\x7e\xa9\xd2\xd1\xd3\x05\xb4\xd1\xa3\xb0\x6e\x0b\xf8\x29\xce\x01\x98\x3d\xf4\x15\x9b\xd6\xd3\x15\xf0\x71\x7c\xa3\x95\xd4\xdf\x2a\x68\xea\xd8\x6d\xfb\x3e\xca\xdf\x02\xae\x4e\x05\x9f\xbd\xe1\xd4\x7f\x31\x78\xba\x00\x87\x29\x27\xb3\x95\x58\x62\xe9\xb7\x79\x87\xf9\x7a\x05\x29\x1e\x9a\xfd\x7e\xe4\xa5\x0d\x82\xf7\x54\xc1\xd3\xc5\x5d\x18\xd4\xcf\x20\x10\x9c\xeb\x88\xfe\x0a\xa0\x37\xd4\xc9\x48\x69\xdf\x66\x33\x0b\xa6\xf1\x8d\xf8\x24\xa1\x3b\xcd\x02\xc7\xff\x27\x27\xa8\xfc\xfb\x19\xd7\x91\x68\xff\x79\xea\x23\xfa\x76\x83\x1f\xf7\xb2\x3c\x35\xcb\xe1\xa3\xd4\x64\x19\xa0\x27\x5e\x2b\xb0\x9e\x19\xbe\xf9\x44\x31\xc8\xec\x25\xfb\x99\x43\xe7\xa8\x55\xaa\x7e\x39\xfe\x2c\x75\x71\x71\xf0\xdd\x29\xbf\x4e\x48\x19\xfc\xf6\xfb\xa2\xd7\x4a\xd5\x97\xd1\x1b\x13\xfe\x37\x00\x00\xff\xff\x98\x33\xf5\x3b\xd6\x13\x00\x00")
+var _aroOpenshiftIo_clustersYaml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x58\x41\x73\xeb\xb8\x0d\xbe\xfb\x57\x60\xd2\xc3\x3b\x34\x56\xde\x9b\xbd\xb4\xbe\x65\xf2\x76\x3b\x99\x6e\x77\x33\x2f\x99\x77\xd9\xec\x01\xa2\x60\x09\x0d\x45\xaa\x24\x94\x3c\xbf\x4e\xff\x7b\x07\xa4\x64\xcb\x8e\x14\xa7\x9d\x59\x5d\x3c\x02\x49\x00\xfc\x00\x7c\x80\xb5\x5a\xaf\xd7\x2b\xec\xf8\x2b\x85\xc8\xde\x6d\x00\x3b\xa6\x6f\x42\x4e\xdf\x62\xf1\xf4\x97\x58\xb0\xbf\x7a\xfe\x54\x92\xe0\xa7\xd5\x13\xbb\x6a\x03\x37\x7d\x14\xdf\x7e\xa1\xe8\xfb\x60\xe8\x33\x6d\xd9\xb1\xb0\x77\xab\x96\x04\x2b\x14\xdc\xac\x00\xd0\x39\x2f\xa8\xe2\xa8\xaf\x00\xc6\x3b\x09\xde\x5a\x0a\xeb\x9a\x5c\xf1\xd4\x97\x54\xf6\x6c\x2b\x0a\xc9\xc2\x68\xff\xf9\x63\xf1\x43\xf1\x71\x05\x60\x02\xa5\xe3\x0f\xdc\x52\x14\x6c\xbb\x0d\xb8\xde\xda\x15\x80\xc3\x96\x36\x60\x6c\x1f\x85\x42\x2c\x30\xf8\xc2\x77\xe4\x62\xc3\x5b\x29\xd8\xaf\x62\x47\x46\x6d\xd6\xc1\xf7\xdd\x06\x5e\xad\x67\x0d\x83\x5b\xc3\x95\xb2\xb2\x24\xb1\x1c\xe5\xef\x53\xe9\xcf\x1c\x25\xad\x74\xb6\x0f\x68\x0f\xa6\x93\x30\xb2\xab\x7b\x8b\x61\x2f\x5e\x01\x44\xe3\x3b\x9a\x6a\x8d\x7d\x19\x06\xbc\x06\xbb\x51\x50\xfa\xb8\x81\x7f\xff\x67\x05\xf0\x8c\x96\xab\x74\xdb\xbc\xa8\xee\x5e\xdf\xdd\x7e\xfd\xe1\xde\x34\xd4\x62\x16\x02\x54\x14\x4d\xe0\x2e\xed\x1b\x95\x03\x47\x90\x86\x20\xef\x84\xad\x0f\xe9\x75\x74\x11\xae\xef\x6e\x87\xd3\x5d\xf0\x1d\x05\xe1\xd1\x03\x7d\x26\x91\xdf\xcb\x4e\xec\x7c\x50\x47\xf2\x1e\xa8\x34\xd6\x94\x0d\x0e\x11\xa3\x0a\x62\x36\xed\xb7\x20\x0d\x47\x08\xd4\x05\x8a\xe4\x72\xf4\x27\x6a\x41\xb7\xa0\x03\x5f\xfe\x93\x8c\x14\x70\x4f\x41\x95\x40\x6c\x7c\x6f\x2b\x4d\x90\x67\x0a\x02\x81\x8c\xaf\x1d\x7f\xdf\x6b\x8e\x20\x3e\x99\xb4\x28\x34\x84\x62\x7c\xd8\x09\x05\x87\x56\x21\xec\xe9\x12\xd0\x55\xd0\xe2\x0e\x02\xa9\x0d\xe8\xdd\x44\x5b\xda\x12\x0b\xf8\x87\x0f\x04\xec\xb6\x7e\x03\x8d\x48\x17\x37\x57\x57\x35\xcb\x98\xeb\xc6\xb7\x6d\xef\x58\x76\x57\x29\x63\xb9\xec\xc5\x87\x78\x55\xd1\x33\xd9\xab\xc8\xf5\x1a\x83\x69\x58\xc8\x48\x1f\xe8\x0a\x3b\x5e\x27\xc7\x5d\x4a\xf5\xa2\xad\xfe\xb4\x0f\xf4\x87\x89\xa7\xb2\xd3\x84\x88\x12\xd8\xd5\x7b\x71\xca\xbd\x45\xdc\x35\x07\x35\xba\x38\x1c\xcb\xfe\x1f\xe0\x55\x91\xa2\xf2\xe5\xc7\xfb\x07\x18\x8d\xa6\x10\x1c\x63\x9e\xd0\x3e\x1c\x8b\x07\xe0\x15\x28\x76\x5b\x0a\x39\x70\xdb\xe0\xdb\xa4\x91\x5c\xd5\x79\x76\x32\x64\x12\x93\x3b\x06\x3d\xf6\x65\xcb\xa2\x91\xfe\x57\x4f\x51\x34\x3e\x05\xdc\xa4\x8a\x87\x92\xa0\xef\x2a\x14\xaa\x0a\xb8\x75\x70\x83\x2d\xd9\x1b\x8c\xf4\x87\xc3\xae\x08\xc7\xb5\x42\x7a\x1e\xf8\x29\x51\x1d\x6f\xcc\x68\xed\xc5\x23\x95\xcc\x46\x68\xa8\xc0\xfb\x8e\xcc\x51\x65\x54\x14\x39\x68\xf6\x0a\x0a\x69\xce\x4f\xd9\x65\xb9\x16\x53\x3d\x9a\xf0\xd9\xb7\xc8\xee\x58\xbc\x70\x8d\xe1\xc4\x2f\xca\x87\xef\xde\xff\xfd\x47\xf7\xcc\xc1\xbb\x96\x9c\xbc\xfb\xd4\x96\x50\x71\x8f\xa7\x07\x8e\xf0\xf8\x69\xd8\x74\x04\xc8\xf5\x97\x5f\x95\xce\x02\x8a\x0f\xa3\x1a\xa8\xb5\x90\x4f\x54\x2d\x41\x92\xd6\x94\x6b\xa2\x90\x93\xbb\xe0\x5b\x92\x86\xfa\x99\x5d\xa3\xff\xa5\xf7\x96\xd0\xcd\xde\xed\x24\xbe\xfa\xd4\xe4\xe8\x19\x7f\xf6\x75\xcd\xae\x3e\xd5\xfa\x96\x57\xc6\xbb\x2d\xd7\x33\xfc\xb9\x3f\x8c\xa2\xec\xb4\x81\x0f\xbf\x7d\x5c\xff\xf5\xf7\x3f\x17\xf9\xe7\xc3\xa2\xe7\x33\xc8\xeb\xd3\x7a\xc7\xe2\x75\xe9\x6f\x37\xf7\x6f\x84\x4f\x1f\x72\x7d\x3b\x27\x5f\xc3\x67\xc6\xda\xf9\x28\x6c\xe2\x5d\xf0\xd5\xec\x9e\x87\x53\x7e\x3d\xeb\xdd\x22\xac\x99\x99\x49\x6e\x1a\x32\x4f\x14\xfe\x17\x60\xfb\x60\x67\xc3\xcb\x42\xed\xec\xc2\x19\xfc\xc6\x65\x0c\x01\x77\xef\xf5\xdf\x7a\x33\xe9\xc8\xef\xb0\x34\x52\xf0\x6d\xf5\x66\x95\x8c\x73\xd3\xed\xe7\xb1\x75\x5f\x7f\xd7\x9a\x38\x1c\xcf\x9d\x94\x26\xf3\xc4\xbb\xec\x3f\x3b\x92\xd7\xb6\x17\xb6\xcf\xb3\x5d\x9e\x49\xce\xf1\x5d\xda\x75\xc4\x78\xbe\x8c\xda\x52\xfe\x2f\xca\x33\xde\x55\x3c\x99\x13\x97\x8c\xef\xb7\x0d\x3d\x91\x24\xd9\x19\xc5\xc0\x2e\x0a\x3a\x43\xb1\x38\x51\xb3\x90\x35\x47\xda\x2f\x0e\x7a\x0e\x8d\x32\xcf\x2a\x7a\xb3\x94\x08\x47\xd3\xcb\x87\x98\xef\x7a\x6a\x4c\x9f\x89\xab\x18\x48\xcf\xec\x27\x6a\x68\xc9\x34\xe8\x38\xb6\xa9\x3e\x5c\x45\x95\x0e\x37\xda\x34\x23\x55\xf0\xd2\x90\x53\x40\x67\x94\x56\x24\xc8\x36\xee\x9d\x38\xb8\xa5\x36\xb4\xf3\x22\x74\x81\x7d\x60\x78\x72\xfe\xc5\x81\x0f\xf0\x92\xe6\xaa\xb4\xd6\x75\xf6\x34\xf3\x53\x1a\x78\x40\x6b\x0f\xd8\x25\xf5\x50\xf3\x33\x39\xd0\xf9\xa3\x80\x47\x37\xbd\xcf\x30\xaa\x95\x04\x58\x55\x34\xc7\x21\xe2\x81\xbe\x75\x96\x0d\x8b\xdd\xe5\x99\x6e\x37\x89\x3d\x48\x83\xa2\x97\x0d\x31\xcd\x6a\xc6\xb7\x9d\x77\x09\x6d\x93\xc0\x2a\x7d\x3f\xc7\x40\x01\xa5\x49\x73\x0a\xba\x34\x76\x70\xc8\xe3\x8f\x8f\x74\xa4\x3d\x61\x99\x66\x1a\xed\xc0\x69\xa2\xf1\x7a\x72\x46\xe5\x04\xc3\x58\xc0\xaf\xce\xd0\x90\xd3\xd5\x65\x4a\xea\x96\xd0\xa9\x91\x04\xc9\x21\x3f\x0c\x3a\xc8\x83\xce\x8c\x4e\x0d\x6e\x4d\x15\x60\x28\x59\x02\x06\xb6\x3b\x58\x03\xeb\x6e\xe3\x5b\x8a\xd0\x61\x90\xb1\xbe\xaf\xef\x6e\xf3\xc0\xda\x60\x2e\xa3\x88\xed\x9c\xd2\x12\xcd\xd3\x0b\x86\x2a\xae\xd3\xee\xad\x0f\xf9\x4d\xb1\x43\xe1\x92\x2d\x4b\x82\xda\x50\x70\x43\x86\xec\xf2\xb5\x93\xbd\xb9\xbb\xef\x3d\x28\x2e\x5e\x77\xdb\x37\xa8\x19\xc0\x62\x94\x87\x80\x2e\xf2\xf8\xef\x6c\x9e\x91\xb7\x3e\xb4\x28\x1b\xd0\x59\x70\x2d\x3c\x7b\xb3\xb3\xbc\xdd\x52\x8c\x58\x2f\x58\x38\x73\x36\x10\xc6\xf9\xbe\xbc\x44\x2d\x5f\xd2\x09\xe5\x97\x93\xe2\x44\xf0\x8e\xd6\x2f\x3e\x54\x97\x87\x89\x76\x56\x31\x9c\xfc\xfd\xd9\x73\x39\x0a\xd5\x3e\xec\xf4\xdd\x60\x1f\x69\xbf\xd0\x87\x40\x4e\x06\xee\x9d\xa3\x13\x7d\x6e\x65\xc6\xab\x44\x19\xec\x52\xe4\x59\x35\xf6\xd2\xf5\x72\x09\xb1\x37\x0d\x60\x4c\x3e\x5b\x76\x4b\x8e\xea\x3f\x70\x23\x16\x6a\x65\xd2\xe1\xa8\xe6\x17\x3b\x88\x7d\xdb\x62\xe0\xef\x29\xfd\x4d\x76\x71\x60\x87\xe4\xfc\x82\x9f\x67\x02\xf2\xba\xbd\xbc\xfb\x68\x5a\x3e\x1f\xc9\x03\x8d\x3f\xec\x3a\x1a\xfb\xab\x1e\xde\xc3\xbd\xaf\xe3\x74\xd5\xd3\x21\x74\x62\x8f\x0d\x5a\xbb\xd3\xd2\x1f\x03\x5e\x81\x66\x80\x12\x6b\x6c\x7c\x10\xe8\x9a\x90\xfe\xd8\x4c\x29\x32\x19\x5b\xd2\x3a\xb0\x27\xbb\x8a\x35\x1f\x86\x6e\xc9\x89\xf2\xe1\xf1\x02\x4b\xa7\x35\x63\xd7\x12\x7a\x7a\xbc\x80\xce\x5b\x0c\x2c\xbb\x02\x7e\xf2\x73\x04\xa6\x0f\x7d\xc3\xb6\xb3\x74\x09\x7c\x7a\xbf\xd1\x4a\xcc\x5d\x05\x55\x1d\x9b\x5d\xce\xa3\xf4\xc1\xe1\x72\xe9\xf2\xc9\x1b\x8e\xf9\xb3\xc4\xe3\x05\x18\x8c\x09\xcc\x2e\xf8\x12\x4b\xbb\x4b\x3b\xd4\xd7\x4b\x88\xfe\xd8\xec\xdb\x37\x2f\xb5\x10\xac\xa5\x0a\x1e\x2f\x6e\xdd\xa0\x7e\x86\x81\xe0\x5c\x46\xe4\x16\x40\xaf\xa6\x1d\x9d\x61\x73\x9a\xcd\x2c\xa8\xc6\x57\xe2\xc5\xf9\x6f\x79\x68\x1c\xff\xce\x2c\x4c\xfe\xef\x9f\xb8\x4e\x44\x87\x6f\x60\x9f\xd0\x76\x0d\x7e\x3a\xc8\x52\xd5\xac\x87\x2f\x5f\x93\x65\x80\x3c\x78\x6d\x40\x73\x66\xf8\xb0\xe4\x83\x52\x66\x96\x1c\x6a\x0e\x8d\xa1\x4e\xa8\xfa\xe5\xf4\xdb\xd7\xc5\xc5\xd1\xc7\xad\xf4\x3a\x19\xca\xe0\xb7\xdf\x57\x59\x2b\x55\x5f\x47\x6f\x54\xf8\xdf\x00\x00\x00\xff\xff\xe6\x41\x09\x04\x3b\x14\x00\x00")
 
 func aroOpenshiftIo_clustersYamlBytes() ([]byte, error) {
 	return bindataRead(
diff --git a/pkg/operator/deploy/deploy.go b/pkg/operator/deploy/deploy.go
index 8f02b6f63..80857e414 100644
--- a/pkg/operator/deploy/deploy.go
+++ b/pkg/operator/deploy/deploy.go
@@ -35,6 +35,7 @@ import (
 	"github.com/Azure/ARO-RP/pkg/util/pullsecret"
 	"github.com/Azure/ARO-RP/pkg/util/ready"
 	"github.com/Azure/ARO-RP/pkg/util/restconfig"
+	"github.com/Azure/ARO-RP/pkg/util/subnet"
 	"github.com/Azure/ARO-RP/pkg/util/tls"
 	"github.com/Azure/ARO-RP/pkg/util/version"
 )
@@ -138,6 +139,11 @@ func (o *operator) resources() ([]runtime.Object, error) {
 		return nil, fmt.Errorf("unsupported cloud environment")
 	}
 
+	vnetID, _, err := subnet.Split(o.oc.Properties.MasterProfile.SubnetID)
+	if err != nil {
+		return nil, err
+	}
+
 	// create a secret here for genevalogging, later we will copy it to
 	// the genevalogging namespace.
 	return append(results,
@@ -157,9 +163,11 @@ func (o *operator) resources() ([]runtime.Object, error) {
 				Name: arov1alpha1.SingletonClusterName,
 			},
 			Spec: arov1alpha1.ClusterSpec{
-				ResourceID: o.oc.ID,
-				ACRDomain:  o.env.ACRDomain(),
-				Location:   o.env.Location(),
+				ResourceID:    o.oc.ID,
+				ACRDomain:     o.env.ACRDomain(),
+				AZEnvironment: o.env.Environment().Name,
+				Location:      o.env.Location(),
+				VNetID:        vnetID,
 				GenevaLogging: arov1alpha1.GenevaLoggingSpec{
 					ConfigVersion:            o.env.ClustersGenevaLoggingConfigVersion(),
 					MonitoringGCSEnvironment: o.env.ClustersGenevaLoggingEnvironment(),
diff --git a/pkg/operator/deploy/staticresources/aro.openshift.io_clusters.yaml b/pkg/operator/deploy/staticresources/aro.openshift.io_clusters.yaml
index f7262dc89..b06cc368c 100644
--- a/pkg/operator/deploy/staticresources/aro.openshift.io_clusters.yaml
+++ b/pkg/operator/deploy/staticresources/aro.openshift.io_clusters.yaml
@@ -40,6 +40,8 @@ spec:
               type: string
             acrName:
               type: string
+            azEnvironment:
+              type: string
             features:
               description: FeaturesSpec defines ARO operator feature gates
               properties:
@@ -69,6 +71,8 @@ spec:
             resourceId:
               description: ResourceID is the Azure resourceId of the cluster
               type: string
+            vnetId:
+              type: string
           type: object
         status:
           description: ClusterStatus defines the observed state of Cluster
diff --git a/pkg/util/aad/aad.go b/pkg/util/aad/aad.go
index 071bad8be..74979a6ec 100644
--- a/pkg/util/aad/aad.go
+++ b/pkg/util/aad/aad.go
@@ -20,10 +20,8 @@ import (
 
 // GetToken authenticates in the customer's tenant as the cluster service
 // principal and returns a token.
-func GetToken(ctx context.Context, log *logrus.Entry, oc *api.OpenShiftCluster, subscriptionDoc *api.SubscriptionDocument, resource string) (*adal.ServicePrincipalToken, error) {
-	spp := &oc.Properties.ServicePrincipalProfile
-
-	conf := auth.NewClientCredentialsConfig(spp.ClientID, string(spp.ClientSecret), subscriptionDoc.Subscription.Properties.TenantID)
+func GetToken(ctx context.Context, log *logrus.Entry, spp *api.ServicePrincipalProfile, tenantID, resource string) (*adal.ServicePrincipalToken, error) {
+	conf := auth.NewClientCredentialsConfig(spp.ClientID, string(spp.ClientSecret), tenantID)
 	conf.Resource = resource
 
 	sp, err := conf.ServicePrincipalToken()


Test patch:
diff --git a/pkg/api/validate/openshiftcluster_validatedynamic_test.go b/pkg/api/validate/openshiftcluster_validatedynamic_test.go
index a5d53efe4..d10bfc92e 100644
--- a/pkg/api/validate/openshiftcluster_validatedynamic_test.go
+++ b/pkg/api/validate/openshiftcluster_validatedynamic_test.go
@@ -147,12 +147,7 @@ func TestValidateProviders(t *testing.T) {
 
 			tt.mocks(providerClient)
 
-			dv := &openShiftClusterDynamicValidator{
-				log:         logrus.NewEntry(logrus.StandardLogger()),
-				spProviders: providerClient,
-			}
-
-			err := dv.validateProviders(ctx)
+			err := validateProviders(ctx, logrus.NewEntry(logrus.StandardLogger()), providerClient)
 			if err != nil && err.Error() != tt.wantErr ||
 				err == nil && tt.wantErr != "" {
 				t.Error(err)
@@ -466,12 +461,7 @@ func TestValidateVnet(t *testing.T) {
 				tt.modifyVnet(vnet)
 			}
 
-			dv := &openShiftClusterDynamicValidator{
-				log: logrus.NewEntry(logrus.StandardLogger()),
-				oc:  oc,
-			}
-
-			err := dv.validateVnet(ctx, vnet)
+			err := validateVnet(ctx, logrus.NewEntry(logrus.StandardLogger()), oc, vnet)
 			if err != nil && err.Error() != tt.wantErr ||
 				err == nil && tt.wantErr != "" {
 				t.Error(err)
@@ -488,10 +478,6 @@ func TestValidateVnetPermissions(t *testing.T) {
 	controller := gomock.NewController(t)
 	defer controller.Finish()
 
-	dv := &openShiftClusterDynamicValidator{
-		log: logrus.NewEntry(logrus.StandardLogger()),
-	}
-
 	for _, tt := range []struct {
 		name    string
 		mocks   func(*mock_authorization.MockPermissionsClient, func())
@@ -563,7 +549,7 @@ func TestValidateVnetPermissions(t *testing.T) {
 
 			tt.mocks(permissionsClient, cancel)
 
-			err := dv.validateVnetPermissions(ctx, mockrefreshable.NewMockAuthorizer(controller), permissionsClient, vnetID, &azure.Resource{}, api.CloudErrorCodeInvalidResourceProviderPermissions, "resource provider")
+			err := validateVnetPermissions(ctx, logrus.NewEntry(logrus.StandardLogger()), mockrefreshable.NewMockAuthorizer(controller), permissionsClient, vnetID, &azure.Resource{}, api.CloudErrorCodeInvalidResourceProviderPermissions, "resource provider")
 			if err != nil && err.Error() != tt.wantErr ||
 				err == nil && tt.wantErr != "" {
 				t.Error(err)
@@ -583,10 +569,6 @@ func TestValidateRouteTablePermissionsSubnet(t *testing.T) {
 	controller := gomock.NewController(t)
 	defer controller.Finish()
 
-	dv := &openShiftClusterDynamicValidator{
-		log: logrus.NewEntry(logrus.StandardLogger()),
-	}
-
 	for _, tt := range []struct {
 		name    string
 		mocks   func(*mock_authorization.MockPermissionsClient, func())
@@ -693,7 +675,7 @@ func TestValidateRouteTablePermissionsSubnet(t *testing.T) {
 				tt.vnet(vnet)
 			}
 
-			err := dv.validateRouteTablePermissionsSubnet(ctx, mockrefreshable.NewMockAuthorizer(controller), permissionsClient, vnet, tt.subnet, "properties.masterProfile.subnetId", api.CloudErrorCodeInvalidResourceProviderPermissions, "resource provider")
+			err := validateRouteTablePermissionsSubnet(ctx, logrus.NewEntry(logrus.StandardLogger()), mockrefreshable.NewMockAuthorizer(controller), permissionsClient, vnet, tt.subnet, "properties.masterProfile.subnetId", api.CloudErrorCodeInvalidResourceProviderPermissions, "resource provider")
 			if err != nil && err.Error() != tt.wantErr ||
 				err == nil && tt.wantErr != "" {
 				t.Error(err)