-
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.
Browse files
Browse the repository at this point in the history
* 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`. * Added tests for new Kubernetes selector behavior * Fix failing tests * Fix test failure after rebase * Rename CLI parameters to match workload ID selector syntax * Fix broken test after flag rename * Remove unused function * Remove user-configurable roles from kubernetes/v2
- Loading branch information
1 parent
843d512
commit 8c3b25a
Showing
17 changed files
with
1,419 additions
and
2 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,104 @@ | ||
/* | ||
* 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" | ||
|
||
"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("name-selector", "An explicit Kubernetes cluster name to include. Repeatable.").StringsVar(&c.KubernetesClusterNames) | ||
cmd.Flag("label-selector", "A set of Kubernetes labels to match in k1=v1,k2=v2 form. Repeatable.").StringsVar(&c.KubernetesClusterLabels) | ||
|
||
return c | ||
} | ||
|
||
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, | ||
}) | ||
} | ||
|
||
if len(selectors) == 0 { | ||
return trace.BadParameter("at least one name-selector or label-selector must be provided") | ||
} | ||
|
||
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
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", | ||
"--name-selector=a", | ||
"--name-selector=b", | ||
"--label-selector=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") | ||
}, | ||
}, | ||
}) | ||
} |
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
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,157 @@ | ||
/* | ||
* 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/trace" | ||
"gopkg.in/yaml.v3" | ||
|
||
"github.com/gravitational/teleport/lib/tbot/bot" | ||
) | ||
|
||
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"` | ||
|
||
// 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 | ||
} | ||
|
||
func (s *KubernetesSelector) UnmarshalYAML(value *yaml.Node) error { | ||
// A custom unmarshaler so Labels is consistently initialized to not-nil. | ||
// Primarily needed for tests. | ||
type temp KubernetesSelector | ||
out := temp{ | ||
Labels: make(map[string]string), | ||
} | ||
|
||
if err := value.Decode(&out); err != nil { | ||
return err | ||
} | ||
|
||
*s = KubernetesSelector(out) | ||
return nil | ||
} |
Oops, something went wrong.