istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/cds_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  package xds_test
    15  
    16  import (
    17  	"testing"
    18  
    19  	cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
    20  	tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
    21  	v1 "k8s.io/api/core/v1"
    22  	discoveryv1 "k8s.io/api/discovery/v1"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  	"k8s.io/apimachinery/pkg/runtime"
    25  
    26  	networking "istio.io/api/networking/v1alpha3"
    27  	"istio.io/istio/pilot/pkg/model"
    28  	v3 "istio.io/istio/pilot/pkg/xds/v3"
    29  	"istio.io/istio/pilot/test/xds"
    30  	"istio.io/istio/pilot/test/xdstest"
    31  	"istio.io/istio/pkg/config"
    32  	"istio.io/istio/pkg/config/schema/gvk"
    33  	"istio.io/istio/pkg/ptr"
    34  	"istio.io/istio/pkg/slices"
    35  	"istio.io/istio/pkg/test/util/assert"
    36  	"istio.io/istio/pkg/util/sets"
    37  )
    38  
    39  func TestCDS(t *testing.T) {
    40  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{})
    41  	ads := s.ConnectADS().WithType(v3.ClusterType)
    42  	ads.RequestResponseAck(t, nil)
    43  }
    44  
    45  func TestSAN(t *testing.T) {
    46  	labels := map[string]string{"app": "test"}
    47  	pod := &v1.Pod{
    48  		ObjectMeta: metav1.ObjectMeta{
    49  			Name:      "pod",
    50  			Namespace: "default",
    51  			Labels:    labels,
    52  		},
    53  		Spec: v1.PodSpec{ServiceAccountName: "pod"},
    54  		Status: v1.PodStatus{
    55  			PodIP: "1.2.3.4",
    56  			Phase: v1.PodPending,
    57  		},
    58  	}
    59  	service := &v1.Service{
    60  		ObjectMeta: metav1.ObjectMeta{
    61  			Name:      "example",
    62  			Namespace: "default",
    63  		},
    64  		Spec: v1.ServiceSpec{
    65  			Ports: []v1.ServicePort{{
    66  				Name: "http",
    67  				Port: 80,
    68  			}},
    69  			Selector:  labels,
    70  			ClusterIP: "9.9.9.9",
    71  		},
    72  	}
    73  	endpoint := &discoveryv1.EndpointSlice{
    74  		ObjectMeta: metav1.ObjectMeta{
    75  			Name:      service.Name,
    76  			Namespace: service.Namespace,
    77  			Labels: map[string]string{
    78  				discoveryv1.LabelServiceName: service.Name,
    79  			},
    80  		},
    81  		Endpoints: []discoveryv1.Endpoint{{
    82  			Addresses: []string{pod.Status.PodIP},
    83  			TargetRef: &v1.ObjectReference{
    84  				Kind:      "Pod",
    85  				Namespace: pod.Namespace,
    86  				Name:      pod.Name,
    87  			},
    88  		}},
    89  		Ports: []discoveryv1.EndpointPort{{Name: ptr.Of("http"), Port: ptr.Of(int32(80))}},
    90  	}
    91  	dr := config.Config{
    92  		Meta: config.Meta{
    93  			GroupVersionKind: gvk.DestinationRule,
    94  			Name:             "dr",
    95  			Namespace:        "test",
    96  		},
    97  		Spec: &networking.DestinationRule{
    98  			Host: "example.default.svc.cluster.local",
    99  			TrafficPolicy: &networking.TrafficPolicy{Tls: &networking.ClientTLSSettings{
   100  				Mode:              networking.ClientTLSSettings_MUTUAL,
   101  				ClientCertificate: "fake",
   102  				PrivateKey:        "fake",
   103  				CaCertificates:    "fake",
   104  			}},
   105  		},
   106  	}
   107  	drIstioMTLS := config.Config{
   108  		Meta: config.Meta{
   109  			GroupVersionKind: gvk.DestinationRule,
   110  			Name:             "dr",
   111  			Namespace:        "test",
   112  		},
   113  		Spec: &networking.DestinationRule{
   114  			Host: "example.default.svc.cluster.local",
   115  			TrafficPolicy: &networking.TrafficPolicy{Tls: &networking.ClientTLSSettings{
   116  				Mode:              networking.ClientTLSSettings_ISTIO_MUTUAL,
   117  				ClientCertificate: "fake",
   118  				PrivateKey:        "fake",
   119  				CaCertificates:    "fake",
   120  			}},
   121  		},
   122  	}
   123  	seEDS := config.Config{
   124  		Meta: config.Meta{
   125  			Name:             "service-entry",
   126  			Namespace:        "test",
   127  			GroupVersionKind: gvk.ServiceEntry,
   128  			Domain:           "cluster.local",
   129  		},
   130  		Spec: &networking.ServiceEntry{
   131  			Hosts: []string{"example.default.svc.cluster.local"},
   132  			Ports: []*networking.ServicePort{{
   133  				Number:   80,
   134  				Protocol: "HTTP",
   135  				Name:     "http",
   136  			}},
   137  			SubjectAltNames: []string{"se-top"},
   138  			Resolution:      networking.ServiceEntry_STATIC,
   139  			Endpoints: []*networking.WorkloadEntry{{
   140  				Address:        "1.1.1.1",
   141  				ServiceAccount: "se-endpoint",
   142  			}},
   143  		},
   144  	}
   145  	seNONE := config.Config{
   146  		Meta: config.Meta{
   147  			Name:             "service-entry",
   148  			Namespace:        "test",
   149  			GroupVersionKind: gvk.ServiceEntry,
   150  			Domain:           "cluster.local",
   151  		},
   152  		Spec: &networking.ServiceEntry{
   153  			Hosts: []string{"example.default.svc.cluster.local"},
   154  			Ports: []*networking.ServicePort{{
   155  				Number:   80,
   156  				Protocol: "HTTP",
   157  				Name:     "http",
   158  			}},
   159  			SubjectAltNames: []string{"custom"},
   160  			Resolution:      networking.ServiceEntry_NONE,
   161  		},
   162  	}
   163  	cases := []struct {
   164  		name    string
   165  		objs    []runtime.Object
   166  		configs []config.Config
   167  		sans    []string
   168  	}{
   169  		{
   170  			name:    "Kubernetes service and EDS ServiceEntry",
   171  			objs:    []runtime.Object{service, pod, endpoint},
   172  			configs: []config.Config{dr, seEDS},
   173  			// The ServiceEntry rule will "win" the pushContext.ServiceAccounts.
   174  			// However, the Service will be processed first into a cluster. Since its not external, we do not add the SANs automatically
   175  			sans: nil,
   176  		},
   177  		{
   178  			name:    "Kubernetes service and NONE ServiceEntry",
   179  			objs:    []runtime.Object{service, pod, endpoint},
   180  			configs: []config.Config{dr, seNONE},
   181  			// Service properly sets SAN. Since it's not external, we do not add the SANs automatically though
   182  			sans: nil,
   183  		},
   184  		{
   185  			name:    "Kubernetes service and EDS ServiceEntry ISTIO_MUTUAL",
   186  			objs:    []runtime.Object{service, pod, endpoint},
   187  			configs: []config.Config{drIstioMTLS, seEDS},
   188  			// The Service has precedence, so its cluster will be used
   189  			sans: []string{"spiffe://cluster.local/ns/default/sa/pod"},
   190  		},
   191  		{
   192  			name:    "Kubernetes service and NONE ServiceEntry ISTIO_MUTUAL",
   193  			objs:    []runtime.Object{service, pod, endpoint},
   194  			configs: []config.Config{drIstioMTLS, seNONE},
   195  			// The Service has precedence, so its cluster will be used
   196  			sans: []string{"spiffe://cluster.local/ns/default/sa/pod"},
   197  		},
   198  		{
   199  			name:    "NONE ServiceEntry ISTIO_MUTUAL",
   200  			configs: []config.Config{drIstioMTLS, seNONE},
   201  			// Totally broken; service level ServiceAccount are ignored.
   202  			sans: []string{"custom"},
   203  		},
   204  	}
   205  	for _, tt := range cases {
   206  		t.Run(tt.name, func(t *testing.T) {
   207  			s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{
   208  				Configs:           tt.configs,
   209  				KubernetesObjects: tt.objs,
   210  			})
   211  
   212  			assertSANs := func(t *testing.T, clusters []*cluster.Cluster, c string, sans []string) {
   213  				t.Helper()
   214  				cluster := xdstest.ExtractClusters(clusters)[c]
   215  				if cluster == nil {
   216  					t.Fatal("cluster not found")
   217  				}
   218  				cluster.GetTransportSocket().GetTypedConfig()
   219  				tl := xdstest.UnmarshalAny[tls.UpstreamTlsContext](t, cluster.GetTransportSocket().GetTypedConfig())
   220  				names := sets.New[string]()
   221  				// nolint: staticcheck
   222  				for _, n := range tl.GetCommonTlsContext().GetCombinedValidationContext().GetDefaultValidationContext().GetMatchSubjectAltNames() {
   223  					names.Insert(n.GetExact())
   224  				}
   225  				assert.Equal(t, sets.SortedList(names), sets.SortedList(sets.New(sans...)))
   226  			}
   227  			// Run multiple assertions to verify idempotency; previous versions had issues here.
   228  			for i := 0; i < 2; i++ {
   229  				clusters := s.Clusters(s.SetupProxy(&model.Proxy{ConfigNamespace: "test"}))
   230  				assertSANs(t, clusters, "outbound|80||example.default.svc.cluster.local", tt.sans)
   231  				t.Logf("iteration %d passed", i)
   232  			}
   233  		})
   234  	}
   235  }
   236  
   237  func TestServiceEntryMerge(t *testing.T) {
   238  	// Regression test for https://github.com/istio/istio/issues/50478
   239  	s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ConfigString: `apiVersion: networking.istio.io/v1beta1
   240  kind: ServiceEntry
   241  metadata:
   242    name: se1
   243  spec:
   244    hosts:
   245    - example.com
   246    ports:
   247    - name: port1
   248      number: 80
   249      protocol: HTTP
   250    resolution: DNS
   251  ---
   252  apiVersion: networking.istio.io/v1beta1
   253  kind: ServiceEntry
   254  metadata:
   255    name: se2
   256  spec:
   257    hosts:
   258    - example.com
   259    ports:
   260    - name: port1
   261      number: 8080
   262      protocol: HTTP
   263    resolution: DNS
   264  ---
   265  apiVersion: networking.istio.io/v1beta1
   266  kind: ServiceEntry
   267  metadata:
   268    name: se3
   269  spec:
   270    hosts:
   271    - example.com
   272    ports:
   273    - name: port1
   274      number: 80
   275      protocol: HTTP
   276    resolution: DNS
   277  ---
   278  apiVersion: networking.istio.io/v1beta1
   279  kind: ServiceEntry
   280  metadata:
   281    name: se4
   282  spec:
   283    hosts:
   284    - example.com
   285    ports:
   286    - name: port1
   287      number: 80
   288      targetPort: 1234
   289      protocol: HTTP
   290    resolution: DNS
   291  ---
   292  apiVersion: networking.istio.io/v1beta1
   293  kind: ServiceEntry
   294  metadata:
   295    name: se5
   296  spec:
   297    hosts:
   298    - example.com
   299    ports:
   300    - name: port1
   301      number: 80
   302      targetPort: 999
   303      protocol: HTTP
   304    resolution: DNS
   305    endpoints:
   306    - address: endpoint.example.com
   307    - address: endpoint-port-override.example.com
   308      ports:
   309        port1: 2345
   310  `})
   311  
   312  	res := xdstest.ExtractClusterEndpoints(s.Clusters(s.SetupProxy(nil)))
   313  	// TODO(https://github.com/istio/istio/issues/50749) order should be deterministic
   314  	slices.Sort(res["outbound|80||example.com"])
   315  	assert.Equal(t, res, map[string][]string{
   316  		"outbound|8080||example.com": {"example.com:8080"},
   317  		// Kind of weird to have multiple here, but it is what it is...
   318  		// If we had targetPort, etc, set here this would be required
   319  		"outbound|80||example.com": {
   320  			"endpoint-port-override.example.com:2345",
   321  			"endpoint.example.com:999",
   322  			"example.com:1234",
   323  			"example.com:80",
   324  			"example.com:80",
   325  		},
   326  	})
   327  }