diff --git a/cloud/linode/client/client.go b/cloud/linode/client/client.go index 9edadef6..cb0d2312 100644 --- a/cloud/linode/client/client.go +++ b/cloud/linode/client/client.go @@ -36,6 +36,9 @@ type Client interface { UpdateInstanceConfigInterface(context.Context, int, int, int, linodego.InstanceConfigInterfaceUpdateOptions) (*linodego.InstanceConfigInterface, error) + ListInterfaces(ctx context.Context, linodeID int, opts *linodego.ListOptions) ([]linodego.LinodeInterface, error) + UpdateInterface(ctx context.Context, linodeID int, interfaceID int, opts linodego.LinodeInterfaceUpdateOptions) (*linodego.LinodeInterface, error) + GetVPC(context.Context, int) (*linodego.VPC, error) GetVPCSubnet(context.Context, int, int) (*linodego.VPCSubnet, error) ListVPCs(context.Context, *linodego.ListOptions) ([]linodego.VPC, error) diff --git a/cloud/linode/client/client_with_metrics.go b/cloud/linode/client/client_with_metrics.go index ba03e509..8a624990 100644 --- a/cloud/linode/client/client_with_metrics.go +++ b/cloud/linode/client/client_with_metrics.go @@ -306,6 +306,19 @@ func (_d ClientWithPrometheus) ListInstances(ctx context.Context, lp1 *linodego. return _d.base.ListInstances(ctx, lp1) } +// ListInterfaces implements Client +func (_d ClientWithPrometheus) ListInterfaces(ctx context.Context, linodeID int, opts *linodego.ListOptions) (la1 []linodego.LinodeInterface, err error) { + defer func() { + result := "ok" + if err != nil { + result = "error" + } + + ClientMethodCounterVec.WithLabelValues("ListInterfaces", result).Inc() + }() + return _d.base.ListInterfaces(ctx, linodeID, opts) +} + // ListNodeBalancerConfigs implements Client func (_d ClientWithPrometheus) ListNodeBalancerConfigs(ctx context.Context, i1 int, lp1 *linodego.ListOptions) (na1 []linodego.NodeBalancerConfig, err error) { defer func() { @@ -462,6 +475,19 @@ func (_d ClientWithPrometheus) UpdateInstanceConfigInterface(ctx context.Context return _d.base.UpdateInstanceConfigInterface(ctx, i1, i2, i3, i4) } +// UpdateInterface implements Client +func (_d ClientWithPrometheus) UpdateInterface(ctx context.Context, linodeID int, interfaceID int, opts linodego.LinodeInterfaceUpdateOptions) (lp1 *linodego.LinodeInterface, err error) { + defer func() { + result := "ok" + if err != nil { + result = "error" + } + + ClientMethodCounterVec.WithLabelValues("UpdateInterface", result).Inc() + }() + return _d.base.UpdateInterface(ctx, linodeID, interfaceID, opts) +} + // UpdateNodeBalancer implements Client func (_d ClientWithPrometheus) UpdateNodeBalancer(ctx context.Context, i1 int, n1 linodego.NodeBalancerUpdateOptions) (np1 *linodego.NodeBalancer, err error) { defer func() { diff --git a/cloud/linode/client/mocks/mock_client.go b/cloud/linode/client/mocks/mock_client.go index dad737ae..b0e5100e 100644 --- a/cloud/linode/client/mocks/mock_client.go +++ b/cloud/linode/client/mocks/mock_client.go @@ -345,6 +345,21 @@ func (mr *MockClientMockRecorder) ListInstances(arg0, arg1 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstances", reflect.TypeOf((*MockClient)(nil).ListInstances), arg0, arg1) } +// ListInterfaces mocks base method. +func (m *MockClient) ListInterfaces(arg0 context.Context, arg1 int, arg2 *linodego.ListOptions) ([]linodego.LinodeInterface, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListInterfaces", arg0, arg1, arg2) + ret0, _ := ret[0].([]linodego.LinodeInterface) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListInterfaces indicates an expected call of ListInterfaces. +func (mr *MockClientMockRecorder) ListInterfaces(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInterfaces", reflect.TypeOf((*MockClient)(nil).ListInterfaces), arg0, arg1, arg2) +} + // ListNodeBalancerConfigs mocks base method. func (m *MockClient) ListNodeBalancerConfigs(arg0 context.Context, arg1 int, arg2 *linodego.ListOptions) ([]linodego.NodeBalancerConfig, error) { m.ctrl.T.Helper() @@ -524,6 +539,21 @@ func (mr *MockClientMockRecorder) UpdateInstanceConfigInterface(arg0, arg1, arg2 return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInstanceConfigInterface", reflect.TypeOf((*MockClient)(nil).UpdateInstanceConfigInterface), arg0, arg1, arg2, arg3, arg4) } +// UpdateInterface mocks base method. +func (m *MockClient) UpdateInterface(arg0 context.Context, arg1, arg2 int, arg3 linodego.LinodeInterfaceUpdateOptions) (*linodego.LinodeInterface, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateInterface", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*linodego.LinodeInterface) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateInterface indicates an expected call of UpdateInterface. +func (mr *MockClientMockRecorder) UpdateInterface(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateInterface", reflect.TypeOf((*MockClient)(nil).UpdateInterface), arg0, arg1, arg2, arg3) +} + // UpdateNodeBalancer mocks base method. func (m *MockClient) UpdateNodeBalancer(arg0 context.Context, arg1 int, arg2 linodego.NodeBalancerUpdateOptions) (*linodego.NodeBalancer, error) { m.ctrl.T.Helper() diff --git a/cloud/linode/route_controller.go b/cloud/linode/route_controller.go index 7bf6f0d4..31fc0b9d 100644 --- a/cloud/linode/route_controller.go +++ b/cloud/linode/route_controller.go @@ -157,6 +157,7 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s // check already configured routes intfRoutes := []string{} + linodeInterfaceRoutes := []linodego.VPCInterfaceIPv4RangeCreateOptions{} intfVPCIP := linodego.VPCIP{} for _, vpcid := range services.GetAllVPCIDs() { @@ -176,6 +177,9 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s } intfRoutes = append(intfRoutes, *ir.AddressRange) + linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{ + Range: *ir.AddressRange, + }) } } @@ -184,16 +188,11 @@ func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint s } intfRoutes = append(intfRoutes, route.DestinationCIDR) - interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{ - IPRanges: &intfRoutes, - } + linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{ + Range: route.DestinationCIDR, + }) - resp, err := r.client.UpdateInstanceConfigInterface(ctx, instance.ID, intfVPCIP.ConfigID, intfVPCIP.InterfaceID, interfaceUpdateOptions) - if err != nil { - return err - } - klog.V(4).Infof("Added routes for node %s. Current routes: %v", route.TargetNode, resp.IPRanges) - return nil + return r.handleInterfaces(ctx, intfRoutes, linodeInterfaceRoutes, instance, intfVPCIP, route) } // 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 // check already configured routes intfRoutes := []string{} + linodeInterfaceRoutes := []linodego.VPCInterfaceIPv4RangeCreateOptions{} intfVPCIP := linodego.VPCIP{} for _, vpcid := range services.GetAllVPCIDs() { @@ -228,6 +228,9 @@ func (r *routes) DeleteRoute(ctx context.Context, clusterName string, route *clo } intfRoutes = append(intfRoutes, *ir.AddressRange) + linodeInterfaceRoutes = append(linodeInterfaceRoutes, linodego.VPCInterfaceIPv4RangeCreateOptions{ + Range: *ir.AddressRange, + }) } } @@ -235,14 +238,36 @@ func (r *routes) DeleteRoute(ctx context.Context, clusterName string, route *clo return fmt.Errorf("unable to remove route %s for node %s. no valid interface found", route.DestinationCIDR, route.TargetNode) } - interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{ - IPRanges: &intfRoutes, - } - resp, err := r.client.UpdateInstanceConfigInterface(ctx, instance.ID, intfVPCIP.ConfigID, intfVPCIP.InterfaceID, interfaceUpdateOptions) - if err != nil { - return err + return r.handleInterfaces(ctx, intfRoutes, linodeInterfaceRoutes, instance, intfVPCIP, route) +} + +// handleInterfaces updates the VPC interface with adding or deleting routes +func (r *routes) handleInterfaces(ctx context.Context, intfRoutes []string, linodeInterfaceRoutes []linodego.VPCInterfaceIPv4RangeCreateOptions, instance *linodego.Instance, intfVPCIP linodego.VPCIP, route *cloudprovider.Route) error { + if instance.InterfaceGeneration == linodego.GenerationLinode { + interfaceUpdateOptions := linodego.LinodeInterfaceUpdateOptions{ + VPC: &linodego.VPCInterfaceCreateOptions{ + SubnetID: intfVPCIP.SubnetID, + IPv4: &linodego.VPCInterfaceIPv4CreateOptions{Ranges: linodeInterfaceRoutes}, + }, + } + resp, err := r.client.UpdateInterface(ctx, instance.ID, intfVPCIP.InterfaceID, interfaceUpdateOptions) + if err != nil { + klog.V(4).Infof("Unable to update legacy interface %d for node %s", intfVPCIP.InterfaceID, route.TargetNode) + return err + } + klog.V(4).Infof("Updated routes for node %s. Current routes: %v", route.TargetNode, resp.VPC.IPv4.Ranges) + } else { + interfaceUpdateOptions := linodego.InstanceConfigInterfaceUpdateOptions{ + IPRanges: &intfRoutes, + } + resp, err := r.client.UpdateInstanceConfigInterface(ctx, instance.ID, intfVPCIP.ConfigID, intfVPCIP.InterfaceID, interfaceUpdateOptions) + if err != nil { + klog.V(4).Infof("Unable to update linode interface %d for node %s", intfVPCIP.InterfaceID, route.TargetNode) + return err + } + klog.V(4).Infof("Updated routes for node %s. Current routes: %v", route.TargetNode, resp.IPRanges) } - klog.V(4).Infof("Deleted route for node %s. Current routes: %v", route.TargetNode, resp.IPRanges) + return nil } diff --git a/cloud/linode/route_controller_test.go b/cloud/linode/route_controller_test.go index 37548b14..4b88385b 100644 --- a/cloud/linode/route_controller_test.go +++ b/cloud/linode/route_controller_test.go @@ -393,6 +393,38 @@ func TestCreateRoute(t *testing.T) { assert.NoError(t, err) }) + interfaceWithVPCAndRoute := linodego.LinodeInterface{ + ID: services.VpcIDs["dummy"], + VPC: &linodego.VPCInterface{ + IPv4: linodego.VPCInterfaceIPv4{ + Ranges: []linodego.VPCInterfaceIPv4Range{{Range: "10.10.10.0/24"}}, + }, + }, + } + validInstance.InterfaceGeneration = linodego.GenerationLinode + t.Run("should return no error if instance exists, connected to VPC we add a route with linode interfaces", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocks.NewMockClient(ctrl) + instanceCache := services.NewInstances(client) + existingK8sCache := registeredK8sNodeCache + defer func() { + registeredK8sNodeCache = existingK8sCache + }() + registeredK8sNodeCache = newK8sNodeCache() + registeredK8sNodeCache.addNodeToCache(node) + routeController, err := newRoutes(client, instanceCache) + require.NoError(t, err) + + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{validInstance}, nil) + client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(noRoutesInVPC, nil) + client.EXPECT().ListVPCIPv6Addresses(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.VPCIP{}, nil) + client.EXPECT().UpdateInterface(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&interfaceWithVPCAndRoute, nil) + err = routeController.CreateRoute(ctx, "dummy", "dummy", route) + assert.NoError(t, err) + }) + validInstance.InterfaceGeneration = "" + v6Route := &cloudprovider.Route{ Name: "route2", TargetNode: types.NodeName(name), @@ -552,6 +584,29 @@ func TestDeleteRoute(t *testing.T) { assert.NoError(t, err) }) + interfaceWitVPCAndNoRoute := linodego.LinodeInterface{ + ID: services.VpcIDs["dummy"], + VPC: &linodego.VPCInterface{IPv4: linodego.VPCInterfaceIPv4{Ranges: nil}}, + } + + validInstance.InterfaceGeneration = linodego.GenerationLinode + 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) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocks.NewMockClient(ctrl) + instanceCache := services.NewInstances(client) + routeController, err := newRoutes(client, instanceCache) + require.NoError(t, err) + + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{validInstance}, nil) + client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(noRoutesInVPC, nil) + client.EXPECT().ListVPCIPv6Addresses(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.VPCIP{}, nil) + client.EXPECT().UpdateInterface(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&interfaceWitVPCAndNoRoute, nil) + err = routeController.DeleteRoute(ctx, "dummy", route) + assert.NoError(t, err) + }) + validInstance.InterfaceGeneration = "" + routesInVPC := []linodego.VPCIP{ { Address: &vpcIP, @@ -584,4 +639,21 @@ func TestDeleteRoute(t *testing.T) { err = routeController.DeleteRoute(ctx, "dummy", route) assert.NoError(t, err) }) + + validInstance.InterfaceGeneration = linodego.GenerationLinode + t.Run("should return no error if instance exists, connected to VPC and route is deleted with linode interfaces", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + client := mocks.NewMockClient(ctrl) + instanceCache := services.NewInstances(client) + routeController, err := newRoutes(client, instanceCache) + require.NoError(t, err) + + client.EXPECT().ListInstances(gomock.Any(), nil).Times(1).Return([]linodego.Instance{validInstance}, nil) + client.EXPECT().ListVPCIPAddresses(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(routesInVPC, nil) + client.EXPECT().ListVPCIPv6Addresses(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.VPCIP{}, nil) + client.EXPECT().UpdateInterface(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(&interfaceWitVPCAndNoRoute, nil) + err = routeController.DeleteRoute(ctx, "dummy", route) + assert.NoError(t, err) + }) } diff --git a/cloud/nodeipam/ipam/cloud_allocator.go b/cloud/nodeipam/ipam/cloud_allocator.go index 0761d5ae..a848e25a 100644 --- a/cloud/nodeipam/ipam/cloud_allocator.go +++ b/cloud/nodeipam/ipam/cloud_allocator.go @@ -337,6 +337,16 @@ func getIPv6RangeFromInterface(iface linodego.InstanceConfigInterface) string { return "" } +func getIPv6RangeFromLinodeInterface(iface linodego.LinodeInterface) string { + if len(iface.VPC.IPv6.SLAAC) > 0 { + return iface.VPC.IPv6.SLAAC[0].Range + } + if len(iface.VPC.IPv6.Ranges) > 0 { + return iface.VPC.IPv6.Ranges[0].Range + } + return "" +} + // allocateIPv6CIDR allocates an IPv6 CIDR for the given node. // It retrieves the instance configuration for the node and extracts the IPv6 range. // 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) (* if err != nil { return nil, fmt.Errorf("failed to parse Linode ID from ProviderID %s: %w", node.Spec.ProviderID, err) } - // Retrieve the instance configuration for the Linode ID - configs, err := c.linodeClient.ListInstanceConfigs(ctx, id, &linodego.ListOptions{}) - if err != nil || len(configs) == 0 { - return nil, fmt.Errorf("failed to list instance configs: %w", err) - } + // fetch the instance so we can determine which interface generation to use + instance, err := c.linodeClient.GetInstance(ctx, id) + if err != nil { + return nil, fmt.Errorf("failed get linode with id %d: %w", id, err) + } ipv6Range := "" - for _, iface := range configs[0].Interfaces { - if iface.Purpose == linodego.InterfacePurposeVPC { - ipv6Range = getIPv6RangeFromInterface(iface) - if ipv6Range != "" { - break + if instance.InterfaceGeneration == linodego.GenerationLinode { + ifaces, listErr := c.linodeClient.ListInterfaces(ctx, id, &linodego.ListOptions{}) + if listErr != nil || len(ifaces) == 0 { + return nil, fmt.Errorf("failed to list interfaces: %w", listErr) + } + for _, iface := range ifaces { + if iface.VPC != nil { + ipv6Range = getIPv6RangeFromLinodeInterface(iface) + if ipv6Range != "" { + break + } } } - } - if ipv6Range == "" { - return nil, fmt.Errorf("failed to find ipv6 range in instance config: %v", configs[0]) + if ipv6Range == "" { + return nil, fmt.Errorf("failed to find ipv6 range in Linode interfaces: %v", ifaces) + } + } else { + // Retrieve the instance configuration for the Linode ID + configs, listErr := c.linodeClient.ListInstanceConfigs(ctx, id, &linodego.ListOptions{}) + if listErr != nil || len(configs) == 0 { + return nil, fmt.Errorf("failed to list instance configs: %w", listErr) + } + + for _, iface := range configs[0].Interfaces { + if iface.Purpose == linodego.InterfacePurposeVPC { + ipv6Range = getIPv6RangeFromInterface(iface) + if ipv6Range != "" { + break + } + } + } + + if ipv6Range == "" { + return nil, fmt.Errorf("failed to find ipv6 range in instance config: %v", configs[0]) + } } ip, _, err := net.ParseCIDR(ipv6Range) diff --git a/cloud/nodeipam/ipam/cloud_allocator_test.go b/cloud/nodeipam/ipam/cloud_allocator_test.go index 6959423a..d3bce604 100644 --- a/cloud/nodeipam/ipam/cloud_allocator_test.go +++ b/cloud/nodeipam/ipam/cloud_allocator_test.go @@ -47,6 +47,41 @@ type testCase struct { allocatedCIDRs map[int][]string // should controller creation fail? ctrlCreateFail bool + instance *linodego.Instance +} + +func TestGetIPv6RangeFromLinodeInterface(t *testing.T) { + for _, tc := range []struct { + iface linodego.LinodeInterface + expectedRange string + }{ + {linodego.LinodeInterface{ + ID: 123, + VPC: &linodego.VPCInterface{ + IPv6: linodego.VPCInterfaceIPv6{}, + }, + }, ""}, + {linodego.LinodeInterface{ + ID: 123, + VPC: &linodego.VPCInterface{ + IPv6: linodego.VPCInterfaceIPv6{ + Ranges: []linodego.VPCInterfaceIPv6Range{{Range: "2001:db8::/64"}, {Range: "2001:db9::/64"}}, + }, + }, + }, "2001:db8::/64"}, + {linodego.LinodeInterface{ + ID: 123, + VPC: &linodego.VPCInterface{ + IPv6: linodego.VPCInterfaceIPv6{ + SLAAC: []linodego.VPCInterfaceIPv6SLAAC{{Range: "2001:db8::/64"}, {Range: "2001:db9::/64"}}, + }, + }, + }, "2001:db8::/64"}, + } { + if getIPv6RangeFromLinodeInterface(tc.iface) != tc.expectedRange { + t.Errorf("getIPv6RangeFromLinodeInterface(%+v) = %s, want %s", tc.iface, getIPv6RangeFromLinodeInterface(tc.iface), tc.expectedRange) + } + } } func TestOccupyPreExistingCIDR(t *testing.T) { @@ -181,6 +216,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) { { description: "When there's no ServiceCIDR return first CIDR in range", linodeClient: client, + instance: &linodego.Instance{ID: 12345}, fakeNodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { @@ -219,6 +255,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) { { description: "Correctly filter out ServiceCIDR", linodeClient: client, + instance: &linodego.Instance{ID: 12345}, fakeNodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { @@ -261,6 +298,7 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) { { description: "Correctly ignore already allocated CIDRs", linodeClient: client, + instance: &linodego.Instance{ID: 12345}, fakeNodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { @@ -305,6 +343,207 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) { { description: "no double counting", linodeClient: client, + instance: &linodego.Instance{ID: 12345}, + fakeNodeHandler: &testutil.FakeNodeHandler{ + Existing: []*v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + PodCIDRs: []string{"10.10.0.0/24"}, + ProviderID: fmt.Sprintf("%s12345", providerIDPrefix), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "172.234.236.202", + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Spec: v1.NodeSpec{ + PodCIDRs: []string{"10.10.2.0/24"}, + ProviderID: fmt.Sprintf("%s22345", providerIDPrefix), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "172.234.236.201", + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + Spec: v1.NodeSpec{ + ProviderID: fmt.Sprintf("%s32345", providerIDPrefix), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "172.234.236.211", + }, + }, + }, + }, + }, + Clientset: fake.NewSimpleClientset(), + }, + allocatorParams: CIDRAllocatorParams{ + ClusterCIDRs: func() []*net.IPNet { + _, clusterCIDR, _ := netutils.ParseCIDRSloppy("10.10.0.0/22") + return []*net.IPNet{clusterCIDR} + }(), + ServiceCIDR: nil, + SecondaryServiceCIDR: nil, + NodeCIDRMaskSizes: []int{24, 112}, + }, + expectedAllocatedCIDR: map[int]string{ + 0: "10.10.1.0/24", + 1: "2300:5800:2:1::/112", + }, + }, + { + description: "When there's no ServiceCIDR return first CIDR in range with linode interfaces", + linodeClient: client, + instance: &linodego.Instance{ID: 12345, InterfaceGeneration: linodego.GenerationLinode}, + fakeNodeHandler: &testutil.FakeNodeHandler{ + Existing: []*v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + ProviderID: fmt.Sprintf("%s12345", providerIDPrefix), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "172.234.236.211", + }, + }, + }, + }, + }, + Clientset: fake.NewSimpleClientset(), + }, + allocatorParams: CIDRAllocatorParams{ + ClusterCIDRs: func() []*net.IPNet { + _, clusterCIDR, _ := netutils.ParseCIDRSloppy("127.123.234.0/24") + return []*net.IPNet{clusterCIDR} + }(), + ServiceCIDR: nil, + SecondaryServiceCIDR: nil, + NodeCIDRMaskSizes: []int{30, 112}, + }, + expectedAllocatedCIDR: map[int]string{ + 0: "127.123.234.0/30", + 1: "2300:5800:2:1::/112", + }, + }, + { + description: "Correctly filter out ServiceCIDR with linode interfaces", + linodeClient: client, + instance: &linodego.Instance{ID: 12345, InterfaceGeneration: linodego.GenerationLinode}, + fakeNodeHandler: &testutil.FakeNodeHandler{ + Existing: []*v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + ProviderID: fmt.Sprintf("%s12345", providerIDPrefix), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "172.234.236.211", + }, + }, + }, + }, + }, + Clientset: fake.NewSimpleClientset(), + }, + allocatorParams: CIDRAllocatorParams{ + ClusterCIDRs: func() []*net.IPNet { + _, clusterCIDR, _ := netutils.ParseCIDRSloppy("127.123.234.0/24") + return []*net.IPNet{clusterCIDR} + }(), + ServiceCIDR: func() *net.IPNet { + _, serviceCIDR, _ := netutils.ParseCIDRSloppy("127.123.234.0/26") + return serviceCIDR + }(), + SecondaryServiceCIDR: nil, + NodeCIDRMaskSizes: []int{30, 112}, + }, + // it should return first /30 CIDR after service range + expectedAllocatedCIDR: map[int]string{ + 0: "127.123.234.64/30", + 1: "2300:5800:2:1::/112", + }, + }, + { + description: "Correctly ignore already allocated CIDRs with linode interfaces", + linodeClient: client, + instance: &linodego.Instance{ID: 12345, InterfaceGeneration: linodego.GenerationLinode}, + fakeNodeHandler: &testutil.FakeNodeHandler{ + Existing: []*v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + ProviderID: fmt.Sprintf("%s12345", providerIDPrefix), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "172.234.236.211", + }, + }, + }, + }, + }, + Clientset: fake.NewSimpleClientset(), + }, + allocatorParams: CIDRAllocatorParams{ + ClusterCIDRs: func() []*net.IPNet { + _, clusterCIDR, _ := netutils.ParseCIDRSloppy("127.123.234.0/24") + return []*net.IPNet{clusterCIDR} + }(), + ServiceCIDR: func() *net.IPNet { + _, serviceCIDR, _ := netutils.ParseCIDRSloppy("127.123.234.0/26") + return serviceCIDR + }(), + SecondaryServiceCIDR: nil, + NodeCIDRMaskSizes: []int{30, 112}, + }, + allocatedCIDRs: map[int][]string{ + 0: {"127.123.234.64/30", "127.123.234.68/30", "127.123.234.72/30", "127.123.234.80/30"}, + }, + expectedAllocatedCIDR: map[int]string{ + 0: "127.123.234.76/30", + 1: "2300:5800:2:1::/112", + }, + }, + { + description: "no double counting with linode interfaces", + linodeClient: client, + instance: &linodego.Instance{ID: 12345, InterfaceGeneration: linodego.GenerationLinode}, fakeNodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { @@ -381,23 +620,37 @@ func TestAllocateOrOccupyCIDRSuccess(t *testing.T) { testFunc := func(tc testCase) { fakeNodeInformer := test.FakeNodeInformer(tc.fakeNodeHandler) nodeList, _ := tc.fakeNodeHandler.List(tCtx, metav1.ListOptions{}) - tc.linodeClient.EXPECT().ListInstanceConfigs(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.InstanceConfig{ - { - Interfaces: []linodego.InstanceConfigInterface{ - { - VPCID: ptr.To(12345), - Purpose: linodego.InterfacePurposeVPC, - IPv6: &linodego.InstanceConfigInterfaceIPv6{ - Ranges: []linodego.InstanceConfigInterfaceIPv6Range{ - { - Range: "2300:5800:2:1::/64", + tc.linodeClient.EXPECT().GetInstance(gomock.Any(), gomock.Any()).AnyTimes().Return(tc.instance, nil) + if tc.instance.InterfaceGeneration == linodego.GenerationLinode { + tc.linodeClient.EXPECT().ListInterfaces(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.LinodeInterface{ + { + ID: 12345, + VPC: &linodego.VPCInterface{ + IPv6: linodego.VPCInterfaceIPv6{ + Ranges: []linodego.VPCInterfaceIPv6Range{{Range: "2300:5800:2:1::/64"}}, + }, + }, + }, + }, nil) + } else { + tc.linodeClient.EXPECT().ListInstanceConfigs(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.InstanceConfig{ + { + Interfaces: []linodego.InstanceConfigInterface{ + { + VPCID: ptr.To(12345), + Purpose: linodego.InterfacePurposeVPC, + IPv6: &linodego.InstanceConfigInterfaceIPv6{ + Ranges: []linodego.InstanceConfigInterfaceIPv6Range{ + { + Range: "2300:5800:2:1::/64", + }, }, }, }, }, }, - }, - }, nil) + }, nil) + } allocator, err := NewLinodeCIDRAllocator(tCtx, tc.linodeClient, tc.fakeNodeHandler, fakeNodeInformer, tc.allocatorParams, nodeList) if err != nil { t.Errorf("%v: failed to create CIDRRangeAllocator with error %v", tc.description, err) @@ -566,6 +819,7 @@ type releaseTestCase struct { expectedAllocatedCIDRSecondRound map[int]string allocatedCIDRs map[int][]string cidrsToRelease [][]string + instance *linodego.Instance } func TestReleaseCIDRSuccess(t *testing.T) { @@ -583,6 +837,7 @@ func TestReleaseCIDRSuccess(t *testing.T) { { description: "Correctly release preallocated CIDR", linodeClient: client, + instance: &linodego.Instance{ID: 12345}, fakeNodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { @@ -627,6 +882,7 @@ func TestReleaseCIDRSuccess(t *testing.T) { { description: "Correctly recycle CIDR", linodeClient: client, + instance: &linodego.Instance{ID: 12345}, fakeNodeHandler: &testutil.FakeNodeHandler{ Existing: []*v1.Node{ { @@ -670,26 +926,132 @@ func TestReleaseCIDRSuccess(t *testing.T) { 0: "127.123.234.0/30", }, }, - } - logger, tCtx := ktesting.NewTestContext(t) - testFunc := func(tc releaseTestCase) { - tc.linodeClient.EXPECT().ListInstanceConfigs(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.InstanceConfig{ - { - Interfaces: []linodego.InstanceConfigInterface{ + { + description: "Correctly release preallocated CIDR with linode interfaces", + instance: &linodego.Instance{ID: 12345, InterfaceGeneration: linodego.GenerationLinode}, + linodeClient: client, + fakeNodeHandler: &testutil.FakeNodeHandler{ + Existing: []*v1.Node{ { - VPCID: ptr.To(12345), - Purpose: linodego.InterfacePurposeVPC, - IPv6: &linodego.InstanceConfigInterfaceIPv6{ - Ranges: []linodego.InstanceConfigInterfaceIPv6Range{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + ProviderID: fmt.Sprintf("%s12345", providerIDPrefix), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ { - Range: "2300:5800:2:1::/64", + Type: v1.NodeExternalIP, + Address: "172.234.236.211", }, }, }, }, }, + Clientset: fake.NewSimpleClientset(), }, - }, nil) + allocatorParams: CIDRAllocatorParams{ + ClusterCIDRs: func() []*net.IPNet { + _, clusterCIDR, _ := netutils.ParseCIDRSloppy("127.123.234.0/28") + return []*net.IPNet{clusterCIDR} + }(), + ServiceCIDR: nil, + SecondaryServiceCIDR: nil, + NodeCIDRMaskSizes: []int{30, 112}, + }, + allocatedCIDRs: map[int][]string{ + 0: {"127.123.234.0/30", "127.123.234.4/30", "127.123.234.8/30", "127.123.234.12/30"}, + }, + expectedAllocatedCIDRFirstRound: nil, + cidrsToRelease: [][]string{ + {"127.123.234.4/30"}, + }, + expectedAllocatedCIDRSecondRound: map[int]string{ + 0: "127.123.234.4/30", + }, + }, + { + description: "Correctly recycle CIDR with linode interfaces", + instance: &linodego.Instance{ID: 12345, InterfaceGeneration: linodego.GenerationLinode}, + linodeClient: client, + fakeNodeHandler: &testutil.FakeNodeHandler{ + Existing: []*v1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "node0", + }, + Spec: v1.NodeSpec{ + ProviderID: fmt.Sprintf("%s12345", providerIDPrefix), + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeExternalIP, + Address: "172.234.236.211", + }, + }, + }, + }, + }, + Clientset: fake.NewSimpleClientset(), + }, + allocatorParams: CIDRAllocatorParams{ + ClusterCIDRs: func() []*net.IPNet { + _, clusterCIDR, _ := netutils.ParseCIDRSloppy("127.123.234.0/28") + return []*net.IPNet{clusterCIDR} + }(), + ServiceCIDR: nil, + SecondaryServiceCIDR: nil, + NodeCIDRMaskSizes: []int{30, 112}, + }, + allocatedCIDRs: map[int][]string{ + 0: {"127.123.234.4/30", "127.123.234.8/30", "127.123.234.12/30"}, + }, + expectedAllocatedCIDRFirstRound: map[int]string{ + 0: "127.123.234.0/30", + }, + cidrsToRelease: [][]string{ + {"127.123.234.0/30"}, + }, + expectedAllocatedCIDRSecondRound: map[int]string{ + 0: "127.123.234.0/30", + }, + }, + } + logger, tCtx := ktesting.NewTestContext(t) + testFunc := func(tc releaseTestCase) { + tc.linodeClient.EXPECT().GetInstance(gomock.Any(), gomock.Any()).AnyTimes().Return(tc.instance, nil) + if tc.instance.InterfaceGeneration == linodego.GenerationLinode { + tc.linodeClient.EXPECT().ListInterfaces(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.LinodeInterface{ + { + ID: 12345, + VPC: &linodego.VPCInterface{ + IPv6: linodego.VPCInterfaceIPv6{ + Ranges: []linodego.VPCInterfaceIPv6Range{{Range: "2300:5800:2:1::/64"}}, + }, + }, + }, + }, nil) + } else { + tc.linodeClient.EXPECT().ListInstanceConfigs(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return([]linodego.InstanceConfig{ + { + Interfaces: []linodego.InstanceConfigInterface{ + { + VPCID: ptr.To(12345), + Purpose: linodego.InterfacePurposeVPC, + IPv6: &linodego.InstanceConfigInterfaceIPv6{ + Ranges: []linodego.InstanceConfigInterfaceIPv6Range{ + { + Range: "2300:5800:2:1::/64", + }, + }, + }, + }, + }, + }, + }, nil) + } allocator, _ := NewLinodeCIDRAllocator(tCtx, tc.linodeClient, tc.fakeNodeHandler, test.FakeNodeInformer(tc.fakeNodeHandler), tc.allocatorParams, nil) rangeAllocator, ok := allocator.(*cloudAllocator) if !ok {