Skip to content

Commit 59b7b06

Browse files
Add a firewall rule to open the LB health check and service port(s) when using service.beta.kubernetes.io/do-loadbalancer-type=REGIONAL_NETWORK (#748)
1 parent b8f3f45 commit 59b7b06

File tree

3 files changed

+208
-22
lines changed

3 files changed

+208
-22
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## unreleased
22

3+
* When using the LoadBalancer `service.beta.kubernetes.io/do-loadbalancer-type=REGIONAL_NETWORK` (under closed beta), firewall rules
4+
are added to open up the underlying health check port and all the defined (port, protocols) defined on the service. This is to
5+
permit traffic to arrive directly on the underlying worker nodes.
6+
37
## v0.1.54 (beta) - June 12, 2024
48

59
* Fixes an issue with load balancer health checks when the LB is using PROXY protocol. The new health check

cloud-controller-manager/do/firewall_controller.go

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"net/http"
23+
"sort"
2324
"strconv"
2425
"time"
2526

@@ -270,20 +271,25 @@ func (fm *firewallManager) updateFirewall(ctx context.Context, fwID string, fr *
270271
return fm.executeInstrumentedFirewallOperationUpdate(ctx, fwID, fr)
271272
}
272273

274+
type portProtocol struct {
275+
port int
276+
protocol string
277+
}
278+
273279
// createReconciledFirewallRequest creates a firewall request that has the correct rules, name and tag
274-
func (fm *firewallManager) createReconciledFirewallRequest(serviceList []*v1.Service) *godo.FirewallRequest {
280+
func (fm *firewallManager) createReconciledFirewallRequest(serviceList []*v1.Service) (*godo.FirewallRequest, error) {
275281
var nodePortInboundRules []godo.InboundRule
282+
loadBalancerPorts := make(map[portProtocol]struct{})
276283
for _, svc := range serviceList {
277-
managed, err := isManaged(svc)
278-
if err != nil {
279-
klog.Warningf("managing service %s/%s for which no correct management flag setting could be detected: %s", svc.Namespace, svc.Name, err)
280-
managed = true
281-
}
282-
if !managed {
283-
continue
284-
}
285-
286284
if svc.Spec.Type == v1.ServiceTypeNodePort {
285+
managed, err := isManaged(svc)
286+
if err != nil {
287+
klog.Warningf("managing service %s/%s for which no correct management flag setting could be detected: %s", svc.Namespace, svc.Name, err)
288+
managed = true
289+
}
290+
if !managed {
291+
continue
292+
}
287293
// this is a nodeport service so we should check for existing inbound rules on all ports.
288294
for _, servicePort := range svc.Spec.Ports {
289295
// In the odd case that a failure is asynchronous causing the NodePort to be set to zero.
@@ -312,14 +318,62 @@ func (fm *firewallManager) createReconciledFirewallRequest(serviceList []*v1.Ser
312318
},
313319
)
314320
}
321+
} else if svc.Spec.Type == v1.ServiceTypeLoadBalancer {
322+
lbType, err := getType(svc)
323+
if err != nil {
324+
return nil, fmt.Errorf("failed to get load balancer type for service %s/%s: %v", svc.Namespace, svc.Name, err)
325+
}
326+
lbNetwork, err := getNetwork(svc)
327+
if err != nil {
328+
return nil, fmt.Errorf("failed to get load balancer network for service %s/%s: %v", svc.Namespace, svc.Name, err)
329+
}
330+
if lbType == godo.LoadBalancerTypeRegionalNetwork && lbNetwork == godo.LoadBalancerNetworkTypeExternal {
331+
// Add the health check port
332+
_, healthCheckPort := healthCheckPathAndPort(svc)
333+
if healthCheckPort == 0 {
334+
continue
335+
}
336+
loadBalancerPorts[portProtocol{protocol: "tcp", port: healthCheckPort}] = struct{}{}
337+
338+
// Add the services (port, protocol)
339+
var protocol string
340+
for _, servicePort := range svc.Spec.Ports {
341+
switch servicePort.Protocol {
342+
case v1.ProtocolTCP:
343+
protocol = "tcp"
344+
case v1.ProtocolUDP:
345+
protocol = "udp"
346+
default:
347+
klog.Warningf("unsupported service protocol %v, skipping service port %v", servicePort.Protocol, servicePort.Name)
348+
continue
349+
}
350+
loadBalancerPorts[portProtocol{protocol: protocol, port: int(servicePort.Port)}] = struct{}{}
351+
}
352+
}
315353
}
316354
}
355+
for p := range loadBalancerPorts {
356+
nodePortInboundRules = append(nodePortInboundRules, godo.InboundRule{
357+
Protocol: p.protocol,
358+
PortRange: strconv.Itoa(p.port),
359+
Sources: &godo.Sources{
360+
Addresses: []string{"0.0.0.0/0", "::/0"},
361+
},
362+
})
363+
}
364+
// Sort for deterministic output
365+
sort.SliceStable(nodePortInboundRules, func(i, j int) bool {
366+
if nodePortInboundRules[i].Protocol == nodePortInboundRules[j].Protocol {
367+
return nodePortInboundRules[i].PortRange < nodePortInboundRules[j].PortRange
368+
}
369+
return nodePortInboundRules[i].Protocol < nodePortInboundRules[j].Protocol
370+
})
317371
return &godo.FirewallRequest{
318372
Name: fm.workerFirewallName,
319373
InboundRules: nodePortInboundRules,
320374
OutboundRules: allowAllOutboundRules,
321375
Tags: fm.workerFirewallTags,
322-
}
376+
}, nil
323377
}
324378

325379
// isManaged returns if the given Service should be firewall-managed based on the
@@ -399,7 +453,10 @@ func (fc *FirewallController) ensureReconciledFirewall(ctx context.Context) (ski
399453
if err != nil {
400454
return false, fmt.Errorf("failed to list services: %v", err)
401455
}
402-
fr := fc.fwManager.createReconciledFirewallRequest(serviceList)
456+
fr, err := fc.fwManager.createReconciledFirewallRequest(serviceList)
457+
if err != nil {
458+
return false, fmt.Errorf("failed to create reconciled firewall request: %v", err)
459+
}
403460

404461
fw, err := fc.fwManager.GetPreferFromCache(ctx)
405462
if err != nil {

cloud-controller-manager/do/firewall_controller_test.go

Lines changed: 135 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"strconv"
2324
"testing"
2425
"time"
2526

@@ -296,6 +297,7 @@ func TestFirewallController_createReconciledFirewallRequest(t *testing.T) {
296297
firewallRequest *godo.FirewallRequest
297298
firewallController FirewallController
298299
serviceList []*v1.Service
300+
expectedError error
299301
}{
300302
{
301303
name: "nothing to reconcile when there are no changes",
@@ -425,21 +427,21 @@ func TestFirewallController_createReconciledFirewallRequest(t *testing.T) {
425427
InboundRules: []godo.InboundRule{
426428
{
427429
Protocol: "tcp",
428-
PortRange: "31000",
430+
PortRange: "30000",
429431
Sources: &godo.Sources{
430432
Addresses: []string{"0.0.0.0/0", "::/0"},
431433
},
432434
},
433435
{
434-
Protocol: "udp",
436+
Protocol: "tcp",
435437
PortRange: "31000",
436438
Sources: &godo.Sources{
437439
Addresses: []string{"0.0.0.0/0", "::/0"},
438440
},
439441
},
440442
{
441443
Protocol: "tcp",
442-
PortRange: "30000",
444+
PortRange: "32727",
443445
Sources: &godo.Sources{
444446
Addresses: []string{"0.0.0.0/0", "::/0"},
445447
},
@@ -452,8 +454,8 @@ func TestFirewallController_createReconciledFirewallRequest(t *testing.T) {
452454
},
453455
},
454456
{
455-
Protocol: "tcp",
456-
PortRange: "32727",
457+
Protocol: "udp",
458+
PortRange: "31000",
457459
Sources: &godo.Sources{
458460
Addresses: []string{"0.0.0.0/0", "::/0"},
459461
},
@@ -524,6 +526,119 @@ func TestFirewallController_createReconciledFirewallRequest(t *testing.T) {
524526
},
525527
},
526528
},
529+
{
530+
name: "reconcile firewall with REGIONAL_NETWORK LB w/ externalTrafficPolicy=Cluster",
531+
firewallRequest: &godo.FirewallRequest{
532+
Name: testWorkerFWName,
533+
InboundRules: []godo.InboundRule{
534+
{
535+
Protocol: "tcp",
536+
PortRange: strconv.Itoa(kubeProxyHealthPort),
537+
Sources: &godo.Sources{
538+
Addresses: []string{"0.0.0.0/0", "::/0"},
539+
},
540+
},
541+
{
542+
Protocol: "tcp",
543+
PortRange: strconv.Itoa(443),
544+
Sources: &godo.Sources{
545+
Addresses: []string{"0.0.0.0/0", "::/0"},
546+
},
547+
},
548+
{
549+
Protocol: "tcp",
550+
PortRange: strconv.Itoa(80),
551+
Sources: &godo.Sources{
552+
Addresses: []string{"0.0.0.0/0", "::/0"},
553+
},
554+
},
555+
},
556+
OutboundRules: testOutboundRules,
557+
Tags: testWorkerFWTags,
558+
},
559+
serviceList: []*v1.Service{
560+
{
561+
ObjectMeta: metav1.ObjectMeta{
562+
Name: "regional_network",
563+
UID: "abc123",
564+
Annotations: map[string]string{
565+
annDOType: godo.LoadBalancerTypeRegionalNetwork,
566+
},
567+
},
568+
Spec: v1.ServiceSpec{
569+
Type: v1.ServiceTypeLoadBalancer,
570+
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyCluster,
571+
Ports: []v1.ServicePort{
572+
{
573+
Protocol: v1.ProtocolTCP,
574+
Port: 80,
575+
},
576+
{
577+
Protocol: v1.ProtocolTCP,
578+
Port: 443,
579+
},
580+
},
581+
},
582+
},
583+
},
584+
},
585+
{
586+
name: "reconcile firewall with REGIONAL_NETWORK LB w/ externalTrafficPolicy=Local",
587+
firewallRequest: &godo.FirewallRequest{
588+
Name: testWorkerFWName,
589+
InboundRules: []godo.InboundRule{
590+
{
591+
Protocol: "tcp",
592+
PortRange: strconv.Itoa(15000),
593+
Sources: &godo.Sources{
594+
Addresses: []string{"0.0.0.0/0", "::/0"},
595+
},
596+
},
597+
{
598+
Protocol: "tcp",
599+
PortRange: strconv.Itoa(443),
600+
Sources: &godo.Sources{
601+
Addresses: []string{"0.0.0.0/0", "::/0"},
602+
},
603+
},
604+
{
605+
Protocol: "tcp",
606+
PortRange: strconv.Itoa(80),
607+
Sources: &godo.Sources{
608+
Addresses: []string{"0.0.0.0/0", "::/0"},
609+
},
610+
},
611+
},
612+
OutboundRules: testOutboundRules,
613+
Tags: testWorkerFWTags,
614+
},
615+
serviceList: []*v1.Service{
616+
{
617+
ObjectMeta: metav1.ObjectMeta{
618+
Name: "regional_network",
619+
UID: "abc123",
620+
Annotations: map[string]string{
621+
annDOType: godo.LoadBalancerTypeRegionalNetwork,
622+
},
623+
},
624+
Spec: v1.ServiceSpec{
625+
Type: v1.ServiceTypeLoadBalancer,
626+
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyLocal,
627+
HealthCheckNodePort: 15000,
628+
Ports: []v1.ServicePort{
629+
{
630+
Protocol: v1.ProtocolTCP,
631+
Port: 80,
632+
},
633+
{
634+
Protocol: v1.ProtocolTCP,
635+
Port: 443,
636+
},
637+
},
638+
},
639+
},
640+
},
641+
},
527642
{
528643
name: "reconcile firewall with management flag",
529644
firewallRequest: &godo.FirewallRequest{
@@ -638,7 +753,11 @@ func TestFirewallController_createReconciledFirewallRequest(t *testing.T) {
638753
workerFirewallTags: testWorkerFWTags,
639754
workerFirewallName: testWorkerFWName,
640755
}
641-
fwReq := fm.createReconciledFirewallRequest(test.serviceList)
756+
fwReq, err := fm.createReconciledFirewallRequest(test.serviceList)
757+
758+
if (err != nil && test.expectedError == nil) || (err == nil && test.expectedError != nil) {
759+
t.Fatalf("expected error %q, got %q", test.expectedError, err)
760+
}
642761
if diff := cmp.Diff(test.firewallRequest, fwReq); diff != "" {
643762
t.Errorf("createReconciledFirewallRequest() mismatch (-want +got):\n%s", diff)
644763
}
@@ -675,15 +794,18 @@ func TestFirewallController_ensureReconciledFirewall(t *testing.T) {
675794
}
676795
}
677796

678-
serviceToFirewall := func(fm *firewallManager, svc *v1.Service) *godo.Firewall {
679-
fr := fm.createReconciledFirewallRequest([]*v1.Service{svc})
797+
serviceToFirewall := func(fm *firewallManager, svc *v1.Service) (*godo.Firewall, error) {
798+
fr, err := fm.createReconciledFirewallRequest([]*v1.Service{svc})
799+
if err != nil {
800+
return nil, err
801+
}
680802
return &godo.Firewall{
681803
ID: "id",
682804
Name: fr.Name,
683805
InboundRules: fr.InboundRules,
684806
OutboundRules: fr.OutboundRules,
685807
Tags: fr.Tags,
686-
}
808+
}, nil
687809
}
688810

689811
tests := []struct {
@@ -757,7 +879,10 @@ func TestFirewallController_ensureReconciledFirewall(t *testing.T) {
757879

758880
// Populate the firewall cache.
759881
if test.nodePortForCachedFirewall != nil {
760-
fw := serviceToFirewall(fwManager, nodePortToService(*test.nodePortForCachedFirewall))
882+
fw, err := serviceToFirewall(fwManager, nodePortToService(*test.nodePortForCachedFirewall))
883+
if err != nil {
884+
t.Fatal(err)
885+
}
761886
fwManager.fwCache.updateCache(fw)
762887
}
763888

0 commit comments

Comments
 (0)