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 }