...

Source file src/k8s.io/kubernetes/pkg/controller/endpointslicemirroring/reconciler_test.go

Documentation: k8s.io/kubernetes/pkg/controller/endpointslicemirroring

     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  	endpointsv1 "k8s.io/kubernetes/pkg/api/v1/endpoints"
    33  	"k8s.io/kubernetes/pkg/controller/endpointslicemirroring/metrics"
    34  	"k8s.io/kubernetes/test/utils/ktesting"
    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  			tCtx := ktesting.Init(t)
  1011  			client := newClientset()
  1012  			setupMetrics()
  1013  			namespace := "test"
  1014  			endpoints := corev1.Endpoints{
  1015  				ObjectMeta: metav1.ObjectMeta{Name: "test-ep", Namespace: namespace, Labels: tc.epLabels, Annotations: tc.epAnnotations},
  1016  				Subsets:    tc.subsets,
  1017  			}
  1018  
  1019  			if tc.endpointsDeletionPending {
  1020  				now := metav1.Now()
  1021  				endpoints.DeletionTimestamp = &now
  1022  			}
  1023  
  1024  			numInitialActions := 0
  1025  			for _, epSlice := range tc.existingEndpointSlices {
  1026  				epSlice.Labels = map[string]string{
  1027  					discovery.LabelServiceName: endpoints.Name,
  1028  					discovery.LabelManagedBy:   controllerName,
  1029  				}
  1030  				_, err := client.DiscoveryV1().EndpointSlices(namespace).Create(context.TODO(), epSlice, metav1.CreateOptions{})
  1031  				if err != nil {
  1032  					t.Fatalf("Expected no error creating EndpointSlice, got %v", err)
  1033  				}
  1034  				numInitialActions++
  1035  			}
  1036  
  1037  			maxEndpointsPerSubset := tc.maxEndpointsPerSubset
  1038  			if maxEndpointsPerSubset == 0 {
  1039  				maxEndpointsPerSubset = defaultMaxEndpointsPerSubset
  1040  			}
  1041  			r := newReconciler(tCtx, client, maxEndpointsPerSubset)
  1042  			reconcileHelper(t, r, &endpoints, tc.existingEndpointSlices)
  1043  
  1044  			numExtraActions := len(client.Actions()) - numInitialActions
  1045  			if numExtraActions != tc.expectedClientActions {
  1046  				t.Fatalf("Expected %d additional client actions, got %d: %#v", tc.expectedClientActions, numExtraActions, client.Actions()[numInitialActions:])
  1047  			}
  1048  
  1049  			if tc.expectedMetrics != nil {
  1050  				expectMetrics(t, *tc.expectedMetrics)
  1051  			}
  1052  
  1053  			endpointSlices := fetchEndpointSlices(t, client, namespace)
  1054  			expectEndpointSlices(t, tc.expectedNumSlices, int(maxEndpointsPerSubset), endpoints, endpointSlices)
  1055  		})
  1056  	}
  1057  }
  1058  
  1059  // Test Helpers
  1060  
  1061  func newReconciler(ctx context.Context, client *fake.Clientset, maxEndpointsPerSubset int32) *reconciler {
  1062  	broadcaster := record.NewBroadcaster(record.WithContext(ctx))
  1063  	recorder := broadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "endpoint-slice-mirroring-controller"})
  1064  
  1065  	return &reconciler{
  1066  		client:                client,
  1067  		maxEndpointsPerSubset: maxEndpointsPerSubset,
  1068  		endpointSliceTracker:  endpointsliceutil.NewEndpointSliceTracker(),
  1069  		metricsCache:          metrics.NewCache(maxEndpointsPerSubset),
  1070  		eventRecorder:         recorder,
  1071  	}
  1072  }
  1073  
  1074  func expectEndpointSlices(t *testing.T, num, maxEndpointsPerSubset int, endpoints corev1.Endpoints, endpointSlices []discovery.EndpointSlice) {
  1075  	t.Helper()
  1076  	if len(endpointSlices) != num {
  1077  		t.Fatalf("Expected %d EndpointSlices, got %d", num, len(endpointSlices))
  1078  	}
  1079  
  1080  	if num == 0 {
  1081  		return
  1082  	}
  1083  
  1084  	for _, epSlice := range endpointSlices {
  1085  		if !strings.HasPrefix(epSlice.Name, endpoints.Name) {
  1086  			t.Errorf("Expected EndpointSlice name to start with %s, got %s", endpoints.Name, epSlice.Name)
  1087  		}
  1088  
  1089  		serviceNameVal, ok := epSlice.Labels[discovery.LabelServiceName]
  1090  		if !ok {
  1091  			t.Errorf("Expected EndpointSlice to have %s label set", discovery.LabelServiceName)
  1092  		}
  1093  		if serviceNameVal != endpoints.Name {
  1094  			t.Errorf("Expected EndpointSlice to have %s label set to %s, got %s", discovery.LabelServiceName, endpoints.Name, serviceNameVal)
  1095  		}
  1096  
  1097  		_, ok = epSlice.Annotations[corev1.LastAppliedConfigAnnotation]
  1098  		if ok {
  1099  			t.Errorf("Expected LastAppliedConfigAnnotation to be unset, got %s", epSlice.Annotations[corev1.LastAppliedConfigAnnotation])
  1100  		}
  1101  
  1102  		_, ok = epSlice.Annotations[corev1.EndpointsLastChangeTriggerTime]
  1103  		if ok {
  1104  			t.Errorf("Expected EndpointsLastChangeTriggerTime to be unset, got %s", epSlice.Annotations[corev1.EndpointsLastChangeTriggerTime])
  1105  		}
  1106  
  1107  		for annotation, val := range endpoints.Annotations {
  1108  			if annotation == corev1.EndpointsLastChangeTriggerTime || annotation == corev1.LastAppliedConfigAnnotation {
  1109  				continue
  1110  			}
  1111  			if epSlice.Annotations[annotation] != val {
  1112  				t.Errorf("Expected endpoint annotation %s to be mirrored correctly, got %s", annotation, epSlice.Annotations[annotation])
  1113  			}
  1114  		}
  1115  	}
  1116  
  1117  	// canonicalize endpoints to match the expected endpoints, otherwise the test
  1118  	// that creates more endpoints than allowed fail becaused the list of final
  1119  	// endpoints doesn't match.
  1120  	for _, epSubset := range endpointsv1.RepackSubsets(endpoints.Subsets) {
  1121  		if len(epSubset.Addresses) == 0 && len(epSubset.NotReadyAddresses) == 0 {
  1122  			continue
  1123  		}
  1124  
  1125  		var matchingEndpointsV4, matchingEndpointsV6 []discovery.Endpoint
  1126  
  1127  		for _, epSlice := range endpointSlices {
  1128  			if portsMatch(epSubset.Ports, epSlice.Ports) {
  1129  				switch epSlice.AddressType {
  1130  				case discovery.AddressTypeIPv4:
  1131  					matchingEndpointsV4 = append(matchingEndpointsV4, epSlice.Endpoints...)
  1132  				case discovery.AddressTypeIPv6:
  1133  					matchingEndpointsV6 = append(matchingEndpointsV6, epSlice.Endpoints...)
  1134  				default:
  1135  					t.Fatalf("Unexpected EndpointSlice address type found: %v", epSlice.AddressType)
  1136  				}
  1137  			}
  1138  		}
  1139  
  1140  		if len(matchingEndpointsV4) == 0 && len(matchingEndpointsV6) == 0 {
  1141  			t.Fatalf("No EndpointSlices match Endpoints subset: %#v", epSubset.Ports)
  1142  		}
  1143  
  1144  		expectMatchingAddresses(t, epSubset, matchingEndpointsV4, discovery.AddressTypeIPv4, maxEndpointsPerSubset)
  1145  		expectMatchingAddresses(t, epSubset, matchingEndpointsV6, discovery.AddressTypeIPv6, maxEndpointsPerSubset)
  1146  	}
  1147  }
  1148  
  1149  func portsMatch(epPorts []corev1.EndpointPort, epsPorts []discovery.EndpointPort) bool {
  1150  	if len(epPorts) != len(epsPorts) {
  1151  		return false
  1152  	}
  1153  
  1154  	portsToBeMatched := map[int32]corev1.EndpointPort{}
  1155  
  1156  	for _, epPort := range epPorts {
  1157  		portsToBeMatched[epPort.Port] = epPort
  1158  	}
  1159  
  1160  	for _, epsPort := range epsPorts {
  1161  		epPort, ok := portsToBeMatched[*epsPort.Port]
  1162  		if !ok {
  1163  			return false
  1164  		}
  1165  		delete(portsToBeMatched, *epsPort.Port)
  1166  
  1167  		if epPort.Name != *epsPort.Name {
  1168  			return false
  1169  		}
  1170  		if epPort.Port != *epsPort.Port {
  1171  			return false
  1172  		}
  1173  		if epPort.Protocol != *epsPort.Protocol {
  1174  			return false
  1175  		}
  1176  		if epPort.AppProtocol != epsPort.AppProtocol {
  1177  			return false
  1178  		}
  1179  	}
  1180  
  1181  	return true
  1182  }
  1183  
  1184  func expectMatchingAddresses(t *testing.T, epSubset corev1.EndpointSubset, esEndpoints []discovery.Endpoint, addrType discovery.AddressType, maxEndpointsPerSubset int) {
  1185  	t.Helper()
  1186  	type addressInfo struct {
  1187  		ready     bool
  1188  		epAddress corev1.EndpointAddress
  1189  	}
  1190  
  1191  	// This approach assumes that each IP is unique within an EndpointSubset.
  1192  	expectedEndpoints := map[string]addressInfo{}
  1193  
  1194  	for _, address := range epSubset.Addresses {
  1195  		at := getAddressType(address.IP)
  1196  		if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset {
  1197  			expectedEndpoints[address.IP] = addressInfo{
  1198  				ready:     true,
  1199  				epAddress: address,
  1200  			}
  1201  		}
  1202  	}
  1203  
  1204  	for _, address := range epSubset.NotReadyAddresses {
  1205  		at := getAddressType(address.IP)
  1206  		if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset {
  1207  			expectedEndpoints[address.IP] = addressInfo{
  1208  				ready:     false,
  1209  				epAddress: address,
  1210  			}
  1211  		}
  1212  	}
  1213  
  1214  	if len(expectedEndpoints) != len(esEndpoints) {
  1215  		t.Errorf("Expected %d endpoints, got %d", len(expectedEndpoints), len(esEndpoints))
  1216  	}
  1217  
  1218  	for _, endpoint := range esEndpoints {
  1219  		if len(endpoint.Addresses) != 1 {
  1220  			t.Fatalf("Expected endpoint to have 1 address, got %d", len(endpoint.Addresses))
  1221  		}
  1222  		address := endpoint.Addresses[0]
  1223  		expectedEndpoint, ok := expectedEndpoints[address]
  1224  
  1225  		if !ok {
  1226  			t.Fatalf("EndpointSlice has endpoint with unexpected address: %s", address)
  1227  		}
  1228  
  1229  		if expectedEndpoint.ready != *endpoint.Conditions.Ready {
  1230  			t.Errorf("Expected ready to be %t, got %t", expectedEndpoint.ready, *endpoint.Conditions.Ready)
  1231  		}
  1232  
  1233  		if endpoint.Hostname == nil {
  1234  			if expectedEndpoint.epAddress.Hostname != "" {
  1235  				t.Errorf("Expected hostname to be %s, got nil", expectedEndpoint.epAddress.Hostname)
  1236  			}
  1237  		} else if expectedEndpoint.epAddress.Hostname != *endpoint.Hostname {
  1238  			t.Errorf("Expected hostname to be %s, got %s", expectedEndpoint.epAddress.Hostname, *endpoint.Hostname)
  1239  		}
  1240  
  1241  		if expectedEndpoint.epAddress.NodeName != nil {
  1242  			if endpoint.NodeName == nil {
  1243  				t.Errorf("Expected nodeName to be set")
  1244  			}
  1245  			if *expectedEndpoint.epAddress.NodeName != *endpoint.NodeName {
  1246  				t.Errorf("Expected nodeName to be %s, got %s", *expectedEndpoint.epAddress.NodeName, *endpoint.NodeName)
  1247  			}
  1248  		}
  1249  	}
  1250  }
  1251  
  1252  func fetchEndpointSlices(t *testing.T, client *fake.Clientset, namespace string) []discovery.EndpointSlice {
  1253  	t.Helper()
  1254  	fetchedSlices, err := client.DiscoveryV1().EndpointSlices(namespace).List(context.TODO(), metav1.ListOptions{
  1255  		LabelSelector: discovery.LabelManagedBy + "=" + controllerName,
  1256  	})
  1257  	if err != nil {
  1258  		t.Fatalf("Expected no error fetching Endpoint Slices, got: %v", err)
  1259  		return []discovery.EndpointSlice{}
  1260  	}
  1261  	return fetchedSlices.Items
  1262  }
  1263  
  1264  func reconcileHelper(t *testing.T, r *reconciler, endpoints *corev1.Endpoints, existingSlices []*discovery.EndpointSlice) {
  1265  	t.Helper()
  1266  	logger, _ := ktesting.NewTestContext(t)
  1267  	err := r.reconcile(logger, endpoints, existingSlices)
  1268  	if err != nil {
  1269  		t.Fatalf("Expected no error reconciling Endpoint Slices, got: %v", err)
  1270  	}
  1271  }
  1272  
  1273  // Metrics helpers
  1274  
  1275  type expectedMetrics struct {
  1276  	desiredSlices    int
  1277  	actualSlices     int
  1278  	desiredEndpoints int
  1279  	addedPerSync     int
  1280  	updatedPerSync   int
  1281  	removedPerSync   int
  1282  	skippedPerSync   int
  1283  	numCreated       int
  1284  	numUpdated       int
  1285  	numDeleted       int
  1286  }
  1287  
  1288  func expectMetrics(t *testing.T, em expectedMetrics) {
  1289  	t.Helper()
  1290  
  1291  	actualDesiredSlices, err := testutil.GetGaugeMetricValue(metrics.DesiredEndpointSlices.WithLabelValues())
  1292  	handleErr(t, err, "desiredEndpointSlices")
  1293  	if actualDesiredSlices != float64(em.desiredSlices) {
  1294  		t.Errorf("Expected desiredEndpointSlices to be %d, got %v", em.desiredSlices, actualDesiredSlices)
  1295  	}
  1296  
  1297  	actualNumSlices, err := testutil.GetGaugeMetricValue(metrics.NumEndpointSlices.WithLabelValues())
  1298  	handleErr(t, err, "numEndpointSlices")
  1299  	if actualNumSlices != float64(em.actualSlices) {
  1300  		t.Errorf("Expected numEndpointSlices to be %d, got %v", em.actualSlices, actualNumSlices)
  1301  	}
  1302  
  1303  	actualEndpointsDesired, err := testutil.GetGaugeMetricValue(metrics.EndpointsDesired.WithLabelValues())
  1304  	handleErr(t, err, "desiredEndpoints")
  1305  	if actualEndpointsDesired != float64(em.desiredEndpoints) {
  1306  		t.Errorf("Expected desiredEndpoints to be %d, got %v", em.desiredEndpoints, actualEndpointsDesired)
  1307  	}
  1308  
  1309  	actualAddedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsAddedPerSync.WithLabelValues())
  1310  	handleErr(t, err, "endpointsAddedPerSync")
  1311  	if actualAddedPerSync != float64(em.addedPerSync) {
  1312  		t.Errorf("Expected endpointsAddedPerSync to be %d, got %v", em.addedPerSync, actualAddedPerSync)
  1313  	}
  1314  
  1315  	actualUpdatedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsUpdatedPerSync.WithLabelValues())
  1316  	handleErr(t, err, "endpointsUpdatedPerSync")
  1317  	if actualUpdatedPerSync != float64(em.updatedPerSync) {
  1318  		t.Errorf("Expected endpointsUpdatedPerSync to be %d, got %v", em.updatedPerSync, actualUpdatedPerSync)
  1319  	}
  1320  
  1321  	actualRemovedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsRemovedPerSync.WithLabelValues())
  1322  	handleErr(t, err, "endpointsRemovedPerSync")
  1323  	if actualRemovedPerSync != float64(em.removedPerSync) {
  1324  		t.Errorf("Expected endpointsRemovedPerSync to be %d, got %v", em.removedPerSync, actualRemovedPerSync)
  1325  	}
  1326  
  1327  	actualSkippedPerSync, err := testutil.GetHistogramMetricValue(metrics.AddressesSkippedPerSync.WithLabelValues())
  1328  	handleErr(t, err, "addressesSkippedPerSync")
  1329  	if actualSkippedPerSync != float64(em.skippedPerSync) {
  1330  		t.Errorf("Expected addressesSkippedPerSync to be %d, got %v", em.skippedPerSync, actualSkippedPerSync)
  1331  	}
  1332  
  1333  	actualCreated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("create"))
  1334  	handleErr(t, err, "endpointSliceChangesCreated")
  1335  	if actualCreated != float64(em.numCreated) {
  1336  		t.Errorf("Expected endpointSliceChangesCreated to be %d, got %v", em.numCreated, actualCreated)
  1337  	}
  1338  
  1339  	actualUpdated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("update"))
  1340  	handleErr(t, err, "endpointSliceChangesUpdated")
  1341  	if actualUpdated != float64(em.numUpdated) {
  1342  		t.Errorf("Expected endpointSliceChangesUpdated to be %d, got %v", em.numUpdated, actualUpdated)
  1343  	}
  1344  
  1345  	actualDeleted, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("delete"))
  1346  	handleErr(t, err, "desiredEndpointSlices")
  1347  	if actualDeleted != float64(em.numDeleted) {
  1348  		t.Errorf("Expected endpointSliceChangesDeleted to be %d, got %v", em.numDeleted, actualDeleted)
  1349  	}
  1350  }
  1351  
  1352  func handleErr(t *testing.T, err error, metricName string) {
  1353  	if err != nil {
  1354  		t.Errorf("Failed to get %s value, err: %v", metricName, err)
  1355  	}
  1356  }
  1357  
  1358  func setupMetrics() {
  1359  	metrics.RegisterMetrics()
  1360  	metrics.NumEndpointSlices.Delete(map[string]string{})
  1361  	metrics.DesiredEndpointSlices.Delete(map[string]string{})
  1362  	metrics.EndpointsDesired.Delete(map[string]string{})
  1363  	metrics.EndpointsAddedPerSync.Delete(map[string]string{})
  1364  	metrics.EndpointsUpdatedPerSync.Delete(map[string]string{})
  1365  	metrics.EndpointsRemovedPerSync.Delete(map[string]string{})
  1366  	metrics.AddressesSkippedPerSync.Delete(map[string]string{})
  1367  	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "create"})
  1368  	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "update"})
  1369  	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "delete"})
  1370  }
  1371  

View as plain text