istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/workload_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 xds_test 16 17 import ( 18 "context" 19 "testing" 20 "time" 21 22 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 23 corev1 "k8s.io/api/core/v1" 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/runtime" 26 27 "istio.io/api/security/v1beta1" 28 metav1beta1 "istio.io/api/type/v1beta1" 29 securityclient "istio.io/client-go/pkg/apis/security/v1beta1" 30 "istio.io/istio/pilot/pkg/features" 31 "istio.io/istio/pilot/pkg/model" 32 v3 "istio.io/istio/pilot/pkg/xds/v3" 33 "istio.io/istio/pilot/test/xds" 34 "istio.io/istio/pkg/config/constants" 35 "istio.io/istio/pkg/kube/kclient/clienttest" 36 "istio.io/istio/pkg/test" 37 "istio.io/istio/pkg/test/util/assert" 38 "istio.io/istio/pkg/util/sets" 39 ) 40 41 func buildExpect(t *testing.T) func(resp *discovery.DeltaDiscoveryResponse, names ...string) { 42 return func(resp *discovery.DeltaDiscoveryResponse, names ...string) { 43 t.Helper() 44 want := sets.New(names...) 45 have := sets.New[string]() 46 for _, r := range resp.Resources { 47 have.Insert(r.Name) 48 } 49 if len(resp.RemovedResources) > 0 { 50 t.Fatalf("unexpected removals: %v", resp.RemovedResources) 51 } 52 assert.Equal(t, sets.SortedList(have), sets.SortedList(want)) 53 } 54 } 55 56 func buildExpectExpectRemoved(t *testing.T) func(resp *discovery.DeltaDiscoveryResponse, names ...string) { 57 return func(resp *discovery.DeltaDiscoveryResponse, names ...string) { 58 t.Helper() 59 want := sets.New(names...) 60 have := sets.New[string]() 61 for _, r := range resp.RemovedResources { 62 have.Insert(r) 63 } 64 if len(resp.Resources) > 0 { 65 t.Fatalf("unexpected resources: %v", resp.Resources) 66 } 67 assert.Equal(t, sets.SortedList(have), sets.SortedList(want)) 68 } 69 } 70 71 func buildExpectAddedAndRemoved(t *testing.T) func(resp *discovery.DeltaDiscoveryResponse, added []string, removed []string) { 72 return func(resp *discovery.DeltaDiscoveryResponse, added []string, removed []string) { 73 t.Helper() 74 wantAdded := sets.New(added...) 75 wantRemoved := sets.New(removed...) 76 have := sets.New[string]() 77 haveRemoved := sets.New[string]() 78 for _, r := range resp.Resources { 79 have.Insert(r.Name) 80 } 81 for _, r := range resp.RemovedResources { 82 haveRemoved.Insert(r) 83 } 84 assert.Equal(t, sets.SortedList(have), sets.SortedList(wantAdded)) 85 assert.Equal(t, sets.SortedList(haveRemoved), sets.SortedList(wantRemoved)) 86 } 87 } 88 89 func TestWorkloadReconnect(t *testing.T) { 90 test.SetForTest(t, &features.EnableAmbient, true) 91 t.Run("ondemand", func(t *testing.T) { 92 expect := buildExpect(t) 93 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 94 KubernetesObjects: []runtime.Object{mkPod("pod", "sa", "127.0.0.1", "not-node")}, 95 }) 96 ads := s.ConnectDeltaADS().WithType(v3.AddressType).WithMetadata(model.NodeMetadata{NodeName: "node"}) 97 ads.Request(&discovery.DeltaDiscoveryRequest{ 98 ResourceNamesSubscribe: []string{"*"}, 99 ResourceNamesUnsubscribe: []string{"*"}, 100 }) 101 ads.ExpectEmptyResponse() 102 103 // Now subscribe to the pod, should get it back 104 resp := ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{ 105 ResourceNamesSubscribe: []string{"/127.0.0.1"}, 106 }) 107 expect(resp, "Kubernetes//Pod/default/pod") 108 ads.Cleanup() 109 110 // Create new pod in the meantime 111 createPod(s, "pod2", "sa", "127.0.0.2", "node") 112 113 // Reconnect 114 ads = s.ConnectDeltaADS().WithType(v3.AddressType).WithMetadata(model.NodeMetadata{NodeName: "node"}) 115 ads.Request(&discovery.DeltaDiscoveryRequest{ 116 ResourceNamesSubscribe: []string{"*"}, 117 ResourceNamesUnsubscribe: []string{"*"}, 118 InitialResourceVersions: map[string]string{ 119 "/127.0.0.1": "", 120 }, 121 }) 122 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod", "Kubernetes//Pod/default/pod2") 123 }) 124 t.Run("wildcard", func(t *testing.T) { 125 expect := buildExpect(t) 126 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 127 KubernetesObjects: []runtime.Object{mkPod("pod", "sa", "127.0.0.1", "not-node")}, 128 }) 129 ads := s.ConnectDeltaADS().WithType(v3.AddressType).WithMetadata(model.NodeMetadata{NodeName: "node"}) 130 131 // Subscribe to everything, expect to get the pod back 132 resp := ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{ 133 ResourceNamesSubscribe: []string{}, 134 ResourceNamesUnsubscribe: []string{}, 135 }) 136 expect(resp, "Kubernetes//Pod/default/pod") 137 // Close the connection 138 ads.Cleanup() 139 140 // Create new pod in the meantime 141 createPod(s, "pod2", "sa", "127.0.0.2", "node") 142 143 // Reconnect 144 ads = s.ConnectDeltaADS().WithType(v3.AddressType).WithMetadata(model.NodeMetadata{NodeName: "node"}) 145 ads.Request(&discovery.DeltaDiscoveryRequest{ 146 ResourceNamesSubscribe: []string{}, 147 ResourceNamesUnsubscribe: []string{}, 148 InitialResourceVersions: map[string]string{ 149 "/127.0.0.1": "", 150 }, 151 }) 152 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod", "Kubernetes//Pod/default/pod2") 153 }) 154 } 155 156 func TestWorkload(t *testing.T) { 157 test.SetForTest(t, &features.EnableAmbient, true) 158 t.Run("ondemand", func(t *testing.T) { 159 expect := buildExpect(t) 160 expectRemoved := buildExpectExpectRemoved(t) 161 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 162 DebounceTime: time.Millisecond * 25, 163 }) 164 ads := s.ConnectDeltaADS().WithTimeout(time.Second * 5).WithType(v3.AddressType).WithMetadata(model.NodeMetadata{NodeName: "node"}) 165 166 ads.Request(&discovery.DeltaDiscoveryRequest{ 167 ResourceNamesSubscribe: []string{"*"}, 168 ResourceNamesUnsubscribe: []string{"*"}, 169 }) 170 ads.ExpectEmptyResponse() 171 172 // Create pod we are not subscribe to; should be a NOP 173 createPod(s, "pod", "sa", "127.0.0.1", "not-node") 174 ads.ExpectNoResponse() 175 176 // Now subscribe to it, should get it back 177 resp := ads.RequestResponseAck(&discovery.DeltaDiscoveryRequest{ 178 ResourceNamesSubscribe: []string{"/127.0.0.1"}, 179 }) 180 expect(resp, "Kubernetes//Pod/default/pod") 181 182 // Subscribe to unknown pod 183 ads.Request(&discovery.DeltaDiscoveryRequest{ 184 ResourceNamesSubscribe: []string{"/127.0.0.2"}, 185 }) 186 // "Removed" is a misnomer, but per the spec this is how we report "not found" 187 expectRemoved(ads.ExpectResponse(), "/127.0.0.2") 188 189 // Once we create it, we should get a push 190 createPod(s, "pod2", "sa", "127.0.0.2", "node") 191 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod2") 192 193 // TODO: implement pod update; this actually cannot really be done without waypoints or VIPs 194 deletePod(s, "pod") 195 expectRemoved(ads.ExpectResponse(), "Kubernetes//Pod/default/pod") 196 197 // Create pod we are not subscribed to; due to same-node optimization this will push 198 createPod(s, "pod-same-node", "sa", "127.0.0.3", "node") 199 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod-same-node") 200 201 deletePod(s, "pod-same-node") 202 expectRemoved(ads.ExpectResponse(), "Kubernetes//Pod/default/pod-same-node") 203 204 // Add service: we should not get any new resources, but updates to existing ones 205 // Note: we are not subscribed to svc1 explicitly, but it impacts pods we are subscribed to 206 createService(s, "svc1", "default", map[string]string{"app": "sa"}) 207 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod2") 208 // Creating a pod in the service should send an update as usual 209 createPod(s, "pod", "sa", "127.0.0.1", "node") 210 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod") 211 212 // Make service not select workload should also update things 213 createService(s, "svc1", "default", map[string]string{"app": "not-sa"}) 214 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod", "Kubernetes//Pod/default/pod2") 215 216 // Now create pods in the service... 217 createPod(s, "pod4", "not-sa", "127.0.0.4", "not-node") 218 // Not subscribed, no response 219 ads.ExpectNoResponse() 220 221 // Now we subscribe to the service explicitly 222 ads.Request(&discovery.DeltaDiscoveryRequest{ 223 ResourceNamesSubscribe: []string{"/10.0.0.1"}, 224 }) 225 // Should get updates for all pods in the service 226 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod4", "default/svc1.default.svc.cluster.local") 227 // Adding a pod in the service should not trigger an update for that pod - we didn't explicitly subscribe 228 createPod(s, "pod5", "not-sa", "127.0.0.5", "not-node") 229 ads.ExpectNoResponse() 230 231 // And if the service changes to no longer select them, we should see them *removed* (not updated) 232 createService(s, "svc1", "default", map[string]string{"app": "nothing"}) 233 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod4") 234 }) 235 t.Run("wildcard", func(t *testing.T) { 236 expect := buildExpect(t) 237 expectRemoved := buildExpectExpectRemoved(t) 238 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 239 DebounceTime: time.Millisecond * 25, 240 }) 241 ads := s.ConnectDeltaADS().WithTimeout(time.Second * 5).WithType(v3.AddressType).WithMetadata(model.NodeMetadata{NodeName: "node"}) 242 243 ads.Request(&discovery.DeltaDiscoveryRequest{ 244 ResourceNamesSubscribe: []string{"*"}, 245 }) 246 ads.ExpectEmptyResponse() 247 248 // Create pod, due to wildcard subscribe we should receive it 249 createPod(s, "pod", "sa", "127.0.0.1", "not-node") 250 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod") 251 252 // A new pod should push only that one 253 createPod(s, "pod2", "sa", "127.0.0.2", "node") 254 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod2") 255 256 // TODO: implement pod update; this actually cannot really be done without waypoints or VIPs 257 deletePod(s, "pod") 258 expectRemoved(ads.ExpectResponse(), "Kubernetes//Pod/default/pod") 259 260 // Add service: we should not get any new resources, but updates to existing ones 261 createService(s, "svc1", "default", map[string]string{"app": "sa"}) 262 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod2", "default/svc1.default.svc.cluster.local") 263 // Creating a pod in the service should send an update as usual 264 createPod(s, "pod", "sa", "127.0.0.3", "node") 265 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod") 266 267 // Make service not select workload should also update things 268 createService(s, "svc1", "default", map[string]string{"app": "not-sa"}) 269 expect(ads.ExpectResponse(), "Kubernetes//Pod/default/pod", "Kubernetes//Pod/default/pod2") 270 }) 271 } 272 273 func deletePod(s *xds.FakeDiscoveryServer, name string) { 274 err := s.KubeClient().Kube().CoreV1().Pods("default").Delete(context.Background(), name, metav1.DeleteOptions{}) 275 if err != nil { 276 s.T().Fatal(err) 277 } 278 } 279 280 func createAuthorizationPolicy(s *xds.FakeDiscoveryServer, name string, ns string) { 281 clienttest.NewWriter[*securityclient.AuthorizationPolicy](s.T(), s.KubeClient()).Create(&securityclient.AuthorizationPolicy{ 282 ObjectMeta: metav1.ObjectMeta{ 283 Name: name, 284 Namespace: ns, 285 }, 286 Spec: v1beta1.AuthorizationPolicy{}, 287 }) 288 } 289 290 func deletePeerAuthentication(s *xds.FakeDiscoveryServer, name string, ns string) { 291 clienttest.NewWriter[*securityclient.PeerAuthentication](s.T(), s.KubeClient()).Delete(name, ns) 292 } 293 294 func createPeerAuthentication(s *xds.FakeDiscoveryServer, name string, ns string, spec *v1beta1.PeerAuthentication) { 295 c := &securityclient.PeerAuthentication{ 296 ObjectMeta: metav1.ObjectMeta{ 297 Name: name, 298 Namespace: ns, 299 }, 300 Spec: *spec, //nolint: govet 301 } 302 clienttest.NewWriter[*securityclient.PeerAuthentication](s.T(), s.KubeClient()).CreateOrUpdate(c) 303 } 304 305 func deleteRBAC(s *xds.FakeDiscoveryServer, name string, ns string) { 306 clienttest.NewWriter[*securityclient.AuthorizationPolicy](s.T(), s.KubeClient()).Delete(name, ns) 307 } 308 309 func mkPod(name string, sa string, ip string, node string) *corev1.Pod { 310 return &corev1.Pod{ 311 ObjectMeta: metav1.ObjectMeta{ 312 Name: name, 313 Namespace: "default", 314 Annotations: map[string]string{ 315 constants.AmbientRedirection: constants.AmbientRedirectionEnabled, 316 }, 317 Labels: map[string]string{ 318 "app": sa, 319 }, 320 }, 321 Spec: corev1.PodSpec{ 322 ServiceAccountName: sa, 323 NodeName: node, 324 }, 325 Status: corev1.PodStatus{ 326 PodIP: ip, 327 PodIPs: []corev1.PodIP{ 328 { 329 IP: ip, 330 }, 331 }, 332 Phase: corev1.PodRunning, 333 Conditions: []corev1.PodCondition{ 334 { 335 Type: corev1.PodReady, 336 Status: corev1.ConditionTrue, 337 LastTransitionTime: metav1.Now(), 338 }, 339 }, 340 }, 341 } 342 } 343 344 func createPod(s *xds.FakeDiscoveryServer, name string, sa string, ip string, node string) { 345 pod := mkPod(name, sa, ip, node) 346 pods := clienttest.NewWriter[*corev1.Pod](s.T(), s.KubeClient()) 347 pods.CreateOrUpdate(pod) 348 pods.UpdateStatus(pod) 349 } 350 351 // nolint: unparam 352 func createService(s *xds.FakeDiscoveryServer, name, namespace string, selector map[string]string) { 353 service := &corev1.Service{ 354 ObjectMeta: metav1.ObjectMeta{ 355 Name: name, 356 Namespace: namespace, 357 }, 358 Spec: corev1.ServiceSpec{ 359 ClusterIP: "10.0.0.1", 360 Ports: []corev1.ServicePort{{ 361 Name: "tcp", 362 Port: 80, 363 Protocol: "TCP", 364 }}, 365 Selector: selector, 366 Type: corev1.ServiceTypeClusterIP, 367 }, 368 } 369 370 svcs := clienttest.NewWriter[*corev1.Service](s.T(), s.KubeClient()) 371 svcs.CreateOrUpdate(service) 372 } 373 374 func TestWorkloadAuthorizationPolicy(t *testing.T) { 375 test.SetForTest(t, &features.EnableAmbient, true) 376 expect := buildExpect(t) 377 expectRemoved := buildExpectExpectRemoved(t) 378 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{}) 379 ads := s.ConnectDeltaADS().WithType(v3.WorkloadAuthorizationType).WithTimeout(time.Second * 10).WithNodeType(model.Ztunnel) 380 381 ads.Request(&discovery.DeltaDiscoveryRequest{ 382 ResourceNamesSubscribe: []string{"*"}, 383 }) 384 ads.ExpectEmptyResponse() 385 386 // Create policy, due to wildcard subscribe we should receive it 387 createAuthorizationPolicy(s, "policy1", "ns") 388 expect(ads.ExpectResponse(), "ns/policy1") 389 390 // A new policy should push only that one 391 createAuthorizationPolicy(s, "policy2", "ns") 392 expect(ads.ExpectResponse(), "ns/policy2") 393 394 deleteRBAC(s, "policy2", "ns") 395 expectRemoved(ads.ExpectResponse(), "ns/policy2") 396 397 // Irrelevant update shouldn't push 398 createPod(s, "pod", "sa", "127.0.0.1", "node") 399 ads.ExpectNoResponse() 400 } 401 402 func TestWorkloadPeerAuthentication(t *testing.T) { 403 test.SetForTest(t, &features.EnableAmbient, true) 404 expect := buildExpect(t) 405 expectAddedAndRemoved := buildExpectAddedAndRemoved(t) 406 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{}) 407 ads := s.ConnectDeltaADS().WithType(v3.WorkloadAuthorizationType).WithTimeout(time.Second * 10).WithNodeType(model.Ztunnel) 408 409 ads.Request(&discovery.DeltaDiscoveryRequest{ 410 ResourceNamesSubscribe: []string{"*"}, 411 }) 412 ads.ExpectEmptyResponse() 413 414 // Create policy; it should push only the static strict policy 415 // We expect a removal because the policy exists in the cluster but is not sent to the proxy (because it's not port-specific, STRICT, etc.) 416 createPeerAuthentication(s, "policy1", "ns", &v1beta1.PeerAuthentication{}) 417 expectAddedAndRemoved(ads.ExpectResponse(), []string{"istio-system/istio_converted_static_strict"}, nil) 418 419 createPeerAuthentication(s, "policy2", "ns", &v1beta1.PeerAuthentication{ 420 Mtls: &v1beta1.PeerAuthentication_MutualTLS{ 421 Mode: v1beta1.PeerAuthentication_MutualTLS_PERMISSIVE, 422 }, 423 PortLevelMtls: map[uint32]*v1beta1.PeerAuthentication_MutualTLS{ 424 9080: { 425 Mode: v1beta1.PeerAuthentication_MutualTLS_STRICT, 426 }, 427 }, 428 Selector: &metav1beta1.WorkloadSelector{ 429 MatchLabels: map[string]string{ 430 "app": "sa", // This patches the pod we will create 431 }, 432 }, 433 }) 434 expect(ads.ExpectResponse(), "ns/converted_peer_authentication_policy2") 435 436 // We expect a removal because the policy was deleted 437 // Note that policy1 was not removed because its config was not updated (i.e. this is a partial push) 438 deletePeerAuthentication(s, "policy2", "ns") 439 expectAddedAndRemoved(ads.ExpectResponse(), nil, []string{"ns/converted_peer_authentication_policy2"}) 440 441 // Irrelevant update (pod is in the default namespace and not "ns") shouldn't push 442 createPod(s, "pod", "sa", "127.0.0.1", "node") 443 ads.ExpectNoResponse() 444 }