Skip to content

Commit 7e603c6

Browse files
committed
Add support for using linode interfaces (beta-only)
1 parent 36df8ec commit 7e603c6

File tree

7 files changed

+569
-53
lines changed

7 files changed

+569
-53
lines changed

cloud/linode/client/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ type Client interface {
3636

3737
UpdateInstanceConfigInterface(context.Context, int, int, int, linodego.InstanceConfigInterfaceUpdateOptions) (*linodego.InstanceConfigInterface, error)
3838

39+
ListInterfaces(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.LinodeInterface, error)
40+
UpdateInterface(ctx context.Context, linodeID int, interfaceID int, opts linodego.LinodeInterfaceUpdateOptions) (*linodego.LinodeInterface, error)
41+
3942
GetVPC(context.Context, int) (*linodego.VPC, error)
4043
GetVPCSubnet(context.Context, int, int) (*linodego.VPCSubnet, error)
4144
ListVPCs(context.Context, *linodego.ListOptions) ([]linodego.VPC, error)

cloud/linode/client/client_with_metrics.go

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/linode/client/mocks/mock_client.go

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/linode/route_controller.go

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s
154154

155155
// check already configured routes
156156
intfRoutes := []string{}
157+
linodeInterfaceRoutes := []linodego.VPCInterfaceIPv4RangeCreateOptions{}
157158
intfVPCIP := linodego.VPCIP{}
158159

159160
for _, vpcid := range GetAllVPCIDs() {
@@ -173,6 +174,9 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s
173174
}
174175

175176
intfRoutes = append(intfRoutes, *ir.AddressRange)
177+
linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{
178+
Range: *ir.AddressRange,
179+
})
176180
}
177181
}
178182

@@ -181,16 +185,11 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s
181185
}
182186

183187
intfRoutes = append(intfRoutes, route.DestinationCIDR)
184-
interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{
185-
IPRanges: &intfRoutes,
186-
}
188+
linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{
189+
Range: route.DestinationCIDR,
190+
})
187191

188-
resp, err := r.client.UpdateInstanceConfigInterface(ctx, instance.ID, intfVPCIP.ConfigID, intfVPCIP.InterfaceID, interfaceUpdateOptions)
189-
if err != nil {
190-
return err
191-
}
192-
klog.V(4).Infof("Added routes for node %s. Current routes: %v", route.TargetNode, resp.IPRanges)
193-
return nil
192+
return r.handleInterfaces(ctx, intfRoutes, linodeInterfaceRoutes, instance, intfVPCIP, route)
194193
}
195194

196195
// DeleteRoute removes route's subnet from ip_ranges of target node's VPC interface
@@ -207,6 +206,7 @@ func (r *routes) DeleteRoute(ctx context.Context, clusterName string, route *clo
207206

208207
// check already configured routes
209208
intfRoutes := []string{}
209+
linodeInterfaceRoutes := []linodego.VPCInterfaceIPv4RangeCreateOptions{}
210210
intfVPCIP := linodego.VPCIP{}
211211

212212
for _, vpcid := range GetAllVPCIDs() {
@@ -225,21 +225,43 @@ func (r *routes) DeleteRoute(ctx context.Context, clusterName string, route *clo
225225
}
226226

227227
intfRoutes = append(intfRoutes, *ir.AddressRange)
228+
linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{
229+
Range: *ir.AddressRange,
230+
})
228231
}
229232
}
230233

231234
if intfVPCIP.Address == nil {
232235
return fmt.Errorf("unable to remove route %s for node %s. no valid interface found", route.DestinationCIDR, route.TargetNode)
233236
}
234237

235-
interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{
236-
IPRanges: &intfRoutes,
237-
}
238-
resp, err := r.client.UpdateInstanceConfigInterface(ctx, instance.ID, intfVPCIP.ConfigID, intfVPCIP.InterfaceID, interfaceUpdateOptions)
239-
if err != nil {
240-
return err
238+
return r.handleInterfaces(ctx, intfRoutes, linodeInterfaceRoutes, instance, intfVPCIP, route)
239+
}
240+
241+
// handleInterfaces updates the VPC interface with adding or deleting routes
242+
func (r *routes) handleInterfaces(ctx context.Context, intfRoutes []string, linodeInterfaceRoutes []linodego.VPCInterfaceIPv4RangeCreateOptions, instance *linodego.Instance, intfVPCIP linodego.VPCIP, route *cloudprovider.Route) error {
243+
if instance.InterfaceGeneration == linodego.GenerationLinode {
244+
interfaceUpdateOptions := linodego.LinodeInterfaceUpdateOptions{
245+
VPC: &linodego.VPCInterfaceCreateOptions{
246+
IPv4: &linodego.VPCInterfaceIPv4CreateOptions{Ranges: linodeInterfaceRoutes},
247+
},
248+
}
249+
resp, err := r.client.UpdateInterface(ctx, instance.ID, intfVPCIP.InterfaceID, interfaceUpdateOptions)
250+
if err != nil {
251+
return err
252+
}
253+
klog.V(4).Infof("Updated routes for node %s. Current routes: %v", route.TargetNode, resp.VPC.IPv4.Ranges)
254+
} else {
255+
interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{
256+
IPRanges: &intfRoutes,
257+
}
258+
resp, err := r.client.UpdateInstanceConfigInterface(ctx, instance.ID, intfVPCIP.ConfigID, intfVPCIP.InterfaceID, interfaceUpdateOptions)
259+
if err != nil {
260+
return err
261+
}
262+
klog.V(4).Infof("Updated routes for node %s. Current routes: %v", route.TargetNode, resp.IPRanges)
241263
}
242-
klog.V(4).Infof("Deleted route for node %s. Current routes: %v", route.TargetNode, resp.IPRanges)
264+
243265
return nil
244266
}
245267

cloud/linode/route_controller_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,38 @@ func TestCreateRoute(t *testing.T) {
390390
assert.NoError(t, err)
391391
})
392392

393+
interfaceWithVPCAndRoute := linodego.LinodeInterface{
394+
ID: vpcIDs["dummy"],
395+
VPC: &linodego.VPCInterface{
396+
IPv4: linodego.VPCInterfaceIPv4{
397+
Ranges: []linodego.VPCInterfaceIPv4Range{{Range: "10.10.10.0/24"}},
398+
},
399+
},
400+
}
401+
validInstance.InterfaceGeneration = linodego.GenerationLinode
402+
t.Run("should return no error if instance exists, connected to VPC we add a route with linode interfaces", func(t *testing.T) {
403+
ctrl := gomock.NewController(t)
404+
defer ctrl.Finish()
405+
client := mocks.NewMockClient(ctrl)
406+
instanceCache := newInstances(client)
407+
existingK8sCache := registeredK8sNodeCache
408+
defer func() {
409+
registeredK8sNodeCache = existingK8sCache
410+
}()
411+
registeredK8sNodeCache = newK8sNodeCache()
412+
registeredK8sNodeCache.addNodeToCache(node)
413+
routeController, err := newRoutes(client, instanceCache)
414+
require.NoError(t, err)
415+
416+
client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{validInstance}, nil)
417+
client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(noRoutesInVPC, nil)
418+
client.EXPECT().ListVPCIPv6Addresses(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.VPCIP{}, nil)
419+
client.EXPECT().UpdateInterface(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&interfaceWithVPCAndRoute, nil)
420+
err = routeController.CreateRoute(ctx, "dummy", "dummy", route)
421+
assert.NoError(t, err)
422+
})
423+
validInstance.InterfaceGeneration = ""
424+
393425
v6Route := &cloudprovider.Route{
394426
Name: "route2",
395427
TargetNode: types.NodeName(name),
@@ -549,6 +581,29 @@ func TestDeleteRoute(t *testing.T) {
549581
assert.NoError(t, err)
550582
})
551583

584+
interfaceWitVPCAndNoRoute := linodego.LinodeInterface{
585+
ID: vpcIDs["dummy"],
586+
VPC: &linodego.VPCInterface{IPv4: linodego.VPCInterfaceIPv4{Ranges: nil}},
587+
}
588+
589+
validInstance.InterfaceGeneration = linodego.GenerationLinode
590+
t.Run("should return no error if instance exists, connected to VPC, route doesn't exist and we try to delete route with linode interfaces", func(t *testing.T) {
591+
ctrl := gomock.NewController(t)
592+
defer ctrl.Finish()
593+
client := mocks.NewMockClient(ctrl)
594+
instanceCache := newInstances(client)
595+
routeController, err := newRoutes(client, instanceCache)
596+
require.NoError(t, err)
597+
598+
client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{validInstance}, nil)
599+
client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(noRoutesInVPC, nil)
600+
client.EXPECT().ListVPCIPv6Addresses(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.VPCIP{}, nil)
601+
client.EXPECT().UpdateInterface(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&interfaceWitVPCAndNoRoute, nil)
602+
err = routeController.DeleteRoute(ctx, "dummy", route)
603+
assert.NoError(t, err)
604+
})
605+
validInstance.InterfaceGeneration = ""
606+
552607
routesInVPC := []linodego.VPCIP{
553608
{
554609
Address: &vpcIP,
@@ -581,4 +636,21 @@ func TestDeleteRoute(t *testing.T) {
581636
err = routeController.DeleteRoute(ctx, "dummy", route)
582637
assert.NoError(t, err)
583638
})
639+
640+
validInstance.InterfaceGeneration = linodego.GenerationLinode
641+
t.Run("should return no error if instance exists, connected to VPC and route is deleted with linode interfaces", func(t *testing.T) {
642+
ctrl := gomock.NewController(t)
643+
defer ctrl.Finish()
644+
client := mocks.NewMockClient(ctrl)
645+
instanceCache := newInstances(client)
646+
routeController, err := newRoutes(client, instanceCache)
647+
require.NoError(t, err)
648+
649+
client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{validInstance}, nil)
650+
client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(routesInVPC, nil)
651+
client.EXPECT().ListVPCIPv6Addresses(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.VPCIP{}, nil)
652+
client.EXPECT().UpdateInterface(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&interfaceWitVPCAndNoRoute, nil)
653+
err = routeController.DeleteRoute(ctx, "dummy", route)
654+
assert.NoError(t, err)
655+
})
584656
}

cloud/nodeipam/ipam/cloud_allocator.go

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,16 @@ func getIPv6RangeFromInterface(iface linodego.InstanceConfigInterface) string {
337337
return ""
338338
}
339339

340+
func getIPv6RangeFromLinodeInterface(iface linodego.LinodeInterface) string {
341+
if len(iface.VPC.IPv6.SLAAC) > 0 {
342+
return iface.VPC.IPv6.SLAAC[0].Range
343+
}
344+
if len(iface.VPC.IPv6.Ranges) > 0 {
345+
return iface.VPC.IPv6.Ranges[0].Range
346+
}
347+
return ""
348+
}
349+
340350
// allocateIPv6CIDR allocates an IPv6 CIDR for the given node.
341351
// It retrieves the instance configuration for the node and extracts the IPv6 range.
342352
// It then creates a new net.IPNet with the IPv6 address and mask size defined
@@ -355,24 +365,49 @@ func (c *cloudAllocator) allocateIPv6CIDR(ctx context.Context, node *v1.Node) (*
355365
if err != nil {
356366
return nil, fmt.Errorf("failed to parse Linode ID from ProviderID %s: %w", node.Spec.ProviderID, err)
357367
}
358-
// Retrieve the instance configuration for the Linode ID
359-
configs, err := c.linodeClient.ListInstanceConfigs(ctx, id, &linodego.ListOptions{})
360-
if err != nil || len(configs) == 0 {
361-
return nil, fmt.Errorf("failed to list instance configs: %w", err)
362-
}
363368

369+
// fetch the instance so we can determine which interface generation to use
370+
instance, err := c.linodeClient.GetInstance(ctx, id)
371+
if err != nil {
372+
return nil, fmt.Errorf("failed get linode with id %d: %w", id, err)
373+
}
364374
ipv6Range := ""
365-
for _, iface := range configs[0].Interfaces {
366-
if iface.Purpose == linodego.InterfacePurposeVPC {
367-
ipv6Range = getIPv6RangeFromInterface(iface)
368-
if ipv6Range != "" {
369-
break
375+
if instance.InterfaceGeneration == linodego.GenerationLinode {
376+
ifaces, listErr := c.linodeClient.ListInterfaces(ctx, id, &linodego.ListOptions{})
377+
if listErr != nil || len(ifaces) == 0 {
378+
return nil, fmt.Errorf("failed to list interfaces: %w", listErr)
379+
}
380+
for _, iface := range ifaces {
381+
if iface.VPC != nil {
382+
ipv6Range = getIPv6RangeFromLinodeInterface(iface)
383+
if ipv6Range != "" {
384+
break
385+
}
370386
}
371387
}
372-
}
373388

374-
if ipv6Range == "" {
375-
return nil, fmt.Errorf("failed to find ipv6 range in instance config: %v", configs[0])
389+
if ipv6Range == "" {
390+
return nil, fmt.Errorf("failed to find ipv6 range in Linode interfaces: %v", ifaces)
391+
}
392+
} else {
393+
// Retrieve the instance configuration for the Linode ID
394+
configs, listErr := c.linodeClient.ListInstanceConfigs(ctx, id, &linodego.ListOptions{})
395+
if listErr != nil || len(configs) == 0 {
396+
return nil, fmt.Errorf("failed to list instance configs: %w", listErr)
397+
}
398+
399+
for _, iface := range configs[0].Interfaces {
400+
if iface.Purpose == linodego.InterfacePurposeVPC {
401+
ipv6Range = getIPv6RangeFromInterface(iface)
402+
if ipv6Range != "" {
403+
break
404+
}
405+
}
406+
}
407+
408+
if ipv6Range == "" {
409+
return nil, fmt.Errorf("failed to find ipv6 range in instance config: %v", configs[0])
410+
}
376411
}
377412

378413
ip, _, err := net.ParseCIDR(ipv6Range)

0 commit comments

Comments
 (0)