github.com/cilium/cilium@v1.16.2/pkg/alibabacloud/eni/node_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package eni
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/stretchr/testify/require"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  
    15  	"github.com/cilium/cilium/pkg/alibabacloud/api/mock"
    16  	"github.com/cilium/cilium/pkg/alibabacloud/eni/limits"
    17  	eniTypes "github.com/cilium/cilium/pkg/alibabacloud/eni/types"
    18  	"github.com/cilium/cilium/pkg/alibabacloud/utils"
    19  	"github.com/cilium/cilium/pkg/ipam"
    20  	metricsmock "github.com/cilium/cilium/pkg/ipam/metrics/mock"
    21  	ipamTypes "github.com/cilium/cilium/pkg/ipam/types"
    22  	v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    23  	"github.com/cilium/cilium/pkg/testutils"
    24  )
    25  
    26  var (
    27  	k8sapi     = &k8sMock{}
    28  	alibabaAPI *mock.API
    29  	instances  *InstancesManager
    30  	metricsapi = metricsmock.NewMockMetrics()
    31  )
    32  
    33  func setup(tb testing.TB) {
    34  	tb.Helper()
    35  	limits.Update(map[string]ipamTypes.Limits{
    36  		"ecs.g7ne.large":    {Adapters: 3, IPv4: 10, IPv6: 0},
    37  		"ecs.g7ne.24xlarge": {Adapters: 15, IPv4: 50, IPv6: 0},
    38  		"ecs.g8m.small":     {Adapters: 2, IPv4: 3, IPv6: 0},
    39  	})
    40  
    41  	metricsapi = metricsmock.NewMockMetrics()
    42  	alibabaAPI = mock.NewAPI(subnets, vpcs, securityGroups)
    43  	require.NotNil(tb, alibabaAPI)
    44  	alibabaAPI.UpdateENIs(primaryENIs)
    45  	instances = NewInstancesManager(alibabaAPI)
    46  	require.NotNil(tb, instances)
    47  
    48  	tb.Cleanup(func() {
    49  		metricsapi = nil
    50  		alibabaAPI = nil
    51  		instances = nil
    52  	})
    53  }
    54  
    55  func TestGetMaximumAllocatableIPv4(t *testing.T) {
    56  	setup(t)
    57  
    58  	n := &Node{}
    59  	n.k8sObj = newCiliumNode("node", "i-1", "ecs.g7ne.24xlarge", "cn-hangzhou-i", "vpc-1")
    60  	require.Equal(t, n.GetMaximumAllocatableIPv4(), 700)
    61  }
    62  
    63  func TestCreateInterface(t *testing.T) {
    64  	setup(t)
    65  
    66  	alibabaAPI.UpdateENIs(primaryENIs)
    67  	instances.Resync(context.TODO())
    68  
    69  	mngr, err := ipam.NewNodeManager(instances, k8sapi, metricsapi, 10, false, false)
    70  	require.NoError(t, err)
    71  	require.NotNil(t, mngr)
    72  
    73  	mngr.Upsert(newCiliumNode("node1", "i-1", "ecs.g7ne.large", "cn-hangzhou-i", "vpc-1"))
    74  	mngr.Upsert(newCiliumNode("node2", "i-2", "ecs.g7ne.large", "cn-hangzhou-h", "vpc-1"))
    75  	names := mngr.GetNames()
    76  	require.Len(t, names, 2)
    77  
    78  	err = testutils.WaitUntil(func() bool {
    79  		return mngr.InstancesAPIIsReady()
    80  	}, 10*time.Second)
    81  	require.NoError(t, err)
    82  
    83  	instances.ForeachInstance("i-1", func(instanceID, interfaceID string, rev ipamTypes.InterfaceRevision) error {
    84  		e, ok := rev.Resource.(*eniTypes.ENI)
    85  		if !ok {
    86  			return fmt.Errorf("resource is not ENI type")
    87  		}
    88  		switch e.Type {
    89  		case eniTypes.ENITypeSecondary:
    90  			require.Equal(t, utils.GetENIIndexFromTags(e.Tags), 1)
    91  		case eniTypes.ENITypePrimary:
    92  			require.Equal(t, utils.GetENIIndexFromTags(e.Tags), 0)
    93  		}
    94  		return nil
    95  	})
    96  
    97  	toAlloc, _, err := mngr.Get("node1").Ops().CreateInterface(context.Background(), &ipam.AllocationAction{
    98  		IPv4: ipam.IPAllocationAction{
    99  			MaxIPsToAllocate: 10,
   100  		},
   101  		EmptyInterfaceSlots: 2,
   102  	}, log)
   103  	require.NoError(t, err)
   104  	require.Equal(t, toAlloc, 10)
   105  
   106  	toAlloc, _, err = mngr.Get("node1").Ops().CreateInterface(context.Background(), &ipam.AllocationAction{
   107  		IPv4: ipam.IPAllocationAction{
   108  			MaxIPsToAllocate: 11,
   109  		},
   110  		EmptyInterfaceSlots: 1,
   111  	}, log)
   112  	require.NoError(t, err)
   113  	require.Equal(t, toAlloc, 10)
   114  }
   115  
   116  func TestCandidateAndEmptyInterfaces(t *testing.T) {
   117  	setup(t)
   118  
   119  	alibabaAPI.UpdateENIs(primaryENIs)
   120  	instances.Resync(context.TODO())
   121  
   122  	mngr, err := ipam.NewNodeManager(instances, k8sapi, metricsapi, 10, false, false)
   123  	require.NoError(t, err)
   124  	require.NotNil(t, mngr)
   125  	// Set PreAllocate as 1
   126  	cn := newCiliumNodeWithIpamParams("node3", "i-3", "ecs.g8m.small", "cn-hangzhou-h", "vpc-1", 1, 0, 0)
   127  	cn.Spec.AlibabaCloud.VSwitches = []string{"vsw-2"}
   128  	mngr.Upsert(cn)
   129  
   130  	n := &Node{}
   131  	n.k8sObj = cn
   132  	// Primary ENI excluded, max allocatable = 3 ( 1 (ENI) * 3 (IPv4/ENI) )
   133  	require.Equal(t, n.GetMaximumAllocatableIPv4(), 3)
   134  
   135  	// Wait for IPs to become available
   136  	require.Eventually(t, func() bool { return reachedAddressesNeeded(mngr, "node3", 0) }, 5*time.Second, 1*time.Second)
   137  
   138  	node3 := mngr.Get("node3")
   139  	a, err := node3.Ops().PrepareIPAllocation(log)
   140  	require.NoError(t, err)
   141  	// 1 ENI attached, 1/3 IPs allocated, 0 empty slots left
   142  	require.Equal(t, a.IPv4.InterfaceCandidates, 1)
   143  	require.Equal(t, a.EmptyInterfaceSlots, 0)
   144  	require.Equal(t, 1, node3.Stats().IPv4.AvailableIPs)
   145  }
   146  
   147  func TestPrepareIPAllocation(t *testing.T) {
   148  	setup(t)
   149  
   150  	alibabaAPI.UpdateENIs(primaryENIs)
   151  	instances.Resync(context.TODO())
   152  
   153  	mngr, err := ipam.NewNodeManager(instances, k8sapi, metricsapi, 10, false, false)
   154  	require.NoError(t, err)
   155  	require.NotNil(t, mngr)
   156  	mngr.SetInstancesAPIReadiness(false) // to avoid the manager background jobs starting and racing us.
   157  
   158  	mngr.Upsert(newCiliumNode("node1", "i-1", "ecs.g7ne.large", "cn-hangzhou-i", "vpc-1"))
   159  	a, err := mngr.Get("node1").Ops().PrepareIPAllocation(log)
   160  	require.NoError(t, err)
   161  	require.Equal(t, 2, a.EmptyInterfaceSlots+a.IPv4.InterfaceCandidates, fmt.Sprintf("empty: %v, candidates: %v", a.EmptyInterfaceSlots, a.IPv4.InterfaceCandidates))
   162  
   163  	// create one eni
   164  	toAlloc, _, err := mngr.Get("node1").Ops().CreateInterface(context.Background(), &ipam.AllocationAction{
   165  		IPv4: ipam.IPAllocationAction{
   166  			MaxIPsToAllocate: 10,
   167  		},
   168  		EmptyInterfaceSlots: 2,
   169  	}, log)
   170  	require.NoError(t, err)
   171  	require.Equal(t, toAlloc, 10)
   172  
   173  	// one eni left
   174  	a, err = mngr.Get("node1").Ops().PrepareIPAllocation(log)
   175  	require.NoError(t, err)
   176  	require.Equal(t, 1, a.EmptyInterfaceSlots, fmt.Sprintf("empty: %v, candidates: %v", a.EmptyInterfaceSlots, a.IPv4.InterfaceCandidates))
   177  }
   178  
   179  func TestNode_allocENIIndex(t *testing.T) {
   180  	n := Node{enis: map[string]eniTypes.ENI{
   181  		"eni-1": {
   182  			InstanceID: "eni-1",
   183  			Type:       eniTypes.ENITypePrimary,
   184  			Tags:       nil,
   185  		},
   186  	}}
   187  	index, err := n.allocENIIndex()
   188  	require.NoError(t, err)
   189  	require.Equal(t, index, 1)
   190  
   191  	n.enis["eni-2"] = eniTypes.ENI{
   192  		InstanceID: "eni-2",
   193  		Type:       eniTypes.ENITypeSecondary,
   194  		Tags:       map[string]string{"cilium-eni-index": "1"},
   195  	}
   196  	index, err = n.allocENIIndex()
   197  	require.NoError(t, err)
   198  	require.Equal(t, index, 2)
   199  }
   200  
   201  type k8sMock struct{}
   202  
   203  func (k *k8sMock) Create(node *v2.CiliumNode) (*v2.CiliumNode, error) {
   204  	return nil, nil
   205  }
   206  
   207  func (k *k8sMock) Update(origNode, node *v2.CiliumNode) (*v2.CiliumNode, error) {
   208  	return nil, nil
   209  }
   210  
   211  func (k *k8sMock) UpdateStatus(origNode, node *v2.CiliumNode) (*v2.CiliumNode, error) {
   212  	return nil, nil
   213  }
   214  
   215  func (k *k8sMock) Get(node string) (*v2.CiliumNode, error) {
   216  	return &v2.CiliumNode{}, nil
   217  }
   218  
   219  func newCiliumNode(node, instanceID, instanceType, az, vpcID string) *v2.CiliumNode {
   220  	cn := &v2.CiliumNode{
   221  		ObjectMeta: metav1.ObjectMeta{Name: node, Namespace: "default"},
   222  		Spec: v2.NodeSpec{
   223  			InstanceID: instanceID,
   224  			AlibabaCloud: eniTypes.Spec{
   225  				InstanceType:     instanceType,
   226  				VPCID:            vpcID,
   227  				AvailabilityZone: az,
   228  			},
   229  			IPAM: ipamTypes.IPAMSpec{
   230  				Pool: ipamTypes.AllocationMap{},
   231  			},
   232  		},
   233  		Status: v2.NodeStatus{
   234  			IPAM: ipamTypes.IPAMStatus{
   235  				Used: ipamTypes.AllocationMap{},
   236  			},
   237  		},
   238  	}
   239  
   240  	return cn
   241  }
   242  
   243  func newCiliumNodeWithIpamParams(node, instanceID, instanceType, az, vpcID string, preAllocate, minAllocate, maxAllocate int) *v2.CiliumNode {
   244  	cn := &v2.CiliumNode{
   245  		ObjectMeta: metav1.ObjectMeta{Name: node, Namespace: "default"},
   246  		Spec: v2.NodeSpec{
   247  			InstanceID: instanceID,
   248  			AlibabaCloud: eniTypes.Spec{
   249  				InstanceType:     instanceType,
   250  				VPCID:            vpcID,
   251  				AvailabilityZone: az,
   252  			},
   253  			IPAM: ipamTypes.IPAMSpec{
   254  				Pool:        ipamTypes.AllocationMap{},
   255  				PreAllocate: preAllocate,
   256  				MinAllocate: minAllocate,
   257  				MaxAllocate: maxAllocate,
   258  			},
   259  		},
   260  		Status: v2.NodeStatus{
   261  			IPAM: ipamTypes.IPAMStatus{
   262  				Used: ipamTypes.AllocationMap{},
   263  			},
   264  		},
   265  	}
   266  
   267  	return cn
   268  }
   269  
   270  func reachedAddressesNeeded(mngr *ipam.NodeManager, nodeName string, needed int) (success bool) {
   271  	if node := mngr.Get(nodeName); node != nil {
   272  		success = node.GetNeededAddresses() == needed
   273  	}
   274  	return
   275  }