mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 22:01:06 +00:00 
			
		
		
		
	Handles incorrect mirroring of endpoint annotations to created endpoint slices, specifically the last-applied-config. Also updates tests and adds test cases for the same
		
			
				
	
	
		
			1267 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1267 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2020 The Kubernetes Authors.
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package endpointslicemirroring
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	corev1 "k8s.io/api/core/v1"
 | |
| 	discovery "k8s.io/api/discovery/v1"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/client-go/kubernetes/fake"
 | |
| 	"k8s.io/client-go/kubernetes/scheme"
 | |
| 	"k8s.io/client-go/tools/record"
 | |
| 	"k8s.io/component-base/metrics/testutil"
 | |
| 	"k8s.io/kubernetes/pkg/controller/endpointslicemirroring/metrics"
 | |
| 	utilpointer "k8s.io/utils/pointer"
 | |
| )
 | |
| 
 | |
| const defaultMaxEndpointsPerSubset = int32(1000)
 | |
| 
 | |
| // TestReconcile ensures that Endpoints are reconciled into corresponding
 | |
| // EndpointSlices with appropriate fields.
 | |
| func TestReconcile(t *testing.T) {
 | |
| 	protoTCP := corev1.ProtocolTCP
 | |
| 	protoUDP := corev1.ProtocolUDP
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		testName                 string
 | |
| 		subsets                  []corev1.EndpointSubset
 | |
| 		epLabels                 map[string]string
 | |
| 		epAnnotations            map[string]string
 | |
| 		endpointsDeletionPending bool
 | |
| 		maxEndpointsPerSubset    int32
 | |
| 		existingEndpointSlices   []*discovery.EndpointSlice
 | |
| 		expectedNumSlices        int
 | |
| 		expectedClientActions    int
 | |
| 		expectedMetrics          *expectedMetrics
 | |
| 	}{{
 | |
| 		testName:               "Endpoints with no subsets",
 | |
| 		subsets:                []corev1.EndpointSubset{},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      0,
 | |
| 		expectedClientActions:  0,
 | |
| 		expectedMetrics:        &expectedMetrics{},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with no addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      0,
 | |
| 		expectedClientActions:  0,
 | |
| 		expectedMetrics:        &expectedMetrics{},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 1 subset, port, and address",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      1,
 | |
| 		expectedClientActions:  1,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 1, addedPerSync: 1, numCreated: 1},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 1 subset, port, and address, pending deletion",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		endpointsDeletionPending: true,
 | |
| 		existingEndpointSlices:   []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:        0,
 | |
| 		expectedClientActions:    0,
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 1 subset, port, and address and existing slice with same fields",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "test-ep-1",
 | |
| 			},
 | |
| 			AddressType: discovery.AddressTypeIPv4,
 | |
| 			Ports: []discovery.EndpointPort{{
 | |
| 				Name:     utilpointer.StringPtr("http"),
 | |
| 				Port:     utilpointer.Int32Ptr(80),
 | |
| 				Protocol: &protoTCP,
 | |
| 			}},
 | |
| 			Endpoints: []discovery.Endpoint{{
 | |
| 				Addresses:  []string{"10.0.0.1"},
 | |
| 				Hostname:   utilpointer.StringPtr("pod-1"),
 | |
| 				Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
 | |
| 			}},
 | |
| 		}},
 | |
| 		expectedNumSlices:     1,
 | |
| 		expectedClientActions: 0,
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 1 subset, port, and address and existing slice with an additional annotation",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name:        "test-ep-1",
 | |
| 				Annotations: map[string]string{"foo": "bar"},
 | |
| 			},
 | |
| 			AddressType: discovery.AddressTypeIPv4,
 | |
| 			Ports: []discovery.EndpointPort{{
 | |
| 				Name:     utilpointer.StringPtr("http"),
 | |
| 				Port:     utilpointer.Int32Ptr(80),
 | |
| 				Protocol: &protoTCP,
 | |
| 			}},
 | |
| 			Endpoints: []discovery.Endpoint{{
 | |
| 				Addresses:  []string{"10.0.0.1"},
 | |
| 				Hostname:   utilpointer.StringPtr("pod-1"),
 | |
| 				Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
 | |
| 			}},
 | |
| 		}},
 | |
| 		expectedNumSlices:     1,
 | |
| 		expectedClientActions: 1,
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 1 subset, port, label and address and existing slice with same fields but the label",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 			}},
 | |
| 		}},
 | |
| 		epLabels: map[string]string{"foo": "bar"},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name:        "test-ep-1",
 | |
| 				Annotations: map[string]string{"foo": "bar"},
 | |
| 			},
 | |
| 			AddressType: discovery.AddressTypeIPv4,
 | |
| 			Ports: []discovery.EndpointPort{{
 | |
| 				Name:     utilpointer.StringPtr("http"),
 | |
| 				Port:     utilpointer.Int32Ptr(80),
 | |
| 				Protocol: &protoTCP,
 | |
| 			}},
 | |
| 			Endpoints: []discovery.Endpoint{{
 | |
| 				Addresses:  []string{"10.0.0.1"},
 | |
| 				Hostname:   utilpointer.StringPtr("pod-1"),
 | |
| 				Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
 | |
| 			}},
 | |
| 		}},
 | |
| 		expectedNumSlices:     1,
 | |
| 		expectedClientActions: 1,
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 1 subset, 2 ports, and 2 addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      1,
 | |
| 		expectedClientActions:  1,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 1 subset, 2 ports, and 2 not ready addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			NotReadyAddresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      1,
 | |
| 		expectedClientActions:  1,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 1 subset, 2 ports, and 2 ready and 2 not ready addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.1.1.1",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.1.1.2",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 			NotReadyAddresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      1,
 | |
| 		expectedClientActions:  1,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 4, addedPerSync: 4, numCreated: 1},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, multiple ports and addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.1.1",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.2",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.3",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      2,
 | |
| 		expectedClientActions:  2,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 2},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, multiple ports and addresses, existing empty EndpointSlice",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.1.1",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.2",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.3",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "test-ep-1",
 | |
| 			},
 | |
| 			AddressType: discovery.AddressTypeIPv4,
 | |
| 			Ports: []discovery.EndpointPort{{
 | |
| 				Name:     utilpointer.StringPtr("http"),
 | |
| 				Port:     utilpointer.Int32Ptr(80),
 | |
| 				Protocol: &protoTCP,
 | |
| 			}, {
 | |
| 				Name:     utilpointer.StringPtr("https"),
 | |
| 				Port:     utilpointer.Int32Ptr(443),
 | |
| 				Protocol: &protoUDP,
 | |
| 			}},
 | |
| 		}},
 | |
| 		expectedNumSlices:     2,
 | |
| 		expectedClientActions: 2,
 | |
| 		expectedMetrics:       &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 1, numUpdated: 1},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice with some addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.1.1",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.2",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.3",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "test-ep-1",
 | |
| 			},
 | |
| 			AddressType: discovery.AddressTypeIPv4,
 | |
| 			Ports: []discovery.EndpointPort{{
 | |
| 				Name:     utilpointer.StringPtr("http"),
 | |
| 				Port:     utilpointer.Int32Ptr(80),
 | |
| 				Protocol: &protoTCP,
 | |
| 			}, {
 | |
| 				Name:     utilpointer.StringPtr("https"),
 | |
| 				Port:     utilpointer.Int32Ptr(443),
 | |
| 				Protocol: &protoUDP,
 | |
| 			}},
 | |
| 			Endpoints: []discovery.Endpoint{{
 | |
| 				Addresses: []string{"10.0.0.2"},
 | |
| 				Hostname:  utilpointer.StringPtr("pod-2"),
 | |
| 			}, {
 | |
| 				Addresses: []string{"10.0.0.1", "10.0.0.3"},
 | |
| 				Hostname:  utilpointer.StringPtr("pod-1"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		expectedNumSlices:     2,
 | |
| 		expectedClientActions: 2,
 | |
| 		expectedMetrics:       &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 4, updatedPerSync: 1, removedPerSync: 1, numCreated: 1, numUpdated: 1},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice identical to subset",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.1.1",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.2",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.3",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "test-ep-1",
 | |
| 			},
 | |
| 			AddressType: discovery.AddressTypeIPv4,
 | |
| 			Ports: []discovery.EndpointPort{{
 | |
| 				Name:     utilpointer.StringPtr("http"),
 | |
| 				Port:     utilpointer.Int32Ptr(80),
 | |
| 				Protocol: &protoTCP,
 | |
| 			}, {
 | |
| 				Name:     utilpointer.StringPtr("https"),
 | |
| 				Port:     utilpointer.Int32Ptr(443),
 | |
| 				Protocol: &protoUDP,
 | |
| 			}},
 | |
| 			Endpoints: []discovery.Endpoint{{
 | |
| 				Addresses:  []string{"10.0.0.1"},
 | |
| 				Hostname:   utilpointer.StringPtr("pod-1"),
 | |
| 				NodeName:   utilpointer.StringPtr("node-1"),
 | |
| 				Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
 | |
| 			}, {
 | |
| 				Addresses:  []string{"10.0.0.2"},
 | |
| 				Hostname:   utilpointer.StringPtr("pod-2"),
 | |
| 				NodeName:   utilpointer.StringPtr("node-2"),
 | |
| 				Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
 | |
| 			}},
 | |
| 		}},
 | |
| 		expectedNumSlices:     2,
 | |
| 		expectedClientActions: 1,
 | |
| 		expectedMetrics:       &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 3, numCreated: 1},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, multiple ports, and dual stack addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "2001:db8:2222:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.1.1",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.2",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "2001:db8:3333:4444:5555:6666:7777:8888",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      4,
 | |
| 		expectedClientActions:  4,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 4, actualSlices: 4, desiredEndpoints: 5, addedPerSync: 5, numCreated: 4},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, multiple ports, ipv6 only addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "2001:db8:1111:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "2001:db8:2222:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "2001:db8:3333:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "2001:db8:4444:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "2001:db8:5555:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      2,
 | |
| 		expectedClientActions:  2,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 2},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, multiple ports, some invalid addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "2001:db8:1111:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "this-is-not-an-ip",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "this-is-also-not-an-ip",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "2001:db8:4444:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "2001:db8:5555:3333:4444:5555:6666:7777",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      2,
 | |
| 		expectedClientActions:  2,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 3, addedPerSync: 3, skippedPerSync: 2, numCreated: 2},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, multiple ports, all invalid addresses",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "this-is-not-an-ip1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "this-is-not-an-ip12",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "this-is-not-an-ip11",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "this-is-not-an-ip12",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "this-is-not-an-ip3",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      0,
 | |
| 		expectedClientActions:  0,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 0, actualSlices: 0, desiredEndpoints: 0, addedPerSync: 0, skippedPerSync: 5, numCreated: 0},
 | |
| 	}, {
 | |
| 		testName: "Endpoints with 2 subsets, 1 exceeding maxEndpointsPerSubset",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     443,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.0.2",
 | |
| 				Hostname: "pod-2",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}},
 | |
| 		}, {
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     3000,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}, {
 | |
| 				Name:     "https",
 | |
| 				Port:     3001,
 | |
| 				Protocol: corev1.ProtocolUDP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.1.1",
 | |
| 				Hostname: "pod-11",
 | |
| 				NodeName: utilpointer.StringPtr("node-1"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.2",
 | |
| 				Hostname: "pod-12",
 | |
| 				NodeName: utilpointer.StringPtr("node-2"),
 | |
| 			}, {
 | |
| 				IP:       "10.0.1.3",
 | |
| 				Hostname: "pod-13",
 | |
| 				NodeName: utilpointer.StringPtr("node-3"),
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      2,
 | |
| 		expectedClientActions:  2,
 | |
| 		maxEndpointsPerSubset:  2,
 | |
| 		expectedMetrics:        &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 4, addedPerSync: 4, updatedPerSync: 0, removedPerSync: 0, skippedPerSync: 1, numCreated: 2, numUpdated: 0},
 | |
| 	}, {
 | |
| 		testName: "The last-applied-configuration annotation should not get mirrored to created or updated endpoint slices",
 | |
| 		epAnnotations: map[string]string{
 | |
| 			corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}",
 | |
| 		},
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      1,
 | |
| 		expectedClientActions:  1,
 | |
| 		expectedMetrics:        &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1},
 | |
| 	}, {
 | |
| 		testName: "The last-applied-configuration annotation shouldn't get added to created endpoint slices",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      1,
 | |
| 		expectedClientActions:  1,
 | |
| 		expectedMetrics:        &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1},
 | |
| 	}, {
 | |
| 		testName: "The last-applied-configuration shouldn't get mirrored to endpoint slices when it's value is empty",
 | |
| 		epAnnotations: map[string]string{
 | |
| 			corev1.LastAppliedConfigAnnotation: "",
 | |
| 		},
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      1,
 | |
| 		expectedClientActions:  1,
 | |
| 		expectedMetrics:        &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1},
 | |
| 	}, {
 | |
| 		testName: "Annotations other than last-applied-configuration should get correctly mirrored",
 | |
| 		epAnnotations: map[string]string{
 | |
| 			corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}",
 | |
| 			"foo":                              "bar",
 | |
| 		},
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{},
 | |
| 		expectedNumSlices:      1,
 | |
| 		expectedClientActions:  1,
 | |
| 		expectedMetrics:        &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1},
 | |
| 	}, {
 | |
| 		testName: "Annotation mirroring should remove the last-applied-configuration annotation from existing endpoint slices",
 | |
| 		subsets: []corev1.EndpointSubset{{
 | |
| 			Ports: []corev1.EndpointPort{{
 | |
| 				Name:     "http",
 | |
| 				Port:     80,
 | |
| 				Protocol: corev1.ProtocolTCP,
 | |
| 			}},
 | |
| 			Addresses: []corev1.EndpointAddress{{
 | |
| 				IP:       "10.0.0.1",
 | |
| 				Hostname: "pod-1",
 | |
| 			}},
 | |
| 		}},
 | |
| 		existingEndpointSlices: []*discovery.EndpointSlice{{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "test-ep-1",
 | |
| 				Annotations: map[string]string{
 | |
| 					corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}",
 | |
| 				},
 | |
| 			},
 | |
| 			AddressType: discovery.AddressTypeIPv4,
 | |
| 			Ports: []discovery.EndpointPort{{
 | |
| 				Name:     utilpointer.StringPtr("http"),
 | |
| 				Port:     utilpointer.Int32Ptr(80),
 | |
| 				Protocol: &protoTCP,
 | |
| 			}},
 | |
| 			Endpoints: []discovery.Endpoint{{
 | |
| 				Addresses:  []string{"10.0.0.1"},
 | |
| 				Hostname:   utilpointer.StringPtr("pod-1"),
 | |
| 				Conditions: discovery.EndpointConditions{Ready: utilpointer.BoolPtr(true)},
 | |
| 			}},
 | |
| 		}},
 | |
| 		expectedNumSlices:     1,
 | |
| 		expectedClientActions: 1,
 | |
| 	}}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		t.Run(tc.testName, func(t *testing.T) {
 | |
| 			client := newClientset()
 | |
| 			setupMetrics()
 | |
| 			namespace := "test"
 | |
| 			endpoints := corev1.Endpoints{
 | |
| 				ObjectMeta: metav1.ObjectMeta{Name: "test-ep", Namespace: namespace, Labels: tc.epLabels, Annotations: tc.epAnnotations},
 | |
| 				Subsets:    tc.subsets,
 | |
| 			}
 | |
| 
 | |
| 			if tc.endpointsDeletionPending {
 | |
| 				now := metav1.Now()
 | |
| 				endpoints.DeletionTimestamp = &now
 | |
| 			}
 | |
| 
 | |
| 			numInitialActions := 0
 | |
| 			for _, epSlice := range tc.existingEndpointSlices {
 | |
| 				epSlice.Labels = map[string]string{
 | |
| 					discovery.LabelServiceName: endpoints.Name,
 | |
| 					discovery.LabelManagedBy:   controllerName,
 | |
| 				}
 | |
| 				_, err := client.DiscoveryV1().EndpointSlices(namespace).Create(context.TODO(), epSlice, metav1.CreateOptions{})
 | |
| 				if err != nil {
 | |
| 					t.Fatalf("Expected no error creating EndpointSlice, got %v", err)
 | |
| 				}
 | |
| 				numInitialActions++
 | |
| 			}
 | |
| 
 | |
| 			maxEndpointsPerSubset := tc.maxEndpointsPerSubset
 | |
| 			if maxEndpointsPerSubset == 0 {
 | |
| 				maxEndpointsPerSubset = defaultMaxEndpointsPerSubset
 | |
| 			}
 | |
| 			r := newReconciler(client, maxEndpointsPerSubset)
 | |
| 			reconcileHelper(t, r, &endpoints, tc.existingEndpointSlices)
 | |
| 
 | |
| 			numExtraActions := len(client.Actions()) - numInitialActions
 | |
| 			if numExtraActions != tc.expectedClientActions {
 | |
| 				t.Fatalf("Expected %d additional client actions, got %d: %#v", tc.expectedClientActions, numExtraActions, client.Actions()[numInitialActions:])
 | |
| 			}
 | |
| 
 | |
| 			if tc.expectedMetrics != nil {
 | |
| 				expectMetrics(t, *tc.expectedMetrics)
 | |
| 			}
 | |
| 
 | |
| 			endpointSlices := fetchEndpointSlices(t, client, namespace)
 | |
| 			expectEndpointSlices(t, tc.expectedNumSlices, int(maxEndpointsPerSubset), endpoints, endpointSlices)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Test Helpers
 | |
| 
 | |
| func newReconciler(client *fake.Clientset, maxEndpointsPerSubset int32) *reconciler {
 | |
| 	broadcaster := record.NewBroadcaster()
 | |
| 	recorder := broadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "endpoint-slice-mirroring-controller"})
 | |
| 
 | |
| 	return &reconciler{
 | |
| 		client:                client,
 | |
| 		maxEndpointsPerSubset: maxEndpointsPerSubset,
 | |
| 		endpointSliceTracker:  newEndpointSliceTracker(),
 | |
| 		metricsCache:          metrics.NewCache(maxEndpointsPerSubset),
 | |
| 		eventRecorder:         recorder,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func expectEndpointSlices(t *testing.T, num, maxEndpointsPerSubset int, endpoints corev1.Endpoints, endpointSlices []discovery.EndpointSlice) {
 | |
| 	t.Helper()
 | |
| 	if len(endpointSlices) != num {
 | |
| 		t.Fatalf("Expected %d EndpointSlices, got %d", num, len(endpointSlices))
 | |
| 	}
 | |
| 
 | |
| 	if num == 0 {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	for _, epSlice := range endpointSlices {
 | |
| 		if !strings.HasPrefix(epSlice.Name, endpoints.Name) {
 | |
| 			t.Errorf("Expected EndpointSlice name to start with %s, got %s", endpoints.Name, epSlice.Name)
 | |
| 		}
 | |
| 
 | |
| 		serviceNameVal, ok := epSlice.Labels[discovery.LabelServiceName]
 | |
| 		if !ok {
 | |
| 			t.Errorf("Expected EndpointSlice to have %s label set", discovery.LabelServiceName)
 | |
| 		}
 | |
| 		if serviceNameVal != endpoints.Name {
 | |
| 			t.Errorf("Expected EndpointSlice to have %s label set to %s, got %s", discovery.LabelServiceName, endpoints.Name, serviceNameVal)
 | |
| 		}
 | |
| 
 | |
| 		_, ok = epSlice.Annotations[corev1.LastAppliedConfigAnnotation]
 | |
| 		if ok {
 | |
| 			t.Errorf("Expected LastAppliedConfigAnnotation to be unset, got %s", epSlice.Annotations[corev1.LastAppliedConfigAnnotation])
 | |
| 		}
 | |
| 
 | |
| 		_, ok = epSlice.Annotations[corev1.EndpointsLastChangeTriggerTime]
 | |
| 		if ok {
 | |
| 			t.Errorf("Expected EndpointsLastChangeTriggerTime to be unset, got %s", epSlice.Annotations[corev1.EndpointsLastChangeTriggerTime])
 | |
| 		}
 | |
| 
 | |
| 		for annotation, val := range endpoints.Annotations {
 | |
| 			if annotation == corev1.EndpointsLastChangeTriggerTime || annotation == corev1.LastAppliedConfigAnnotation {
 | |
| 				continue
 | |
| 			}
 | |
| 			if epSlice.Annotations[annotation] != val {
 | |
| 				t.Errorf("Expected endpoint annotation %s to be mirrored correctly, got %s", annotation, epSlice.Annotations[annotation])
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, epSubset := range endpoints.Subsets {
 | |
| 		if len(epSubset.Addresses) == 0 && len(epSubset.NotReadyAddresses) == 0 {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		var matchingEndpointsV4, matchingEndpointsV6 []discovery.Endpoint
 | |
| 
 | |
| 		for _, epSlice := range endpointSlices {
 | |
| 			if portsMatch(epSubset.Ports, epSlice.Ports) {
 | |
| 				switch epSlice.AddressType {
 | |
| 				case discovery.AddressTypeIPv4:
 | |
| 					matchingEndpointsV4 = append(matchingEndpointsV4, epSlice.Endpoints...)
 | |
| 				case discovery.AddressTypeIPv6:
 | |
| 					matchingEndpointsV6 = append(matchingEndpointsV6, epSlice.Endpoints...)
 | |
| 				default:
 | |
| 					t.Fatalf("Unexpected EndpointSlice address type found: %v", epSlice.AddressType)
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		if len(matchingEndpointsV4) == 0 && len(matchingEndpointsV6) == 0 {
 | |
| 			t.Fatalf("No EndpointSlices match Endpoints subset: %#v", epSubset.Ports)
 | |
| 		}
 | |
| 
 | |
| 		expectMatchingAddresses(t, epSubset, matchingEndpointsV4, discovery.AddressTypeIPv4, maxEndpointsPerSubset)
 | |
| 		expectMatchingAddresses(t, epSubset, matchingEndpointsV6, discovery.AddressTypeIPv6, maxEndpointsPerSubset)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func portsMatch(epPorts []corev1.EndpointPort, epsPorts []discovery.EndpointPort) bool {
 | |
| 	if len(epPorts) != len(epsPorts) {
 | |
| 		return false
 | |
| 	}
 | |
| 
 | |
| 	portsToBeMatched := map[int32]corev1.EndpointPort{}
 | |
| 
 | |
| 	for _, epPort := range epPorts {
 | |
| 		portsToBeMatched[epPort.Port] = epPort
 | |
| 	}
 | |
| 
 | |
| 	for _, epsPort := range epsPorts {
 | |
| 		epPort, ok := portsToBeMatched[*epsPort.Port]
 | |
| 		if !ok {
 | |
| 			return false
 | |
| 		}
 | |
| 		delete(portsToBeMatched, *epsPort.Port)
 | |
| 
 | |
| 		if epPort.Name != *epsPort.Name {
 | |
| 			return false
 | |
| 		}
 | |
| 		if epPort.Port != *epsPort.Port {
 | |
| 			return false
 | |
| 		}
 | |
| 		if epPort.Protocol != *epsPort.Protocol {
 | |
| 			return false
 | |
| 		}
 | |
| 		if epPort.AppProtocol != epsPort.AppProtocol {
 | |
| 			return false
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| func expectMatchingAddresses(t *testing.T, epSubset corev1.EndpointSubset, esEndpoints []discovery.Endpoint, addrType discovery.AddressType, maxEndpointsPerSubset int) {
 | |
| 	t.Helper()
 | |
| 	type addressInfo struct {
 | |
| 		ready     bool
 | |
| 		epAddress corev1.EndpointAddress
 | |
| 	}
 | |
| 
 | |
| 	// This approach assumes that each IP is unique within an EndpointSubset.
 | |
| 	expectedEndpoints := map[string]addressInfo{}
 | |
| 
 | |
| 	for _, address := range epSubset.Addresses {
 | |
| 		at := getAddressType(address.IP)
 | |
| 		if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset {
 | |
| 			expectedEndpoints[address.IP] = addressInfo{
 | |
| 				ready:     true,
 | |
| 				epAddress: address,
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	for _, address := range epSubset.NotReadyAddresses {
 | |
| 		at := getAddressType(address.IP)
 | |
| 		if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset {
 | |
| 			expectedEndpoints[address.IP] = addressInfo{
 | |
| 				ready:     false,
 | |
| 				epAddress: address,
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(expectedEndpoints) != len(esEndpoints) {
 | |
| 		t.Errorf("Expected %d endpoints, got %d", len(expectedEndpoints), len(esEndpoints))
 | |
| 	}
 | |
| 
 | |
| 	for _, endpoint := range esEndpoints {
 | |
| 		if len(endpoint.Addresses) != 1 {
 | |
| 			t.Fatalf("Expected endpoint to have 1 address, got %d", len(endpoint.Addresses))
 | |
| 		}
 | |
| 		address := endpoint.Addresses[0]
 | |
| 		expectedEndpoint, ok := expectedEndpoints[address]
 | |
| 
 | |
| 		if !ok {
 | |
| 			t.Fatalf("EndpointSlice has endpoint with unexpected address: %s", address)
 | |
| 		}
 | |
| 
 | |
| 		if expectedEndpoint.ready != *endpoint.Conditions.Ready {
 | |
| 			t.Errorf("Expected ready to be %t, got %t", expectedEndpoint.ready, *endpoint.Conditions.Ready)
 | |
| 		}
 | |
| 
 | |
| 		if endpoint.Hostname == nil {
 | |
| 			if expectedEndpoint.epAddress.Hostname != "" {
 | |
| 				t.Errorf("Expected hostname to be %s, got nil", expectedEndpoint.epAddress.Hostname)
 | |
| 			}
 | |
| 		} else if expectedEndpoint.epAddress.Hostname != *endpoint.Hostname {
 | |
| 			t.Errorf("Expected hostname to be %s, got %s", expectedEndpoint.epAddress.Hostname, *endpoint.Hostname)
 | |
| 		}
 | |
| 
 | |
| 		if expectedEndpoint.epAddress.NodeName != nil {
 | |
| 			if endpoint.NodeName == nil {
 | |
| 				t.Errorf("Expected nodeName to be set")
 | |
| 			}
 | |
| 			if *expectedEndpoint.epAddress.NodeName != *endpoint.NodeName {
 | |
| 				t.Errorf("Expected nodeName to be %s, got %s", *expectedEndpoint.epAddress.NodeName, *endpoint.NodeName)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func fetchEndpointSlices(t *testing.T, client *fake.Clientset, namespace string) []discovery.EndpointSlice {
 | |
| 	t.Helper()
 | |
| 	fetchedSlices, err := client.DiscoveryV1().EndpointSlices(namespace).List(context.TODO(), metav1.ListOptions{
 | |
| 		LabelSelector: discovery.LabelManagedBy + "=" + controllerName,
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Expected no error fetching Endpoint Slices, got: %v", err)
 | |
| 		return []discovery.EndpointSlice{}
 | |
| 	}
 | |
| 	return fetchedSlices.Items
 | |
| }
 | |
| 
 | |
| func reconcileHelper(t *testing.T, r *reconciler, endpoints *corev1.Endpoints, existingSlices []*discovery.EndpointSlice) {
 | |
| 	t.Helper()
 | |
| 	err := r.reconcile(endpoints, existingSlices)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Expected no error reconciling Endpoint Slices, got: %v", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Metrics helpers
 | |
| 
 | |
| type expectedMetrics struct {
 | |
| 	desiredSlices    int
 | |
| 	actualSlices     int
 | |
| 	desiredEndpoints int
 | |
| 	addedPerSync     int
 | |
| 	updatedPerSync   int
 | |
| 	removedPerSync   int
 | |
| 	skippedPerSync   int
 | |
| 	numCreated       int
 | |
| 	numUpdated       int
 | |
| 	numDeleted       int
 | |
| }
 | |
| 
 | |
| func expectMetrics(t *testing.T, em expectedMetrics) {
 | |
| 	t.Helper()
 | |
| 
 | |
| 	actualDesiredSlices, err := testutil.GetGaugeMetricValue(metrics.DesiredEndpointSlices.WithLabelValues())
 | |
| 	handleErr(t, err, "desiredEndpointSlices")
 | |
| 	if actualDesiredSlices != float64(em.desiredSlices) {
 | |
| 		t.Errorf("Expected desiredEndpointSlices to be %d, got %v", em.desiredSlices, actualDesiredSlices)
 | |
| 	}
 | |
| 
 | |
| 	actualNumSlices, err := testutil.GetGaugeMetricValue(metrics.NumEndpointSlices.WithLabelValues())
 | |
| 	handleErr(t, err, "numEndpointSlices")
 | |
| 	if actualNumSlices != float64(em.actualSlices) {
 | |
| 		t.Errorf("Expected numEndpointSlices to be %d, got %v", em.actualSlices, actualNumSlices)
 | |
| 	}
 | |
| 
 | |
| 	actualEndpointsDesired, err := testutil.GetGaugeMetricValue(metrics.EndpointsDesired.WithLabelValues())
 | |
| 	handleErr(t, err, "desiredEndpoints")
 | |
| 	if actualEndpointsDesired != float64(em.desiredEndpoints) {
 | |
| 		t.Errorf("Expected desiredEndpoints to be %d, got %v", em.desiredEndpoints, actualEndpointsDesired)
 | |
| 	}
 | |
| 
 | |
| 	actualAddedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsAddedPerSync.WithLabelValues())
 | |
| 	handleErr(t, err, "endpointsAddedPerSync")
 | |
| 	if actualAddedPerSync != float64(em.addedPerSync) {
 | |
| 		t.Errorf("Expected endpointsAddedPerSync to be %d, got %v", em.addedPerSync, actualAddedPerSync)
 | |
| 	}
 | |
| 
 | |
| 	actualUpdatedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsUpdatedPerSync.WithLabelValues())
 | |
| 	handleErr(t, err, "endpointsUpdatedPerSync")
 | |
| 	if actualUpdatedPerSync != float64(em.updatedPerSync) {
 | |
| 		t.Errorf("Expected endpointsUpdatedPerSync to be %d, got %v", em.updatedPerSync, actualUpdatedPerSync)
 | |
| 	}
 | |
| 
 | |
| 	actualRemovedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsRemovedPerSync.WithLabelValues())
 | |
| 	handleErr(t, err, "endpointsRemovedPerSync")
 | |
| 	if actualRemovedPerSync != float64(em.removedPerSync) {
 | |
| 		t.Errorf("Expected endpointsRemovedPerSync to be %d, got %v", em.removedPerSync, actualRemovedPerSync)
 | |
| 	}
 | |
| 
 | |
| 	actualSkippedPerSync, err := testutil.GetHistogramMetricValue(metrics.AddressesSkippedPerSync.WithLabelValues())
 | |
| 	handleErr(t, err, "addressesSkippedPerSync")
 | |
| 	if actualSkippedPerSync != float64(em.skippedPerSync) {
 | |
| 		t.Errorf("Expected addressesSkippedPerSync to be %d, got %v", em.skippedPerSync, actualSkippedPerSync)
 | |
| 	}
 | |
| 
 | |
| 	actualCreated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("create"))
 | |
| 	handleErr(t, err, "endpointSliceChangesCreated")
 | |
| 	if actualCreated != float64(em.numCreated) {
 | |
| 		t.Errorf("Expected endpointSliceChangesCreated to be %d, got %v", em.numCreated, actualCreated)
 | |
| 	}
 | |
| 
 | |
| 	actualUpdated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("update"))
 | |
| 	handleErr(t, err, "endpointSliceChangesUpdated")
 | |
| 	if actualUpdated != float64(em.numUpdated) {
 | |
| 		t.Errorf("Expected endpointSliceChangesUpdated to be %d, got %v", em.numUpdated, actualUpdated)
 | |
| 	}
 | |
| 
 | |
| 	actualDeleted, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("delete"))
 | |
| 	handleErr(t, err, "desiredEndpointSlices")
 | |
| 	if actualDeleted != float64(em.numDeleted) {
 | |
| 		t.Errorf("Expected endpointSliceChangesDeleted to be %d, got %v", em.numDeleted, actualDeleted)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func handleErr(t *testing.T, err error, metricName string) {
 | |
| 	if err != nil {
 | |
| 		t.Errorf("Failed to get %s value, err: %v", metricName, err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func setupMetrics() {
 | |
| 	metrics.RegisterMetrics()
 | |
| 	metrics.NumEndpointSlices.Delete(map[string]string{})
 | |
| 	metrics.DesiredEndpointSlices.Delete(map[string]string{})
 | |
| 	metrics.EndpointsDesired.Delete(map[string]string{})
 | |
| 	metrics.EndpointsAddedPerSync.Delete(map[string]string{})
 | |
| 	metrics.EndpointsUpdatedPerSync.Delete(map[string]string{})
 | |
| 	metrics.EndpointsRemovedPerSync.Delete(map[string]string{})
 | |
| 	metrics.AddressesSkippedPerSync.Delete(map[string]string{})
 | |
| 	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "create"})
 | |
| 	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "update"})
 | |
| 	metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "delete"})
 | |
| }
 |