istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/xds/lds_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 "os" 18 "testing" 19 20 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 21 listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" 22 hcm "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" 23 discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" 24 25 meshconfig "istio.io/api/mesh/v1alpha1" 26 "istio.io/istio/pilot/pkg/model" 27 v3 "istio.io/istio/pilot/pkg/xds/v3" 28 "istio.io/istio/pilot/test/xds" 29 "istio.io/istio/pilot/test/xdstest" 30 "istio.io/istio/pkg/config/labels" 31 "istio.io/istio/pkg/config/mesh" 32 "istio.io/istio/pkg/wellknown" 33 "istio.io/istio/tests/util" 34 ) 35 36 // TestLDS using isolated namespaces 37 func TestLDSIsolated(t *testing.T) { 38 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ConfigString: mustReadfolder(t, "tests/testdata/config")}) 39 40 // Sidecar in 'none' mode 41 t.Run("sidecar_none", func(t *testing.T) { 42 wd := t.TempDir() 43 adscon := s.Connect(&model.Proxy{ 44 Metadata: &model.NodeMetadata{ 45 InterceptionMode: model.InterceptionNone, 46 HTTP10: "1", 47 }, 48 IPAddresses: []string{"10.11.0.1"}, // matches none.yaml s1tcp.none 49 ConfigNamespace: "none", 50 }, nil, watchAll) 51 52 err := adscon.Save(wd + "/none") 53 if err != nil { 54 t.Fatal(err) 55 } 56 57 // 7071 (inbound), 2001 (service - also as http proxy), 18010 (fortio) 58 if len(adscon.GetHTTPListeners()) != 3 { 59 t.Error("HTTP listeners, expecting 3 got", len(adscon.GetHTTPListeners()), xdstest.MapKeys(adscon.GetHTTPListeners())) 60 } 61 62 // s1tcp:2000 outbound, bind=true (to reach other instances of the service) 63 // s1:5005 outbound, bind=true 64 // :443 - https external, bind=false 65 // 10.11.0.1_7070, bind=true -> inbound|2000|s1 - on port 7070, fwd to 37070 66 // virtual 67 if len(adscon.GetTCPListeners()) == 0 { 68 t.Fatal("No response") 69 } 70 71 for _, s := range []string{"lds_tcp", "lds_http", "rds", "cds", "ecds"} { 72 want, err := os.ReadFile(wd + "/none_" + s + ".json") 73 if err != nil { 74 t.Fatal(err) 75 } 76 got, err := os.ReadFile("testdata/none_" + s + ".json") 77 if err != nil { 78 t.Fatal(err) 79 } 80 81 if err = util.Compare(got, want); err != nil { 82 // Just log for now - golden changes every time there is a config generation update. 83 // It is mostly intended as a reference for what is generated - we need to add explicit checks 84 // for things we need, like the number of expected listeners. 85 // This is mainly using for debugging what changed from the snapshot in the golden files. 86 if os.Getenv("CONFIG_DIFF") == "1" { 87 t.Logf("error in golden file %s %v", s, err) 88 } 89 } 90 } 91 }) 92 93 // Test for the examples in the ServiceEntry doc 94 t.Run("se_example", func(t *testing.T) { 95 // TODO: add a Service with EDS resolution in the none ns. 96 // The ServiceEntry only allows STATIC - both STATIC and EDS should generated TCP listeners on :port 97 // while DNS and NONE should generate old-style bind ports. 98 // Right now 'STATIC' and 'EDS' result in ClientSideLB in the internal object, so listener test is valid. 99 100 s.Connect(&model.Proxy{ 101 IPAddresses: []string{"10.12.0.1"}, // matches none.yaml s1tcp.none 102 ConfigNamespace: "seexamples", 103 }, nil, watchAll) 104 }) 105 106 // Test for the examples in the ServiceEntry doc 107 t.Run("se_examplegw", func(t *testing.T) { 108 // TODO: add a Service with EDS resolution in the none ns. 109 // The ServiceEntry only allows STATIC - both STATIC and EDS should generated TCP listeners on :port 110 // while DNS and NONE should generate old-style bind ports. 111 // Right now 'STATIC' and 'EDS' result in ClientSideLB in the internal object, so listener test is valid. 112 113 s.Connect(&model.Proxy{ 114 IPAddresses: []string{"10.13.0.1"}, // matches none.yaml s1tcp.none 115 ConfigNamespace: "exampleegressgw", 116 }, nil, watchAll) 117 }) 118 } 119 120 // TestLDS using default sidecar in root namespace 121 func TestLDSWithDefaultSidecar(t *testing.T) { 122 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 123 ConfigString: mustReadfolder(t, "tests/testdata/networking/sidecar-ns-scope"), 124 MeshConfig: func() *meshconfig.MeshConfig { 125 m := mesh.DefaultMeshConfig() 126 m.RootNamespace = "istio-config" 127 return m 128 }(), 129 }) 130 adsc := s.Connect(&model.Proxy{ConfigNamespace: "ns1", IPAddresses: []string{"100.1.1.2"}}, nil, watchAll) 131 132 // Expect 2 listeners : 2 orig_dst, 2 outbound (http, tcp1) 133 if (len(adsc.GetHTTPListeners()) + len(adsc.GetTCPListeners())) != 4 { 134 t.Fatalf("Expected 4 listeners, got %d\n", len(adsc.GetHTTPListeners())+len(adsc.GetTCPListeners())) 135 } 136 137 // Expect 9 CDS clusters: 138 // 2 inbound(http, inbound passthroughipv4) notes: no passthroughipv6 139 // 9 outbound (2 http services, 1 tcp service, 140 // and 2 subsets of http1, 1 blackhole, 1 passthrough) 141 if (len(adsc.GetClusters()) + len(adsc.GetEdsClusters())) != 9 { 142 t.Fatalf("Expected 9 clusters in CDS output. Got %d", len(adsc.GetClusters())+len(adsc.GetEdsClusters())) 143 } 144 145 // Expect two vhost blocks in RDS output for 8080 (one for http1, another for http2) 146 // plus one extra due to mem registry 147 if len(adsc.GetRoutes()["8080"].VirtualHosts) != 3 { 148 t.Fatalf("Expected 3 VirtualHosts in RDS output. Got %d", len(adsc.GetRoutes()["8080"].VirtualHosts)) 149 } 150 } 151 152 // TestLDS using gateways 153 func TestLDSWithIngressGateway(t *testing.T) { 154 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 155 ConfigString: mustReadfolder(t, "tests/testdata/networking/ingress-gateway"), 156 MeshConfig: func() *meshconfig.MeshConfig { 157 m := mesh.DefaultMeshConfig() 158 m.RootNamespace = "istio-config" 159 return m 160 }(), 161 }) 162 labels := labels.Instance{"istio": "ingressgateway"} 163 adsc := s.Connect(&model.Proxy{ 164 ConfigNamespace: "istio-system", 165 Metadata: &model.NodeMetadata{Labels: labels}, 166 IPAddresses: []string{"99.1.1.1"}, 167 Type: model.Router, 168 }, nil, []string{v3.ClusterType, v3.EndpointType, v3.ListenerType}) 169 170 // Expect 2 listeners : 1 for 80, 1 for 443 171 // where 443 listener has 3 filter chains 172 if (len(adsc.GetHTTPListeners()) + len(adsc.GetTCPListeners())) != 2 { 173 t.Fatalf("Expected 2 listeners, got %d\n", len(adsc.GetHTTPListeners())+len(adsc.GetTCPListeners())) 174 } 175 176 // TODO: This is flimsy. The ADSC code treats any listener with http connection manager as a HTTP listener 177 // instead of looking at it as a listener with multiple filter chains 178 l := adsc.GetHTTPListeners()["0.0.0.0_443"] 179 180 if l != nil { 181 if len(l.FilterChains) != 3 { 182 t.Fatalf("Expected 3 filter chains, got %d\n", len(l.FilterChains)) 183 } 184 } 185 } 186 187 // TestLDS is running LDS tests. 188 func TestLDS(t *testing.T) { 189 t.Run("sidecar", func(t *testing.T) { 190 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{}) 191 ads := s.ConnectADS().WithType(v3.ListenerType) 192 ads.RequestResponseAck(t, nil) 193 }) 194 195 // 'router' or 'gateway' type of listener 196 t.Run("gateway", func(t *testing.T) { 197 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ConfigString: mustReadfolder(t, "tests/testdata/config")}) 198 // Matches Gateway config in test data 199 labels := map[string]string{"version": "v2", "app": "my-gateway-controller"} 200 ads := s.ConnectADS().WithType(v3.ListenerType).WithID(gatewayID(gatewayIP)) 201 ads.RequestResponseAck(t, &discovery.DiscoveryRequest{ 202 Node: &core.Node{ 203 Id: ads.ID, 204 Metadata: model.NodeMetadata{Labels: labels}.ToStruct(), 205 }, 206 }) 207 }) 208 } 209 210 // TestLDS using sidecar scoped on workload without Service 211 func TestLDSWithSidecarForWorkloadWithoutService(t *testing.T) { 212 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 213 ConfigString: mustReadfolder(t, "tests/testdata/networking/sidecar-without-service"), 214 MeshConfig: func() *meshconfig.MeshConfig { 215 m := mesh.DefaultMeshConfig() 216 m.RootNamespace = "istio-config" 217 return m 218 }(), 219 }) 220 labels := labels.Instance{"app": "consumeronly"} 221 s.MemRegistry.AddWorkload("98.1.1.1", labels) // These labels must match the sidecars workload selector 222 adsc := s.Connect(&model.Proxy{ 223 ConfigNamespace: "consumerns", 224 Metadata: &model.NodeMetadata{Labels: labels}, 225 IPAddresses: []string{"98.1.1.1"}, 226 }, nil, watchAll) 227 228 // Expect 2 HTTP listeners for outbound 8081 and one virtualInbound which has the same inbound 9080 229 // as a filter chain. Since the adsclient code treats any listener with a HTTP connection manager filter in ANY 230 // filter chain, as a HTTP listener, we end up getting both 9080 and virtualInbound. 231 if len(adsc.GetHTTPListeners()) != 2 { 232 t.Fatalf("Expected 2 http listeners, got %d", len(adsc.GetHTTPListeners())) 233 } 234 235 // TODO: This is flimsy. The ADSC code treats any listener with http connection manager as a HTTP listener 236 // instead of looking at it as a listener with multiple filter chains 237 if l := adsc.GetHTTPListeners()["0.0.0.0_8081"]; l != nil { 238 expected := 1 239 if len(l.FilterChains) != expected { 240 t.Fatalf("Expected %d filter chains, got %d", expected, len(l.FilterChains)) 241 } 242 } else { 243 t.Fatal("Expected listener for 0.0.0.0_8081") 244 } 245 246 if l := adsc.GetHTTPListeners()["virtualInbound"]; l == nil { 247 t.Fatal("Expected listener virtualInbound") 248 } 249 250 // Expect only one eds cluster for http1.ns1.svc.cluster.local 251 if len(adsc.GetEdsClusters()) != 1 { 252 t.Fatalf("Expected 1 eds cluster, got %d", len(adsc.GetEdsClusters())) 253 } 254 if cluster, ok := adsc.GetEdsClusters()["outbound|8081||http1.ns1.svc.cluster.local"]; !ok { 255 t.Fatalf("Expected eds cluster outbound|8081||http1.ns1.svc.cluster.local, got %v", cluster.Name) 256 } 257 } 258 259 // TestLDS using default sidecar in root namespace 260 func TestLDSEnvoyFilterWithWorkloadSelector(t *testing.T) { 261 s := xds.NewFakeDiscoveryServer(t, xds.FakeOptions{ 262 ConfigString: mustReadfolder(t, "tests/testdata/networking/envoyfilter-without-service"), 263 }) 264 // The labels of 98.1.1.1 must match the envoyfilter workload selector 265 s.MemRegistry.AddWorkload("98.1.1.1", labels.Instance{"app": "envoyfilter-test-app", "some": "otherlabel"}) 266 s.MemRegistry.AddWorkload("98.1.1.2", labels.Instance{"app": "no-envoyfilter-test-app"}) 267 s.MemRegistry.AddWorkload("98.1.1.3", labels.Instance{}) 268 269 tests := []struct { 270 name string 271 ip string 272 labels labels.Instance 273 expectLuaFilter bool 274 }{ 275 { 276 name: "Add filter with matching labels to sidecar", 277 ip: "98.1.1.1", 278 labels: labels.Instance{"app": "envoyfilter-test-app", "some": "otherlabel"}, 279 expectLuaFilter: true, 280 }, 281 { 282 name: "Ignore filter with not matching labels to sidecar", 283 ip: "98.1.1.2", 284 labels: labels.Instance{"app": "no-envoyfilter-test-app"}, 285 expectLuaFilter: false, 286 }, 287 { 288 name: "Ignore filter with empty labels to sidecar", 289 ip: "98.1.1.3", 290 labels: labels.Instance{}, 291 expectLuaFilter: false, 292 }, 293 } 294 295 for _, test := range tests { 296 test := test 297 t.Run(test.name, func(t *testing.T) { 298 adsc := s.Connect(&model.Proxy{ 299 ConfigNamespace: "consumerns", 300 Metadata: &model.NodeMetadata{Labels: test.labels}, 301 IPAddresses: []string{test.ip}, 302 }, nil, watchAll) 303 304 // Expect 1 HTTP listeners for 8081 305 if len(adsc.GetHTTPListeners()) != 1 { 306 t.Fatalf("Expected 2 http listeners, got %v", xdstest.MapKeys(adsc.GetHTTPListeners())) 307 } 308 // TODO: This is flimsy. The ADSC code treats any listener with http connection manager as a HTTP listener 309 // instead of looking at it as a listener with multiple filter chains 310 l := adsc.GetHTTPListeners()["0.0.0.0_8081"] 311 312 expectLuaFilter(t, l, test.expectLuaFilter) 313 }) 314 } 315 } 316 317 func expectLuaFilter(t *testing.T, l *listener.Listener, expected bool) { 318 t.Helper() 319 if l != nil { 320 var chain *listener.FilterChain 321 for _, fc := range l.FilterChains { 322 if len(fc.Filters) == 1 && fc.Filters[0].Name == wellknown.HTTPConnectionManager { 323 chain = fc 324 } 325 } 326 if chain == nil { 327 t.Fatalf("Failed to find http_connection_manager") 328 } 329 if len(chain.Filters) != 1 { 330 t.Fatalf("Expected 1 filter in first filter chain, got %d", len(l.FilterChains)) 331 } 332 filter := chain.Filters[0] 333 if filter.Name != wellknown.HTTPConnectionManager { 334 t.Fatalf("Expected HTTP connection, found %v", chain.Filters[0].Name) 335 } 336 httpCfg, ok := filter.ConfigType.(*listener.Filter_TypedConfig) 337 if !ok { 338 t.Fatalf("Expected Http Connection Manager Config Filter_TypedConfig, found %T", filter.ConfigType) 339 } 340 connectionManagerCfg := hcm.HttpConnectionManager{} 341 err := httpCfg.TypedConfig.UnmarshalTo(&connectionManagerCfg) 342 if err != nil { 343 t.Fatalf("Could not deserialize http connection manager config: %v", err) 344 } 345 found := false 346 for _, filter := range connectionManagerCfg.HttpFilters { 347 if filter.Name == "envoy.lua" { 348 found = true 349 } 350 } 351 if expected != found { 352 t.Fatalf("Expected Lua filter: %v, found: %v", expected, found) 353 } 354 } 355 }