diff --git a/lib/service/service.go b/lib/service/service.go index eadde605b5b7d..543baacc233a0 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -5761,6 +5761,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error { webConfig := web.Config{ Proxy: tsrv, + HighRateLimiterConfig: cfg.Proxy.WebUnauthenticatedHighRateLimiter, AuthServers: cfg.AuthServerAddresses()[0], ProxyClient: conn.Client, ProxySSHAddr: proxySSHAddr, diff --git a/lib/service/servicecfg/proxy.go b/lib/service/servicecfg/proxy.go index e073de9265d28..cbc4473fbaf05 100644 --- a/lib/service/servicecfg/proxy.go +++ b/lib/service/servicecfg/proxy.go @@ -92,6 +92,11 @@ type ProxyConfig struct { Limiter limiter.Config + // WebUnauthenticatedHighRateLimiter overrides the rate limiter the web API + // applies to unauthenticated endpoints that expect high request rates, + // such as /webapi/ping/:connector. Used only in tests. + WebUnauthenticatedHighRateLimiter *limiter.Config + // PublicAddrs is a list of the public addresses the proxy advertises // for the HTTP endpoint. The hosts in PublicAddr are included in the // list of host principals on the TLS and SSH certificate. diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 1816db70928cc..190d3c343b222 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -373,6 +373,11 @@ type Config struct { // DatabaseREPLRegistry is used for retrieving database REPL. DatabaseREPLRegistry dbrepl.REPLRegistry + + // HighRateLimiterConfig overrides the rate limiter applied to + // unauthenticated endpoints that expect high request rates, such as + // /webapi/ping/:connector. Used only in tests. + HighRateLimiterConfig *limiter.Config } // SetDefaults ensures proper default values are set if @@ -676,7 +681,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { return nil, trace.Wrap(err) } // highLimiter is used for endpoints which are only CPU constrained and require high request rates - h.highLimiter, err = limiter.NewRateLimiter(limiter.Config{ + highLimiterConfig := limiter.Config{ Rates: []limiter.Rate{ { Period: defaults.LimiterHighPeriod, @@ -685,7 +690,11 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { }, }, MaxConnections: defaults.LimiterMaxConnections, - }) + } + if cfg.HighRateLimiterConfig != nil { + highLimiterConfig = *cfg.HighRateLimiterConfig + } + h.highLimiter, err = limiter.NewRateLimiter(highLimiterConfig) if err != nil { return nil, trace.Wrap(err) } diff --git a/tool/tsh/common/kube_shared_fixture_test.go b/tool/tsh/common/kube_shared_fixture_test.go new file mode 100644 index 0000000000000..41ee5e74ef6fc --- /dev/null +++ b/tool/tsh/common/kube_shared_fixture_test.go @@ -0,0 +1,137 @@ +/* + * Teleport + * Copyright (C) 2026 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package common + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" + kubeserver "github.com/gravitational/teleport/lib/kube/proxy/testing/kube_server" + "github.com/gravitational/teleport/lib/limiter" + "github.com/gravitational/teleport/lib/service/servicecfg" + "github.com/gravitational/teleport/lib/services" + "github.com/gravitational/teleport/lib/utils" +) + +// Kube cluster names registered by the fixture. +const ( + sharedRootKubeCluster1 = "root-cluster" + sharedRootKubeCluster2 = "first-cluster" + sharedLeafKubeCluster = "leaf-cluster-some-suffix-added-by-discovery-service" + sharedLeafKubeClusterDiscoveredName = "leaf-cluster" +) + +type kubeFixtureKey struct { + multiplexMode bool +} + +var ( + kubeFixtureMu sync.Mutex + kubeFixtures = map[kubeFixtureKey]*suite{} +) + +// getKubeFixture returns the root+leaf Teleport suite for key, building it at most once per distinct key. +// Bring-up costs ~4-5s and dominates TestKube/TestKubeLogin, whose bodies run in milliseconds, +// so paying it once rather than per iteration is what keeps the package under the timeout at `-count 100`. +func getKubeFixture(t *testing.T, key kubeFixtureKey) *suite { + t.Helper() + kubeFixtureMu.Lock() + defer kubeFixtureMu.Unlock() + if s, ok := kubeFixtures[key]; ok { + if s == nil { + t.Fatalf("shared kube fixture %+v failed to build earlier in this run", key) + } + return s + } + kubeFixtures[key] = nil // Prevent later -count iterations from rebuilding. + + rootLabels := map[string]string{ + "label1": "val1", + "ultra_long_label_for_teleport_kubernetes_service_list_kube_clusters_method": "ultra_long_label_value_for_teleport_kubernetes_service_list_kube_clusters_method", + } + leafLabels := map[string]string{ + "label1": "val1", + "ultra_long_label_for_teleport_kubernetes_service_list_kube_clusters_method": "ultra_long_label_value_for_teleport_kubernetes_service_list_kube_clusters_method", + // mock a discovered kube cluster in the leaf Teleport cluster. + types.DiscoveredNameLabel: sharedLeafKubeClusterDiscoveredName, + } + + s := newTestSuite(t, + withSharedFixture(), + withRootConfigFunc(func(cfg *servicecfg.Config) { + if key.multiplexMode { + cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) + } + cfg.InsecureMode = true + cfg.Proxy.WebUnauthenticatedHighRateLimiter = &limiter.Config{} // no rate limiting + cfg.Kube.Enabled = true + cfg.Kube.ListenAddr = utils.MustParseAddr(localListenerAddr()) + cfg.Kube.KubeconfigPath = newSharedKubeConfigFile(t, sharedRootKubeCluster1, sharedRootKubeCluster2) + cfg.Kube.StaticLabels = rootLabels + cfg.Proxy.Kube.Enabled = true + cfg.Proxy.Kube.ListenAddr = *utils.MustParseAddr(localListenerAddr()) + cfg.SSH.Enabled = false + }), + withLeafCluster(), + withLeafConfigFunc( + func(cfg *servicecfg.Config) { + if key.multiplexMode { + cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) + } + cfg.InsecureMode = true + cfg.Kube.Enabled = true + cfg.Kube.ListenAddr = utils.MustParseAddr(localListenerAddr()) + cfg.Kube.KubeconfigPath = newSharedKubeConfigFile(t, sharedLeafKubeCluster) + cfg.Kube.StaticLabels = leafLabels + cfg.SSH.Enabled = false + }, + ), + withValidationFunc(func(s *suite) bool { + // Wait for cache propagation of the kubernetes resources before proceeding with the tests. + var foundRoot1, foundRoot2, foundLeaf bool + for ks := range s.root.GetAuthServer().UnifiedResourceCache.KubernetesServers(t.Context(), services.UnifiedResourcesIterateParams{}) { + foundRoot1 = foundRoot1 || ks.GetCluster().GetName() == sharedRootKubeCluster1 + foundRoot2 = foundRoot2 || ks.GetCluster().GetName() == sharedRootKubeCluster2 + } + + for ks := range s.leaf.GetAuthServer().UnifiedResourceCache.KubernetesServers(t.Context(), services.UnifiedResourcesIterateParams{}) { + foundLeaf = foundLeaf || ks.GetCluster().GetName() == sharedLeafKubeCluster + } + + return foundRoot1 && foundRoot2 && foundLeaf + }), + ) + + kubeFixtures[key] = s + return s +} + +func newSharedKubeConfigFile(t *testing.T, clusterNames ...string) string { + return buildKubeConfigFile(t, sharedTempDir(t), newSharedKubeSelfSubjectServer, clusterNames...) +} + +func newSharedKubeSelfSubjectServer(t *testing.T) string { + srv, err := kubeserver.NewKubeAPIMock() + require.NoError(t, err) + registerSharedFixtureTeardown(func() { srv.Close() }) + return srv.URL +} diff --git a/tool/tsh/common/kube_test.go b/tool/tsh/common/kube_test.go index 627c2a86b84fe..f8d07de74c713 100644 --- a/tool/tsh/common/kube_test.go +++ b/tool/tsh/common/kube_test.go @@ -59,7 +59,7 @@ import ( ) func TestKube(t *testing.T) { - pack := setupKubeTestPack(t, true) + pack := setupKubeTestPack(t, kubeFixtureKey{multiplexMode: true}) t.Run("list kube", pack.testListKube) t.Run("proxy kube", pack.testProxyKube) t.Run("proxy kube with exec-cmd", pack.testProxyKubeWithExecCmd) @@ -90,14 +90,14 @@ func TestKubeLogin(t *testing.T) { } t.Run("kube login with multiplex mode", func(t *testing.T) { - pack := setupKubeTestPack(t, true /* withMultiplexMode */) + pack := setupKubeTestPack(t, kubeFixtureKey{multiplexMode: true}) webProxyAddr, err := pack.root.ProxyWebAddr() require.NoError(t, err) testKubeLogin(t, pack.rootKubeCluster1, webProxyAddr.String()) }) t.Run("kube login without multiplex mode", func(t *testing.T) { - pack := setupKubeTestPack(t, false /* withMultiplexMode */) + pack := setupKubeTestPack(t, kubeFixtureKey{multiplexMode: false}) proxyAddr, err := pack.root.ProxyKubeAddr() require.NoError(t, err) addr := net.JoinHostPort("localhost", fmt.Sprintf("%d", proxyAddr.Port(defaults.KubeListenPort))) @@ -115,77 +115,22 @@ type kubeTestPack struct { leafKubeCluster string } -func setupKubeTestPack(t *testing.T, withMultiplexMode bool) *kubeTestPack { +// setupKubeTestPack returns a pack over the shared kube suite for key, logging +// the current test in against it. The suite itself is built once per distinct +// key and reused across -count iterations — see getKubeFixture. +func setupKubeTestPack(t *testing.T, key kubeFixtureKey) *kubeTestPack { t.Helper() - ctx := context.Background() - rootKubeCluster1 := "root-cluster" - rootKubeCluster2 := "first-cluster" - // mock a discovered kube cluster name in the leaf Teleport cluster. - leafKubeCluster := "leaf-cluster-some-suffix-added-by-discovery-service" - rootLabels := map[string]string{ - "label1": "val1", - "ultra_long_label_for_teleport_kubernetes_service_list_kube_clusters_method": "ultra_long_label_value_for_teleport_kubernetes_service_list_kube_clusters_method", - } - leafLabels := map[string]string{ - "label1": "val1", - "ultra_long_label_for_teleport_kubernetes_service_list_kube_clusters_method": "ultra_long_label_value_for_teleport_kubernetes_service_list_kube_clusters_method", - // mock a discovered kube cluster in the leaf Teleport cluster. - types.DiscoveredNameLabel: "leaf-cluster", - } - - s := newTestSuite(t, - withRootConfigFunc(func(cfg *servicecfg.Config) { - if withMultiplexMode { - cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) - } - cfg.InsecureMode = true - cfg.Kube.Enabled = true - cfg.Kube.ListenAddr = utils.MustParseAddr(localListenerAddr()) - cfg.Kube.KubeconfigPath = newKubeConfigFile(t, rootKubeCluster1, rootKubeCluster2) - cfg.Kube.StaticLabels = rootLabels - cfg.Proxy.Kube.Enabled = true - cfg.Proxy.Kube.ListenAddr = *utils.MustParseAddr(localListenerAddr()) - cfg.SSH.Enabled = false - }), - withLeafCluster(), - withLeafConfigFunc( - func(cfg *servicecfg.Config) { - if withMultiplexMode { - cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) - } - cfg.InsecureMode = true - cfg.Kube.Enabled = true - cfg.Kube.ListenAddr = utils.MustParseAddr(localListenerAddr()) - cfg.Kube.KubeconfigPath = newKubeConfigFile(t, leafKubeCluster) - cfg.Kube.StaticLabels = leafLabels - cfg.SSH.Enabled = false - }, - ), - withValidationFunc(func(s *suite) bool { - // Wait for cache propagation of the kubernetes resources before proceeding with the tests. - var foundRoot1, foundRoot2, foundLeaf bool - for ks := range s.root.GetAuthServer().UnifiedResourceCache.KubernetesServers(ctx, services.UnifiedResourcesIterateParams{}) { - foundRoot1 = foundRoot1 || ks.GetCluster().GetName() == rootKubeCluster1 - foundRoot2 = foundRoot2 || ks.GetCluster().GetName() == rootKubeCluster2 - } - - for ks := range s.leaf.GetAuthServer().UnifiedResourceCache.KubernetesServers(ctx, services.UnifiedResourcesIterateParams{}) { - foundLeaf = foundLeaf || ks.GetCluster().GetName() == leafKubeCluster - } - - return foundRoot1 && foundRoot2 && foundLeaf - }), - ) + s := getKubeFixture(t, key) mustLoginSetEnvLegacy(t, s) return &kubeTestPack{ suite: s, rootClusterName: s.root.Config.Auth.ClusterName.GetClusterName(), leafClusterName: s.leaf.Config.Auth.ClusterName.GetClusterName(), - rootKubeCluster1: rootKubeCluster1, - rootKubeCluster2: rootKubeCluster2, - leafKubeCluster: leafKubeCluster, + rootKubeCluster1: sharedRootKubeCluster1, + rootKubeCluster2: sharedRootKubeCluster2, + leafKubeCluster: sharedLeafKubeCluster, } } @@ -248,11 +193,11 @@ func (p *kubeTestPack) testListKube(t *testing.T) { table := asciitable.MakeTableWithTruncatedColumn( []string{"Proxy", "Cluster", "Kube Cluster Name", "Labels", "Scope"}, [][]string{ - // "leaf-cluster" should be displayed instead of the + // The discovered name should be displayed instead of the // full leaf cluster name, since it is mocked as a // discovered resource and the discovered resource name // is displayed in non-verbose mode. - {p.root.Config.Proxy.WebAddr.String(), "leaf1", "leaf-cluster", formattedLeafLabels, ""}, + {p.root.Config.Proxy.WebAddr.String(), "leaf1", sharedLeafKubeClusterDiscoveredName, formattedLeafLabels, ""}, {p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster2, formattedRootLabels, ""}, {p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster1, formattedRootLabels, ""}, }, @@ -279,7 +224,7 @@ func (p *kubeTestPack) testListKube(t *testing.T) { args: []string{"--all", "--quiet"}, wantTable: func() string { table := asciitable.MakeHeadlessTable(5) - table.AddRow([]string{p.root.Config.Proxy.WebAddr.String(), "leaf1", "leaf-cluster", formattedLeafLabels, ""}) + table.AddRow([]string{p.root.Config.Proxy.WebAddr.String(), "leaf1", sharedLeafKubeClusterDiscoveredName, formattedLeafLabels, ""}) table.AddRow([]string{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster2, formattedRootLabels, ""}) table.AddRow([]string{p.root.Config.Proxy.WebAddr.String(), "root", p.rootKubeCluster1, formattedRootLabels, ""}) return table.AsBuffer().String() @@ -742,12 +687,14 @@ func TestKubeSelection(t *testing.T) { } func newKubeConfigFile(t *testing.T, clusterNames ...string) string { - tmpDir := t.TempDir() + return buildKubeConfigFile(t, t.TempDir(), newKubeSelfSubjectServer, clusterNames...) +} +func buildKubeConfigFile(t *testing.T, dir string, newServer func(t *testing.T) string, clusterNames ...string) string { kubeConf := clientcmdapi.NewConfig() for _, name := range clusterNames { kubeConf.Clusters[name] = &clientcmdapi.Cluster{ - Server: newKubeSelfSubjectServer(t), + Server: newServer(t), InsecureSkipTLSVerify: true, } kubeConf.AuthInfos[name] = &clientcmdapi.AuthInfo{} @@ -757,7 +704,7 @@ func newKubeConfigFile(t *testing.T, clusterNames ...string) string { AuthInfo: name, } } - kubeConfigLocation := filepath.Join(tmpDir, "kubeconfig") + kubeConfigLocation := filepath.Join(dir, "kubeconfig") err := clientcmd.WriteToFile(*kubeConf, kubeConfigLocation) require.NoError(t, err) return kubeConfigLocation diff --git a/tool/tsh/common/shared_fixture_test.go b/tool/tsh/common/shared_fixture_test.go new file mode 100644 index 0000000000000..4b932e48e6cc2 --- /dev/null +++ b/tool/tsh/common/shared_fixture_test.go @@ -0,0 +1,67 @@ +/* + * Teleport + * Copyright (C) 2026 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package common + +import ( + "os" + "sync" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + sharedFixtureMu sync.Mutex + sharedFixtureTeardowns []func() +) + +func registerSharedFixtureTeardown(fn func()) { + sharedFixtureMu.Lock() + defer sharedFixtureMu.Unlock() + sharedFixtureTeardowns = append(sharedFixtureTeardowns, fn) +} + +// teardownSharedFixtures runs the registered teardowns. +// Called once from TestMain. +func teardownSharedFixtures() { + sharedFixtureMu.Lock() + defer sharedFixtureMu.Unlock() + for i := len(sharedFixtureTeardowns) - 1; i >= 0; i-- { + sharedFixtureTeardowns[i]() + } + sharedFixtureTeardowns = nil +} + +// dataDirFor returns a data dir for a cluster's file config. +func dataDirFor(t *testing.T, shared bool) string { + t.Helper() + if !shared { + return t.TempDir() + } + return sharedTempDir(t) +} + +// sharedTempDir returns a temp dir removed by TestMain. +func sharedTempDir(t *testing.T) string { + t.Helper() + dir, err := os.MkdirTemp("", "tsh-shared-fixture") + require.NoError(t, err) + registerSharedFixtureTeardown(func() { os.RemoveAll(dir) }) + return dir +} diff --git a/tool/tsh/common/tsh_helper_test.go b/tool/tsh/common/tsh_helper_test.go index adef1f2aba66c..633aa47aef356 100644 --- a/tool/tsh/common/tsh_helper_test.go +++ b/tool/tsh/common/tsh_helper_test.go @@ -65,7 +65,7 @@ func (s *suite) setupRootCluster(t *testing.T, options testSuiteOptions) { fileConfig := &config.FileConfig{ Version: "v2", Global: config.Global{ - DataDir: t.TempDir(), + DataDir: dataDirFor(t, options.shared), NodeName: "rootnode", }, SSH: config.SSH{ @@ -156,7 +156,7 @@ func (s *suite) setupRootCluster(t *testing.T, options testSuiteOptions) { options.rootConfigFunc(cfg) } - s.root = runTeleport(t, cfg) + s.root = startTeleport(t, cfg, options.shared) } func (s *suite) setupLeafCluster(t *testing.T, options testSuiteOptions) { @@ -164,7 +164,7 @@ func (s *suite) setupLeafCluster(t *testing.T, options testSuiteOptions) { fileConfig := &config.FileConfig{ Version: "v2", Global: config.Global{ - DataDir: t.TempDir(), + DataDir: dataDirFor(t, options.shared), NodeName: "leafnode", }, SSH: config.SSH{ @@ -254,7 +254,7 @@ func (s *suite) setupLeafCluster(t *testing.T, options testSuiteOptions) { if options.leafConfigFunc != nil { options.leafConfigFunc(cfg) } - s.leaf = runTeleport(t, cfg) + s.leaf = startTeleport(t, cfg, options.shared) _, err = s.leaf.GetAuthServer().UpsertTrustedClusterV2(s.leaf.ExitContext(), tc) require.NoError(t, err) @@ -265,6 +265,7 @@ type testSuiteOptions struct { leafConfigFunc func(cfg *servicecfg.Config) leafCluster bool validationFunc func(*suite) bool + shared bool } type testSuiteOptionFunc func(o *testSuiteOptions) @@ -293,6 +294,13 @@ func withValidationFunc(f func(*suite) bool) testSuiteOptionFunc { } } +// withSharedFixture marks the suite as a long-lived fixture. +func withSharedFixture() testSuiteOptionFunc { + return func(o *testSuiteOptions) { + o.shared = true + } +} + // deprecated: Use `tools/teleport/testenv.MakeTestServer` instead. func newTestSuite(t *testing.T, opts ...testSuiteOptionFunc) *suite { var options testSuiteOptions @@ -305,10 +313,11 @@ func newTestSuite(t *testing.T, opts ...testSuiteOptionFunc) *suite { if options.leafCluster || options.leafConfigFunc != nil { s.setupLeafCluster(t, options) - require.Eventually(t, func() bool { - rt, err := s.root.GetAuthServer().GetTunnelConnections(t.Context(), s.leaf.Config.Auth.ClusterName.GetClusterName()) + ctx := t.Context() + require.EventuallyWithT(t, func(t *assert.CollectT) { + rt, err := s.root.GetAuthServer().GetTunnelConnections(ctx, s.leaf.Config.Auth.ClusterName.GetClusterName()) require.NoError(t, err) - return len(rt) == 1 + require.Len(t, rt, 1) }, 10*time.Second, 100*time.Millisecond) } @@ -322,6 +331,12 @@ func newTestSuite(t *testing.T, opts ...testSuiteOptionFunc) *suite { } func runTeleport(t *testing.T, cfg *servicecfg.Config) *service.TeleportProcess { + return startTeleport(t, cfg, false /* shared */) +} + +// startTeleport starts cfg and waits for its configured services to become ready. +func startTeleport(t *testing.T, cfg *servicecfg.Config, shared bool) *service.TeleportProcess { + t.Helper() if cfg.InstanceMetadataClient == nil { // Disables cloud auto-imported labels when running tests in cloud envs // such as Github Actions. @@ -338,10 +353,19 @@ func runTeleport(t *testing.T, cfg *servicecfg.Config) *service.TeleportProcess process, err := service.NewTeleport(cfg) require.NoError(t, err, trace.DebugReport(err)) require.NoError(t, process.Start()) - t.Cleanup(func() { - require.NoError(t, process.Close()) - require.NoError(t, process.Wait()) - }) + if shared { + // Unlike the t.Cleanup below, this can't assert on Close/Wait. + // It runs from TestMain after tests finished; calling t.Errorf that late panics. + registerSharedFixtureTeardown(func() { + _ = process.Close() + _ = process.Wait() + }) + } else { + t.Cleanup(func() { + require.NoError(t, process.Close()) + require.NoError(t, process.Wait()) + }) + } var serviceReadyEvents []string if cfg.Proxy.Enabled { diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 8e434ccf42b15..00b582ff774a4 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -143,6 +143,7 @@ func TestMain(m *testing.M) { ctx, cancel := context.WithCancel(context.Background()) cryptosuitestest.PrecomputeRSAKeys(ctx) exitCode := m.Run() + teardownSharedFixtures() cancel() os.Exit(exitCode) }