Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions lib/service/servicecfg/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 11 additions & 2 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
Expand Down
137 changes: 137 additions & 0 deletions tool/tsh/common/kube_shared_fixture_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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
}
91 changes: 19 additions & 72 deletions tool/tsh/common/kube_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)))
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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, ""},
},
Expand All @@ -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()
Expand Down Expand Up @@ -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{}
Expand All @@ -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
Expand Down
Loading
Loading