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 @@ var Options struct {
// Deprecated: use VPCNames instead
VPCName string
VPCNames string
SubnetNames string
LoadBalancerType string
BGPNodeSelector string
IpHolderSuffix string
Expand Down Expand Up @@ -132,6 +133,12 @@ func newCloud() (cloudprovider.Interface, error) {
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 = ""
}

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

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

Expand All @@ -16,16 +18,30 @@ var (
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)
}

// GetAllVPCIDs returns vpc ids stored in map
func GetAllVPCIDs() []int {
Mu.Lock()
Expand Down Expand Up @@ -59,13 +75,65 @@ func GetVPCID(ctx context.Context, client client.Client, vpcName string) (int, e
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
subnetNames := strings.Split(Options.SubnetNames, ",")
subnetIDList := []string{} // Making this a slice of strings for ease of use with resultFilter

for _, name := range subnetNames {
subnetID, err := GetSubnetID(ctx, client, vpcID, name) // For caching

if err != nil { // Don't filter subnets we can't find
klog.Errorf("subnet %s not found. Skipping.", name)
continue
}

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

// 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")
}
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)
}
})
}
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