diff --git a/pkg/base/test_server_args.go b/pkg/base/test_server_args.go index 0d3ff49e6252..2f528b278d66 100644 --- a/pkg/base/test_server_args.go +++ b/pkg/base/test_server_args.go @@ -59,6 +59,15 @@ type TestServerArgs struct { // DisableTLSForHTTP if set, disables TLS for the HTTP interface. DisableTLSForHTTP bool + // SecondaryTenantPortOffset if non-zero forces the network addresses + // generated for servers started by the serverController to be offset + // from the base addressed by the specified amount. + SecondaryTenantPortOffset int + + // SecondaryTenantKnobs contains the testing knobs to use + // for tenant servers started by the serverController. + SecondaryTenantKnobs TestingKnobs + // JoinAddr is the address of a node we are joining. // // If left empty and the TestServer is being added to a nonempty cluster, this diff --git a/pkg/cli/democluster/demo_cluster.go b/pkg/cli/democluster/demo_cluster.go index 125215dd1103..35c3c1ae1307 100644 --- a/pkg/cli/democluster/demo_cluster.go +++ b/pkg/cli/democluster/demo_cluster.go @@ -218,7 +218,9 @@ func NewDemoCluster( // tenant. // Note: this logic can be removed once we use a single // listener for HTTP and SQL. - c.httpFirstPort += c.demoCtx.NumNodes + if !c.demoCtx.InProcessTenant { + c.httpFirstPort += c.demoCtx.NumNodes + } c.sqlFirstPort += c.demoCtx.NumNodes c.rpcFirstPort += c.demoCtx.NumNodes } @@ -419,23 +421,16 @@ func (c *transientCluster) Start(ctx context.Context) (err error) { latencyMap := c.servers[i].Cfg.TestingKnobs.Server.(*server.TestingKnobs). ContextTestingKnobs.InjectedLatencyOracle c.infoLog(ctx, "starting tenant node %d", i) - tenantStopper := stop.NewStopper() - ts, err := c.servers[i].StartTenant(ctx, base.TestTenantArgs{ + + args := base.TestTenantArgs{ // We set the tenant ID to i+2, since tenant 0 is not a tenant, and // tenant 1 is the system tenant. We also subtract 2 for the "starting" // SQL/HTTP ports so the first tenant ends up with the desired default // ports. - DisableCreateTenant: !createTenant, - TenantName: roachpb.TenantName(fmt.Sprintf("demo-tenant-%d", secondaryTenantID)), - TenantID: roachpb.MustMakeTenantID(secondaryTenantID), - Stopper: tenantStopper, - ForceInsecure: c.demoCtx.Insecure, - SSLCertsDir: c.demoDir, - DisableTLSForHTTP: true, - EnableDemoLoginEndpoint: true, - StartingRPCAndSQLPort: c.demoCtx.SQLPort - secondaryTenantID + i, - StartingHTTPPort: c.demoCtx.HTTPPort - secondaryTenantID + i, - Locality: c.demoCtx.Localities[i], + DisableCreateTenant: !createTenant, + TenantName: roachpb.TenantName("demo-tenant"), + TenantID: roachpb.MustMakeTenantID(secondaryTenantID), + InProcessTenant: c.demoCtx.InProcessTenant, TestingKnobs: base.TestingKnobs{ Server: &server.TestingKnobs{ ContextTestingKnobs: rpc.ContextTestingKnobs{ @@ -444,15 +439,33 @@ func (c *transientCluster) Start(ctx context.Context) (err error) { }, }, }, - InProcessTenant: c.demoCtx.InProcessTenant, - }) - c.stopper.AddCloser(stop.CloserFn(func() { - stopCtx := context.Background() - if ts != nil { - stopCtx = ts.AnnotateCtx(stopCtx) - } - tenantStopper.Stop(stopCtx) - })) + } + + var tenantStopper *stop.Stopper + if !c.demoCtx.InProcessTenant { + tenantStopper = stop.NewStopper() + args.Stopper = tenantStopper + args.ForceInsecure = c.demoCtx.Insecure + args.SSLCertsDir = c.demoDir + args.DisableTLSForHTTP = true + args.EnableDemoLoginEndpoint = true + args.StartingRPCAndSQLPort = c.demoCtx.SQLPort - secondaryTenantID + i + args.StartingHTTPPort = c.demoCtx.HTTPPort - secondaryTenantID + i + args.Locality = c.demoCtx.Localities[i] + } + + ts, err := c.servers[i].StartTenant(ctx, args) + if !c.demoCtx.InProcessTenant { + // InProcessTenant means that the server controller is + // already taking care of shutdown. + c.stopper.AddCloser(stop.CloserFn(func() { + stopCtx := context.Background() + if ts != nil { + stopCtx = ts.AnnotateCtx(stopCtx) + } + tenantStopper.Stop(stopCtx) + })) + } if err != nil { return err } @@ -601,10 +614,14 @@ func (c *transientCluster) createAndAddNode( // The latency map will be populated after all servers have // started listening on RPC, and before they proceed with their // startup routine. - serverKnobs.ContextTestingKnobs = rpc.ContextTestingKnobs{ + rpcKnobs := rpc.ContextTestingKnobs{ InjectedLatencyOracle: regionlatency.MakeAddrMap(), InjectedLatencyEnabled: c.latencyEnabled.Get, } + serverKnobs.ContextTestingKnobs = rpcKnobs + args.SecondaryTenantKnobs.Server = &server.TestingKnobs{ + ContextTestingKnobs: rpcKnobs, + } } // Create the server instance. This also registers the in-memory store @@ -806,6 +823,7 @@ func (demoCtx *Context) testServerArgsForTransientCluster( // Demo clusters by default will create their own tenants, so we // don't need to create them here. DisableDefaultTestTenant: true, + Knobs: base.TestingKnobs{ Server: &server.TestingKnobs{ StickyEngineRegistry: stickyEngineRegistry, @@ -839,6 +857,15 @@ func (demoCtx *Context) testServerArgsForTransientCluster( args.Addr = fmt.Sprintf("127.0.0.1:%d", rpcPort) args.SQLAddr = fmt.Sprintf("127.0.0.1:%d", sqlPort) args.HTTPAddr = fmt.Sprintf("127.0.0.1:%d", httpPort) + + if demoCtx.InProcessTenant { + // The code in NewDemoCluster put the KV ports higher + // so we need to subtract the number of nodes to get + // back to the "good" ports. + // We reduce NumNodes by 1 because the server controller + // uses 1-based indexing for servers. + args.SecondaryTenantPortOffset = -(demoCtx.NumNodes + 1) + } } if demoCtx.Localities != nil { @@ -1793,6 +1820,11 @@ func (c *transientCluster) ListDemoNodes(w, ew io.Writer, justOne, verbose bool) // Connection parameters for the system tenant follow. uiURL := s.Cfg.AdminURL() + if q := uiURL.Query(); c.demoCtx.Multitenant && c.demoCtx.InProcessTenant && !q.Has(server.TenantNameParamInQueryURL) { + q.Add(server.TenantNameParamInQueryURL, catconstants.SystemTenantName) + uiURL.RawQuery = q.Encode() + } + sqlURL, err := c.getNetworkURLForServer(context.Background(), i, false /* includeAppName */, false /* forSecondaryTenant */) if err != nil { @@ -1824,12 +1856,11 @@ func (c *transientCluster) printURLs( // Print node ID and web UI URL. Embed the autologin feature inside the URL. // We avoid printing those when insecure, as the autologin path is not available // in that case. - pwauth := url.Values{ - "username": []string{c.adminUser.Normalized()}, - "password": []string{c.adminPassword}, - } + q := uiURL.Query() + q.Add("username", c.adminUser.Normalized()) + q.Add("password", c.adminPassword) uiURL.Path = server.DemoLoginPath - uiURL.RawQuery = pwauth.Encode() + uiURL.RawQuery = q.Encode() } fmt.Fprintln(w, " (webui) ", uiURL) diff --git a/pkg/cli/democluster/demo_cluster_test.go b/pkg/cli/democluster/demo_cluster_test.go index cb60a048de7d..91070e9057d3 100644 --- a/pkg/cli/democluster/demo_cluster_test.go +++ b/pkg/cli/democluster/demo_cluster_test.go @@ -67,17 +67,18 @@ func TestTestServerArgsForTransientCluster(t *testing.T) { sqlPoolMemorySize: 2 << 10, cacheSize: 1 << 10, expected: base.TestServerArgs{ - DisableDefaultTestTenant: true, - PartOfCluster: true, - JoinAddr: "127.0.0.1", - DisableTLSForHTTP: true, - Addr: "127.0.0.1:7890", - SQLAddr: "127.0.0.1:1234", - HTTPAddr: "127.0.0.1:4567", - SQLMemoryPoolSize: 2 << 10, - CacheSize: 1 << 10, - NoAutoInitializeCluster: true, - EnableDemoLoginEndpoint: true, + DisableDefaultTestTenant: true, + PartOfCluster: true, + JoinAddr: "127.0.0.1", + DisableTLSForHTTP: true, + Addr: "127.0.0.1:7890", + SQLAddr: "127.0.0.1:1234", + HTTPAddr: "127.0.0.1:4567", + SecondaryTenantPortOffset: -2, + SQLMemoryPoolSize: 2 << 10, + CacheSize: 1 << 10, + NoAutoInitializeCluster: true, + EnableDemoLoginEndpoint: true, Knobs: base.TestingKnobs{ Server: &server.TestingKnobs{ StickyEngineRegistry: stickyEnginesRegistry, @@ -91,17 +92,18 @@ func TestTestServerArgsForTransientCluster(t *testing.T) { sqlPoolMemorySize: 4 << 10, cacheSize: 4 << 10, expected: base.TestServerArgs{ - DisableDefaultTestTenant: true, - PartOfCluster: true, - JoinAddr: "127.0.0.1", - Addr: "127.0.0.1:7892", - SQLAddr: "127.0.0.1:1236", - HTTPAddr: "127.0.0.1:4569", - DisableTLSForHTTP: true, - SQLMemoryPoolSize: 4 << 10, - CacheSize: 4 << 10, - NoAutoInitializeCluster: true, - EnableDemoLoginEndpoint: true, + DisableDefaultTestTenant: true, + PartOfCluster: true, + JoinAddr: "127.0.0.1", + Addr: "127.0.0.1:7892", + SQLAddr: "127.0.0.1:1236", + HTTPAddr: "127.0.0.1:4569", + SecondaryTenantPortOffset: -2, + DisableTLSForHTTP: true, + SQLMemoryPoolSize: 4 << 10, + CacheSize: 4 << 10, + NoAutoInitializeCluster: true, + EnableDemoLoginEndpoint: true, Knobs: base.TestingKnobs{ Server: &server.TestingKnobs{ StickyEngineRegistry: stickyEnginesRegistry, @@ -257,6 +259,7 @@ func TestTransientClusterMultitenant(t *testing.T) { // This test is too slow to complete under the race detector, sometimes. skip.UnderRace(t) + skip.UnderStress(t) demoCtx := newDemoCtx() // Set up an empty 3-node cluster with tenants on each node. diff --git a/pkg/cli/interactive_tests/test_demo.tcl b/pkg/cli/interactive_tests/test_demo.tcl index e257be258e79..60c4ca2a4aa4 100644 --- a/pkg/cli/interactive_tests/test_demo.tcl +++ b/pkg/cli/interactive_tests/test_demo.tcl @@ -65,7 +65,7 @@ eexpect "sslmode=disable" eexpect "System tenant" eexpect "(webui)" eexpect "http://" -eexpect ":8081" +eexpect ":8080" eexpect "(sql)" eexpect "root@" eexpect ":26258" @@ -133,7 +133,7 @@ eexpect "sslrootcert=" eexpect "System tenant" eexpect "(webui)" -eexpect "http://127.0.0.1:8081/demologin" +eexpect "http://127.0.0.1:8080/demologin" eexpect "(sql)" eexpect "postgresql://demo:" eexpect ":26258" @@ -261,11 +261,11 @@ eexpect "defaultdb>" # Show the URLs. send "\\demo ls\r" eexpect "http://" -eexpect ":8003" +eexpect ":8000" eexpect "http://" -eexpect ":8004" +eexpect ":8001" eexpect "http://" -eexpect ":8005" +eexpect ":8002" eexpect "defaultdb>" send_eof diff --git a/pkg/cli/interactive_tests/test_demo_cli_integration.tcl b/pkg/cli/interactive_tests/test_demo_cli_integration.tcl index d4021f9bdfd1..7e12d35a979e 100644 --- a/pkg/cli/interactive_tests/test_demo_cli_integration.tcl +++ b/pkg/cli/interactive_tests/test_demo_cli_integration.tcl @@ -89,10 +89,10 @@ eexpect ":/# " # Check that the cookies work. set pyfile [file join [file dirname $argv0] test_auth_cookie.py] -send "$python $pyfile cookie_system.txt 'http://localhost:8081/_admin/v1/users'\r" +send "$python $pyfile cookie_system.txt 'http://localhost:8080/_admin/v1/users?tenant_name=system'\r" eexpect "username" eexpect "demo" -send "$python $pyfile cookie_app.txt 'http://localhost:8080/_admin/v1/users'\r" +send "$python $pyfile cookie_app.txt 'http://localhost:8080/_admin/v1/users?tenant_name=demo-tenant'\r" eexpect "username" eexpect "demo" end_test @@ -111,10 +111,10 @@ eexpect "defaultdb>" set spawn_id $shell_spawn_id -send "$python $pyfile cookie_system.txt 'http://localhost:8081/_admin/v1/users'\r" +send "$python $pyfile cookie_system.txt 'http://localhost:8080/_admin/v1/users?tenant_name=system'\r" eexpect "username" eexpect "demo" -send "$python $pyfile cookie_app.txt 'http://localhost:8080/_admin/v1/users'\r" +send "$python $pyfile cookie_app.txt 'http://localhost:8080/_admin/v1/users?tenant_name=demo-tenant'\r" eexpect "username" eexpect "demo" end_test diff --git a/pkg/server/config.go b/pkg/server/config.go index 3bad5ce56970..c0e9d485c9fc 100644 --- a/pkg/server/config.go +++ b/pkg/server/config.go @@ -183,6 +183,10 @@ type BaseConfig struct { // TestingKnobs is used for internal test controls only. TestingKnobs base.TestingKnobs + // SecondaryTenantKnobs contains the testing knobs to use + // for tenant servers started by the serverController. + SecondaryTenantKnobs base.TestingKnobs + // TestingInsecureWebAccess enables uses of the HTTP and UI // endpoints without a valid authentication token. This should be // used only in tests what want a secure cluster with RPC diff --git a/pkg/server/server_controller.go b/pkg/server/server_controller.go index 1c745a602792..715a552fc9fb 100644 --- a/pkg/server/server_controller.go +++ b/pkg/server/server_controller.go @@ -93,7 +93,6 @@ type newServerFn func( tenantName roachpb.TenantName, index int, deregister func(), - opts *BaseConfig, ) (onDemandServer, error) // serverController manages a fleet of multiple servers side-by-side. @@ -107,10 +106,6 @@ type serverController struct { // stopper is the parent stopper. stopper *stop.Stopper - // tenantBaseCfg allows overriding of the baseCfg for all new tenants. - // Used for testing. - tenantBaseCfg *BaseConfig - mu struct { syncutil.Mutex @@ -169,7 +164,7 @@ func (c *serverController) getOrCreateServer( // Server does not exist yet: instantiate and start it. c.mu.nextServerIdx++ idx := c.mu.nextServerIdx - s, err := c.newServerFn(ctx, tenantName, idx, deregisterFn, c.tenantBaseCfg) + s, err := c.newServerFn(ctx, tenantName, idx, deregisterFn) if err != nil { return nil, err } @@ -207,6 +202,9 @@ func (c *serverController) getServers() (res []onDemandServer) { // TenantSelectHeader is the HTTP header used to select a particular tenant. const TenantSelectHeader = `X-Cockroach-Tenant` +// TenantNameParamInQueryURL is the HTTP query URL parameter used to select a particular tenant. +const TenantNameParamInQueryURL = "tenant_name" + // TenantSelectCookieName is the name of the HTTP cookie used to select a particular tenant, // if the custom header is not specified. const TenantSelectCookieName = `tenant` @@ -239,8 +237,7 @@ func (c *serverController) httpMux(w http.ResponseWriter, r *http.Request) { func getTenantNameFromHTTPRequest(r *http.Request) (roachpb.TenantName, bool) { // Highest priority is manual override on the URL query parameters. - const tenantNameParamInQueryURL = "tenant_name" - if tenantName := r.URL.Query().Get(tenantNameParamInQueryURL); tenantName != "" { + if tenantName := r.URL.Query().Get(TenantNameParamInQueryURL); tenantName != "" { return roachpb.TenantName(tenantName), true } @@ -470,11 +467,7 @@ var ErrInvalidTenant error = errInvalidTenantMarker{} // is not active), the returned error will contain the // ErrInvalidTenant mark, which can be checked with errors.Is. func (s *Server) newServerForTenant( - ctx context.Context, - tenantName roachpb.TenantName, - index int, - deregister func(), - baseCfg *BaseConfig, + ctx context.Context, tenantName roachpb.TenantName, index int, deregister func(), ) (onDemandServer, error) { // Look up the ID of the requested tenant. // @@ -502,7 +495,7 @@ func (s *Server) newServerForTenant( } // Start the tenant server. - tenantStopper, tenantServer, err := s.startInMemoryTenantServerInternal(ctx, tenantID, index, baseCfg) + tenantStopper, tenantServer, err := s.startInMemoryTenantServerInternal(ctx, tenantID, index) if err != nil { // Abandon any work done so far. tenantStopper.Stop(ctx) @@ -570,7 +563,7 @@ func (t *systemServerWrapper) testingGetSQLAddr() string { // simultaneously running server. This can be used to allocate // distinct but predictable network listeners. func (s *Server) startInMemoryTenantServerInternal( - ctx context.Context, tenantID roachpb.TenantID, index int, baseCfgOverride *BaseConfig, + ctx context.Context, tenantID roachpb.TenantID, index int, ) (stopper *stop.Stopper, tenantServer *SQLServerWrapper, err error) { stopper = stop.NewStopper() @@ -582,9 +575,6 @@ func (s *Server) startInMemoryTenantServerInternal( if err != nil { return stopper, nil, err } - if baseCfgOverride != nil { - baseCfg = *baseCfgOverride - } // Create a child stopper for this tenant's server. ambientCtx := baseCfg.AmbientCtx @@ -694,6 +684,7 @@ func makeInMemoryTenantServerConfig( baseCfg.Locality = kvServerCfg.BaseConfig.Locality baseCfg.SpanConfigsDisabled = kvServerCfg.BaseConfig.SpanConfigsDisabled baseCfg.EnableDemoLoginEndpoint = kvServerCfg.BaseConfig.EnableDemoLoginEndpoint + baseCfg.TestingKnobs = kvServerCfg.BaseConfig.SecondaryTenantKnobs // TODO(knz): use a single network interface for all tenant servers. // See: https://github.com/cockroachdb/cockroach/issues/84585 @@ -709,6 +700,18 @@ func makeInMemoryTenantServerConfig( return baseCfg, sqlCfg, err } + // This will change when we can use a single SQL listener. + const splitSQL = false + if splitSQL { + baseCfg.SplitListenSQL = true + } else { + baseCfg.SplitListenSQL = false + baseCfg.Addr, baseCfg.SQLAddr = baseCfg.SQLAddr, baseCfg.Addr + baseCfg.AdvertiseAddr, baseCfg.SQLAdvertiseAddr = baseCfg.SQLAdvertiseAddr, baseCfg.AdvertiseAddr + baseCfg.SQLAddr = "" + baseCfg.SQLAdvertiseAddr = "" + } + // The parent server will route HTTP requests to us. baseCfg.DisableHTTPListener = true // Nevertheless, we like to know our own HTTP address. @@ -719,8 +722,6 @@ func makeInMemoryTenantServerConfig( // See: https://github.com/cockroachdb/cockroach/issues/84585 baseCfg.SocketFile = "" - baseCfg.SplitListenSQL = false - // TODO(knz): Make the TLS config separate per tenant. // See https://cockroachlabs.atlassian.net/browse/CRDB-14539. baseCfg.SSLCertsDir = kvServerCfg.BaseConfig.SSLCertsDir diff --git a/pkg/server/testserver.go b/pkg/server/testserver.go index 023bd512669f..d98db7c66cab 100644 --- a/pkg/server/testserver.go +++ b/pkg/server/testserver.go @@ -215,6 +215,9 @@ func makeTestConfigFromParams(params base.TestServerArgs) Config { cfg.SQLAdvertiseAddr = util.IsolatedTestAddr.String() cfg.HTTPAddr = util.IsolatedTestAddr.String() } + if params.SecondaryTenantPortOffset != 0 { + cfg.SecondaryTenantPortOffset = params.SecondaryTenantPortOffset + } if params.Addr != "" { cfg.Addr = params.Addr cfg.AdvertiseAddr = params.Addr @@ -828,6 +831,26 @@ func (ts *TestServer) StartTenant( } } + if params.InProcessTenant { + onDemandServer, err := ts.serverController.getOrCreateServer(ctx, params.TenantName) + if err != nil { + return nil, err + } + sw := onDemandServer.(*tenantServerWrapper) + + hts := &httpTestServer{} + hts.t.authentication = sw.server.authentication + hts.t.sqlServer = sw.server.sqlServer + hts.t.tenantName = params.TenantName + + return &TestTenant{ + SQLServer: sw.server.sqlServer, + Cfg: sw.server.sqlServer.cfg, + httpTestServer: hts, + drain: sw.server.drainServer, + }, err + } + st := params.Settings if st == nil { st = cluster.MakeTestingClusterSettings() @@ -928,61 +951,39 @@ func (ts *TestServer) StartTenant( baseCfg.HTTPAdvertiseAddr = newAddr } - if !params.InProcessTenant { - sw, err := NewTenantServer( - ctx, - stopper, - baseCfg, - sqlCfg, - ) - if err != nil { - return nil, err - } - go func() { - // If the server requests a shutdown, do that simply by stopping the - // tenant's stopper. - select { - case <-sw.ShutdownRequested(): - stopper.Stop(sw.AnnotateCtx(context.Background())) - case <-stopper.ShouldQuiesce(): - } - }() - - if err := sw.Start(ctx); err != nil { - return nil, err - } - - hts := &httpTestServer{} - hts.t.authentication = sw.authentication - hts.t.sqlServer = sw.sqlServer - - return &TestTenant{ - SQLServer: sw.sqlServer, - Cfg: &baseCfg, - httpTestServer: hts, - drain: sw.drainServer, - }, err + sw, err := NewTenantServer( + ctx, + stopper, + baseCfg, + sqlCfg, + ) + if err != nil { + return nil, err } + go func() { + // If the server requests a shutdown, do that simply by stopping the + // tenant's stopper. + select { + case <-sw.ShutdownRequested(): + stopper.Stop(sw.AnnotateCtx(context.Background())) + case <-stopper.ShouldQuiesce(): + } + }() - ts.serverController.tenantBaseCfg = &baseCfg - onDemandServer, err := ts.serverController.getOrCreateServer(ctx, params.TenantName) - if err != nil { + if err := sw.Start(ctx); err != nil { return nil, err } - sw := onDemandServer.(*tenantServerWrapper) hts := &httpTestServer{} - hts.t.authentication = sw.server.authentication - hts.t.sqlServer = sw.server.sqlServer - hts.t.tenantName = params.TenantName + hts.t.authentication = sw.authentication + hts.t.sqlServer = sw.sqlServer return &TestTenant{ - SQLServer: sw.server.sqlServer, - Cfg: sw.server.sqlServer.cfg, + SQLServer: sw.sqlServer, + Cfg: &baseCfg, httpTestServer: hts, - drain: sw.server.drainServer, + drain: sw.drainServer, }, err - } // ExpectedInitialRangeCount returns the expected number of ranges that should diff --git a/pkg/server/testserver_http.go b/pkg/server/testserver_http.go index 6edd7ebeeebe..273a723d34ec 100644 --- a/pkg/server/testserver_http.go +++ b/pkg/server/testserver_http.go @@ -54,7 +54,9 @@ type tenantHeaderDecorator struct { } func (t tenantHeaderDecorator) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Add(TenantSelectHeader, string(t.tenantName)) + if t.tenantName != "" { + req.Header.Add(TenantSelectHeader, string(t.tenantName)) + } return t.RoundTripper.RoundTrip(req) } @@ -62,7 +64,13 @@ var _ http.RoundTripper = &tenantHeaderDecorator{} // AdminURL implements TestServerInterface. func (ts *httpTestServer) AdminURL() string { - return ts.t.sqlServer.execCfg.RPCContext.Config.AdminURL().String() + u := ts.t.sqlServer.execCfg.RPCContext.Config.AdminURL() + if ts.t.tenantName != "" { + q := u.Query() + q.Add(TenantNameParamInQueryURL, string(ts.t.tenantName)) + u.RawQuery = q.Encode() + } + return u.String() } // GetUnauthenticatedHTTPClient implements TestServerInterface.