@@ -55,6 +55,10 @@ const (
5555 serviceEndpointServiceRegexPattern = `^Microsoft\.[a-zA-Z]{1,42}[a-zA-Z0-9]{0,42}$`
5656 // Must start with an alpha character and then can include alnum OR be only *.
5757 serviceEndpointLocationRegexPattern = `^([a-z]{1,42}\d{0,5}|[*])$`
58+ // described in https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules.
59+ privateEndpointRegex = `^[-\w\._]+$`
60+ // resource ID Pattern.
61+ resourceIDPattern = `(?i)subscriptions/(.+)/resourceGroups/(.+)/providers/(.+?)/(.+?)/(.+)`
5862)
5963
6064var (
@@ -207,6 +211,10 @@ func validateSubnets(subnets Subnets, vnet VnetSpec, fldPath *field.Path) field.
207211 if len (subnet .ServiceEndpoints ) > 0 {
208212 allErrs = append (allErrs , validateServiceEndpoints (subnet .ServiceEndpoints , fldPath .Index (i ).Child ("serviceEndpoints" ))... )
209213 }
214+
215+ if len (subnet .PrivateEndpoints ) > 0 {
216+ allErrs = append (allErrs , validatePrivateEndpoints (subnet .PrivateEndpoints , subnet .CIDRBlocks , fldPath .Index (i ).Child ("privateEndpoints" ))... )
217+ }
210218 }
211219 for k , v := range requiredSubnetRoles {
212220 if ! v {
@@ -611,3 +619,82 @@ func validateServiceEndpointLocationName(location string, fldPath *field.Path) *
611619 }
612620 return nil
613621}
622+
623+ func validatePrivateEndpoints (privateEndpointSpecs []PrivateEndpointSpec , subnetCIDRs []string , fldPath * field.Path ) field.ErrorList {
624+ var allErrs field.ErrorList
625+
626+ for i , pe := range privateEndpointSpecs {
627+ if err := validatePrivateEndpointName (pe .Name , fldPath .Index (i ).Child ("name" )); err != nil {
628+ allErrs = append (allErrs , err )
629+ }
630+
631+ if len (pe .PrivateLinkServiceConnections ) == 0 {
632+ allErrs = append (allErrs , field .Invalid (fldPath .Index (i ), pe .PrivateLinkServiceConnections , "privateLinkServiceConnections cannot be empty" ))
633+ }
634+
635+ for j , privateLinkServiceConnection := range pe .PrivateLinkServiceConnections {
636+ if privateLinkServiceConnection .PrivateLinkServiceID == "" {
637+ allErrs = append (allErrs , field .Required (fldPath .Index (i ).Child ("privateLinkServiceConnections" ).Index (j ), "privateLinkServiceID is required for all privateLinkServiceConnections in private endpoints" ))
638+ } else {
639+ if err := validatePrivateEndpointPrivateLinkServiceConnection (privateLinkServiceConnection , fldPath .Index (i ).Child ("privateLinkServiceConnections" ).Index (j )); err != nil {
640+ allErrs = append (allErrs , err )
641+ }
642+ }
643+ }
644+
645+ for _ , privateIP := range pe .PrivateIPAddresses {
646+ if err := validatePrivateEndpointIPAddress (privateIP , subnetCIDRs , fldPath .Index (i ).Child ("privateIPAddresses" )); err != nil {
647+ allErrs = append (allErrs , err )
648+ }
649+ }
650+ }
651+
652+ return allErrs
653+ }
654+
655+ // validatePrivateEndpointName validates the Name of a Private Endpoint.
656+ func validatePrivateEndpointName (name string , fldPath * field.Path ) * field.Error {
657+ if name == "" {
658+ return field .Invalid (fldPath , name , "name of private endpoint cannot be empty" )
659+ }
660+
661+ if success , _ := regexp .MatchString (privateEndpointRegex , name ); ! success {
662+ return field .Invalid (fldPath , name ,
663+ fmt .Sprintf ("name of private endpoint doesn't match regex %s" , privateEndpointRegex ))
664+ }
665+ return nil
666+ }
667+
668+ // validatePrivateEndpointServiceID validates the service ID of a Private Endpoint.
669+ func validatePrivateEndpointPrivateLinkServiceConnection (privateLinkServiceConnection PrivateLinkServiceConnection , fldPath * field.Path ) * field.Error {
670+ if success , _ := regexp .MatchString (resourceIDPattern , privateLinkServiceConnection .PrivateLinkServiceID ); ! success {
671+ return field .Invalid (fldPath , privateLinkServiceConnection .PrivateLinkServiceID ,
672+ fmt .Sprintf ("private endpoint privateLinkServiceConnection service ID doesn't match regex %s" , resourceIDPattern ))
673+ }
674+ if privateLinkServiceConnection .Name != "" {
675+ if success , _ := regexp .MatchString (privateEndpointRegex , privateLinkServiceConnection .Name ); ! success {
676+ return field .Invalid (fldPath , privateLinkServiceConnection .Name ,
677+ fmt .Sprintf ("private endpoint privateLinkServiceConnection name doesn't match regex %s" , privateEndpointRegex ))
678+ }
679+ }
680+ return nil
681+ }
682+
683+ // validatePrivateEndpointIPAddress validates a Private Endpoint IP Address.
684+ func validatePrivateEndpointIPAddress (address string , cidrs []string , fldPath * field.Path ) * field.Error {
685+ ip := net .ParseIP (address )
686+ if ip == nil {
687+ return field .Invalid (fldPath , address ,
688+ "Private Endpoint IP address isn't a valid IPv4 or IPv6 address" )
689+ }
690+
691+ for _ , cidr := range cidrs {
692+ _ , subnet , _ := net .ParseCIDR (cidr )
693+ if subnet != nil && subnet .Contains (ip ) {
694+ return nil
695+ }
696+ }
697+
698+ return field .Invalid (fldPath , address ,
699+ fmt .Sprintf ("Private Endpoint IP address needs to be in subnet range (%s)" , cidrs ))
700+ }
0 commit comments