pkg/api/v20200430/openshiftcluster_validatestatic.go (299 lines of code) (raw):
package v20200430
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"fmt"
"net"
"net/http"
"net/url"
"strings"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/ARO-RP/pkg/api"
"github.com/Azure/ARO-RP/pkg/api/util/immutable"
apisubnet "github.com/Azure/ARO-RP/pkg/api/util/subnet"
"github.com/Azure/ARO-RP/pkg/api/util/uuid"
"github.com/Azure/ARO-RP/pkg/api/validate"
"github.com/Azure/ARO-RP/pkg/util/pullsecret"
)
type openShiftClusterStaticValidator struct {
location string
domain string
requireD2sWorkers bool
resourceID string
r azure.Resource
}
// Validate validates an OpenShift cluster
func (sv openShiftClusterStaticValidator) Static(_oc interface{}, _current *api.OpenShiftCluster, location, domain string, requireD2sWorkers bool, resourceID string) error {
sv.location = location
sv.domain = domain
sv.requireD2sWorkers = requireD2sWorkers
sv.resourceID = resourceID
oc := _oc.(*OpenShiftCluster)
var current *OpenShiftCluster
if _current != nil {
current = (&openShiftClusterConverter{}).ToExternal(_current).(*OpenShiftCluster)
}
var err error
sv.r, err = azure.ParseResourceID(sv.resourceID)
if err != nil {
return err
}
err = sv.validate(oc, current == nil)
if err != nil {
return err
}
if current == nil {
return nil
}
return sv.validateDelta(oc, current)
}
func (sv openShiftClusterStaticValidator) validate(oc *OpenShiftCluster, isCreate bool) error {
if !strings.EqualFold(oc.ID, sv.resourceID) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeMismatchingResourceID, "id", fmt.Sprintf("The provided resource ID '%s' did not match the name in the Url '%s'.", oc.ID, sv.resourceID))
}
if !strings.EqualFold(oc.Name, sv.r.ResourceName) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeMismatchingResourceName, "name", fmt.Sprintf("The provided resource name '%s' did not match the name in the Url '%s'.", oc.Name, sv.r.ResourceName))
}
if !strings.EqualFold(oc.Type, resourceProviderNamespace+"/"+resourceType) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeMismatchingResourceType, "type", fmt.Sprintf("The provided resource type '%s' did not match the name in the Url '%s'.", oc.Type, resourceProviderNamespace+"/"+resourceType))
}
if !strings.EqualFold(oc.Location, sv.location) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "location", fmt.Sprintf("The provided location '%s' is invalid.", oc.Location))
}
return sv.validateProperties("properties", &oc.Properties, isCreate)
}
func (sv openShiftClusterStaticValidator) validateProperties(path string, p *OpenShiftClusterProperties, isCreate bool) error {
switch p.ProvisioningState {
case ProvisioningStateCreating, ProvisioningStateUpdating,
ProvisioningStateAdminUpdating, ProvisioningStateDeleting,
ProvisioningStateSucceeded, ProvisioningStateFailed:
default:
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".provisioningState", fmt.Sprintf("The provided provisioning state '%s' is invalid.", p.ProvisioningState))
}
if err := sv.validateClusterProfile(path+".clusterProfile", &p.ClusterProfile, isCreate); err != nil {
return err
}
if err := sv.validateConsoleProfile(path+".consoleProfile", &p.ConsoleProfile); err != nil {
return err
}
if err := sv.validateServicePrincipalProfile(path+".servicePrincipalProfile", p.ServicePrincipalProfile); err != nil {
return err
}
if err := sv.validateNetworkProfile(path+".networkProfile", &p.NetworkProfile); err != nil {
return err
}
if err := sv.validateMasterProfile(path+".masterProfile", &p.MasterProfile); err != nil {
return err
}
if err := sv.validateAPIServerProfile(path+".apiserverProfile", &p.APIServerProfile); err != nil {
return err
}
if isCreate {
if len(p.WorkerProfiles) != 1 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".workerProfiles", "There should be exactly one worker profile.")
}
if err := sv.validateWorkerProfile(path+".workerProfiles['"+p.WorkerProfiles[0].Name+"']", &p.WorkerProfiles[0], &p.MasterProfile); err != nil {
return err
}
if len(p.IngressProfiles) != 1 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ingressProfiles", "There should be exactly one ingress profile.")
}
if err := sv.validateIngressProfile(path+".ingressProfiles['"+p.IngressProfiles[0].Name+"']", &p.IngressProfiles[0]); err != nil {
return err
}
}
return nil
}
func (sv openShiftClusterStaticValidator) validateClusterProfile(path string, cp *ClusterProfile, isCreate bool) error {
if pullsecret.Validate(cp.PullSecret) != nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".pullSecret", "The provided pull secret is invalid.")
}
if isCreate {
if !validate.RxDomainName.MatchString(cp.Domain) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".domain", fmt.Sprintf("The provided domain '%s' is invalid.", cp.Domain))
}
} else {
// We currently do not allow domains with a digit as a first charecter,
// for new clusters, but we already have some existing clusters with
// domains like this and we need to allow customers to update them.
if !validate.RxDomainNameRFC1123.MatchString(cp.Domain) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".domain", fmt.Sprintf("The provided domain '%s' is invalid.", cp.Domain))
}
}
// domain ends .aroapp.io, but doesn't end .<rp-location>.aroapp.io
if strings.HasSuffix(cp.Domain, "."+strings.SplitN(sv.domain, ".", 2)[1]) &&
!strings.HasSuffix(cp.Domain, "."+sv.domain) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".domain", fmt.Sprintf("The provided domain '%s' is invalid.", cp.Domain))
}
// domain is of form multiple.names.<rp-location>.aroapp.io
if strings.HasSuffix(cp.Domain, "."+sv.domain) &&
strings.ContainsRune(strings.TrimSuffix(cp.Domain, "."+sv.domain), '.') {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".domain", fmt.Sprintf("The provided domain '%s' is invalid.", cp.Domain))
}
if !validate.RxResourceGroupID.MatchString(cp.ResourceGroupID) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".resourceGroupId", fmt.Sprintf("The provided resource group '%s' is invalid.", cp.ResourceGroupID))
}
if strings.Split(cp.ResourceGroupID, "/")[2] != sv.r.SubscriptionID {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".resourceGroupId", fmt.Sprintf("The provided resource group '%s' is invalid: must be in same subscription as cluster.", cp.ResourceGroupID))
}
if strings.EqualFold(cp.ResourceGroupID, fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", sv.r.SubscriptionID, sv.r.ResourceGroup)) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".resourceGroupId", fmt.Sprintf("The provided resource group '%s' is invalid: must be different from resourceGroup of the OpenShift cluster object.", cp.ResourceGroupID))
}
return nil
}
func (sv openShiftClusterStaticValidator) validateConsoleProfile(path string, cp *ConsoleProfile) error {
if cp.URL != "" {
if _, err := url.Parse(cp.URL); err != nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".url", fmt.Sprintf("The provided console URL '%s' is invalid.", cp.URL))
}
}
return nil
}
func (sv openShiftClusterStaticValidator) validateServicePrincipalProfile(path string, spp *ServicePrincipalProfile) error {
if spp == nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".servicePrincipalProfile", "ServicePrincipalProfile cannot be nil in this API version.")
}
valid := uuid.IsValid(spp.ClientID)
if !valid {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".clientId", fmt.Sprintf("The provided client ID '%s' is invalid.", spp.ClientID))
}
if spp.ClientSecret == "" {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".clientSecret", "The provided client secret is invalid.")
}
return nil
}
func (sv openShiftClusterStaticValidator) validateNetworkProfile(path string, np *NetworkProfile) error {
podIP, pod, err := net.ParseCIDR(np.PodCIDR)
if err != nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".podCidr", fmt.Sprintf("The provided pod CIDR '%s' is invalid: '%s'.", np.PodCIDR, err))
}
for _, s := range api.JoinCIDRRange {
_, cidr, _ := net.ParseCIDR(s)
if cidr.Contains(pod.IP) || pod.Contains(cidr.IP) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidCIDRRange, path, fmt.Sprintf("Azure Red Hat OpenShift uses 100.64.0.0/16, 169.254.169.0/29, and 100.88.0.0/16 IP address ranges internally. Do not include this '%s' IP address range in any other CIDR definitions in your cluster.", np.PodCIDR))
}
}
if pod.IP.To4() == nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".podCidr", fmt.Sprintf("The provided pod CIDR '%s' is invalid: must be IPv4.", np.PodCIDR))
}
ones, _ := pod.Mask.Size()
if ones > 18 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".podCidr", fmt.Sprintf("The provided vnet CIDR '%s' is invalid: must be /18 or larger.", np.PodCIDR))
}
nip := podIP.Mask(pod.Mask)
if nip.String() != podIP.String() {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidNetworkAddress, path+".podCidr", fmt.Sprintf("The provided pod CIDR '%s' is invalid, expecting: '%s/%d'.", np.PodCIDR, nip.String(), ones))
}
serviceIP, service, err := net.ParseCIDR(np.ServiceCIDR)
if err != nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".serviceCidr", fmt.Sprintf("The provided service CIDR '%s' is invalid: '%s'.", np.ServiceCIDR, err))
}
for _, s := range api.JoinCIDRRange {
_, cidr, _ := net.ParseCIDR(s)
if cidr.Contains(service.IP) || service.Contains(cidr.IP) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidCIDRRange, path, fmt.Sprintf("Azure Red Hat OpenShift uses 100.64.0.0/16, 169.254.169.0/29, and 100.88.0.0/16 IP address ranges internally. Do not include this '%s' IP address range in any other CIDR definitions in your cluster.", np.ServiceCIDR))
}
}
if service.IP.To4() == nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".serviceCidr", fmt.Sprintf("The provided service CIDR '%s' is invalid: must be IPv4.", np.ServiceCIDR))
}
ones, _ = service.Mask.Size()
if ones > 22 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".serviceCidr", fmt.Sprintf("The provided vnet CIDR '%s' is invalid: must be /22 or larger.", np.ServiceCIDR))
}
nip = serviceIP.Mask(service.Mask)
if nip.String() != serviceIP.String() {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidNetworkAddress, path+".serviceCidr", fmt.Sprintf("The provided service CIDR '%s' is invalid, expecting: '%s/%d'.", np.ServiceCIDR, nip.String(), ones))
}
return nil
}
func (sv openShiftClusterStaticValidator) validateMasterProfile(path string, mp *MasterProfile) error {
if !validate.VMSizeIsValid(api.VMSize(mp.VMSize), sv.requireD2sWorkers, true) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".vmSize", fmt.Sprintf("The provided master VM size '%s' is invalid.", mp.VMSize))
}
if !validate.RxSubnetID.MatchString(mp.SubnetID) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", fmt.Sprintf("The provided master VM subnet '%s' is invalid.", mp.SubnetID))
}
sr, err := azure.ParseResourceID(mp.SubnetID)
if err != nil {
return err
}
if sr.SubscriptionID != sv.r.SubscriptionID {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", fmt.Sprintf("The provided master VM subnet '%s' is invalid: must be in same subscription as cluster.", mp.SubnetID))
}
return nil
}
func (sv openShiftClusterStaticValidator) validateWorkerProfile(path string, wp *WorkerProfile, mp *MasterProfile) error {
if wp.Name != "worker" {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".name", fmt.Sprintf("The provided worker name '%s' is invalid.", wp.Name))
}
if !validate.VMSizeIsValid(api.VMSize(wp.VMSize), sv.requireD2sWorkers, false) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".vmSize", fmt.Sprintf("The provided worker VM size '%s' is invalid.", wp.VMSize))
}
if !validate.DiskSizeIsValid(wp.DiskSizeGB) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".diskSizeGB", fmt.Sprintf("The provided worker disk size '%d' is invalid.", wp.DiskSizeGB))
}
if !validate.RxSubnetID.MatchString(wp.SubnetID) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", fmt.Sprintf("The provided worker VM subnet '%s' is invalid.", wp.SubnetID))
}
workerVnetID, _, err := apisubnet.Split(wp.SubnetID)
if err != nil {
return err
}
masterVnetID, _, err := apisubnet.Split(mp.SubnetID)
if err != nil {
return err
}
if !strings.EqualFold(masterVnetID, workerVnetID) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", fmt.Sprintf("The provided worker VM subnet '%s' is invalid: must be in the same vnet as master VM subnet '%s'.", wp.SubnetID, mp.SubnetID))
}
if strings.EqualFold(mp.SubnetID, wp.SubnetID) {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".subnetId", fmt.Sprintf("The provided worker VM subnet '%s' is invalid: must be different to master VM subnet '%s'.", wp.SubnetID, mp.SubnetID))
}
if wp.Count < 2 || wp.Count > 50 {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".count", fmt.Sprintf("The provided worker count '%d' is invalid.", wp.Count))
}
return nil
}
func (sv openShiftClusterStaticValidator) validateAPIServerProfile(path string, ap *APIServerProfile) error {
switch ap.Visibility {
case VisibilityPublic, VisibilityPrivate:
default:
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".visibility", fmt.Sprintf("The provided visibility '%s' is invalid.", ap.Visibility))
}
if ap.URL != "" {
if _, err := url.Parse(ap.URL); err != nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".url", fmt.Sprintf("The provided URL '%s' is invalid.", ap.URL))
}
}
if ap.IP != "" {
ip := net.ParseIP(ap.IP)
if ip == nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ip", fmt.Sprintf("The provided IP '%s' is invalid.", ap.IP))
}
if ip.To4() == nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ip", fmt.Sprintf("The provided IP '%s' is invalid: must be IPv4.", ap.IP))
}
}
return nil
}
func (sv openShiftClusterStaticValidator) validateIngressProfile(path string, p *IngressProfile) error {
if p.Name != "default" {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".name", fmt.Sprintf("The provided ingress name '%s' is invalid.", p.Name))
}
switch p.Visibility {
case VisibilityPublic, VisibilityPrivate:
default:
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".visibility", fmt.Sprintf("The provided visibility '%s' is invalid.", p.Visibility))
}
if p.IP != "" {
ip := net.ParseIP(p.IP)
if ip == nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ip", fmt.Sprintf("The provided IP '%s' is invalid.", p.IP))
}
if ip.To4() == nil {
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, path+".ip", fmt.Sprintf("The provided IP '%s' is invalid: must be IPv4.", p.IP))
}
}
return nil
}
func (sv openShiftClusterStaticValidator) validateDelta(oc, current *OpenShiftCluster) error {
err := immutable.Validate("", oc, current)
if err != nil {
err := err.(*immutable.ValidationError)
return api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodePropertyChangeNotAllowed, err.Target, err.Message)
}
return nil
}