diff --git a/apis/elbv2/v1beta1/ingressclassparams_types.go b/apis/elbv2/v1beta1/ingressclassparams_types.go index 4fc921694..27b9abcb6 100644 --- a/apis/elbv2/v1beta1/ingressclassparams_types.go +++ b/apis/elbv2/v1beta1/ingressclassparams_types.go @@ -107,6 +107,13 @@ type MinimumLoadBalancerCapacity struct { CapacityUnits int32 `json:"capacityUnits"` } +// IPAMConfiguration defines the IPAM configuration for an Ingress. +type IPAMConfiguration struct { + // IPv4IPAMPoolId defines the IPAM pool ID used for IPv4 Addresses on the ALB. + // +optional + IPv4IPAMPoolId *string `json:"ipv4IPAMPoolId,omitempty"` +} + // IngressClassParamsSpec defines the desired state of IngressClassParams type IngressClassParamsSpec struct { // CertificateArn specifies the ARN of the certificates for all Ingresses that belong to IngressClass with this IngressClassParams. @@ -156,6 +163,10 @@ type IngressClassParamsSpec struct { // MinimumLoadBalancerCapacity define the capacity reservation for LoadBalancers for all Ingress that belong to IngressClass with this IngressClassParams. // +optional MinimumLoadBalancerCapacity *MinimumLoadBalancerCapacity `json:"minimumLoadBalancerCapacity,omitempty"` + + // IPAMConfiguration defines the IPAM settings for a Load Balancer. + // +optional + IPAMConfiguration *IPAMConfiguration `json:"ipamConfiguration,omitempty"` } // +kubebuilder:object:root=true diff --git a/apis/elbv2/v1beta1/zz_generated.deepcopy.go b/apis/elbv2/v1beta1/zz_generated.deepcopy.go index 108289a52..068b3dcf3 100644 --- a/apis/elbv2/v1beta1/zz_generated.deepcopy.go +++ b/apis/elbv2/v1beta1/zz_generated.deepcopy.go @@ -41,6 +41,26 @@ func (in *Attribute) DeepCopy() *Attribute { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPAMConfiguration) DeepCopyInto(out *IPAMConfiguration) { + *out = *in + if in.IPv4IPAMPoolId != nil { + in, out := &in.IPv4IPAMPoolId, &out.IPv4IPAMPoolId + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPAMConfiguration. +func (in *IPAMConfiguration) DeepCopy() *IPAMConfiguration { + if in == nil { + return nil + } + out := new(IPAMConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPBlock) DeepCopyInto(out *IPBlock) { *out = *in @@ -174,6 +194,11 @@ func (in *IngressClassParamsSpec) DeepCopyInto(out *IngressClassParamsSpec) { *out = new(MinimumLoadBalancerCapacity) **out = **in } + if in.IPAMConfiguration != nil { + in, out := &in.IPAMConfiguration, &out.IPAMConfiguration + *out = new(IPAMConfiguration) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressClassParamsSpec. diff --git a/config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml b/config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml index 7f65f28ad..18da6634e 100644 --- a/config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml +++ b/config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml @@ -85,6 +85,15 @@ spec: - dualstack - dualstack-without-public-ipv4 type: string + ipamConfiguration: + description: IPAMConfiguration defines the IPAM settings for a Load + Balancer. + properties: + ipv4IPAMPoolId: + description: IPv4IPAMPoolId defines the IPAM pool ID used for + IPv4 Addresses on the ALB. + type: string + type: object listeners: description: Listeners define a list of listeners with their protocol, port and attributes. diff --git a/docs/guide/ingress/annotations.md b/docs/guide/ingress/annotations.md index 0dd09ed56..c1c6c7fcd 100644 --- a/docs/guide/ingress/annotations.md +++ b/docs/guide/ingress/annotations.md @@ -14,54 +14,55 @@ You can add annotations to kubernetes Ingress and Service objects to customize t - Merge: such annotation can be specified on all Ingresses within IngressGroup, and will be merged together. ## Annotations -| Name | Type |Default| Location | MergeBehavior | -|-------------------------------------------------------------------------------------------------------|-----------------------------|------|-----------------|---------------| -| [alb.ingress.kubernetes.io/load-balancer-name](#load-balancer-name) | string |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/group.name](#group.name) | string |N/A| Ingress | N/A | -| [alb.ingress.kubernetes.io/group.order](#group.order) | integer |0| Ingress | N/A | -| [alb.ingress.kubernetes.io/tags](#tags) | stringMap |N/A| Ingress,Service | Merge | -| [alb.ingress.kubernetes.io/ip-address-type](#ip-address-type) | ipv4 \| dualstack \| dualstack-without-public-ipv4 |ipv4| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/scheme](#scheme) | internal \| internet-facing |internal| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/subnets](#subnets) | stringList |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/security-groups](#security-groups) | stringList |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/manage-backend-security-group-rules](#manage-backend-security-group-rules) | boolean |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/customer-owned-ipv4-pool](#customer-owned-ipv4-pool) | string |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/load-balancer-attributes](#load-balancer-attributes) | stringMap |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/wafv2-acl-arn](#wafv2-acl-arn) | string |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/waf-acl-id](#waf-acl-id) | string |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/shield-advanced-protection](#shield-advanced-protection) | boolean |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/listen-ports](#listen-ports) | json |'[{"HTTP": 80}]' \| '[{"HTTPS": 443}]'| Ingress | Merge | -| [alb.ingress.kubernetes.io/ssl-redirect](#ssl-redirect) | integer |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/inbound-cidrs](#inbound-cidrs) | stringList |0.0.0.0/0, ::/0| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/security-group-prefix-lists](#security-group-prefix-lists) | stringList |pl-00000000, pl-1111111| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/certificate-arn](#certificate-arn) | stringList |N/A| Ingress | Merge | -| [alb.ingress.kubernetes.io/ssl-policy](#ssl-policy) | string |ELBSecurityPolicy-2016-08| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/target-type](#target-type) | instance \| ip |instance| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/backend-protocol](#backend-protocol) | HTTP \| HTTPS |HTTP| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/backend-protocol-version](#backend-protocol-version) | string | HTTP1 | Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/target-group-attributes](#target-group-attributes) | stringMap |N/A| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/healthcheck-port](#healthcheck-port) | integer \| traffic-port |traffic-port| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/healthcheck-protocol](#healthcheck-protocol) | HTTP \| HTTPS |HTTP| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/healthcheck-path](#healthcheck-path) | string |/ \| /AWS.ALB/healthcheck | Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/healthcheck-interval-seconds](#healthcheck-interval-seconds) | integer |'15'| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/healthcheck-timeout-seconds](#healthcheck-timeout-seconds) | integer |'5'| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/healthy-threshold-count](#healthy-threshold-count) | integer |'2'| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/unhealthy-threshold-count](#unhealthy-threshold-count) | integer |'2'| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/success-codes](#success-codes) | string |'200' \| '12' | Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/auth-type](#auth-type) | none\|oidc\|cognito |none| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/auth-idp-cognito](#auth-idp-cognito) | json |N/A| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/auth-idp-oidc](#auth-idp-oidc) | json |N/A| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/auth-on-unauthenticated-request](#auth-on-unauthenticated-request) | authenticate\|allow\|deny |authenticate| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/auth-scope](#auth-scope) | string |openid| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/auth-session-cookie](#auth-session-cookie) | string |AWSELBAuthSessionCookie| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/auth-session-timeout](#auth-session-timeout) | integer |'604800'| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/actions.${action-name}](#actions) | json |N/A| Ingress | N/A | -| [alb.ingress.kubernetes.io/conditions.${conditions-name}](#conditions) | json |N/A| Ingress | N/A | -| [alb.ingress.kubernetes.io/target-node-labels](#target-node-labels) | stringMap |N/A| Ingress,Service | N/A | -| [alb.ingress.kubernetes.io/mutual-authentication](#mutual-authentication) | json |N/A| Ingress | Exclusive | -| [alb.ingress.kubernetes.io/multi-cluster-target-group](#multi-cluster-target-group) | boolean |N/A| Ingress, Service | N/A | -| [alb.ingress.kubernetes.io/listener-attributes.${Protocol}-${Port}](#listener-attributes) | stringMap |N/A| Ingress |Merge| -| [alb.ingress.kubernetes.io/minimum-load-balancer-capacity](#load-balancer-capacity-reservation) | stringMap |N/A| Ingress | Exclusive | +| Name | Type |Default| Location | MergeBehavior | +|-------------------------------------------------------------------------------------------------------|----------------------------------------------------|------|-----------------|---------------| +| [alb.ingress.kubernetes.io/load-balancer-name](#load-balancer-name) | string |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/group.name](#group.name) | string |N/A| Ingress | N/A | +| [alb.ingress.kubernetes.io/group.order](#group.order) | integer |0| Ingress | N/A | +| [alb.ingress.kubernetes.io/tags](#tags) | stringMap |N/A| Ingress,Service | Merge | +| [alb.ingress.kubernetes.io/ip-address-type](#ip-address-type) | ipv4 \| dualstack \| dualstack-without-public-ipv4 |ipv4| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/scheme](#scheme) | internal \| internet-facing |internal| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/subnets](#subnets) | stringList |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/security-groups](#security-groups) | stringList |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/manage-backend-security-group-rules](#manage-backend-security-group-rules) | boolean |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/customer-owned-ipv4-pool](#customer-owned-ipv4-pool) | string |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/load-balancer-attributes](#load-balancer-attributes) | stringMap |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/wafv2-acl-arn](#wafv2-acl-arn) | string |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/waf-acl-id](#waf-acl-id) | string |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/shield-advanced-protection](#shield-advanced-protection) | boolean |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/listen-ports](#listen-ports) | json |'[{"HTTP": 80}]' \| '[{"HTTPS": 443}]'| Ingress | Merge | +| [alb.ingress.kubernetes.io/ssl-redirect](#ssl-redirect) | integer |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/inbound-cidrs](#inbound-cidrs) | stringList |0.0.0.0/0, ::/0| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/security-group-prefix-lists](#security-group-prefix-lists) | stringList |pl-00000000, pl-1111111| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/certificate-arn](#certificate-arn) | stringList |N/A| Ingress | Merge | +| [alb.ingress.kubernetes.io/ssl-policy](#ssl-policy) | string |ELBSecurityPolicy-2016-08| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/target-type](#target-type) | instance \| ip |instance| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/backend-protocol](#backend-protocol) | HTTP \| HTTPS |HTTP| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/backend-protocol-version](#backend-protocol-version) | string | HTTP1 | Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/target-group-attributes](#target-group-attributes) | stringMap |N/A| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/healthcheck-port](#healthcheck-port) | integer \| traffic-port |traffic-port| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/healthcheck-protocol](#healthcheck-protocol) | HTTP \| HTTPS |HTTP| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/healthcheck-path](#healthcheck-path) | string |/ \| /AWS.ALB/healthcheck | Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/healthcheck-interval-seconds](#healthcheck-interval-seconds) | integer |'15'| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/healthcheck-timeout-seconds](#healthcheck-timeout-seconds) | integer |'5'| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/healthy-threshold-count](#healthy-threshold-count) | integer |'2'| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/unhealthy-threshold-count](#unhealthy-threshold-count) | integer |'2'| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/success-codes](#success-codes) | string |'200' \| '12' | Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/auth-type](#auth-type) | none\|oidc\|cognito |none| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/auth-idp-cognito](#auth-idp-cognito) | json |N/A| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/auth-idp-oidc](#auth-idp-oidc) | json |N/A| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/auth-on-unauthenticated-request](#auth-on-unauthenticated-request) | authenticate\|allow\|deny |authenticate| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/auth-scope](#auth-scope) | string |openid| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/auth-session-cookie](#auth-session-cookie) | string |AWSELBAuthSessionCookie| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/auth-session-timeout](#auth-session-timeout) | integer |'604800'| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/actions.${action-name}](#actions) | json |N/A| Ingress | N/A | +| [alb.ingress.kubernetes.io/conditions.${conditions-name}](#conditions) | json |N/A| Ingress | N/A | +| [alb.ingress.kubernetes.io/target-node-labels](#target-node-labels) | stringMap |N/A| Ingress,Service | N/A | +| [alb.ingress.kubernetes.io/mutual-authentication](#mutual-authentication) | json |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/multi-cluster-target-group](#multi-cluster-target-group) | boolean |N/A| Ingress, Service | N/A | +| [alb.ingress.kubernetes.io/listener-attributes.${Protocol}-${Port}](#listener-attributes) | stringMap |N/A| Ingress |Merge| +| [alb.ingress.kubernetes.io/minimum-load-balancer-capacity](#load-balancer-capacity-reservation) | stringMap |N/A| Ingress | Exclusive | +| [alb.ingress.kubernetes.io/ipam-ipv4-pool-id](#ipam-ipv4-pool-id) | string |N/A| Ingress | Exclusive | ## IngressGroup IngressGroup feature enables you to group multiple Ingress resources together. @@ -247,6 +248,20 @@ Traffic Routing can be controlled with following annotations: alb.ingress.kubernetes.io/subnets: subnet-xxxx, mySubnet ``` +- <a name="ipam-ipv4-pool-id">`alb.ingress.kubernetes.io/ipam-ipv4-pool-id`</a> Specifies the [IPv4 IPAM Pool ID](https://docs.aws.amazon.com/vpc/latest/ipam/tutorials-byoip-ipam-console-ipv4.html) which will be used by your load balancer to assign IP addresses. + + !!!note "" + The chosen IPAM pool is always the prioritized source when assigning public IPv4 addresses. + If there are no more assignable IP addresses in the IPAM pool, AWS managed IPv4 addresses are assigned. + + !!!tip + To remove an IPAM pool associated to your ALB, remove the annotation from your ingress. + + !!!example + ``` + alb.ingress.kubernetes.io/ipam-ipv4-pool-id: ipam-pool-0f995c17c00375b48 + ``` + - <a name="actions">`alb.ingress.kubernetes.io/actions.${action-name}`</a> Provides a method for configuring custom actions on a listener, such as Redirect Actions. The `action-name` in the annotation must match the serviceName in the Ingress rules, and servicePort must be `use-annotation`. diff --git a/docs/guide/ingress/ingress_class.md b/docs/guide/ingress/ingress_class.md index fd5d6ee95..3a21487a5 100644 --- a/docs/guide/ingress/ingress_class.md +++ b/docs/guide/ingress/ingress_class.md @@ -251,4 +251,10 @@ They may specify `capacityUnits`. If the field is specified, LBC will ignore the ##### spec.minimumLoadBalancerCapacity.capacityUnits -If `capacityUnits` is specified, it must be to valid positive value greater than 0. If set to 0, the LBC will reset the capacity reservation for the load balancer. \ No newline at end of file +If `capacityUnits` is specified, it must be to valid positive value greater than 0. If set to 0, the LBC will reset the capacity reservation for the load balancer. + +#### spec.ipv4IPAMPoolId + +The IPAM pool you choose will be the preferred source of public IPv4 addresses. +If the pool is depleted, IPv4 addresses will be assigned by AWS. +To remove the IPAM pool from your ALB, remove `spec.ipv4IPAMPoolId` from the IngressClass definition. diff --git a/docs/install/iam_policy.json b/docs/install/iam_policy.json index 0da4ee564..fe1976170 100644 --- a/docs/install/iam_policy.json +++ b/docs/install/iam_policy.json @@ -30,6 +30,7 @@ "ec2:GetCoipPoolUsage", "ec2:DescribeCoipPools", "ec2:GetSecurityGroupsForVpc", + "ec2:DescribeIpamPools", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeListeners", @@ -193,7 +194,8 @@ "elasticloadbalancing:ModifyTargetGroupAttributes", "elasticloadbalancing:DeleteTargetGroup", "elasticloadbalancing:ModifyListenerAttributes", - "elasticloadbalancing:ModifyCapacityReservation" + "elasticloadbalancing:ModifyCapacityReservation", + "elasticloadbalancing:ModifyIpPools" ], "Resource": "*", "Condition": { diff --git a/docs/install/iam_policy_cn.json b/docs/install/iam_policy_cn.json index bb475f6ec..bd2b8b8c9 100644 --- a/docs/install/iam_policy_cn.json +++ b/docs/install/iam_policy_cn.json @@ -30,6 +30,7 @@ "ec2:GetCoipPoolUsage", "ec2:DescribeCoipPools", "ec2:GetSecurityGroupsForVpc", + "ec2:DescribeIpamPools", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeListeners", @@ -215,7 +216,8 @@ "elasticloadbalancing:ModifyTargetGroupAttributes", "elasticloadbalancing:DeleteTargetGroup", "elasticloadbalancing:ModifyListenerAttributes", - "elasticloadbalancing:ModifyCapacityReservation" + "elasticloadbalancing:ModifyCapacityReservation", + "elasticloadbalancing:ModifyIpPools" ], "Resource": "*", "Condition": { diff --git a/docs/install/iam_policy_iso.json b/docs/install/iam_policy_iso.json index 46187d30e..9d032e395 100644 --- a/docs/install/iam_policy_iso.json +++ b/docs/install/iam_policy_iso.json @@ -30,6 +30,7 @@ "ec2:GetCoipPoolUsage", "ec2:DescribeCoipPools", "ec2:GetSecurityGroupsForVpc", + "ec2:DescribeIpamPools", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeListeners", @@ -210,7 +211,8 @@ "elasticloadbalancing:DeleteLoadBalancer", "elasticloadbalancing:ModifyTargetGroup", "elasticloadbalancing:ModifyTargetGroupAttributes", - "elasticloadbalancing:DeleteTargetGroup" + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:ModifyIpPools" ], "Resource": "*", "Condition": { diff --git a/docs/install/iam_policy_isob.json b/docs/install/iam_policy_isob.json index c7107d710..6288c9c62 100644 --- a/docs/install/iam_policy_isob.json +++ b/docs/install/iam_policy_isob.json @@ -30,6 +30,7 @@ "ec2:GetCoipPoolUsage", "ec2:DescribeCoipPools", "ec2:GetSecurityGroupsForVpc", + "ec2:DescribeIpamPools", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeListeners", @@ -210,7 +211,8 @@ "elasticloadbalancing:DeleteLoadBalancer", "elasticloadbalancing:ModifyTargetGroup", "elasticloadbalancing:ModifyTargetGroupAttributes", - "elasticloadbalancing:DeleteTargetGroup" + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:ModifyIpPools" ], "Resource": "*", "Condition": { diff --git a/docs/install/iam_policy_isoe.json b/docs/install/iam_policy_isoe.json index 6db25833e..499afd947 100644 --- a/docs/install/iam_policy_isoe.json +++ b/docs/install/iam_policy_isoe.json @@ -30,6 +30,7 @@ "ec2:GetCoipPoolUsage", "ec2:DescribeCoipPools", "ec2:GetSecurityGroupsForVpc", + "ec2:DescribeIpamPools", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeListeners", @@ -210,7 +211,8 @@ "elasticloadbalancing:DeleteLoadBalancer", "elasticloadbalancing:ModifyTargetGroup", "elasticloadbalancing:ModifyTargetGroupAttributes", - "elasticloadbalancing:DeleteTargetGroup" + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:ModifyIpPools" ], "Resource": "*", "Condition": { diff --git a/docs/install/iam_policy_isof.json b/docs/install/iam_policy_isof.json index 7a43d3e40..08c1d1390 100644 --- a/docs/install/iam_policy_isof.json +++ b/docs/install/iam_policy_isof.json @@ -30,6 +30,7 @@ "ec2:GetCoipPoolUsage", "ec2:DescribeCoipPools", "ec2:GetSecurityGroupsForVpc", + "ec2:DescribeIpamPools", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeListeners", @@ -210,7 +211,8 @@ "elasticloadbalancing:DeleteLoadBalancer", "elasticloadbalancing:ModifyTargetGroup", "elasticloadbalancing:ModifyTargetGroupAttributes", - "elasticloadbalancing:DeleteTargetGroup" + "elasticloadbalancing:DeleteTargetGroup", + "elasticloadbalancing:ModifyIpPools" ], "Resource": "*", "Condition": { diff --git a/docs/install/iam_policy_us-gov.json b/docs/install/iam_policy_us-gov.json index 790304851..e08541c3a 100644 --- a/docs/install/iam_policy_us-gov.json +++ b/docs/install/iam_policy_us-gov.json @@ -30,6 +30,7 @@ "ec2:GetCoipPoolUsage", "ec2:DescribeCoipPools", "ec2:GetSecurityGroupsForVpc", + "ec2:DescribeIpamPools", "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeLoadBalancerAttributes", "elasticloadbalancing:DescribeListeners", @@ -215,7 +216,8 @@ "elasticloadbalancing:ModifyTargetGroupAttributes", "elasticloadbalancing:DeleteTargetGroup", "elasticloadbalancing:ModifyListenerAttributes", - "elasticloadbalancing:ModifyCapacityReservation" + "elasticloadbalancing:ModifyCapacityReservation", + "elasticloadbalancing:ModifyIpPools" ], "Resource": "*", "Condition": { diff --git a/go.mod b/go.mod index a2186f227..cd974f81d 100644 --- a/go.mod +++ b/go.mod @@ -4,21 +4,21 @@ go 1.23.6 require ( github.com/aws/aws-sdk-go v1.55.5 - github.com/aws/aws-sdk-go-v2 v1.32.6 + github.com/aws/aws-sdk-go-v2 v1.36.3 github.com/aws/aws-sdk-go-v2/config v1.27.27 github.com/aws/aws-sdk-go-v2/credentials v1.17.27 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 github.com/aws/aws-sdk-go-v2/service/acm v1.28.4 github.com/aws/aws-sdk-go-v2/service/appmesh v1.27.7 github.com/aws/aws-sdk-go-v2/service/ec2 v1.173.0 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.43.1 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.45.0 github.com/aws/aws-sdk-go-v2/service/resourcegroupstaggingapi v1.23.3 github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.31.7 github.com/aws/aws-sdk-go-v2/service/shield v1.27.3 github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 github.com/aws/aws-sdk-go-v2/service/wafregional v1.23.3 github.com/aws/aws-sdk-go-v2/service/wafv2 v1.51.4 - github.com/aws/smithy-go v1.22.1 + github.com/aws/smithy-go v1.22.2 github.com/evanphx/json-patch v5.7.0+incompatible github.com/gavv/httpexpect/v2 v2.9.0 github.com/go-logr/logr v1.4.2 @@ -58,8 +58,8 @@ require ( github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect diff --git a/go.sum b/go.sum index 292ee96ce..1f07bb6f0 100644 --- a/go.sum +++ b/go.sum @@ -38,18 +38,18 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= -github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= +github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/acm v1.28.4 h1:wiW1Y6/1lysA0eJZRq0I53YYKuV9MNAzL15z2eZRlEE= @@ -58,8 +58,8 @@ github.com/aws/aws-sdk-go-v2/service/appmesh v1.27.7 h1:q44a6kysAfej9zZwRnraOg9s github.com/aws/aws-sdk-go-v2/service/appmesh v1.27.7/go.mod h1:ZYSmrgAMp0rTCHH+SGsoxZo+PPbgsDqBzewTp3tSJ60= github.com/aws/aws-sdk-go-v2/service/ec2 v1.173.0 h1:ta62lid9JkIpKZtZZXSj6rP2AqY5x1qYGq53ffxqD9Q= github.com/aws/aws-sdk-go-v2/service/ec2 v1.173.0/go.mod h1:o6QDjdVKpP5EF0dp/VlvqckzuSDATr1rLdHt3A5m0YY= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.43.1 h1:L9Wt9zgtoYKIlaeFTy+EztGjL4oaXBBGtVXA+jaeYko= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.43.1/go.mod h1:yxzLdxt7bVGvIOPYIKFtiaJCJnx2ChlIIvlhW4QgI6M= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.45.0 h1:RB7V8wT9ypjE/YJVBgKjoydTOh4IFoqceGiKxFH70mY= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.45.0/go.mod h1:xnCC3vFBfOKpU6PcsCKL2ktgBTZfOwTGxj6V8/X3IS4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= @@ -80,8 +80,8 @@ github.com/aws/aws-sdk-go-v2/service/wafregional v1.23.3 h1:7dr6En0/6KRFoz8VmnYk github.com/aws/aws-sdk-go-v2/service/wafregional v1.23.3/go.mod h1:24TtlRsv4LKAE3VnRJQhpatr8cpX0yj8NSzg8/lxOCw= github.com/aws/aws-sdk-go-v2/service/wafv2 v1.51.4 h1:1khBA5uryBRJoCb4G2iR5RT06BkfPEjjDCHAiRb8P3Q= github.com/aws/aws-sdk-go-v2/service/wafv2 v1.51.4/go.mod h1:QpFImaPGKNwa+MiZ+oo6LbV1PVQBapc0CnrAMRScoxM= -github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= -github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= diff --git a/helm/aws-load-balancer-controller/crds/crds.yaml b/helm/aws-load-balancer-controller/crds/crds.yaml index ea31acc11..e5d1cc177 100644 --- a/helm/aws-load-balancer-controller/crds/crds.yaml +++ b/helm/aws-load-balancer-controller/crds/crds.yaml @@ -84,6 +84,15 @@ spec: - dualstack - dualstack-without-public-ipv4 type: string + ipamConfiguration: + description: IPAMConfiguration defines the IPAM settings for a Load + Balancer. + properties: + ipv4IPAMPoolId: + description: IPv4IPAMPoolId defines the IPAM pool ID used for + IPv4 Addresses on the ALB. + type: string + type: object listeners: description: Listeners define a list of listeners with their protocol, port and attributes. diff --git a/pkg/annotations/constants.go b/pkg/annotations/constants.go index ba7021654..e10709fc9 100644 --- a/pkg/annotations/constants.go +++ b/pkg/annotations/constants.go @@ -58,6 +58,7 @@ const ( IngressSuffixlsAttsAnnotationPrefix = "listener-attributes" IngressLBSuffixMultiClusterTargetGroup = "multi-cluster-target-group" IngressSuffixLoadBalancerCapacityReservation = "minimum-load-balancer-capacity" + IngressSuffixIPAMIPv4PoolId = "ipam-ipv4-pool-id" // NLB annotation suffixes // prefixes service.beta.kubernetes.io, service.kubernetes.io diff --git a/pkg/aws/services/elbv2.go b/pkg/aws/services/elbv2.go index c2eb0d401..1db7d21b0 100644 --- a/pkg/aws/services/elbv2.go +++ b/pkg/aws/services/elbv2.go @@ -61,6 +61,7 @@ type ELBV2 interface { ModifyListenerAttributesWithContext(ctx context.Context, input *elasticloadbalancingv2.ModifyListenerAttributesInput) (*elasticloadbalancingv2.ModifyListenerAttributesOutput, error) ModifyCapacityReservationWithContext(ctx context.Context, input *elasticloadbalancingv2.ModifyCapacityReservationInput) (*elasticloadbalancingv2.ModifyCapacityReservationOutput, error) DescribeCapacityReservationWithContext(ctx context.Context, input *elasticloadbalancingv2.DescribeCapacityReservationInput) (*elasticloadbalancingv2.DescribeCapacityReservationOutput, error) + ModifyIPPoolsWithContext(ctx context.Context, input *elasticloadbalancingv2.ModifyIpPoolsInput) (*elasticloadbalancingv2.ModifyIpPoolsOutput, error) AssumeRole(ctx context.Context, assumeRoleArn string, externalId string) (ELBV2, error) } @@ -485,6 +486,14 @@ func (c *elbv2Client) DescribeCapacityReservationWithContext(ctx context.Context return client.DescribeCapacityReservation(ctx, input) } +func (c *elbv2Client) ModifyIPPoolsWithContext(ctx context.Context, input *elasticloadbalancingv2.ModifyIpPoolsInput) (*elasticloadbalancingv2.ModifyIpPoolsOutput, error) { + client, err := c.getClient(ctx, "ModifyIpPools") + if err != nil { + return nil, err + } + return client.ModifyIpPools(ctx, input) +} + func (c *elbv2Client) getClient(ctx context.Context, operation string) (*elasticloadbalancingv2.Client, error) { if c.staticELBClient != nil { return c.staticELBClient, nil diff --git a/pkg/aws/services/elbv2_mocks.go b/pkg/aws/services/elbv2_mocks.go index e04343b2a..055a7aa80 100644 --- a/pkg/aws/services/elbv2_mocks.go +++ b/pkg/aws/services/elbv2_mocks.go @@ -471,6 +471,21 @@ func (mr *MockELBV2MockRecorder) ModifyCapacityReservationWithContext(arg0, arg1 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyCapacityReservationWithContext", reflect.TypeOf((*MockELBV2)(nil).ModifyCapacityReservationWithContext), arg0, arg1) } +// ModifyIPPoolsWithContext mocks base method. +func (m *MockELBV2) ModifyIPPoolsWithContext(arg0 context.Context, arg1 *elasticloadbalancingv2.ModifyIpPoolsInput) (*elasticloadbalancingv2.ModifyIpPoolsOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ModifyIPPoolsWithContext", arg0, arg1) + ret0, _ := ret[0].(*elasticloadbalancingv2.ModifyIpPoolsOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ModifyIPPoolsWithContext indicates an expected call of ModifyIPPoolsWithContext. +func (mr *MockELBV2MockRecorder) ModifyIPPoolsWithContext(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModifyIPPoolsWithContext", reflect.TypeOf((*MockELBV2)(nil).ModifyIPPoolsWithContext), arg0, arg1) +} + // ModifyListenerAttributesWithContext mocks base method. func (m *MockELBV2) ModifyListenerAttributesWithContext(arg0 context.Context, arg1 *elasticloadbalancingv2.ModifyListenerAttributesInput) (*elasticloadbalancingv2.ModifyListenerAttributesOutput, error) { m.ctrl.T.Helper() diff --git a/pkg/deploy/elbv2/load_balancer_manager.go b/pkg/deploy/elbv2/load_balancer_manager.go index bc76c45aa..093991679 100644 --- a/pkg/deploy/elbv2/load_balancer_manager.go +++ b/pkg/deploy/elbv2/load_balancer_manager.go @@ -90,6 +90,10 @@ func (m *defaultLoadBalancerManager) Create(ctx context.Context, resLB *elbv2mod } func (m *defaultLoadBalancerManager) Update(ctx context.Context, resLB *elbv2model.LoadBalancer, sdkLB LoadBalancerWithTags) (elbv2model.LoadBalancerStatus, error) { + // It's important to remove ipam pools first, because we need to remove any ipam pools before changing the IP Address type. + if err := m.removeIPAMPools(ctx, resLB, sdkLB); err != nil { + return elbv2model.LoadBalancerStatus{}, err + } if err := m.updateSDKLoadBalancerWithTags(ctx, resLB, sdkLB); err != nil { return elbv2model.LoadBalancerStatus{}, err } @@ -108,6 +112,11 @@ func (m *defaultLoadBalancerManager) Update(ctx context.Context, resLB *elbv2mod if err := m.checkSDKLoadBalancerWithCOIPv4Pool(ctx, resLB, sdkLB); err != nil { return elbv2model.LoadBalancerStatus{}, err } + // We can safely change the IPAM pool here after all other modifications are done. + if err := m.addIPAMPools(ctx, resLB, sdkLB); err != nil { + return elbv2model.LoadBalancerStatus{}, err + } + return buildResLoadBalancerStatus(sdkLB), nil } @@ -278,6 +287,43 @@ func (m *defaultLoadBalancerManager) updateSDKLoadBalancerWithTags(ctx context.C WithIgnoredTagKeys(m.externalManagedTags)) } +func (m *defaultLoadBalancerManager) removeIPAMPools(ctx context.Context, resLB *elbv2model.LoadBalancer, sdkLB LoadBalancerWithTags) error { + // No IPAM pool to remove or the request is to actually add / change IPAM pool. + if sdkLB.LoadBalancer.IpamPools == nil || resLB.Spec.IPv4IPAMPool != nil { + return nil + } + + req := &elbv2sdk.ModifyIpPoolsInput{ + RemoveIpamPools: []elbv2types.RemoveIpamPoolEnum{elbv2types.RemoveIpamPoolEnumIpv4}, + LoadBalancerArn: sdkLB.LoadBalancer.LoadBalancerArn, + } + + _, err := m.elbv2Client.ModifyIPPoolsWithContext(ctx, req) + return err +} + +func (m *defaultLoadBalancerManager) addIPAMPools(ctx context.Context, resLB *elbv2model.LoadBalancer, sdkLB LoadBalancerWithTags) error { + // No IPAM pool to set, this case should be handled by removeIPAMPools + if resLB.Spec.IPv4IPAMPool == nil { + return nil + } + + // IPAM pool is already correctly set + if sdkLB.LoadBalancer.IpamPools != nil && awssdk.ToString(sdkLB.LoadBalancer.IpamPools.Ipv4IpamPoolId) == awssdk.ToString(resLB.Spec.IPv4IPAMPool) { + return nil + } + + req := &elbv2sdk.ModifyIpPoolsInput{ + LoadBalancerArn: sdkLB.LoadBalancer.LoadBalancerArn, + IpamPools: &elbv2types.IpamPools{ + Ipv4IpamPoolId: resLB.Spec.IPv4IPAMPool, + }, + } + + _, err := m.elbv2Client.ModifyIPPoolsWithContext(ctx, req) + return err +} + func buildSDKCreateLoadBalancerInput(lbSpec elbv2model.LoadBalancerSpec) (*elbv2sdk.CreateLoadBalancerInput, error) { sdkObj := &elbv2sdk.CreateLoadBalancerInput{} sdkObj.Name = awssdk.String(lbSpec.Name) @@ -296,6 +342,12 @@ func buildSDKCreateLoadBalancerInput(lbSpec elbv2model.LoadBalancerSpec) (*elbv2 sdkObj.EnablePrefixForIpv6SourceNat = elbv2types.EnablePrefixForIpv6SourceNatEnum(lbSpec.EnablePrefixForIpv6SourceNat) } + if lbSpec.IPv4IPAMPool != nil && awssdk.ToString(lbSpec.IPv4IPAMPool) != "" { + sdkObj.IpamPools = &elbv2types.IpamPools{ + Ipv4IpamPoolId: lbSpec.IPv4IPAMPool, + } + } + sdkObj.CustomerOwnedIpv4Pool = lbSpec.CustomerOwnedIPv4Pool return sdkObj, nil } diff --git a/pkg/deploy/elbv2/load_balancer_manager_test.go b/pkg/deploy/elbv2/load_balancer_manager_test.go index e2b8567d1..02cdb9b6e 100644 --- a/pkg/deploy/elbv2/load_balancer_manager_test.go +++ b/pkg/deploy/elbv2/load_balancer_manager_test.go @@ -213,6 +213,48 @@ func Test_buildSDKCreateLoadBalancerInput(t *testing.T) { CustomerOwnedIpv4Pool: awssdk.String("coIP-pool-x"), }, }, + { + name: "application loadBalancer - with ipv4 ipam pool case", + args: args{ + lbSpec: elbv2model.LoadBalancerSpec{ + Name: "my-alb", + Type: elbv2model.LoadBalancerTypeApplication, + Scheme: schemeInternetFacing, + IPAddressType: addressTypeDualStack, + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + }, + { + SubnetID: "subnet-B", + }, + }, + SecurityGroups: []coremodel.StringToken{ + coremodel.LiteralStringToken("sg-A"), + coremodel.LiteralStringToken("sg-B"), + }, + IPv4IPAMPool: awssdk.String("my-ipv4-ipam-pool"), + }, + }, + want: &elbv2sdk.CreateLoadBalancerInput{ + Name: awssdk.String("my-alb"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + IpAddressType: elbv2types.IpAddressTypeDualstack, + Scheme: elbv2types.LoadBalancerSchemeEnumInternetFacing, + SubnetMappings: []elbv2types.SubnetMapping{ + { + SubnetId: awssdk.String("subnet-A"), + }, + { + SubnetId: awssdk.String("subnet-B"), + }, + }, + SecurityGroups: []string{"sg-A", "sg-B"}, + IpamPools: &elbv2types.IpamPools{ + Ipv4IpamPoolId: awssdk.String("my-ipv4-ipam-pool"), + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -686,3 +728,308 @@ func Test_defaultLoadBalancerManager_updateSDKLoadBalancerWithSubnetMappings(t * }) } } + +func Test_defaultLoadBalancerManager_removeIPAMPools(t *testing.T) { + stack := coremodel.NewDefaultStack(coremodel.StackID{Namespace: "namespace", Name: "name"}) + tests := []struct { + name string + resLB *elbv2model.LoadBalancer + sdkLB LoadBalancerWithTags + wantErr error + wantModify int + }{ + { + name: "remove ipam pools when ipam already not set", + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A")}, {SubnetId: awssdk.String("subnet-B")}}, + IpAddressType: elbv2types.IpAddressTypeIpv4, + }, + }, + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + IPv6Address: aws.String("2600:1f18::1"), + }, + { + SubnetID: "subnet-B", + IPv6Address: aws.String("2600:1f18::2"), + }, + }, + Type: elbv2model.LoadBalancerTypeNetwork, + IPAddressType: elbv2model.IPAddressTypeDualStack, + }, + }, + }, + { + name: "remove ipam pools when ipam set", + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A")}, {SubnetId: awssdk.String("subnet-B")}}, + IpAddressType: elbv2types.IpAddressTypeIpv4, + IpamPools: &elbv2types.IpamPools{Ipv4IpamPoolId: awssdk.String("foo")}, + }, + }, + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + IPv6Address: aws.String("2600:1f18::1"), + }, + { + SubnetID: "subnet-B", + IPv6Address: aws.String("2600:1f18::2"), + }, + }, + Type: elbv2model.LoadBalancerTypeNetwork, + IPAddressType: elbv2model.IPAddressTypeDualStack, + }, + }, + wantModify: 1, + }, + { + name: "add ipam pools when ipam set", + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A")}, {SubnetId: awssdk.String("subnet-B")}}, + IpAddressType: elbv2types.IpAddressTypeIpv4, + IpamPools: &elbv2types.IpamPools{Ipv4IpamPoolId: awssdk.String("foo")}, + }, + }, + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + IPv6Address: aws.String("2600:1f18::1"), + }, + { + SubnetID: "subnet-B", + IPv6Address: aws.String("2600:1f18::2"), + }, + }, + Type: elbv2model.LoadBalancerTypeNetwork, + IPAddressType: elbv2model.IPAddressTypeDualStack, + IPv4IPAMPool: awssdk.String("bar"), + }, + }, + }, + { + name: "add ipam pools when ipam not set", + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A")}, {SubnetId: awssdk.String("subnet-B")}}, + IpAddressType: elbv2types.IpAddressTypeIpv4, + }, + }, + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + IPv6Address: aws.String("2600:1f18::1"), + }, + { + SubnetID: "subnet-B", + IPv6Address: aws.String("2600:1f18::2"), + }, + }, + Type: elbv2model.LoadBalancerTypeNetwork, + IPAddressType: elbv2model.IPAddressTypeDualStack, + IPv4IPAMPool: awssdk.String("bar"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + elbv2Client := services.NewMockELBV2(ctrl) + m := &defaultLoadBalancerManager{ + logger: logr.New(&log.NullLogSink{}), + elbv2Client: elbv2Client, + } + + elbv2Client.EXPECT().ModifyIPPoolsWithContext(gomock.Any(), &elbv2sdk.ModifyIpPoolsInput{LoadBalancerArn: awssdk.String("LoadBalancerArn"), RemoveIpamPools: []elbv2types.RemoveIpamPoolEnum{elbv2types.RemoveIpamPoolEnumIpv4}}).Return(&elbv2sdk.ModifyIpPoolsOutput{}, tt.wantErr).Times(tt.wantModify) + + err := m.removeIPAMPools(context.Background(), tt.resLB, tt.sdkLB) + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + } + + }) + } +} + +func Test_defaultLoadBalancerManager_addIPAMPools(t *testing.T) { + stack := coremodel.NewDefaultStack(coremodel.StackID{Namespace: "namespace", Name: "name"}) + tests := []struct { + name string + resLB *elbv2model.LoadBalancer + sdkLB LoadBalancerWithTags + wantErr error + wantModify int + }{ + { + name: "add ipam pools when ipam not set", + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A")}, {SubnetId: awssdk.String("subnet-B")}}, + IpAddressType: elbv2types.IpAddressTypeIpv4, + }, + }, + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + IPv6Address: aws.String("2600:1f18::1"), + }, + { + SubnetID: "subnet-B", + IPv6Address: aws.String("2600:1f18::2"), + }, + }, + Type: elbv2model.LoadBalancerTypeNetwork, + IPAddressType: elbv2model.IPAddressTypeDualStack, + IPv4IPAMPool: awssdk.String("bar"), + }, + }, + wantModify: 1, + }, + { + name: "change ipam pools when already ipam set", + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A")}, {SubnetId: awssdk.String("subnet-B")}}, + IpAddressType: elbv2types.IpAddressTypeIpv4, + IpamPools: &elbv2types.IpamPools{Ipv4IpamPoolId: awssdk.String("foo")}, + }, + }, + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + IPv6Address: aws.String("2600:1f18::1"), + }, + { + SubnetID: "subnet-B", + IPv6Address: aws.String("2600:1f18::2"), + }, + }, + Type: elbv2model.LoadBalancerTypeNetwork, + IPAddressType: elbv2model.IPAddressTypeDualStack, + IPv4IPAMPool: awssdk.String("bar"), + }, + }, + wantModify: 1, + }, + { + name: "ipam pool equal between sdk and res", + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A")}, {SubnetId: awssdk.String("subnet-B")}}, + IpAddressType: elbv2types.IpAddressTypeIpv4, + IpamPools: &elbv2types.IpamPools{Ipv4IpamPoolId: awssdk.String("bar")}, + }, + }, + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + IPv6Address: aws.String("2600:1f18::1"), + }, + { + SubnetID: "subnet-B", + IPv6Address: aws.String("2600:1f18::2"), + }, + }, + Type: elbv2model.LoadBalancerTypeNetwork, + IPAddressType: elbv2model.IPAddressTypeDualStack, + IPv4IPAMPool: awssdk.String("bar"), + }, + }, + }, + { + name: "ipam pool not set in res", + sdkLB: LoadBalancerWithTags{ + LoadBalancer: &elbv2types.LoadBalancer{ + LoadBalancerArn: awssdk.String("LoadBalancerArn"), + Type: elbv2types.LoadBalancerTypeEnumApplication, + AvailabilityZones: []elbv2types.AvailabilityZone{{SubnetId: awssdk.String("subnet-A")}, {SubnetId: awssdk.String("subnet-B")}}, + IpAddressType: elbv2types.IpAddressTypeIpv4, + IpamPools: &elbv2types.IpamPools{Ipv4IpamPoolId: awssdk.String("foo")}, + }, + }, + resLB: &elbv2model.LoadBalancer{ + ResourceMeta: coremodel.NewResourceMeta(stack, "AWS::ElasticLoadBalancingV2::LoadBalancer", "id-1"), + Spec: elbv2model.LoadBalancerSpec{ + SubnetMappings: []elbv2model.SubnetMapping{ + { + SubnetID: "subnet-A", + IPv6Address: aws.String("2600:1f18::1"), + }, + { + SubnetID: "subnet-B", + IPv6Address: aws.String("2600:1f18::2"), + }, + }, + Type: elbv2model.LoadBalancerTypeNetwork, + IPAddressType: elbv2model.IPAddressTypeDualStack, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + elbv2Client := services.NewMockELBV2(ctrl) + m := &defaultLoadBalancerManager{ + logger: logr.New(&log.NullLogSink{}), + elbv2Client: elbv2Client, + } + + elbv2Client.EXPECT().ModifyIPPoolsWithContext(gomock.Any(), &elbv2sdk.ModifyIpPoolsInput{LoadBalancerArn: awssdk.String("LoadBalancerArn"), IpamPools: &elbv2types.IpamPools{Ipv4IpamPoolId: awssdk.String("bar")}}).Return(&elbv2sdk.ModifyIpPoolsOutput{}, tt.wantErr).Times(tt.wantModify) + + err := m.addIPAMPools(context.Background(), tt.resLB, tt.sdkLB) + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.NoError(t, err) + } + + }) + } +} diff --git a/pkg/ingress/model_build_ipam.go b/pkg/ingress/model_build_ipam.go new file mode 100644 index 000000000..86b238f88 --- /dev/null +++ b/pkg/ingress/model_build_ipam.go @@ -0,0 +1,50 @@ +package ingress + +import ( + "github.com/pkg/errors" + "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations" +) + +func (t *defaultModelBuildTask) buildIPv4IPAMPoolID() (*string, error) { + // We give precedence to the IngressClass value for IPv4 IPAM pool ID. + if len(t.ingGroup.Members) > 0 { + poolId := t.getIPv4IPAMFromIngressClass(t.ingGroup.Members[0].IngClassConfig) + if poolId != nil { + return poolId, nil + } + } + + var poolIdToReturn *string + + for _, ing := range t.ingGroup.Members { + poolId := t.getIPv4IPAMFromAnnotation(ing) + + if poolId != nil { + if poolIdToReturn != nil && *poolId != *poolIdToReturn { + return nil, errors.Errorf("conflicting ipv4 ipam pools %v: %v", *poolIdToReturn, *poolId) + } + poolIdToReturn = poolId + } + } + + return poolIdToReturn, nil +} + +// getIPv4IPAMFromAnnotation gets the ipv4 ipam value from the ingress annotation +func (t *defaultModelBuildTask) getIPv4IPAMFromAnnotation(ing ClassifiedIngress) *string { + var ipamPool string + if exist := t.annotationParser.ParseStringAnnotation(annotations.IngressSuffixIPAMIPv4PoolId, &ipamPool, ing.Ing.Annotations); exist { + if len(ipamPool) > 0 { + return &ipamPool + } + } + return nil +} + +// buildIngressClassIPv4IPAM builds the ipv4 ipam pool id for an IngressClass. +func (t *defaultModelBuildTask) getIPv4IPAMFromIngressClass(ingClassConfig ClassConfiguration) *string { + if ingClassConfig.IngClassParams == nil || ingClassConfig.IngClassParams.Spec.IPAMConfiguration == nil || ingClassConfig.IngClassParams.Spec.IPAMConfiguration.IPv4IPAMPoolId == nil { + return nil + } + return ingClassConfig.IngClassParams.Spec.IPAMConfiguration.IPv4IPAMPoolId +} diff --git a/pkg/ingress/model_build_ipam_test.go b/pkg/ingress/model_build_ipam_test.go new file mode 100644 index 000000000..2d87fadd9 --- /dev/null +++ b/pkg/ingress/model_build_ipam_test.go @@ -0,0 +1,387 @@ +package ingress + +import ( + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1" + "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations" + "strings" + "testing" +) + +func Test_buildIPv4IPAMPoolID(t *testing.T) { + testCases := []struct { + name string + ingGroup Group + expectedPoolId *string + errSubString string + }{ + { + name: "no ingresses configured", + }, + { + name: "one ingress configured", + ingGroup: Group{ + Members: []ClassifiedIngress{ + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + }, + }, + expectedPoolId: awssdk.String("foo"), + }, + { + name: "multiple ingress configured", + ingGroup: Group{ + Members: []ClassifiedIngress{ + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing2", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns3", + Name: "awesome-ing", + Annotations: map[string]string{}, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing4", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + }, + }, + expectedPoolId: awssdk.String("foo"), + }, + { + name: "multiple ingress configured with different pool ids", + ingGroup: Group{ + Members: []ClassifiedIngress{ + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing2", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "bar", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns3", + Name: "awesome-ing", + Annotations: map[string]string{}, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing4", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "baz", + }, + }, + }, + }, + }, + }, + errSubString: "conflicting ipv4 ipam pools", + }, + { + name: "no ing class params", + ingGroup: Group{ + Members: []ClassifiedIngress{ + { + IngClassConfig: ClassConfiguration{ + IngClass: nil, + IngClassParams: nil, + }, + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing2", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "bar", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns3", + Name: "awesome-ing", + Annotations: map[string]string{}, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing4", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "baz", + }, + }, + }, + }, + }, + }, + errSubString: "conflicting ipv4 ipam pools", + }, + { + name: "no ipam configuration", + ingGroup: Group{ + Members: []ClassifiedIngress{ + { + IngClassConfig: ClassConfiguration{ + IngClassParams: &elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{}, + }, + }, + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing2", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "bar", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns3", + Name: "awesome-ing", + Annotations: map[string]string{}, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing4", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "baz", + }, + }, + }, + }, + }, + }, + errSubString: "conflicting ipv4 ipam pools", + }, + { + name: "no pool configuration", + ingGroup: Group{ + Members: []ClassifiedIngress{ + { + IngClassConfig: ClassConfiguration{ + IngClassParams: &elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + IPAMConfiguration: &elbv2api.IPAMConfiguration{}, + }, + }, + }, + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing2", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "bar", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns3", + Name: "awesome-ing", + Annotations: map[string]string{}, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing4", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "baz", + }, + }, + }, + }, + }, + }, + errSubString: "conflicting ipv4 ipam pools", + }, + { + name: "ingress class parameter preferred", + ingGroup: Group{ + Members: []ClassifiedIngress{ + { + IngClassConfig: ClassConfiguration{ + IngClassParams: &elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + IPAMConfiguration: &elbv2api.IPAMConfiguration{ + IPv4IPAMPoolId: awssdk.String("ing-class-value"), + }, + }, + }, + }, + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "foo", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing2", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "bar", + }, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns3", + Name: "awesome-ing", + Annotations: map[string]string{}, + }, + }, + }, + { + Ing: &v1.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Name: "awesome-ing4", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/ipam-ipv4-pool-id": "baz", + }, + }, + }, + }, + }, + }, + expectedPoolId: awssdk.String("ing-class-value"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + annotationParser := annotations.NewSuffixAnnotationParser("alb.ingress.kubernetes.io") + buildTask := &defaultModelBuildTask{ + ingGroup: tc.ingGroup, + annotationParser: annotationParser, + } + + resolvedPoolId, err := buildTask.buildIPv4IPAMPoolID() + if len(tc.errSubString) > 0 { + assert.True(t, strings.Contains(err.Error(), tc.errSubString)) + } else { + assert.Nil(t, err) + assert.Equal(t, tc.expectedPoolId, resolvedPoolId) + } + }) + } +} diff --git a/pkg/ingress/model_build_load_balancer.go b/pkg/ingress/model_build_load_balancer.go index 111905cbf..1db3b8a2f 100644 --- a/pkg/ingress/model_build_load_balancer.go +++ b/pkg/ingress/model_build_load_balancer.go @@ -75,6 +75,11 @@ func (t *defaultModelBuildTask) buildLoadBalancerSpec(ctx context.Context, liste if err != nil { return elbv2model.LoadBalancerSpec{}, err } + ipv4IPAM, err := t.buildIPv4IPAMPoolID() + if err != nil { + return elbv2model.LoadBalancerSpec{}, err + } + return elbv2model.LoadBalancerSpec{ Name: name, Type: elbv2model.LoadBalancerTypeApplication, @@ -86,6 +91,7 @@ func (t *defaultModelBuildTask) buildLoadBalancerSpec(ctx context.Context, liste LoadBalancerAttributes: loadBalancerAttributes, MinimumLoadBalancerCapacity: lbMinimumCapacity, Tags: tags, + IPv4IPAMPool: ipv4IPAM, }, nil } diff --git a/pkg/model/elbv2/load_balancer.go b/pkg/model/elbv2/load_balancer.go index b52143096..62fb45417 100644 --- a/pkg/model/elbv2/load_balancer.go +++ b/pkg/model/elbv2/load_balancer.go @@ -201,6 +201,9 @@ type LoadBalancerSpec struct { // The tags. // +optional Tags map[string]string `json:"tags,omitempty"` + + // The IPv4 IPAM pool ID + IPv4IPAMPool *string `json:"ipv4IPAMPool,omitempty"` } // LoadBalancerStatus defines the observed state of LoadBalancer