Skip to content

Commit 11db125

Browse files
authored
[improvement] : add flag to set nodebalancer subnet (#342)
* add flag to set nodebalancer subnet and make sure nb backend ips lie within that subnet * add flag to docs as well
1 parent 07ba90a commit 11db125

File tree

8 files changed

+198
-13
lines changed

8 files changed

+198
-13
lines changed

cloud/linode/cloud.go

+12-11
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,18 @@ var Options struct {
3939
EnableRouteController bool
4040
EnableTokenHealthChecker bool
4141
// Deprecated: use VPCNames instead
42-
VPCName string
43-
VPCNames string
44-
SubnetNames string
45-
LoadBalancerType string
46-
BGPNodeSelector string
47-
IpHolderSuffix string
48-
LinodeExternalNetwork *net.IPNet
49-
NodeBalancerTags []string
50-
DefaultNBType string
51-
GlobalStopChannel chan<- struct{}
52-
EnableIPv6ForLoadBalancers bool
42+
VPCName string
43+
VPCNames string
44+
SubnetNames string
45+
LoadBalancerType string
46+
BGPNodeSelector string
47+
IpHolderSuffix string
48+
LinodeExternalNetwork *net.IPNet
49+
NodeBalancerTags []string
50+
DefaultNBType string
51+
NodeBalancerBackendIPv4Subnet string
52+
GlobalStopChannel chan<- struct{}
53+
EnableIPv6ForLoadBalancers bool
5354
}
5455

5556
type linodeCloud struct {

cloud/linode/loadbalancers.go

+41-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"net"
89
"net/http"
910
"os"
1011
"reflect"
@@ -384,8 +385,11 @@ func (l *loadbalancers) updateNodeBalancer(
384385
// Add all of the Nodes to the config
385386
newNBNodes := make([]linodego.NodeBalancerConfigRebuildNodeOptions, 0, len(nodes))
386387
subnetID := 0
387-
_, ok := service.GetAnnotations()[annotations.NodeBalancerBackendIPv4Range]
388+
backendIPv4Range, ok := service.GetAnnotations()[annotations.NodeBalancerBackendIPv4Range]
388389
if ok {
390+
if err := validateNodeBalancerBackendIPv4Range(backendIPv4Range); err != nil {
391+
return err
392+
}
389393
id, err := l.getSubnetIDForSVC(ctx, service)
390394
if err != nil {
391395
sentry.CaptureError(ctx, err)
@@ -664,6 +668,9 @@ func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName stri
664668

665669
backendIPv4Range, ok := service.GetAnnotations()[annotations.NodeBalancerBackendIPv4Range]
666670
if ok {
671+
if err := validateNodeBalancerBackendIPv4Range(backendIPv4Range); err != nil {
672+
return nil, err
673+
}
667674
subnetID, err := l.getSubnetIDForSVC(ctx, service)
668675
if err != nil {
669676
return nil, err
@@ -824,8 +831,11 @@ func (l *loadbalancers) buildLoadBalancerRequest(ctx context.Context, clusterNam
824831
configs := make([]*linodego.NodeBalancerConfigCreateOptions, 0, len(ports))
825832

826833
subnetID := 0
827-
_, ok := service.GetAnnotations()[annotations.NodeBalancerBackendIPv4Range]
834+
backendIPv4Range, ok := service.GetAnnotations()[annotations.NodeBalancerBackendIPv4Range]
828835
if ok {
836+
if err := validateNodeBalancerBackendIPv4Range(backendIPv4Range); err != nil {
837+
return nil, err
838+
}
829839
id, err := l.getSubnetIDForSVC(ctx, service)
830840
if err != nil {
831841
return nil, err
@@ -1117,3 +1127,32 @@ func getServiceBoolAnnotation(service *v1.Service, name string) bool {
11171127
boolValue, err := strconv.ParseBool(value)
11181128
return err == nil && boolValue
11191129
}
1130+
1131+
// validateNodeBalancerBackendIPv4Range validates the NodeBalancerBackendIPv4Range
1132+
// annotation to be within the NodeBalancerBackendIPv4Subnet if it is set.
1133+
func validateNodeBalancerBackendIPv4Range(backendIPv4Range string) error {
1134+
if Options.NodeBalancerBackendIPv4Subnet == "" {
1135+
return nil
1136+
}
1137+
withinCIDR, err := isCIDRWithinCIDR(Options.NodeBalancerBackendIPv4Subnet, backendIPv4Range)
1138+
if err != nil {
1139+
return fmt.Errorf("invalid IPv4 range: %v", err)
1140+
}
1141+
if !withinCIDR {
1142+
return fmt.Errorf("IPv4 range %s is not within the subnet %s", backendIPv4Range, Options.NodeBalancerBackendIPv4Subnet)
1143+
}
1144+
return nil
1145+
}
1146+
1147+
// isCIDRWithinCIDR returns true if the inner CIDR is within the outer CIDR.
1148+
func isCIDRWithinCIDR(outer, inner string) (bool, error) {
1149+
_, ipNet1, err := net.ParseCIDR(outer)
1150+
if err != nil {
1151+
return false, fmt.Errorf("invalid CIDR: %v", err)
1152+
}
1153+
_, ipNet2, err := net.ParseCIDR(inner)
1154+
if err != nil {
1155+
return false, fmt.Errorf("invalid CIDR: %v", err)
1156+
}
1157+
return ipNet1.Contains(ipNet2.IP), nil
1158+
}

cloud/linode/loadbalancers_test.go

+135
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ func TestCCMLoadBalancers(t *testing.T) {
156156
name: "Create Load Balancer With VPC Backend",
157157
f: testCreateNodeBalancerWithVPCBackend,
158158
},
159+
{
160+
name: "Update Load Balancer With VPC Backend",
161+
f: testUpdateNodeBalancerWithVPCBackend,
162+
},
159163
{
160164
name: "Create Load Balancer With VPC Backend - Overwrite VPC Name and Subnet with Annotation",
161165
f: testCreateNodeBalancerWithVPCAnnotationOverwrite,
@@ -529,15 +533,110 @@ func testCreateNodeBalancerWithVPCBackend(t *testing.T, client *linodego.Client,
529533
if err != nil {
530534
t.Fatalf("expected a nil error, got %v", err)
531535
}
536+
537+
f.ResetRequests()
538+
539+
// test with IPv4Range outside of defined NodeBalancer subnet
540+
nodebalancerBackendIPv4Subnet := Options.NodeBalancerBackendIPv4Subnet
541+
defer func() {
542+
Options.NodeBalancerBackendIPv4Subnet = nodebalancerBackendIPv4Subnet
543+
}()
544+
Options.NodeBalancerBackendIPv4Subnet = "10.99.0.0/24"
545+
if err := testCreateNodeBalancer(t, client, f, ann, nil); err == nil {
546+
t.Fatalf("expected nodebalancer creation to fail")
547+
}
548+
}
549+
550+
func testUpdateNodeBalancerWithVPCBackend(t *testing.T, client *linodego.Client, f *fakeAPI) {
551+
// provision vpc and test
552+
vpcNames := Options.VPCNames
553+
subnetNames := Options.SubnetNames
554+
defer func() {
555+
Options.VPCNames = vpcNames
556+
Options.SubnetNames = subnetNames
557+
}()
558+
Options.VPCNames = "test1"
559+
Options.SubnetNames = "default"
560+
_, _ = client.CreateVPC(context.TODO(), linodego.VPCCreateOptions{
561+
Label: "test1",
562+
Description: "",
563+
Region: "us-west",
564+
Subnets: []linodego.VPCSubnetCreateOptions{
565+
{
566+
Label: "default",
567+
IPv4: "10.0.0.0/8",
568+
},
569+
},
570+
})
571+
572+
svc := &v1.Service{
573+
ObjectMeta: metav1.ObjectMeta{
574+
Name: randString(),
575+
UID: "foobar123",
576+
Annotations: map[string]string{
577+
annotations.NodeBalancerBackendIPv4Range: "10.100.0.0/30",
578+
},
579+
},
580+
Spec: v1.ServiceSpec{
581+
Ports: []v1.ServicePort{
582+
{
583+
Name: randString(),
584+
Protocol: "TCP",
585+
Port: int32(80),
586+
NodePort: int32(30000),
587+
},
588+
},
589+
},
590+
}
591+
592+
nodes := []*v1.Node{
593+
{
594+
Status: v1.NodeStatus{
595+
Addresses: []v1.NodeAddress{
596+
{
597+
Type: v1.NodeInternalIP,
598+
Address: "127.0.0.1",
599+
},
600+
},
601+
},
602+
},
603+
}
604+
605+
lb := newLoadbalancers(client, "us-west").(*loadbalancers)
606+
fakeClientset := fake.NewSimpleClientset()
607+
lb.kubeClient = fakeClientset
608+
609+
defer func() {
610+
_ = lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc)
611+
}()
612+
613+
lbStatus, err := lb.EnsureLoadBalancer(context.TODO(), "linodelb", svc, nodes)
614+
if err != nil {
615+
t.Errorf("EnsureLoadBalancer returned an error: %s", err)
616+
}
617+
svc.Status.LoadBalancer = *lbStatus
618+
619+
stubService(fakeClientset, svc)
620+
svc.ObjectMeta.SetAnnotations(map[string]string{
621+
annotations.NodeBalancerBackendIPv4Range: "10.100.1.0/30",
622+
})
623+
624+
err = lb.UpdateLoadBalancer(context.TODO(), "linodelb", svc, nodes)
625+
if err != nil {
626+
t.Errorf("UpdateLoadBalancer returned an error while updated annotations: %s", err)
627+
}
532628
}
533629

534630
func testCreateNodeBalancerWithVPCAnnotationOverwrite(t *testing.T, client *linodego.Client, f *fakeAPI) {
535631
// provision multiple vpcs
536632
vpcNames := Options.VPCNames
633+
nodebalancerBackendIPv4Subnet := Options.NodeBalancerBackendIPv4Subnet
537634
defer func() {
538635
Options.VPCNames = vpcNames
636+
Options.NodeBalancerBackendIPv4Subnet = nodebalancerBackendIPv4Subnet
539637
}()
540638
Options.VPCNames = "test1"
639+
Options.NodeBalancerBackendIPv4Subnet = "10.100.0.0/24"
541640

542641
_, _ = client.CreateVPC(context.TODO(), linodego.VPCCreateOptions{
543642
Label: "test1",
@@ -3956,3 +4055,39 @@ func Test_loadbalancers_GetLinodeNBType(t *testing.T) {
39564055
})
39574056
}
39584057
}
4058+
4059+
func Test_validateNodeBalancerBackendIPv4Range(t *testing.T) {
4060+
type args struct {
4061+
backendIPv4Range string
4062+
}
4063+
tests := []struct {
4064+
name string
4065+
args args
4066+
wantErr bool
4067+
}{
4068+
{
4069+
name: "Valid IPv4 range",
4070+
args: args{backendIPv4Range: "10.100.0.0/30"},
4071+
wantErr: false,
4072+
},
4073+
{
4074+
name: "Invalid IPv4 range",
4075+
args: args{backendIPv4Range: "10.100.0.0"},
4076+
wantErr: true,
4077+
},
4078+
}
4079+
4080+
nbBackendSubnet := Options.NodeBalancerBackendIPv4Subnet
4081+
defer func() {
4082+
Options.NodeBalancerBackendIPv4Subnet = nbBackendSubnet
4083+
}()
4084+
Options.NodeBalancerBackendIPv4Subnet = "10.100.0.0/24"
4085+
4086+
for _, tt := range tests {
4087+
t.Run(tt.name, func(t *testing.T) {
4088+
if err := validateNodeBalancerBackendIPv4Range(tt.args.backendIPv4Range); (err != nil) != tt.wantErr {
4089+
t.Errorf("validateNodeBalancerBackendIPv4Range() error = %v, wantErr %v", err, tt.wantErr)
4090+
}
4091+
})
4092+
}
4093+
}

deploy/chart/templates/daemonset.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ spec:
103103
{{- if .Values.enableIPv6ForLoadBalancers }}
104104
- --enable-ipv6-for-loadbalancers={{ .Values.enableIPv6ForLoadBalancers }}
105105
{{- end }}
106+
{{- if .Values.nodeBalancerBackendIPv4Subnet }}
107+
- --nodebalancer-backend-ipv4-subnet={{ .Values.nodeBalancerBackendIPv4Subnet }}
108+
{{- end }}
106109
{{- with .Values.containerSecurityContext }}
107110
securityContext:
108111
{{- toYaml . | nindent 12 }}

deploy/chart/values.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ tolerations:
9191
# This can also be controlled per-service using the "service.beta.kubernetes.io/linode-loadbalancer-enable-ipv6-ingress" annotation
9292
# enableIPv6ForLoadBalancers: true
9393

94+
# nodeBalancerBackendIPv4Subnet is the subnet to use for the backend ips of the NodeBalancer
95+
# nodeBalancerBackendIPv4Subnet: ""
96+
9497
# This section adds the ability to pass environment variables to adjust CCM defaults
9598
# https://github.com/linode/linode-cloud-controller-manager/blob/master/cloud/linode/loadbalancers.go
9699
# LINODE_HOSTNAME_ONLY_INGRESS type bool is supported

docs/configuration/environment.md

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ The CCM supports the following flags:
4444
| `--ip-holder-suffix` | `""` | Suffix to append to the IP holder name when using shared IP fail-over with BGP |
4545
| `--default-nodebalancer-type` | `common` | Default type of NodeBalancer to create (options: common, premium) |
4646
| `--nodebalancer-tags` | `[]` | Linode tags to apply to all NodeBalancers |
47+
| `--nodebalancer-backend-ipv4-subnet` | `""` | ipv4 subnet to use for NodeBalancer backends |
4748
| `--enable-ipv6-for-loadbalancers` | `false` | Set both IPv4 and IPv6 addresses for all LoadBalancer services (when disabled, only IPv4 is used). This can also be configured per-service using the `service.beta.kubernetes.io/linode-loadbalancer-enable-ipv6-ingress` annotation. |
4849

4950
## Configuration Methods

docs/configuration/loadbalancer.md

+2
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ metadata:
197197
service.beta.kubernetes.io/linode-loadbalancer-subnet-name: "subnet1"
198198
```
199199

200+
If CCM is started with `--nodebalancer-backend-ipv4-subnet` flag, then it will not allow provisioning of nodebalancer unless subnet specified in service annotation lie within the subnet specified using the flag. This is to prevent accidental overlap between nodebalancer backend ips and pod CIDRs.
201+
200202
## Advanced Configuration
201203

202204
### Using Existing NodeBalancers

main.go

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ func main() {
9090
command.Flags().StringVar(&linode.Options.BGPNodeSelector, "bgp-node-selector", "", "node selector to use to perform shared IP fail-over with BGP (e.g. cilium-bgp-peering=true")
9191
command.Flags().StringVar(&linode.Options.IpHolderSuffix, "ip-holder-suffix", "", "suffix to append to the ip holder name when using shared IP fail-over with BGP (e.g. ip-holder-suffix=my-cluster-name")
9292
command.Flags().StringVar(&linode.Options.DefaultNBType, "default-nodebalancer-type", string(linodego.NBTypeCommon), "default type of NodeBalancer to create (options: common, premium)")
93+
command.Flags().StringVar(&linode.Options.NodeBalancerBackendIPv4Subnet, "nodebalancer-backend-ipv4-subnet", "", "ipv4 subnet to use for NodeBalancer backends")
9394
command.Flags().StringSliceVar(&linode.Options.NodeBalancerTags, "nodebalancer-tags", []string{}, "Linode tags to apply to all NodeBalancers")
9495
command.Flags().BoolVar(&linode.Options.EnableIPv6ForLoadBalancers, "enable-ipv6-for-loadbalancers", false, "set both IPv4 and IPv6 addresses for all LoadBalancer services (when disabled, only IPv4 is used)")
9596

0 commit comments

Comments
 (0)