Skip to content

Commit 9b917e8

Browse files
committed
Support adding custom secondary VPC CIDR blocks in AWSCluster
1 parent ad71a54 commit 9b917e8

22 files changed

+510
-93
lines changed

api/v1beta1/awscluster_conversion.go

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error {
103103

104104
dst.Spec.NetworkSpec.VPC.EmptyRoutesDefaultVPCSecurityGroup = restored.Spec.NetworkSpec.VPC.EmptyRoutesDefaultVPCSecurityGroup
105105
dst.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch = restored.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch
106+
dst.Spec.NetworkSpec.VPC.SecondaryCidrBlocks = restored.Spec.NetworkSpec.VPC.SecondaryCidrBlocks
106107

107108
// Restore SubnetSpec.ResourceID field, if any.
108109
for _, subnet := range restored.Spec.NetworkSpec.Subnets {

api/v1beta1/zz_generated.conversion.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1beta2/awscluster_webhook.go

+8
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,14 @@ func (r *AWSCluster) validateNetwork() field.ErrorList {
264264
}
265265
}
266266

267+
secondaryCidrBlocks := r.Spec.NetworkSpec.VPC.SecondaryCidrBlocks
268+
secondaryCidrBlocksField := field.NewPath("spec", "network", "vpc", "secondaryCidrBlocks")
269+
for _, cidrBlock := range secondaryCidrBlocks {
270+
if r.Spec.NetworkSpec.VPC.CidrBlock != "" && r.Spec.NetworkSpec.VPC.CidrBlock == cidrBlock.IPv4CidrBlock {
271+
allErrs = append(allErrs, field.Invalid(secondaryCidrBlocksField, secondaryCidrBlocks, fmt.Sprintf("AWSCluster.spec.network.vpc.secondaryCidrBlocks must not contain the primary AWSCluster.spec.network.vpc.cidrBlock %v", r.Spec.NetworkSpec.VPC.CidrBlock)))
272+
}
273+
}
274+
267275
return allErrs
268276
}
269277

api/v1beta2/network_types.go

+24
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ type IPAMPool struct {
298298
NetmaskLength int64 `json:"netmaskLength,omitempty"`
299299
}
300300

301+
// VpcCidrBlock defines the CIDR block and settings to associate with the managed VPC. Currently, only IPv4 is supported.
302+
type VpcCidrBlock struct {
303+
// IPv4CidrBlock is the IPv4 CIDR block to associate with the managed VPC.
304+
// +kubebuilder:validation:MinLength=1
305+
IPv4CidrBlock string `json:"ipv4CidrBlock"`
306+
}
307+
301308
// VPCSpec configures an AWS VPC.
302309
type VPCSpec struct {
303310
// ID is the vpc-id of the VPC this provider should use to create resources.
@@ -308,6 +315,12 @@ type VPCSpec struct {
308315
// Mutually exclusive with IPAMPool.
309316
CidrBlock string `json:"cidrBlock,omitempty"`
310317

318+
// SecondaryCidrBlocks are additional CIDR blocks to be associated when the provider creates a managed VPC.
319+
// Defaults to none. Mutually exclusive with IPAMPool. This makes sense to use if, for example, you want to use
320+
// a separate IP range for pods (e.g. Cilium ENI mode).
321+
// +optional
322+
SecondaryCidrBlocks []VpcCidrBlock `json:"secondaryCidrBlocks,omitempty"`
323+
311324
// IPAMPool defines the IPAMv4 pool to be used for VPC.
312325
// Mutually exclusive with CidrBlock.
313326
IPAMPool *IPAMPool `json:"ipamPool,omitempty"`
@@ -510,6 +523,17 @@ func (s Subnets) FilterPrivate() (res Subnets) {
510523
return
511524
}
512525

526+
// FilterNonCni returns the subnets that are NOT intended for usage with the CNI pod network
527+
// (i.e. do NOT have the `sigs.k8s.io/cluster-api-provider-aws/association=secondary` tag).
528+
func (s Subnets) FilterNonCni() (res Subnets) {
529+
for _, x := range s {
530+
if x.Tags[NameAWSSubnetAssociation] != SecondarySubnetTagValue {
531+
res = append(res, x)
532+
}
533+
}
534+
return
535+
}
536+
513537
// FilterPublic returns a slice containing all subnets marked as public.
514538
func (s Subnets) FilterPublic() (res Subnets) {
515539
for _, x := range s {

api/v1beta2/zz_generated.deepcopy.go

+20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml

+40
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,26 @@ spec:
660660
- ip-name
661661
- resource-name
662662
type: string
663+
secondaryCidrBlocks:
664+
description: SecondaryCidrBlocks are additional CIDR blocks
665+
to be associated when the provider creates a managed VPC.
666+
Defaults to none. Mutually exclusive with IPAMPool. This
667+
makes sense to use if, for example, you want to use a separate
668+
IP range for pods (e.g. Cilium ENI mode).
669+
items:
670+
description: VpcCidrBlock defines the CIDR block and settings
671+
to associate with the managed VPC. Currently, only IPv4
672+
is supported.
673+
properties:
674+
ipv4CidrBlock:
675+
description: IPv4CidrBlock is the IPv4 CIDR block to
676+
associate with the managed VPC.
677+
minLength: 1
678+
type: string
679+
required:
680+
- ipv4CidrBlock
681+
type: object
682+
type: array
663683
tags:
664684
additionalProperties:
665685
type: string
@@ -2512,6 +2532,26 @@ spec:
25122532
- ip-name
25132533
- resource-name
25142534
type: string
2535+
secondaryCidrBlocks:
2536+
description: SecondaryCidrBlocks are additional CIDR blocks
2537+
to be associated when the provider creates a managed VPC.
2538+
Defaults to none. Mutually exclusive with IPAMPool. This
2539+
makes sense to use if, for example, you want to use a separate
2540+
IP range for pods (e.g. Cilium ENI mode).
2541+
items:
2542+
description: VpcCidrBlock defines the CIDR block and settings
2543+
to associate with the managed VPC. Currently, only IPv4
2544+
is supported.
2545+
properties:
2546+
ipv4CidrBlock:
2547+
description: IPv4CidrBlock is the IPv4 CIDR block to
2548+
associate with the managed VPC.
2549+
minLength: 1
2550+
type: string
2551+
required:
2552+
- ipv4CidrBlock
2553+
type: object
2554+
type: array
25152555
tags:
25162556
additionalProperties:
25172557
type: string

config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml

+20
Original file line numberDiff line numberDiff line change
@@ -1494,6 +1494,26 @@ spec:
14941494
- ip-name
14951495
- resource-name
14961496
type: string
1497+
secondaryCidrBlocks:
1498+
description: SecondaryCidrBlocks are additional CIDR blocks
1499+
to be associated when the provider creates a managed VPC.
1500+
Defaults to none. Mutually exclusive with IPAMPool. This
1501+
makes sense to use if, for example, you want to use a separate
1502+
IP range for pods (e.g. Cilium ENI mode).
1503+
items:
1504+
description: VpcCidrBlock defines the CIDR block and settings
1505+
to associate with the managed VPC. Currently, only IPv4
1506+
is supported.
1507+
properties:
1508+
ipv4CidrBlock:
1509+
description: IPv4CidrBlock is the IPv4 CIDR block to
1510+
associate with the managed VPC.
1511+
minLength: 1
1512+
type: string
1513+
required:
1514+
- ipv4CidrBlock
1515+
type: object
1516+
type: array
14971517
tags:
14981518
additionalProperties:
14991519
type: string

config/crd/bases/infrastructure.cluster.x-k8s.io_awsclustertemplates.yaml

+21
Original file line numberDiff line numberDiff line change
@@ -1119,6 +1119,27 @@ spec:
11191119
- ip-name
11201120
- resource-name
11211121
type: string
1122+
secondaryCidrBlocks:
1123+
description: SecondaryCidrBlocks are additional CIDR
1124+
blocks to be associated when the provider creates
1125+
a managed VPC. Defaults to none. Mutually exclusive
1126+
with IPAMPool. This makes sense to use if, for example,
1127+
you want to use a separate IP range for pods (e.g.
1128+
Cilium ENI mode).
1129+
items:
1130+
description: VpcCidrBlock defines the CIDR block
1131+
and settings to associate with the managed VPC.
1132+
Currently, only IPv4 is supported.
1133+
properties:
1134+
ipv4CidrBlock:
1135+
description: IPv4CidrBlock is the IPv4 CIDR
1136+
block to associate with the managed VPC.
1137+
minLength: 1
1138+
type: string
1139+
required:
1140+
- ipv4CidrBlock
1141+
type: object
1142+
type: array
11221143
tags:
11231144
additionalProperties:
11241145
type: string

controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go

+35-5
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func (r *AWSManagedControlPlane) ValidateUpdate(old runtime.Object) (admission.W
164164

165165
if oldAWSManagedControlplane.Spec.NetworkSpec.VPC.IsIPv6Enabled() != r.Spec.NetworkSpec.VPC.IsIPv6Enabled() {
166166
allErrs = append(allErrs,
167-
field.Invalid(field.NewPath("spec", "networkSpec", "vpc", "enableIPv6"), r.Spec.NetworkSpec.VPC.IsIPv6Enabled(), "changing IP family is not allowed after it has been set"))
167+
field.Invalid(field.NewPath("spec", "network", "vpc", "enableIPv6"), r.Spec.NetworkSpec.VPC.IsIPv6Enabled(), "changing IP family is not allowed after it has been set"))
168168
}
169169

170170
if len(allErrs) == 0 {
@@ -406,23 +406,53 @@ func (r *AWSManagedControlPlane) validatePrivateDNSHostnameTypeOnLaunch() field.
406406
func (r *AWSManagedControlPlane) validateNetwork() field.ErrorList {
407407
var allErrs field.ErrorList
408408

409+
// If only `AWSManagedControlPlane.spec.secondaryCidrBlock` is set, no additional checks are done to remain
410+
// backward-compatible. The `VPCSpec.SecondaryCidrBlocks` field was added later - if that list is not empty, we
411+
// require `AWSManagedControlPlane.spec.secondaryCidrBlock` to be listed in there as well. This may allow merging
412+
// the fields later on.
413+
podSecondaryCidrBlock := r.Spec.SecondaryCidrBlock
414+
secondaryCidrBlocks := r.Spec.NetworkSpec.VPC.SecondaryCidrBlocks
415+
secondaryCidrBlocksField := field.NewPath("spec", "network", "vpc", "secondaryCidrBlocks")
416+
if podSecondaryCidrBlock != nil && len(secondaryCidrBlocks) > 0 {
417+
found := false
418+
for _, cidrBlock := range secondaryCidrBlocks {
419+
if cidrBlock.IPv4CidrBlock == *podSecondaryCidrBlock {
420+
found = true
421+
break
422+
}
423+
}
424+
if !found {
425+
allErrs = append(allErrs, field.Invalid(secondaryCidrBlocksField, secondaryCidrBlocks, fmt.Sprintf("AWSManagedControlPlane.spec.secondaryCidrBlock %v must be listed in AWSManagedControlPlane.spec.network.vpc.secondaryCidrBlocks (required if both fields are filled)", *podSecondaryCidrBlock)))
426+
}
427+
}
428+
429+
if podSecondaryCidrBlock != nil && r.Spec.NetworkSpec.VPC.CidrBlock != "" && r.Spec.NetworkSpec.VPC.CidrBlock == *podSecondaryCidrBlock {
430+
secondaryCidrBlockField := field.NewPath("spec", "vpc", "secondaryCidrBlock")
431+
allErrs = append(allErrs, field.Invalid(secondaryCidrBlockField, secondaryCidrBlocks, fmt.Sprintf("AWSManagedControlPlane.spec.secondaryCidrBlock %v must not be equal to the primary AWSManagedControlPlane.spec.network.vpc.cidrBlock", *podSecondaryCidrBlock)))
432+
}
433+
for _, cidrBlock := range secondaryCidrBlocks {
434+
if r.Spec.NetworkSpec.VPC.CidrBlock != "" && r.Spec.NetworkSpec.VPC.CidrBlock == cidrBlock.IPv4CidrBlock {
435+
allErrs = append(allErrs, field.Invalid(secondaryCidrBlocksField, secondaryCidrBlocks, fmt.Sprintf("AWSManagedControlPlane.spec.network.vpc.secondaryCidrBlocks must not contain the primary AWSManagedControlPlane.spec.network.vpc.cidrBlock %v", r.Spec.NetworkSpec.VPC.CidrBlock)))
436+
}
437+
}
438+
409439
if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.IPv6.CidrBlock != "" && r.Spec.NetworkSpec.VPC.IPv6.PoolID == "" {
410-
poolField := field.NewPath("spec", "networkSpec", "vpc", "ipv6", "poolId")
440+
poolField := field.NewPath("spec", "network", "vpc", "ipv6", "poolId")
411441
allErrs = append(allErrs, field.Invalid(poolField, r.Spec.NetworkSpec.VPC.IPv6.PoolID, "poolId cannot be empty if cidrBlock is set"))
412442
}
413443

414444
if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.IPv6.PoolID != "" && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool != nil {
415-
poolField := field.NewPath("spec", "networkSpec", "vpc", "ipv6", "poolId")
445+
poolField := field.NewPath("spec", "network", "vpc", "ipv6", "poolId")
416446
allErrs = append(allErrs, field.Invalid(poolField, r.Spec.NetworkSpec.VPC.IPv6.PoolID, "poolId and ipamPool cannot be used together"))
417447
}
418448

419449
if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.IPv6.CidrBlock != "" && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool != nil {
420-
cidrBlockField := field.NewPath("spec", "networkSpec", "vpc", "ipv6", "cidrBlock")
450+
cidrBlockField := field.NewPath("spec", "network", "vpc", "ipv6", "cidrBlock")
421451
allErrs = append(allErrs, field.Invalid(cidrBlockField, r.Spec.NetworkSpec.VPC.IPv6.CidrBlock, "cidrBlock and ipamPool cannot be used together"))
422452
}
423453

424454
if r.Spec.NetworkSpec.VPC.IsIPv6Enabled() && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool != nil && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool.ID == "" && r.Spec.NetworkSpec.VPC.IPv6.IPAMPool.Name == "" {
425-
ipamPoolField := field.NewPath("spec", "networkSpec", "vpc", "ipv6", "ipamPool")
455+
ipamPoolField := field.NewPath("spec", "network", "vpc", "ipv6", "ipamPool")
426456
allErrs = append(allErrs, field.Invalid(ipamPoolField, r.Spec.NetworkSpec.VPC.IPv6.IPAMPool, "ipamPool must have either id or name"))
427457
}
428458

controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go

+34-9
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,17 @@ func TestDefaultingWebhook(t *testing.T) {
167167

168168
func TestWebhookCreate(t *testing.T) {
169169
tests := []struct { //nolint:maligned
170-
name string
171-
eksClusterName string
172-
expectError bool
173-
eksVersion string
174-
hasAddons bool
175-
vpcCNI VpcCni
176-
additionalTags infrav1.Tags
177-
secondaryCidr *string
178-
kubeProxy KubeProxy
170+
name string
171+
eksClusterName string
172+
expectError bool
173+
expectErrorToContain string // if non-empty, the error message must contain this substring
174+
eksVersion string
175+
hasAddons bool
176+
vpcCNI VpcCni
177+
additionalTags infrav1.Tags
178+
secondaryCidr *string
179+
secondaryCidrBlocks []infrav1.VpcCidrBlock
180+
kubeProxy KubeProxy
179181
}{
180182
{
181183
name: "ekscluster specified",
@@ -253,6 +255,15 @@ func TestWebhookCreate(t *testing.T) {
253255
vpcCNI: VpcCni{Disable: true},
254256
secondaryCidr: aws.String("100.64.0.0/10"),
255257
},
258+
{
259+
name: "secondary CIDR block not listed in NetworkSpec.VPC.SecondaryCidrBlocks",
260+
eksClusterName: "default_cluster1",
261+
eksVersion: "v1.19",
262+
expectError: true,
263+
expectErrorToContain: "100.64.0.0/16 must be listed in AWSManagedControlPlane.spec.network.vpc.secondaryCidrBlocks",
264+
secondaryCidr: aws.String("100.64.0.0/16"),
265+
secondaryCidrBlocks: []infrav1.VpcCidrBlock{{IPv4CidrBlock: "123.456.0.0/16"}},
266+
},
256267
{
257268
name: "invalid tags not allowed",
258269
eksClusterName: "default_cluster1",
@@ -327,6 +338,11 @@ func TestWebhookCreate(t *testing.T) {
327338
KubeProxy: tc.kubeProxy,
328339
AdditionalTags: tc.additionalTags,
329340
VpcCni: tc.vpcCNI,
341+
NetworkSpec: infrav1.NetworkSpec{
342+
VPC: infrav1.VPCSpec{
343+
SecondaryCidrBlocks: tc.secondaryCidrBlocks,
344+
},
345+
},
330346
},
331347
}
332348
if tc.eksVersion != "" {
@@ -353,7 +369,16 @@ func TestWebhookCreate(t *testing.T) {
353369

354370
if tc.expectError {
355371
g.Expect(err).ToNot(BeNil())
372+
373+
if tc.expectErrorToContain != "" && err != nil {
374+
g.Expect(err.Error()).To(ContainSubstring(tc.expectErrorToContain))
375+
}
356376
} else {
377+
if tc.expectErrorToContain != "" {
378+
t.Error("Logic error: expectError=false means that expectErrorToContain must be empty")
379+
t.FailNow()
380+
}
381+
357382
g.Expect(err).To(BeNil())
358383
}
359384
})

pkg/cloud/scope/cluster.go

+11
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,17 @@ func (s *ClusterScope) SecondaryCidrBlock() *string {
153153
return nil
154154
}
155155

156+
// SecondaryCidrBlocks returns the additional CIDR blocks to be associated with the managed VPC.
157+
func (s *ClusterScope) SecondaryCidrBlocks() []infrav1.VpcCidrBlock {
158+
return s.AWSCluster.Spec.NetworkSpec.VPC.SecondaryCidrBlocks
159+
}
160+
161+
// AllSecondaryCidrBlocks returns all secondary CIDR blocks (combining `SecondaryCidrBlock` and `SecondaryCidrBlocks`).
162+
func (s *ClusterScope) AllSecondaryCidrBlocks() []infrav1.VpcCidrBlock {
163+
// Non-EKS clusters don't have anything in `SecondaryCidrBlock()`
164+
return s.SecondaryCidrBlocks()
165+
}
166+
156167
// Name returns the CAPI cluster name.
157168
func (s *ClusterScope) Name() string {
158169
return s.Cluster.Name

0 commit comments

Comments
 (0)