Skip to content
1 change: 1 addition & 0 deletions cloud/linode/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Client interface {

ListVPCs(context.Context, *linodego.ListOptions) ([]linodego.VPC, error)
ListVPCIPAddresses(context.Context, int, *linodego.ListOptions) ([]linodego.VPCIP, error)
ListVPCSubnets(context.Context, int, *linodego.ListOptions) ([]linodego.VPCSubnet, error)

CreateNodeBalancer(context.Context, linodego.NodeBalancerCreateOptions) (*linodego.NodeBalancer, error)
GetNodeBalancer(context.Context, int) (*linodego.NodeBalancer, error)
Expand Down
13 changes: 13 additions & 0 deletions cloud/linode/client/client_with_metrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions cloud/linode/client/mocks/mock_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions cloud/linode/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
// Deprecated: use VPCNames instead
VPCName string
VPCNames string
SubnetNames string
LoadBalancerType string
BGPNodeSelector string
IpHolderSuffix string
Expand Down Expand Up @@ -132,6 +133,12 @@
Options.VPCNames = Options.VPCName
}

// SubnetNames can't be used without VPCNames also being set
if Options.SubnetNames != "" && Options.VPCNames == "" {
klog.Warningf("failed to set flag subnet-names: vpc-names must be set to a non-empty value")
Options.SubnetNames = ""
}

Check warning on line 140 in cloud/linode/cloud.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/cloud.go#L138-L140

Added lines #L138 - L140 were not covered by tests

instanceCache = newInstances(linodeClient)
routes, err := newRoutes(linodeClient, instanceCache)
if err != nil {
Expand Down
73 changes: 72 additions & 1 deletion cloud/linode/vpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"sync"

Expand All @@ -16,16 +18,30 @@
Mu sync.RWMutex
// vpcIDs map stores vpc id's for given vpc labels
vpcIDs = make(map[string]int, 0)
// subnetIDs map stores subnet id's for given subnet labels
subnetIDs = make(map[string]int, 0)
)

type vpcLookupError struct {
value string
}

type subnetLookupError struct {
value string
}

type subnetFilter struct {
SubnetID string `json:"subnet_id"`
}

func (e vpcLookupError) Error() string {
return fmt.Sprintf("failed to find VPC: %q", e.value)
}

func (e subnetLookupError) Error() string {
return fmt.Sprintf("failed to find subnet: %q", e.value)

Check warning on line 42 in cloud/linode/vpc.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/vpc.go#L41-L42

Added lines #L41 - L42 were not covered by tests
}

// GetAllVPCIDs returns vpc ids stored in map
func GetAllVPCIDs() []int {
Mu.Lock()
Expand Down Expand Up @@ -59,13 +75,68 @@
return 0, vpcLookupError{vpcName}
}

// GetSubnetID returns the subnet ID of given subnet label
func GetSubnetID(ctx context.Context, client client.Client, vpcID int, subnetName string) (int, error) {
Mu.Lock()
defer Mu.Unlock()

// Check if map contains the id for the given label
if subnetid, ok := subnetIDs[subnetName]; ok {
return subnetid, nil
}
// Otherwise, get it from linodego.ListVPCSubnets()
subnets, err := client.ListVPCSubnets(ctx, vpcID, &linodego.ListOptions{})
if err != nil {
return 0, err
}
for _, subnet := range subnets {
if subnet.Label == subnetName {
subnetIDs[subnetName] = subnet.ID
return subnet.ID, nil
}
}

return 0, subnetLookupError{subnetName}
}

// GetVPCIPAddresses returns vpc ip's for given VPC label
func GetVPCIPAddresses(ctx context.Context, client client.Client, vpcName string) ([]linodego.VPCIP, error) {
vpcID, err := GetVPCID(ctx, client, strings.TrimSpace(vpcName))
if err != nil {
return nil, err
}
resp, err := client.ListVPCIPAddresses(ctx, vpcID, linodego.NewListOptions(0, ""))

resultFilter := ""

// Get subnet ID(s) from name(s) if subnet-names is specified
if Options.SubnetNames != "" {
// Get the IDs and store them
// subnetIDList is a slice of strings for ease of use with resultFilter
subnetNames := strings.Split(Options.SubnetNames, ",")
subnetIDList := []string{}

for _, name := range subnetNames {
// For caching
subnetID, err := GetSubnetID(ctx, client, vpcID, name)
// Don't filter subnets we can't find
if err != nil {
klog.Errorf("subnet %s not found due to error: %v. Skipping.", name, err)
continue

Check warning on line 124 in cloud/linode/vpc.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/vpc.go#L123-L124

Added lines #L123 - L124 were not covered by tests
}

// For use with the JSON filter
subnetIDList = append(subnetIDList, strconv.Itoa(subnetID))
}

// Assign the list of IDs to a stringified JSON filter
filter, err := json.Marshal(subnetFilter{SubnetID: strings.Join(subnetIDList, ",")})
if err != nil {
klog.Error("could not create JSON filter for subnet_id")
}

Check warning on line 135 in cloud/linode/vpc.go

View check run for this annotation

Codecov / codecov/patch

cloud/linode/vpc.go#L134-L135

Added lines #L134 - L135 were not covered by tests
resultFilter = string(filter)
}

resp, err := client.ListVPCIPAddresses(ctx, vpcID, linodego.NewListOptions(0, resultFilter))
if err != nil {
if linodego.ErrHasStatus(err, http.StatusNotFound) {
Mu.Lock()
Expand Down
76 changes: 76 additions & 0 deletions cloud/linode/vpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,80 @@ func TestGetVPCIPAddresses(t *testing.T) {
_, exists := vpcIDs["test10"]
assert.True(t, exists, "test10 key should be present in vpcIDs map")
})

t.Run("vpc id found and ip addresses found with subnet filtering", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
sn := Options.SubnetNames
defer func() { Options.SubnetNames = sn }()
Options.SubnetNames = "subnet4"
vpcIDs = map[string]int{"test1": 1}
subnetIDs = map[string]int{"subnet1": 1}
client.EXPECT().ListVPCs(gomock.Any(), gomock.Any()).Times(1).Return([]linodego.VPC{{ID: 10, Label: "test10"}}, nil)
client.EXPECT().ListVPCSubnets(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]linodego.VPCSubnet{{ID: 4, Label: "subnet4"}}, nil)
client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]linodego.VPCIP{}, nil)
_, err := GetVPCIPAddresses(context.TODO(), client, "test10")
assert.NoError(t, err)
_, exists := subnetIDs["subnet4"]
assert.True(t, exists, "subnet4 should be present in subnetIDs map")
})
}

func TestGetSubnetID(t *testing.T) {
t.Run("subnet in cache", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
subnetIDs = map[string]int{"test1": 1, "test2": 2, "test3": 3}
got, err := GetSubnetID(context.TODO(), client, 0, "test3")
if err != nil {
t.Errorf("GetSubnetID() error = %v", err)
return
}
if got != subnetIDs["test3"] {
t.Errorf("GetSubnetID() = %v, want %v", got, subnetIDs["test3"])
}
})

t.Run("subnetID not in cache and listVPCSubnets return error", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
subnetIDs = map[string]int{"test1": 1, "test2": 2, "test3": 3}
client.EXPECT().ListVPCSubnets(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]linodego.VPCSubnet{}, errors.New("error"))
got, err := GetSubnetID(context.TODO(), client, 0, "test4")
assert.Error(t, err)
if got != 0 {
t.Errorf("GetSubnetID() = %v, want %v", got, 0)
}
_, exists := subnetIDs["test4"]
assert.False(t, exists, "subnet4 should not be present in subnetIDs")
})

t.Run("subnetID not in cache and listVPCSubnets return nothing", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
subnetIDs = map[string]int{"test1": 1, "test2": 2, "test3": 3}
client.EXPECT().ListVPCSubnets(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]linodego.VPCSubnet{}, nil)
got, err := GetSubnetID(context.TODO(), client, 0, "test4")
assert.ErrorIs(t, err, subnetLookupError{"test4"})
if got != 0 {
t.Errorf("GetSubnetID() = %v, want %v", got, 0)
}
})

t.Run("subnetID not in cache and listVPCSubnets return subnet info", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := mocks.NewMockClient(ctrl)
subnetIDs = map[string]int{"test1": 1, "test2": 2, "test3": 3}
client.EXPECT().ListVPCSubnets(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]linodego.VPCSubnet{{ID: 4, Label: "test4"}}, nil)
got, err := GetSubnetID(context.TODO(), client, 0, "test4")
assert.NoError(t, err)
if got != 4 {
t.Errorf("GetSubnetID() = %v, want %v", got, 4)
}
})
}
7 changes: 7 additions & 0 deletions deploy/chart/templates/daemonset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ spec:
{{- if and $vpcName $vpcNames }}
{{- fail "Both vpcName and vpcNames are set. Please use only vpcNames." }}
{{- end }}
{{- $subnetNames := .Values.subnetNames }}
{{- if and .Values.routeController .Values.routeController.subnetNames }}
{{- $subnetNames = .Values.routeController.subnetNames }}
{{- end }}
{{- if .Values.routeController }}
- --enable-route-controller=true
{{- if not (or $vpcName $vpcNames) }}
Expand All @@ -72,6 +76,9 @@ spec:
{{- with $vpcName }}
- --vpc-name={{ . }}
{{- end }}
{{- with $subnetNames }}
- --subnet-names={{ . }}
{{ end }}
{{- if .Values.sharedIPLoadBalancing }}
{{- with .Values.sharedIPLoadBalancing.bgpNodeSelector }}
- --bgp-node-selector={{ . }}
Expand Down
4 changes: 3 additions & 1 deletion deploy/chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ tolerations:
# routeController:
# vpcName: <name of VPC> [Deprecated: use vpcNames instead]
# vpcNames: <comma separated list of vpc names>
# subnetNames: <comma separated list of subnet names>
# clusterCIDR: 10.0.0.0/8
# configureCloudRoutes: true

# vpcs that node internal IPs will be assigned from (not required if already specified in routeController)
# vpcs and subnets that node internal IPs will be assigned from (not required if already specified in routeController)
# vpcName: <name of VPC> [Deprecated: use vpcNames instead]
# vpcNames: <comma separated list of vpc names>
# subnetNames: <comma separated list of subnet names>

# Enable Linode token health checker
# tokenHealthChecker: true
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func main() {
command.Flags().BoolVar(&linode.Options.EnableTokenHealthChecker, "enable-token-health-checker", false, "enables Linode API token health checker")
command.Flags().StringVar(&linode.Options.VPCName, "vpc-name", "", "[deprecated: use vpc-names instead] vpc name whose routes will be managed by route-controller")
command.Flags().StringVar(&linode.Options.VPCNames, "vpc-names", "", "comma separated vpc names whose routes will be managed by route-controller")
command.Flags().StringVar(&linode.Options.SubnetNames, "subnet-names", "", "comma separated subnet names whose routes will be managed by route-controller (requires vpc-names flag to also be set)")
command.Flags().StringVar(&linode.Options.LoadBalancerType, "load-balancer-type", "nodebalancer", "configures which type of load-balancing to use for LoadBalancer Services (options: nodebalancer, cilium-bgp)")
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")
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")
Expand Down
Loading