github.com/cilium/cilium@v1.16.2/pkg/clustermesh/mcsapi/service_controller_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package mcsapi 5 6 import ( 7 "context" 8 "testing" 9 10 "github.com/stretchr/testify/require" 11 corev1 "k8s.io/api/core/v1" 12 k8sApiErrors "k8s.io/apimachinery/pkg/api/errors" 13 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 "k8s.io/apimachinery/pkg/runtime" 15 "k8s.io/apimachinery/pkg/types" 16 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 17 clientgoscheme "k8s.io/client-go/kubernetes/scheme" 18 ctrl "sigs.k8s.io/controller-runtime" 19 "sigs.k8s.io/controller-runtime/pkg/client" 20 "sigs.k8s.io/controller-runtime/pkg/client/fake" 21 mcsapiv1alpha1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 22 mcsapicontrollers "sigs.k8s.io/mcs-api/pkg/controllers" 23 24 "github.com/cilium/cilium/pkg/annotation" 25 "github.com/cilium/cilium/pkg/logging" 26 ) 27 28 var ( 29 typeMetaSvcImport = metav1.TypeMeta{ 30 Kind: "ServiceImport", 31 APIVersion: mcsapiv1alpha1.GroupVersion.String(), 32 } 33 typeMetaSvcExport = metav1.TypeMeta{ 34 Kind: "ServiceExport", 35 APIVersion: mcsapiv1alpha1.GroupVersion.String(), 36 } 37 38 mcsFixtures = []client.Object{ 39 &mcsapiv1alpha1.ServiceExport{ 40 TypeMeta: typeMetaSvcExport, 41 ObjectMeta: metav1.ObjectMeta{ 42 Name: "full", 43 Namespace: "default", 44 }, 45 }, 46 &mcsapiv1alpha1.ServiceImport{ 47 TypeMeta: typeMetaSvcImport, 48 ObjectMeta: metav1.ObjectMeta{ 49 Name: "full", 50 Namespace: "default", 51 Annotations: map[string]string{ 52 annotation.SharedService: "not-used", 53 annotation.GlobalService: "not-used", 54 "test-annotation": "copied", 55 }, 56 Labels: map[string]string{ 57 mcsapiv1alpha1.LabelSourceCluster: "not-used", 58 mcsapiv1alpha1.LabelServiceName: "not-used", 59 "test-label": "copied", 60 }, 61 }, 62 Spec: mcsapiv1alpha1.ServiceImportSpec{ 63 Ports: []mcsapiv1alpha1.ServicePort{{ 64 Name: "my-port-1", 65 }}, 66 }, 67 }, 68 &corev1.Service{ 69 ObjectMeta: metav1.ObjectMeta{ 70 Name: "full", 71 Namespace: "default", 72 }, 73 Spec: corev1.ServiceSpec{ 74 Selector: map[string]string{ 75 "selector": "value", 76 }, 77 Ports: []corev1.ServicePort{{ 78 Name: "not-used", 79 }}, 80 }, 81 }, 82 83 &mcsapiv1alpha1.ServiceExport{ 84 TypeMeta: typeMetaSvcExport, 85 ObjectMeta: metav1.ObjectMeta{ 86 Name: "full-update", 87 Namespace: "default", 88 }, 89 }, 90 &mcsapiv1alpha1.ServiceImport{ 91 TypeMeta: typeMetaSvcImport, 92 ObjectMeta: metav1.ObjectMeta{ 93 Name: "full-update", 94 Namespace: "default", 95 Annotations: map[string]string{ 96 "test-annotation": "copied", 97 }, 98 Labels: map[string]string{ 99 "test-label": "copied", 100 }, 101 }, 102 Spec: mcsapiv1alpha1.ServiceImportSpec{ 103 Ports: []mcsapiv1alpha1.ServicePort{{ 104 Name: "my-port-1", 105 }}, 106 }, 107 }, 108 &corev1.Service{ 109 ObjectMeta: metav1.ObjectMeta{ 110 Name: "full-update", 111 Namespace: "default", 112 }, 113 Spec: corev1.ServiceSpec{ 114 Selector: map[string]string{ 115 "selector": "value", 116 }, 117 }, 118 }, 119 &corev1.Service{ 120 ObjectMeta: metav1.ObjectMeta{ 121 Name: derivedName(types.NamespacedName{Name: "full-update", Namespace: "default"}), 122 Namespace: "default", 123 }, 124 }, 125 126 &mcsapiv1alpha1.ServiceImport{ 127 TypeMeta: typeMetaSvcImport, 128 ObjectMeta: metav1.ObjectMeta{ 129 Name: "import-only", 130 Namespace: "default", 131 Annotations: map[string]string{ 132 annotation.SharedService: "not-used", 133 annotation.GlobalService: "not-used", 134 }, 135 Labels: map[string]string{ 136 mcsapiv1alpha1.LabelSourceCluster: "not-used", 137 }, 138 }, 139 Spec: mcsapiv1alpha1.ServiceImportSpec{ 140 Ports: []mcsapiv1alpha1.ServicePort{{ 141 Name: "my-port-2", 142 }}, 143 }, 144 }, 145 146 &mcsapiv1alpha1.ServiceImport{ 147 TypeMeta: typeMetaSvcImport, 148 ObjectMeta: metav1.ObjectMeta{ 149 Name: "import-and-local", 150 Namespace: "default", 151 }, 152 Spec: mcsapiv1alpha1.ServiceImportSpec{ 153 Ports: []mcsapiv1alpha1.ServicePort{{ 154 Name: "my-port-2", 155 }}, 156 }, 157 }, 158 &corev1.Service{ 159 ObjectMeta: metav1.ObjectMeta{ 160 Name: "import-and-local", 161 Namespace: "default", 162 }, 163 Spec: corev1.ServiceSpec{ 164 Selector: map[string]string{ 165 "selector": "value", 166 }, 167 }, 168 }, 169 170 &mcsapiv1alpha1.ServiceExport{ 171 TypeMeta: typeMetaSvcExport, 172 ObjectMeta: metav1.ObjectMeta{ 173 Name: "export-only", 174 Namespace: "default", 175 }, 176 }, 177 &corev1.Service{ 178 ObjectMeta: metav1.ObjectMeta{ 179 Name: "export-only", 180 Namespace: "default", 181 }, 182 Spec: corev1.ServiceSpec{ 183 Ports: []corev1.ServicePort{{ 184 Name: "my-port-3", 185 }}, 186 ClusterIP: corev1.ClusterIPNone, 187 }, 188 }, 189 190 &mcsapiv1alpha1.ServiceExport{ 191 TypeMeta: typeMetaSvcExport, 192 ObjectMeta: metav1.ObjectMeta{ 193 Name: "export-no-svc", 194 Namespace: "default", 195 }, 196 }, 197 198 &mcsapiv1alpha1.ServiceImport{ 199 TypeMeta: typeMetaSvcImport, 200 ObjectMeta: metav1.ObjectMeta{ 201 Name: "switch-to-headless", 202 Namespace: "default", 203 }, 204 Spec: mcsapiv1alpha1.ServiceImportSpec{ 205 Type: mcsapiv1alpha1.Headless, 206 }, 207 }, 208 &corev1.Service{ 209 ObjectMeta: metav1.ObjectMeta{ 210 Name: derivedName(types.NamespacedName{Name: "switch-to-headless", Namespace: "default"}), 211 Namespace: "default", 212 }, 213 }, 214 } 215 ) 216 217 func testScheme() *runtime.Scheme { 218 scheme := runtime.NewScheme() 219 utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 220 utilruntime.Must(mcsapiv1alpha1.AddToScheme(scheme)) 221 return scheme 222 } 223 224 func Test_httpRouteReconciler_Reconcile(t *testing.T) { 225 c := fake.NewClientBuilder(). 226 WithObjects(mcsFixtures...). 227 WithScheme(testScheme()). 228 Build() 229 r := &mcsAPIServiceReconciler{ 230 Client: c, 231 Logger: logging.DefaultLogger, 232 clusterName: "cluster1", 233 } 234 235 t.Run("Test service creation/update with export and import", func(t *testing.T) { 236 for _, name := range []string{"full", "full-update"} { 237 key := types.NamespacedName{ 238 Name: name, 239 Namespace: "default", 240 } 241 result, err := r.Reconcile(context.Background(), ctrl.Request{ 242 NamespacedName: key, 243 }) 244 245 require.NoError(t, err) 246 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 247 248 keyDerived := types.NamespacedName{ 249 Name: derivedName(key), 250 Namespace: key.Namespace, 251 } 252 svc := &corev1.Service{} 253 err = c.Get(context.Background(), keyDerived, svc) 254 require.NoError(t, err) 255 256 require.Len(t, svc.OwnerReferences, 2) 257 258 require.Equal(t, "cluster1", svc.Labels[mcsapiv1alpha1.LabelSourceCluster]) 259 require.Equal(t, key.Name, svc.Labels[mcsapiv1alpha1.LabelServiceName]) 260 require.Equal(t, "copied", svc.Labels["test-label"]) 261 262 require.Equal(t, "true", svc.Annotations[annotation.GlobalService]) 263 require.Equal(t, "true", svc.Annotations[annotation.SharedService]) 264 require.Equal(t, "copied", svc.Annotations["test-annotation"]) 265 266 require.Len(t, svc.Spec.Ports, 1) 267 require.Equal(t, "my-port-1", svc.Spec.Ports[0].Name) 268 269 require.Equal(t, "value", svc.Spec.Selector["selector"]) 270 271 svcImport := &mcsapiv1alpha1.ServiceImport{} 272 err = c.Get(context.Background(), key, svcImport) 273 require.NoError(t, err) 274 require.Equal(t, keyDerived.Name, svcImport.Annotations[mcsapicontrollers.DerivedServiceAnnotation]) 275 } 276 }) 277 278 t.Run("Test service creation with only import", func(t *testing.T) { 279 key := types.NamespacedName{ 280 Name: "import-only", 281 Namespace: "default", 282 } 283 result, err := r.Reconcile(context.Background(), ctrl.Request{ 284 NamespacedName: key, 285 }) 286 287 require.NoError(t, err) 288 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 289 290 keyDerived := types.NamespacedName{ 291 Name: derivedName(key), 292 Namespace: key.Namespace, 293 } 294 svc := &corev1.Service{} 295 err = c.Get(context.Background(), keyDerived, svc) 296 require.NoError(t, err) 297 298 require.Len(t, svc.OwnerReferences, 1) 299 require.Equal(t, "ServiceImport", svc.OwnerReferences[0].Kind) 300 301 require.Nil(t, svc.Spec.Selector) 302 303 require.Equal(t, "cluster1", svc.Labels[mcsapiv1alpha1.LabelSourceCluster]) 304 305 require.Equal(t, "true", svc.Annotations[annotation.GlobalService]) 306 require.Equal(t, "false", svc.Annotations[annotation.SharedService]) 307 308 require.Len(t, svc.Spec.Ports, 1) 309 require.Equal(t, "my-port-2", svc.Spec.Ports[0].Name) 310 }) 311 312 t.Run("Test service creation with import and local svc", func(t *testing.T) { 313 key := types.NamespacedName{ 314 Name: "import-and-local", 315 Namespace: "default", 316 } 317 result, err := r.Reconcile(context.Background(), ctrl.Request{ 318 NamespacedName: key, 319 }) 320 321 require.NoError(t, err) 322 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 323 324 keyDerived := types.NamespacedName{ 325 Name: derivedName(key), 326 Namespace: key.Namespace, 327 } 328 svc := &corev1.Service{} 329 err = c.Get(context.Background(), keyDerived, svc) 330 require.NoError(t, err) 331 332 require.Equal(t, "value", svc.Spec.Selector["selector"]) 333 }) 334 335 t.Run("Test service creation with only export", func(t *testing.T) { 336 key := types.NamespacedName{ 337 Name: "export-only", 338 Namespace: "default", 339 } 340 result, err := r.Reconcile(context.Background(), ctrl.Request{ 341 NamespacedName: key, 342 }) 343 344 require.NoError(t, err) 345 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 346 347 keyDerived := types.NamespacedName{ 348 Name: derivedName(key), 349 Namespace: key.Namespace, 350 } 351 svc := &corev1.Service{} 352 err = c.Get(context.Background(), keyDerived, svc) 353 require.NoError(t, err) 354 355 require.Len(t, svc.OwnerReferences, 1) 356 require.Equal(t, "ServiceExport", svc.OwnerReferences[0].Kind) 357 358 require.Equal(t, "true", svc.Annotations[annotation.GlobalService]) 359 require.Equal(t, "true", svc.Annotations[annotation.SharedService]) 360 361 require.Len(t, svc.Spec.Ports, 1) 362 require.Equal(t, "my-port-3", svc.Spec.Ports[0].Name) 363 364 require.Equal(t, corev1.ClusterIPNone, svc.Spec.ClusterIP) 365 }) 366 367 t.Run("Test service creation with export but no exported service", func(t *testing.T) { 368 key := types.NamespacedName{ 369 Name: "export-no-svc", 370 Namespace: "default", 371 } 372 result, err := r.Reconcile(context.Background(), ctrl.Request{ 373 NamespacedName: key, 374 }) 375 376 require.True(t, k8sApiErrors.IsNotFound(err), "Should return not found error") 377 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 378 }) 379 380 t.Run("Test service recreation to headless service", func(t *testing.T) { 381 key := types.NamespacedName{ 382 Name: "switch-to-headless", 383 Namespace: "default", 384 } 385 result, err := r.Reconcile(context.Background(), ctrl.Request{ 386 NamespacedName: key, 387 }) 388 389 require.NoError(t, err) 390 require.Equal(t, ctrl.Result{}, result, "Result should be empty") 391 392 keyDerived := types.NamespacedName{ 393 Name: derivedName(key), 394 Namespace: key.Namespace, 395 } 396 svc := &corev1.Service{} 397 err = c.Get(context.Background(), keyDerived, svc) 398 require.NoError(t, err) 399 400 require.Equal(t, corev1.ClusterIPNone, svc.Spec.ClusterIP) 401 }) 402 }