Skip to content

Commit f10b41a

Browse files
committed
💎 Add support to reconcile subnet's Nat Gateway
1 parent 6a8d7e6 commit f10b41a

33 files changed

+1947
-45
lines changed

api/v1alpha3/azurecluster_conversion.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ func (src *AzureCluster) ConvertTo(dstRaw conversion.Hub) error { // nolint
7070
}
7171
}
7272
dst.Spec.NetworkSpec.Subnets[i].SecurityGroup.SecurityRules = append(dst.Spec.NetworkSpec.Subnets[i].SecurityGroup.SecurityRules, restoredOutboundRules...)
73+
dst.Spec.NetworkSpec.Subnets[i].NatGateway = restoredSubnet.NatGateway
74+
7375
break
7476
}
7577
}

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_default.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ func (c *AzureCluster) setNodeOutboundLBDefaults() {
175175
if c.Spec.NetworkSpec.APIServerLB.Type == Internal {
176176
return
177177
}
178+
nodeSubnet, err := c.Spec.NetworkSpec.GetNodeSubnet()
179+
// Only one outbound mechanism can be defined, so if Nat Gateway is defined, we don't default the LB.
180+
if err == nil && nodeSubnet.NatGateway.Name != "" {
181+
return
182+
}
178183
c.Spec.NetworkSpec.NodeOutboundLB = &LoadBalancerSpec{}
179184
}
180185

api/v1alpha4/azurecluster_validation.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,19 @@ func validateNetworkSpec(networkSpec NetworkSpec, old NetworkSpec, fldPath *fiel
123123
}
124124

125125
var cidrBlocks []string
126-
subnet, err := networkSpec.GetControlPlaneSubnet()
126+
controlPlaneSubnet, err := networkSpec.GetControlPlaneSubnet()
127127
if err != nil {
128128
allErrs = append(allErrs, field.Invalid(fldPath.Child("subnets"), networkSpec.Subnets, "ControlPlaneSubnet invalid"))
129129
}
130-
cidrBlocks = subnet.CIDRBlocks
130+
nodeSubnet, err := networkSpec.GetNodeSubnet()
131+
if err != nil {
132+
allErrs = append(allErrs, field.Invalid(fldPath.Child("subnets"), networkSpec.Subnets, "NodeSubnet invalid"))
133+
}
134+
cidrBlocks = controlPlaneSubnet.CIDRBlocks
131135

132136
allErrs = append(allErrs, validateAPIServerLB(networkSpec.APIServerLB, old.APIServerLB, cidrBlocks, fldPath.Child("apiServerLB"))...)
133137

134-
allErrs = append(allErrs, validateNodeOutboundLB(networkSpec.NodeOutboundLB, old.NodeOutboundLB, networkSpec.APIServerLB, fldPath.Child("nodeOutboundLB"))...)
138+
allErrs = append(allErrs, validateNodeOutboundLB(networkSpec.NodeOutboundLB, old.NodeOutboundLB, networkSpec.APIServerLB, nodeSubnet, fldPath.Child("nodeOutboundLB"))...)
135139

136140
allErrs = append(allErrs, validatePrivateDNSZoneName(networkSpec, fldPath)...)
137141

@@ -349,14 +353,19 @@ func validateAPIServerLB(lb LoadBalancerSpec, old LoadBalancerSpec, cidrs []stri
349353
return allErrs
350354
}
351355

352-
func validateNodeOutboundLB(lb *LoadBalancerSpec, old *LoadBalancerSpec, apiserverLB LoadBalancerSpec, fldPath *field.Path) field.ErrorList {
356+
func validateNodeOutboundLB(lb *LoadBalancerSpec, old *LoadBalancerSpec, apiserverLB LoadBalancerSpec, nodeSubnet SubnetSpec, fldPath *field.Path) field.ErrorList {
353357
var allErrs field.ErrorList
354358

355359
// LB can be nil when disabled for private clusters.
356360
if lb == nil && apiserverLB.Type == Internal {
357361
return allErrs
358362
}
359363

364+
// no need to validate LB when using nat gateway.
365+
if lb == nil && nodeSubnet.NatGateway.Name != "" {
366+
return allErrs
367+
}
368+
360369
if lb == nil {
361370
allErrs = append(allErrs, field.Required(fldPath, "Node outbound load balancer cannot be nil for public clusters."))
362371
return allErrs

api/v1alpha4/azurecluster_validation_test.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,13 @@ func TestNetworkSpecWithPreexistingVnetLackRequiredSubnets(t *testing.T) {
273273

274274
t.Run(testCase.name, func(t *testing.T) {
275275
errs := validateNetworkSpec(testCase.networkSpec, NetworkSpec{}, field.NewPath("spec").Child("networkSpec"))
276-
g.Expect(errs).To(HaveLen(1))
276+
g.Expect(errs).To(HaveLen(2))
277277
g.Expect(errs[0].Type).To(Equal(field.ErrorTypeRequired))
278278
g.Expect(errs[0].Field).To(Equal("spec.networkSpec.subnets"))
279279
g.Expect(errs[0].Error()).To(ContainSubstring("required role node not included"))
280+
g.Expect(errs[1].Type).To(Equal(field.ErrorTypeInvalid))
281+
g.Expect(errs[1].Field).To(Equal("spec.networkSpec.subnets"))
282+
g.Expect(errs[1].Error()).To(ContainSubstring("NodeSubnet invalid"))
280283
})
281284
}
282285

@@ -956,13 +959,15 @@ func TestValidateNodeOutboundLB(t *testing.T) {
956959
lb *LoadBalancerSpec
957960
old *LoadBalancerSpec
958961
apiServerLB LoadBalancerSpec
962+
nodeSubnet SubnetSpec
959963
wantErr bool
960964
expectedErr field.Error
961965
}{
962966
{
963967
name: "no lb for public clusters",
964968
lb: nil,
965969
apiServerLB: LoadBalancerSpec{Type: Public},
970+
nodeSubnet: createValidNodeSubnet(),
966971
wantErr: true,
967972
expectedErr: field.Error{
968973
Type: "FieldValueRequired",
@@ -985,7 +990,8 @@ func TestValidateNodeOutboundLB(t *testing.T) {
985990
old: &LoadBalancerSpec{
986991
ID: "old-id",
987992
},
988-
wantErr: true,
993+
nodeSubnet: createValidNodeSubnet(),
994+
wantErr: true,
989995
expectedErr: field.Error{
990996
Type: "FieldValueForbidden",
991997
Field: "nodeOutboundLB.id",
@@ -1001,7 +1007,8 @@ func TestValidateNodeOutboundLB(t *testing.T) {
10011007
old: &LoadBalancerSpec{
10021008
Name: "old-name",
10031009
},
1004-
wantErr: true,
1010+
nodeSubnet: createValidNodeSubnet(),
1011+
wantErr: true,
10051012
expectedErr: field.Error{
10061013
Type: "FieldValueForbidden",
10071014
Field: "nodeOutboundLB.name",
@@ -1017,7 +1024,8 @@ func TestValidateNodeOutboundLB(t *testing.T) {
10171024
old: &LoadBalancerSpec{
10181025
SKU: "old-sku",
10191026
},
1020-
wantErr: true,
1027+
nodeSubnet: createValidNodeSubnet(),
1028+
wantErr: true,
10211029
expectedErr: field.Error{
10221030
Type: "FieldValueForbidden",
10231031
Field: "nodeOutboundLB.sku",
@@ -1037,7 +1045,8 @@ func TestValidateNodeOutboundLB(t *testing.T) {
10371045
Name: "old-frontend-ip",
10381046
}},
10391047
},
1040-
wantErr: true,
1048+
nodeSubnet: createValidNodeSubnet(),
1049+
wantErr: true,
10411050
expectedErr: field.Error{
10421051
Type: "FieldValueForbidden",
10431052
Field: "nodeOutboundLB.frontendIPs[0]",
@@ -1062,28 +1071,37 @@ func TestValidateNodeOutboundLB(t *testing.T) {
10621071
Name: "old-frontend-ip",
10631072
}},
10641073
},
1065-
wantErr: false,
1074+
nodeSubnet: createValidNodeSubnet(),
1075+
wantErr: false,
10661076
},
10671077
{
10681078
name: "frontend ips count exceeds max value",
10691079
lb: &LoadBalancerSpec{
10701080
FrontendIPsCount: pointer.Int32Ptr(100),
10711081
},
1072-
wantErr: true,
1082+
nodeSubnet: createValidNodeSubnet(),
1083+
wantErr: true,
10731084
expectedErr: field.Error{
10741085
Type: "FieldValueInvalid",
10751086
Field: "nodeOutboundLB.frontendIPsCount",
10761087
BadValue: 100,
10771088
Detail: "Max front end ips allowed is 16",
10781089
},
10791090
},
1091+
{
1092+
name: "no lb when using nat gateway",
1093+
lb: nil,
1094+
apiServerLB: LoadBalancerSpec{Type: Public},
1095+
nodeSubnet: createValidNodeSubnetWithNatGateway(),
1096+
wantErr: false,
1097+
},
10801098
}
10811099

10821100
for _, test := range testcases {
10831101
test := test
10841102
t.Run(test.name, func(t *testing.T) {
10851103
t.Parallel()
1086-
err := validateNodeOutboundLB(test.lb, test.old, test.apiServerLB, field.NewPath("nodeOutboundLB"))
1104+
err := validateNodeOutboundLB(test.lb, test.old, test.apiServerLB, test.nodeSubnet, field.NewPath("nodeOutboundLB"))
10871105
if test.wantErr {
10881106
g.Expect(err).NotTo(HaveLen(0))
10891107
found := false
@@ -1234,6 +1252,21 @@ func createValidSubnets() Subnets {
12341252
}
12351253
}
12361254

1255+
func createValidNodeSubnetWithNatGateway() SubnetSpec {
1256+
return SubnetSpec{
1257+
Role: "node",
1258+
Name: "node-subnet",
1259+
NatGateway: NatGateway{Name: "node-natgateway"},
1260+
}
1261+
}
1262+
1263+
func createValidNodeSubnet() SubnetSpec {
1264+
return SubnetSpec{
1265+
Role: "node",
1266+
Name: "node-subnet",
1267+
}
1268+
}
1269+
12371270
func createValidVnet() VnetSpec {
12381271
return VnetSpec{
12391272
ResourceGroup: "custom-vnet",

api/v1alpha4/types.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ type RouteTable struct {
112112
Name string `json:"name,omitempty"`
113113
}
114114

115+
// NatGateway defines an Azure Nat Gateway.
116+
// NAT gateway resources are part of Vnet NAT and provide outbound Internet connectivity for subnets of a virtual network.
117+
type NatGateway struct {
118+
ID string `json:"id,omitempty"`
119+
Name string `json:"name,omitempty"`
120+
NatGatewayIP PublicIPSpec `json:"ip,omitempty"`
121+
}
122+
115123
// SecurityGroupProtocol defines the protocol type for a security group rule.
116124
type SecurityGroupProtocol string
117125

@@ -477,6 +485,10 @@ type SubnetSpec struct {
477485
// RouteTable defines the route table that should be attached to this subnet.
478486
// +optional
479487
RouteTable RouteTable `json:"routeTable,omitempty"`
488+
489+
// NatGateway associated with this subnet.
490+
// +optional
491+
NatGateway NatGateway `json:"natGateway,omitempty"`
480492
}
481493

482494
// GetControlPlaneSubnet returns the cluster control plane subnet.

api/v1alpha4/zz_generated.deepcopy.go

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

azure/defaults.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ func GenerateFrontendIPConfigName(lbName string) string {
9797
return fmt.Sprintf("%s-%s", lbName, "frontEnd")
9898
}
9999

100+
// GenerateNatGatewayIPName generates a nat gateway IP name.
101+
func GenerateNatGatewayIPName(clusterName, subnetName string) string {
102+
return fmt.Sprintf("pip-%s-%s-natgw", clusterName, subnetName)
103+
}
104+
100105
// GenerateNodeOutboundIPName generates a public IP name, based on the cluster name.
101106
func GenerateNodeOutboundIPName(clusterName string) string {
102107
return fmt.Sprintf("pip-%s-node-outbound", clusterName)
@@ -195,6 +200,11 @@ func SecurityGroupID(subscriptionID, resourceGroup, nsgName string) string {
195200
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkSecurityGroups/%s", subscriptionID, resourceGroup, nsgName)
196201
}
197202

203+
// NatGatewayID returns the azure resource ID for a given nat gateway.
204+
func NatGatewayID(subscriptionID, resourceGroup, natgatewayName string) string {
205+
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/natGateways/%s", subscriptionID, resourceGroup, natgatewayName)
206+
}
207+
198208
// NetworkInterfaceID returns the azure resource ID for a given network interface.
199209
func NetworkInterfaceID(subscriptionID, resourceGroup, nicName string) string {
200210
return fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkInterfaces/%s", subscriptionID, resourceGroup, nicName)

azure/interfaces.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type NetworkDescriber interface {
6868
IsIPv6Enabled() bool
6969
NodeRouteTable() infrav1.RouteTable
7070
ControlPlaneRouteTable() infrav1.RouteTable
71+
NodeNatGateway() infrav1.NatGateway
7172
APIServerLBName() string
7273
APIServerLBPoolName(string) string
7374
IsAPIServerPrivate() bool

azure/mocks/service_mock.go

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

0 commit comments

Comments
 (0)