k8s.io/kubernetes@v1.29.3/pkg/controller/endpointslicemirroring/reconciler_test.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package endpointslicemirroring
    18  
    19  import (
    20  	"context"
    21  	"strings"
    22  	"testing"
    23  
    24  	corev1 "k8s.io/api/core/v1"
    25  	discovery "k8s.io/api/discovery/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	"k8s.io/client-go/kubernetes/fake"
    28  	"k8s.io/client-go/kubernetes/scheme"
    29  	"k8s.io/client-go/tools/record"
    30  	"k8s.io/component-base/metrics/testutil"
    31  	endpointsliceutil "k8s.io/endpointslice/util"
    32  	"k8s.io/klog/v2/ktesting"
    33  	endpointsv1 "k8s.io/kubernetes/pkg/api/v1/endpoints"
    34  	"k8s.io/kubernetes/pkg/controller/endpointslicemirroring/metrics"
    35  	"k8s.io/utils/pointer"
    36  )
    37  
    38  const defaultMaxEndpointsPerSubset = int32(1000)
    39  
    40  // TestReconcile ensures that Endpoints are reconciled into corresponding
    41  // EndpointSlices with appropriate fields.
    42  func TestReconcile(t *testing.T) {
    43  	protoTCP := corev1.ProtocolTCP
    44  	protoUDP := corev1.ProtocolUDP
    45  
    46  	testCases := []struct {
    47  		testName                 string
    48  		subsets                  []corev1.EndpointSubset
    49  		epLabels                 map[string]string
    50  		epAnnotations            map[string]string
    51  		endpointsDeletionPending bool
    52  		maxEndpointsPerSubset    int32
    53  		existingEndpointSlices   []*discovery.EndpointSlice
    54  		expectedNumSlices        int
    55  		expectedClientActions    int
    56  		expectedMetrics          *expectedMetrics
    57  	}{{
    58  		testName:               "Endpoints with no subsets",
    59  		subsets:                []corev1.EndpointSubset{},
    60  		existingEndpointSlices: []*discovery.EndpointSlice{},
    61  		expectedNumSlices:      0,
    62  		expectedClientActions:  0,
    63  		expectedMetrics:        &expectedMetrics{},
    64  	}, {
    65  		testName: "Endpoints with no addresses",
    66  		subsets: []corev1.EndpointSubset{{
    67  			Ports: []corev1.EndpointPort{{
    68  				Name:     "http",
    69  				Port:     80,
    70  				Protocol: corev1.ProtocolTCP,
    71  			}},
    72  		}},
    73  		existingEndpointSlices: []*discovery.EndpointSlice{},
    74  		expectedNumSlices:      0,
    75  		expectedClientActions:  0,
    76  		expectedMetrics:        &expectedMetrics{},
    77  	}, {
    78  		testName: "Endpoints with 1 subset, port, and address",
    79  		subsets: []corev1.EndpointSubset{{
    80  			Ports: []corev1.EndpointPort{{
    81  				Name:     "http",
    82  				Port:     80,
    83  				Protocol: corev1.ProtocolTCP,
    84  			}},
    85  			Addresses: []corev1.EndpointAddress{{
    86  				IP:       "10.0.0.1",
    87  				Hostname: "pod-1",
    88  				NodeName: pointer.String("node-1"),
    89  			}},
    90  		}},
    91  		existingEndpointSlices: []*discovery.EndpointSlice{},
    92  		expectedNumSlices:      1,
    93  		expectedClientActions:  1,
    94  		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 1, addedPerSync: 1, numCreated: 1},
    95  	}, {
    96  		testName: "Endpoints with 2 subset, different port and address",
    97  		subsets: []corev1.EndpointSubset{
    98  			{
    99  				Ports: []corev1.EndpointPort{{
   100  					Name:     "http",
   101  					Port:     80,
   102  					Protocol: corev1.ProtocolTCP,
   103  				}},
   104  				Addresses: []corev1.EndpointAddress{{
   105  					IP:       "10.0.0.1",
   106  					Hostname: "pod-1",
   107  					NodeName: pointer.String("node-1"),
   108  				}},
   109  			},
   110  			{
   111  				Ports: []corev1.EndpointPort{{
   112  					Name:     "https",
   113  					Port:     443,
   114  					Protocol: corev1.ProtocolTCP,
   115  				}},
   116  				Addresses: []corev1.EndpointAddress{{
   117  					IP:       "10.0.0.2",
   118  					Hostname: "pod-2",
   119  					NodeName: pointer.String("node-1"),
   120  				}},
   121  			},
   122  		},
   123  		existingEndpointSlices: []*discovery.EndpointSlice{},
   124  		expectedNumSlices:      2,
   125  		expectedClientActions:  2,
   126  		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 2, addedPerSync: 2, numCreated: 2},
   127  	}, {
   128  		testName: "Endpoints with 2 subset, different port and same address",
   129  		subsets: []corev1.EndpointSubset{
   130  			{
   131  				Ports: []corev1.EndpointPort{{
   132  					Name:     "http",
   133  					Port:     80,
   134  					Protocol: corev1.ProtocolTCP,
   135  				}},
   136  				Addresses: []corev1.EndpointAddress{{
   137  					IP:       "10.0.0.1",
   138  					Hostname: "pod-1",
   139  					NodeName: pointer.String("node-1"),
   140  				}},
   141  			},
   142  			{
   143  				Ports: []corev1.EndpointPort{{
   144  					Name:     "https",
   145  					Port:     443,
   146  					Protocol: corev1.ProtocolTCP,
   147  				}},
   148  				Addresses: []corev1.EndpointAddress{{
   149  					IP:       "10.0.0.1",
   150  					Hostname: "pod-1",
   151  					NodeName: pointer.String("node-1"),
   152  				}},
   153  			},
   154  		},
   155  		existingEndpointSlices: []*discovery.EndpointSlice{},
   156  		expectedNumSlices:      1,
   157  		expectedClientActions:  1,
   158  		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 1, addedPerSync: 1, numCreated: 1},
   159  	}, {
   160  		testName: "Endpoints with 2 subset, different address and same port",
   161  		subsets: []corev1.EndpointSubset{
   162  			{
   163  				Ports: []corev1.EndpointPort{{
   164  					Name:     "http",
   165  					Port:     80,
   166  					Protocol: corev1.ProtocolTCP,
   167  				}},
   168  				Addresses: []corev1.EndpointAddress{{
   169  					IP:       "10.0.0.1",
   170  					Hostname: "pod-1",
   171  					NodeName: pointer.String("node-1"),
   172  				}},
   173  			},
   174  			{
   175  				Ports: []corev1.EndpointPort{{
   176  					Name:     "http",
   177  					Port:     80,
   178  					Protocol: corev1.ProtocolTCP,
   179  				}},
   180  				Addresses: []corev1.EndpointAddress{{
   181  					IP:       "10.0.0.2",
   182  					Hostname: "pod-2",
   183  					NodeName: pointer.String("node-1"),
   184  				}},
   185  			},
   186  		},
   187  		existingEndpointSlices: []*discovery.EndpointSlice{},
   188  		expectedNumSlices:      1,
   189  		expectedClientActions:  1,
   190  		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1},
   191  	}, {
   192  		testName: "Endpoints with 1 subset, port, and address, pending deletion",
   193  		subsets: []corev1.EndpointSubset{{
   194  			Ports: []corev1.EndpointPort{{
   195  				Name:     "http",
   196  				Port:     80,
   197  				Protocol: corev1.ProtocolTCP,
   198  			}},
   199  			Addresses: []corev1.EndpointAddress{{
   200  				IP:       "10.0.0.1",
   201  				Hostname: "pod-1",
   202  				NodeName: pointer.String("node-1"),
   203  			}},
   204  		}},
   205  		endpointsDeletionPending: true,
   206  		existingEndpointSlices:   []*discovery.EndpointSlice{},
   207  		expectedNumSlices:        0,
   208  		expectedClientActions:    0,
   209  	}, {
   210  		testName: "Endpoints with 1 subset, port, and address and existing slice with same fields",
   211  		subsets: []corev1.EndpointSubset{{
   212  			Ports: []corev1.EndpointPort{{
   213  				Name:     "http",
   214  				Port:     80,
   215  				Protocol: corev1.ProtocolTCP,
   216  			}},
   217  			Addresses: []corev1.EndpointAddress{{
   218  				IP:       "10.0.0.1",
   219  				Hostname: "pod-1",
   220  			}},
   221  		}},
   222  		existingEndpointSlices: []*discovery.EndpointSlice{{
   223  			ObjectMeta: metav1.ObjectMeta{
   224  				Name: "test-ep-1",
   225  			},
   226  			AddressType: discovery.AddressTypeIPv4,
   227  			Ports: []discovery.EndpointPort{{
   228  				Name:     pointer.String("http"),
   229  				Port:     pointer.Int32(80),
   230  				Protocol: &protoTCP,
   231  			}},
   232  			Endpoints: []discovery.Endpoint{{
   233  				Addresses:  []string{"10.0.0.1"},
   234  				Hostname:   pointer.String("pod-1"),
   235  				Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)},
   236  			}},
   237  		}},
   238  		expectedNumSlices:     1,
   239  		expectedClientActions: 0,
   240  	}, {
   241  		testName: "Endpoints with 1 subset, port, and address and existing slice with an additional annotation",
   242  		subsets: []corev1.EndpointSubset{{
   243  			Ports: []corev1.EndpointPort{{
   244  				Name:     "http",
   245  				Port:     80,
   246  				Protocol: corev1.ProtocolTCP,
   247  			}},
   248  			Addresses: []corev1.EndpointAddress{{
   249  				IP:       "10.0.0.1",
   250  				Hostname: "pod-1",
   251  			}},
   252  		}},
   253  		existingEndpointSlices: []*discovery.EndpointSlice{{
   254  			ObjectMeta: metav1.ObjectMeta{
   255  				Name:        "test-ep-1",
   256  				Annotations: map[string]string{"foo": "bar"},
   257  			},
   258  			AddressType: discovery.AddressTypeIPv4,
   259  			Ports: []discovery.EndpointPort{{
   260  				Name:     pointer.String("http"),
   261  				Port:     pointer.Int32(80),
   262  				Protocol: &protoTCP,
   263  			}},
   264  			Endpoints: []discovery.Endpoint{{
   265  				Addresses:  []string{"10.0.0.1"},
   266  				Hostname:   pointer.String("pod-1"),
   267  				Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)},
   268  			}},
   269  		}},
   270  		expectedNumSlices:     1,
   271  		expectedClientActions: 1,
   272  	}, {
   273  		testName: "Endpoints with 1 subset, port, label and address and existing slice with same fields but the label",
   274  		subsets: []corev1.EndpointSubset{{
   275  			Ports: []corev1.EndpointPort{{
   276  				Name:     "http",
   277  				Port:     80,
   278  				Protocol: corev1.ProtocolTCP,
   279  			}},
   280  			Addresses: []corev1.EndpointAddress{{
   281  				IP:       "10.0.0.1",
   282  				Hostname: "pod-1",
   283  			}},
   284  		}},
   285  		epLabels: map[string]string{"foo": "bar"},
   286  		existingEndpointSlices: []*discovery.EndpointSlice{{
   287  			ObjectMeta: metav1.ObjectMeta{
   288  				Name:        "test-ep-1",
   289  				Annotations: map[string]string{"foo": "bar"},
   290  			},
   291  			AddressType: discovery.AddressTypeIPv4,
   292  			Ports: []discovery.EndpointPort{{
   293  				Name:     pointer.String("http"),
   294  				Port:     pointer.Int32(80),
   295  				Protocol: &protoTCP,
   296  			}},
   297  			Endpoints: []discovery.Endpoint{{
   298  				Addresses:  []string{"10.0.0.1"},
   299  				Hostname:   pointer.String("pod-1"),
   300  				Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)},
   301  			}},
   302  		}},
   303  		expectedNumSlices:     1,
   304  		expectedClientActions: 1,
   305  	}, {
   306  		testName: "Endpoints with 1 subset, 2 ports, and 2 addresses",
   307  		subsets: []corev1.EndpointSubset{{
   308  			Ports: []corev1.EndpointPort{{
   309  				Name:     "http",
   310  				Port:     80,
   311  				Protocol: corev1.ProtocolTCP,
   312  			}, {
   313  				Name:     "https",
   314  				Port:     443,
   315  				Protocol: corev1.ProtocolUDP,
   316  			}},
   317  			Addresses: []corev1.EndpointAddress{{
   318  				IP:       "10.0.0.1",
   319  				Hostname: "pod-1",
   320  				NodeName: pointer.String("node-1"),
   321  			}, {
   322  				IP:       "10.0.0.2",
   323  				Hostname: "pod-2",
   324  				NodeName: pointer.String("node-2"),
   325  			}},
   326  		}},
   327  		existingEndpointSlices: []*discovery.EndpointSlice{},
   328  		expectedNumSlices:      1,
   329  		expectedClientActions:  1,
   330  		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1},
   331  	}, {
   332  		testName: "Endpoints with 1 subset, 2 ports, and 2 not ready addresses",
   333  		subsets: []corev1.EndpointSubset{{
   334  			Ports: []corev1.EndpointPort{{
   335  				Name:     "http",
   336  				Port:     80,
   337  				Protocol: corev1.ProtocolTCP,
   338  			}, {
   339  				Name:     "https",
   340  				Port:     443,
   341  				Protocol: corev1.ProtocolUDP,
   342  			}},
   343  			NotReadyAddresses: []corev1.EndpointAddress{{
   344  				IP:       "10.0.0.1",
   345  				Hostname: "pod-1",
   346  				NodeName: pointer.String("node-1"),
   347  			}, {
   348  				IP:       "10.0.0.2",
   349  				Hostname: "pod-2",
   350  				NodeName: pointer.String("node-2"),
   351  			}},
   352  		}},
   353  		existingEndpointSlices: []*discovery.EndpointSlice{},
   354  		expectedNumSlices:      1,
   355  		expectedClientActions:  1,
   356  		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1},
   357  	}, {
   358  		testName: "Endpoints with 1 subset, 2 ports, and 2 ready and 2 not ready addresses",
   359  		subsets: []corev1.EndpointSubset{{
   360  			Ports: []corev1.EndpointPort{{
   361  				Name:     "http",
   362  				Port:     80,
   363  				Protocol: corev1.ProtocolTCP,
   364  			}, {
   365  				Name:     "https",
   366  				Port:     443,
   367  				Protocol: corev1.ProtocolUDP,
   368  			}},
   369  			Addresses: []corev1.EndpointAddress{{
   370  				IP:       "10.1.1.1",
   371  				Hostname: "pod-11",
   372  				NodeName: pointer.String("node-1"),
   373  			}, {
   374  				IP:       "10.1.1.2",
   375  				Hostname: "pod-12",
   376  				NodeName: pointer.String("node-2"),
   377  			}},
   378  			NotReadyAddresses: []corev1.EndpointAddress{{
   379  				IP:       "10.0.0.1",
   380  				Hostname: "pod-1",
   381  				NodeName: pointer.String("node-1"),
   382  			}, {
   383  				IP:       "10.0.0.2",
   384  				Hostname: "pod-2",
   385  				NodeName: pointer.String("node-2"),
   386  			}},
   387  		}},
   388  		existingEndpointSlices: []*discovery.EndpointSlice{},
   389  		expectedNumSlices:      1,
   390  		expectedClientActions:  1,
   391  		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 4, addedPerSync: 4, numCreated: 1},
   392  	}, {
   393  		testName: "Endpoints with 2 subsets, multiple ports and addresses",
   394  		subsets: []corev1.EndpointSubset{{
   395  			Ports: []corev1.EndpointPort{{
   396  				Name:     "http",
   397  				Port:     80,
   398  				Protocol: corev1.ProtocolTCP,
   399  			}, {
   400  				Name:     "https",
   401  				Port:     443,
   402  				Protocol: corev1.ProtocolUDP,
   403  			}},
   404  			Addresses: []corev1.EndpointAddress{{
   405  				IP:       "10.0.0.1",
   406  				Hostname: "pod-1",
   407  				NodeName: pointer.String("node-1"),
   408  			}, {
   409  				IP:       "10.0.0.2",
   410  				Hostname: "pod-2",
   411  				NodeName: pointer.String("node-2"),
   412  			}},
   413  		}, {
   414  			Ports: []corev1.EndpointPort{{
   415  				Name:     "http",
   416  				Port:     3000,
   417  				Protocol: corev1.ProtocolTCP,
   418  			}, {
   419  				Name:     "https",
   420  				Port:     3001,
   421  				Protocol: corev1.ProtocolUDP,
   422  			}},
   423  			Addresses: []corev1.EndpointAddress{{
   424  				IP:       "10.0.1.1",
   425  				Hostname: "pod-11",
   426  				NodeName: pointer.String("node-1"),
   427  			}, {
   428  				IP:       "10.0.1.2",
   429  				Hostname: "pod-12",
   430  				NodeName: pointer.String("node-2"),
   431  			}, {
   432  				IP:       "10.0.1.3",
   433  				Hostname: "pod-13",
   434  				NodeName: pointer.String("node-3"),
   435  			}},
   436  		}},
   437  		existingEndpointSlices: []*discovery.EndpointSlice{},
   438  		expectedNumSlices:      2,
   439  		expectedClientActions:  2,
   440  		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 2},
   441  	}, {
   442  		testName: "Endpoints with 2 subsets, multiple ports and addresses, existing empty EndpointSlice",
   443  		subsets: []corev1.EndpointSubset{{
   444  			Ports: []corev1.EndpointPort{{
   445  				Name:     "http",
   446  				Port:     80,
   447  				Protocol: corev1.ProtocolTCP,
   448  			}, {
   449  				Name:     "https",
   450  				Port:     443,
   451  				Protocol: corev1.ProtocolUDP,
   452  			}},
   453  			Addresses: []corev1.EndpointAddress{{
   454  				IP:       "10.0.0.1",
   455  				Hostname: "pod-1",
   456  				NodeName: pointer.String("node-1"),
   457  			}, {
   458  				IP:       "10.0.0.2",
   459  				Hostname: "pod-2",
   460  				NodeName: pointer.String("node-2"),
   461  			}},
   462  		}, {
   463  			Ports: []corev1.EndpointPort{{
   464  				Name:     "http",
   465  				Port:     3000,
   466  				Protocol: corev1.ProtocolTCP,
   467  			}, {
   468  				Name:     "https",
   469  				Port:     3001,
   470  				Protocol: corev1.ProtocolUDP,
   471  			}},
   472  			Addresses: []corev1.EndpointAddress{{
   473  				IP:       "10.0.1.1",
   474  				Hostname: "pod-11",
   475  				NodeName: pointer.String("node-1"),
   476  			}, {
   477  				IP:       "10.0.1.2",
   478  				Hostname: "pod-12",
   479  				NodeName: pointer.String("node-2"),
   480  			}, {
   481  				IP:       "10.0.1.3",
   482  				Hostname: "pod-13",
   483  				NodeName: pointer.String("node-3"),
   484  			}},
   485  		}},
   486  		existingEndpointSlices: []*discovery.EndpointSlice{{
   487  			ObjectMeta: metav1.ObjectMeta{
   488  				Name: "test-ep-1",
   489  			},
   490  			AddressType: discovery.AddressTypeIPv4,
   491  			Ports: []discovery.EndpointPort{{
   492  				Name:     pointer.String("http"),
   493  				Port:     pointer.Int32(80),
   494  				Protocol: &protoTCP,
   495  			}, {
   496  				Name:     pointer.String("https"),
   497  				Port:     pointer.Int32(443),
   498  				Protocol: &protoUDP,
   499  			}},
   500  		}},
   501  		expectedNumSlices:     2,
   502  		expectedClientActions: 2,
   503  		expectedMetrics:       &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 1, numUpdated: 1},
   504  	}, {
   505  		testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice with some addresses",
   506  		subsets: []corev1.EndpointSubset{{
   507  			Ports: []corev1.EndpointPort{{
   508  				Name:     "http",
   509  				Port:     80,
   510  				Protocol: corev1.ProtocolTCP,
   511  			}, {
   512  				Name:     "https",
   513  				Port:     443,
   514  				Protocol: corev1.ProtocolUDP,
   515  			}},
   516  			Addresses: []corev1.EndpointAddress{{
   517  				IP:       "10.0.0.1",
   518  				Hostname: "pod-1",
   519  				NodeName: pointer.String("node-1"),
   520  			}, {
   521  				IP:       "10.0.0.2",
   522  				Hostname: "pod-2",
   523  				NodeName: pointer.String("node-2"),
   524  			}},
   525  		}, {
   526  			Ports: []corev1.EndpointPort{{
   527  				Name:     "http",
   528  				Port:     3000,
   529  				Protocol: corev1.ProtocolTCP,
   530  			}, {
   531  				Name:     "https",
   532  				Port:     3001,
   533  				Protocol: corev1.ProtocolUDP,
   534  			}},
   535  			Addresses: []corev1.EndpointAddress{{
   536  				IP:       "10.0.1.1",
   537  				Hostname: "pod-11",
   538  				NodeName: pointer.String("node-1"),
   539  			}, {
   540  				IP:       "10.0.1.2",
   541  				Hostname: "pod-12",
   542  				NodeName: pointer.String("node-2"),
   543  			}, {
   544  				IP:       "10.0.1.3",
   545  				Hostname: "pod-13",
   546  				NodeName: pointer.String("node-3"),
   547  			}},
   548  		}},
   549  		existingEndpointSlices: []*discovery.EndpointSlice{{
   550  			ObjectMeta: metav1.ObjectMeta{
   551  				Name: "test-ep-1",
   552  			},
   553  			AddressType: discovery.AddressTypeIPv4,
   554  			Ports: []discovery.EndpointPort{{
   555  				Name:     pointer.String("http"),
   556  				Port:     pointer.Int32(80),
   557  				Protocol: &protoTCP,
   558  			}, {
   559  				Name:     pointer.String("https"),
   560  				Port:     pointer.Int32(443),
   561  				Protocol: &protoUDP,
   562  			}},
   563  			Endpoints: []discovery.Endpoint{{
   564  				Addresses: []string{"10.0.0.2"},
   565  				Hostname:  pointer.String("pod-2"),
   566  			}, {
   567  				Addresses: []string{"10.0.0.1", "10.0.0.3"},
   568  				Hostname:  pointer.String("pod-1"),
   569  			}},
   570  		}},
   571  		expectedNumSlices:     2,
   572  		expectedClientActions: 2,
   573  		expectedMetrics:       &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 4, updatedPerSync: 1, removedPerSync: 1, numCreated: 1, numUpdated: 1},
   574  	}, {
   575  		testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice identical to subset",
   576  		subsets: []corev1.EndpointSubset{{
   577  			Ports: []corev1.EndpointPort{{
   578  				Name:     "http",
   579  				Port:     80,
   580  				Protocol: corev1.ProtocolTCP,
   581  			}, {
   582  				Name:     "https",
   583  				Port:     443,
   584  				Protocol: corev1.ProtocolUDP,
   585  			}},
   586  			Addresses: []corev1.EndpointAddress{{
   587  				IP:       "10.0.0.1",
   588  				Hostname: "pod-1",
   589  				NodeName: pointer.String("node-1"),
   590  			}, {
   591  				IP:       "10.0.0.2",
   592  				Hostname: "pod-2",
   593  				NodeName: pointer.String("node-2"),
   594  			}},
   595  		}, {
   596  			Ports: []corev1.EndpointPort{{
   597  				Name:     "http",
   598  				Port:     3000,
   599  				Protocol: corev1.ProtocolTCP,
   600  			}, {
   601  				Name:     "https",
   602  				Port:     3001,
   603  				Protocol: corev1.ProtocolUDP,
   604  			}},
   605  			Addresses: []corev1.EndpointAddress{{
   606  				IP:       "10.0.1.1",
   607  				Hostname: "pod-11",
   608  				NodeName: pointer.String("node-1"),
   609  			}, {
   610  				IP:       "10.0.1.2",
   611  				Hostname: "pod-12",
   612  				NodeName: pointer.String("node-2"),
   613  			}, {
   614  				IP:       "10.0.1.3",
   615  				Hostname: "pod-13",
   616  				NodeName: pointer.String("node-3"),
   617  			}},
   618  		}},
   619  		existingEndpointSlices: []*discovery.EndpointSlice{{
   620  			ObjectMeta: metav1.ObjectMeta{
   621  				Name: "test-ep-1",
   622  			},
   623  			AddressType: discovery.AddressTypeIPv4,
   624  			Ports: []discovery.EndpointPort{{
   625  				Name:     pointer.String("http"),
   626  				Port:     pointer.Int32(80),
   627  				Protocol: &protoTCP,
   628  			}, {
   629  				Name:     pointer.String("https"),
   630  				Port:     pointer.Int32(443),
   631  				Protocol: &protoUDP,
   632  			}},
   633  			Endpoints: []discovery.Endpoint{{
   634  				Addresses:  []string{"10.0.0.1"},
   635  				Hostname:   pointer.String("pod-1"),
   636  				NodeName:   pointer.String("node-1"),
   637  				Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)},
   638  			}, {
   639  				Addresses:  []string{"10.0.0.2"},
   640  				Hostname:   pointer.String("pod-2"),
   641  				NodeName:   pointer.String("node-2"),
   642  				Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)},
   643  			}},
   644  		}},
   645  		expectedNumSlices:     2,
   646  		expectedClientActions: 1,
   647  		expectedMetrics:       &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 3, numCreated: 1},
   648  	}, {
   649  		testName: "Endpoints with 2 subsets, multiple ports, and dual stack addresses",
   650  		subsets: []corev1.EndpointSubset{{
   651  			Ports: []corev1.EndpointPort{{
   652  				Name:     "http",
   653  				Port:     80,
   654  				Protocol: corev1.ProtocolTCP,
   655  			}, {
   656  				Name:     "https",
   657  				Port:     443,
   658  				Protocol: corev1.ProtocolUDP,
   659  			}},
   660  			Addresses: []corev1.EndpointAddress{{
   661  				IP:       "2001:db8:2222:3333:4444:5555:6666:7777",
   662  				Hostname: "pod-1",
   663  				NodeName: pointer.String("node-1"),
   664  			}, {
   665  				IP:       "10.0.0.2",
   666  				Hostname: "pod-2",
   667  				NodeName: pointer.String("node-2"),
   668  			}},
   669  		}, {
   670  			Ports: []corev1.EndpointPort{{
   671  				Name:     "http",
   672  				Port:     3000,
   673  				Protocol: corev1.ProtocolTCP,
   674  			}, {
   675  				Name:     "https",
   676  				Port:     3001,
   677  				Protocol: corev1.ProtocolUDP,
   678  			}},
   679  			Addresses: []corev1.EndpointAddress{{
   680  				IP:       "10.0.1.1",
   681  				Hostname: "pod-11",
   682  				NodeName: pointer.String("node-1"),
   683  			}, {
   684  				IP:       "10.0.1.2",
   685  				Hostname: "pod-12",
   686  				NodeName: pointer.String("node-2"),
   687  			}, {
   688  				IP:       "2001:db8:3333:4444:5555:6666:7777:8888",
   689  				Hostname: "pod-13",
   690  				NodeName: pointer.String("node-3"),
   691  			}},
   692  		}},
   693  		existingEndpointSlices: []*discovery.EndpointSlice{},
   694  		expectedNumSlices:      4,
   695  		expectedClientActions:  4,
   696  		expectedMetrics:        &expectedMetrics{desiredSlices: 4, actualSlices: 4, desiredEndpoints: 5, addedPerSync: 5, numCreated: 4},
   697  	}, {
   698  		testName: "Endpoints with 2 subsets, multiple ports, ipv6 only addresses",
   699  		subsets: []corev1.EndpointSubset{{
   700  			Ports: []corev1.EndpointPort{{
   701  				Name:     "http",
   702  				Port:     80,
   703  				Protocol: corev1.ProtocolTCP,
   704  			}, {
   705  				Name:     "https",
   706  				Port:     443,
   707  				Protocol: corev1.ProtocolUDP,
   708  			}},
   709  			Addresses: []corev1.EndpointAddress{{
   710  				IP:       "2001:db8:1111:3333:4444:5555:6666:7777",
   711  				Hostname: "pod-1",
   712  				NodeName: pointer.String("node-1"),
   713  			}, {
   714  				IP:       "2001:db8:2222:3333:4444:5555:6666:7777",
   715  				Hostname: "pod-2",
   716  				NodeName: pointer.String("node-2"),
   717  			}},
   718  		}, {
   719  			Ports: []corev1.EndpointPort{{
   720  				Name:     "http",
   721  				Port:     3000,
   722  				Protocol: corev1.ProtocolTCP,
   723  			}, {
   724  				Name:     "https",
   725  				Port:     3001,
   726  				Protocol: corev1.ProtocolUDP,
   727  			}},
   728  			Addresses: []corev1.EndpointAddress{{
   729  				IP:       "2001:db8:3333:3333:4444:5555:6666:7777",
   730  				Hostname: "pod-11",
   731  				NodeName: pointer.String("node-1"),
   732  			}, {
   733  				IP:       "2001:db8:4444:3333:4444:5555:6666:7777",
   734  				Hostname: "pod-12",
   735  				NodeName: pointer.String("node-2"),
   736  			}, {
   737  				IP:       "2001:db8:5555:3333:4444:5555:6666:7777",
   738  				Hostname: "pod-13",
   739  				NodeName: pointer.String("node-3"),
   740  			}},
   741  		}},
   742  		existingEndpointSlices: []*discovery.EndpointSlice{},
   743  		expectedNumSlices:      2,
   744  		expectedClientActions:  2,
   745  		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 2},
   746  	}, {
   747  		testName: "Endpoints with 2 subsets, multiple ports, some invalid addresses",
   748  		subsets: []corev1.EndpointSubset{{
   749  			Ports: []corev1.EndpointPort{{
   750  				Name:     "http",
   751  				Port:     80,
   752  				Protocol: corev1.ProtocolTCP,
   753  			}, {
   754  				Name:     "https",
   755  				Port:     443,
   756  				Protocol: corev1.ProtocolUDP,
   757  			}},
   758  			Addresses: []corev1.EndpointAddress{{
   759  				IP:       "2001:db8:1111:3333:4444:5555:6666:7777",
   760  				Hostname: "pod-1",
   761  				NodeName: pointer.String("node-1"),
   762  			}, {
   763  				IP:       "this-is-not-an-ip",
   764  				Hostname: "pod-2",
   765  				NodeName: pointer.String("node-2"),
   766  			}},
   767  		}, {
   768  			Ports: []corev1.EndpointPort{{
   769  				Name:     "http",
   770  				Port:     3000,
   771  				Protocol: corev1.ProtocolTCP,
   772  			}, {
   773  				Name:     "https",
   774  				Port:     3001,
   775  				Protocol: corev1.ProtocolUDP,
   776  			}},
   777  			Addresses: []corev1.EndpointAddress{{
   778  				IP:       "this-is-also-not-an-ip",
   779  				Hostname: "pod-11",
   780  				NodeName: pointer.String("node-1"),
   781  			}, {
   782  				IP:       "2001:db8:4444:3333:4444:5555:6666:7777",
   783  				Hostname: "pod-12",
   784  				NodeName: pointer.String("node-2"),
   785  			}, {
   786  				IP:       "2001:db8:5555:3333:4444:5555:6666:7777",
   787  				Hostname: "pod-13",
   788  				NodeName: pointer.String("node-3"),
   789  			}},
   790  		}},
   791  		existingEndpointSlices: []*discovery.EndpointSlice{},
   792  		expectedNumSlices:      2,
   793  		expectedClientActions:  2,
   794  		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 3, addedPerSync: 3, skippedPerSync: 2, numCreated: 2},
   795  	}, {
   796  		testName: "Endpoints with 2 subsets, multiple ports, all invalid addresses",
   797  		subsets: []corev1.EndpointSubset{{
   798  			Ports: []corev1.EndpointPort{{
   799  				Name:     "http",
   800  				Port:     80,
   801  				Protocol: corev1.ProtocolTCP,
   802  			}, {
   803  				Name:     "https",
   804  				Port:     443,
   805  				Protocol: corev1.ProtocolUDP,
   806  			}},
   807  			Addresses: []corev1.EndpointAddress{{
   808  				IP:       "this-is-not-an-ip1",
   809  				Hostname: "pod-1",
   810  				NodeName: pointer.String("node-1"),
   811  			}, {
   812  				IP:       "this-is-not-an-ip12",
   813  				Hostname: "pod-2",
   814  				NodeName: pointer.String("node-2"),
   815  			}},
   816  		}, {
   817  			Ports: []corev1.EndpointPort{{
   818  				Name:     "http",
   819  				Port:     3000,
   820  				Protocol: corev1.ProtocolTCP,
   821  			}, {
   822  				Name:     "https",
   823  				Port:     3001,
   824  				Protocol: corev1.ProtocolUDP,
   825  			}},
   826  			Addresses: []corev1.EndpointAddress{{
   827  				IP:       "this-is-not-an-ip11",
   828  				Hostname: "pod-11",
   829  				NodeName: pointer.String("node-1"),
   830  			}, {
   831  				IP:       "this-is-not-an-ip12",
   832  				Hostname: "pod-12",
   833  				NodeName: pointer.String("node-2"),
   834  			}, {
   835  				IP:       "this-is-not-an-ip3",
   836  				Hostname: "pod-13",
   837  				NodeName: pointer.String("node-3"),
   838  			}},
   839  		}},
   840  		existingEndpointSlices: []*discovery.EndpointSlice{},
   841  		expectedNumSlices:      0,
   842  		expectedClientActions:  0,
   843  		expectedMetrics:        &expectedMetrics{desiredSlices: 0, actualSlices: 0, desiredEndpoints: 0, addedPerSync: 0, skippedPerSync: 5, numCreated: 0},
   844  	}, {
   845  		testName: "Endpoints with 2 subsets, 1 exceeding maxEndpointsPerSubset",
   846  		subsets: []corev1.EndpointSubset{{
   847  			Ports: []corev1.EndpointPort{{
   848  				Name:     "http",
   849  				Port:     80,
   850  				Protocol: corev1.ProtocolTCP,
   851  			}, {
   852  				Name:     "https",
   853  				Port:     443,
   854  				Protocol: corev1.ProtocolUDP,
   855  			}},
   856  			Addresses: []corev1.EndpointAddress{{
   857  				IP:       "10.0.0.1",
   858  				Hostname: "pod-1",
   859  				NodeName: pointer.String("node-1"),
   860  			}, {
   861  				IP:       "10.0.0.2",
   862  				Hostname: "pod-2",
   863  				NodeName: pointer.String("node-2"),
   864  			}},
   865  		}, {
   866  			Ports: []corev1.EndpointPort{{
   867  				Name:     "http",
   868  				Port:     3000,
   869  				Protocol: corev1.ProtocolTCP,
   870  			}, {
   871  				Name:     "https",
   872  				Port:     3001,
   873  				Protocol: corev1.ProtocolUDP,
   874  			}},
   875  			Addresses: []corev1.EndpointAddress{{
   876  				IP:       "10.0.1.1",
   877  				Hostname: "pod-11",
   878  				NodeName: pointer.String("node-1"),
   879  			}, {
   880  				IP:       "10.0.1.2",
   881  				Hostname: "pod-12",
   882  				NodeName: pointer.String("node-2"),
   883  			}, {
   884  				IP:       "10.0.1.3",
   885  				Hostname: "pod-13",
   886  				NodeName: pointer.String("node-3"),
   887  			}},
   888  		}},
   889  		existingEndpointSlices: []*discovery.EndpointSlice{},
   890  		expectedNumSlices:      2,
   891  		expectedClientActions:  2,
   892  		maxEndpointsPerSubset:  2,
   893  		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 4, addedPerSync: 4, updatedPerSync: 0, removedPerSync: 0, skippedPerSync: 1, numCreated: 2, numUpdated: 0},
   894  	}, {
   895  		testName: "The last-applied-configuration annotation should not get mirrored to created or updated endpoint slices",
   896  		epAnnotations: map[string]string{
   897  			corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}",
   898  		},
   899  		subsets: []corev1.EndpointSubset{{
   900  			Ports: []corev1.EndpointPort{{
   901  				Name:     "http",
   902  				Port:     80,
   903  				Protocol: corev1.ProtocolTCP,
   904  			}},
   905  			Addresses: []corev1.EndpointAddress{{
   906  				IP:       "10.0.0.1",
   907  				Hostname: "pod-1",
   908  			}},
   909  		}},
   910  		existingEndpointSlices: []*discovery.EndpointSlice{},
   911  		expectedNumSlices:      1,
   912  		expectedClientActions:  1,
   913  		expectedMetrics:        &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1},
   914  	}, {
   915  		testName: "The last-applied-configuration annotation shouldn't get added to created endpoint slices",
   916  		subsets: []corev1.EndpointSubset{{
   917  			Ports: []corev1.EndpointPort{{
   918  				Name:     "http",
   919  				Port:     80,
   920  				Protocol: corev1.ProtocolTCP,
   921  			}},
   922  			Addresses: []corev1.EndpointAddress{{
   923  				IP:       "10.0.0.1",
   924  				Hostname: "pod-1",
   925  			}},
   926  		}},
   927  		existingEndpointSlices: []*discovery.EndpointSlice{},
   928  		expectedNumSlices:      1,
   929  		expectedClientActions:  1,
   930  		expectedMetrics:        &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1},
   931  	}, {
   932  		testName: "The last-applied-configuration shouldn't get mirrored to endpoint slices when it's value is empty",
   933  		epAnnotations: map[string]string{
   934  			corev1.LastAppliedConfigAnnotation: "",
   935  		},
   936  		subsets: []corev1.EndpointSubset{{
   937  			Ports: []corev1.EndpointPort{{
   938  				Name:     "http",
   939  				Port:     80,
   940  				Protocol: corev1.ProtocolTCP,
   941  			}},
   942  			Addresses: []corev1.EndpointAddress{{
   943  				IP:       "10.0.0.1",
   944  				Hostname: "pod-1",
   945  			}},
   946  		}},
   947  		existingEndpointSlices: []*discovery.EndpointSlice{},
   948  		expectedNumSlices:      1,
   949  		expectedClientActions:  1,
   950  		expectedMetrics:        &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1},
   951  	}, {
   952  		testName: "Annotations other than last-applied-configuration should get correctly mirrored",
   953  		epAnnotations: map[string]string{
   954  			corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}",
   955  			"foo":                              "bar",
   956  		},
   957  		subsets: []corev1.EndpointSubset{{
   958  			Ports: []corev1.EndpointPort{{
   959  				Name:     "http",
   960  				Port:     80,
   961  				Protocol: corev1.ProtocolTCP,
   962  			}},
   963  			Addresses: []corev1.EndpointAddress{{
   964  				IP:       "10.0.0.1",
   965  				Hostname: "pod-1",
   966  			}},
   967  		}},
   968  		existingEndpointSlices: []*discovery.EndpointSlice{},
   969  		expectedNumSlices:      1,
   970  		expectedClientActions:  1,
   971  		expectedMetrics:        &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1},
   972  	}, {
   973  		testName: "Annotation mirroring should remove the last-applied-configuration annotation from existing endpoint slices",
   974  		subsets: []corev1.EndpointSubset{{
   975  			Ports: []corev1.EndpointPort{{
   976  				Name:     "http",
   977  				Port:     80,
   978  				Protocol: corev1.ProtocolTCP,
   979  			}},
   980  			Addresses: []corev1.EndpointAddress{{
   981  				IP:       "10.0.0.1",
   982  				Hostname: "pod-1",
   983  			}},
   984  		}},
   985  		existingEndpointSlices: []*discovery.EndpointSlice{{
   986  			ObjectMeta: metav1.ObjectMeta{
   987  				Name: "test-ep-1",
   988  				Annotations: map[string]string{
   989  					corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}",
   990  				},
   991  			},
   992  			AddressType: discovery.AddressTypeIPv4,
   993  			Ports: []discovery.EndpointPort{{
   994  				Name:     pointer.String("http"),
   995  				Port:     pointer.Int32(80),
   996  				Protocol: &protoTCP,
   997  			}},
   998  			Endpoints: []discovery.Endpoint{{
   999  				Addresses:  []string{"10.0.0.1"},
  1000  				Hostname:   pointer.String("pod-1"),
  1001  				Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)},
  1002  			}},
  1003  		}},
  1004  		expectedNumSlices:     1,
  1005  		expectedClientActions: 1,
  1006  	}}
  1007  
  1008  	for _, tc := range testCases {
  1009  		t.Run(tc.testName, func(t *testing.T) {
  1010  			client := newClientset()
  1011  			setupMetrics()
  1012  			namespace := "test"
  1013  			endpoints := corev1.Endpoints{
  1014  				ObjectMeta: metav1.ObjectMeta{Name: "test-ep", Namespace: namespace, Labels: tc.epLabels, Annotations: tc.epAnnotations},
  1015  				Subsets:    tc.subsets,
  1016  			}
  1017  
  1018  			if tc.endpointsDeletionPending {
  1019  				now := metav1.Now()
  1020  				endpoints.DeletionTimestamp = &now
  1021  			}
  1022  
  1023  			numInitialActions := 0
  1024  			for _, epSlice := range tc.existingEndpointSlices {
  1025  				epSlice.Labels = map[string]string{
  1026  					discovery.LabelServiceName: endpoints.Name,
  1027  					discovery.LabelManagedBy:   controllerName,
  1028  				}
  1029  				_, err := client.DiscoveryV1().EndpointSlices(namespace).Create(context.TODO(), epSlice, metav1.CreateOptions{})
  1030  				if err != nil {
  1031  					t.Fatalf("Expected no error creating EndpointSlice, got %v", err)
  1032  				}
  1033  				numInitialActions++
  1034  			}
  1035  
  1036  			maxEndpointsPerSubset := tc.maxEndpointsPerSubset
  1037  			if maxEndpointsPerSubset == 0 {
  1038  				maxEndpointsPerSubset = defaultMaxEndpointsPerSubset
  1039  			}
  1040  			r := newReconciler(client, maxEndpointsPerSubset)
  1041  			reconcileHelper(t, r, &endpoints, tc.existingEndpointSlices)
  1042  
  1043  			numExtraActions := len(client.Actions()) - numInitialActions
  1044  			if numExtraActions != tc.expectedClientActions {
  1045  				t.Fatalf("Expected %d additional client actions, got %d: %#v", tc.expectedClientActions, numExtraActions, client.Actions()[numInitialActions:])
  1046  			}
  1047  
  1048  			if tc.expectedMetrics != nil {
  1049  				expectMetrics(t, *tc.expectedMetrics)
  1050  			}
  1051  
  1052  			endpointSlices := fetchEndpointSlices(t, client, namespace)
  1053  			expectEndpointSlices(t, tc.expectedNumSlices, int(maxEndpointsPerSubset), endpoints, endpointSlices)
  1054  		})
  1055  	}
  1056  }
  1057  
  1058  // Test Helpers
  1059  
  1060  func newReconciler(client *fake.Clientset, maxEndpointsPerSubset int32) *reconciler {
  1061  	broadcaster := record.NewBroadcaster()
  1062  	recorder := broadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "endpoint-slice-mirroring-controller"})
  1063  
  1064  	return &reconciler{
  1065  		client:                client,
  1066  		maxEndpointsPerSubset: maxEndpointsPerSubset,
  1067  		endpointSliceTracker:  endpointsliceutil.NewEndpointSliceTracker(),
  1068  		metricsCache:          metrics.NewCache(maxEndpointsPerSubset),
  1069  		eventRecorder:         recorder,
  1070  	}
  1071  }
  1072  
  1073  func expectEndpointSlices(t *testing.T, num, maxEndpointsPerSubset int, endpoints corev1.Endpoints, endpointSlices []discovery.EndpointSlice) {
  1074  	t.Helper()
  1075  	if len(endpointSlices) != num {
  1076  		t.Fatalf("Expected %d EndpointSlices, got %d", num, len(endpointSlices))
  1077  	}
  1078  
  1079  	if num == 0 {
  1080  		return
  1081  	}
  1082  
  1083  	for _, epSlice := range endpointSlices {
  1084  		if !strings.HasPrefix(epSlice.Name, endpoints.Name) {
  1085  			t.Errorf("Expected EndpointSlice name to start with %s, got %s", endpoints.Name, epSlice.Name)
  1086  		}
  1087  
  1088  		serviceNameVal, ok := epSlice.Labels[discovery.LabelServiceName]
  1089  		if !ok {
  1090  			t.Errorf("Expected EndpointSlice to have %s label set", discovery.LabelServiceName)
  1091  		}
  1092  		if serviceNameVal != endpoints.Name {
  1093  			t.Errorf("Expected EndpointSlice to have %s label set to %s, got %s", discovery.LabelServiceName, endpoints.Name, serviceNameVal)
  1094  		}
  1095  
  1096  		_, ok = epSlice.Annotations[corev1.LastAppliedConfigAnnotation]
  1097  		if ok {
  1098  			t.Errorf("Expected LastAppliedConfigAnnotation to be unset, got %s", epSlice.Annotations[corev1.LastAppliedConfigAnnotation])
  1099  		}
  1100  
  1101  		_, ok = epSlice.Annotations[corev1.EndpointsLastChangeTriggerTime]
  1102  		if ok {
  1103  			t.Errorf("Expected EndpointsLastChangeTriggerTime to be unset, got %s", epSlice.Annotations[corev1.EndpointsLastChangeTriggerTime])
  1104  		}
  1105  
  1106  		for annotation, val := range endpoints.Annotations {
  1107  			if annotation == corev1.EndpointsLastChangeTriggerTime || annotation == corev1.LastAppliedConfigAnnotation {
  1108  				continue
  1109  			}
  1110  			if epSlice.Annotations[annotation] != val {
  1111  				t.Errorf("Expected endpoint annotation %s to be mirrored correctly, got %s", annotation, epSlice.Annotations[annotation])
  1112  			}
  1113  		}
  1114  	}
  1115  
  1116  	// canonicalize endpoints to match the expected endpoints, otherwise the test
  1117  	// that creates more endpoints than allowed fail becaused the list of final
  1118  	// endpoints doesn't match.
  1119  	for _, epSubset := range endpointsv1.RepackSubsets(endpoints.Subsets) {
  1120  		if len(epSubset.Addresses) == 0 && len(epSubset.NotReadyAddresses) == 0 {
  1121  			continue
  1122  		}
  1123  
  1124  		var matchingEndpointsV4, matchingEndpointsV6 []discovery.Endpoint
  1125  
  1126  		for _, epSlice := range endpointSlices {
  1127  			if portsMatch(epSubset.Ports, epSlice.Ports) {
  1128  				switch epSlice.AddressType {
  1129  				case discovery.AddressTypeIPv4:
  1130  					matchingEndpointsV4 = append(matchingEndpointsV4, epSlice.Endpoints...)
  1131  				case discovery.AddressTypeIPv6:
  1132  					matchingEndpointsV6 = append(matchingEndpointsV6, epSlice.Endpoints...)
  1133  				default:
  1134  					t.Fatalf("Unexpected EndpointSlice address type found: %v", epSlice.AddressType)
  1135  				}
  1136  			}
  1137  		}
  1138  
  1139  		if len(matchingEndpointsV4) == 0 && len(matchingEndpointsV6) == 0 {
  1140  			t.Fatalf("No EndpointSlices match Endpoints subset: %#v", epSubset.Ports)
  1141  		}
  1142  
  1143  		expectMatchingAddresses(t, epSubset, matchingEndpointsV4, discovery.AddressTypeIPv4, maxEndpointsPerSubset)
  1144  		expectMatchingAddresses(t, epSubset, matchingEndpointsV6, discovery.AddressTypeIPv6, maxEndpointsPerSubset)
  1145  	}
  1146  }
  1147  
  1148  func portsMatch(epPorts []corev1.EndpointPort, epsPorts []discovery.EndpointPort) bool {
  1149  	if len(epPorts) != len(epsPorts) {
  1150  		return false
  1151  	}
  1152  
  1153  	portsToBeMatched := map[int32]corev1.EndpointPort{}
  1154  
  1155  	for _, epPort := range epPorts {
  1156  		portsToBeMatched[epPort.Port] = epPort
  1157  	}
  1158  
  1159  	for _, epsPort := range epsPorts {
  1160  		epPort, ok := portsToBeMatched[*epsPort.Port]
  1161  		if !ok {
  1162  			return false
  1163  		}
  1164  		delete(portsToBeMatched, *epsPort.Port)
  1165  
  1166  		if epPort.Name != *epsPort.Name {
  1167  			return false
  1168  		}
  1169  		if epPort.Port != *epsPort.Port {
  1170  			return false
  1171  		}
  1172  		if epPort.Protocol != *epsPort.Protocol {
  1173  			return false
  1174  		}
  1175  		if epPort.AppProtocol != epsPort.AppProtocol {
  1176  			return false
  1177  		}
  1178  	}
  1179  
  1180  	return true
  1181  }
  1182  
  1183  func expectMatchingAddresses(t *testing.T, epSubset corev1.EndpointSubset, esEndpoints []discovery.Endpoint, addrType discovery.AddressType, maxEndpointsPerSubset int) {
  1184  	t.Helper()
  1185  	type addressInfo struct {
  1186  		ready     bool
  1187  		epAddress corev1.EndpointAddress
  1188  	}
  1189  
  1190  	// This approach assumes that each IP is unique within an EndpointSubset.
  1191  	expectedEndpoints := map[string]addressInfo{}
  1192  
  1193  	for _, address := range epSubset.Addresses {
  1194  		at := getAddressType(address.IP)
  1195  		if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset {
  1196  			expectedEndpoints[address.IP] = addressInfo{
  1197  				ready:     true,
  1198  				epAddress: address,
  1199  			}
  1200  		}
  1201  	}
  1202  
  1203  	for _, address := range epSubset.NotReadyAddresses {
  1204  		at := getAddressType(address.IP)
  1205  		if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset {
  1206  			expectedEndpoints[address.IP] = addressInfo{
  1207  				ready:     false,
  1208  				epAddress: address,
  1209  			}
  1210  		}
  1211  	}
  1212  
  1213  	if len(expectedEndpoints) != len(esEndpoints) {
  1214  		t.Errorf("Expected %d endpoints, got %d", len(expectedEndpoints), len(esEndpoints))
  1215  	}
  1216  
  1217  	for _, endpoint := range esEndpoints {
  1218  		if len(endpoint.Addresses) != 1 {
  1219  			t.Fatalf("Expected endpoint to have 1 address, got %d", len(endpoint.Addresses))
  1220  		}
  1221  		address := endpoint.Addresses[0]
  1222  		expectedEndpoint, ok := expectedEndpoints[address]
  1223  
  1224  		if !ok {
  1225  			t.Fatalf("EndpointSlice has endpoint with unexpected address: %s", address)
  1226  		}
  1227  
  1228  		if expectedEndpoint.ready != *endpoint.Conditions.Ready {
  1229  			t.Errorf("Expected ready to be %t, got %t", expectedEndpoint.ready, *endpoint.Conditions.Ready)
  1230  		}
  1231  
  1232  		if endpoint.Hostname == nil {
  1233  			if expectedEndpoint.epAddress.Hostname != "" {
  1234  				t.Errorf("Expected hostname to be %s, got nil", expectedEndpoint.epAddress.Hostname)
  1235  			}
  1236  		} else if expectedEndpoint.epAddress.Hostname != *endpoint.Hostname {
  1237  			t.Errorf("Expected hostname to be %s, got %s", expectedEndpoint.epAddress.Hostname, *endpoint.Hostname)
  1238  		}
  1239  
  1240  		if expectedEndpoint.epAddress.NodeName != nil {
  1241  			if endpoint.NodeName == nil {
  1242  				t.Errorf("Expected nodeName to be set")
  1243  			}
  1244  			if *expectedEndpoint.epAddress.NodeName != *endpoint.NodeName {
  1245  				t.Errorf("Expected nodeName to be %s, got %s", *expectedEndpoint.epAddress.NodeName, *endpoint.NodeName)
  1246  			}
  1247  		}
  1248  	}
  1249  }
  1250  
  1251  func fetchEndpointSlices(t *testing.T, client *fake.Clientset, namespace string) []discovery.EndpointSlice {
  1252  	t.Helper()
  1253  	fetchedSlices, err := client.DiscoveryV1().EndpointSlices(namespace).List(context.TODO(), metav1.ListOptions{
  1254  		LabelSelector: discovery.LabelManagedBy + "=" + controllerName,
  1255  	})
  1256  	if err != nil {
  1257  		t.Fatalf("Expected no error fetching Endpoint Slices, got: %v", err)
  1258  		return []discovery.EndpointSlice{}
  1259  	}
  1260  	return fetchedSlices.Items
  1261  }
  1262  
  1263  func reconcileHelper(t *testing.T, r *reconciler, endpoints *corev1.Endpoints, existingSlices []*discovery.EndpointSlice) {
  1264  	t.Helper()
  1265  	logger, _ := ktesting.NewTestContext(t)
  1266  	err := r.reconcile(logger, endpoints, existingSlices)
  1267  	if err != nil {
  1268  		t.Fatalf("Expected no error reconciling Endpoint Slices, got: %v", err)
  1269  	}
  1270  }
  1271  
  1272  // Metrics helpers
  1273  
  1274  type expectedMetrics struct {
  1275  	desiredSlices    int
  1276  	actualSlices     int
  1277  	desiredEndpoints int
  1278  	addedPerSync     int
  1279  	updatedPerSync   int
  1280  	removedPerSync   int
  1281  	skippedPerSync   int
  1282  	numCreated       int
  1283  	numUpdated       int
  1284  	numDeleted       int
  1285  }
  1286  
  1287  func expectMetrics(t *testing.T, em expectedMetrics) {
  1288  	t.Helper()
  1289  
  1290  	actualDesiredSlices, err := testutil.GetGaugeMetricValue(metrics.DesiredEndpointSlices.WithLabelValues())
  1291  	handleErr(t, err, "desiredEndpointSlices")
  1292  	if actualDesiredSlices != float64(em.desiredSlices) {
  1293  		t.Errorf("Expected desiredEndpointSlices to be %d, got %v", em.desiredSlices, actualDesiredSlices)
  1294  	}
  1295  
  1296  	actualNumSlices, err := testutil.GetGaugeMetricValue(metrics.NumEndpointSlices.WithLabelValues())
  1297  	handleErr(t, err, "numEndpointSlices")
  1298  	if actualNumSlices != float64(em.actualSlices) {
  1299  		t.Errorf("Expected numEndpointSlices to be %d, got %v", em.actualSlices, actualNumSlices)
  1300  	}
  1301  
  1302  	actualEndpointsDesired, err := testutil.GetGaugeMetricValue(metrics.EndpointsDesired.WithLabelValues())
  1303  	handleErr(t, err, "desiredEndpoints")
  1304  	if actualEndpointsDesired != float64(em.desiredEndpoints) {
  1305  		t.Errorf("Expected desiredEndpoints to be %d, got %v", em.desiredEndpoints, actualEndpointsDesired)
  1306  	}
  1307  
  1308  	actualAddedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsAddedPerSync.WithLabelValues())
  1309  	handleErr(t, err, "endpointsAddedPerSync")
  1310  	if actualAddedPerSync != float64(em.addedPerSync) {
  1311  		t.Errorf("Expected endpointsAddedPerSync to be %d, got %v", em.addedPerSync, actualAddedPerSync)
  1312  	}
  1313  
  1314  	actualUpdatedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsUpdatedPerSync.WithLabelValues())
  1315  	handleErr(t, err, "endpointsUpdatedPerSync")
  1316  	if actualUpdatedPerSync != float64(em.updatedPerSync) {
  1317  		t.Errorf("Expected endpointsUpdatedPerSync to be %d, got %v", em.updatedPerSync, actualUpdatedPerSync)
  1318  	}
  1319  
  1320  	actualRemovedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsRemovedPerSync.WithLabelValues())
  1321  	handleErr(t, err, "endpointsRemovedPerSync")
  1322  	if actualRemovedPerSync != float64(em.removedPerSync) {
  1323  		t.Errorf("Expected endpointsRemovedPerSync to be %d, got %v", em.removedPerSync, actualRemovedPerSync)
  1324  	}
  1325  
  1326  	actualSkippedPerSync, err := testutil.GetHistogramMetricValue(metrics.AddressesSkippedPerSync.WithLabelValues())
  1327  	handleErr(t, err, "addressesSkippedPerSync")
  1328  	if actualSkippedPerSync != float64(em.skippedPerSync) {
  1329  		t.Errorf("Expected addressesSkippedPerSync to be %d, got %v", em.skippedPerSync, actualSkippedPerSync)
  1330  	}
  1331  
  1332  	actualCreated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("create"))
  1333  	handleErr(t, err, "endpointSliceChangesCreated")
  1334  	if actualCreated != float64(em.numCreated) {
  1335  		t.Errorf("Expected endpointSliceChangesCreated to be %d, got %v", em.numCreated, actualCreated)
  1336  	}
  1337  
  1338  	actualUpdated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("update"))
  1339  	handleErr(t, err, "endpointSliceChangesUpdated")
  1340  	if actualUpdated != float64(em.numUpdated) {
  1341  		t.Errorf("Expected endpointSliceChangesUpdated to be %d, got %v", em.numUpdated, actualUpdated)
  1342  	}
  1343  
  1344  	actualDeleted, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("delete"))
  1345  	handleErr(t, err, "desiredEndpointSlices")
  1346  	if actualDeleted != float64(em.numDeleted) {
  1347  		t.Errorf("Expected endpointSliceChangesDeleted to be %d, got %v", em.numDeleted, actualDeleted)
  1348  	}
  1349  }
  1350  
  1351  func handleErr(t *testing.T, err error, metricName string) {
  1352  	if err != nil {
  1353  		t.Errorf("Failed to get %s value, err: %v", metricName, err)
  1354  	}
  1355  }
  1356  
  1357  func setupMetrics() {
  1358  	metrics.RegisterMetrics()
  1359  	metrics.NumEndpointSlices.Delete(map[string]string{})
  1360  	metrics.DesiredEndpointSlices.Delete(map[string]string{})
  1361  	metrics.EndpointsDesired.Delete(map[string]string{})
  1362  	metrics.EndpointsAddedPerSync.Delete(map[string]string{})
  1363  	metrics.EndpointsUpdatedPerSync.Delete(map[string]string{})
  1364  	metrics.EndpointsRemovedPerSync.Delete(map[string]string{})
  1365  	metrics.AddressesSkippedPerSync.Delete(map[string]string{})
  1366  	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "create"})
  1367  	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "update"})
  1368  	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "delete"})
  1369  }