github.com/cilium/cilium@v1.16.2/pkg/policy/k8s/service_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package k8s
     5  
     6  import (
     7  	"cmp"
     8  	"context"
     9  	"net/netip"
    10  	"slices"
    11  	"testing"
    12  
    13  	"github.com/cilium/stream"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  
    19  	cmtypes "github.com/cilium/cilium/pkg/clustermesh/types"
    20  	"github.com/cilium/cilium/pkg/k8s"
    21  	cilium_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    22  	"github.com/cilium/cilium/pkg/k8s/resource"
    23  	slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
    24  	k8sSynced "github.com/cilium/cilium/pkg/k8s/synced"
    25  	"github.com/cilium/cilium/pkg/k8s/types"
    26  	"github.com/cilium/cilium/pkg/labels"
    27  	"github.com/cilium/cilium/pkg/loadbalancer"
    28  	"github.com/cilium/cilium/pkg/option"
    29  	"github.com/cilium/cilium/pkg/policy"
    30  	"github.com/cilium/cilium/pkg/policy/api"
    31  )
    32  
    33  type fakePolicyManager struct {
    34  	OnPolicyAdd    func(rules api.Rules, opts *policy.AddOptions) (newRev uint64, err error)
    35  	OnPolicyDelete func(labels labels.LabelArray, opts *policy.DeleteOptions) (newRev uint64, err error)
    36  }
    37  
    38  func (f *fakePolicyManager) PolicyAdd(rules api.Rules, opts *policy.AddOptions) (newRev uint64, err error) {
    39  	if f.OnPolicyAdd != nil {
    40  		return f.OnPolicyAdd(rules, opts)
    41  	}
    42  	panic("OnPolicyAdd(api.Rules, *policy.AddOptions) (uint64, error) was called and is not set!")
    43  }
    44  
    45  func (f *fakePolicyManager) PolicyDelete(labels labels.LabelArray, opts *policy.DeleteOptions) (newRev uint64, err error) {
    46  	if f.OnPolicyDelete != nil {
    47  		return f.OnPolicyDelete(labels, opts)
    48  	}
    49  	panic("OnPolicyDelete(labels.LabelArray, *policy.DeleteOptions) (uint64, error) was called and is not set!")
    50  }
    51  
    52  type fakeService struct {
    53  	svc *k8s.Service
    54  	eps *k8s.Endpoints
    55  }
    56  
    57  type fakeServiceCache map[k8s.ServiceID]fakeService
    58  
    59  func (f fakeServiceCache) ForEachService(yield func(svcID k8s.ServiceID, svc *k8s.Service, eps *k8s.Endpoints) bool) {
    60  	for svcID, s := range f {
    61  		if !yield(svcID, s.svc, s.eps) {
    62  			break
    63  		}
    64  	}
    65  }
    66  
    67  func addrToCIDRRule(addr netip.Addr) api.CIDRRule {
    68  	return api.CIDRRule{
    69  		Cidr:      api.CIDR(netip.PrefixFrom(addr, addr.BitLen()).String()),
    70  		Generated: true,
    71  	}
    72  }
    73  
    74  func sortCIDRSet(s api.CIDRRuleSlice) api.CIDRRuleSlice {
    75  	slices.SortFunc(s, func(a, b api.CIDRRule) int {
    76  		return cmp.Compare(a.Cidr, b.Cidr)
    77  	})
    78  	return s
    79  }
    80  
    81  func TestPolicyWatcher_updateToServicesPolicies(t *testing.T) {
    82  	policyAdd := make(chan api.Rules, 3)
    83  	policyManager := &fakePolicyManager{
    84  		OnPolicyAdd: func(rules api.Rules, opts *policy.AddOptions) (newRev uint64, err error) {
    85  			policyAdd <- rules
    86  			return 0, nil
    87  		},
    88  	}
    89  
    90  	barSvcLabels := map[string]string{
    91  		"app": "bar",
    92  	}
    93  	barSvcSelector := api.ServiceSelector(api.NewESFromMatchRequirements(barSvcLabels, nil))
    94  
    95  	svcByNameCNP := &types.SlimCNP{
    96  		CiliumNetworkPolicy: &cilium_v2.CiliumNetworkPolicy{
    97  			TypeMeta: metav1.TypeMeta{
    98  				APIVersion: "cilium.io/v2",
    99  				Kind:       "CiliumNetworkPolicy",
   100  			},
   101  			ObjectMeta: metav1.ObjectMeta{
   102  				Name:      "svc-by-name",
   103  				Namespace: "test",
   104  			},
   105  			Spec: &api.Rule{
   106  				EndpointSelector: api.NewESFromLabels(),
   107  				Egress: []api.EgressRule{
   108  					{
   109  						EgressCommonRule: api.EgressCommonRule{
   110  							ToServices: []api.Service{
   111  								{
   112  									// Selects foo service by name
   113  									K8sService: &api.K8sServiceNamespace{
   114  										ServiceName: "foo-svc",
   115  										Namespace:   "foo-ns",
   116  									},
   117  								},
   118  								{
   119  									// Selects bar service by name
   120  									K8sService: &api.K8sServiceNamespace{
   121  										ServiceName: "bar-svc",
   122  										Namespace:   "bar-ns",
   123  									},
   124  								},
   125  							},
   126  						},
   127  					},
   128  				},
   129  			},
   130  			Specs: api.Rules{
   131  				{
   132  					EndpointSelector: api.NewESFromLabels(),
   133  					Egress: []api.EgressRule{
   134  						{
   135  							EgressCommonRule: api.EgressCommonRule{
   136  								ToServices: []api.Service{
   137  									{
   138  										// Selects foo service by name
   139  										K8sService: &api.K8sServiceNamespace{
   140  											ServiceName: "foo-svc",
   141  											Namespace:   "foo-ns",
   142  										},
   143  									},
   144  								},
   145  							},
   146  						},
   147  					},
   148  				},
   149  			},
   150  		},
   151  	}
   152  	svcByNameLbl := labels.NewLabel("io.cilium.k8s.policy.name", svcByNameCNP.Name, "k8s")
   153  	svcByNameKey := resource.NewKey(svcByNameCNP)
   154  	svcByNameResourceID := resourceIDForCiliumNetworkPolicy(svcByNameKey, svcByNameCNP)
   155  
   156  	svcByLabelCNP := &types.SlimCNP{
   157  		CiliumNetworkPolicy: &cilium_v2.CiliumNetworkPolicy{
   158  			TypeMeta: metav1.TypeMeta{
   159  				APIVersion: "cilium.io/v2",
   160  				Kind:       "ClusterwideCiliumNetworkPolicy",
   161  			},
   162  			ObjectMeta: metav1.ObjectMeta{
   163  				Name:      "svc-by-label",
   164  				Namespace: "",
   165  			},
   166  			Spec: &api.Rule{
   167  				EndpointSelector: api.NewESFromLabels(),
   168  				Egress: []api.EgressRule{
   169  					{
   170  						EgressCommonRule: api.EgressCommonRule{
   171  							ToServices: []api.Service{
   172  								{
   173  									// Selects bar service by label selector
   174  									K8sServiceSelector: &api.K8sServiceSelectorNamespace{
   175  										Selector: barSvcSelector,
   176  									},
   177  								},
   178  							},
   179  						},
   180  					},
   181  				},
   182  			},
   183  		},
   184  	}
   185  	svcByLabelLbl := labels.NewLabel("io.cilium.k8s.policy.name", svcByLabelCNP.Name, "k8s")
   186  	svcByLabelKey := resource.NewKey(svcByLabelCNP)
   187  	svcByLabelResourceID := resourceIDForCiliumNetworkPolicy(svcByLabelKey, svcByLabelCNP)
   188  
   189  	fooEpAddr1 := cmtypes.MustParseAddrCluster("10.1.1.1")
   190  	fooEpAddr2 := cmtypes.MustParseAddrCluster("10.1.1.2")
   191  	fooSvcID := k8s.ServiceID{
   192  		Name:      "foo-svc",
   193  		Namespace: "foo-ns",
   194  	}
   195  	fooSvc := &k8s.Service{}
   196  	fooEps := &k8s.Endpoints{
   197  		Backends: map[cmtypes.AddrCluster]*k8s.Backend{
   198  			fooEpAddr1: {
   199  				Ports: map[string]*loadbalancer.L4Addr{
   200  					"port": {
   201  						Protocol: loadbalancer.TCP,
   202  						Port:     80,
   203  					},
   204  				},
   205  			},
   206  			fooEpAddr2: {
   207  				Ports: map[string]*loadbalancer.L4Addr{
   208  					"port": {
   209  						Protocol: loadbalancer.TCP,
   210  						Port:     80,
   211  					},
   212  				},
   213  			},
   214  		},
   215  	}
   216  
   217  	barEpAddr := cmtypes.MustParseAddrCluster("192.168.1.1")
   218  	barSvcID := k8s.ServiceID{
   219  		Name:      "bar-svc",
   220  		Namespace: "bar-ns",
   221  	}
   222  	barSvc := &k8s.Service{
   223  		Labels: barSvcLabels,
   224  	}
   225  	barEps := &k8s.Endpoints{
   226  		Backends: map[cmtypes.AddrCluster]*k8s.Backend{
   227  			barEpAddr: {
   228  				Ports: map[string]*loadbalancer.L4Addr{
   229  					"port": {
   230  						Protocol: loadbalancer.UDP,
   231  						Port:     53,
   232  					},
   233  				},
   234  			},
   235  		},
   236  	}
   237  
   238  	// baz is similar to bar, but not an external service (thus not selectable)
   239  	bazSvcID := k8s.ServiceID{
   240  		Name:      "baz-svc",
   241  		Namespace: "baz-ns",
   242  	}
   243  	bazSvc := &k8s.Service{
   244  		Labels: barSvcLabels,
   245  		Selector: map[string]string{
   246  			"app": "baz",
   247  		},
   248  	}
   249  	bazEps := barEps.DeepCopy()
   250  
   251  	logger := logrus.New()
   252  	logger.SetLevel(logrus.DebugLevel)
   253  
   254  	svcCache := fakeServiceCache{}
   255  	p := &policyWatcher{
   256  		log:                logrus.NewEntry(logger),
   257  		config:             &option.DaemonConfig{},
   258  		k8sResourceSynced:  &k8sSynced.Resources{CacheStatus: make(k8sSynced.CacheStatus)},
   259  		k8sAPIGroups:       &k8sSynced.APIGroups{},
   260  		policyManager:      policyManager,
   261  		svcCache:           svcCache,
   262  		cnpCache:           map[resource.Key]*types.SlimCNP{},
   263  		toServicesPolicies: map[resource.Key]struct{}{},
   264  		cnpByServiceID:     map[k8s.ServiceID]map[resource.Key]struct{}{},
   265  	}
   266  
   267  	// Upsert policies. No services are known, so generated ToCIDRSet should be empty
   268  	err := p.onUpsert(svcByNameCNP, svcByNameKey, k8sAPIGroupCiliumNetworkPolicyV2, svcByNameResourceID)
   269  	assert.NoError(t, err)
   270  	rules := <-policyAdd
   271  	assert.Len(t, rules, 2)
   272  	assert.Len(t, rules[0].Egress, 1)
   273  	assert.Empty(t, rules[0].Egress[0].ToCIDRSet)
   274  	assert.Len(t, rules[1].Egress, 1)
   275  	assert.Empty(t, rules[1].Egress[0].ToCIDRSet)
   276  
   277  	err = p.onUpsert(svcByLabelCNP, svcByLabelKey, k8sAPIGroupCiliumNetworkPolicyV2, svcByLabelResourceID)
   278  	assert.NoError(t, err)
   279  	rules = <-policyAdd
   280  	assert.Len(t, rules, 1)
   281  	assert.Len(t, rules[0].Egress, 1)
   282  	assert.Empty(t, rules[0].Egress[0].ToCIDRSet)
   283  
   284  	// Check that policies are recognized as ToServices policies
   285  	assert.Equal(t, p.toServicesPolicies, map[resource.Key]struct{}{
   286  		svcByNameKey:  {},
   287  		svcByLabelKey: {},
   288  	})
   289  
   290  	// Add foo-svc, which is selected by svcByNameCNP twice
   291  	svcCache[fooSvcID] = fakeService{
   292  		svc: fooSvc,
   293  		eps: fooEps,
   294  	}
   295  	err = p.updateToServicesPolicies(fooSvcID, fooSvc, nil)
   296  	assert.NoError(t, err)
   297  	rules = <-policyAdd
   298  	assert.Len(t, rules, 2)
   299  
   300  	// Check that Spec was translated
   301  	assert.Len(t, rules[0].Egress, 1)
   302  	assert.Contains(t, rules[0].Labels, svcByNameLbl)
   303  	assert.Equal(t, svcByNameCNP.Spec.Egress[0].ToServices, rules[0].Egress[0].ToServices)
   304  	assert.Equal(t, sortCIDRSet(rules[0].Egress[0].ToCIDRSet), api.CIDRRuleSlice{
   305  		addrToCIDRRule(fooEpAddr1.Addr()),
   306  		addrToCIDRRule(fooEpAddr2.Addr()),
   307  	})
   308  
   309  	// Check that Specs was translated
   310  	assert.Len(t, rules[1].Egress, 1)
   311  	assert.Contains(t, rules[1].Labels, svcByNameLbl)
   312  	assert.Equal(t, svcByNameCNP.Specs[0].Egress[0].ToServices, rules[1].Egress[0].ToServices)
   313  	assert.Equal(t, sortCIDRSet(rules[1].Egress[0].ToCIDRSet), api.CIDRRuleSlice{
   314  		addrToCIDRRule(fooEpAddr1.Addr()),
   315  		addrToCIDRRule(fooEpAddr2.Addr()),
   316  	})
   317  
   318  	// Check that policy has been marked
   319  	assert.Equal(t, p.cnpByServiceID, map[k8s.ServiceID]map[resource.Key]struct{}{
   320  		fooSvcID: {
   321  			svcByNameKey: {},
   322  		},
   323  	})
   324  
   325  	// Add bar-svc, which is selected by both policies
   326  	svcCache[barSvcID] = fakeService{
   327  		svc: barSvc,
   328  		eps: barEps,
   329  	}
   330  	err = p.updateToServicesPolicies(barSvcID, barSvc, nil)
   331  	assert.NoError(t, err)
   332  
   333  	// Expect two policies to be updated (in any order)
   334  	var policies [2]api.Rules
   335  	policies[0] = <-policyAdd
   336  	policies[1] = <-policyAdd
   337  	slices.SortFunc(policies[:], func(a, b api.Rules) int {
   338  		return cmp.Compare(a.String(), b.String())
   339  	})
   340  	byNameRules, byLabelRules := policies[0], policies[1]
   341  
   342  	// Check that svcByNameCNP Spec (matching foo and bar) was translated
   343  	assert.Len(t, byNameRules, 2)
   344  	assert.Len(t, byNameRules[0].Egress, 1)
   345  	assert.Contains(t, byNameRules[0].Labels, svcByNameLbl)
   346  	assert.Equal(t, svcByNameCNP.Spec.Egress[0].ToServices, byNameRules[0].Egress[0].ToServices)
   347  	assert.Equal(t, sortCIDRSet(byNameRules[0].Egress[0].ToCIDRSet), api.CIDRRuleSlice{
   348  		addrToCIDRRule(fooEpAddr1.Addr()),
   349  		addrToCIDRRule(fooEpAddr2.Addr()),
   350  		addrToCIDRRule(barEpAddr.Addr()),
   351  	})
   352  
   353  	// Check that svcByNameCNP Specs (matching only foo) was translated
   354  	assert.Len(t, byNameRules[1].Egress, 1)
   355  	assert.Contains(t, byNameRules[1].Labels, svcByNameLbl)
   356  	assert.Equal(t, svcByNameCNP.Specs[0].Egress[0].ToServices, byNameRules[1].Egress[0].ToServices)
   357  	assert.Equal(t, sortCIDRSet(byNameRules[1].Egress[0].ToCIDRSet), api.CIDRRuleSlice{
   358  		addrToCIDRRule(fooEpAddr1.Addr()),
   359  		addrToCIDRRule(fooEpAddr2.Addr()),
   360  	})
   361  
   362  	// Check that svcByLabelCNP Spec (matching only bar) was translated
   363  	assert.Len(t, byLabelRules, 1)
   364  	assert.Len(t, byLabelRules[0].Egress, 1)
   365  	assert.Contains(t, byLabelRules[0].Labels, svcByLabelLbl)
   366  	assert.Equal(t, svcByLabelCNP.Spec.Egress[0].ToServices, byLabelRules[0].Egress[0].ToServices)
   367  	assert.Equal(t, byLabelRules[0].Egress[0].ToCIDRSet, api.CIDRRuleSlice{
   368  		addrToCIDRRule(barEpAddr.Addr()),
   369  	})
   370  
   371  	// Check that policies have been marked
   372  	assert.Equal(t, p.cnpByServiceID, map[k8s.ServiceID]map[resource.Key]struct{}{
   373  		fooSvcID: {
   374  			svcByNameKey: {},
   375  		},
   376  		barSvcID: {
   377  			svcByNameKey:  {},
   378  			svcByLabelKey: {},
   379  		},
   380  	})
   381  
   382  	// Change foo-svc endpoints, which is selected by svcByNameCNP twice
   383  	delete(fooEps.Backends, fooEpAddr2)
   384  	err = p.updateToServicesPolicies(fooSvcID, fooSvc, fooSvc)
   385  	assert.NoError(t, err)
   386  	byNameRules = <-policyAdd
   387  	assert.Len(t, byNameRules, 2)
   388  
   389  	// Check that svcByNameCNP Spec (matching foo and bar) was translated
   390  	assert.Len(t, byNameRules[0].Egress, 1)
   391  	assert.Contains(t, byNameRules[0].Labels, svcByNameLbl)
   392  	assert.Equal(t, svcByNameCNP.Spec.Egress[0].ToServices, byNameRules[0].Egress[0].ToServices)
   393  	assert.Equal(t, sortCIDRSet(byNameRules[0].Egress[0].ToCIDRSet), api.CIDRRuleSlice{
   394  		addrToCIDRRule(fooEpAddr1.Addr()),
   395  		addrToCIDRRule(barEpAddr.Addr()),
   396  	})
   397  
   398  	// Check that Specs was translated (matching only foo) was translated
   399  	assert.Len(t, byNameRules[1].Egress, 1)
   400  	assert.Contains(t, byNameRules[1].Labels, svcByNameLbl)
   401  	assert.Equal(t, svcByNameCNP.Specs[0].Egress[0].ToServices, byNameRules[1].Egress[0].ToServices)
   402  	assert.Equal(t, sortCIDRSet(byNameRules[1].Egress[0].ToCIDRSet), api.CIDRRuleSlice{
   403  		addrToCIDRRule(fooEpAddr1.Addr()),
   404  	})
   405  
   406  	// Delete bar-svc labels. This should remove all CIDRs from svcByLabelCNP
   407  	oldBarSvc := barSvc.DeepCopy()
   408  	barSvc.Labels = nil
   409  	err = p.updateToServicesPolicies(barSvcID, barSvc, oldBarSvc)
   410  	assert.NoError(t, err)
   411  
   412  	// Expect two policies to be updated (in any order)
   413  	oldByNameRules := byNameRules.DeepCopy()
   414  	policies[0] = <-policyAdd
   415  	policies[1] = <-policyAdd
   416  	slices.SortFunc(policies[:], func(a, b api.Rules) int {
   417  		return cmp.Compare(a.String(), b.String())
   418  	})
   419  	byNameRules, byLabelRules = policies[0], policies[1]
   420  
   421  	// Check that svcByNameCNP has not changed
   422  	assert.Equal(t,
   423  		sortCIDRSet(byNameRules[0].Egress[0].ToCIDRSet),
   424  		sortCIDRSet(oldByNameRules[0].Egress[0].ToCIDRSet))
   425  	assert.Equal(t,
   426  		sortCIDRSet(byNameRules[1].Egress[0].ToCIDRSet),
   427  		sortCIDRSet(oldByNameRules[1].Egress[0].ToCIDRSet))
   428  
   429  	// Check that svcByLabelCNP Spec no longer matches anything
   430  	assert.Len(t, byLabelRules, 1)
   431  	assert.Len(t, byLabelRules[0].Egress, 1)
   432  	assert.Contains(t, byLabelRules[0].Labels, svcByLabelLbl)
   433  	assert.Equal(t, svcByLabelCNP.Spec.Egress[0].ToServices, byLabelRules[0].Egress[0].ToServices)
   434  	assert.Empty(t, byLabelRules[0].Egress[0].ToCIDRSet)
   435  
   436  	// Check that policies have been cleared
   437  	assert.Equal(t, p.cnpByServiceID, map[k8s.ServiceID]map[resource.Key]struct{}{
   438  		fooSvcID: {
   439  			svcByNameKey: {},
   440  		},
   441  		barSvcID: {
   442  			svcByNameKey: {},
   443  		},
   444  	})
   445  
   446  	// Add baz-svc, which is not selectable and thus must not trigger a policyAdd
   447  	svcCache[bazSvcID] = fakeService{
   448  		svc: bazSvc,
   449  		eps: bazEps,
   450  	}
   451  	err = p.updateToServicesPolicies(bazSvcID, bazSvc, nil)
   452  	assert.NoError(t, err)
   453  	assert.Empty(t, policyAdd)
   454  }
   455  
   456  func Test_hasMatchingToServices(t *testing.T) {
   457  	type args struct {
   458  		spec  *api.Rule
   459  		svcID k8s.ServiceID
   460  		svc   *k8s.Service
   461  	}
   462  	tests := []struct {
   463  		name string
   464  		args args
   465  		want bool
   466  	}{
   467  		{
   468  			name: "nil rule",
   469  			args: args{
   470  				spec:  nil,
   471  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   472  				svc:   &k8s.Service{},
   473  			},
   474  			want: false,
   475  		},
   476  		{
   477  			name: "by name and namespace",
   478  			args: args{
   479  				spec: &api.Rule{Egress: []api.EgressRule{
   480  					{
   481  						EgressCommonRule: api.EgressCommonRule{
   482  							ToServices: []api.Service{
   483  								{K8sService: &api.K8sServiceNamespace{
   484  									ServiceName: "test-svc",
   485  									Namespace:   "test-ns",
   486  								}},
   487  							},
   488  						},
   489  					},
   490  				}},
   491  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   492  				svc:   &k8s.Service{},
   493  			},
   494  			want: true,
   495  		},
   496  		{
   497  			name: "by name without namespace",
   498  			args: args{
   499  				spec: &api.Rule{Egress: []api.EgressRule{
   500  					{
   501  						EgressCommonRule: api.EgressCommonRule{
   502  							ToServices: []api.Service{
   503  								{K8sService: &api.K8sServiceNamespace{
   504  									ServiceName: "test-svc",
   505  									Namespace:   "",
   506  								}},
   507  							},
   508  						},
   509  					},
   510  				}},
   511  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   512  				svc:   &k8s.Service{},
   513  			},
   514  			want: true,
   515  		},
   516  		{
   517  			name: "by name with wrong namespace",
   518  			args: args{
   519  				spec: &api.Rule{Egress: []api.EgressRule{
   520  					{
   521  						EgressCommonRule: api.EgressCommonRule{
   522  							ToServices: []api.Service{
   523  								{
   524  									K8sService: &api.K8sServiceNamespace{
   525  										ServiceName: "test-svc",
   526  										Namespace:   "test-ns",
   527  									},
   528  								},
   529  							},
   530  						},
   531  					},
   532  				}},
   533  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "not-test-ns"},
   534  				svc:   &k8s.Service{},
   535  			},
   536  			want: false,
   537  		},
   538  		{
   539  			name: "invalid namespace-only selector",
   540  			args: args{
   541  				spec: &api.Rule{Egress: []api.EgressRule{
   542  					{
   543  						EgressCommonRule: api.EgressCommonRule{
   544  							ToServices: []api.Service{
   545  								{
   546  									K8sService: &api.K8sServiceNamespace{
   547  										ServiceName: "",
   548  										Namespace:   "test-ns",
   549  									},
   550  								},
   551  							},
   552  						},
   553  					},
   554  				}},
   555  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   556  				svc:   &k8s.Service{},
   557  			},
   558  			want: false,
   559  		},
   560  		{
   561  			name: "empty selector",
   562  			args: args{
   563  				spec: &api.Rule{Egress: []api.EgressRule{
   564  					{
   565  						EgressCommonRule: api.EgressCommonRule{
   566  							ToServices: []api.Service{
   567  								{
   568  									K8sService: &api.K8sServiceNamespace{
   569  										ServiceName: "",
   570  										Namespace:   "",
   571  									},
   572  								},
   573  							},
   574  						},
   575  					},
   576  				}},
   577  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   578  				svc:   &k8s.Service{},
   579  			},
   580  			want: false,
   581  		},
   582  		{
   583  			name: "second selector",
   584  			args: args{
   585  				spec: &api.Rule{Egress: []api.EgressRule{
   586  					{
   587  						EgressCommonRule: api.EgressCommonRule{
   588  							ToServices: []api.Service{
   589  								{
   590  									K8sService: &api.K8sServiceNamespace{
   591  										ServiceName: "foo-svc",
   592  										Namespace:   "",
   593  									},
   594  								},
   595  								{
   596  									K8sService: &api.K8sServiceNamespace{
   597  										ServiceName: "test-svc",
   598  										Namespace:   "test-ns",
   599  									},
   600  								},
   601  							},
   602  						},
   603  					},
   604  				}},
   605  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   606  				svc:   &k8s.Service{},
   607  			},
   608  			want: true,
   609  		},
   610  		{
   611  			name: "by label",
   612  			args: args{
   613  				spec: &api.Rule{Egress: []api.EgressRule{
   614  					{
   615  						EgressCommonRule: api.EgressCommonRule{
   616  							ToServices: []api.Service{
   617  								{
   618  									K8sServiceSelector: &api.K8sServiceSelectorNamespace{
   619  										Selector: api.ServiceSelector(api.NewESFromMatchRequirements(map[string]string{
   620  											"foo": "bar",
   621  										}, nil)),
   622  									},
   623  								},
   624  							},
   625  						},
   626  					},
   627  				}},
   628  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   629  				svc:   &k8s.Service{Labels: map[string]string{"foo": "bar", "baz": "qux"}},
   630  			},
   631  			want: true,
   632  		},
   633  		{
   634  			name: "by label requirements",
   635  			args: args{
   636  				spec: &api.Rule{Egress: []api.EgressRule{
   637  					{
   638  						EgressCommonRule: api.EgressCommonRule{
   639  							ToServices: []api.Service{
   640  								{
   641  									K8sServiceSelector: &api.K8sServiceSelectorNamespace{
   642  										Selector: api.ServiceSelector(api.NewESFromMatchRequirements(nil, []slim_metav1.LabelSelectorRequirement{
   643  											{Key: "foo", Operator: "Exists"},
   644  										})),
   645  									},
   646  								},
   647  							},
   648  						},
   649  					},
   650  				}},
   651  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   652  				svc:   &k8s.Service{Labels: map[string]string{"foo": "bar", "baz": "qux"}},
   653  			},
   654  			want: true,
   655  		},
   656  		{
   657  			name: "overspecific label selector",
   658  			args: args{
   659  				spec: &api.Rule{Egress: []api.EgressRule{
   660  					{
   661  						EgressCommonRule: api.EgressCommonRule{
   662  							ToServices: []api.Service{
   663  								{
   664  									K8sServiceSelector: &api.K8sServiceSelectorNamespace{
   665  										Selector: api.ServiceSelector(api.NewESFromMatchRequirements(map[string]string{
   666  											"foo": "bar",
   667  											"not": "present",
   668  										}, nil)),
   669  									},
   670  								},
   671  							},
   672  						},
   673  					},
   674  				}},
   675  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   676  				svc:   &k8s.Service{Labels: map[string]string{"foo": "bar", "baz": "qux"}},
   677  			},
   678  			want: false,
   679  		},
   680  		{
   681  			name: "by label with wrong namespace",
   682  			args: args{
   683  				spec: &api.Rule{Egress: []api.EgressRule{
   684  					{
   685  						EgressCommonRule: api.EgressCommonRule{
   686  							ToServices: []api.Service{
   687  								{
   688  									K8sServiceSelector: &api.K8sServiceSelectorNamespace{
   689  										Selector: api.ServiceSelector(api.NewESFromMatchRequirements(map[string]string{
   690  											"foo": "bar",
   691  										}, nil)),
   692  										Namespace: "not-test-ns",
   693  									},
   694  								},
   695  							},
   696  						},
   697  					},
   698  				}},
   699  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   700  				svc:   &k8s.Service{Labels: map[string]string{"foo": "bar", "baz": "qux"}},
   701  			},
   702  			want: false,
   703  		},
   704  		{
   705  			name: "by label takes precedence over by name",
   706  			args: args{
   707  				spec: &api.Rule{Egress: []api.EgressRule{
   708  					{
   709  						EgressCommonRule: api.EgressCommonRule{
   710  							ToServices: []api.Service{
   711  								{
   712  									K8sService: &api.K8sServiceNamespace{
   713  										ServiceName: "test-svc",
   714  										Namespace:   "test-ns",
   715  									},
   716  									K8sServiceSelector: &api.K8sServiceSelectorNamespace{
   717  										Selector: api.ServiceSelector(api.NewESFromMatchRequirements(map[string]string{
   718  											"no": "match",
   719  										}, nil)),
   720  									},
   721  								},
   722  							},
   723  						},
   724  					},
   725  				}},
   726  				svcID: k8s.ServiceID{Name: "test-svc", Namespace: "test-ns"},
   727  				svc:   &k8s.Service{Labels: map[string]string{"foo": "bar", "baz": "qux"}},
   728  			},
   729  			want: false,
   730  		},
   731  	}
   732  	for _, tt := range tests {
   733  		t.Run(tt.name, func(t *testing.T) {
   734  			assert.Equalf(t, tt.want, hasMatchingToServices(tt.args.spec, tt.args.svcID, tt.args.svc), "hasMatchingToServices(%v, %v, %v)", tt.args.spec, tt.args.svcID, tt.args.svc)
   735  		})
   736  	}
   737  }
   738  
   739  func Test_serviceNotificationsQueue(t *testing.T) {
   740  	ctx, cancel := context.WithCancel(context.Background())
   741  	defer cancel()
   742  
   743  	upstream := make(chan k8s.ServiceNotification)
   744  	downstream := serviceNotificationsQueue(ctx, stream.FromChannel(upstream))
   745  
   746  	// Test that sending events in upstream does not block on unbuffered channel
   747  	upstream <- k8s.ServiceNotification{ID: k8s.ServiceID{Name: "svc1"}}
   748  	upstream <- k8s.ServiceNotification{ID: k8s.ServiceID{Name: "svc2"}}
   749  	upstream <- k8s.ServiceNotification{ID: k8s.ServiceID{Name: "svc3"}}
   750  
   751  	// Test that events are received in order
   752  	require.Equal(t, k8s.ServiceNotification{ID: k8s.ServiceID{Name: "svc1"}}, <-downstream)
   753  	require.Equal(t, k8s.ServiceNotification{ID: k8s.ServiceID{Name: "svc2"}}, <-downstream)
   754  	require.Equal(t, k8s.ServiceNotification{ID: k8s.ServiceID{Name: "svc3"}}, <-downstream)
   755  	require.Empty(t, downstream)
   756  
   757  	// Test that Go routine exits on empty upstream if ctx is cancelled
   758  	cancel()
   759  	_, ok := <-downstream
   760  	require.False(t, ok, "service notification channel was not closed on cancellation")
   761  
   762  	// Test that Go routine exits on upstream close
   763  	ctx, cancel = context.WithCancel(context.Background())
   764  	defer cancel()
   765  	upstream = make(chan k8s.ServiceNotification)
   766  	downstream = serviceNotificationsQueue(ctx, stream.FromChannel(upstream))
   767  
   768  	upstream <- k8s.ServiceNotification{ID: k8s.ServiceID{Name: "svc4"}}
   769  	require.Equal(t, k8s.ServiceNotification{ID: k8s.ServiceID{Name: "svc4"}}, <-downstream)
   770  
   771  	close(upstream)
   772  	_, ok = <-downstream
   773  	require.False(t, ok, "service notification channel was not closed on upstream close")
   774  }