k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/controlplane/reconcilers/endpointsadapter_test.go (about)

     1  /*
     2  Copyright 2019 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 reconcilers
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"testing"
    23  
    24  	corev1 "k8s.io/api/core/v1"
    25  	discovery "k8s.io/api/discovery/v1"
    26  	apiequality "k8s.io/apimachinery/pkg/api/equality"
    27  	"k8s.io/apimachinery/pkg/api/errors"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"k8s.io/client-go/kubernetes/fake"
    32  )
    33  
    34  var noEndpoints = &corev1.Endpoints{}
    35  
    36  func TestEndpointsAdapterGet(t *testing.T) {
    37  	endpoints1, epSlice1 := generateEndpointsAndSlice("foo", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4"})
    38  
    39  	testCases := map[string]struct {
    40  		expectedError     error
    41  		expectedEndpoints *corev1.Endpoints
    42  		initialState      []runtime.Object
    43  		namespaceParam    string
    44  		nameParam         string
    45  	}{
    46  		"single-existing-endpoints": {
    47  			expectedError:     nil,
    48  			expectedEndpoints: endpoints1,
    49  			initialState:      []runtime.Object{endpoints1, epSlice1},
    50  			namespaceParam:    "testing",
    51  			nameParam:         "foo",
    52  		},
    53  		"endpoints exists, endpointslice does not": {
    54  			expectedError:     nil,
    55  			expectedEndpoints: endpoints1,
    56  			initialState:      []runtime.Object{endpoints1},
    57  			namespaceParam:    "testing",
    58  			nameParam:         "foo",
    59  		},
    60  		"endpointslice exists, endpoints does not": {
    61  			expectedError:     errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "foo"),
    62  			expectedEndpoints: noEndpoints,
    63  			initialState:      []runtime.Object{epSlice1},
    64  			namespaceParam:    "testing",
    65  			nameParam:         "foo",
    66  		},
    67  		"wrong-namespace": {
    68  			expectedError:     errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "foo"),
    69  			expectedEndpoints: noEndpoints,
    70  			initialState:      []runtime.Object{endpoints1, epSlice1},
    71  			namespaceParam:    "foo",
    72  			nameParam:         "foo",
    73  		},
    74  		"wrong-name": {
    75  			expectedError:     errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "bar"),
    76  			expectedEndpoints: noEndpoints,
    77  			initialState:      []runtime.Object{endpoints1, epSlice1},
    78  			namespaceParam:    "testing",
    79  			nameParam:         "bar",
    80  		},
    81  	}
    82  
    83  	for name, testCase := range testCases {
    84  		t.Run(name, func(t *testing.T) {
    85  			client := fake.NewSimpleClientset(testCase.initialState...)
    86  			epAdapter := NewEndpointsAdapter(client.CoreV1(), client.DiscoveryV1())
    87  
    88  			endpoints, err := epAdapter.Get(testCase.namespaceParam, testCase.nameParam, metav1.GetOptions{})
    89  
    90  			if !apiequality.Semantic.DeepEqual(testCase.expectedError, err) {
    91  				t.Errorf("Expected error: %v, got: %v", testCase.expectedError, err)
    92  			}
    93  
    94  			if !apiequality.Semantic.DeepEqual(endpoints, testCase.expectedEndpoints) {
    95  				t.Errorf("Expected endpoints: %v, got: %v", testCase.expectedEndpoints, endpoints)
    96  			}
    97  		})
    98  	}
    99  }
   100  
   101  func TestEndpointsAdapterCreate(t *testing.T) {
   102  	endpoints1, epSlice1 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.3", "10.1.2.4"})
   103  
   104  	// even if an Endpoints resource includes an IPv6 address, it should not be
   105  	// included in the corresponding EndpointSlice.
   106  	endpoints2, _ := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.5", "10.1.2.6", "1234::5678:0000:0000:9abc:def0"})
   107  	_, epSlice2 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.5", "10.1.2.6"})
   108  
   109  	// ensure that Endpoints with only IPv6 addresses result in EndpointSlice
   110  	// with an IPv6 address type.
   111  	endpoints3, epSlice3 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"1234::5678:0000:0000:9abc:def0"})
   112  	epSlice3.AddressType = discovery.AddressTypeIPv6
   113  
   114  	testCases := map[string]struct {
   115  		expectedError  error
   116  		expectedResult *corev1.Endpoints
   117  		expectCreate   []runtime.Object
   118  		expectUpdate   []runtime.Object
   119  		initialState   []runtime.Object
   120  		namespaceParam string
   121  		endpointsParam *corev1.Endpoints
   122  	}{
   123  		"single-endpoint": {
   124  			expectedError:  nil,
   125  			expectedResult: endpoints1,
   126  			expectCreate:   []runtime.Object{endpoints1, epSlice1},
   127  			initialState:   []runtime.Object{},
   128  			namespaceParam: endpoints1.Namespace,
   129  			endpointsParam: endpoints1,
   130  		},
   131  		"single-endpoint-partial-ipv6": {
   132  			expectedError:  nil,
   133  			expectedResult: endpoints2,
   134  			expectCreate:   []runtime.Object{endpoints2, epSlice2},
   135  			initialState:   []runtime.Object{},
   136  			namespaceParam: endpoints2.Namespace,
   137  			endpointsParam: endpoints2,
   138  		},
   139  		"single-endpoint-full-ipv6": {
   140  			expectedError:  nil,
   141  			expectedResult: endpoints3,
   142  			expectCreate:   []runtime.Object{endpoints3, epSlice3},
   143  			initialState:   []runtime.Object{},
   144  			namespaceParam: endpoints3.Namespace,
   145  			endpointsParam: endpoints3,
   146  		},
   147  		"existing-endpoints": {
   148  			expectedError:  errors.NewAlreadyExists(schema.GroupResource{Group: "", Resource: "endpoints"}, "foo"),
   149  			expectedResult: noEndpoints,
   150  			initialState:   []runtime.Object{endpoints1, epSlice1},
   151  			namespaceParam: endpoints1.Namespace,
   152  			endpointsParam: endpoints1,
   153  
   154  			// We expect the create to be attempted, we just also expect it to fail
   155  			expectCreate: []runtime.Object{endpoints1},
   156  		},
   157  		"existing-endpointslice-incorrect": {
   158  			// No error when we need to create the Endpoints but the correct
   159  			// EndpointSlice already exists
   160  			expectedError:  nil,
   161  			expectedResult: endpoints1,
   162  			expectCreate:   []runtime.Object{endpoints1},
   163  			initialState:   []runtime.Object{epSlice1},
   164  			namespaceParam: endpoints1.Namespace,
   165  			endpointsParam: endpoints1,
   166  		},
   167  		"existing-endpointslice-correct": {
   168  			// No error when we need to create the Endpoints but an incorrect
   169  			// EndpointSlice already exists
   170  			expectedError:  nil,
   171  			expectedResult: endpoints2,
   172  			expectCreate:   []runtime.Object{endpoints2},
   173  			expectUpdate:   []runtime.Object{epSlice2},
   174  			initialState:   []runtime.Object{epSlice1},
   175  			namespaceParam: endpoints2.Namespace,
   176  			endpointsParam: endpoints2,
   177  		},
   178  	}
   179  
   180  	for name, testCase := range testCases {
   181  		t.Run(name, func(t *testing.T) {
   182  			client := fake.NewSimpleClientset(testCase.initialState...)
   183  			epAdapter := NewEndpointsAdapter(client.CoreV1(), client.DiscoveryV1())
   184  
   185  			endpoints, err := epAdapter.Create(testCase.namespaceParam, testCase.endpointsParam)
   186  
   187  			if !apiequality.Semantic.DeepEqual(testCase.expectedError, err) {
   188  				t.Errorf("Expected error: %v, got: %v", testCase.expectedError, err)
   189  			}
   190  
   191  			if !apiequality.Semantic.DeepEqual(endpoints, testCase.expectedResult) {
   192  				t.Errorf("Expected endpoints: %v, got: %v", testCase.expectedResult, endpoints)
   193  			}
   194  
   195  			err = verifyCreatesAndUpdates(client, testCase.expectCreate, testCase.expectUpdate)
   196  			if err != nil {
   197  				t.Errorf("unexpected error in side effects: %v", err)
   198  			}
   199  		})
   200  	}
   201  }
   202  
   203  func TestEndpointsAdapterUpdate(t *testing.T) {
   204  	endpoints1, epSlice1 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.3", "10.1.2.4"})
   205  	endpoints2, epSlice2 := generateEndpointsAndSlice("foo", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4", "10.1.2.5"})
   206  	endpoints3, _ := generateEndpointsAndSlice("bar", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4", "10.1.2.5"})
   207  
   208  	// ensure that EndpointSlice with deprecated IP address type is replaced
   209  	// with one that has an IPv4 address type.
   210  	endpoints4, _ := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.7", "10.1.2.8"})
   211  	_, epSlice4IP := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.7", "10.1.2.8"})
   212  	// "IP" is a deprecated address type, ensuring that it is handled properly.
   213  	epSlice4IP.AddressType = discovery.AddressType("IP")
   214  	_, epSlice4IPv4 := generateEndpointsAndSlice("foo", "testing", []int{80}, []string{"10.1.2.7", "10.1.2.8"})
   215  
   216  	testCases := map[string]struct {
   217  		expectedError  error
   218  		expectedResult *corev1.Endpoints
   219  		expectCreate   []runtime.Object
   220  		expectUpdate   []runtime.Object
   221  		initialState   []runtime.Object
   222  		namespaceParam string
   223  		endpointsParam *corev1.Endpoints
   224  	}{
   225  		"single-existing-endpoints-no-change": {
   226  			expectedError:  nil,
   227  			expectedResult: endpoints1,
   228  			initialState:   []runtime.Object{endpoints1, epSlice1},
   229  			namespaceParam: "testing",
   230  			endpointsParam: endpoints1,
   231  
   232  			// Even though there's no change, we still expect Update() to be
   233  			// called, because this unit test ALWAYS calls Update().
   234  			expectUpdate: []runtime.Object{endpoints1},
   235  		},
   236  		"existing-endpointslice-replaced-with-updated-ipv4-address-type": {
   237  			expectedError:  nil,
   238  			expectedResult: endpoints4,
   239  			initialState:   []runtime.Object{endpoints4, epSlice4IP},
   240  			namespaceParam: "testing",
   241  			endpointsParam: endpoints4,
   242  
   243  			// When AddressType changes, we Delete+Create the EndpointSlice,
   244  			// so that shows up in expectCreate, not expectUpdate.
   245  			expectUpdate: []runtime.Object{endpoints4},
   246  			expectCreate: []runtime.Object{epSlice4IPv4},
   247  		},
   248  		"add-ports-and-ips": {
   249  			expectedError:  nil,
   250  			expectedResult: endpoints2,
   251  			expectUpdate:   []runtime.Object{endpoints2, epSlice2},
   252  			initialState:   []runtime.Object{endpoints1, epSlice1},
   253  			namespaceParam: "testing",
   254  			endpointsParam: endpoints2,
   255  		},
   256  		"endpoints-correct-endpointslice-wrong": {
   257  			expectedError:  nil,
   258  			expectedResult: endpoints2,
   259  			expectUpdate:   []runtime.Object{endpoints2, epSlice2},
   260  			initialState:   []runtime.Object{endpoints2, epSlice1},
   261  			namespaceParam: "testing",
   262  			endpointsParam: endpoints2,
   263  		},
   264  		"endpointslice-correct-endpoints-wrong": {
   265  			expectedError:  nil,
   266  			expectedResult: endpoints2,
   267  			expectUpdate:   []runtime.Object{endpoints2},
   268  			initialState:   []runtime.Object{endpoints1, epSlice2},
   269  			namespaceParam: "testing",
   270  			endpointsParam: endpoints2,
   271  		},
   272  		"wrong-endpoints": {
   273  			expectedError:  errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "bar"),
   274  			expectedResult: noEndpoints,
   275  			expectUpdate:   []runtime.Object{endpoints3},
   276  			initialState:   []runtime.Object{endpoints1, epSlice1},
   277  			namespaceParam: "testing",
   278  			endpointsParam: endpoints3,
   279  		},
   280  		"missing-endpoints": {
   281  			expectedError:  errors.NewNotFound(schema.GroupResource{Group: "", Resource: "endpoints"}, "bar"),
   282  			expectedResult: noEndpoints,
   283  			initialState:   []runtime.Object{endpoints1, epSlice1},
   284  			namespaceParam: "testing",
   285  			endpointsParam: endpoints3,
   286  
   287  			// We expect the update to be attempted, we just also expect it to fail
   288  			expectUpdate: []runtime.Object{endpoints3},
   289  		},
   290  		"missing-endpointslice": {
   291  			// No error when we need to update the Endpoints but the
   292  			// EndpointSlice doesn't exist
   293  			expectedError:  nil,
   294  			expectedResult: endpoints1,
   295  			expectUpdate:   []runtime.Object{endpoints1},
   296  			expectCreate:   []runtime.Object{epSlice1},
   297  			initialState:   []runtime.Object{endpoints2},
   298  			namespaceParam: "testing",
   299  			endpointsParam: endpoints1,
   300  		},
   301  	}
   302  
   303  	for name, testCase := range testCases {
   304  		t.Run(name, func(t *testing.T) {
   305  			client := fake.NewSimpleClientset(testCase.initialState...)
   306  			epAdapter := NewEndpointsAdapter(client.CoreV1(), client.DiscoveryV1())
   307  
   308  			endpoints, err := epAdapter.Update(testCase.namespaceParam, testCase.endpointsParam)
   309  
   310  			if !apiequality.Semantic.DeepEqual(testCase.expectedError, err) {
   311  				t.Errorf("Expected error: %v, got: %v", testCase.expectedError, err)
   312  			}
   313  
   314  			if !apiequality.Semantic.DeepEqual(endpoints, testCase.expectedResult) {
   315  				t.Errorf("Expected endpoints: %v, got: %v", testCase.expectedResult, endpoints)
   316  			}
   317  
   318  			err = verifyCreatesAndUpdates(client, testCase.expectCreate, testCase.expectUpdate)
   319  			if err != nil {
   320  				t.Errorf("unexpected error in side effects: %v", err)
   321  			}
   322  		})
   323  	}
   324  }
   325  
   326  func generateEndpointsAndSlice(name, namespace string, ports []int, addresses []string) (*corev1.Endpoints, *discovery.EndpointSlice) {
   327  	trueBool := true
   328  
   329  	epSlice := &discovery.EndpointSlice{
   330  		ObjectMeta:  metav1.ObjectMeta{Name: name, Namespace: namespace},
   331  		AddressType: discovery.AddressTypeIPv4,
   332  	}
   333  	epSlice.Labels = map[string]string{discovery.LabelServiceName: name}
   334  	subset := corev1.EndpointSubset{}
   335  
   336  	for i, port := range ports {
   337  		endpointPort := corev1.EndpointPort{
   338  			Name:     fmt.Sprintf("port-%d", i),
   339  			Port:     int32(port),
   340  			Protocol: corev1.ProtocolTCP,
   341  		}
   342  		subset.Ports = append(subset.Ports, endpointPort)
   343  		epSlice.Ports = append(epSlice.Ports, discovery.EndpointPort{
   344  			Name:     &endpointPort.Name,
   345  			Port:     &endpointPort.Port,
   346  			Protocol: &endpointPort.Protocol,
   347  		})
   348  	}
   349  
   350  	for i, address := range addresses {
   351  		endpointAddress := corev1.EndpointAddress{
   352  			IP: address,
   353  			TargetRef: &corev1.ObjectReference{
   354  				Kind: "Pod",
   355  				Name: fmt.Sprintf("pod-%d", i),
   356  			},
   357  		}
   358  
   359  		subset.Addresses = append(subset.Addresses, endpointAddress)
   360  
   361  		epSlice.Endpoints = append(epSlice.Endpoints, discovery.Endpoint{
   362  			Addresses:  []string{endpointAddress.IP},
   363  			TargetRef:  endpointAddress.TargetRef,
   364  			Conditions: discovery.EndpointConditions{Ready: &trueBool},
   365  		})
   366  	}
   367  
   368  	return &corev1.Endpoints{
   369  		ObjectMeta: metav1.ObjectMeta{
   370  			Name:      name,
   371  			Namespace: namespace,
   372  			Labels: map[string]string{
   373  				discovery.LabelSkipMirror: "true",
   374  			},
   375  		},
   376  		Subsets: []corev1.EndpointSubset{subset},
   377  	}, epSlice
   378  }
   379  
   380  func TestEndpointManagerEnsureEndpointSliceFromEndpoints(t *testing.T) {
   381  	endpoints1, epSlice1 := generateEndpointsAndSlice("foo", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4"})
   382  	endpoints2, epSlice2 := generateEndpointsAndSlice("foo", "testing", []int{80, 443}, []string{"10.1.2.3", "10.1.2.4", "10.1.2.5"})
   383  
   384  	testCases := map[string]struct {
   385  		expectedError         error
   386  		expectedEndpointSlice *discovery.EndpointSlice
   387  		initialState          []runtime.Object
   388  		namespaceParam        string
   389  		endpointsParam        *corev1.Endpoints
   390  	}{
   391  		"existing-endpointslice-no-change": {
   392  			expectedError:         nil,
   393  			expectedEndpointSlice: epSlice1,
   394  			initialState:          []runtime.Object{epSlice1},
   395  			namespaceParam:        "testing",
   396  			endpointsParam:        endpoints1,
   397  		},
   398  		"existing-endpointslice-change": {
   399  			expectedError:         nil,
   400  			expectedEndpointSlice: epSlice2,
   401  			initialState:          []runtime.Object{epSlice1},
   402  			namespaceParam:        "testing",
   403  			endpointsParam:        endpoints2,
   404  		},
   405  		"missing-endpointslice": {
   406  			expectedError:         nil,
   407  			expectedEndpointSlice: epSlice1,
   408  			initialState:          []runtime.Object{},
   409  			namespaceParam:        "testing",
   410  			endpointsParam:        endpoints1,
   411  		},
   412  	}
   413  
   414  	for name, testCase := range testCases {
   415  		t.Run(name, func(t *testing.T) {
   416  			client := fake.NewSimpleClientset(testCase.initialState...)
   417  			epAdapter := NewEndpointsAdapter(client.CoreV1(), client.DiscoveryV1())
   418  
   419  			err := epAdapter.EnsureEndpointSliceFromEndpoints(testCase.namespaceParam, testCase.endpointsParam)
   420  			if !apiequality.Semantic.DeepEqual(testCase.expectedError, err) {
   421  				t.Errorf("Expected error: %v, got: %v", testCase.expectedError, err)
   422  			}
   423  
   424  			endpointSlice, err := client.DiscoveryV1().EndpointSlices(testCase.namespaceParam).Get(context.TODO(), testCase.endpointsParam.Name, metav1.GetOptions{})
   425  			if err != nil && !errors.IsNotFound(err) {
   426  				t.Fatalf("Error getting Endpoint Slice: %v", err)
   427  			}
   428  
   429  			if !apiequality.Semantic.DeepEqual(endpointSlice, testCase.expectedEndpointSlice) {
   430  				t.Errorf("Expected Endpoint Slice: %v, got: %v", testCase.expectedEndpointSlice, endpointSlice)
   431  			}
   432  		})
   433  	}
   434  }