diff --git a/docs/pages/reference/machine-id/configuration.mdx b/docs/pages/reference/machine-id/configuration.mdx index 9f416df9f74f9..2c2b789f4dadc 100644 --- a/docs/pages/reference/machine-id/configuration.mdx +++ b/docs/pages/reference/machine-id/configuration.mdx @@ -381,6 +381,15 @@ renewal_interval: 15m # invocation of `kubectl`. Defaults to `false`. disable_exec_plugin: false +# context_name_template determines the format of context names in the generated +# kubeconfig. It is a Go template string that supports the following variables: +# +# - {{.ClusterName}} - Name of the Teleport cluster +# - {{.KubeName}} - Name of the Kubernetes cluster resource +# +# By default, the following template will be used: "{{.ClusterName}}-{{.KubeName}}" +context_name_template: "{{.KubeName}}" + # name optionally overrides the name of the service used in logs and the `/readyz` # endpoint. It must only contain letters, numbers, hyphens, underscores, and plus # symbols. diff --git a/lib/tbot/services/k8s/argocd_output_config.go b/lib/tbot/services/k8s/argocd_output_config.go index cf201fce6caca..e4eeb513b095f 100644 --- a/lib/tbot/services/k8s/argocd_output_config.go +++ b/lib/tbot/services/k8s/argocd_output_config.go @@ -32,7 +32,7 @@ import ( const ArgoCDOutputServiceType = "kubernetes/argo-cd" -var defaultArgoClusterNameTemplate = kubeconfig.ContextName("{{.ClusterName}}", "{{.KubeName}}") +var defaultContextNameTemplate = kubeconfig.ContextName("{{.ClusterName}}", "{{.KubeName}}") // ArgoCDOutputConfig contains configuration for the service that registers // Kubernetes cluster credentials in Argo CD. @@ -81,7 +81,7 @@ type ArgoCDOutputConfig struct { // when Namespaces is non-empty). ClusterResources bool `yaml:"cluster_resources,omitempty"` - // cluster_name_template determines the format of cluster names in Argo CD. + // ClusterNameTemplate determines the format of cluster names in Argo CD. // It is a "text/template" string that supports the following variables: // // - {{.ClusterName}} - Name of the Teleport cluster @@ -136,7 +136,7 @@ func (o *ArgoCDOutputConfig) CheckAndSetDefaults() error { } if o.ClusterNameTemplate == "" { - o.ClusterNameTemplate = defaultArgoClusterNameTemplate + o.ClusterNameTemplate = defaultContextNameTemplate } else { if _, err := kubeconfig.ContextNameFromTemplate(o.ClusterNameTemplate, "", ""); err != nil { return trace.BadParameter("cluster_name_template is invalid: %v", err) diff --git a/lib/tbot/services/k8s/output_v2.go b/lib/tbot/services/k8s/output_v2.go index 0ba5aacbf9fa2..a06a8dd8cf018 100644 --- a/lib/tbot/services/k8s/output_v2.go +++ b/lib/tbot/services/k8s/output_v2.go @@ -357,7 +357,7 @@ func (s *OutputV2Service) render( if s.cfg.DisableExecPlugin { // If they've disabled the exec plugin, we just write the credentials // directly into the kubeconfig. - kubeCfg, err = generateKubeConfigV2WithoutPlugin(status) + kubeCfg, err = s.generateKubeConfigV2WithoutPlugin(status) if err != nil { return trace.Wrap(err) } @@ -369,7 +369,7 @@ func (s *OutputV2Service) render( return trace.Wrap(err) } - kubeCfg, err = generateKubeConfigV2WithPlugin(status, destinationDir.Path, executablePath) + kubeCfg, err = s.generateKubeConfigV2WithPlugin(status, destinationDir.Path, executablePath) if err != nil { return trace.Wrap(err) } @@ -400,7 +400,7 @@ func encodePathComponent(input string) string { // generateKubeConfigWithPlugin creates a Kubernetes config object with the // given cluster config, using the `tbot kube credentials` auth helper plugin to // fetch refreshed certificate data at runtime. -func generateKubeConfigV2WithPlugin(ks *kubernetesStatusV2, destPath string, executablePath string) (*clientcmdapi.Config, error) { +func (o *OutputV2Service) generateKubeConfigV2WithPlugin(ks *kubernetesStatusV2, destPath string, executablePath string) (*clientcmdapi.Config, error) { config := clientcmdapi.NewConfig() // Implementation note: tsh/kube.go generates a kubeconfig with all @@ -442,7 +442,10 @@ func generateKubeConfigV2WithPlugin(ks *kubernetesStatusV2, destPath string, exe } for i, cluster := range ks.kubernetesClusterNames { - contextName := kubeconfig.ContextName(ks.teleportClusterName, cluster) + contextName, err := kubeconfig.ContextNameFromTemplate(o.cfg.ContextNameTemplate, ks.teleportClusterName, cluster) + if err != nil { + return nil, trace.Wrap(err, "templating context name") + } suffix := fmt.Sprintf("/v1/teleport/%s/%s", encodePathComponent(ks.teleportClusterName), encodePathComponent(cluster)) config.Clusters[contextName] = &clientcmdapi.Cluster{ @@ -476,7 +479,7 @@ func generateKubeConfigV2WithPlugin(ks *kubernetesStatusV2, destPath string, exe return config, nil } -func generateKubeConfigV2WithoutPlugin(ks *kubernetesStatusV2) (*clientcmdapi.Config, error) { +func (o *OutputV2Service) generateKubeConfigV2WithoutPlugin(ks *kubernetesStatusV2) (*clientcmdapi.Config, error) { config := clientcmdapi.NewConfig() // Configure the cluster. @@ -496,7 +499,10 @@ func generateKubeConfigV2WithoutPlugin(ks *kubernetesStatusV2) (*clientcmdapi.Co } for i, cluster := range ks.kubernetesClusterNames { - contextName := kubeconfig.ContextName(ks.teleportClusterName, cluster) + contextName, err := kubeconfig.ContextNameFromTemplate(o.cfg.ContextNameTemplate, ks.teleportClusterName, cluster) + if err != nil { + return nil, trace.Wrap(err, "templating context name") + } suffix := fmt.Sprintf("/v1/teleport/%s/%s", encodePathComponent(ks.teleportClusterName), encodePathComponent(cluster)) config.Clusters[contextName] = &clientcmdapi.Cluster{ diff --git a/lib/tbot/services/k8s/output_v2_config.go b/lib/tbot/services/k8s/output_v2_config.go index b385d184acd0e..10d86cc1b0970 100644 --- a/lib/tbot/services/k8s/output_v2_config.go +++ b/lib/tbot/services/k8s/output_v2_config.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/trace" "gopkg.in/yaml.v3" + "github.com/gravitational/teleport/lib/kube/kubeconfig" "github.com/gravitational/teleport/lib/tbot/bot" "github.com/gravitational/teleport/lib/tbot/bot/destination" "github.com/gravitational/teleport/lib/tbot/internal" @@ -57,6 +58,16 @@ type OutputV2Config struct { // CredentialLifetime contains configuration for how long credentials will // last and the frequency at which they'll be renewed. CredentialLifetime bot.CredentialLifetime `yaml:",inline"` + + // ContextNameTemplate determines the format of context names in the + // generated kubeconfig. It is a "text/template" string that supports the + // following variables: + // + // - {{.ClusterName}} - Name of the Teleport cluster + // - {{.KubeName}} - Name of the Kubernetes cluster resource + // + // By default, the following template will be used: "{{.ClusterName}}-{{.KubeName}}". + ContextNameTemplate string `yaml:"context_name_template,omitempty"` } // GetName returns the user-given name of the service, used for validation purposes. @@ -82,6 +93,14 @@ func (o *OutputV2Config) CheckAndSetDefaults() error { } } + if o.ContextNameTemplate == "" { + o.ContextNameTemplate = defaultContextNameTemplate + } else { + if _, err := kubeconfig.ContextNameFromTemplate(o.ContextNameTemplate, "", ""); err != nil { + return trace.BadParameter("context_name_template is invalid: %v", err) + } + } + return trace.Wrap(o.Destination.CheckAndSetDefaults()) } diff --git a/lib/tbot/services/k8s/output_v2_config_test.go b/lib/tbot/services/k8s/output_v2_config_test.go index b848f1db5cb81..f739473be8a8f 100644 --- a/lib/tbot/services/k8s/output_v2_config_test.go +++ b/lib/tbot/services/k8s/output_v2_config_test.go @@ -56,6 +56,7 @@ func TestKubernetesV2Output_YAML(t *testing.T) { TTL: 1 * time.Minute, RenewalInterval: 30 * time.Second, }, + ContextNameTemplate: "{{.KubeName}}", }, }, { @@ -84,6 +85,7 @@ func TestKubernetesV2Output_CheckAndSetDefaults(t *testing.T) { Selectors: []*KubernetesSelector{ {Name: "foo", Labels: map[string]string{}}, }, + ContextNameTemplate: "{{.KubeName}}", } }, }, @@ -97,6 +99,7 @@ func TestKubernetesV2Output_CheckAndSetDefaults(t *testing.T) { "foo": "bar", }}, }, + ContextNameTemplate: "{{.KubeName}}", } }, }, @@ -108,6 +111,7 @@ func TestKubernetesV2Output_CheckAndSetDefaults(t *testing.T) { Selectors: []*KubernetesSelector{ {Name: "foo"}, }, + ContextNameTemplate: "{{.KubeName}}", } }, wantErr: "no destination configured for output", @@ -116,7 +120,8 @@ func TestKubernetesV2Output_CheckAndSetDefaults(t *testing.T) { name: "missing selectors", in: func() *OutputV2Config { return &OutputV2Config{ - Destination: destination.NewMemory(), + Destination: destination.NewMemory(), + ContextNameTemplate: "{{.KubeName}}", } }, wantErr: "at least one selector must be provided", @@ -129,6 +134,7 @@ func TestKubernetesV2Output_CheckAndSetDefaults(t *testing.T) { Selectors: []*KubernetesSelector{ {}, }, + ContextNameTemplate: "{{.KubeName}}", } }, wantErr: "selectors: one of 'name' and 'labels' must be specified", @@ -146,10 +152,24 @@ func TestKubernetesV2Output_CheckAndSetDefaults(t *testing.T) { }, }, }, + ContextNameTemplate: "{{.KubeName}}", } }, wantErr: "selectors: only one of 'name' and 'labels' may be specified", }, + { + name: "invalid context_name_template", + in: func() *OutputV2Config { + return &OutputV2Config{ + Destination: destination.NewMemory(), + Selectors: []*KubernetesSelector{ + {Name: "foo", Labels: map[string]string{}}, + }, + ContextNameTemplate: "{{.InvalidVariable}}", + } + }, + wantErr: "can't evaluate field InvalidVariable", + }, } testCheckAndSetDefaults(t, tests) } diff --git a/lib/tbot/services/k8s/testdata/TestKubernetesV2Output_YAML/full.golden b/lib/tbot/services/k8s/testdata/TestKubernetesV2Output_YAML/full.golden index f7101f4a1d59e..79551f169be41 100644 --- a/lib/tbot/services/k8s/testdata/TestKubernetesV2Output_YAML/full.golden +++ b/lib/tbot/services/k8s/testdata/TestKubernetesV2Output_YAML/full.golden @@ -9,3 +9,4 @@ selectors: default_namespace: foo-namespace credential_ttl: 1m0s renewal_interval: 30s +context_name_template: '{{.KubeName}}'