istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/pod_test.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package controller 16 17 import ( 18 "context" 19 "fmt" 20 "reflect" 21 "testing" 22 "time" 23 24 v1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/types" 27 "k8s.io/client-go/kubernetes" 28 29 "istio.io/istio/pilot/pkg/model" 30 "istio.io/istio/pilot/pkg/serviceregistry/util/xdsfake" 31 "istio.io/istio/pkg/config/labels" 32 "istio.io/istio/pkg/kube/kclient/clienttest" 33 "istio.io/istio/pkg/test" 34 "istio.io/istio/pkg/test/util/assert" 35 "istio.io/istio/pkg/test/util/retry" 36 "istio.io/istio/pkg/util/sets" 37 ) 38 39 // Prepare k8s. This can be used in multiple tests, to 40 // avoid duplicating creation, which can be tricky. It can be used with the fake or 41 // standalone apiserver. 42 func initTestEnv(t *testing.T, ki kubernetes.Interface, fx *xdsfake.Updater) { 43 cleanup(ki) 44 for _, n := range []string{"nsa", "nsb"} { 45 _, err := ki.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ 46 ObjectMeta: metav1.ObjectMeta{ 47 Name: n, 48 Labels: map[string]string{ 49 "istio-injection": "enabled", 50 }, 51 }, 52 }, metav1.CreateOptions{}) 53 if err != nil { 54 t.Fatalf("failed creating test namespace: %v", err) 55 } 56 57 // K8S 1.10 also checks if service account exists 58 _, err = ki.CoreV1().ServiceAccounts(n).Create(context.TODO(), &v1.ServiceAccount{ 59 ObjectMeta: metav1.ObjectMeta{ 60 Name: "default", 61 Annotations: map[string]string{ 62 "kubernetes.io/enforce-mountable-secrets": "false", 63 }, 64 }, 65 Secrets: []v1.ObjectReference{ 66 { 67 Name: "default-token-2", 68 UID: "1", 69 }, 70 }, 71 }, metav1.CreateOptions{}) 72 if err != nil { 73 t.Fatalf("failed creating test service account: %v", err) 74 } 75 76 _, err = ki.CoreV1().Secrets(n).Create(context.TODO(), &v1.Secret{ 77 ObjectMeta: metav1.ObjectMeta{ 78 Name: "default-token-2", 79 Annotations: map[string]string{ 80 "kubernetes.io/service-account.name": "default", 81 "kubernetes.io/service-account.uid": "1", 82 }, 83 }, 84 Type: v1.SecretTypeServiceAccountToken, 85 Data: map[string][]byte{ 86 "token": []byte("1"), 87 }, 88 }, metav1.CreateOptions{}) 89 if err != nil { 90 t.Fatalf("failed creating test secret: %v", err) 91 } 92 } 93 fx.Clear() 94 } 95 96 func cleanup(ki kubernetes.Interface) { 97 for _, n := range []string{"nsa", "nsb"} { 98 n := n 99 pods, err := ki.CoreV1().Pods(n).List(context.TODO(), metav1.ListOptions{}) 100 if err == nil { 101 // Make sure the pods don't exist 102 for _, pod := range pods.Items { 103 _ = ki.CoreV1().Pods(pod.Namespace).Delete(context.TODO(), pod.Name, metav1.DeleteOptions{}) 104 } 105 } 106 } 107 } 108 109 func TestPodLabelUpdate(t *testing.T) { 110 c, fx := NewFakeControllerWithOptions(t, FakeControllerOptions{ 111 WatchedNamespaces: "nsa,nsb", 112 }) 113 114 initTestEnv(t, c.client.Kube(), fx) 115 // Setup a service with 1 pod and endpointslices 116 createServiceWait(c, "ratings", "nsa", 117 nil, nil, []int32{8080}, map[string]string{"app": "test"}, t) 118 pod := generatePod("128.0.0.1", "cpod1", "nsa", "", "", map[string]string{"app": "test", "foo": "bar"}, map[string]string{}) 119 addPods(t, c, fx, pod) 120 createEndpoints(t, c, "rating", "nsa", []string{"tcp-port"}, []string{"128.0.0.1"}, []*v1.ObjectReference{ 121 { 122 Kind: "Pod", 123 Namespace: "nsa", 124 Name: "cpod1", 125 }, 126 }, nil) 127 fx.WaitOrFail(t, "eds") 128 fx.Clear() 129 130 // Verify podCache 131 got := c.pods.getPodsByIP("128.0.0.1") 132 assert.Equal(t, got != nil, true) 133 assert.Equal(t, map[string]string{"app": "test", "foo": "bar"}, got[0].Labels) 134 135 pod.Labels["foo"] = "not-bar" 136 clienttest.Wrap(t, c.podsClient).CreateOrUpdate(pod) 137 fx.StrictMatchOrFail(t, xdsfake.Event{ 138 Type: "proxy", 139 ID: "128.0.0.1", 140 }, 141 xdsfake.Event{ 142 Type: "xds", 143 ID: "ratings.nsa.svc.company.com", 144 }) 145 146 got = c.pods.getPodsByIP("128.0.0.1") 147 assert.Equal(t, got != nil, true) 148 assert.Equal(t, map[string]string{"app": "test", "foo": "not-bar"}, got[0].Labels) 149 } 150 151 func TestHostNetworkPod(t *testing.T) { 152 c, fx := NewFakeControllerWithOptions(t, FakeControllerOptions{}) 153 pods := clienttest.Wrap(t, c.podsClient) 154 events := assert.NewTracker[string](t) 155 c.AppendWorkloadHandler(func(instance *model.WorkloadInstance, event model.Event) { 156 events.Record(fmt.Sprintf("%v/%v", instance.Name, event)) 157 }) 158 initTestEnv(t, c.client.Kube(), fx) 159 createPod := func(ip, name string) { 160 addPods(t, c, fx, generatePod(ip, name, "ns", "1", "", map[string]string{}, map[string]string{})) 161 } 162 163 createPod("128.0.0.1", "pod1") 164 assert.Equal(t, c.pods.getPodKeys("128.0.0.1"), []types.NamespacedName{{Name: "pod1", Namespace: "ns"}}) 165 events.WaitOrdered("pod1/add", "pod1/update") 166 createPod("128.0.0.1", "pod2") 167 events.WaitOrdered("pod2/add", "pod2/update") 168 assert.Equal(t, sets.New(c.pods.getPodKeys("128.0.0.1")...), sets.New( 169 types.NamespacedName{Name: "pod1", Namespace: "ns"}, 170 types.NamespacedName{Name: "pod2", Namespace: "ns"}, 171 )) 172 173 p := c.pods.getPodByKey(types.NamespacedName{Name: "pod1", Namespace: "ns"}) 174 if p == nil || p.Name != "pod1" { 175 t.Fatalf("unexpected pod: %v", p) 176 } 177 pods.Delete("pod1", "ns") 178 pods.Delete("pod2", "ns") 179 events.WaitOrdered("pod1/delete", "pod2/delete") 180 } 181 182 // Regression test for https://github.com/istio/istio/issues/20676 183 func TestIPReuse(t *testing.T) { 184 c, fx := NewFakeControllerWithOptions(t, FakeControllerOptions{}) 185 pods := clienttest.Wrap(t, c.podsClient) 186 initTestEnv(t, c.client.Kube(), fx) 187 188 createPod := func(ip, name string) { 189 addPods(t, c, fx, generatePod(ip, name, "ns", "1", "", map[string]string{}, map[string]string{})) 190 } 191 192 createPod("128.0.0.1", "pod") 193 assert.Equal(t, c.pods.getPodKeys("128.0.0.1"), []types.NamespacedName{{Name: "pod", Namespace: "ns"}}) 194 195 // Change the pod IP. This can happen if the pod moves to another node, for example. 196 createPod("128.0.0.2", "pod") 197 assert.Equal(t, c.pods.getPodKeys("128.0.0.2"), []types.NamespacedName{{Name: "pod", Namespace: "ns"}}) 198 assert.Equal(t, c.pods.getPodKeys("128.0.0.1"), nil) 199 200 // A new pod is created with the old IP. We should get new-pod, not pod 201 createPod("128.0.0.1", "new-pod") 202 assert.Equal(t, c.pods.getPodKeys("128.0.0.1"), []types.NamespacedName{{Name: "new-pod", Namespace: "ns"}}) 203 204 // A new pod is created with the same IP. This happens with hostNetwork, or maybe we miss an update somehow. 205 createPod("128.0.0.1", "another-pod") 206 assert.Equal(t, sets.New(c.pods.getPodKeys("128.0.0.1")...), sets.New( 207 types.NamespacedName{Name: "new-pod", Namespace: "ns"}, 208 types.NamespacedName{Name: "another-pod", Namespace: "ns"}, 209 )) 210 211 fetch := func() sets.Set[types.NamespacedName] { return sets.New(c.pods.getPodKeys("128.0.0.1")...) } 212 pods.Delete("another-pod", "ns") 213 assert.EventuallyEqual(t, fetch, sets.New(types.NamespacedName{Name: "new-pod", Namespace: "ns"})) 214 215 pods.Delete("new-pod", "ns") 216 assert.EventuallyEqual(t, fetch, nil) 217 } 218 219 func waitForPod(t test.Failer, c *FakeController, ip string) { 220 retry.UntilOrFail(t, func() bool { 221 c.pods.RLock() 222 defer c.pods.RUnlock() 223 if _, ok := c.pods.podsByIP[ip]; ok { 224 return true 225 } 226 return false 227 }) 228 } 229 230 func waitForNode(t test.Failer, c *FakeController, name string) { 231 retry.UntilOrFail(t, func() bool { 232 return c.nodes.Get(name, "") != nil 233 }, retry.Timeout(time.Second*1), retry.Delay(time.Millisecond*5)) 234 } 235 236 // Checks that events from the watcher create the proper internal structures 237 func TestPodCacheEvents(t *testing.T) { 238 t.Parallel() 239 c, _ := NewFakeControllerWithOptions(t, FakeControllerOptions{}) 240 241 ns := "default" 242 podCache := c.pods 243 244 handled := 0 245 podCache.c.handlers.AppendWorkloadHandler(func(*model.WorkloadInstance, model.Event) { 246 handled++ 247 }) 248 249 f := podCache.onEvent 250 251 ip := "172.0.3.35" 252 pod1 := metav1.ObjectMeta{Name: "pod1", Namespace: ns} 253 if err := f(nil, &v1.Pod{ObjectMeta: pod1}, model.EventAdd); err != nil { 254 t.Error(err) 255 } 256 257 notReadyCondition := []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionFalse}} 258 readyCondition := []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue}} 259 260 if err := f(nil, 261 &v1.Pod{ObjectMeta: pod1, Status: v1.PodStatus{Conditions: notReadyCondition, PodIP: ip, Phase: v1.PodPending}}, 262 model.EventUpdate); err != nil { 263 t.Error(err) 264 } 265 if handled != 0 { 266 t.Errorf("notified workload handler %d times, want %d", handled, 0) 267 } 268 269 if err := f(nil, &v1.Pod{ObjectMeta: pod1, Status: v1.PodStatus{Conditions: readyCondition, PodIP: ip, Phase: v1.PodPending}}, model.EventUpdate); err != nil { 270 t.Error(err) 271 } 272 if handled != 1 { 273 t.Errorf("notified workload handler %d times, want %d", handled, 1) 274 } 275 assert.Equal(t, c.pods.getPodKeys(ip), []types.NamespacedName{{Name: "pod1", Namespace: "default"}}) 276 277 if err := f(nil, 278 &v1.Pod{ObjectMeta: pod1, Status: v1.PodStatus{Conditions: readyCondition, PodIP: ip, Phase: v1.PodFailed}}, model.EventUpdate); err != nil { 279 t.Error(err) 280 } 281 if handled != 2 { 282 t.Errorf("notified workload handler %d times, want %d", handled, 2) 283 } 284 assert.Equal(t, podCache.getPodKeys(ip), nil) 285 286 pod1.DeletionTimestamp = &metav1.Time{Time: time.Now()} 287 if err := f(nil, &v1.Pod{ObjectMeta: pod1, Status: v1.PodStatus{PodIP: ip, Phase: v1.PodFailed}}, model.EventUpdate); err != nil { 288 t.Error(err) 289 } 290 if handled != 2 { 291 t.Errorf("notified workload handler %d times, want %d", handled, 2) 292 } 293 294 pod2 := metav1.ObjectMeta{Name: "pod2", Namespace: ns} 295 if err := f(nil, &v1.Pod{ObjectMeta: pod2, Status: v1.PodStatus{Conditions: readyCondition, PodIP: ip, Phase: v1.PodRunning}}, model.EventAdd); err != nil { 296 t.Error(err) 297 } 298 if handled != 3 { 299 t.Errorf("notified workload handler %d times, want %d", handled, 3) 300 } 301 assert.Equal(t, sets.New(c.pods.getPodKeys(ip)...), sets.New(types.NamespacedName{Name: "pod2", Namespace: "default"})) 302 303 if err := f(nil, &v1.Pod{ObjectMeta: pod1, Status: v1.PodStatus{PodIP: ip, Phase: v1.PodFailed}}, model.EventDelete); err != nil { 304 t.Error(err) 305 } 306 if handled != 3 { 307 t.Errorf("notified workload handler %d times, want %d", handled, 3) 308 } 309 assert.Equal(t, sets.New(c.pods.getPodKeys(ip)...), sets.New(types.NamespacedName{Name: "pod2", Namespace: "default"})) 310 311 if err := f(nil, &v1.Pod{ObjectMeta: pod2, Spec: v1.PodSpec{ 312 RestartPolicy: v1.RestartPolicyOnFailure, 313 }, Status: v1.PodStatus{Conditions: readyCondition, PodIP: ip, Phase: v1.PodFailed}}, model.EventUpdate); err != nil { 314 t.Error(err) 315 } 316 if handled != 4 { 317 t.Errorf("notified workload handler %d times, want %d", handled, 4) 318 } 319 320 assert.Equal(t, c.pods.getPodsByIP(ip), nil) 321 322 if err := f(nil, &v1.Pod{ObjectMeta: pod2, Status: v1.PodStatus{Conditions: readyCondition, PodIP: ip, Phase: v1.PodFailed}}, model.EventDelete); err != nil { 323 t.Error(err) 324 } 325 if handled != 4 { 326 t.Errorf("notified workload handler %d times, want %d", handled, 5) 327 } 328 } 329 330 func TestPodUpdates(t *testing.T) { 331 c, fx := NewFakeControllerWithOptions(t, FakeControllerOptions{ 332 WatchedNamespaces: "nsa,nsb", 333 }) 334 335 initTestEnv(t, c.client.Kube(), fx) 336 337 // Namespace must be lowercase (nsA doesn't work) 338 pods := []*v1.Pod{ 339 generatePod("128.0.0.1", "cpod1", "nsa", "", "", map[string]string{"app": "test-app"}, map[string]string{}), 340 generatePod("128.0.0.2", "cpod2", "nsa", "", "", map[string]string{"app": "prod-app-1"}, map[string]string{}), 341 generatePod("128.0.0.3", "cpod3", "nsb", "", "", map[string]string{"app": "prod-app-2"}, map[string]string{}), 342 } 343 344 addPods(t, c, fx, pods...) 345 346 // Verify podCache 347 wantLabels := map[string]labels.Instance{ 348 "128.0.0.1": {"app": "test-app"}, 349 "128.0.0.2": {"app": "prod-app-1"}, 350 "128.0.0.3": {"app": "prod-app-2"}, 351 } 352 for addr, wantTag := range wantLabels { 353 pod := c.pods.getPodsByIP(addr) 354 if pod == nil { 355 t.Error("Not found ", addr) 356 continue 357 } 358 if !reflect.DeepEqual(wantTag, labels.Instance(pod[0].Labels)) { 359 t.Errorf("Expected %v got %v", wantTag, labels.Instance(pod[0].Labels)) 360 } 361 } 362 363 // This pod exists, but should not be in the cache because it is in a 364 // namespace not watched by the controller. 365 assert.Equal(t, c.pods.getPodsByIP("128.0.0.4"), nil) 366 367 // This pod should not be in the cache because it never existed. 368 assert.Equal(t, c.pods.getPodsByIP("128.0.0.128"), nil) 369 }