google.golang.org/grpc@v1.72.2/test/xds/xds_client_federation_test.go (about)

     1  /*
     2   *
     3   * Copyright 2021 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package xds_test
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/google/uuid"
    29  	"google.golang.org/grpc"
    30  	"google.golang.org/grpc/codes"
    31  	"google.golang.org/grpc/credentials/insecure"
    32  	"google.golang.org/grpc/internal"
    33  	"google.golang.org/grpc/internal/stubserver"
    34  	"google.golang.org/grpc/internal/testutils"
    35  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    36  	"google.golang.org/grpc/internal/testutils/xds/e2e/setup"
    37  	"google.golang.org/grpc/internal/xds/bootstrap"
    38  	"google.golang.org/grpc/resolver"
    39  	"google.golang.org/grpc/status"
    40  
    41  	v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
    42  	v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
    43  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    44  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    45  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    46  	testpb "google.golang.org/grpc/interop/grpc_testing"
    47  )
    48  
    49  // TestClientSideFederation tests that federation is supported.
    50  //
    51  // In this test, some xDS responses contain resource names in another authority
    52  // (in the new resource name style):
    53  // - LDS: old style, no authority (default authority)
    54  // - RDS: new style, in a different authority
    55  // - CDS: old style, no authority (default authority)
    56  // - EDS: new style, in a different authority
    57  func (s) TestClientSideFederation(t *testing.T) {
    58  	// Start a management server as the default authority.
    59  	serverDefaultAuth := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
    60  
    61  	// Start another management server as the other authority.
    62  	const nonDefaultAuth = "non-default-auth"
    63  	serverAnotherAuth := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
    64  
    65  	// Create a bootstrap file in a temporary directory.
    66  	nodeID := uuid.New().String()
    67  	bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
    68  		Servers: []byte(fmt.Sprintf(`[{
    69  			"server_uri": %q,
    70  			"channel_creds": [{"type": "insecure"}]
    71  		}]`, serverDefaultAuth.Address)),
    72  		Node:                               []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
    73  		ServerListenerResourceNameTemplate: e2e.ServerListenerResourceNameTemplate,
    74  		// Specify the address of the non-default authority.
    75  		Authorities: map[string]json.RawMessage{
    76  			nonDefaultAuth: []byte(fmt.Sprintf(`{
    77  				"xds_servers": [
    78  					{
    79  						"server_uri": %q,
    80  						"channel_creds": [{"type": "insecure"}]
    81  					}
    82  				]
    83  			}`, serverAnotherAuth.Address)),
    84  		},
    85  	})
    86  	if err != nil {
    87  		t.Fatalf("Failed to create bootstrap file: %v", err)
    88  	}
    89  
    90  	if internal.NewXDSResolverWithConfigForTesting == nil {
    91  		t.Fatalf("internal.NewXDSResolverWithConfigForTesting is nil")
    92  	}
    93  	resolver, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
    94  	if err != nil {
    95  		t.Fatalf("Failed to create xDS resolver for testing: %v", err)
    96  	}
    97  	server := stubserver.StartTestService(t, nil)
    98  	defer server.Stop()
    99  
   100  	const serviceName = "my-service-client-side-xds"
   101  	// LDS is old style name.
   102  	ldsName := serviceName
   103  	// RDS is new style, with the non default authority.
   104  	rdsName := fmt.Sprintf("xdstp://%s/envoy.config.route.v3.RouteConfiguration/%s", nonDefaultAuth, "route-"+serviceName)
   105  	// CDS is old style name.
   106  	cdsName := "cluster-" + serviceName
   107  	// EDS is new style, with the non default authority.
   108  	edsName := fmt.Sprintf("xdstp://%s/envoy.config.route.v3.ClusterLoadAssignment/%s", nonDefaultAuth, "endpoints-"+serviceName)
   109  
   110  	// Split resources, put LDS/CDS in the default authority, and put RDS/EDS in
   111  	// the other authority.
   112  	resourcesDefault := e2e.UpdateOptions{
   113  		NodeID: nodeID,
   114  		// This has only LDS and CDS.
   115  		Listeners:      []*v3listenerpb.Listener{e2e.DefaultClientListener(ldsName, rdsName)},
   116  		Clusters:       []*v3clusterpb.Cluster{e2e.DefaultCluster(cdsName, edsName, e2e.SecurityLevelNone)},
   117  		SkipValidation: true,
   118  	}
   119  	resourcesAnother := e2e.UpdateOptions{
   120  		NodeID: nodeID,
   121  		// This has only RDS and EDS.
   122  		Routes:         []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(rdsName, ldsName, cdsName)},
   123  		Endpoints:      []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})},
   124  		SkipValidation: true,
   125  	}
   126  
   127  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   128  	defer cancel()
   129  	// This has only LDS and CDS.
   130  	if err := serverDefaultAuth.Update(ctx, resourcesDefault); err != nil {
   131  		t.Fatal(err)
   132  	}
   133  	// This has only RDS and EDS.
   134  	if err := serverAnotherAuth.Update(ctx, resourcesAnother); err != nil {
   135  		t.Fatal(err)
   136  	}
   137  
   138  	// Create a ClientConn and make a successful RPC.
   139  	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
   140  	if err != nil {
   141  		t.Fatalf("grpc.NewClient() failed: %v", err)
   142  	}
   143  	defer cc.Close()
   144  
   145  	client := testgrpc.NewTestServiceClient(cc)
   146  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
   147  		t.Fatalf("rpc EmptyCall() failed: %v", err)
   148  	}
   149  }
   150  
   151  // TestClientSideFederationWithOnlyXDSTPStyleLDS tests that federation is
   152  // supported with new xdstp style names for LDS only while using the old style
   153  // for other resources. This test in addition also checks that when service name
   154  // contains escapable characters, we "fully" encode it for looking up
   155  // VirtualHosts in xDS RouteConfiguration.
   156  func (s) TestClientSideFederationWithOnlyXDSTPStyleLDS(t *testing.T) {
   157  	// Start a management server as a sophisticated authority.
   158  	const authority = "traffic-manager.xds.notgoogleapis.com"
   159  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
   160  
   161  	// Create a bootstrap file in a temporary directory.
   162  	nodeID := uuid.New().String()
   163  	bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
   164  		Servers: []byte(fmt.Sprintf(`[{
   165  			"server_uri": %q,
   166  			"channel_creds": [{"type": "insecure"}]
   167  		}]`, mgmtServer.Address)),
   168  		Node: []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
   169  		ClientDefaultListenerResourceNameTemplate: fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/%%s", authority),
   170  		// Specify the address of the non-default authority.
   171  		Authorities: map[string]json.RawMessage{
   172  			authority: []byte(fmt.Sprintf(`{
   173  				"xds_servers": [
   174  					{
   175  						"server_uri": %q,
   176  						"channel_creds": [{"type": "insecure"}]
   177  					}
   178  				]
   179  			}`, mgmtServer.Address)),
   180  		},
   181  	})
   182  	if err != nil {
   183  		t.Fatalf("Failed to create bootstrap file: %v", err)
   184  	}
   185  
   186  	if internal.NewXDSResolverWithConfigForTesting == nil {
   187  		t.Fatalf("internal.NewXDSResolverWithConfigForTesting is nil")
   188  	}
   189  	resolver, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
   190  	if err != nil {
   191  		t.Fatalf("Failed to create xDS resolver for testing: %v", err)
   192  	}
   193  	server := stubserver.StartTestService(t, nil)
   194  	defer server.Stop()
   195  
   196  	// serviceName with escapable characters - ' ', and '/'.
   197  	const serviceName = "my-service-client-side-xds/2nd component"
   198  
   199  	// All other resources are with old style name.
   200  	const rdsName = "route-" + serviceName
   201  	const cdsName = "cluster-" + serviceName
   202  	const edsName = "endpoints-" + serviceName
   203  
   204  	// Resource update sent to go-control-plane mgmt server.
   205  	resourceUpdate := e2e.UpdateOptions{
   206  		NodeID: nodeID,
   207  		Listeners: func() []*v3listenerpb.Listener {
   208  			// LDS is new style xdstp name. Since the LDS resource name is prefixed
   209  			// with xdstp, the string will be %-encoded excluding '/'s. See
   210  			// bootstrap.PopulateResourceTemplate().
   211  			const specialEscapedServiceName = "my-service-client-side-xds/2nd%20component" // same as bootstrap.percentEncode(serviceName)
   212  			ldsName := fmt.Sprintf("xdstp://%s/envoy.config.listener.v3.Listener/%s", authority, specialEscapedServiceName)
   213  			return []*v3listenerpb.Listener{e2e.DefaultClientListener(ldsName, rdsName)}
   214  		}(),
   215  		Routes: func() []*v3routepb.RouteConfiguration {
   216  			// RouteConfiguration will has one entry in []VirtualHosts that contains the
   217  			// "fully" escaped service name in []Domains. This is to assert that gRPC
   218  			// uses the escaped service name to lookup VirtualHosts. RDS is also with
   219  			// old style name.
   220  			const fullyEscapedServiceName = "my-service-client-side-xds%2F2nd%20component" // same as url.PathEscape(serviceName)
   221  			return []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(rdsName, fullyEscapedServiceName, cdsName)}
   222  		}(),
   223  		Clusters:       []*v3clusterpb.Cluster{e2e.DefaultCluster(cdsName, edsName, e2e.SecurityLevelNone)},
   224  		Endpoints:      []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})},
   225  		SkipValidation: true,
   226  	}
   227  
   228  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   229  	defer cancel()
   230  	if err := mgmtServer.Update(ctx, resourceUpdate); err != nil {
   231  		t.Fatal(err)
   232  	}
   233  
   234  	// Create a ClientConn and make a successful RPC.
   235  	cc, err := grpc.NewClient(fmt.Sprintf("xds:///%s", serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolver))
   236  	if err != nil {
   237  		t.Fatalf("grpc.NewClient() failed: %v", err)
   238  	}
   239  	defer cc.Close()
   240  
   241  	client := testgrpc.NewTestServiceClient(cc)
   242  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
   243  		t.Fatalf("rpc EmptyCall() failed: %v", err)
   244  	}
   245  }
   246  
   247  // TestFederation_UnknownAuthorityInDialTarget tests the case where a ClientConn
   248  // is created with a dial target containing an authority which is not specified
   249  // in the bootstrap configuration. The test verifies that RPCs on the ClientConn
   250  // fail with an appropriate error.
   251  func (s) TestFederation_UnknownAuthorityInDialTarget(t *testing.T) {
   252  	// Setting up the management server is not *really* required for this test
   253  	// case. All we need is a bootstrap configuration which does not contain the
   254  	// authority mentioned in the dial target. But setting up the management
   255  	// server and actually making an RPC ensures that the xDS client is
   256  	// configured properly, and when we dial with an unknown authority in the
   257  	// next step, we can be sure that the error we receive is legitimate.
   258  	managementServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
   259  
   260  	server := stubserver.StartTestService(t, nil)
   261  	defer server.Stop()
   262  
   263  	const serviceName = "my-service-client-side-xds"
   264  	resources := e2e.DefaultClientResources(e2e.ResourceParams{
   265  		DialTarget: serviceName,
   266  		NodeID:     nodeID,
   267  		Host:       "localhost",
   268  		Port:       testutils.ParsePort(t, server.Address),
   269  		SecLevel:   e2e.SecurityLevelNone,
   270  	})
   271  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   272  	defer cancel()
   273  	if err := managementServer.Update(ctx, resources); err != nil {
   274  		t.Fatal(err)
   275  	}
   276  
   277  	// Create a ClientConn and make a successful RPC.
   278  	target := fmt.Sprintf("xds:///%s", serviceName)
   279  	cc, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
   280  	if err != nil {
   281  		t.Fatalf("grpc.NewClient() failed %q: %v", target, err)
   282  	}
   283  	defer cc.Close()
   284  	t.Log("Created ClientConn to test service")
   285  
   286  	client := testgrpc.NewTestServiceClient(cc)
   287  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
   288  		t.Fatalf("EmptyCall() RPC: %v", err)
   289  	}
   290  	t.Log("Successfully performed an EmptyCall RPC")
   291  
   292  	target = fmt.Sprintf("xds://unknown-authority/%s", serviceName)
   293  	t.Logf("Creating a channel with unknown authority %q, expecting failure", target)
   294  	wantErr := fmt.Sprintf("authority \"unknown-authority\" specified in dial target %q is not found in the bootstrap file", target)
   295  	cc, err = grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
   296  	if err != nil {
   297  		t.Fatalf("Unexpected error while creating ClientConn: %v", err)
   298  	}
   299  	defer cc.Close()
   300  	client = testgrpc.NewTestServiceClient(cc)
   301  	_, err = client.EmptyCall(ctx, &testpb.Empty{})
   302  	if err == nil || !strings.Contains(err.Error(), wantErr) {
   303  		t.Fatalf("EmptyCall(_, _) = _, %v; want _, %q", err, wantErr)
   304  	}
   305  }
   306  
   307  // TestFederation_UnknownAuthorityInReceivedResponse tests the case where the
   308  // LDS resource associated with the dial target contains an RDS resource name
   309  // with an authority which is not specified in the bootstrap configuration. The
   310  // test verifies that RPCs fail with an appropriate error.
   311  func (s) TestFederation_UnknownAuthorityInReceivedResponse(t *testing.T) {
   312  	mgmtServer, nodeID, _, xdsResolver := setup.ManagementServerAndResolver(t)
   313  
   314  	// LDS is old style name.
   315  	// RDS is new style, with an unknown authority.
   316  	const serviceName = "my-service-client-side-xds"
   317  	const unknownAuthority = "unknown-authority"
   318  	ldsName := serviceName
   319  	rdsName := fmt.Sprintf("xdstp://%s/envoy.config.route.v3.RouteConfiguration/%s", unknownAuthority, "route-"+serviceName)
   320  
   321  	resources := e2e.UpdateOptions{
   322  		NodeID:         nodeID,
   323  		Listeners:      []*v3listenerpb.Listener{e2e.DefaultClientListener(ldsName, rdsName)},
   324  		Routes:         []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(rdsName, ldsName, "cluster-"+serviceName)},
   325  		SkipValidation: true, // This update has only LDS and RDS resources.
   326  	}
   327  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   328  	defer cancel()
   329  	if err := mgmtServer.Update(ctx, resources); err != nil {
   330  		t.Fatal(err)
   331  	}
   332  
   333  	target := fmt.Sprintf("xds:///%s", serviceName)
   334  	cc, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(xdsResolver))
   335  	if err != nil {
   336  		t.Fatalf("grpc.NewClient() failed %q: %v", target, err)
   337  	}
   338  	defer cc.Close()
   339  	t.Log("Created ClientConn to test service")
   340  
   341  	client := testgrpc.NewTestServiceClient(cc)
   342  	_, err = client.EmptyCall(ctx, &testpb.Empty{})
   343  	if err == nil {
   344  		t.Fatal("EmptyCall RPC succeeded for target with unknown authority when expected to fail")
   345  	}
   346  	if got, want := status.Code(err), codes.Unavailable; got != want {
   347  		t.Fatalf("EmptyCall RPC returned status code: %v, want %v", got, want)
   348  	}
   349  }