github.com/cilium/cilium@v1.16.2/pkg/clustermesh/endpointslicesync/endpointslice_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package endpointslicesync
     5  
     6  import (
     7  	"context"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/cilium/hive/cell"
    12  	"github.com/cilium/hive/hivetest"
    13  	"github.com/sirupsen/logrus"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  	corev1 "k8s.io/api/core/v1"
    17  	discovery "k8s.io/api/discovery/v1"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  	cache "k8s.io/client-go/tools/cache"
    20  	mcsapiv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1"
    21  
    22  	"github.com/cilium/cilium/operator/k8s"
    23  	"github.com/cilium/cilium/pkg/annotation"
    24  	"github.com/cilium/cilium/pkg/clustermesh/common"
    25  	"github.com/cilium/cilium/pkg/hive"
    26  	k8sClient "github.com/cilium/cilium/pkg/k8s/client"
    27  	"github.com/cilium/cilium/pkg/k8s/resource"
    28  	slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1"
    29  	slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
    30  	"github.com/cilium/cilium/pkg/k8s/utils"
    31  	"github.com/cilium/cilium/pkg/loadbalancer"
    32  	"github.com/cilium/cilium/pkg/metrics/metric"
    33  	"github.com/cilium/cilium/pkg/service/store"
    34  )
    35  
    36  const (
    37  	remoteClusterName string = "cluster-1"
    38  	globalSvcIP       string = "42.42.42.42"
    39  )
    40  
    41  func createService(name string) *slim_corev1.Service {
    42  	return &slim_corev1.Service{
    43  		ObjectMeta: slim_metav1.ObjectMeta{
    44  			Name:      name,
    45  			Namespace: name,
    46  			Annotations: map[string]string{
    47  				annotation.GlobalService:                   "true",
    48  				annotation.GlobalServiceSyncEndpointSlices: "true",
    49  			},
    50  		},
    51  		Spec: slim_corev1.ServiceSpec{
    52  			Selector:   map[string]string{"test": "test"},
    53  			IPFamilies: []slim_corev1.IPFamily{slim_corev1.IPv4Protocol},
    54  		},
    55  	}
    56  }
    57  
    58  func createGlobalService(
    59  	globalService *common.GlobalServiceCache,
    60  	podInformer *meshPodInformer,
    61  	svcName string,
    62  	updateClusterSvc func(*store.ClusterService)) *store.ClusterService {
    63  	clusterSvc := &store.ClusterService{
    64  		Cluster:   remoteClusterName,
    65  		Namespace: svcName,
    66  		Name:      svcName,
    67  		Backends: map[string]store.PortConfiguration{
    68  			globalSvcIP: {"port-name": &loadbalancer.L4Addr{Protocol: loadbalancer.TCP, Port: 42}},
    69  		},
    70  		Shared:    true,
    71  		ClusterID: 1,
    72  	}
    73  	if updateClusterSvc != nil {
    74  		updateClusterSvc(clusterSvc)
    75  	}
    76  	globalService.OnUpdate(clusterSvc)
    77  	// We manually call the rest of the informer for convenience
    78  	podInformer.onClusterServiceUpdate(clusterSvc)
    79  	return clusterSvc
    80  }
    81  
    82  func getEndpointSlice(clientset k8sClient.Clientset, svcName string) (*discovery.EndpointSliceList, error) {
    83  	labelSelector := metav1.LabelSelector{MatchLabels: map[string]string{
    84  		discovery.LabelServiceName: svcName,
    85  		discovery.LabelManagedBy:   utils.EndpointSliceMeshControllerName,
    86  	}}
    87  	return clientset.DiscoveryV1().EndpointSlices(svcName).
    88  		List(context.Background(), metav1.ListOptions{LabelSelector: metav1.FormatLabelSelector(&labelSelector)})
    89  }
    90  
    91  func Test_meshEndpointSlice_Reconcile(t *testing.T) {
    92  	var fakeClient k8sClient.FakeClientset
    93  	var services resource.Resource[*slim_corev1.Service]
    94  	logger := logrus.New()
    95  	hive := hive.New(
    96  		k8sClient.FakeClientCell,
    97  		k8s.ResourcesCell,
    98  		cell.Invoke(func(
    99  			c *k8sClient.FakeClientset,
   100  			svc resource.Resource[*slim_corev1.Service],
   101  		) error {
   102  			fakeClient = *c
   103  			services = svc
   104  			return nil
   105  		}),
   106  	)
   107  	tlog := hivetest.Logger(t)
   108  	err := hive.Start(tlog, context.Background())
   109  	if err != nil {
   110  		t.Fatal(err)
   111  	}
   112  	defer hive.Stop(tlog, context.Background())
   113  
   114  	globalService := common.NewGlobalServiceCache(metric.NewGauge(metric.GaugeOpts{}))
   115  	podInformer := newMeshPodInformer(logger, globalService)
   116  	nodeInformer := newMeshNodeInformer(logger)
   117  	controller, serviceInformer, endpointsliceInformer := newEndpointSliceMeshController(
   118  		context.Background(), logger,
   119  		EndpointSliceSyncConfig{ClusterMeshMaxEndpointsPerSlice: 100},
   120  		podInformer, nodeInformer, &fakeClient, services, globalService,
   121  	)
   122  	endpointsliceInformer.Start(context.Background().Done())
   123  	go serviceInformer.Start(context.Background())
   124  	cache.WaitForCacheSync(context.Background().Done(), serviceInformer.HasSynced)
   125  
   126  	go controller.Run(context.Background(), 1)
   127  	nodeInformer.onClusterAdd(remoteClusterName)
   128  
   129  	svcStore, _ := services.Store(context.Background())
   130  
   131  	tick := 10 * time.Millisecond
   132  	timeout := 200 * time.Millisecond
   133  
   134  	t.Run("Create service then global service", func(t *testing.T) {
   135  		svcName := "local-svc-global-svc"
   136  		hostname := svcName + "-0"
   137  		svc1 := createService(svcName)
   138  		svcStore.CacheStore().Add(svc1)
   139  		serviceInformer.refreshAllCluster(svc1)
   140  		createGlobalService(globalService, podInformer, svcName, func(clusterSvc *store.ClusterService) {
   141  			clusterSvc.Hostnames = map[string]string{globalSvcIP: hostname}
   142  		})
   143  
   144  		var epList *discovery.EndpointSliceList
   145  		require.EventuallyWithT(t, func(c *assert.CollectT) {
   146  			epList, err = getEndpointSlice(&fakeClient, svcName)
   147  			assert.NoError(c, err)
   148  			assert.Len(c, epList.Items, 1)
   149  		}, timeout, tick, "endpointslice is not reconciled correctly")
   150  
   151  		require.Equal(t, map[string]string{
   152  			discovery.LabelServiceName:        svcName,
   153  			discovery.LabelManagedBy:          utils.EndpointSliceMeshControllerName,
   154  			mcsapiv1alpha1.LabelSourceCluster: remoteClusterName,
   155  			corev1.IsHeadlessService:          "",
   156  		}, epList.Items[0].Labels)
   157  		require.Len(t, epList.Items[0].Endpoints, 1)
   158  
   159  		require.NotNil(t, epList.Items[0].Endpoints[0].Hostname)
   160  		require.Equal(t, hostname, *epList.Items[0].Endpoints[0].Hostname)
   161  
   162  		require.Len(t, epList.Items[0].OwnerReferences, 1)
   163  		require.Equal(t, "v1", epList.Items[0].OwnerReferences[0].APIVersion)
   164  		require.Equal(t, "Service", epList.Items[0].OwnerReferences[0].Kind)
   165  		require.Equal(t, "local-svc-global-svc", epList.Items[0].OwnerReferences[0].Name)
   166  	})
   167  
   168  	t.Run("Create global service then service", func(t *testing.T) {
   169  		svcName := "global-svc-local-svc"
   170  		createGlobalService(globalService, podInformer, svcName, nil)
   171  		svc1 := createService(svcName)
   172  		svcStore.CacheStore().Add(svc1)
   173  		serviceInformer.refreshAllCluster(svc1)
   174  
   175  		var epList *discovery.EndpointSliceList
   176  		require.EventuallyWithT(t, func(c *assert.CollectT) {
   177  			epList, err = getEndpointSlice(&fakeClient, svcName)
   178  			assert.NoError(c, err)
   179  			assert.Len(c, epList.Items, 1)
   180  		}, timeout, tick, "endpointslice is not reconciled correctly")
   181  
   182  		require.Equal(t, map[string]string{
   183  			discovery.LabelServiceName:        svcName,
   184  			discovery.LabelManagedBy:          utils.EndpointSliceMeshControllerName,
   185  			mcsapiv1alpha1.LabelSourceCluster: remoteClusterName,
   186  			corev1.IsHeadlessService:          "",
   187  		}, epList.Items[0].Labels)
   188  	})
   189  
   190  	t.Run("Create headless service and global service", func(t *testing.T) {
   191  		svcName := "local-svc-headless-global-svc"
   192  		svc1 := createService(svcName)
   193  		svc1.Spec.ClusterIP = corev1.ClusterIPNone
   194  
   195  		svcStore.CacheStore().Add(svc1)
   196  		serviceInformer.refreshAllCluster(svc1)
   197  		createGlobalService(globalService, podInformer, svcName, nil)
   198  
   199  		require.EventuallyWithT(t, func(c *assert.CollectT) {
   200  			epList, err := getEndpointSlice(&fakeClient, svcName)
   201  			assert.NoError(c, err)
   202  			assert.Len(c, epList.Items, 1)
   203  		}, timeout, tick, "endpointslice is not reconciled correctly")
   204  	})
   205  
   206  	t.Run("Create service with global annotation and remove it", func(t *testing.T) {
   207  		svcName := "svc-remove-annotation"
   208  		createGlobalService(globalService, podInformer, svcName, nil)
   209  		svc1 := createService(svcName)
   210  		svcStore.CacheStore().Add(svc1)
   211  		serviceInformer.refreshAllCluster(svc1)
   212  
   213  		var epList *discovery.EndpointSliceList
   214  		require.EventuallyWithT(t, func(c *assert.CollectT) {
   215  			// Make sure that we have 1 endpointslice
   216  			epList, err = getEndpointSlice(&fakeClient, svcName)
   217  			assert.NoError(c, err)
   218  			assert.Len(c, epList.Items, 1)
   219  		}, timeout, tick, "endpointslice is not reconciled correctly")
   220  
   221  		svc1.Annotations[annotation.GlobalServiceSyncEndpointSlices] = "false"
   222  		svcStore.CacheStore().Update(svc1)
   223  		serviceInformer.refreshAllCluster(svc1)
   224  
   225  		require.EventuallyWithT(t, func(c *assert.CollectT) {
   226  			epList, err := getEndpointSlice(&fakeClient, svcName)
   227  			assert.NoError(c, err)
   228  			assert.Len(c, epList.Items, 0)
   229  		}, timeout, tick, "endpointslice is not reconciled correctly")
   230  	})
   231  
   232  	t.Run("Create service and global service and then delete global svc", func(t *testing.T) {
   233  		svcName := "local-svc-no-global-svc"
   234  		clusterSvc := createGlobalService(globalService, podInformer, svcName, nil)
   235  		svc1 := createService(svcName)
   236  		svcStore.CacheStore().Add(svc1)
   237  		serviceInformer.refreshAllCluster(svc1)
   238  
   239  		var epList *discovery.EndpointSliceList
   240  		require.EventuallyWithT(t, func(c *assert.CollectT) {
   241  			// Make sure that we have 1 endpointslice
   242  			epList, err = getEndpointSlice(&fakeClient, svcName)
   243  			assert.NoError(c, err)
   244  			assert.Len(c, epList.Items, 1)
   245  		}, timeout, tick, "endpointslice is not reconciled correctly")
   246  
   247  		globalService.OnDelete(clusterSvc)
   248  		// We manually call the rest of the informer for convenience
   249  		podInformer.onClusterServiceDelete(clusterSvc)
   250  
   251  		require.EventuallyWithT(t, func(c *assert.CollectT) {
   252  			epList, err := getEndpointSlice(&fakeClient, svcName)
   253  			assert.NoError(c, err)
   254  			assert.Len(c, epList.Items, 0)
   255  		}, timeout, tick, "endpointslice is not reconciled correctly")
   256  	})
   257  }