-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Machine ID: Support path-based Kubernetes routing
This adds a new `kubernetes/v2` service to support path-based routing, which allows clients to access an arbitrary number of Kubernetes clusters using a single issued identity. It can be used with `tbot start kubernetes/v2` and specifying one or more explicit clusters with `--kubernetes-cluster-name` or a label selector with `--kubernetes-cluster-labels`.
- Loading branch information
1 parent
f7883aa
commit ca0c4e8
Showing
10 changed files
with
917 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/* | ||
* 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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package cli | ||
|
||
import ( | ||
"fmt" | ||
"log/slog" | ||
"strings" | ||
|
||
"github.com/alecthomas/kingpin/v2" | ||
"github.com/gravitational/trace" | ||
|
||
"github.com/gravitational/teleport/lib/client" | ||
"github.com/gravitational/teleport/lib/tbot/config" | ||
) | ||
|
||
// KubernetesV2Command implements `tbot start kubernetes` and | ||
// `tbot configure kubernetes`. | ||
type KubernetesV2Command struct { | ||
*sharedStartArgs | ||
*sharedDestinationArgs | ||
*genericMutatorHandler | ||
|
||
DisableExecPlugin bool | ||
KubernetesClusterNames []string | ||
|
||
// KubernetesClusterLabels contains a list of strings representing label | ||
// selectors. Each entry generates one selector, but may contain several | ||
// comma-separated strings to match multiple labels at once. | ||
KubernetesClusterLabels []string | ||
} | ||
|
||
// NewKubernetesCommand initializes the command and flags for kubernetes outputs | ||
// and returns a struct to contain the parse result. | ||
func NewKubernetesV2Command(parentCmd *kingpin.CmdClause, action MutatorAction, mode CommandMode) *KubernetesV2Command { | ||
cmd := parentCmd.Command("kubernetes/v2", fmt.Sprintf("%s tbot with a Kubernetes V2 output.", mode)).Alias("k8s/v2") | ||
|
||
c := &KubernetesV2Command{} | ||
c.sharedStartArgs = newSharedStartArgs(cmd) | ||
c.sharedDestinationArgs = newSharedDestinationArgs(cmd) | ||
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action) | ||
|
||
cmd.Flag("disable-exec-plugin", "If set, disables the exec plugin. This allows credentials to be used without the `tbot` binary.").BoolVar(&c.DisableExecPlugin) | ||
cmd.Flag("kubernetes-cluster-name", "An explicit Kubernetes cluster name to include. Repeatable.").StringsVar(&c.KubernetesClusterNames) | ||
cmd.Flag("kubernetes-cluster-labels", "A set of Kubernetes labels to match in k1=v1,k2=v2 form. Repeatable.").StringsVar(&c.KubernetesClusterLabels) | ||
|
||
// Note: excluding roles; the bot will fetch all available in CLI mode. | ||
|
||
return c | ||
} | ||
|
||
// parseLabelSelectorString parses a string containing key/value pairs in | ||
// "k1=v1,k2=v2" form into a Kubernetes selector. | ||
func parseLabelSelectorString(selectors string) (*config.KubernetesSelector, error) { | ||
labels := map[string]string{} | ||
|
||
for _, selector := range strings.Split(selectors, ",") { | ||
k, v, found := strings.Cut(selector, "=") | ||
if !found { | ||
return nil, trace.BadParameter("invalid selector: %s", selector) | ||
} | ||
|
||
labels[k] = v | ||
} | ||
|
||
return &config.KubernetesSelector{ | ||
Labels: labels, | ||
}, nil | ||
} | ||
|
||
func (c *KubernetesV2Command) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error { | ||
if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
dest, err := c.BuildDestination() | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
selectors := []*config.KubernetesSelector{} | ||
for _, name := range c.KubernetesClusterNames { | ||
selectors = append(selectors, &config.KubernetesSelector{ | ||
Name: name, | ||
}) | ||
} | ||
for _, s := range c.KubernetesClusterLabels { | ||
labels, err := client.ParseLabelSpec(s) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
selectors = append(selectors, &config.KubernetesSelector{ | ||
Labels: labels, | ||
}) | ||
} | ||
|
||
cfg.Services = append(cfg.Services, &config.KubernetesV2Output{ | ||
Destination: dest, | ||
DisableExecPlugin: c.DisableExecPlugin, | ||
Selectors: selectors, | ||
}) | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
/* | ||
* 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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
package config | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/gravitational/teleport/lib/tbot/bot" | ||
"github.com/gravitational/trace" | ||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
var ( | ||
_ ServiceConfig = &KubernetesV2Output{} | ||
_ Initable = &KubernetesV2Output{} | ||
) | ||
|
||
const KubernetesV2OutputType = "kubernetes/v2" | ||
|
||
// KubernetesOutput produces credentials which can be used to connect to a | ||
// Kubernetes Cluster through teleport. | ||
type KubernetesV2Output struct { | ||
// Destination is where the credentials should be written to. | ||
Destination bot.Destination `yaml:"destination"` | ||
|
||
// Roles is the list of roles to request for the generated credentials. | ||
// If empty, it defaults to all the bot's roles. | ||
Roles []string `yaml:"roles,omitempty"` | ||
|
||
// DisableExecPlugin disables the default behavior of using `tbot` as a | ||
// `kubectl` credentials exec plugin. This is useful in environments where | ||
// `tbot` may not exist on the system that will consume the outputted | ||
// kubeconfig. It does mean that kubectl will not be able to automatically | ||
// refresh the credentials within an individual invocation. | ||
DisableExecPlugin bool `yaml:"disable_exec_plugin,omitempty"` | ||
|
||
// Selectors is a list of selectors for path-based routing. Multiple | ||
// selectors can be used to generate an output containing all matches. | ||
Selectors []*KubernetesSelector `yaml:"selectors,omitempty"` | ||
} | ||
|
||
func (o *KubernetesV2Output) CheckAndSetDefaults() error { | ||
if err := validateOutputDestination(o.Destination); err != nil { | ||
return trace.Wrap(err) | ||
} | ||
|
||
if len(o.Selectors) == 0 { | ||
return trace.BadParameter("at least one selector must be provided") | ||
} | ||
|
||
for _, s := range o.Selectors { | ||
if err := s.CheckAndSetDefaults(); err != nil { | ||
return trace.Wrap(err) | ||
} | ||
} | ||
|
||
return trace.Wrap(o.Destination.CheckAndSetDefaults()) | ||
} | ||
|
||
func (o *KubernetesV2Output) GetDestination() bot.Destination { | ||
return o.Destination | ||
} | ||
|
||
func (o *KubernetesV2Output) Init(ctx context.Context) error { | ||
return trace.Wrap(o.Destination.Init(ctx, []string{})) | ||
} | ||
|
||
func (o *KubernetesV2Output) Describe() []FileDescription { | ||
// Based on tbot.KubernetesOutputService.Render | ||
return []FileDescription{ | ||
{ | ||
Name: "kubeconfig.yaml", | ||
}, | ||
{ | ||
Name: IdentityFilePath, | ||
}, | ||
{ | ||
Name: HostCAPath, | ||
}, | ||
} | ||
} | ||
|
||
func (o *KubernetesV2Output) MarshalYAML() (interface{}, error) { | ||
type raw KubernetesV2Output | ||
return withTypeHeader((*raw)(o), KubernetesV2OutputType) | ||
} | ||
|
||
func (o *KubernetesV2Output) UnmarshalYAML(node *yaml.Node) error { | ||
dest, err := extractOutputDestination(node) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
// Alias type to remove UnmarshalYAML to avoid recursion | ||
type raw KubernetesV2Output | ||
if err := node.Decode((*raw)(o)); err != nil { | ||
return trace.Wrap(err) | ||
} | ||
o.Destination = dest | ||
return nil | ||
} | ||
|
||
func (o *KubernetesV2Output) Type() string { | ||
return KubernetesV2OutputType | ||
} | ||
|
||
// KubernetesSelector allows querying for a Kubernetes cluster to include either | ||
// by its name or labels. | ||
type KubernetesSelector struct { | ||
Name string `yaml:"name,omitempty"` | ||
|
||
Labels map[string]string `yaml:"labels,omitempty"` | ||
} | ||
|
||
func (s *KubernetesSelector) CheckAndSetDefaults() error { | ||
if s.Name == "" && len(s.Labels) == 0 { | ||
return trace.BadParameter("selectors: one of 'name' and 'labels' must be specified") | ||
} | ||
|
||
if s.Name != "" && len(s.Labels) > 0 { | ||
return trace.BadParameter("selectors: only one of 'name' and 'labels' may be specified") | ||
} | ||
|
||
if s.Labels == nil { | ||
s.Labels = map[string]string{} | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.