k8s.io/kubernetes@v1.29.3/test/integration/endpointslice/endpointsliceterminating_test.go (about) 1 /* 2 Copyright 2021 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 endpointslice 18 19 import ( 20 "context" 21 "reflect" 22 "testing" 23 "time" 24 25 corev1 "k8s.io/api/core/v1" 26 discovery "k8s.io/api/discovery/v1" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/util/intstr" 29 "k8s.io/apimachinery/pkg/util/wait" 30 "k8s.io/client-go/informers" 31 clientset "k8s.io/client-go/kubernetes" 32 "k8s.io/klog/v2/ktesting" 33 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" 34 "k8s.io/kubernetes/pkg/controller/endpointslice" 35 "k8s.io/kubernetes/test/integration/framework" 36 utilpointer "k8s.io/utils/pointer" 37 ) 38 39 // TestEndpointSliceTerminating tests that terminating endpoints are included with the 40 // correct conditions set for ready, serving and terminating. 41 func TestEndpointSliceTerminating(t *testing.T) { 42 testcases := []struct { 43 name string 44 podStatus corev1.PodStatus 45 expectedEndpoints []discovery.Endpoint 46 }{ 47 { 48 name: "ready terminating pods", 49 podStatus: corev1.PodStatus{ 50 Phase: corev1.PodRunning, 51 Conditions: []corev1.PodCondition{ 52 { 53 Type: corev1.PodReady, 54 Status: corev1.ConditionTrue, 55 }, 56 }, 57 PodIP: "10.0.0.1", 58 PodIPs: []corev1.PodIP{ 59 { 60 IP: "10.0.0.1", 61 }, 62 }, 63 }, 64 expectedEndpoints: []discovery.Endpoint{ 65 { 66 Addresses: []string{"10.0.0.1"}, 67 Conditions: discovery.EndpointConditions{ 68 Ready: utilpointer.BoolPtr(false), 69 Serving: utilpointer.BoolPtr(true), 70 Terminating: utilpointer.BoolPtr(true), 71 }, 72 }, 73 }, 74 }, 75 { 76 name: "not ready terminating pods", 77 podStatus: corev1.PodStatus{ 78 Phase: corev1.PodRunning, 79 Conditions: []corev1.PodCondition{ 80 { 81 Type: corev1.PodReady, 82 Status: corev1.ConditionFalse, 83 }, 84 }, 85 PodIP: "10.0.0.1", 86 PodIPs: []corev1.PodIP{ 87 { 88 IP: "10.0.0.1", 89 }, 90 }, 91 }, 92 expectedEndpoints: []discovery.Endpoint{ 93 { 94 Addresses: []string{"10.0.0.1"}, 95 Conditions: discovery.EndpointConditions{ 96 Ready: utilpointer.BoolPtr(false), 97 Serving: utilpointer.BoolPtr(false), 98 Terminating: utilpointer.BoolPtr(true), 99 }, 100 }, 101 }, 102 }, 103 } 104 105 for _, testcase := range testcases { 106 t.Run(testcase.name, func(t *testing.T) { 107 // Disable ServiceAccount admission plugin as we don't have serviceaccount controller running. 108 server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{"--disable-admission-plugins=ServiceAccount"}, framework.SharedEtcd()) 109 defer server.TearDownFn() 110 111 client, err := clientset.NewForConfig(server.ClientConfig) 112 if err != nil { 113 t.Fatalf("Error creating clientset: %v", err) 114 } 115 116 resyncPeriod := 12 * time.Hour 117 informers := informers.NewSharedInformerFactory(client, resyncPeriod) 118 119 _, ctx := ktesting.NewTestContext(t) 120 epsController := endpointslice.NewController( 121 ctx, 122 informers.Core().V1().Pods(), 123 informers.Core().V1().Services(), 124 informers.Core().V1().Nodes(), 125 informers.Discovery().V1().EndpointSlices(), 126 int32(100), 127 client, 128 1*time.Second) 129 130 // Start informer and controllers 131 ctx, cancel := context.WithCancel(ctx) 132 defer cancel() 133 informers.Start(ctx.Done()) 134 go epsController.Run(ctx, 1) 135 136 // Create namespace 137 ns := framework.CreateNamespaceOrDie(client, "test-endpoints-terminating", t) 138 defer framework.DeleteNamespaceOrDie(client, ns, t) 139 140 node := &corev1.Node{ 141 ObjectMeta: metav1.ObjectMeta{ 142 Name: "fake-node", 143 }, 144 } 145 146 _, err = client.CoreV1().Nodes().Create(context.TODO(), node, metav1.CreateOptions{}) 147 if err != nil { 148 t.Fatalf("Failed to create test node: %v", err) 149 } 150 151 svc := &corev1.Service{ 152 ObjectMeta: metav1.ObjectMeta{ 153 Name: "test-service", 154 Namespace: ns.Name, 155 Labels: map[string]string{ 156 "foo": "bar", 157 }, 158 }, 159 Spec: corev1.ServiceSpec{ 160 Selector: map[string]string{ 161 "foo": "bar", 162 }, 163 Ports: []corev1.ServicePort{ 164 {Name: "port-443", Port: 443, Protocol: "TCP", TargetPort: intstr.FromInt32(443)}, 165 }, 166 }, 167 } 168 169 _, err = client.CoreV1().Services(ns.Name).Create(context.TODO(), svc, metav1.CreateOptions{}) 170 if err != nil { 171 t.Fatalf("Failed to create test Service: %v", err) 172 } 173 174 pod := &corev1.Pod{ 175 ObjectMeta: metav1.ObjectMeta{ 176 Name: "test-pod", 177 Labels: map[string]string{ 178 "foo": "bar", 179 }, 180 }, 181 Spec: corev1.PodSpec{ 182 NodeName: "fake-node", 183 Containers: []corev1.Container{ 184 { 185 Name: "fakename", 186 Image: "fakeimage", 187 Ports: []corev1.ContainerPort{ 188 { 189 Name: "port-443", 190 ContainerPort: 443, 191 }, 192 }, 193 }, 194 }, 195 }, 196 } 197 198 pod, err = client.CoreV1().Pods(ns.Name).Create(context.TODO(), pod, metav1.CreateOptions{}) 199 if err != nil { 200 t.Fatalf("Failed to create test ready pod: %v", err) 201 } 202 203 pod.Status = testcase.podStatus 204 _, err = client.CoreV1().Pods(ns.Name).UpdateStatus(context.TODO(), pod, metav1.UpdateOptions{}) 205 if err != nil { 206 t.Fatalf("Failed to update status for test ready pod: %v", err) 207 } 208 209 // first check that endpoints are included, test should always have 1 initial endpoint 210 err = wait.PollImmediate(1*time.Second, 10*time.Second, func() (bool, error) { 211 esList, err := client.DiscoveryV1().EndpointSlices(ns.Name).List(context.TODO(), metav1.ListOptions{ 212 LabelSelector: discovery.LabelServiceName + "=" + svc.Name, 213 }) 214 215 if err != nil { 216 return false, err 217 } 218 219 if len(esList.Items) == 0 { 220 return false, nil 221 } 222 223 numEndpoints := 0 224 for _, slice := range esList.Items { 225 numEndpoints += len(slice.Endpoints) 226 } 227 228 if numEndpoints > 0 { 229 return true, nil 230 } 231 232 return false, nil 233 }) 234 if err != nil { 235 t.Errorf("Error waiting for endpoint slices: %v", err) 236 } 237 238 // Delete pod and check endpoints slice conditions 239 err = client.CoreV1().Pods(ns.Name).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{}) 240 if err != nil { 241 t.Fatalf("Failed to delete pod in terminating state: %v", err) 242 } 243 244 // Validate that terminating the endpoint will result in the expected endpoints in EndpointSlice. 245 // Use a stricter timeout value here since we should try to catch regressions in the time it takes to remove terminated endpoints. 246 var endpoints []discovery.Endpoint 247 err = wait.PollImmediate(1*time.Second, 10*time.Second, func() (bool, error) { 248 esList, err := client.DiscoveryV1().EndpointSlices(ns.Name).List(context.TODO(), metav1.ListOptions{ 249 LabelSelector: discovery.LabelServiceName + "=" + svc.Name, 250 }) 251 252 if err != nil { 253 return false, err 254 } 255 256 if len(esList.Items) == 0 { 257 return false, nil 258 } 259 260 endpoints = esList.Items[0].Endpoints 261 if len(endpoints) == 0 && len(testcase.expectedEndpoints) == 0 { 262 return true, nil 263 } 264 265 if len(endpoints) != len(testcase.expectedEndpoints) { 266 return false, nil 267 } 268 269 if !reflect.DeepEqual(endpoints[0].Addresses, testcase.expectedEndpoints[0].Addresses) { 270 return false, nil 271 } 272 273 if !reflect.DeepEqual(endpoints[0].Conditions, testcase.expectedEndpoints[0].Conditions) { 274 return false, nil 275 } 276 277 return true, nil 278 }) 279 if err != nil { 280 t.Logf("actual endpoints: %v", endpoints) 281 t.Logf("expected endpoints: %v", testcase.expectedEndpoints) 282 t.Errorf("unexpected endpoints: %v", err) 283 } 284 }) 285 } 286 }