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  }