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  }