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  }