github.com/cilium/cilium@v1.16.2/operator/pkg/nodeipam/nodesvclb_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package nodeipam
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/stretchr/testify/require"
    13  	corev1 "k8s.io/api/core/v1"
    14  	discoveryv1 "k8s.io/api/discovery/v1"
    15  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    16  	"k8s.io/apimachinery/pkg/types"
    17  	ctrl "sigs.k8s.io/controller-runtime"
    18  	"sigs.k8s.io/controller-runtime/pkg/client"
    19  	"sigs.k8s.io/controller-runtime/pkg/client/fake"
    20  
    21  	"github.com/cilium/cilium/pkg/logging"
    22  )
    23  
    24  var (
    25  	nodeSvcLbFixtures = []client.Object{
    26  		&corev1.Node{
    27  			ObjectMeta: metav1.ObjectMeta{
    28  				Name: "node-1",
    29  			},
    30  			Status: corev1.NodeStatus{
    31  				Addresses: []corev1.NodeAddress{
    32  					{Type: corev1.NodeInternalIP, Address: "10.0.0.1"},
    33  					{Type: corev1.NodeExternalIP, Address: "2001:0000::1"},
    34  				},
    35  			},
    36  		},
    37  		&corev1.Node{
    38  			ObjectMeta: metav1.ObjectMeta{
    39  				Name: "node-2",
    40  			},
    41  			Status: corev1.NodeStatus{
    42  				Addresses: []corev1.NodeAddress{
    43  					{Type: corev1.NodeInternalIP, Address: "fc00::2"},
    44  					{Type: corev1.NodeExternalIP, Address: "42.0.0.2"},
    45  				},
    46  			},
    47  		},
    48  		&corev1.Node{
    49  			ObjectMeta: metav1.ObjectMeta{
    50  				Name: "node-3",
    51  			},
    52  			Status: corev1.NodeStatus{
    53  				Addresses: []corev1.NodeAddress{
    54  					{Type: corev1.NodeExternalIP, Address: "2001:0000::3"},
    55  					{Type: corev1.NodeExternalIP, Address: "42.0.0.3"},
    56  				},
    57  			},
    58  		},
    59  
    60  		&corev1.Node{
    61  			ObjectMeta: metav1.ObjectMeta{
    62  				Name:              "node-4-excluded",
    63  				DeletionTimestamp: &metav1.Time{Time: time.Now()},
    64  				Finalizers:        []string{"myfinalizer"},
    65  			},
    66  			Status: corev1.NodeStatus{
    67  				Addresses: []corev1.NodeAddress{
    68  					{Type: corev1.NodeExternalIP, Address: "2001:0000:4"},
    69  					{Type: corev1.NodeExternalIP, Address: "42.0.0.4"},
    70  				},
    71  			},
    72  		},
    73  		&corev1.Node{
    74  			ObjectMeta: metav1.ObjectMeta{
    75  				Name: "node-5-excluded",
    76  				Labels: map[string]string{
    77  					corev1.LabelNodeExcludeBalancers: "",
    78  				},
    79  			},
    80  			Status: corev1.NodeStatus{
    81  				Addresses: []corev1.NodeAddress{
    82  					{Type: corev1.NodeExternalIP, Address: "2001:0000:5"},
    83  					{Type: corev1.NodeExternalIP, Address: "42.0.0.5"},
    84  				},
    85  			},
    86  		},
    87  		&corev1.Node{
    88  			ObjectMeta: metav1.ObjectMeta{
    89  				Name: "node-6-excluded",
    90  			},
    91  			Spec: corev1.NodeSpec{
    92  				Taints: []corev1.Taint{
    93  					{Key: toBeDeletedTaint},
    94  				},
    95  			},
    96  			Status: corev1.NodeStatus{
    97  				Addresses: []corev1.NodeAddress{
    98  					{Type: corev1.NodeExternalIP, Address: "2001:0000:6"},
    99  					{Type: corev1.NodeExternalIP, Address: "42.0.0.6"},
   100  				},
   101  			},
   102  		},
   103  
   104  		&discoveryv1.EndpointSlice{
   105  			ObjectMeta: metav1.ObjectMeta{
   106  				Name:      "ipv4-internal",
   107  				Namespace: "default",
   108  				Labels:    map[string]string{discoveryv1.LabelServiceName: "ipv4-internal"},
   109  			},
   110  			Endpoints: []discoveryv1.Endpoint{
   111  				{NodeName: stringPtr("node-1")},
   112  				{NodeName: stringPtr("node-2"), Conditions: discoveryv1.EndpointConditions{Ready: boolPtr(false)}},
   113  			},
   114  		},
   115  		&corev1.Service{
   116  			ObjectMeta: metav1.ObjectMeta{
   117  				Name:      "ipv4-internal",
   118  				Namespace: "default",
   119  			},
   120  			Spec: corev1.ServiceSpec{
   121  				Type:                  corev1.ServiceTypeLoadBalancer,
   122  				IPFamilies:            []corev1.IPFamily{corev1.IPv4Protocol},
   123  				LoadBalancerClass:     &nodeSvcLBClass,
   124  				ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal,
   125  			},
   126  		},
   127  
   128  		&discoveryv1.EndpointSlice{
   129  			ObjectMeta: metav1.ObjectMeta{
   130  				Name:      "ipv4-external",
   131  				Namespace: "default",
   132  				Labels:    map[string]string{discoveryv1.LabelServiceName: "ipv4-external"},
   133  			},
   134  			Endpoints: []discoveryv1.Endpoint{
   135  				{NodeName: stringPtr("node-1")},
   136  				{NodeName: stringPtr("node-2")},
   137  			},
   138  		},
   139  		&corev1.Service{
   140  			ObjectMeta: metav1.ObjectMeta{
   141  				Name:      "ipv4-external",
   142  				Namespace: "default",
   143  			},
   144  			Spec: corev1.ServiceSpec{
   145  				Type:                  corev1.ServiceTypeLoadBalancer,
   146  				IPFamilies:            []corev1.IPFamily{corev1.IPv4Protocol},
   147  				LoadBalancerClass:     &nodeSvcLBClass,
   148  				ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal,
   149  			},
   150  		},
   151  
   152  		&discoveryv1.EndpointSlice{
   153  			ObjectMeta: metav1.ObjectMeta{
   154  				Name:      "ipv6-internal",
   155  				Namespace: "default",
   156  				Labels:    map[string]string{discoveryv1.LabelServiceName: "ipv6-internal"},
   157  			},
   158  			Endpoints: []discoveryv1.Endpoint{
   159  				{NodeName: stringPtr("node-2")},
   160  			},
   161  		},
   162  		&corev1.Service{
   163  			ObjectMeta: metav1.ObjectMeta{
   164  				Name:      "ipv6-internal",
   165  				Namespace: "default",
   166  			},
   167  			Spec: corev1.ServiceSpec{
   168  				Type:                  corev1.ServiceTypeLoadBalancer,
   169  				IPFamilies:            []corev1.IPFamily{corev1.IPv6Protocol},
   170  				LoadBalancerClass:     &nodeSvcLBClass,
   171  				ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal,
   172  			},
   173  		},
   174  
   175  		&discoveryv1.EndpointSlice{
   176  			ObjectMeta: metav1.ObjectMeta{
   177  				Name:      "ipv6-external",
   178  				Namespace: "default",
   179  				Labels:    map[string]string{discoveryv1.LabelServiceName: "ipv6-external"},
   180  			},
   181  			Endpoints: []discoveryv1.Endpoint{
   182  				{NodeName: stringPtr("node-1")},
   183  				{NodeName: stringPtr("node-2")},
   184  			},
   185  		},
   186  		&corev1.Service{
   187  			ObjectMeta: metav1.ObjectMeta{
   188  				Name:      "ipv6-external",
   189  				Namespace: "default",
   190  			},
   191  			Spec: corev1.ServiceSpec{
   192  				Type:                  corev1.ServiceTypeLoadBalancer,
   193  				IPFamilies:            []corev1.IPFamily{corev1.IPv6Protocol},
   194  				LoadBalancerClass:     &nodeSvcLBClass,
   195  				ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal,
   196  			},
   197  		},
   198  
   199  		&discoveryv1.EndpointSlice{
   200  			ObjectMeta: metav1.ObjectMeta{
   201  				Name:      "dualstack-external",
   202  				Namespace: "default",
   203  				Labels:    map[string]string{discoveryv1.LabelServiceName: "dualstack-external"},
   204  			},
   205  			Endpoints: []discoveryv1.Endpoint{
   206  				{NodeName: stringPtr("node-1")},
   207  				{NodeName: stringPtr("node-2")},
   208  				{NodeName: stringPtr("does-not-exist")},
   209  			},
   210  		},
   211  		&corev1.Service{
   212  			ObjectMeta: metav1.ObjectMeta{
   213  				Name:      "dualstack-external",
   214  				Namespace: "default",
   215  			},
   216  			Spec: corev1.ServiceSpec{
   217  				Type:                  corev1.ServiceTypeLoadBalancer,
   218  				IPFamilies:            []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol},
   219  				LoadBalancerClass:     &nodeSvcLBClass,
   220  				ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal,
   221  			},
   222  		},
   223  
   224  		&corev1.Service{
   225  			ObjectMeta: metav1.ObjectMeta{
   226  				Name:      "etp-cluster",
   227  				Namespace: "default",
   228  			},
   229  			Spec: corev1.ServiceSpec{
   230  				Type:                  corev1.ServiceTypeLoadBalancer,
   231  				IPFamilies:            []corev1.IPFamily{corev1.IPv4Protocol},
   232  				LoadBalancerClass:     &nodeSvcLBClass,
   233  				ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyCluster,
   234  			},
   235  		},
   236  
   237  		&discoveryv1.EndpointSlice{
   238  			ObjectMeta: metav1.ObjectMeta{
   239  				Name:      "not-supported-1",
   240  				Namespace: "default",
   241  				Labels:    map[string]string{discoveryv1.LabelServiceName: "not-supported-1"},
   242  			},
   243  			Endpoints: []discoveryv1.Endpoint{
   244  				{NodeName: stringPtr("node-1")},
   245  				{NodeName: stringPtr("node-2")},
   246  			},
   247  		},
   248  		&corev1.Service{
   249  			ObjectMeta: metav1.ObjectMeta{
   250  				Name:      "not-supported-1",
   251  				Namespace: "default",
   252  			},
   253  			Spec: corev1.ServiceSpec{
   254  				Type:       corev1.ServiceTypeLoadBalancer,
   255  				IPFamilies: []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol},
   256  			},
   257  			Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{
   258  				Ingress: []corev1.LoadBalancerIngress{{IP: "100.100.100.100"}},
   259  			}},
   260  		},
   261  
   262  		&discoveryv1.EndpointSlice{
   263  			ObjectMeta: metav1.ObjectMeta{
   264  				Name:      "not-supported-2",
   265  				Namespace: "default",
   266  				Labels:    map[string]string{discoveryv1.LabelServiceName: "not-supported-2"},
   267  			},
   268  			Endpoints: []discoveryv1.Endpoint{
   269  				{NodeName: stringPtr("node-1")},
   270  				{NodeName: stringPtr("node-2")},
   271  			},
   272  		},
   273  		&corev1.Service{
   274  			ObjectMeta: metav1.ObjectMeta{
   275  				Name:      "not-supported-2",
   276  				Namespace: "default",
   277  			},
   278  			Spec: corev1.ServiceSpec{
   279  				IPFamilies:            []corev1.IPFamily{corev1.IPv4Protocol, corev1.IPv6Protocol},
   280  				LoadBalancerClass:     &nodeSvcLBClass,
   281  				ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyLocal,
   282  			},
   283  			Status: corev1.ServiceStatus{LoadBalancer: corev1.LoadBalancerStatus{
   284  				Ingress: []corev1.LoadBalancerIngress{{IP: "100.100.100.100"}},
   285  			}},
   286  		},
   287  	}
   288  
   289  	nodeSvcLabelFixtures = []client.Object{
   290  		&corev1.Node{
   291  			ObjectMeta: metav1.ObjectMeta{
   292  				Name:   "node-1",
   293  				Labels: map[string]string{"ingress-ready": "true", "all": "true", "group": "first"},
   294  			},
   295  			Status: corev1.NodeStatus{
   296  				Addresses: []corev1.NodeAddress{
   297  					{Type: corev1.NodeInternalIP, Address: "10.0.0.1"},
   298  				},
   299  			},
   300  		},
   301  		&corev1.Node{
   302  			ObjectMeta: metav1.ObjectMeta{
   303  				Name:   "node-2",
   304  				Labels: map[string]string{"all": "true", "group": "notfirst", "test/label": "is-good"},
   305  			},
   306  			Status: corev1.NodeStatus{
   307  				Addresses: []corev1.NodeAddress{
   308  					{Type: corev1.NodeInternalIP, Address: "10.0.0.2"},
   309  				},
   310  			},
   311  		},
   312  		&corev1.Node{
   313  			ObjectMeta: metav1.ObjectMeta{
   314  				Name:   "node-3",
   315  				Labels: map[string]string{"all": "true", "group": "notfirst"},
   316  			},
   317  			Status: corev1.NodeStatus{
   318  				Addresses: []corev1.NodeAddress{
   319  					{Type: corev1.NodeInternalIP, Address: "10.0.0.3"},
   320  				},
   321  			},
   322  		},
   323  		&corev1.Service{
   324  			ObjectMeta: metav1.ObjectMeta{
   325  				Name:      "svclabels",
   326  				Namespace: "default",
   327  			},
   328  			Spec: corev1.ServiceSpec{
   329  				Type:              corev1.ServiceTypeLoadBalancer,
   330  				IPFamilies:        []corev1.IPFamily{corev1.IPv4Protocol},
   331  				LoadBalancerClass: &nodeSvcLBClass,
   332  			},
   333  		},
   334  	}
   335  )
   336  
   337  func stringPtr(str string) *string {
   338  	return &str
   339  }
   340  func boolPtr(boolean bool) *bool {
   341  	return &boolean
   342  }
   343  
   344  func Test_httpRouteReconciler_Reconcile(t *testing.T) {
   345  	c := fake.NewClientBuilder().
   346  		WithObjects(nodeSvcLbFixtures...).
   347  		WithStatusSubresource(&corev1.Service{}).
   348  		Build()
   349  	r := &nodeSvcLBReconciler{Client: c, Logger: logging.DefaultLogger}
   350  
   351  	t.Run("unsupported service reset", func(t *testing.T) {
   352  		for _, name := range []string{"not-supported-1", "not-supported-2"} {
   353  			key := types.NamespacedName{
   354  				Name:      name,
   355  				Namespace: "default",
   356  			}
   357  			result, err := r.Reconcile(context.Background(), ctrl.Request{
   358  				NamespacedName: key,
   359  			})
   360  
   361  			require.NoError(t, err)
   362  			require.Equal(t, ctrl.Result{}, result, "Result should be empty")
   363  
   364  			svc := &corev1.Service{}
   365  			err = c.Get(context.Background(), key, svc)
   366  
   367  			require.NoError(t, err)
   368  			// It did not change the IPs already advertised
   369  			require.Len(t, svc.Status.LoadBalancer.Ingress, 1)
   370  			require.Equal(t, svc.Status.LoadBalancer.Ingress[0].IP, "100.100.100.100")
   371  		}
   372  	})
   373  
   374  	t.Run("single address test in single stack", func(t *testing.T) {
   375  		for _, param := range []struct {
   376  			name    string
   377  			address string
   378  		}{
   379  			{name: "ipv4-internal", address: "10.0.0.1"},
   380  			{name: "ipv4-external", address: "42.0.0.2"},
   381  			{name: "ipv6-internal", address: "fc00::2"},
   382  			{name: "ipv6-external", address: "2001:0000::1"},
   383  		} {
   384  			key := types.NamespacedName{
   385  				Name:      param.name,
   386  				Namespace: "default",
   387  			}
   388  			result, err := r.Reconcile(context.Background(), ctrl.Request{
   389  				NamespacedName: key,
   390  			})
   391  
   392  			require.NoError(t, err)
   393  			require.Equal(t, ctrl.Result{}, result, "Result should be empty")
   394  
   395  			svc := &corev1.Service{}
   396  			err = c.Get(context.Background(), key, svc)
   397  
   398  			require.NoError(t, err)
   399  			require.Len(t, svc.Status.LoadBalancer.Ingress, 1)
   400  			require.Equal(t, svc.Status.LoadBalancer.Ingress[0].IP, param.address)
   401  		}
   402  	})
   403  
   404  	t.Run("dual stack", func(t *testing.T) {
   405  		key := types.NamespacedName{
   406  			Name:      "dualstack-external",
   407  			Namespace: "default",
   408  		}
   409  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   410  			NamespacedName: key,
   411  		})
   412  
   413  		require.NoError(t, err)
   414  		require.Equal(t, ctrl.Result{}, result, "Result should be empty")
   415  
   416  		svc := &corev1.Service{}
   417  		err = c.Get(context.Background(), key, svc)
   418  
   419  		require.NoError(t, err)
   420  		require.Len(t, svc.Status.LoadBalancer.Ingress, 2)
   421  		require.Equal(t, svc.Status.LoadBalancer.Ingress[0].IP, "2001:0000::1")
   422  		require.Equal(t, svc.Status.LoadBalancer.Ingress[1].IP, "42.0.0.2")
   423  	})
   424  
   425  	//
   426  	t.Run("external traffic policy cluster", func(t *testing.T) {
   427  		key := types.NamespacedName{
   428  			Name:      "etp-cluster",
   429  			Namespace: "default",
   430  		}
   431  		result, err := r.Reconcile(context.Background(), ctrl.Request{
   432  			NamespacedName: key,
   433  		})
   434  
   435  		require.NoError(t, err)
   436  		require.Equal(t, ctrl.Result{}, result, "Result should be empty")
   437  
   438  		svc := &corev1.Service{}
   439  		err = c.Get(context.Background(), key, svc)
   440  
   441  		require.NoError(t, err)
   442  		require.Len(t, svc.Status.LoadBalancer.Ingress, 2)
   443  		require.Equal(t, svc.Status.LoadBalancer.Ingress[0].IP, "42.0.0.2")
   444  		require.Equal(t, svc.Status.LoadBalancer.Ingress[1].IP, "42.0.0.3")
   445  	})
   446  }
   447  
   448  func Test_CiliumResources_Reconcile(t *testing.T) {
   449  	c := fake.NewClientBuilder().
   450  		WithObjects(nodeSvcLabelFixtures...).
   451  		WithStatusSubresource(&corev1.Service{}).
   452  		Build()
   453  	r := &nodeSvcLBReconciler{Client: c, Logger: logging.DefaultLogger}
   454  
   455  	key := types.NamespacedName{
   456  		Name:      "svclabels",
   457  		Namespace: "default",
   458  	}
   459  
   460  	t.Run("Managed Resource", func(t *testing.T) {
   461  		ctx := context.Background()
   462  		result, err := r.Reconcile(ctx, ctrl.Request{
   463  			NamespacedName: key,
   464  		})
   465  
   466  		require.NoError(t, err)
   467  		require.Equal(t, ctrl.Result{}, result, "Result should be empty")
   468  
   469  		svc := &corev1.Service{}
   470  		err = c.Get(ctx, key, svc)
   471  
   472  		require.NoError(t, err)
   473  		require.Len(t, svc.Status.LoadBalancer.Ingress, 3)
   474  		var ips []string
   475  		for _, v := range svc.Status.LoadBalancer.Ingress {
   476  			ips = append(ips, v.IP)
   477  		}
   478  		require.Equal(t, ips, []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"})
   479  	})
   480  
   481  	t.Run("Node Label Filter", func(t *testing.T) {
   482  		ctx := context.Background()
   483  
   484  		for _, param := range []struct {
   485  			labelFilter string
   486  			results     []string
   487  		}{
   488  			{labelFilter: "all=true", results: []string{"10.0.0.1", "10.0.0.2", "10.0.0.3"}},
   489  			{labelFilter: "ingress-ready=true", results: []string{"10.0.0.1"}},
   490  			{labelFilter: "group=notfirst", results: []string{"10.0.0.2", "10.0.0.3"}},
   491  			{labelFilter: "group notin (first),test/label=is-good", results: []string{"10.0.0.2"}},
   492  		} {
   493  			svc := &corev1.Service{}
   494  			_ = c.Get(ctx, key, svc)
   495  			// Add the label to the service which should return on the first node
   496  			svc.Annotations = map[string]string{nodeSvcLBMatchLabelsAnnotation: param.labelFilter}
   497  			_ = c.Update(ctx, svc)
   498  			result, err := r.Reconcile(ctx, ctrl.Request{NamespacedName: key})
   499  
   500  			require.NoError(t, err)
   501  			require.Equal(t, ctrl.Result{}, result, "Result should be empty")
   502  
   503  			svc = &corev1.Service{}
   504  			err = c.Get(ctx, key, svc)
   505  
   506  			require.NoError(t, err)
   507  			require.NotNil(t, svc.Annotations[nodeSvcLBMatchLabelsAnnotation])
   508  			require.Len(t, svc.Status.LoadBalancer.Ingress, len(param.results))
   509  
   510  			var ips []string
   511  			for _, v := range svc.Status.LoadBalancer.Ingress {
   512  				ips = append(ips, v.IP)
   513  			}
   514  			require.Equal(t, ips, param.results)
   515  		}
   516  	})
   517  
   518  	t.Run("Bad Node Label Filter", func(t *testing.T) {
   519  		ctx := context.Background()
   520  		svc := &corev1.Service{}
   521  		_ = c.Get(ctx, key, svc)
   522  		// Add the label to the service which should return on the first node
   523  		svc.Annotations = map[string]string{nodeSvcLBMatchLabelsAnnotation: "this is completely/bad=!;lf"}
   524  		_ = c.Update(ctx, svc)
   525  		result, err := r.Reconcile(ctx, ctrl.Request{
   526  			NamespacedName: key,
   527  		})
   528  
   529  		require.Error(t, err)
   530  		require.Equal(t, ctrl.Result{}, result, "Result should be empty")
   531  
   532  	})
   533  
   534  	t.Run("Ensure Warning raised if no Nodes found using configured label selector", func(t *testing.T) {
   535  		var buf bytes.Buffer
   536  		logger := logging.DefaultLogger
   537  		logger.SetOutput(&buf)
   538  
   539  		r.Logger = logger
   540  
   541  		ctx := context.Background()
   542  		svc := &corev1.Service{}
   543  		_ = c.Get(ctx, key, svc)
   544  		// Add the label to the service which should return on the first node
   545  		svc.Annotations = map[string]string{nodeSvcLBMatchLabelsAnnotation: "foo=bar"}
   546  		_ = c.Update(ctx, svc)
   547  		result, err := r.Reconcile(ctx, ctrl.Request{
   548  			NamespacedName: key,
   549  		})
   550  
   551  		require.NoError(t, err)
   552  		require.Equal(t, ctrl.Result{}, result, "Result should be empty")
   553  		require.Contains(t, buf.String(), "level=warning msg=\"No Nodes found with configured label selector\"")
   554  	})
   555  }