Skip to content

Commit 2b80324

Browse files
authored
Merge pull request #4162 from shuqz/main
[feat:gw-api] add support for capacity reservation
2 parents 4121037 + aa17274 commit 2b80324

File tree

6 files changed

+313
-5
lines changed

6 files changed

+313
-5
lines changed

apis/gateway/v1beta1/loadbalancerconfig_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ type LoadBalancerConfigurationSpec struct {
243243
// when you specify securityGroups
244244
// +optional
245245
ManageBackendSecurityGroupRules bool `json:"manageBackendSecurityGroupRules,omitempty"`
246+
247+
// MinimumLoadBalancerCapacity define the capacity reservation for LoadBalancers
248+
// +optional
249+
MinimumLoadBalancerCapacity *MinimumLoadBalancerCapacity `json:"minimumLoadBalancerCapacity,omitempty"`
246250
}
247251

248252
// TODO -- these can be used to set what generation the gateway is currently on to track progress on reconcile.
@@ -279,6 +283,12 @@ type LoadBalancerConfigurationList struct {
279283
Items []LoadBalancerConfiguration `json:"items"`
280284
}
281285

286+
// MinimumLoadBalancerCapacity Information about a load balancer capacity reservation.
287+
type MinimumLoadBalancerCapacity struct {
288+
// The Capacity Units Value.
289+
CapacityUnits int32 `json:"capacityUnits"`
290+
}
291+
282292
func init() {
283293
SchemeBuilder.Register(&LoadBalancerConfiguration{}, &LoadBalancerConfigurationList{})
284294
}

apis/gateway/v1beta1/zz_generated.deepcopy.go

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

config/crd/gateway/gateway.k8s.aws_loadbalancerconfigurations.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,17 @@ spec:
231231
specifies whether you want the controller to configure security group rules on Node/Pod for traffic access
232232
when you specify securityGroups
233233
type: boolean
234+
minimumLoadBalancerCapacity:
235+
description: MinimumLoadBalancerCapacity define the capacity reservation
236+
for LoadBalancers
237+
properties:
238+
capacityUnits:
239+
description: The Capacity Units Value.
240+
format: int32
241+
type: integer
242+
required:
243+
- capacityUnits
244+
type: object
234245
scheme:
235246
description: scheme defines the type of LB to provision. If unspecified,
236247
it will be automatically inferred.

controllers/gateway/gateway_controller.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,12 @@ func (r *gatewayReconciler) reconcileHelper(ctx context.Context, req reconcile.R
188188
return err
189189
}
190190

191-
stack, lb, backendSGRequired, err := r.buildModel(ctx, gw, gwClass, allRoutes)
191+
lbConfig, err := r.getLoadBalancerConfigForGateway(ctx, r.k8sClient, gw, gwClass)
192+
if err != nil {
193+
return err
194+
}
195+
196+
stack, lb, backendSGRequired, err := r.buildModel(ctx, gw, lbConfig, allRoutes)
192197

193198
if err != nil {
194199
return err
@@ -205,6 +210,43 @@ func (r *gatewayReconciler) reconcileHelper(ctx context.Context, req reconcile.R
205210
return r.reconcileUpdate(ctx, gw, stack, lb, backendSGRequired)
206211
}
207212

213+
func (r *gatewayReconciler) getLoadBalancerConfigForGateway(ctx context.Context, k8sClient client.Client, gw *gwv1.Gateway, gwClass *gwv1.GatewayClass) (*elbv2gw.LoadBalancerConfiguration, error) {
214+
var gwParametersRef *gwv1.ParametersReference
215+
if gw.Spec.Infrastructure != nil && gw.Spec.Infrastructure.ParametersRef != nil {
216+
// Convert local param ref -> namespaced param ref
217+
ns := gwv1.Namespace(gw.Namespace)
218+
gwParametersRef = &gwv1.ParametersReference{
219+
Group: gw.Spec.Infrastructure.ParametersRef.Group,
220+
Kind: gw.Spec.Infrastructure.ParametersRef.Kind,
221+
Name: gw.Spec.Infrastructure.ParametersRef.Name,
222+
Namespace: &ns,
223+
}
224+
}
225+
226+
gatewayLBConfig, err := r.resolveLoadBalancerConfig(ctx, k8sClient, gwParametersRef)
227+
if err != nil {
228+
return &elbv2gw.LoadBalancerConfiguration{}, err
229+
}
230+
return gatewayLBConfig, nil
231+
}
232+
233+
func (r *gatewayReconciler) resolveLoadBalancerConfig(ctx context.Context, k8sClient client.Client, reference *gwv1.ParametersReference) (*elbv2gw.LoadBalancerConfiguration, error) {
234+
var lbConf *elbv2gw.LoadBalancerConfiguration
235+
var err error
236+
if reference != nil {
237+
lbConf = &elbv2gw.LoadBalancerConfiguration{}
238+
if reference.Namespace != nil {
239+
err = k8sClient.Get(ctx, types.NamespacedName{
240+
Namespace: string(*reference.Namespace),
241+
Name: reference.Name,
242+
}, lbConf)
243+
} else {
244+
err = errors.New("Namespace must be specified in ParametersRef")
245+
}
246+
}
247+
return lbConf, err
248+
}
249+
208250
func (r *gatewayReconciler) reconcileDelete(ctx context.Context, gw *gwv1.Gateway, routes map[int32][]routeutils.RouteDescriptor) error {
209251
for _, routeList := range routes {
210252
if len(routeList) != 0 {
@@ -259,8 +301,8 @@ func (r *gatewayReconciler) deployModel(ctx context.Context, gw *gwv1.Gateway, s
259301
return nil
260302
}
261303

262-
func (r *gatewayReconciler) buildModel(ctx context.Context, gw *gwv1.Gateway, gwClass *gwv1.GatewayClass, listenerToRoute map[int32][]routeutils.RouteDescriptor) (core.Stack, *elbv2model.LoadBalancer, bool, error) {
263-
stack, lb, backendSGRequired, err := r.modelBuilder.Build(ctx, gw, &elbv2gw.LoadBalancerConfiguration{}, listenerToRoute)
304+
func (r *gatewayReconciler) buildModel(ctx context.Context, gw *gwv1.Gateway, lbConf *elbv2gw.LoadBalancerConfiguration, listenerToRoute map[int32][]routeutils.RouteDescriptor) (core.Stack, *elbv2model.LoadBalancer, bool, error) {
305+
stack, lb, backendSGRequired, err := r.modelBuilder.Build(ctx, gw, lbConf, listenerToRoute)
264306
if err != nil {
265307
r.eventRecorder.Event(gw, corev1.EventTypeWarning, k8s.ServiceEventReasonFailedBuildModel, fmt.Sprintf("Failed build model due to %v", err))
266308
return nil, nil, false, err

pkg/gateway/model/model_build_loadbalancer.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,11 @@ func (lbModelBuilder *loadBalancerBuilderImpl) buildLoadBalancerAttributes(lbCon
117117
return attributes
118118
}
119119

120-
// TODO -- Fill this in at a later time.
121120
func (lbModelBuilder *loadBalancerBuilderImpl) buildLoadBalancerMinimumCapacity(lbConf *elbv2gw.LoadBalancerConfiguration) *elbv2model.MinimumLoadBalancerCapacity {
122-
return nil
121+
if lbConf == nil || lbConf.Spec.MinimumLoadBalancerCapacity == nil {
122+
return nil
123+
}
124+
return &elbv2model.MinimumLoadBalancerCapacity{
125+
CapacityUnits: lbConf.Spec.MinimumLoadBalancerCapacity.CapacityUnits,
126+
}
123127
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package model
2+
3+
import (
4+
"github.com/aws/aws-sdk-go-v2/aws"
5+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
"k8s.io/apimachinery/pkg/types"
7+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
8+
"strings"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
elbv2gw "sigs.k8s.io/aws-load-balancer-controller/apis/gateway/v1beta1"
13+
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
14+
)
15+
16+
func TestLoadBalancerBuilderImpl_BuildLoadBalancerName(t *testing.T) {
17+
tests := []struct {
18+
name string
19+
lbConf *elbv2gw.LoadBalancerConfiguration
20+
gw *gwv1.Gateway
21+
scheme elbv2model.LoadBalancerScheme
22+
clusterName string
23+
want string
24+
wantErr bool
25+
}{
26+
{
27+
name: "specify load balancer name in load balancer configuration",
28+
lbConf: &elbv2gw.LoadBalancerConfiguration{
29+
Spec: elbv2gw.LoadBalancerConfigurationSpec{
30+
LoadBalancerName: aws.String("my-example-lb"),
31+
},
32+
},
33+
gw: &gwv1.Gateway{},
34+
scheme: elbv2model.LoadBalancerSchemeInternal,
35+
want: "my-example-lb",
36+
wantErr: false,
37+
},
38+
{
39+
name: "generated name with valid characters",
40+
lbConf: &elbv2gw.LoadBalancerConfiguration{
41+
Spec: elbv2gw.LoadBalancerConfigurationSpec{},
42+
},
43+
gw: &gwv1.Gateway{
44+
ObjectMeta: metav1.ObjectMeta{
45+
Namespace: "test-namespace",
46+
Name: "test-gateway",
47+
UID: types.UID("123e4567-e89b-12d3-a456-426614174000"),
48+
},
49+
},
50+
scheme: elbv2model.LoadBalancerSchemeInternal,
51+
clusterName: "test-cluster",
52+
want: "k8s-test-name-test-gate-0123456789",
53+
wantErr: false,
54+
},
55+
{
56+
name: "generated name with invalid characters",
57+
lbConf: &elbv2gw.LoadBalancerConfiguration{
58+
Spec: elbv2gw.LoadBalancerConfigurationSpec{},
59+
},
60+
gw: &gwv1.Gateway{
61+
ObjectMeta: metav1.ObjectMeta{
62+
Namespace: "test@namespace",
63+
Name: "test#gateway",
64+
UID: types.UID("123e4567-e89b-12d3-a456-426614174000"),
65+
},
66+
},
67+
scheme: elbv2model.LoadBalancerSchemeInternal,
68+
clusterName: "test-cluster",
69+
want: "k8s-testnames-testgatew-0123456789",
70+
wantErr: false,
71+
},
72+
{
73+
name: "provide long namespace and name",
74+
lbConf: &elbv2gw.LoadBalancerConfiguration{
75+
Spec: elbv2gw.LoadBalancerConfigurationSpec{},
76+
},
77+
gw: &gwv1.Gateway{
78+
ObjectMeta: metav1.ObjectMeta{
79+
Namespace: "very-long-namespace-that-exceeds-limit",
80+
Name: "very-long-gateway-name-that-exceeds-limit",
81+
UID: types.UID("123e4567-e89b-12d3-a456-426614174000"),
82+
},
83+
},
84+
scheme: elbv2model.LoadBalancerSchemeInternal,
85+
clusterName: "test-cluster",
86+
want: "k8s-very-long-very-long-0123456789",
87+
wantErr: false,
88+
},
89+
}
90+
91+
for _, tt := range tests {
92+
t.Run(tt.name, func(t *testing.T) {
93+
lbModelBuilder := &loadBalancerBuilderImpl{
94+
clusterName: tt.clusterName,
95+
}
96+
97+
got, err := lbModelBuilder.buildLoadBalancerName(tt.lbConf, tt.gw, tt.scheme)
98+
99+
if tt.wantErr {
100+
assert.Error(t, err)
101+
return
102+
}
103+
104+
assert.NoError(t, err)
105+
106+
if tt.lbConf.Spec.LoadBalancerName != nil {
107+
assert.Equal(t, tt.want, got)
108+
} else {
109+
assert.Regexp(t, "^k8s-[a-zA-Z0-9-]+-[a-zA-Z0-9-]+-[a-zA-Z0-9]+$", got)
110+
assert.LessOrEqual(t, len(got), 32) // AWS LB name length limit
111+
parts := strings.Split(got, "-")
112+
assert.Equal(t, 4, len(parts))
113+
assert.LessOrEqual(t, len(parts[1]), 8) // namespace part
114+
assert.LessOrEqual(t, len(parts[2]), 8) // name part
115+
assert.Equal(t, 10, len(parts[3])) // uuid part
116+
}
117+
})
118+
}
119+
}
120+
121+
func TestLoadBalancerBuilderImpl_BuildLoadBalancerAttributes(t *testing.T) {
122+
tests := []struct {
123+
name string
124+
lbConf *elbv2gw.LoadBalancerConfiguration
125+
want []elbv2model.LoadBalancerAttribute
126+
}{
127+
{
128+
name: "provide single attribute",
129+
lbConf: &elbv2gw.LoadBalancerConfiguration{
130+
Spec: elbv2gw.LoadBalancerConfigurationSpec{
131+
LoadBalancerAttributes: []elbv2gw.LoadBalancerAttribute{
132+
{
133+
Key: "deletion_protection.enabled",
134+
Value: "true",
135+
},
136+
},
137+
},
138+
},
139+
want: []elbv2model.LoadBalancerAttribute{
140+
{
141+
Key: "deletion_protection.enabled",
142+
Value: "true",
143+
},
144+
},
145+
},
146+
{
147+
name: "provide multiple attributes",
148+
lbConf: &elbv2gw.LoadBalancerConfiguration{
149+
Spec: elbv2gw.LoadBalancerConfigurationSpec{
150+
LoadBalancerAttributes: []elbv2gw.LoadBalancerAttribute{
151+
{
152+
Key: "deletion_protection.enabled",
153+
Value: "true",
154+
},
155+
{
156+
Key: "idle_timeout.timeout_seconds",
157+
Value: "60",
158+
},
159+
},
160+
},
161+
},
162+
want: []elbv2model.LoadBalancerAttribute{
163+
{
164+
Key: "deletion_protection.enabled",
165+
Value: "true",
166+
},
167+
{
168+
Key: "idle_timeout.timeout_seconds",
169+
Value: "60",
170+
},
171+
},
172+
},
173+
}
174+
175+
for _, tt := range tests {
176+
t.Run(tt.name, func(t *testing.T) {
177+
lbModelBuilder := &loadBalancerBuilderImpl{}
178+
got := lbModelBuilder.buildLoadBalancerAttributes(tt.lbConf)
179+
assert.Equal(t, tt.want, got)
180+
})
181+
}
182+
}
183+
184+
func TestLoadBalancerBuilderImpl_BuildLoadBalancerMinimumCapacity(t *testing.T) {
185+
tests := []struct {
186+
name string
187+
lbConf *elbv2gw.LoadBalancerConfiguration
188+
want *elbv2model.MinimumLoadBalancerCapacity
189+
}{
190+
{
191+
name: "MinimumLoadBalancerCapacity is nil",
192+
lbConf: &elbv2gw.LoadBalancerConfiguration{
193+
Spec: elbv2gw.LoadBalancerConfigurationSpec{
194+
MinimumLoadBalancerCapacity: nil,
195+
},
196+
},
197+
want: nil,
198+
},
199+
{
200+
name: "MinimumLoadBalancerCapacity with a valid value",
201+
lbConf: &elbv2gw.LoadBalancerConfiguration{
202+
Spec: elbv2gw.LoadBalancerConfigurationSpec{
203+
MinimumLoadBalancerCapacity: &elbv2gw.MinimumLoadBalancerCapacity{
204+
CapacityUnits: 100,
205+
},
206+
},
207+
},
208+
want: &elbv2model.MinimumLoadBalancerCapacity{
209+
CapacityUnits: 100,
210+
},
211+
},
212+
}
213+
214+
for _, tt := range tests {
215+
t.Run(tt.name, func(t *testing.T) {
216+
lbModelBuilder := &loadBalancerBuilderImpl{}
217+
got := lbModelBuilder.buildLoadBalancerMinimumCapacity(tt.lbConf)
218+
assert.Equal(t, tt.want, got)
219+
})
220+
}
221+
}

0 commit comments

Comments
 (0)