github.com/cilium/cilium@v1.16.2/pkg/ciliumenvoyconfig/cec_reconciler_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package ciliumenvoyconfig
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"maps"
    11  	"slices"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/sirupsen/logrus"
    16  	"github.com/stretchr/testify/assert"
    17  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  	k8sRuntime "k8s.io/apimachinery/pkg/runtime"
    19  
    20  	ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    21  	"github.com/cilium/cilium/pkg/k8s/resource"
    22  	slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
    23  	"github.com/cilium/cilium/pkg/node"
    24  	"github.com/cilium/cilium/pkg/node/types"
    25  )
    26  
    27  type configTestCase struct {
    28  	name              string
    29  	configs           map[resource.Key]*config
    30  	currentNodeLabels map[string]string
    31  	kind              resource.EventKind
    32  	configSpecOpt     func(spec *ciliumv2.CiliumEnvoyConfigSpec)
    33  	shouldFailFor     []string
    34  	configKey         resource.Key
    35  	expectedError     bool
    36  	expectedAdded     []string
    37  	expectedUpdated   []string
    38  	expectedDeleted   []string
    39  }
    40  
    41  var configTestCases = []configTestCase{
    42  	// Additions
    43  	{
    44  		name:              "Upsert event: new / no nodeselector / empty list of node labels / match",
    45  		configs:           map[resource.Key]*config{},
    46  		currentNodeLabels: map[string]string{},
    47  		configSpecOpt:     withoutNodeSelector(),
    48  		kind:              resource.Upsert,
    49  		expectedError:     false,
    50  		expectedAdded:     []string{"test/test"},
    51  		expectedUpdated:   []string{},
    52  		expectedDeleted:   []string{},
    53  	},
    54  	{
    55  		name:              "Upsert event: new / no nodeselector / populated list of node labels / match",
    56  		configs:           map[resource.Key]*config{},
    57  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
    58  		configSpecOpt:     withoutNodeSelector(),
    59  		kind:              resource.Upsert,
    60  		expectedError:     false,
    61  		expectedAdded:     []string{"test/test"},
    62  		expectedUpdated:   []string{},
    63  		expectedDeleted:   []string{},
    64  	},
    65  	{
    66  		name:              "Upsert event: new / existing nodeselector / populated list of node labels / match",
    67  		configs:           map[resource.Key]*config{},
    68  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
    69  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "infra"}),
    70  		kind:              resource.Upsert,
    71  		expectedError:     false,
    72  		expectedAdded:     []string{"test/test"},
    73  		expectedUpdated:   []string{},
    74  		expectedDeleted:   []string{},
    75  	},
    76  	{
    77  		name:              "Upsert event: new / existing nodeselector / populated list of node labels / no match",
    78  		configs:           map[resource.Key]*config{},
    79  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
    80  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "app"}),
    81  		kind:              resource.Upsert,
    82  		expectedError:     false,
    83  		expectedAdded:     []string{},
    84  		expectedUpdated:   []string{},
    85  		expectedDeleted:   []string{},
    86  	},
    87  	{
    88  		name:              "Upsert event: new / existing nodeselector / empty list of node labels / no match",
    89  		configs:           map[resource.Key]*config{},
    90  		currentNodeLabels: map[string]string{},
    91  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "app"}),
    92  		kind:              resource.Upsert,
    93  		expectedError:     false,
    94  		expectedAdded:     []string{},
    95  		expectedUpdated:   []string{},
    96  		expectedDeleted:   []string{},
    97  	},
    98  
    99  	// Updates
   100  	{
   101  		name: "Upsert event: update / no nodeselector / empty list of node labels / match / previously match",
   102  		configs: map[resource.Key]*config{
   103  			{Namespace: "test", Name: "test"}: testConfig("test", "test", nil, true),
   104  		},
   105  		currentNodeLabels: map[string]string{},
   106  		configSpecOpt:     withoutNodeSelector(),
   107  		kind:              resource.Upsert,
   108  		expectedError:     false,
   109  		expectedAdded:     []string{},
   110  		expectedUpdated:   []string{"test/test"},
   111  		expectedDeleted:   []string{},
   112  	},
   113  	{
   114  		name: "Upsert event: update / no nodeselector / empty list of node labels / match / previously no match",
   115  		configs: map[resource.Key]*config{
   116  			{Namespace: "test", Name: "test"}: testConfig("test", "test", nil, false),
   117  		},
   118  		currentNodeLabels: map[string]string{},
   119  		configSpecOpt:     withoutNodeSelector(),
   120  		kind:              resource.Upsert,
   121  		expectedError:     false,
   122  		expectedAdded:     []string{"test/test"},
   123  		expectedUpdated:   []string{},
   124  		expectedDeleted:   []string{},
   125  	},
   126  	{
   127  		name: "Upsert event: new / no nodeselector / populated list of node labels / match / previously match",
   128  		configs: map[resource.Key]*config{
   129  			{Namespace: "test", Name: "test"}: testConfig("test", "test", nil, true),
   130  		},
   131  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
   132  		configSpecOpt:     withoutNodeSelector(),
   133  		kind:              resource.Upsert,
   134  		expectedError:     false,
   135  		expectedAdded:     []string{},
   136  		expectedUpdated:   []string{"test/test"},
   137  		expectedDeleted:   []string{},
   138  	},
   139  	{
   140  		name: "Upsert event: new / no nodeselector / populated list of node labels / match / previously no match",
   141  		configs: map[resource.Key]*config{
   142  			{Namespace: "test", Name: "test"}: testConfig("test", "test", nil, false),
   143  		},
   144  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
   145  		configSpecOpt:     withoutNodeSelector(),
   146  		kind:              resource.Upsert,
   147  		expectedError:     false,
   148  		expectedAdded:     []string{"test/test"},
   149  		expectedUpdated:   []string{},
   150  		expectedDeleted:   []string{},
   151  	},
   152  	{
   153  		name: "Upsert event: new / existing nodeselector / populated list of node labels / match / previously match",
   154  		configs: map[resource.Key]*config{
   155  			{Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "infra"}, true),
   156  		},
   157  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
   158  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "infra"}),
   159  		kind:              resource.Upsert,
   160  		expectedError:     false,
   161  		expectedAdded:     []string{},
   162  		expectedUpdated:   []string{"test/test"},
   163  		expectedDeleted:   []string{},
   164  	},
   165  	{
   166  		name: "Upsert event: new / existing nodeselector / populated list of node labels / match / previously no match",
   167  		configs: map[resource.Key]*config{
   168  			{Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "infra"}, false),
   169  		},
   170  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
   171  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "infra"}),
   172  		kind:              resource.Upsert,
   173  		expectedError:     false,
   174  		expectedAdded:     []string{"test/test"},
   175  		expectedUpdated:   []string{},
   176  		expectedDeleted:   []string{},
   177  	},
   178  	{
   179  		name: "Upsert event: new / existing nodeselector / populated list of node labels / no match / previously match",
   180  		configs: map[resource.Key]*config{
   181  			{Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "infra"}, true),
   182  		},
   183  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
   184  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "app"}),
   185  		kind:              resource.Upsert,
   186  		expectedError:     false,
   187  		expectedAdded:     []string{},
   188  		expectedUpdated:   []string{},
   189  		expectedDeleted:   []string{"test/test"},
   190  	},
   191  	{
   192  		name: "Upsert event: new / existing nodeselector / populated list of node labels / no match / previously no match",
   193  		configs: map[resource.Key]*config{
   194  			{Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "app"}, false),
   195  		},
   196  		currentNodeLabels: map[string]string{"role": "infra", "name": "node1"},
   197  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "app"}),
   198  		kind:              resource.Upsert,
   199  		expectedError:     false,
   200  		expectedAdded:     []string{},
   201  		expectedUpdated:   []string{},
   202  		expectedDeleted:   []string{},
   203  	},
   204  	{
   205  		name: "Upsert event: new / existing nodeselector / empty list of node labels / no match / previously match",
   206  		configs: map[resource.Key]*config{
   207  			{Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "infra"}, true),
   208  		},
   209  		currentNodeLabels: map[string]string{},
   210  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "app"}),
   211  		kind:              resource.Upsert,
   212  		expectedError:     false,
   213  		expectedAdded:     []string{},
   214  		expectedUpdated:   []string{},
   215  		expectedDeleted:   []string{"test/test"},
   216  	},
   217  	{
   218  		name: "Upsert event: new / existing nodeselector / empty list of node labels / no match / previously no match",
   219  		configs: map[resource.Key]*config{
   220  			{Namespace: "test", Name: "test"}: testConfig("test", "test", map[string]string{"role": "app"}, false),
   221  		},
   222  		currentNodeLabels: map[string]string{},
   223  		configSpecOpt:     withNodeLabelSelector(map[string]string{"role": "app"}),
   224  		kind:              resource.Upsert,
   225  		expectedError:     false,
   226  		expectedAdded:     []string{},
   227  		expectedUpdated:   []string{},
   228  		expectedDeleted:   []string{},
   229  	},
   230  
   231  	// Deletions
   232  	{
   233  		name: "Delete event: existing / previously matched",
   234  		configs: map[resource.Key]*config{
   235  			{Namespace: "test", Name: "test"}: testConfig("test", "test", nil, true),
   236  		},
   237  		currentNodeLabels: map[string]string{},
   238  		configKey:         resource.Key{Namespace: "test", Name: "test"},
   239  		kind:              resource.Delete,
   240  		expectedError:     false,
   241  		expectedAdded:     []string{},
   242  		expectedUpdated:   []string{},
   243  		expectedDeleted:   []string{"test/test"},
   244  	},
   245  	{
   246  		name: "Delete event: existing / previously not matched",
   247  		configs: map[resource.Key]*config{
   248  			{Namespace: "test", Name: "test"}: testConfig("test", "test", nil, false),
   249  		},
   250  		currentNodeLabels: map[string]string{},
   251  		configKey:         resource.Key{Namespace: "test", Name: "test"},
   252  		kind:              resource.Delete,
   253  		expectedError:     false,
   254  		expectedAdded:     []string{},
   255  		expectedUpdated:   []string{},
   256  		expectedDeleted:   []string{},
   257  	},
   258  	{
   259  		name:              "Delete event: not existing",
   260  		configs:           map[resource.Key]*config{},
   261  		currentNodeLabels: map[string]string{},
   262  		configKey:         resource.Key{Namespace: "test", Name: "test"},
   263  		kind:              resource.Delete,
   264  		expectedError:     false,
   265  		expectedAdded:     []string{},
   266  		expectedUpdated:   []string{},
   267  		expectedDeleted:   []string{},
   268  	},
   269  
   270  	// Synced
   271  	{
   272  		name:              "Sync events shouldn't be handled",
   273  		configs:           map[resource.Key]*config{},
   274  		currentNodeLabels: map[string]string{},
   275  		configSpecOpt:     withoutNodeSelector(),
   276  		kind:              resource.Sync,
   277  		expectedError:     false,
   278  		expectedAdded:     []string{},
   279  		expectedUpdated:   []string{},
   280  		expectedDeleted:   []string{},
   281  	},
   282  }
   283  
   284  func TestHandleCECEvent(t *testing.T) {
   285  	executeForConfigType(t,
   286  		configTestCases,
   287  		testCEC,
   288  		func(reconciler *ciliumEnvoyConfigReconciler) func(context.Context, resource.Event[*ciliumv2.CiliumEnvoyConfig]) error {
   289  			return reconciler.handleCECEvent
   290  		},
   291  	)
   292  }
   293  
   294  func TestHandleCCECEvent(t *testing.T) {
   295  	executeForConfigType(t,
   296  		configTestCases,
   297  		testCCEC,
   298  		func(reconciler *ciliumEnvoyConfigReconciler) func(context.Context, resource.Event[*ciliumv2.CiliumClusterwideEnvoyConfig]) error {
   299  			return reconciler.handleCCECEvent
   300  		},
   301  	)
   302  }
   303  
   304  // executeForConfigType executes the given test casese for the CEC and CCEC
   305  func executeForConfigType[T k8sRuntime.Object](t *testing.T,
   306  	tests []configTestCase,
   307  	createConfigFunc func(opts ...cecOpts) T,
   308  	handleEventFunc func(*ciliumEnvoyConfigReconciler) func(context.Context, resource.Event[T]) error,
   309  ) {
   310  	for _, tc := range tests {
   311  		t.Run(tc.name, func(t *testing.T) {
   312  			logger := logrus.New()
   313  			logger.SetOutput(io.Discard)
   314  
   315  			manager := &fakeCECManager{
   316  				shouldFailFor: tc.shouldFailFor,
   317  			}
   318  
   319  			reconciler := newCiliumEnvoyConfigReconciler(reconcilerParams{Logger: logger, Manager: manager})
   320  
   321  			// init current state
   322  			configs := map[resource.Key]*config{}
   323  			maps.Copy(configs, tc.configs)
   324  			reconciler.configs = configs
   325  
   326  			currentNodeLabels := map[string]string{}
   327  			maps.Copy(currentNodeLabels, tc.currentNodeLabels)
   328  			reconciler.localNodeLabels = currentNodeLabels
   329  
   330  			doneCalled := false
   331  			var doneError error
   332  
   333  			doneFunc := func(err error) {
   334  				doneCalled = true
   335  				doneError = err
   336  			}
   337  
   338  			event := resource.Event[T]{}
   339  			event.Kind = tc.kind
   340  			if len(tc.configKey.Name) == 0 && len(tc.configKey.Namespace) == 0 && tc.configSpecOpt != nil {
   341  				event.Object = createConfigFunc(tc.configSpecOpt)
   342  				event.Key = resource.NewKey(event.Object)
   343  			} else {
   344  				event.Key = tc.configKey
   345  			}
   346  			event.Done = doneFunc
   347  
   348  			err := handleEventFunc(reconciler)(context.Background(), event)
   349  			assert.Equal(t, tc.expectedError, err != nil)
   350  
   351  			assert.True(t, doneCalled, "Done must be called on the event in all cases")
   352  			assert.Equal(t, tc.expectedError, doneError != nil, "Expected done error should match")
   353  
   354  			assert.ElementsMatch(t, tc.expectedAdded, manager.addedConfigNames, "Expected added configs should match")
   355  			assert.ElementsMatch(t, tc.expectedUpdated, manager.updatedConfigNames, "Expected updated configs should match")
   356  			assert.ElementsMatch(t, tc.expectedDeleted, manager.deletedConfigNames, "Expected deleted configs should match")
   357  
   358  			// Assert that the stored state whether a config selects the local Node or not has been updated
   359  			for _, n := range append(manager.addedConfigNames, manager.updatedConfigNames...) {
   360  				split := strings.Split(n, "/")
   361  				ns, name := split[0], split[1]
   362  				assert.True(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode)
   363  			}
   364  			for _, n := range manager.deletedConfigNames {
   365  				split := strings.Split(n, "/")
   366  				ns, name := split[0], split[1]
   367  				if event.Kind == resource.Delete {
   368  					assert.NotContains(t, reconciler.configs, resource.Key{Namespace: ns, Name: name},
   369  						"Deleted configs due to deletion event should be deleted from local cache")
   370  				} else {
   371  					assert.False(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode,
   372  						"Deleted configs due to update should be kept in the local cache - but marked as not selecting local node")
   373  				}
   374  			}
   375  		})
   376  	}
   377  }
   378  
   379  type cecOpts func(spec *ciliumv2.CiliumEnvoyConfigSpec)
   380  
   381  func withoutNodeSelector() func(spec *ciliumv2.CiliumEnvoyConfigSpec) {
   382  	return func(spec *ciliumv2.CiliumEnvoyConfigSpec) {
   383  		spec.NodeSelector = nil
   384  	}
   385  }
   386  
   387  func withNodeLabelSelector(labels map[string]string) func(spec *ciliumv2.CiliumEnvoyConfigSpec) {
   388  	return func(spec *ciliumv2.CiliumEnvoyConfigSpec) {
   389  		spec.NodeSelector = &slim_metav1.LabelSelector{
   390  			MatchLabels: labels,
   391  		}
   392  	}
   393  }
   394  
   395  func testCEC(opts ...cecOpts) *ciliumv2.CiliumEnvoyConfig {
   396  	cec := &ciliumv2.CiliumEnvoyConfig{
   397  		ObjectMeta: metav1.ObjectMeta{
   398  			Namespace: "test",
   399  			Name:      "test",
   400  		},
   401  		Spec: ciliumv2.CiliumEnvoyConfigSpec{},
   402  	}
   403  
   404  	for _, opt := range opts {
   405  		opt(&cec.Spec)
   406  	}
   407  
   408  	return cec
   409  }
   410  
   411  func testCCEC(opts ...cecOpts) *ciliumv2.CiliumClusterwideEnvoyConfig {
   412  	ccec := &ciliumv2.CiliumClusterwideEnvoyConfig{
   413  		ObjectMeta: metav1.ObjectMeta{
   414  			Namespace: "test",
   415  			Name:      "test",
   416  		},
   417  		Spec: ciliumv2.CiliumEnvoyConfigSpec{},
   418  	}
   419  
   420  	for _, opt := range opts {
   421  		opt(&ccec.Spec)
   422  	}
   423  
   424  	return ccec
   425  }
   426  
   427  func TestReconcileExistingConfigs(t *testing.T) {
   428  	tests := []struct {
   429  		name                  string
   430  		configs               map[resource.Key]*config
   431  		currentNodeLabels     map[string]string
   432  		failFor               []string
   433  		expectedError         bool
   434  		expectedErrorMessages []string
   435  		expectedAdded         []string
   436  		expectedDeleted       []string
   437  	}{
   438  		{
   439  			name:              "No changes if no configs are present",
   440  			configs:           map[resource.Key]*config{},
   441  			currentNodeLabels: map[string]string{},
   442  			expectedError:     false,
   443  			expectedAdded:     []string{},
   444  			expectedDeleted:   []string{},
   445  		},
   446  		{
   447  			name: "No changes if there are no changes in configs selecting nodes or not",
   448  			configs: map[resource.Key]*config{
   449  				{Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", nil, true),
   450  				{Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true),
   451  			},
   452  			currentNodeLabels: map[string]string{
   453  				"role": "worker",
   454  			},
   455  			expectedError:   false,
   456  			expectedAdded:   []string{},
   457  			expectedDeleted: []string{},
   458  		},
   459  		{
   460  			name: "Delete configs that no longer select the local node",
   461  			configs: map[resource.Key]*config{
   462  				{Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra"}, true),
   463  				{Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true),
   464  			},
   465  			currentNodeLabels: map[string]string{
   466  				"role": "worker",
   467  			},
   468  			expectedError:   false,
   469  			expectedAdded:   []string{},
   470  			expectedDeleted: []string{"ns1/config1"},
   471  		},
   472  		{
   473  			name: "Add configs that start to select the local node",
   474  			configs: map[resource.Key]*config{
   475  				{Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra"}, false),
   476  				{Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true),
   477  			},
   478  			currentNodeLabels: map[string]string{
   479  				"role": "infra",
   480  			},
   481  			expectedError:   false,
   482  			expectedAdded:   []string{"ns1/config1"},
   483  			expectedDeleted: []string{},
   484  		},
   485  		{
   486  			name: "Multiple changes",
   487  			configs: map[resource.Key]*config{
   488  				{Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra", "node": "node1"}, false),
   489  				{Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", map[string]string{"role": "infra", "node": "node1"}, false),
   490  				{Namespace: "ns1", Name: "config3"}: testConfig("ns1", "config3", map[string]string{"role": "infra", "node": "node1", "environment": "test"}, false),
   491  				{Namespace: "ns1", Name: "config4"}: testConfig("ns1", "config4", nil, true),
   492  				{Namespace: "ns1", Name: "config5"}: testConfig("ns1", "config5", map[string]string{"role": "worker", "node": "node1"}, true),
   493  				{Namespace: "ns1", Name: "config6"}: testConfig("ns1", "config6", map[string]string{"role": "worker", "node": "node1"}, true),
   494  				{Namespace: "ns1", Name: "config7"}: testConfig("ns1", "config7", map[string]string{"role": "worker", "node": "node1", "environment": "test"}, false),
   495  			},
   496  			currentNodeLabels: map[string]string{
   497  				"node": "node1",
   498  				"role": "infra",
   499  			},
   500  			expectedError:   false,
   501  			expectedAdded:   []string{"ns1/config1", "ns1/config2"},
   502  			expectedDeleted: []string{"ns1/config5", "ns1/config6"},
   503  		},
   504  		{
   505  			name: "Failures during updating individual configs should't abort",
   506  			configs: map[resource.Key]*config{
   507  				{Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra", "node": "node1"}, false),
   508  				{Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", map[string]string{"role": "infra", "node": "node1"}, false),
   509  				{Namespace: "ns1", Name: "config3"}: testConfig("ns1", "config3", map[string]string{"role": "infra", "node": "node1", "environment": "test"}, false),
   510  				{Namespace: "ns1", Name: "config4"}: testConfig("ns1", "config4", nil, true),
   511  				{Namespace: "ns1", Name: "config5"}: testConfig("ns1", "config5", map[string]string{"role": "worker", "node": "node1"}, true),
   512  				{Namespace: "ns1", Name: "config6"}: testConfig("ns1", "config6", map[string]string{"role": "worker", "node": "node1"}, true),
   513  				{Namespace: "ns1", Name: "config7"}: testConfig("ns1", "config7", map[string]string{"role": "worker", "node": "node1", "environment": "test"}, false),
   514  			},
   515  			currentNodeLabels: map[string]string{
   516  				"node": "node1",
   517  				"role": "infra",
   518  			},
   519  			failFor:       []string{"ns1/config2", "ns1/config5"},
   520  			expectedError: true,
   521  			expectedErrorMessages: []string{
   522  				"failed to reconcile existing config (ns1/config2): failed to add config ns1/config2",
   523  				"failed to reconcile existing config (ns1/config5): failed to delete config ns1/config5",
   524  			},
   525  			expectedAdded:   []string{"ns1/config1"},
   526  			expectedDeleted: []string{"ns1/config6"},
   527  		},
   528  	}
   529  
   530  	for _, tc := range tests {
   531  		t.Run(tc.name, func(t *testing.T) {
   532  			logger := logrus.New()
   533  			logger.SetOutput(io.Discard)
   534  
   535  			manager := &fakeCECManager{
   536  				shouldFailFor: tc.failFor,
   537  			}
   538  
   539  			reconciler := newCiliumEnvoyConfigReconciler(reconcilerParams{Logger: logger, Manager: manager})
   540  
   541  			// init current state
   542  			reconciler.configs = make(map[resource.Key]*config, len(tc.configs))
   543  			for k, v := range tc.configs {
   544  				reconciler.configs[k] = &config{
   545  					meta:             v.meta,
   546  					spec:             v.spec.DeepCopy(),
   547  					selectsLocalNode: v.selectsLocalNode,
   548  				}
   549  			}
   550  			reconciler.localNodeLabels = tc.currentNodeLabels
   551  
   552  			err := reconciler.reconcileExistingConfigs(context.Background())
   553  			assert.Equal(t, tc.expectedError, err != nil)
   554  			if tc.expectedError {
   555  				for _, expectedErrorMessage := range tc.expectedErrorMessages {
   556  					assert.ErrorContains(t, err, expectedErrorMessage)
   557  				}
   558  			}
   559  
   560  			assert.ElementsMatch(t, tc.expectedAdded, manager.addedConfigNames)
   561  			assert.ElementsMatch(t, tc.expectedDeleted, manager.deletedConfigNames)
   562  
   563  			assert.Empty(t, manager.updatedConfigNames, "Should never update an existing config")
   564  
   565  			// Assert that the stored state whether a config selects the local Node or not has been updated
   566  			for _, n := range manager.addedConfigNames {
   567  				split := strings.Split(n, "/")
   568  				ns, name := split[0], split[1]
   569  				assert.True(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode)
   570  			}
   571  
   572  			for _, n := range manager.deletedConfigNames {
   573  				split := strings.Split(n, "/")
   574  				ns, name := split[0], split[1]
   575  				assert.False(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode)
   576  			}
   577  
   578  			// Check that state didn't change for configs that failed to reconcile
   579  			for _, n := range tc.failFor {
   580  				split := strings.Split(n, "/")
   581  				ns, name := split[0], split[1]
   582  				assert.Equal(t,
   583  					tc.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode,
   584  					reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode,
   585  					"Configs shouldn't change their selection state if their reconciliation failed",
   586  				)
   587  			}
   588  		})
   589  	}
   590  }
   591  
   592  func TestHandleLocalNodeLabels(t *testing.T) {
   593  	tests := []struct {
   594  		name              string
   595  		configs           map[resource.Key]*config
   596  		currentNodeLabels map[string]string
   597  		newNodeLabels     map[string]string
   598  		failFor           []string
   599  		expectedDeleted   []string
   600  	}{
   601  		{
   602  			name:              "No changes if no configs are present",
   603  			configs:           map[resource.Key]*config{},
   604  			currentNodeLabels: map[string]string{},
   605  			newNodeLabels:     map[string]string{},
   606  			expectedDeleted:   []string{},
   607  		},
   608  		{
   609  			name: "No changes if node labels don't change",
   610  			configs: map[resource.Key]*config{
   611  				{Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", nil, true),
   612  			},
   613  			currentNodeLabels: map[string]string{
   614  				"role": "infra",
   615  			},
   616  			newNodeLabels: map[string]string{
   617  				"role": "infra",
   618  			},
   619  			expectedDeleted: []string{},
   620  		},
   621  		{
   622  			name: "No changes if there are no changes in configs selecting nodes or not",
   623  			configs: map[resource.Key]*config{
   624  				{Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", nil, true),
   625  				{Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true),
   626  			},
   627  			currentNodeLabels: map[string]string{
   628  				"role": "infra",
   629  			},
   630  			newNodeLabels: map[string]string{
   631  				"role": "worker",
   632  			},
   633  			expectedDeleted: []string{},
   634  		},
   635  		{
   636  			name: "Updated node labels triggers a best-effort reconciliation of existing configs",
   637  			configs: map[resource.Key]*config{
   638  				{Namespace: "ns1", Name: "config1"}: testConfig("ns1", "config1", map[string]string{"role": "infra"}, true),
   639  				{Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", nil, true),
   640  			},
   641  			currentNodeLabels: map[string]string{
   642  				"role": "infra",
   643  			},
   644  			newNodeLabels: map[string]string{
   645  				"role": "worker",
   646  			},
   647  			expectedDeleted: []string{"ns1/config1"},
   648  		},
   649  		{
   650  			name: "Failures during updating individual configs should't result in any error - as it's only best effort",
   651  			configs: map[resource.Key]*config{
   652  				{Namespace: "ns1", Name: "config2"}: testConfig("ns1", "config2", map[string]string{"role": "infra", "node": "node1"}, false),
   653  			},
   654  			currentNodeLabels: map[string]string{
   655  				"node": "node1",
   656  				"role": "worker",
   657  			},
   658  			newNodeLabels: map[string]string{
   659  				"node": "node1",
   660  				"role": "infra",
   661  			},
   662  			failFor:         []string{"ns1/config2"},
   663  			expectedDeleted: []string{},
   664  		},
   665  	}
   666  
   667  	for _, tc := range tests {
   668  		t.Run(tc.name, func(t *testing.T) {
   669  			logger := logrus.New()
   670  			logger.SetOutput(io.Discard)
   671  
   672  			manager := &fakeCECManager{
   673  				shouldFailFor: tc.failFor,
   674  			}
   675  
   676  			reconciler := newCiliumEnvoyConfigReconciler(reconcilerParams{Logger: logger, Manager: manager})
   677  
   678  			// init current state
   679  			reconciler.configs = make(map[resource.Key]*config, len(tc.configs))
   680  			for k, v := range tc.configs {
   681  				reconciler.configs[k] = &config{
   682  					meta:             v.meta,
   683  					spec:             v.spec.DeepCopy(),
   684  					selectsLocalNode: v.selectsLocalNode,
   685  				}
   686  			}
   687  			reconciler.localNodeLabels = tc.currentNodeLabels
   688  
   689  			node := node.LocalNode{Node: types.Node{Name: "test", Labels: tc.newNodeLabels}}
   690  
   691  			err := reconciler.handleLocalNodeEvent(context.Background(), node)
   692  			assert.NoError(t, err)
   693  
   694  			assert.Equal(t, tc.newNodeLabels, reconciler.localNodeLabels)
   695  
   696  			assert.ElementsMatch(t, tc.expectedDeleted, manager.deletedConfigNames)
   697  
   698  			assert.Empty(t, manager.addedConfigNames)
   699  			assert.Empty(t, manager.updatedConfigNames, "Should never update an existing config")
   700  
   701  			// Assert that the stored state whether a config selects the local Node or not has been updated
   702  			for _, n := range manager.addedConfigNames {
   703  				split := strings.Split(n, "/")
   704  				ns, name := split[0], split[1]
   705  				assert.True(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode)
   706  			}
   707  
   708  			for _, n := range manager.deletedConfigNames {
   709  				split := strings.Split(n, "/")
   710  				ns, name := split[0], split[1]
   711  				assert.False(t, reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode)
   712  			}
   713  
   714  			// Check that state didn't change for configs that failed to reconcile
   715  			for _, n := range tc.failFor {
   716  				split := strings.Split(n, "/")
   717  				ns, name := split[0], split[1]
   718  				assert.Equal(t,
   719  					tc.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode,
   720  					reconciler.configs[resource.Key{Namespace: ns, Name: name}].selectsLocalNode,
   721  					"Configs shouldn't change their selection state if their reconciliation failed",
   722  				)
   723  			}
   724  		})
   725  	}
   726  }
   727  
   728  func testConfig(namespace string, name string, nodeSelectorLabels map[string]string, selectsLocalNode bool) *config {
   729  	cfg := &config{
   730  		meta: metav1.ObjectMeta{
   731  			Namespace: namespace,
   732  			Name:      name,
   733  		},
   734  		spec:             &ciliumv2.CiliumEnvoyConfigSpec{},
   735  		selectsLocalNode: selectsLocalNode,
   736  	}
   737  
   738  	if nodeSelectorLabels != nil {
   739  		cfg.spec.NodeSelector = &slim_metav1.LabelSelector{
   740  			MatchLabels: nodeSelectorLabels,
   741  		}
   742  	}
   743  
   744  	return cfg
   745  }
   746  
   747  type fakeCECManager struct {
   748  	addedConfigNames   []string
   749  	deletedConfigNames []string
   750  	updatedConfigNames []string
   751  	shouldFailFor      []string
   752  }
   753  
   754  var _ ciliumEnvoyConfigManager = &fakeCECManager{}
   755  
   756  func (r *fakeCECManager) addCiliumEnvoyConfig(cecObjectMeta metav1.ObjectMeta, cecSpec *ciliumv2.CiliumEnvoyConfigSpec) error {
   757  	namespacedName := fmt.Sprintf("%s/%s", cecObjectMeta.Namespace, cecObjectMeta.Name)
   758  
   759  	if slices.Contains(r.shouldFailFor, namespacedName) {
   760  		return fmt.Errorf("failed to add config %s", namespacedName)
   761  	}
   762  
   763  	r.addedConfigNames = append(r.addedConfigNames, namespacedName)
   764  
   765  	return nil
   766  }
   767  
   768  func (r *fakeCECManager) deleteCiliumEnvoyConfig(cecObjectMeta metav1.ObjectMeta, cecSpec *ciliumv2.CiliumEnvoyConfigSpec) error {
   769  	namespacedName := fmt.Sprintf("%s/%s", cecObjectMeta.Namespace, cecObjectMeta.Name)
   770  
   771  	if slices.Contains(r.shouldFailFor, namespacedName) {
   772  		return fmt.Errorf("failed to delete config %s", namespacedName)
   773  	}
   774  
   775  	r.deletedConfigNames = append(r.deletedConfigNames, namespacedName)
   776  
   777  	return nil
   778  }
   779  
   780  func (r *fakeCECManager) updateCiliumEnvoyConfig(oldCECObjectMeta metav1.ObjectMeta, oldCECSpec *ciliumv2.CiliumEnvoyConfigSpec, newCECObjectMeta metav1.ObjectMeta, newCECSpec *ciliumv2.CiliumEnvoyConfigSpec) error {
   781  	namespacedName := fmt.Sprintf("%s/%s", newCECObjectMeta.Namespace, newCECObjectMeta.Name)
   782  
   783  	if slices.Contains(r.shouldFailFor, namespacedName) {
   784  		return fmt.Errorf("failed to update config %s", namespacedName)
   785  	}
   786  
   787  	r.updatedConfigNames = append(r.updatedConfigNames, namespacedName)
   788  
   789  	return nil
   790  }
   791  
   792  func (r *fakeCECManager) syncCiliumEnvoyConfigService(name string, namespace string, cecSpec *ciliumv2.CiliumEnvoyConfigSpec) error {
   793  	return nil
   794  }