Skip to content

Commit 0e8ff6b

Browse files
authored
Merge pull request #3044 from adriananeci/add-support-for-private-link
Add support for private endpoints
2 parents af02259 + 8b4a671 commit 0e8ff6b

30 files changed

+2285
-5
lines changed

api/v1alpha3/azurecluster_conversion.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error {
104104
}
105105
dst.Spec.NetworkSpec.Subnets[i].SecurityGroup.SecurityRules = append(dst.Spec.NetworkSpec.Subnets[i].SecurityGroup.SecurityRules, restoredOutboundRules...)
106106
dst.Spec.NetworkSpec.Subnets[i].NatGateway = restoredSubnet.NatGateway
107-
108107
dst.Spec.NetworkSpec.Subnets[i].ServiceEndpoints = restoredSubnet.ServiceEndpoints
108+
dst.Spec.NetworkSpec.Subnets[i].PrivateEndpoints = restoredSubnet.PrivateEndpoints
109109

110110
break
111111
}

api/v1alpha3/azuremanagedcontrolplane_conversion.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func (src *AzureManagedControlPlane) ConvertTo(dstRaw conversion.Hub) error {
4343
dst.Spec.AddonProfiles = restored.Spec.AddonProfiles
4444
dst.Spec.VirtualNetwork.ResourceGroup = restored.Spec.VirtualNetwork.ResourceGroup
4545
dst.Spec.VirtualNetwork.Subnet.ServiceEndpoints = restored.Spec.VirtualNetwork.Subnet.ServiceEndpoints
46+
dst.Spec.VirtualNetwork.Subnet.PrivateEndpoints = restored.Spec.VirtualNetwork.Subnet.PrivateEndpoints
4647
dst.Spec.AutoScalerProfile = restored.Spec.AutoScalerProfile
4748
dst.Spec.OutboundType = restored.Spec.OutboundType
4849

api/v1alpha3/zz_generated.conversion.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1alpha4/azurecluster_conversion.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,18 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error {
7070
}
7171
}
7272

73-
// Restore NAT Gateway IP tags and ServiceEndpoints.
73+
// Restore NAT Gateway IP tags, ServiceEndpoints and PrivateEndpoints.
7474
for _, restoredSubnet := range restored.Spec.NetworkSpec.Subnets {
7575
for i, dstSubnet := range dst.Spec.NetworkSpec.Subnets {
7676
if dstSubnet.Name == restoredSubnet.Name {
7777
dst.Spec.NetworkSpec.Subnets[i].NatGateway.NatGatewayIP.IPTags = restoredSubnet.NatGateway.NatGatewayIP.IPTags
7878
dst.Spec.NetworkSpec.Subnets[i].ServiceEndpoints = restoredSubnet.ServiceEndpoints
79+
dst.Spec.NetworkSpec.Subnets[i].PrivateEndpoints = restoredSubnet.PrivateEndpoints
7980
}
8081
}
8182
}
8283

83-
// Restore Azure Bastion IP tags.
84+
// Restore Azure Bastion IP tags, ServiceEndpoints and PrivateEndpoints.
8485
if restored.Spec.BastionSpec.AzureBastion != nil && dst.Spec.BastionSpec.AzureBastion != nil {
8586
if restored.Spec.BastionSpec.AzureBastion.PublicIP.Name == dst.Spec.BastionSpec.AzureBastion.PublicIP.Name {
8687
dst.Spec.BastionSpec.AzureBastion.PublicIP.IPTags = restored.Spec.BastionSpec.AzureBastion.PublicIP.IPTags
@@ -89,6 +90,7 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error {
8990
dst.Spec.BastionSpec.AzureBastion.Subnet.NatGateway.NatGatewayIP.IPTags = restored.Spec.BastionSpec.AzureBastion.Subnet.NatGateway.NatGatewayIP.IPTags
9091
}
9192
dst.Spec.BastionSpec.AzureBastion.Subnet.ServiceEndpoints = restored.Spec.BastionSpec.AzureBastion.Subnet.ServiceEndpoints
93+
dst.Spec.BastionSpec.AzureBastion.Subnet.PrivateEndpoints = restored.Spec.BastionSpec.AzureBastion.Subnet.PrivateEndpoints
9294
}
9395

9496
// Restore load balancers' backend pool name

api/v1alpha4/azuremanagedcontrolplane_conversion.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func (src *AzureManagedControlPlane) ConvertTo(dstRaw conversion.Hub) error {
4040
dst.Status.Conditions = restored.Status.Conditions
4141
dst.Spec.VirtualNetwork.ResourceGroup = restored.Spec.VirtualNetwork.ResourceGroup
4242
dst.Spec.VirtualNetwork.Subnet.ServiceEndpoints = restored.Spec.VirtualNetwork.Subnet.ServiceEndpoints
43+
dst.Spec.VirtualNetwork.Subnet.PrivateEndpoints = restored.Spec.VirtualNetwork.Subnet.PrivateEndpoints
4344
dst.Spec.AutoScalerProfile = restored.Spec.AutoScalerProfile
4445
dst.Spec.OutboundType = restored.Spec.OutboundType
4546

api/v1alpha4/zz_generated.conversion.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta1/azurecluster_validation.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

6064
var (
@@ -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+
}

api/v1beta1/azuremanagedcontrolplane_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ type ManagedControlPlaneSubnet struct {
245245
// ServiceEndpoints is a slice of Virtual Network service endpoints to enable for the subnets.
246246
// +optional
247247
ServiceEndpoints ServiceEndpoints `json:"serviceEndpoints,omitempty"`
248+
249+
// PrivateEndpoints is a slice of Virtual Network private endpoints to create for the subnets.
250+
// +optional
251+
PrivateEndpoints PrivateEndpoints `json:"privateEndpoints,omitempty"`
248252
}
249253

250254
// AzureManagedControlPlaneStatus defines the observed state of AzureManagedControlPlane.

api/v1beta1/azuremanagedcontrolplane_webhook.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ func (m *AzureManagedControlPlane) ValidateDelete(_ client.Client) error {
255255
return nil
256256
}
257257

258-
// Validate the Azure Machine Pool and return an aggregate error.
258+
// Validate the Azure Managed Control Plane and return an aggregate error.
259259
func (m *AzureManagedControlPlane) Validate(cli client.Client) error {
260260
validators := []func(client client.Client) error{
261261
m.validateName,
@@ -434,6 +434,10 @@ func (m *AzureManagedControlPlane) validateManagedClusterNetwork(cli client.Clie
434434
}
435435
}
436436

437+
if errs := validatePrivateEndpoints(m.Spec.VirtualNetwork.Subnet.PrivateEndpoints, []string{m.Spec.VirtualNetwork.Subnet.CIDRBlock}, field.NewPath("Spec", "VirtualNetwork.Subnet.PrivateEndpoints")); len(errs) > 0 {
438+
allErrs = append(allErrs, errs...)
439+
}
440+
437441
if len(allErrs) > 0 {
438442
return kerrors.NewAggregate(allErrs.ToAggregate().Errors())
439443
}
@@ -471,7 +475,7 @@ func (m *AzureManagedControlPlane) validateAPIServerAccessProfileUpdate(old *Azu
471475
return allErrs
472476
}
473477

474-
// validateVirtualNetworkUpdate validates update to APIServerAccessProfile.
478+
// validateVirtualNetworkUpdate validates update to VirtualNetwork.
475479
func (m *AzureManagedControlPlane) validateVirtualNetworkUpdate(old *AzureManagedControlPlane) field.ErrorList {
476480
var allErrs field.ErrorList
477481
if old.Spec.VirtualNetwork.Name != m.Spec.VirtualNetwork.Name {

api/v1beta1/consts.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ const (
128128
DisksReadyCondition clusterv1.ConditionType = "DisksReady"
129129
// NetworkInterfaceReadyCondition means the network interfaces exist and are ready to be used.
130130
NetworkInterfaceReadyCondition clusterv1.ConditionType = "NetworkInterfacesReady"
131+
// PrivateEndpointsReadyCondition means the private endpoints exist and are ready to be used.
132+
PrivateEndpointsReadyCondition clusterv1.ConditionType = "PrivateEndpointsReady"
131133

132134
// CreatingReason means the resource is being created.
133135
CreatingReason = "Creating"

0 commit comments

Comments
 (0)