Skip to content

Commit df34350

Browse files
Merge pull request #435 from linode/linode-interfaces
[feat] add support for using linode interfaces (beta-only)
2 parents ee8fb73 + 2b2d8e8 commit df34350

File tree

7 files changed

+606
-53
lines changed

7 files changed

+606
-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: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s
157157

158158
// check already configured routes
159159
intfRoutes := []string{}
160+
linodeInterfaceRoutes := []linodego.VPCInterfaceIPv4RangeCreateOptions{}
160161
intfVPCIP := linodego.VPCIP{}
161162

162163
for _, vpcid := range services.GetAllVPCIDs() {
@@ -176,6 +177,9 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s
176177
}
177178

178179
intfRoutes = append(intfRoutes, *ir.AddressRange)
180+
linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{
181+
Range: *ir.AddressRange,
182+
})
179183
}
180184
}
181185

@@ -184,16 +188,11 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s
184188
}
185189

186190
intfRoutes = append(intfRoutes, route.DestinationCIDR)
187-
interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{
188-
IPRanges: &intfRoutes,
189-
}
191+
linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{
192+
Range: route.DestinationCIDR,
193+
})
190194

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

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

211210
// check already configured routes
212211
intfRoutes := []string{}
212+
linodeInterfaceRoutes := []linodego.VPCInterfaceIPv4RangeCreateOptions{}
213213
intfVPCIP := linodego.VPCIP{}
214214

215215
for _, vpcid := range services.GetAllVPCIDs() {
@@ -228,21 +228,46 @@ func (r *routes) DeleteRoute(ctx context.Context, clusterName string, route *clo
228228
}
229229

230230
intfRoutes = append(intfRoutes, *ir.AddressRange)
231+
linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{
232+
Range: *ir.AddressRange,
233+
})
231234
}
232235
}
233236

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

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

cloud/linode/route_controller_test.go

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

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

587+
interfaceWitVPCAndNoRoute := linodego.LinodeInterface{
588+
ID: services.VpcIDs["dummy"],
589+
VPC: &linodego.VPCInterface{IPv4: linodego.VPCInterfaceIPv4{Ranges: nil}},
590+
}
591+
592+
validInstance.InterfaceGeneration = linodego.GenerationLinode
593+
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) {
594+
ctrl := gomock.NewController(t)
595+
defer ctrl.Finish()
596+
client := mocks.NewMockClient(ctrl)
597+
instanceCache := services.NewInstances(client)
598+
routeController, err := newRoutes(client, instanceCache)
599+
require.NoError(t, err)
600+
601+
client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{validInstance}, nil)
602+
client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(noRoutesInVPC, nil)
603+
client.EXPECT().ListVPCIPv6Addresses(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.VPCIP{}, nil)
604+
client.EXPECT().UpdateInterface(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&interfaceWitVPCAndNoRoute, nil)
605+
err = routeController.DeleteRoute(ctx, "dummy", route)
606+
assert.NoError(t, err)
607+
})
608+
validInstance.InterfaceGeneration = ""
609+
555610
routesInVPC := []linodego.VPCIP{
556611
{
557612
Address: &vpcIP,
@@ -584,4 +639,21 @@ func TestDeleteRoute(t *testing.T) {
584639
err = routeController.DeleteRoute(ctx, "dummy", route)
585640
assert.NoError(t, err)
586641
})
642+
643+
validInstance.InterfaceGeneration = linodego.GenerationLinode
644+
t.Run("should return no error if instance exists, connected to VPC and route is deleted with linode interfaces", func(t *testing.T) {
645+
ctrl := gomock.NewController(t)
646+
defer ctrl.Finish()
647+
client := mocks.NewMockClient(ctrl)
648+
instanceCache := services.NewInstances(client)
649+
routeController, err := newRoutes(client, instanceCache)
650+
require.NoError(t, err)
651+
652+
client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{validInstance}, nil)
653+
client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(routesInVPC, nil)
654+
client.EXPECT().ListVPCIPv6Addresses(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.VPCIP{}, nil)
655+
client.EXPECT().UpdateInterface(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&interfaceWitVPCAndNoRoute, nil)
656+
err = routeController.DeleteRoute(ctx, "dummy", route)
657+
assert.NoError(t, err)
658+
})
587659
}

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)