Skip to content

Commit 2f230b3

Browse files
committed
✨ Support BYO Public IPv4 Pool for Elastic IPs
Introducing support of BYO Public IPv4 Pool to allow CAPA allocate IPv4 Elastic IPs from user-provided IPv4 pools that was brought to AWS when provisioning base cluster infrastructure. This change introduce the API fields: - AWSCLuster NetworkSpec.ElasticIPPool: allowing the controllers to consume from user-provided public pools when provisioning core components from the infrastructure, like Nat Gateways and public Network Load Balancer (API server only) - AWSMachine ElasticIPPool: allowing the machine to consume from BYO Public IPv4 pool when the instance is deployed in the public subnets. The ElasticIPPool structure defines a custom IPv4 Pool (previously created in the AWS Account) to teach controllers to set the pool when creating public ip addresses (Elastic IPs) for components which requires it, such as Nat Gateways and NLBs.
1 parent 3a28a4d commit 2f230b3

34 files changed

+966
-145
lines changed

api/v1beta1/awscluster_conversion.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,18 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error {
105105
dst.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch = restored.Spec.NetworkSpec.VPC.PrivateDNSHostnameTypeOnLaunch
106106
dst.Spec.NetworkSpec.VPC.CarrierGatewayID = restored.Spec.NetworkSpec.VPC.CarrierGatewayID
107107

108+
if restored.Spec.NetworkSpec.VPC.ElasticIPPool != nil {
109+
if dst.Spec.NetworkSpec.VPC.ElasticIPPool == nil {
110+
dst.Spec.NetworkSpec.VPC.ElasticIPPool = &infrav2.ElasticIPPool{}
111+
}
112+
if restored.Spec.NetworkSpec.VPC.ElasticIPPool.PublicIpv4Pool != nil {
113+
dst.Spec.NetworkSpec.VPC.ElasticIPPool.PublicIpv4Pool = restored.Spec.NetworkSpec.VPC.ElasticIPPool.PublicIpv4Pool
114+
}
115+
if restored.Spec.NetworkSpec.VPC.ElasticIPPool.PublicIpv4PoolFallBackOrder != nil {
116+
dst.Spec.NetworkSpec.VPC.ElasticIPPool.PublicIpv4PoolFallBackOrder = restored.Spec.NetworkSpec.VPC.ElasticIPPool.PublicIpv4PoolFallBackOrder
117+
}
118+
}
119+
108120
// Restore SubnetSpec.ResourceID, SubnetSpec.ParentZoneName, and SubnetSpec.ZoneType fields, if any.
109121
for _, subnet := range restored.Spec.NetworkSpec.Subnets {
110122
for i, dstSubnet := range dst.Spec.NetworkSpec.Subnets {

api/v1beta1/awsmachine_conversion.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error {
4141
dst.Spec.PlacementGroupPartition = restored.Spec.PlacementGroupPartition
4242
dst.Spec.PrivateDNSName = restored.Spec.PrivateDNSName
4343
dst.Spec.SecurityGroupOverrides = restored.Spec.SecurityGroupOverrides
44+
if restored.Spec.ElasticIPPool != nil {
45+
if dst.Spec.ElasticIPPool == nil {
46+
dst.Spec.ElasticIPPool = &infrav1.ElasticIPPool{}
47+
}
48+
if restored.Spec.ElasticIPPool.PublicIpv4Pool != nil {
49+
dst.Spec.ElasticIPPool.PublicIpv4Pool = restored.Spec.ElasticIPPool.PublicIpv4Pool
50+
}
51+
if restored.Spec.ElasticIPPool.PublicIpv4PoolFallBackOrder != nil {
52+
dst.Spec.ElasticIPPool.PublicIpv4PoolFallBackOrder = restored.Spec.ElasticIPPool.PublicIpv4PoolFallBackOrder
53+
}
54+
}
4455

4556
return nil
4657
}
@@ -91,6 +102,17 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error {
91102
dst.Spec.Template.Spec.PlacementGroupPartition = restored.Spec.Template.Spec.PlacementGroupPartition
92103
dst.Spec.Template.Spec.PrivateDNSName = restored.Spec.Template.Spec.PrivateDNSName
93104
dst.Spec.Template.Spec.SecurityGroupOverrides = restored.Spec.Template.Spec.SecurityGroupOverrides
105+
if restored.Spec.Template.Spec.ElasticIPPool != nil {
106+
if dst.Spec.Template.Spec.ElasticIPPool == nil {
107+
dst.Spec.Template.Spec.ElasticIPPool = &infrav1.ElasticIPPool{}
108+
}
109+
if restored.Spec.Template.Spec.ElasticIPPool.PublicIpv4Pool != nil {
110+
dst.Spec.Template.Spec.ElasticIPPool.PublicIpv4Pool = restored.Spec.Template.Spec.ElasticIPPool.PublicIpv4Pool
111+
}
112+
if restored.Spec.Template.Spec.ElasticIPPool.PublicIpv4PoolFallBackOrder != nil {
113+
dst.Spec.Template.Spec.ElasticIPPool.PublicIpv4PoolFallBackOrder = restored.Spec.Template.Spec.ElasticIPPool.PublicIpv4PoolFallBackOrder
114+
}
115+
}
94116

95117
return nil
96118
}

api/v1beta1/zz_generated.conversion.go

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

api/v1beta2/awscluster_webhook.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,22 @@ func (r *AWSCluster) validateNetwork() field.ErrorList {
269269
}
270270
}
271271

272+
if r.Spec.NetworkSpec.VPC.ElasticIPPool != nil {
273+
eipp := r.Spec.NetworkSpec.VPC.ElasticIPPool
274+
if eipp.PublicIpv4Pool != nil {
275+
if eipp.PublicIpv4PoolFallBackOrder == nil {
276+
return append(allErrs, field.Invalid(field.NewPath("elasticIpPool.publicIpv4PoolFallbackOrder"), r.Spec.NetworkSpec.VPC.ElasticIPPool, "publicIpv4PoolFallbackOrder must be set when publicIpv4Pool is defined."))
277+
}
278+
awsPublicIpv4PoolPrefix := "ipv4pool-ec2-"
279+
if !strings.HasPrefix(*eipp.PublicIpv4Pool, awsPublicIpv4PoolPrefix) {
280+
return append(allErrs, field.Invalid(field.NewPath("elasticIpPool.publicIpv4Pool"), r.Spec.NetworkSpec.VPC.ElasticIPPool, fmt.Sprintf("publicIpv4Pool must start with %s.", awsPublicIpv4PoolPrefix)))
281+
}
282+
}
283+
if eipp.PublicIpv4Pool == nil && eipp.PublicIpv4PoolFallBackOrder != nil {
284+
return append(allErrs, field.Invalid(field.NewPath("elasticIpPool.publicIpv4PoolFallbackOrder"), r.Spec.NetworkSpec.VPC.ElasticIPPool, "publicIpv4Pool must be set when publicIpv4PoolFallbackOrder is defined."))
285+
}
286+
}
287+
272288
return allErrs
273289
}
274290

api/v1beta2/awsmachine_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ type AWSMachineSpec struct {
113113
// +optional
114114
PublicIP *bool `json:"publicIP,omitempty"`
115115

116+
// ElasticIPPool is the configuration to allocate Public IPv4 address (Elastic IP/EIP) from user-defined pool.
117+
//
118+
// +optional
119+
ElasticIPPool *ElasticIPPool `json:"elasticIpPool,omitempty"`
120+
116121
// AdditionalSecurityGroups is an array of references to security groups that should be applied to the
117122
// instance. These security groups would be set in addition to any security groups defined
118123
// at the cluster level or in the actuator. It is possible to specify either IDs of Filters. Using Filters

api/v1beta2/awsmachine_webhook.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"k8s.io/apimachinery/pkg/runtime"
3030
"k8s.io/apimachinery/pkg/util/validation"
3131
"k8s.io/apimachinery/pkg/util/validation/field"
32+
"k8s.io/utils/ptr"
3233
ctrl "sigs.k8s.io/controller-runtime"
3334
"sigs.k8s.io/controller-runtime/pkg/webhook"
3435
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -64,6 +65,7 @@ func (r *AWSMachine) ValidateCreate() (admission.Warnings, error) {
6465
allErrs = append(allErrs, r.validateSSHKeyName()...)
6566
allErrs = append(allErrs, r.validateAdditionalSecurityGroups()...)
6667
allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...)
68+
allErrs = append(allErrs, r.validateNetworkElasticIPPool()...)
6769

6870
return nil, aggregateObjErrors(r.GroupVersionKind().GroupKind(), r.Name, allErrs)
6971
}
@@ -334,6 +336,31 @@ func (r *AWSMachine) validateRootVolume() field.ErrorList {
334336
return allErrs
335337
}
336338

339+
func (r *AWSMachine) validateNetworkElasticIPPool() field.ErrorList {
340+
var allErrs field.ErrorList
341+
342+
if r.Spec.ElasticIPPool == nil {
343+
return allErrs
344+
}
345+
if !ptr.Deref(r.Spec.PublicIP, false) {
346+
allErrs = append(allErrs, field.Required(field.NewPath("spec.elasticIpPool"), "publicIp must be set to 'true' to assign custom public IPv4 pools with elasticIpPool"))
347+
}
348+
eipp := r.Spec.ElasticIPPool
349+
if eipp.PublicIpv4Pool != nil {
350+
if eipp.PublicIpv4PoolFallBackOrder == nil {
351+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.elasticIpPool.publicIpv4PoolFallbackOrder"), r.Spec.ElasticIPPool, "publicIpv4PoolFallbackOrder must be set when publicIpv4Pool is defined."))
352+
}
353+
awsPublicIpv4PoolPrefix := "ipv4pool-ec2-"
354+
if !strings.HasPrefix(*eipp.PublicIpv4Pool, awsPublicIpv4PoolPrefix) {
355+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.elasticIpPool.publicIpv4Pool"), r.Spec.ElasticIPPool, fmt.Sprintf("publicIpv4Pool must start with %s.", awsPublicIpv4PoolPrefix)))
356+
}
357+
} else if eipp.PublicIpv4PoolFallBackOrder != nil {
358+
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.elasticIpPool.publicIpv4PoolFallbackOrder"), r.Spec.ElasticIPPool, "publicIpv4Pool must be set when publicIpv4PoolFallbackOrder is defined."))
359+
}
360+
361+
return allErrs
362+
}
363+
337364
func (r *AWSMachine) validateNonRootVolumes() field.ErrorList {
338365
var allErrs field.ErrorList
339366

api/v1beta2/awsmachine_webhook_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,74 @@ func TestAWSMachineCreate(t *testing.T) {
368368
},
369369
wantErr: true,
370370
},
371+
{
372+
name: "create with valid BYOIPv4",
373+
machine: &AWSMachine{
374+
Spec: AWSMachineSpec{
375+
InstanceType: "type",
376+
PublicIP: aws.Bool(true),
377+
ElasticIPPool: &ElasticIPPool{
378+
PublicIpv4Pool: aws.String("ipv4pool-ec2-0123456789abcdef0"),
379+
PublicIpv4PoolFallBackOrder: ptr.To(PublicIpv4PoolFallbackOrderAmazonPool),
380+
},
381+
},
382+
},
383+
wantErr: false,
384+
},
385+
{
386+
name: "error when BYOIPv4 without fallback",
387+
machine: &AWSMachine{
388+
Spec: AWSMachineSpec{
389+
InstanceType: "type",
390+
PublicIP: aws.Bool(true),
391+
ElasticIPPool: &ElasticIPPool{
392+
PublicIpv4Pool: aws.String("ipv4pool-ec2-0123456789abcdef0"),
393+
},
394+
},
395+
},
396+
wantErr: true,
397+
},
398+
{
399+
name: "error when BYOIPv4 without public ipv4 pool",
400+
machine: &AWSMachine{
401+
Spec: AWSMachineSpec{
402+
InstanceType: "type",
403+
PublicIP: aws.Bool(true),
404+
ElasticIPPool: &ElasticIPPool{
405+
PublicIpv4PoolFallBackOrder: ptr.To(PublicIpv4PoolFallbackOrderAmazonPool),
406+
},
407+
},
408+
},
409+
wantErr: true,
410+
},
411+
{
412+
name: "error when BYOIPv4 with non-public IP set",
413+
machine: &AWSMachine{
414+
Spec: AWSMachineSpec{
415+
InstanceType: "type",
416+
PublicIP: aws.Bool(false),
417+
ElasticIPPool: &ElasticIPPool{
418+
PublicIpv4Pool: aws.String("ipv4pool-ec2-0123456789abcdef0"),
419+
PublicIpv4PoolFallBackOrder: ptr.To(PublicIpv4PoolFallbackOrderAmazonPool),
420+
},
421+
},
422+
},
423+
wantErr: true,
424+
},
425+
{
426+
name: "error when BYOIPv4 with invalid pool name",
427+
machine: &AWSMachine{
428+
Spec: AWSMachineSpec{
429+
InstanceType: "type",
430+
PublicIP: aws.Bool(true),
431+
ElasticIPPool: &ElasticIPPool{
432+
PublicIpv4Pool: aws.String("ipv4poolx-ec2-0123456789abcdef"),
433+
PublicIpv4PoolFallBackOrder: ptr.To(PublicIpv4PoolFallbackOrderAmazonPool),
434+
},
435+
},
436+
},
437+
wantErr: true,
438+
},
371439
}
372440
for _, tt := range tests {
373441
t.Run(tt.name, func(t *testing.T) {

api/v1beta2/network_types.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,12 @@ type VPCSpec struct {
455455
// +optional
456456
// +kubebuilder:validation:Enum:=ip-name;resource-name
457457
PrivateDNSHostnameTypeOnLaunch *string `json:"privateDnsHostnameTypeOnLaunch,omitempty"`
458+
459+
// ElasticIPPool contains specific configuration to allocate Public IPv4 address (Elastic IP) from user-defined pool
460+
// brought to AWS for core infrastructure resources, like NAT Gateways and Public Network Load Balancers for
461+
// the API Server.
462+
// +optional
463+
ElasticIPPool *ElasticIPPool `json:"elasticIpPool,omitempty"`
458464
}
459465

460466
// String returns a string representation of the VPC.
@@ -477,6 +483,22 @@ func (v *VPCSpec) IsIPv6Enabled() bool {
477483
return v.IPv6 != nil
478484
}
479485

486+
// GetElasticIPPool returns the custom Elastic IP Pool configuration when present.
487+
func (v *VPCSpec) GetElasticIPPool() *ElasticIPPool {
488+
return v.ElasticIPPool
489+
}
490+
491+
// GetPublicIpv4Pool returns the custom public IPv4 pool brought to AWS when present.
492+
func (v *VPCSpec) GetPublicIpv4Pool() *string {
493+
if v.ElasticIPPool == nil {
494+
return nil
495+
}
496+
if v.ElasticIPPool.PublicIpv4Pool != nil {
497+
return v.ElasticIPPool.PublicIpv4Pool
498+
}
499+
return nil
500+
}
501+
480502
// SubnetSpec configures an AWS Subnet.
481503
type SubnetSpec struct {
482504
// ID defines a unique identifier to reference this resource.
@@ -1013,3 +1035,57 @@ func (z ZoneType) String() string {
10131035
func (z ZoneType) Equal(other ZoneType) bool {
10141036
return z == other
10151037
}
1038+
1039+
// ElasticIPPool allows configuring a Elastic IP pool for resources allocating
1040+
// public IPv4 addresses on public subnets.
1041+
type ElasticIPPool struct {
1042+
// PublicIpv4Pool sets a custom Public IPv4 Pool used to create Elastic IP address for resources
1043+
// created in public IPv4 subnets. Every IPv4 address, Elastic IP, will be allocated from the custom
1044+
// Public IPv4 pool that you brought to AWS, instead of Amazon-provided pool. The public IPv4 pool
1045+
// resource ID starts with 'ipv4pool-ec2'.
1046+
//
1047+
// +kubebuilder:validation:MaxLength=30
1048+
// +optional
1049+
PublicIpv4Pool *string `json:"publicIpv4Pool,omitempty"`
1050+
1051+
// PublicIpv4PoolFallBackOrder defines the fallback action when the Public IPv4 Pool has been exhausted,
1052+
// no more IPv4 address available in the pool.
1053+
//
1054+
// When set to 'amazon-pool', the controller check if the pool has available IPv4 address, when pool has reached the
1055+
// IPv4 limit, the address will be claimed from Amazon-pool (default).
1056+
//
1057+
// When set to 'none', the controller will fail the Elastic IP allocation when the publicIpv4Pool is exhausted.
1058+
//
1059+
// +kubebuilder:validation:Enum:=amazon-pool;none
1060+
// +optional
1061+
PublicIpv4PoolFallBackOrder *PublicIpv4PoolFallbackOrder `json:"publicIpv4PoolFallbackOrder,omitempty"`
1062+
1063+
// TODO(mtulio): add future support of user-defined Elastic IP to allow users to assign BYO Public IP from
1064+
// 'static'/preallocated amazon-provided IPsstrucute currently holds only 'BYO Public IP from Public IPv4 Pool' (user brought to AWS),
1065+
// although a dedicated structure would help to hold 'BYO Elastic IP' variants like:
1066+
// - AllocationIdPoolApiLoadBalancer: an user-defined (static) IP address to the Public API Load Balancer.
1067+
// - AllocationIdPoolNatGateways: an user-defined (static) IP address to allocate to NAT Gateways (egress traffic).
1068+
}
1069+
1070+
// PublicIpv4PoolFallbackOrder defines the list of available fallback action when the PublicIpv4Pool is exhausted.
1071+
// 'none' let the controllers return failures when the PublicIpv4Pool is exhausted - no more IPv4 available.
1072+
// 'amazon-pool' let the controllers to skip the PublicIpv4Pool and use the Amazon pool, the default.
1073+
// +kubebuilder:validation:XValidation:rule="self in ['none','amazon-pool']",message="allowed values are 'none' and 'amazon-pool'"
1074+
type PublicIpv4PoolFallbackOrder string
1075+
1076+
const (
1077+
// PublicIpv4PoolFallbackOrderAmazonPool refers to use Amazon-pool Public IPv4 Pool as a fallback strategy.
1078+
PublicIpv4PoolFallbackOrderAmazonPool = PublicIpv4PoolFallbackOrder("amazon-pool")
1079+
1080+
// PublicIpv4PoolFallbackOrderNone refers to not use any fallback strategy.
1081+
PublicIpv4PoolFallbackOrderNone = PublicIpv4PoolFallbackOrder("none")
1082+
)
1083+
1084+
func (r PublicIpv4PoolFallbackOrder) String() string {
1085+
return string(r)
1086+
}
1087+
1088+
// Equal compares PublicIpv4PoolFallbackOrder types and return true if input param is equal.
1089+
func (r PublicIpv4PoolFallbackOrder) Equal(e PublicIpv4PoolFallbackOrder) bool {
1090+
return r == e
1091+
}

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 35 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)