Skip to content

Commit 93e23fe

Browse files
committed
add COIP support for ALB on outpost
1 parent 9e7d172 commit 93e23fe

File tree

7 files changed

+291
-0
lines changed

7 files changed

+291
-0
lines changed

docs/guide/ingress/annotations.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ You can add annotations to kubernetes Ingress and Service objects to customize t
2323
|[alb.ingress.kubernetes.io/scheme](#scheme)|internal \| internet-facing|internal|Ingress|Exclusive|
2424
|[alb.ingress.kubernetes.io/subnets](#subnets)|stringList|N/A|Ingress|Exclusive|
2525
|[alb.ingress.kubernetes.io/security-groups](#security-groups)|stringList|N/A|Ingress|Exclusive|
26+
|[alb.ingress.kubernetes.io/customer-owned-ipv4-pool](#customer-owned-ipv4-pool)|string|N/A|Ingress|Exclusive|
2627
|[alb.ingress.kubernetes.io/load-balancer-attributes](#load-balancer-attributes)|stringMap|N/A|Ingress|Merge|
2728
|[alb.ingress.kubernetes.io/wafv2-acl-arn](#wafv2-acl-arn)|string|N/A|Ingress|Exclusive|
2829
|[alb.ingress.kubernetes.io/waf-acl-id](#waf-acl-id)|string|N/A|Ingress|Exclusive|
@@ -124,6 +125,16 @@ Traffic Listening can be controlled with following annotations:
124125
alb.ingress.kubernetes.io/ip-address-type: ipv4
125126
```
126127

128+
- <a name="customer-owned-ipv4-pool">`alb.ingress.kubernetes.io/customer-owned-ipv4-pool`</a> specifies the customer-owned IPv4 address pool for ALB on Outpost.
129+
130+
!!!warning ""
131+
This annotation should be treated as immutable. To remove or change coIPv4Pool, you need to recreate Ingress.
132+
133+
!!!example
134+
```
135+
alb.ingress.kubernetes.io/customer-owned-ipv4-poole: ipv4pool-coip-xxxxxxxx
136+
```
137+
127138
## Traffic Routing
128139
Traffic Routing can be controlled with following annotations:
129140

pkg/annotations/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const (
1111
IngressSuffixIPAddressType = "ip-address-type"
1212
IngressSuffixScheme = "scheme"
1313
IngressSuffixSubnets = "subnets"
14+
IngressSuffixCustomerOwnedIPv4Pool = "customer-owned-ipv4-pool"
1415
IngressSuffixLoadBalancerAttributes = "load-balancer-attributes"
1516
IngressSuffixWAFv2ACLARN = "wafv2-acl-arn"
1617
IngressSuffixWAFACLID = "waf-acl-id"

pkg/deploy/elbv2/load_balancer_manager.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ func buildSDKCreateLoadBalancerInput(lbSpec elbv2model.LoadBalancerSpec) (*elbv2
236236
} else {
237237
sdkObj.SecurityGroups = sdkSecurityGroups
238238
}
239+
240+
sdkObj.CustomerOwnedIpv4Pool = lbSpec.CustomerOwnedIPv4Pool
239241
return sdkObj, nil
240242
}
241243

pkg/deploy/elbv2/load_balancer_manager_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,46 @@ func Test_buildSDKCreateLoadBalancerInput(t *testing.T) {
9292
},
9393
},
9494
},
95+
{
96+
name: "application loadBalancer - with CoIP pool",
97+
args: args{
98+
lbSpec: elbv2model.LoadBalancerSpec{
99+
Name: "my-alb",
100+
Type: elbv2model.LoadBalancerTypeApplication,
101+
Scheme: &schemeInternetFacing,
102+
IPAddressType: &addressTypeDualStack,
103+
SubnetMappings: []elbv2model.SubnetMapping{
104+
{
105+
SubnetID: "subnet-A",
106+
},
107+
{
108+
SubnetID: "subnet-B",
109+
},
110+
},
111+
SecurityGroups: []coremodel.StringToken{
112+
coremodel.LiteralStringToken("sg-A"),
113+
coremodel.LiteralStringToken("sg-B"),
114+
},
115+
CustomerOwnedIPv4Pool: awssdk.String("coIP-pool-x"),
116+
},
117+
},
118+
want: &elbv2sdk.CreateLoadBalancerInput{
119+
Name: awssdk.String("my-alb"),
120+
Type: awssdk.String("application"),
121+
IpAddressType: awssdk.String("dualstack"),
122+
Scheme: awssdk.String("internet-facing"),
123+
SubnetMappings: []*elbv2sdk.SubnetMapping{
124+
{
125+
SubnetId: awssdk.String("subnet-A"),
126+
},
127+
{
128+
SubnetId: awssdk.String("subnet-B"),
129+
},
130+
},
131+
SecurityGroups: awssdk.StringSlice([]string{"sg-A", "sg-B"}),
132+
CustomerOwnedIpv4Pool: awssdk.String("coIP-pool-x"),
133+
},
134+
},
95135
}
96136
for _, tt := range tests {
97137
t.Run(tt.name, func(t *testing.T) {

pkg/ingress/model_build_load_balancer.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"regexp"
1414
"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
1515
"sigs.k8s.io/aws-load-balancer-controller/pkg/equality"
16+
"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
1617
"sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
1718
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
1819
"sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
@@ -50,6 +51,10 @@ func (t *defaultModelBuildTask) buildLoadBalancerSpec(ctx context.Context, liste
5051
if err != nil {
5152
return elbv2model.LoadBalancerSpec{}, err
5253
}
54+
coIPv4Pool, err := t.buildLoadBalancerCOIPv4Pool(ctx)
55+
if err != nil {
56+
return elbv2model.LoadBalancerSpec{}, err
57+
}
5358
loadBalancerAttributes, err := t.buildLoadBalancerAttributes(ctx)
5459
if err != nil {
5560
return elbv2model.LoadBalancerSpec{}, err
@@ -66,6 +71,7 @@ func (t *defaultModelBuildTask) buildLoadBalancerSpec(ctx context.Context, liste
6671
IPAddressType: &ipAddressType,
6772
SubnetMappings: subnetMappings,
6873
SecurityGroups: securityGroups,
74+
CustomerOwnedIPv4Pool: coIPv4Pool,
6975
LoadBalancerAttributes: loadBalancerAttributes,
7076
Tags: tags,
7177
}, nil
@@ -216,6 +222,31 @@ func (t *defaultModelBuildTask) buildLoadBalancerSecurityGroups(ctx context.Cont
216222
return sgIDTokens, nil
217223
}
218224

225+
func (t *defaultModelBuildTask) buildLoadBalancerCOIPv4Pool(_ context.Context) (*string, error) {
226+
explicitCOIPv4Pools := sets.NewString()
227+
for _, ing := range t.ingGroup.Members {
228+
rawCOIPv4Pool := ""
229+
if exists := t.annotationParser.ParseStringAnnotation(annotations.IngressSuffixCustomerOwnedIPv4Pool, &rawCOIPv4Pool, ing.Annotations); !exists {
230+
continue
231+
}
232+
if len(rawCOIPv4Pool) == 0 {
233+
return nil, errors.Errorf("cannot use empty value for %s annotation, ingress: %v",
234+
annotations.IngressSuffixCustomerOwnedIPv4Pool, k8s.NamespacedName(ing))
235+
}
236+
explicitCOIPv4Pools.Insert(rawCOIPv4Pool)
237+
}
238+
239+
if len(explicitCOIPv4Pools) == 0 {
240+
return nil, nil
241+
}
242+
if len(explicitCOIPv4Pools) > 1 {
243+
return nil, errors.Errorf("conflicting CustomerOwnedIPv4Pool: %v", explicitCOIPv4Pools.List())
244+
}
245+
246+
rawCOIPv4Pool, _ := explicitCOIPv4Pools.PopAny()
247+
return &rawCOIPv4Pool, nil
248+
}
249+
219250
func (t *defaultModelBuildTask) buildLoadBalancerAttributes(_ context.Context) ([]elbv2model.LoadBalancerAttribute, error) {
220251
mergedAttributes := make(map[string]string)
221252
for _, ing := range t.ingGroup.Members {
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package ingress
2+
3+
import (
4+
"context"
5+
"errors"
6+
awssdk "github.com/aws/aws-sdk-go/aws"
7+
"github.com/stretchr/testify/assert"
8+
networking "k8s.io/api/networking/v1beta1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
11+
"testing"
12+
)
13+
14+
func Test_defaultModelBuildTask_buildLoadBalancerCOIPv4Pool(t *testing.T) {
15+
type fields struct {
16+
ingGroup Group
17+
}
18+
tests := []struct {
19+
name string
20+
fields fields
21+
want *string
22+
wantErr error
23+
}{
24+
{
25+
name: "COIPv4 not configured on standalone Ingress",
26+
fields: fields{
27+
ingGroup: Group{
28+
Members: []*networking.Ingress{
29+
{
30+
ObjectMeta: metav1.ObjectMeta{
31+
Namespace: "awesome-ns",
32+
Name: "ing-1",
33+
Annotations: map[string]string{},
34+
},
35+
},
36+
},
37+
},
38+
},
39+
want: nil,
40+
},
41+
{
42+
name: "COIPv4 configured on standalone Ingress",
43+
fields: fields{
44+
ingGroup: Group{
45+
Members: []*networking.Ingress{
46+
{
47+
ObjectMeta: metav1.ObjectMeta{
48+
Namespace: "awesome-ns",
49+
Name: "ing-1",
50+
Annotations: map[string]string{
51+
"alb.ingress.kubernetes.io/customer-owned-ipv4-pool": "my-ip-pool",
52+
},
53+
},
54+
},
55+
},
56+
},
57+
},
58+
want: awssdk.String("my-ip-pool"),
59+
},
60+
{
61+
name: "specified empty COIPv4 on standalone Ingress",
62+
fields: fields{
63+
ingGroup: Group{
64+
Members: []*networking.Ingress{
65+
{
66+
ObjectMeta: metav1.ObjectMeta{
67+
Namespace: "awesome-ns",
68+
Name: "ing-1",
69+
Annotations: map[string]string{
70+
"alb.ingress.kubernetes.io/customer-owned-ipv4-pool": "",
71+
},
72+
},
73+
},
74+
},
75+
},
76+
},
77+
wantErr: errors.New("cannot use empty value for customer-owned-ipv4-pool annotation, ingress: awesome-ns/ing-1"),
78+
},
79+
{
80+
name: "COIPv4 not configured on all Ingresses among IngressGroup",
81+
fields: fields{
82+
ingGroup: Group{
83+
Members: []*networking.Ingress{
84+
{
85+
ObjectMeta: metav1.ObjectMeta{
86+
Namespace: "awesome-ns",
87+
Name: "ing-1",
88+
Annotations: map[string]string{},
89+
},
90+
},
91+
{
92+
ObjectMeta: metav1.ObjectMeta{
93+
Namespace: "awesome-ns",
94+
Name: "ing-2",
95+
Annotations: map[string]string{},
96+
},
97+
},
98+
},
99+
},
100+
},
101+
want: nil,
102+
},
103+
{
104+
name: "COIPv4 configured on one Ingress among IngressGroup",
105+
fields: fields{
106+
ingGroup: Group{
107+
Members: []*networking.Ingress{
108+
{
109+
ObjectMeta: metav1.ObjectMeta{
110+
Namespace: "awesome-ns",
111+
Name: "ing-1",
112+
Annotations: map[string]string{
113+
"alb.ingress.kubernetes.io/customer-owned-ipv4-pool": "my-ip-pool",
114+
},
115+
},
116+
},
117+
{
118+
ObjectMeta: metav1.ObjectMeta{
119+
Namespace: "awesome-ns",
120+
Name: "ing-2",
121+
Annotations: map[string]string{},
122+
},
123+
},
124+
},
125+
},
126+
},
127+
want: awssdk.String("my-ip-pool"),
128+
},
129+
{
130+
name: "COIPv4 configured on multiple Ingresses among IngressGroup - with same value",
131+
fields: fields{
132+
ingGroup: Group{
133+
Members: []*networking.Ingress{
134+
{
135+
ObjectMeta: metav1.ObjectMeta{
136+
Namespace: "awesome-ns",
137+
Name: "ing-1",
138+
Annotations: map[string]string{
139+
"alb.ingress.kubernetes.io/customer-owned-ipv4-pool": "my-ip-pool",
140+
},
141+
},
142+
},
143+
{
144+
ObjectMeta: metav1.ObjectMeta{
145+
Namespace: "awesome-ns",
146+
Name: "ing-2",
147+
Annotations: map[string]string{
148+
"alb.ingress.kubernetes.io/customer-owned-ipv4-pool": "my-ip-pool",
149+
},
150+
},
151+
},
152+
},
153+
},
154+
},
155+
want: awssdk.String("my-ip-pool"),
156+
},
157+
{
158+
name: "COIPv4 configured on multiple Ingress among IngressGroup - with different value",
159+
fields: fields{
160+
ingGroup: Group{
161+
Members: []*networking.Ingress{
162+
{
163+
ObjectMeta: metav1.ObjectMeta{
164+
Namespace: "awesome-ns",
165+
Name: "ing-1",
166+
Annotations: map[string]string{
167+
"alb.ingress.kubernetes.io/customer-owned-ipv4-pool": "my-ip-pool",
168+
},
169+
},
170+
},
171+
{
172+
ObjectMeta: metav1.ObjectMeta{
173+
Namespace: "awesome-ns",
174+
Name: "ing-2",
175+
Annotations: map[string]string{
176+
"alb.ingress.kubernetes.io/customer-owned-ipv4-pool": "my-another-pool",
177+
},
178+
},
179+
},
180+
},
181+
},
182+
},
183+
wantErr: errors.New("conflicting CustomerOwnedIPv4Pool: [my-another-pool my-ip-pool]"),
184+
},
185+
}
186+
for _, tt := range tests {
187+
t.Run(tt.name, func(t1 *testing.T) {
188+
annotationParser := annotations.NewSuffixAnnotationParser("alb.ingress.kubernetes.io")
189+
task := &defaultModelBuildTask{
190+
annotationParser: annotationParser,
191+
ingGroup: tt.fields.ingGroup,
192+
}
193+
got, err := task.buildLoadBalancerCOIPv4Pool(context.Background())
194+
if tt.wantErr != nil {
195+
assert.EqualError(t, err, tt.wantErr.Error())
196+
} else {
197+
assert.NoError(t, err)
198+
assert.Equal(t, got, tt.want)
199+
}
200+
})
201+
}
202+
}

pkg/model/elbv2/load_balancer.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ type LoadBalancerSpec struct {
140140
// +optional
141141
SecurityGroups []core.StringToken `json:"securityGroups,omitempty"`
142142

143+
// [Application Load Balancers on Outposts] The ID of the customer-owned address pool (CoIP pool).
144+
// +optional
145+
CustomerOwnedIPv4Pool *string `json:"customerOwnedIPv4Pool,omitempty"`
146+
143147
// The load balancer attributes.
144148
// +optional
145149
LoadBalancerAttributes []LoadBalancerAttribute `json:"loadBalancerAttributes,omitempty"`

0 commit comments

Comments
 (0)