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 }