Skip to content

Commit a73972c

Browse files
kishorjTimothy-Dougherty
authored andcommitted
Add support for specifying NLB target group attributes (kubernetes-sigs#1632)
* Refactor NLB model builder * Support NLB target group attributes * add comments, unexport constants * address review comments
1 parent 9e46d34 commit a73972c

14 files changed

+1933
-1258
lines changed

controllers/service/service_controller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
1818
"sigs.k8s.io/aws-load-balancer-controller/pkg/networking"
1919
"sigs.k8s.io/aws-load-balancer-controller/pkg/runtime"
20-
"sigs.k8s.io/aws-load-balancer-controller/pkg/service/nlb"
20+
"sigs.k8s.io/aws-load-balancer-controller/pkg/service"
2121
ctrl "sigs.k8s.io/controller-runtime"
2222
"sigs.k8s.io/controller-runtime/pkg/client"
2323
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -37,7 +37,7 @@ func NewServiceReconciler(cloud aws.Cloud, k8sClient client.Client, eventRecorde
3737
config config.ControllerConfig, logger logr.Logger) *serviceReconciler {
3838

3939
annotationParser := annotations.NewSuffixAnnotationParser(serviceAnnotationPrefix)
40-
modelBuilder := nlb.NewDefaultModelBuilder(annotationParser, subnetsResolver, config.ClusterName)
40+
modelBuilder := service.NewDefaultModelBuilder(annotationParser, subnetsResolver, config.ClusterName)
4141
stackMarshaller := deploy.NewDefaultStackMarshaller()
4242
stackDeployer := deploy.NewDefaultStackDeployer(cloud, k8sClient, networkingSGManager, networkingSGReconciler, config, serviceTagPrefix, logger)
4343
return &serviceReconciler{
@@ -61,7 +61,7 @@ type serviceReconciler struct {
6161
finalizerManager k8s.FinalizerManager
6262
annotationParser annotations.Parser
6363

64-
modelBuilder nlb.ModelBuilder
64+
modelBuilder service.ModelBuilder
6565
stackMarshaller deploy.StackMarshaller
6666
stackDeployer deploy.StackDeployer
6767
logger logr.Logger

docs/guide/controller/pod_readiness_gate.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Annotations: <none>
3636
Status: Active
3737
```
3838
Once labelled, the controller will add the pod readiness gates config to all the pods created subsequently that meet all the following conditions
39+
3940
* There exists a service matching the pod labels in the same namespace
4041
* There exists at least one target group binding that refers to the matching service
4142
* The target type is IP

docs/guide/service/annotations.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
|--------------------------------------------------------------------------------|------------|---------------------------|------------------------|
1414
| service.beta.kubernetes.io/aws-load-balancer-type | string | | |
1515
| service.beta.kubernetes.io/aws-load-balancer-internal | boolean | false | |
16-
| service.beta.kubernetes.io/aws-load-balancer-proxy-protocol | string | | Set to `"*"` to enable |
16+
| [service.beta.kubernetes.io/aws-load-balancer-proxy-protocol](#proxy-protocol-v2) | string | | Set to `"*"` to enable |
1717
| service.beta.kubernetes.io/aws-load-balancer-access-log-enabled | boolean | false | |
1818
| service.beta.kubernetes.io/aws-load-balancer-access-log-s3-bucket-name | string | | |
1919
| service.beta.kubernetes.io/aws-load-balancer-access-log-s3-bucket-prefix | string | | |
@@ -30,4 +30,32 @@
3030
| service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol | string | TCP | |
3131
| service.beta.kubernetes.io/aws-load-balancer-healthcheck-port | string | traffic-port | |
3232
| service.beta.kubernetes.io/aws-load-balancer-healthcheck-path | string | "/" for HTTP(S) protocols | |
33-
| service.beta.kubernetes.io/aws-load-balancer-eip-allocations | stringList | | |
33+
| service.beta.kubernetes.io/aws-load-balancer-eip-allocations | stringList | | |
34+
| [service.beta.kubernetes.io/aws-load-balancer-target-group-attributes](#target-group-attributes) | stringMap | | |
35+
36+
## Resource attributes
37+
NLB target group attributes can be controlled via the following annotations:
38+
39+
- <a name="proxy-protocol-v2">service.beta.kubernetes.io/aws-load-balancer-proxy-protocol</a> specifies whether to enable proxy protocol v2 on the target group.
40+
Set to '*' to enable proxy protocol v2. This annotation takes precedence over the annotation `service.beta.kubernetes.io/aws-load-balancer-target-group-attributes`
41+
for proxy protocol v2 configuration.
42+
43+
!!!note ""
44+
The only valid value for this annotation is `*`.
45+
46+
- <a name="target-group-attributes">`service.beta.kubernetes.io/aws-load-balancer-target-group-attributes`</a> specifies the
47+
[Target Group Attributes](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#target-group-attributes) to be configured.
48+
49+
!!!example
50+
- set the deregistration delay to 120 seconds (available range is 0-3600 seconds)
51+
```
52+
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: deregistration_delay.timeout_seconds=120
53+
```
54+
- enable source IP affinity
55+
```
56+
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: stickiness.enabled=true,stickiness.type=source_ip
57+
```
58+
- enable proxy protocol version 2
59+
```
60+
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: proxy_protocol_v2.enabled=true
61+
```

pkg/annotations/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@ const (
6464
SvcLBSuffixHCPort = "aws-load-balancer-healthcheck-port"
6565
SvcLBSuffixHCPath = "aws-load-balancer-healthcheck-path"
6666
SvcLBSuffixEIPAllocations = "aws-load-balancer-eip-allocations"
67+
SvcLBSuffixTargetGroupAttributes = "aws-load-balancer-target-group-attributes"
6768
)

pkg/ingress/model_build_listener_rules.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package ingress
22

33
import (
44
"context"
5-
"github.com/pkg/errors"
65
"fmt"
6+
"github.com/pkg/errors"
77
networking "k8s.io/api/networking/v1beta1"
88
"sigs.k8s.io/aws-load-balancer-controller/pkg/k8s"
99
"sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"

pkg/service/nlb/mock_visitor.go renamed to pkg/service/mock_visitor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package nlb
1+
package service
22

33
import "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core"
44

pkg/service/model_build_listener.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/aws/aws-sdk-go/aws"
7+
corev1 "k8s.io/api/core/v1"
8+
"k8s.io/apimachinery/pkg/util/sets"
9+
"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
10+
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
11+
"strconv"
12+
)
13+
14+
func (t *defaultModelBuildTask) buildListeners(ctx context.Context) error {
15+
cfg := t.buildListenerConfig(ctx)
16+
for _, port := range t.service.Spec.Ports {
17+
_, err := t.buildListener(ctx, port, cfg)
18+
if err != nil {
19+
return err
20+
}
21+
}
22+
return nil
23+
}
24+
25+
func (t *defaultModelBuildTask) buildListener(ctx context.Context, port corev1.ServicePort, cfg listenerConfig) (*elbv2model.Listener, error) {
26+
lsSpec, err := t.buildListenerSpec(ctx, port, cfg)
27+
if err != nil {
28+
return nil, err
29+
}
30+
listenerResID := fmt.Sprintf("%v", port.Port)
31+
ls := elbv2model.NewListener(t.stack, listenerResID, lsSpec)
32+
return ls, nil
33+
}
34+
35+
func (t *defaultModelBuildTask) buildListenerSpec(ctx context.Context, port corev1.ServicePort, cfg listenerConfig) (elbv2model.ListenerSpec, error) {
36+
tgProtocol := elbv2model.Protocol(port.Protocol)
37+
listenerProtocol := elbv2model.Protocol(port.Protocol)
38+
if tgProtocol != elbv2model.ProtocolUDP && len(cfg.certificates) != 0 && (cfg.tlsPortsSet.Len() == 0 ||
39+
cfg.tlsPortsSet.Has(port.Name) || cfg.tlsPortsSet.Has(strconv.Itoa(int(port.Port)))) {
40+
if cfg.backendProtocol == "ssl" {
41+
tgProtocol = elbv2model.ProtocolTLS
42+
}
43+
listenerProtocol = elbv2model.ProtocolTLS
44+
}
45+
46+
targetGroup, err := t.buildTargetGroup(ctx, port, tgProtocol)
47+
if err != nil {
48+
return elbv2model.ListenerSpec{}, err
49+
}
50+
51+
var sslPolicy *string
52+
var certificates []elbv2model.Certificate
53+
if listenerProtocol == elbv2model.ProtocolTLS {
54+
sslPolicy = cfg.sslPolicy
55+
certificates = cfg.certificates
56+
}
57+
58+
defaultActions := t.buildListenerDefaultActions(ctx, targetGroup)
59+
return elbv2model.ListenerSpec{
60+
LoadBalancerARN: t.loadBalancer.LoadBalancerARN(),
61+
Port: int64(port.Port),
62+
Protocol: listenerProtocol,
63+
Certificates: certificates,
64+
SSLPolicy: sslPolicy,
65+
DefaultActions: defaultActions,
66+
}, nil
67+
}
68+
69+
func (t *defaultModelBuildTask) buildListenerDefaultActions(_ context.Context, targetGroup *elbv2model.TargetGroup) []elbv2model.Action {
70+
return []elbv2model.Action{
71+
{
72+
Type: elbv2model.ActionTypeForward,
73+
ForwardConfig: &elbv2model.ForwardActionConfig{
74+
TargetGroups: []elbv2model.TargetGroupTuple{
75+
{
76+
TargetGroupARN: targetGroup.TargetGroupARN(),
77+
},
78+
},
79+
},
80+
},
81+
}
82+
}
83+
84+
func (t *defaultModelBuildTask) buildSSLNegotiationPolicy(_ context.Context) *string {
85+
rawSslPolicyStr := ""
86+
if exists := t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixSSLNegotiationPolicy, &rawSslPolicyStr, t.service.Annotations); exists {
87+
return &rawSslPolicyStr
88+
}
89+
return nil
90+
}
91+
92+
func (t *defaultModelBuildTask) buildListenerCertificates(_ context.Context) []elbv2model.Certificate {
93+
var rawCertificateARNs []string
94+
_ = t.annotationParser.ParseStringSliceAnnotation(annotations.SvcLBSuffixSSLCertificate, &rawCertificateARNs, t.service.Annotations)
95+
96+
var certificates []elbv2model.Certificate
97+
for _, cert := range rawCertificateARNs {
98+
certificates = append(certificates, elbv2model.Certificate{CertificateARN: aws.String(cert)})
99+
}
100+
return certificates
101+
}
102+
103+
func (t *defaultModelBuildTask) buildTLSPortsSet(_ context.Context) sets.String {
104+
var rawTLSPorts []string
105+
_ = t.annotationParser.ParseStringSliceAnnotation(annotations.SvcLBSuffixSSLPorts, &rawTLSPorts, t.service.Annotations)
106+
return sets.NewString(rawTLSPorts...)
107+
}
108+
109+
func (t *defaultModelBuildTask) buildBackendProtocol(_ context.Context) string {
110+
rawBackendProtocol := ""
111+
_ = t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixBEProtocol, &rawBackendProtocol, t.service.Annotations)
112+
return rawBackendProtocol
113+
}
114+
115+
type listenerConfig struct {
116+
certificates []elbv2model.Certificate
117+
tlsPortsSet sets.String
118+
sslPolicy *string
119+
backendProtocol string
120+
}
121+
122+
func (t *defaultModelBuildTask) buildListenerConfig(ctx context.Context) listenerConfig {
123+
certificates := t.buildListenerCertificates(ctx)
124+
tlsPortsSet := t.buildTLSPortsSet(ctx)
125+
backendProtocol := t.buildBackendProtocol(ctx)
126+
sslPolicy := t.buildSSLNegotiationPolicy(ctx)
127+
128+
return listenerConfig{
129+
certificates: certificates,
130+
tlsPortsSet: tlsPortsSet,
131+
sslPolicy: sslPolicy,
132+
backendProtocol: backendProtocol,
133+
}
134+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"fmt"
8+
"github.com/aws/aws-sdk-go/aws"
9+
"github.com/aws/aws-sdk-go/service/ec2"
10+
"github.com/pkg/errors"
11+
"regexp"
12+
"sigs.k8s.io/aws-load-balancer-controller/pkg/annotations"
13+
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
14+
"strconv"
15+
)
16+
17+
const (
18+
lbAttrsAccessLogsS3Enabled = "access_logs.s3.enabled"
19+
lbAttrsAccessLogsS3Bucket = "access_logs.s3.bucket"
20+
lbAttrsAccessLogsS3Prefix = "access_logs.s3.prefix"
21+
lbAttrsLoadBalancingCrossZoneEnabled = "load_balancing.cross_zone.enabled"
22+
23+
resourceIDLoadBalancer = "LoadBalancer"
24+
)
25+
26+
func (t *defaultModelBuildTask) buildLoadBalancer(ctx context.Context, scheme elbv2model.LoadBalancerScheme) error {
27+
spec, err := t.buildLoadBalancerSpec(ctx, scheme)
28+
if err != nil {
29+
return err
30+
}
31+
t.loadBalancer = elbv2model.NewLoadBalancer(t.stack, resourceIDLoadBalancer, spec)
32+
return nil
33+
}
34+
35+
func (t *defaultModelBuildTask) buildLoadBalancerSpec(ctx context.Context, scheme elbv2model.LoadBalancerScheme) (elbv2model.LoadBalancerSpec, error) {
36+
ipAddressType := elbv2model.IPAddressTypeIPV4
37+
lbAttributes, err := t.buildLoadBalancerAttributes(ctx)
38+
if err != nil {
39+
return elbv2model.LoadBalancerSpec{}, err
40+
}
41+
tags, err := t.buildLoadBalancerTags(ctx)
42+
if err != nil {
43+
return elbv2model.LoadBalancerSpec{}, err
44+
}
45+
subnetMappings, err := t.buildSubnetMappings(ctx, t.ec2Subnets)
46+
if err != nil {
47+
return elbv2model.LoadBalancerSpec{}, err
48+
}
49+
name := t.buildLoadBalancerName(ctx, scheme)
50+
spec := elbv2model.LoadBalancerSpec{
51+
Name: name,
52+
Type: elbv2model.LoadBalancerTypeNetwork,
53+
Scheme: &scheme,
54+
IPAddressType: &ipAddressType,
55+
SubnetMappings: subnetMappings,
56+
LoadBalancerAttributes: lbAttributes,
57+
Tags: tags,
58+
}
59+
return spec, nil
60+
}
61+
62+
func (t *defaultModelBuildTask) buildLoadBalancerScheme(_ context.Context) (elbv2model.LoadBalancerScheme, error) {
63+
internal := false
64+
if _, err := t.annotationParser.ParseBoolAnnotation(annotations.SvcLBSuffixInternal, &internal, t.service.Annotations); err != nil {
65+
return "", err
66+
}
67+
if internal {
68+
return elbv2model.LoadBalancerSchemeInternal, nil
69+
}
70+
return elbv2model.LoadBalancerSchemeInternetFacing, nil
71+
}
72+
73+
func (t *defaultModelBuildTask) buildAdditionalResourceTags(_ context.Context) (map[string]string, error) {
74+
tags := make(map[string]string)
75+
_, err := t.annotationParser.ParseStringMapAnnotation(annotations.SvcLBSuffixAdditionalTags, &tags, t.service.Annotations)
76+
if err != nil {
77+
return nil, err
78+
}
79+
return tags, nil
80+
}
81+
82+
func (t *defaultModelBuildTask) buildLoadBalancerTags(ctx context.Context) (map[string]string, error) {
83+
return t.buildAdditionalResourceTags(ctx)
84+
}
85+
86+
func (t *defaultModelBuildTask) buildSubnetMappings(_ context.Context, ec2Subnets []*ec2.Subnet) ([]elbv2model.SubnetMapping, error) {
87+
var eipAllocation []string
88+
eipConfigured := t.annotationParser.ParseStringSliceAnnotation(annotations.SvcLBSuffixEIPAllocations, &eipAllocation, t.service.Annotations)
89+
if eipConfigured && len(eipAllocation) != len(ec2Subnets) {
90+
return []elbv2model.SubnetMapping{}, errors.Errorf("number of EIP allocations (%d) and subnets (%d) must match", len(eipAllocation), len(ec2Subnets))
91+
}
92+
subnetMappings := make([]elbv2model.SubnetMapping, 0, len(ec2Subnets))
93+
for idx, subnet := range ec2Subnets {
94+
mapping := elbv2model.SubnetMapping{
95+
SubnetID: aws.StringValue(subnet.SubnetId),
96+
}
97+
if idx < len(eipAllocation) {
98+
mapping.AllocationID = aws.String(eipAllocation[idx])
99+
}
100+
subnetMappings = append(subnetMappings, mapping)
101+
}
102+
return subnetMappings, nil
103+
}
104+
105+
func (t *defaultModelBuildTask) buildLoadBalancerAttributes(_ context.Context) ([]elbv2model.LoadBalancerAttribute, error) {
106+
var attrs []elbv2model.LoadBalancerAttribute
107+
accessLogEnabled := t.defaultAccessLogS3Enabled
108+
bucketName := t.defaultAccessLogsS3Bucket
109+
bucketPrefix := t.defaultAccessLogsS3Prefix
110+
if _, err := t.annotationParser.ParseBoolAnnotation(annotations.SvcLBSuffixAccessLogEnabled, &accessLogEnabled, t.service.Annotations); err != nil {
111+
return []elbv2model.LoadBalancerAttribute{}, err
112+
}
113+
if accessLogEnabled {
114+
t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixAccessLogS3BucketName, &bucketName, t.service.Annotations)
115+
t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixAccessLogS3BucketPrefix, &bucketPrefix, t.service.Annotations)
116+
}
117+
crossZoneEnabled := t.defaultLoadBalancingCrossZoneEnabled
118+
if _, err := t.annotationParser.ParseBoolAnnotation(annotations.SvcLBSuffixCrossZoneLoadBalancingEnabled, &crossZoneEnabled, t.service.Annotations); err != nil {
119+
return []elbv2model.LoadBalancerAttribute{}, err
120+
}
121+
122+
attrs = []elbv2model.LoadBalancerAttribute{
123+
{
124+
Key: lbAttrsAccessLogsS3Enabled,
125+
Value: strconv.FormatBool(accessLogEnabled),
126+
},
127+
{
128+
Key: lbAttrsAccessLogsS3Bucket,
129+
Value: bucketName,
130+
},
131+
{
132+
Key: lbAttrsAccessLogsS3Prefix,
133+
Value: bucketPrefix,
134+
},
135+
{
136+
Key: lbAttrsLoadBalancingCrossZoneEnabled,
137+
Value: strconv.FormatBool(crossZoneEnabled),
138+
},
139+
}
140+
141+
return attrs, nil
142+
}
143+
144+
var invalidLoadBalancerNamePattern = regexp.MustCompile("[[:^alnum:]]")
145+
146+
func (t *defaultModelBuildTask) buildLoadBalancerName(_ context.Context, scheme elbv2model.LoadBalancerScheme) string {
147+
uuidHash := sha256.New()
148+
_, _ = uuidHash.Write([]byte(t.clusterName))
149+
_, _ = uuidHash.Write([]byte(t.service.UID))
150+
_, _ = uuidHash.Write([]byte(scheme))
151+
uuid := hex.EncodeToString(uuidHash.Sum(nil))
152+
153+
sanitizedNamespace := invalidLoadBalancerNamePattern.ReplaceAllString(t.service.Namespace, "")
154+
sanitizedName := invalidLoadBalancerNamePattern.ReplaceAllString(t.service.Name, "")
155+
return fmt.Sprintf("k8s-%.8s-%.8s-%.10s", sanitizedNamespace, sanitizedName, uuid)
156+
}

0 commit comments

Comments
 (0)