diff --git a/lib/tbot/cli/start_kubernetes_v2_test.go b/lib/tbot/cli/start_kubernetes_v2_test.go
new file mode 100644
index 0000000000000..291df25720e2e
--- /dev/null
+++ b/lib/tbot/cli/start_kubernetes_v2_test.go
@@ -0,0 +1,85 @@
+/*
+ * Teleport
+ * Copyright (C) 2024 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 cli
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/gravitational/teleport/lib/tbot/config"
+)
+
+// TestKubernetesV2Command tests that the KubernetesCommand properly parses its
+// arguments and applies as expected onto a BotConfig.
+func TestKubernetesV2Command(t *testing.T) {
+ testStartConfigureCommand(t, NewKubernetesV2Command, []startConfigureTestCase{
+ {
+ name: "success",
+ args: []string{
+ "start",
+ "kubernetes/v2",
+ "--destination=/bar",
+ "--token=foo",
+ "--join-method=github",
+ "--proxy-server=example.com:443",
+ "--disable-exec-plugin",
+ "--kubernetes-cluster-name=a",
+ "--kubernetes-cluster-name=b",
+ "--kubernetes-cluster-labels=c=\"foo bar\",d=\"baz qux\"",
+ },
+ assertConfig: func(t *testing.T, cfg *config.BotConfig) {
+ require.Len(t, cfg.Services, 1)
+
+ // It must configure a kubernetes output with a directory destination.
+ svc := cfg.Services[0]
+ k8s, ok := svc.(*config.KubernetesV2Output)
+ require.True(t, ok)
+
+ require.True(t, k8s.DisableExecPlugin)
+
+ dir, ok := k8s.Destination.(*config.DestinationDirectory)
+ require.True(t, ok)
+ require.Equal(t, "/bar", dir.Path)
+
+ var foundA, foundB, foundLabelSelector bool
+ for _, selector := range k8s.Selectors {
+ switch selector.Name {
+ case "a":
+ foundA = true
+ case "b":
+ foundB = true
+ case "":
+ require.Equal(t, map[string]string{
+ "c": "foo bar",
+ "d": "baz qux",
+ }, selector.Labels)
+ foundLabelSelector = true
+ default:
+ require.Fail(t, "unexpected selector name %q", selector.Name)
+ }
+ }
+
+ require.True(t, foundA, "name selector 'a' must exist")
+ require.True(t, foundB, "name selector 'b' must exist")
+ require.True(t, foundLabelSelector, "label selector must exist")
+ },
+ },
+ })
+}
diff --git a/lib/tbot/service_kubernetes_output.go b/lib/tbot/service_kubernetes_output.go
index a4b25b1cc98c2..c93f7a188dfcd 100644
--- a/lib/tbot/service_kubernetes_output.go
+++ b/lib/tbot/service_kubernetes_output.go
@@ -391,7 +391,7 @@ func chooseOneKubeCluster(clusters []types.KubeCluster, name string) (types.Kube
return chooseOneResource(clusters, name, "kubernetes cluster")
}
-func getKubeCluster(ctx context.Context, clt *authclient.Client, name string) (types.KubeCluster, error) {
+func getKubeCluster(ctx context.Context, clt apiclient.GetResourcesClient, name string) (types.KubeCluster, error) {
ctx, span := tracer.Start(ctx, "getKubeCluster")
defer span.End()
diff --git a/lib/tbot/service_kubernetes_v2_output.go b/lib/tbot/service_kubernetes_v2_output.go
index cc18a89be87b0..a6175a59ffc37 100644
--- a/lib/tbot/service_kubernetes_v2_output.go
+++ b/lib/tbot/service_kubernetes_v2_output.go
@@ -30,6 +30,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/auth/authclient"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/kube/kubeconfig"
@@ -142,10 +143,12 @@ func (s *KubernetesV2OutputService) generate(ctx context.Context) error {
clusterNames = append(clusterNames, c.GetName())
}
+ clusterNames = utils.Deduplicate(clusterNames)
+
s.log.InfoContext(
ctx,
"Generated identity for Kubernetes access",
- "matched_cluster_count", len(clusters),
+ "matched_cluster_count", len(clusterNames),
"identity", describeTLSIdentity(ctx, s.log, id),
)
@@ -163,18 +166,6 @@ func (s *KubernetesV2OutputService) generate(ctx context.Context) error {
if err != nil {
return trace.Wrap(err)
}
- // TODO(noah): It's likely the Kubernetes output does not really need to
- // output these CAs - but - for backwards compat reasons, we output them.
- // Revisit this at a later date and make a call.
- // TODO(tim): If we don't need these, we'll drop them from V2.
- // userCAs, err := s.botAuthClient.GetCertAuthorities(ctx, types.UserCA, false)
- // if err != nil {
- // return trace.Wrap(err)
- // }
- // databaseCAs, err := s.botAuthClient.GetCertAuthorities(ctx, types.DatabaseCA, false)
- // if err != nil {
- // return trace.Wrap(err)
- // }
keyRing, err := NewClientKeyRing(id, hostCAs)
if err != nil {
@@ -204,7 +195,7 @@ type kubernetesStatusV2 struct {
// queryKubeClustersByLabels fetches a list of Kubernetes clusters matching the
// given label selector.
-func queryKubeClustersByLabels(ctx context.Context, clt *authclient.Client, labels map[string]string) ([]types.KubeCluster, error) {
+func queryKubeClustersByLabels(ctx context.Context, clt apiclient.GetResourcesClient, labels map[string]string) ([]types.KubeCluster, error) {
ctx, span := tracer.Start(ctx, "queryKubeClustersByLabels")
defer span.End()
@@ -227,7 +218,7 @@ func queryKubeClustersByLabels(ctx context.Context, clt *authclient.Client, labe
// fetchAllMatchingKubeClusters returns a list of all clusters matching the
// given selectors.
-func fetchAllMatchingKubeClusters(ctx context.Context, clt *authclient.Client, selectors []*config.KubernetesSelector) ([]types.KubeCluster, error) {
+func fetchAllMatchingKubeClusters(ctx context.Context, clt apiclient.GetResourcesClient, selectors []*config.KubernetesSelector) ([]types.KubeCluster, error) {
ctx, span := tracer.Start(ctx, "findAllMatchingKubeClusters")
defer span.End()
@@ -281,6 +272,7 @@ func (s *KubernetesV2OutputService) render(
); err != nil {
return trace.Wrap(err, "persisting identity")
}
+
// In exec plugin mode, we write the credentials to disk and write a
// kubeconfig that execs `tbot` to load those credentials.
diff --git a/lib/tbot/service_kubernetes_v2_output_test.go b/lib/tbot/service_kubernetes_v2_output_test.go
new file mode 100644
index 0000000000000..ec74afc5cf999
--- /dev/null
+++ b/lib/tbot/service_kubernetes_v2_output_test.go
@@ -0,0 +1,309 @@
+/*
+ * Teleport
+ * Copyright (C) 2025 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 tbot
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ apiclient "github.com/gravitational/teleport/api/client"
+ "github.com/gravitational/teleport/api/client/proto"
+ "github.com/gravitational/teleport/api/types"
+ apiutils "github.com/gravitational/teleport/api/utils"
+ "github.com/gravitational/teleport/lib/client"
+ "github.com/gravitational/teleport/lib/services"
+ "github.com/gravitational/teleport/lib/tbot/botfs"
+ "github.com/gravitational/teleport/lib/tbot/config"
+ "github.com/gravitational/teleport/lib/tbot/identity"
+ "github.com/gravitational/teleport/lib/utils"
+ "github.com/gravitational/teleport/lib/utils/golden"
+ "github.com/gravitational/trace"
+ "github.com/stretchr/testify/require"
+)
+
+// fakeKubeServerClient provides a minimal implementation of
+// ServerWithRoles.ListResources() that applies real filters to a known set of
+// resources. Useful for testing against more complex queries.
+type fakeKubeServerClient struct {
+ apiclient.GetResourcesClient
+
+ availableKubeServers []types.KubeServer
+}
+
+func (f fakeKubeServerClient) GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) {
+ var matched []types.KubeServer
+
+ filter := services.MatchResourceFilter{
+ ResourceKind: req.ResourceType,
+ Labels: req.Labels,
+ SearchKeywords: req.SearchKeywords,
+ }
+
+ if req.PredicateExpression != "" {
+ expression, err := services.NewResourceExpression(req.PredicateExpression)
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+ filter.PredicateExpression = expression
+ }
+
+ for _, server := range f.availableKubeServers {
+ switch match, err := services.MatchResourceByFilters(server, filter, nil /* ignore dup matches */); {
+ case err != nil:
+ return nil, trace.Wrap(err)
+ case match:
+ matched = append(matched, server)
+ }
+ }
+
+ out := make([]*proto.PaginatedResource, 0, len(matched))
+ for _, match := range matched {
+ server, ok := match.(*types.KubernetesServerV3)
+ if !ok {
+ return nil, trace.BadParameter("invalid type %T, expected *types.KubernetesServerV3", server)
+ }
+ out = append(out, &proto.PaginatedResource{Resource: &proto.PaginatedResource_KubernetesServer{KubernetesServer: server}})
+ }
+
+ return &proto.ListResourcesResponse{Resources: out}, nil
+}
+
+// newTestKubeServer creates a minimal KubeServer appropriate for use with
+// `fakeKubeServerClient`.
+func newTestKubeServer(t *testing.T, name string, labels map[string]string) types.KubeServer {
+ t.Helper()
+
+ server, err := types.NewKubernetesServerV3(types.Metadata{
+ Name: name,
+ Labels: labels,
+ }, types.KubernetesServerSpecV3{
+ HostID: name,
+ Cluster: &types.KubernetesClusterV3{
+ Metadata: types.Metadata{
+ Name: name,
+ Labels: labels,
+ },
+ },
+ })
+ require.NoError(t, err)
+
+ return server
+}
+
+func TestKubernetesV2OutputService_fetch(t *testing.T) {
+ servers := []types.KubeServer{
+ newTestKubeServer(t, "a", map[string]string{}),
+ newTestKubeServer(t, "b", map[string]string{"foo": "1"}),
+ newTestKubeServer(t, "c", map[string]string{"foo": "1", "bar": "2"}),
+ newTestKubeServer(t, "d", map[string]string{"bar": "2"}),
+ }
+
+ client := &fakeKubeServerClient{
+ availableKubeServers: servers,
+ }
+
+ tests := []struct {
+ name string
+ selectors []*config.KubernetesSelector
+ expectError require.ErrorAssertionFunc
+ expectedClusterNames []string
+ }{
+ {
+ name: "matches by name",
+ selectors: []*config.KubernetesSelector{
+ {
+ Name: "a",
+ },
+ {
+ Name: "c",
+ },
+ },
+ expectedClusterNames: []string{"a", "c"},
+ },
+ {
+ name: "errors when direct lookup fails",
+ selectors: []*config.KubernetesSelector{
+ {
+ Name: "nonexistent",
+ },
+ },
+ expectError: func(tt require.TestingT, err error, i ...interface{}) {
+ require.ErrorContains(t, err, "unable to fetch cluster \"nonexistent\" by name")
+ },
+ },
+ {
+ name: "matches with simple label selector",
+ selectors: []*config.KubernetesSelector{
+ {
+ Labels: map[string]string{
+ "foo": "1",
+ },
+ },
+ },
+ expectedClusterNames: []string{"b", "c"},
+ },
+ {
+ name: "matches with complex label selector",
+ selectors: []*config.KubernetesSelector{
+ {
+ Labels: map[string]string{
+ "foo": "1",
+ "bar": "2",
+ },
+ },
+ },
+ expectedClusterNames: []string{"c"},
+ },
+ {
+ name: "matches with multiple selectors",
+ selectors: []*config.KubernetesSelector{
+ {
+ Labels: map[string]string{
+ "foo": "1",
+ },
+ },
+ {
+ Labels: map[string]string{
+ "bar": "2",
+ },
+ },
+ {
+ Name: "a",
+ },
+ },
+ expectedClusterNames: []string{"a", "b", "c", "d"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ matches, err := fetchAllMatchingKubeClusters(context.Background(), client, tt.selectors)
+ if tt.expectError != nil {
+ tt.expectError(t, err)
+ } else {
+ require.NoError(t, err)
+
+ var names []string
+ for _, match := range matches {
+ names = append(names, match.GetName())
+ }
+
+ // `generate()` dedupes downstream, so we'll replicate that
+ // here, otherwise we might see duplicates if some label
+ // selectors overlap.
+ names = apiutils.Deduplicate(names)
+
+ require.ElementsMatch(t, tt.expectedClusterNames, names)
+ }
+ })
+ }
+}
+
+// TestKubernetesV2OutputService_render renders the Kubernetes template and
+// compares it to the saved golden result.
+func TestKubernetesV2OutputService_render(t *testing.T) {
+ // We need a fixed cert/key pair here for the golden files testing
+ // to behave properly.
+ id := &identity.Identity{
+ PrivateKeyBytes: keyPEM,
+ TLSCertBytes: tlsCert,
+ ClusterName: mockClusterName,
+ }
+
+ tests := []struct {
+ name string
+ useRelativePath bool
+ disableExecPlugin bool
+ }{
+ {
+ name: "absolute path",
+ },
+ {
+ name: "relative path",
+ useRelativePath: true,
+ },
+ {
+ name: "exec plugin disabled",
+ disableExecPlugin: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ dir := t.TempDir()
+
+ dest := &config.DestinationDirectory{
+ Path: dir,
+ Symlinks: botfs.SymlinksInsecure,
+ ACLs: botfs.ACLOff,
+ }
+ if tt.useRelativePath {
+ wd, err := os.Getwd()
+ require.NoError(t, err)
+ relativePath, err := filepath.Rel(wd, dir)
+ require.NoError(t, err)
+ dest.Path = relativePath
+ }
+
+ svc := KubernetesV2OutputService{
+ cfg: &config.KubernetesV2Output{
+ DisableExecPlugin: tt.disableExecPlugin,
+ Destination: dest,
+ },
+ executablePath: fakeGetExecutablePath,
+ log: utils.NewSlogLoggerForTests(),
+ }
+
+ keyRing, err := NewClientKeyRing(
+ id,
+ []types.CertAuthority{fakeCA(t, types.HostCA, mockClusterName)},
+ )
+ require.NoError(t, err)
+ status := &kubernetesStatusV2{
+ kubernetesClusterNames: []string{"a", "b", "c"},
+ teleportClusterName: mockClusterName,
+ tlsServerName: client.GetKubeTLSServerName(mockClusterName),
+ credentials: keyRing,
+ clusterAddr: fmt.Sprintf("https://%s:443", mockClusterName),
+ }
+
+ err = svc.render(
+ context.Background(),
+ status,
+ id,
+ []types.CertAuthority{fakeCA(t, types.HostCA, mockClusterName)},
+ )
+ require.NoError(t, err)
+
+ kubeconfigBytes, err := os.ReadFile(filepath.Join(dir, defaultKubeconfigPath))
+ require.NoError(t, err)
+ kubeconfigBytes = bytes.ReplaceAll(kubeconfigBytes, []byte(dir), []byte("/test/dir"))
+
+ if golden.ShouldSet() {
+ golden.SetNamed(t, "kubeconfig.yaml", kubeconfigBytes)
+ }
+ require.Equal(
+ t, string(golden.GetNamed(t, "kubeconfig.yaml")), string(kubeconfigBytes),
+ )
+ })
+ }
+}
diff --git a/lib/tbot/testdata/TestKubernetesV2OutputService_render/absolute_path/kubeconfig.yaml.golden b/lib/tbot/testdata/TestKubernetesV2OutputService_render/absolute_path/kubeconfig.yaml.golden
new file mode 100644
index 0000000000000..12f013ddf6d1b
--- /dev/null
+++ b/lib/tbot/testdata/TestKubernetesV2OutputService_render/absolute_path/kubeconfig.yaml.golden
@@ -0,0 +1,45 @@
+apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/YQ
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-a
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/Yg
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-b
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/Yw
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-c
+contexts:
+- context:
+ cluster: tele.blackmesa.gov-a
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-a
+- context:
+ cluster: tele.blackmesa.gov-b
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-b
+- context:
+ cluster: tele.blackmesa.gov-c
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-c
+current-context: tele.blackmesa.gov-a
+kind: Config
+preferences: {}
+users:
+- name: tele.blackmesa.gov
+ user:
+ exec:
+ apiVersion: client.authentication.k8s.io/v1beta1
+ args:
+ - kube
+ - credentials
+ - --destination-dir=/test/dir
+ command: /path/to/tbot
+ env: null
+ provideClusterInfo: false
diff --git a/lib/tbot/testdata/TestKubernetesV2OutputService_render/exec_plugin_disabled/kubeconfig.yaml.golden b/lib/tbot/testdata/TestKubernetesV2OutputService_render/exec_plugin_disabled/kubeconfig.yaml.golden
new file mode 100644
index 0000000000000..c70f37cdad66d
--- /dev/null
+++ b/lib/tbot/testdata/TestKubernetesV2OutputService_render/exec_plugin_disabled/kubeconfig.yaml.golden
@@ -0,0 +1,38 @@
+apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/YQ
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-a
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/Yg
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-b
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/Yw
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-c
+contexts:
+- context:
+ cluster: tele.blackmesa.gov-a
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-a
+- context:
+ cluster: tele.blackmesa.gov-b
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-b
+- context:
+ cluster: tele.blackmesa.gov-c
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-c
+current-context: tele.blackmesa.gov-a
+kind: Config
+preferences: {}
+users:
+- name: tele.blackmesa.gov
+ user:
+ client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURmRENDQW1TZ0F3SUJBZ0lRUC9qSTg1c3FqRFdEVGQ5cUYwVjVxakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB5TkRBME1ESXhOREE1TWpaYUZ3MHlOREEwTURJeE5URXdNalphCk1ITXhHekFaQmdOVkJBa1RFblJsYkdVdVlteGhZMnR0WlhOaExtZHZkakVOTUFzR0ExVUVFUk1FYm5Wc2JERVIKTUE4R0ExVUVBeE1JWW05MExYUmxjM1F4RGpBTUJnVXJ6ZzhCQVJNRFptOXZNUTR3REFZRks4NFBBUUlUQTJKaApjakVTTUJBR0JTdk9Ed0VERXdkbGVHRnRjR3hsTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCCkNnS0NBUUVBcjhXZkVET3ExVE4wYlQwU0dFdEV1RHJSYWYrVnVkbWJ5cEhva2V3eTQ2bWQ5WEIzZ1FXYmluOU4KLzV0eU5kYkZzV3NEZ0RJeVhQM1ViZTB1YmNQWWxjc0NOdGdDdks0cWQzUnlSdnhZNWxPZlMxcFpFU1BFdHZPLwpzeEV1NkUzTzBvZmN3cTR1S2VuSHVmMUVVUXVWRDZXeEFCVU9hT3MyLzNhYWhtWXk0U25LTlVzTTIvbDFYcmNJCjBla3ZCMGgxMG5YVUM0VkpTNHNLR3pHelRoRDMwOGlhL2JnRFNYYzBmaVV3WlBCNVRMbjdsU2N1aXNpKzhKU3MKcVdjY2tuWG9uR0VFdGlzbTdGTmkrbXNlVjFhaHpqRWJSTS9rZkZ3WjBIK2VrejNDZG5zbWtORDBGbXhCOVdUZgo1UHdHOG9YTTQyUUprd3VFSXUrOFEvVlZTU0ZlNHdJREFRQUJvejh3UFRBT0JnTlZIUThCQWY4RUJBTUNCYUF3CkhRWURWUjBsQkJZd0ZBWUlLd1lCQlFVSEF3RUdDQ3NHQVFVRkJ3TUNNQXdHQTFVZEV3RUIvd1FDTUFBd0RRWUoKS29aSWh2Y05BUUVMQlFBRGdnRUJBRVpZeklTMHR4K1luK2NFZlM4M2hwTDFqRUxxOUM4VjNQVEM2eTQ0a2pBSwppODVNeCtyaWRZcTBkZE1kVi85MUpaNXQxUnFkZTRMU1lVVlVQNnEvVWtpaDBteC9JM3oyU2l3cC9ZanBPdTZ4ClNaWGFrakdudDVwQ0NhL3QxTmNWTlNycVFwTFE4YkowbXJ1UmlOYXdyS28zL2dlNTdyZ2lkbkJGY092M3pWNXQKQklrWWpRSksyWVZZY0pqSjY4b2xYenBDRXc1aFRaOWZ3NDJmczJUT1JISHJQTlVBelBmM1FkcnppMDNocnZoZApwNm5ZTkExOGI2Z2d5Z2tjUlUyTVJ0ZTVFKy8yQUJZaXdSa2x3SldOSU1RSkpIeVpBVzhEeXdzMEUyZStCM3crCjcvU1lQeWVLT2x0N2ZvRDd6MFhNMkt3OW5kdXJKcTk3QVdma2Nuclp3YlE9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=
+ client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBcjhXZkVET3ExVE4wYlQwU0dFdEV1RHJSYWYrVnVkbWJ5cEhva2V3eTQ2bWQ5WEIzCmdRV2JpbjlOLzV0eU5kYkZzV3NEZ0RJeVhQM1ViZTB1YmNQWWxjc0NOdGdDdks0cWQzUnlSdnhZNWxPZlMxcFoKRVNQRXR2Ty9zeEV1NkUzTzBvZmN3cTR1S2VuSHVmMUVVUXVWRDZXeEFCVU9hT3MyLzNhYWhtWXk0U25LTlVzTQoyL2wxWHJjSTBla3ZCMGgxMG5YVUM0VkpTNHNLR3pHelRoRDMwOGlhL2JnRFNYYzBmaVV3WlBCNVRMbjdsU2N1CmlzaSs4SlNzcVdjY2tuWG9uR0VFdGlzbTdGTmkrbXNlVjFhaHpqRWJSTS9rZkZ3WjBIK2VrejNDZG5zbWtORDAKRm14QjlXVGY1UHdHOG9YTTQyUUprd3VFSXUrOFEvVlZTU0ZlNHdJREFRQUJBb0lCQUN2Yjh2blcrcHlpYnozRwp6Rm9WaGZzMmFnUzZDc0ZLSkU2aW85YXRpbkUyWkx6V3FHc2dYQlJ0K2FkN1FUOWY3UXA5T20xbG1SMk5GTkd0CktqV25kY2JDMWpXYkp1dXZ4ZGJ5em9VWitKRFljdG9abkRuam8vVkcweUc2ZXVycVoxNHZHbzNWYXAxNHdTYU8KcE5wWU9vU2lBbzJUczNuSW4zdVZPNitubHJDS0ZCV2lJTU12SjRMODl2U0NYeUkvNWtwd0NVUnhwWWVZSnJRYgpKemk0VHNOMFZpWXFmNlhOZmhSQ1NEK0ZrOWttMmU2enNQSXVXeWZQcnRHeDFjQTJVZUFqbmpLSzlJUzlxa0F5CjYzMlQ2M1g2TTVrV3RpcklITS9yL0liZFNqK2x4R0NNcW5zU2dOWUlMUXY1c05ramdqd2xrS2wxODJ3NWwzQ1oKVGtqeUxQa0NnWUVBMGd1aHMzS0JxTTRBU0R6UUkzK2g1R1RHQTg3SVRiMEIvVGNpdEVsUFErdTNQTHlwd3o5dQpLelM5QkhNcGFXUFdJT0p6WVJBamI4QkRsZHlDY0FobXlyNWx2N08vZXpSbWdHRDlOUFY2NklFNUFYOW5LVk5TClBoSlROaVlQU0g3ZzR6TzNLNHNkNTM5N1l1blJaekF4Z3NWV3p1N0UyZ1VySlNDc2Ywbkw3N1VDZ1lFQTFqcGcKbUpiU0dZVnJZRXlRbXQ2WWpIbndjTGlOVEpEUGJjbjI3ZzBMbWNSYnE5U2ZJdmEvSWZaYWYwcnU4YjljOFFOTQpXTWFnNTdXR1FnaFMyQjc2OThHdmx3RituS1hYWkRqQ1o0ejYrRWZpL1Q1dUhMNVZEY3BIRTR5cWRaRzQyaFRXCm0rSzR3bDRaNkI1MHhHQy9tSmx4ekI2amU0cUJNOVpzbjh3U3d6Y0NnWUVBazVPMGt2NGE5MTExZVV1dythQU4KUVFsRXp4d1VRL3BPVVhqUm0xWCtxVHdPVEZCSi9uS3NseExBMDBXT2pRdW1RUWlhQkZKd2MyM2tqb0NWN04wYQpTOHltZEtCNElycFlZazdDMk5pNCtHOENmSGpsSkhYMFRNUlhUcTVEQXE2U2wwK1luTEZyMjJFSWNpRFNEZXdnCmZUN2xsUkxSb0ZVTlVWSzVuOTFidWhrQ2dZRUF3cVVFQTJCMXdRNkNnMXJOd0lram5lOWxVV1c5cktXZWNxaWcKbmFab3RldTlSeURHL3FPbkFocXVHeDVnZ0hKWTVmc1RNVTQ0QUkva1RyYjFYcnkzVnNrNjJ6OVdaTW9pTEVPTwpEenYvQS90OCtJL3l5RmIvUEtwZmJobk8vMGZKNXd3citqTkRvQWFVRDEwc3h3a0l6SVFPNjJHak5LcWh2aEhEClhHVzFYbjBDZ1lFQXpVTEFJOWlqL3E1UytHTXlZajF4TENVNHF4QnBVLzA0bkUrUGZTU21mdjhNYTM0dWg0UU0KblJEY1pIQnFaWU5SRHQ1ek52UlRqZ3dKaTRpSEd3U0IrRDRTSVlHYjBpb1RJMk1PUzJGN3pCRHlsOEZYcDdkVAozVFNLcm9ud29XWW9MU2lzcW5uL3M4aU4xTTlSSkE5cHlJeTdGVFZ3cTU5WEwzTm9ldElTdUtjPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==
diff --git a/lib/tbot/testdata/TestKubernetesV2OutputService_render/relative_path/kubeconfig.yaml.golden b/lib/tbot/testdata/TestKubernetesV2OutputService_render/relative_path/kubeconfig.yaml.golden
new file mode 100644
index 0000000000000..12f013ddf6d1b
--- /dev/null
+++ b/lib/tbot/testdata/TestKubernetesV2OutputService_render/relative_path/kubeconfig.yaml.golden
@@ -0,0 +1,45 @@
+apiVersion: v1
+clusters:
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/YQ
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-a
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/Yg
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-b
+- cluster:
+ certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLakNDQWhLZ0F3SUJBZ0lRSnRKREpaWkJrZy9hZk04ZDJaSkNUakFOQmdrcWhraUc5dzBCQVFzRkFEQkEKTVJVd0V3WURWUVFLRXd4VVpXeGxjRzl5ZENCUFUxTXhKekFsQmdOVkJBTVRIblJsYkdWd2IzSjBMbXh2WTJGcwphRzl6ZEM1c2IyTmhiR1J2YldGcGJqQWVGdzB4TnpBMU1Ea3hPVFF3TXpaYUZ3MHlOekExTURjeE9UUXdNelphCk1FQXhGVEFUQmdOVkJBb1RERlJsYkdWd2IzSjBJRTlUVXpFbk1DVUdBMVVFQXhNZWRHVnNaWEJ2Y25RdWJHOWoKWVd4b2IzTjBMbXh2WTJGc1pHOXRZV2x1TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQwpBUUVBdUtGTGFmMmlJSS94RFIrbTJZajZQblVFYStxenF3eHNkTFVqbnVuRlphQVhHK2habTRNbDgwU0NpQmdJCmdUSFFsSnlMSWtUdHVSb0g1YWVNeXoxRVJVQ3RpaTRac1RxRHJqalV5YnhQNHIrNEhWWDZtMzRzNmh3RXI4RmkKZnRzOXBNcDRpUzN0UWd1UmMyOGdQZERvL1Q2VnJKVFZZVWZVVXNORFJ0SXJsQjVPOWlncXFMbnVhWTllcUdpNApQVXgwRzB3UllKcFJ5d29qOEcwSWtwZlFUaVgrQ0FDN2R0NXdzN1pybkdxQ05CTEdpNWJHc2FNbXB0VmJzU0VwCjFUZW5udEY1NFYxaVI0OUlWNUpxRGhtMVMwSG1rbGVvSnpLZGMrNnNQL3hOZXB6OVBKenVGOWQ5TnViVExXZ0IKc0syOFlJdGNtV0hkSFhEL09EeFZhZWhSandJREFRQUJveUF3SGpBT0JnTlZIUThCQWY4RUJBTUNCNEF3REFZRApWUjBUQVFIL0JBSXdBREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBQVZVNnNOQmRqNzZzYUh3T3hHU2RuRXFRCm8ydE11UjNtc1NNNEY2d0ZLMlVrS2Vwc0Q3Q1lJZi9Qek5TTlVxQTVKSUVVVmVNcUd5aUh1QWJVNEM2NTVuVDEKSXlKWDFELytyNzNzU3A1amJJcFFtMnhvUUdabmo2Zy9LbHR3OE9TT0F3K0RzTUYvUExWcW9XSnAwN3U2ZXcvbQpOeFdzSktjWjVrK3E0ZU14Y2k5bUtSSEhxc3F1V0tYelFsVVJNTkZJK21HYUZ3cktNNGRtemFSMEJFYytpbFN4ClFxVXZRNzRzbXNMSyt6aE5pa21namxHQzVvYjlnOFhraFZBa0pNQWgycmI5b25ETmlSbDY4aUFnY3pQODhtWHUKdk4vbzk4ZHlwenNQeFhtdzZ0a0RxSVJQVUFVYmg0NjVybFk1c0tNbVJnWGkyclVmbC9RVjVuYm96VW8vSFE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0t
+ server: https://tele.blackmesa.gov:443/v1/teleport/dGVsZS5ibGFja21lc2EuZ292/Yw
+ tls-server-name: kube-teleport-proxy-alpn.tele.blackmesa.gov
+ name: tele.blackmesa.gov-c
+contexts:
+- context:
+ cluster: tele.blackmesa.gov-a
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-a
+- context:
+ cluster: tele.blackmesa.gov-b
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-b
+- context:
+ cluster: tele.blackmesa.gov-c
+ user: tele.blackmesa.gov
+ name: tele.blackmesa.gov-c
+current-context: tele.blackmesa.gov-a
+kind: Config
+preferences: {}
+users:
+- name: tele.blackmesa.gov
+ user:
+ exec:
+ apiVersion: client.authentication.k8s.io/v1beta1
+ args:
+ - kube
+ - credentials
+ - --destination-dir=/test/dir
+ command: /path/to/tbot
+ env: null
+ provideClusterInfo: false