Machine ID: Refactor tbot CLI (#47130)

* Machine ID: Refactor `tbot` CLI

This significantly refactors the tbot CLI. In addition to a major
overhaul of our CLI config handling, it also exposes a number of new
subcommands for starting more than just an identity output via pure
CLI.

* Move new CLI handling into lib/tbot/cli; add all new subcommands

This moves the new CLI handling code into `lib/tbot/cli`, splits
`cli.go` into several better organized files, and adds all missing
subcommands for major tbot functionality.

* Add sane global handling, convert most other commands to new style

This introduces a dedicated global args struct and removes CLIConf.
As most other non-start (and configure) commands depended on that
effectively global namespace, this refactors all of them to use the
new style with properly namespaced command structs.

This also introduces a `genericExecutorHandler` helper to simplify
subcommands that run an arbitrary action without modifying the
config.

This also solves a number of longstanding CLI handling bugs resulting
from the shared arg namespace. A few args were moved to globals that
should always have been exposed, so now things like `--log-format`
will work consistently.

A few commands were left unconverted since they don't make use of
any of the global args or config loading machinery. They could be
converted easily but are technically simpler as written. We may
opt to convert them, or to do so later.

Some headway on testing, but more work is needed.

* First batch of tests

This adds the first batch of tests on the new CLI along with a bunch
of associated changes made while fixing these tests and others.

This also splits off 2 new flag helpers: LegacyDestinationDirArgs and
AuthProxyArgs. These are reusable embeddable structs to handle
legacy-style --destination-dir behavior, and modern --auth-server
and --proxy-server. `tbot init` and its tests had a subtle dependency
old-style `--destination-dir` handling so we can now reuse that.

There may be other subcommands that require something similar.

* Remove outdated TODO

* Restore TestConfigCLIOnlySample

* Fix lints

* Fix missing embedded flag init

* Code review feedback

* Add docstrings

* Add unit tests for all "start" commands and globals

Also includes a few minor fixes discovered in testing - DiagAddr
was not handled properly in shared args, and removed an unused field.

* Fix imports

* Add helper for repetitive test

* Fix command name

* Adjust behaviour of `tbot kube credentials` command

* Add tests for remaining commands

* Appease linter

* Add godocs

---------

Co-authored-by: Noah Stride <noah.stride@goteleport.com>
This commit is contained in:
Tim Buckley
2024-10-11 08:22:34 +00:00
committed by GitHub
co-authored by Noah Stride
parent 4d3f741ed3
commit 83f10035ff
47 changed files with 3215 additions and 602 deletions
+231
View File
@@ -0,0 +1,231 @@
/*
* 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 (
"log/slog"
"strings"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/tbot/config"
logutils "github.com/gravitational/teleport/lib/utils/log"
)
const (
// AuthServerEnvVar is the environment variable that overrides the
// configured auth server address.
AuthServerEnvVar = "TELEPORT_AUTH_SERVER"
// TokenEnvVar is the environment variable that overrides the configured
// bot token name.
TokenEnvVar = "TELEPORT_BOT_TOKEN"
// ProxyServerEnvVar is the environment variable that overrides the
// configured proxy server address.
ProxyServerEnvVar = "TELEPORT_PROXY"
)
var log = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentTBot)
// CommandRunner defines a contract for `TryRun` that allows commands to
// either execute (possibly returning an error), or pass execution to the next
// command candidate.
type CommandRunner interface {
TryRun(cmd string) (match bool, err error)
}
// MutatorAction is an action that is called by a config mutator-style command.
type MutatorAction func(mutator ConfigMutator) error
// genericMutatorHandler supplies a generic `TryRun` that works for all commands
// that - broadly - load config, mutate that config, and run an action. It's
// meant to be embedded within a command struct to provide the `TryRun`
// implementation.
type genericMutatorHandler struct {
cmd *kingpin.CmdClause
mutator ConfigMutator
action MutatorAction
}
// newGenericMutatorHandler creates a new generic genericMutatorHandler that
// provides a generic `TryRun` implementation.
func newGenericMutatorHandler(cmd *kingpin.CmdClause, mutator ConfigMutator, action MutatorAction) *genericMutatorHandler {
return &genericMutatorHandler{
cmd: cmd,
mutator: mutator,
action: action,
}
}
func (g *genericMutatorHandler) TryRun(cmd string) (match bool, err error) {
switch cmd {
case g.cmd.FullCommand():
err = g.action(g.mutator)
default:
return false, nil
}
return true, trace.Wrap(err)
}
// ConfigMutator is an interface that can apply changes to a BotConfig.
type ConfigMutator interface {
ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error
}
// genericExecutorHandler is a helper that can be embedded to provide a simpler
// TryRun implementation that just runs a function. These functions can be
// passed in while building the CLI to more easily glue behaviors together, or
// specified directly.
type genericExecutorHandler[T any] struct {
cmd *kingpin.CmdClause
args *T
// actions is a list of functions to run when `TryRun` matches the `cmd`.
// Generally at most one action should be exposed to the top level glue in
// main, but commands might want to inject some handler logic for e.g.
// flag migrations.
actions []func(*T) error
}
// newGenericExecutorHandler creates a genericExecutorHandler with the given
// command and action to execute when that command is matched.
func newGenericExecutorHandler[T any](cmd *kingpin.CmdClause, args *T, actions ...func(*T) error) *genericExecutorHandler[T] {
return &genericExecutorHandler[T]{
cmd: cmd,
args: args,
actions: actions,
}
}
func (e *genericExecutorHandler[T]) TryRun(cmd string) (match bool, err error) {
switch cmd {
case e.cmd.FullCommand():
for _, action := range e.actions {
err = action(e.args)
if err != nil {
break
}
}
default:
return false, nil
}
return true, trace.Wrap(err)
}
func applyMutators(l *slog.Logger, cfg *config.BotConfig, mutators ...ConfigMutator) error {
for _, mutator := range mutators {
if mutator == nil {
continue
}
if err := mutator.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
}
return nil
}
// LoadConfigWithMutators builds a config from an optional config file and a CLI
// mutator. If an empty path is provided, an empty base config is used. The CLI
// mutator may override or append to the loaded configuration, if any. The
// GlobalArgs will be applied as a mutator, and `CheckAndSetDefaults()` will be
// called on the end result.
func LoadConfigWithMutators(globals *GlobalArgs, mutators ...ConfigMutator) (*config.BotConfig, error) {
var cfg *config.BotConfig
var err error
if globals.staticConfigYAML != "" {
cfg, err = config.ReadConfig(strings.NewReader(globals.staticConfigYAML), false)
if err != nil {
return nil, trace.Wrap(err)
}
} else if globals.ConfigPath != "" {
cfg, err = config.ReadConfigFromFile(globals.ConfigPath, false)
if err != nil {
return nil, trace.Wrap(err, "loading bot config from path %s", globals.ConfigPath)
}
} else {
cfg = &config.BotConfig{}
}
mutatorsWithGlobals := append([]ConfigMutator{globals}, mutators...)
l := log.With("config_path", globals.ConfigPath)
if err := applyMutators(l, cfg, mutatorsWithGlobals...); err != nil {
return nil, trace.Wrap(err)
}
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return cfg, nil
}
// BaseConfigWithMutators returns a base bot config with the given CLI mutators
// applied. `CheckAndSetDefaults()` will be called on the result. This is useful
// for explicitly _not_ loading a config file, like in `tbot configure ...`
func BaseConfigWithMutators(mutators ...ConfigMutator) (*config.BotConfig, error) {
cfg := &config.BotConfig{}
if err := applyMutators(log, cfg, mutators...); err != nil {
return nil, trace.Wrap(err)
}
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err)
}
return cfg, nil
}
// RemainingArgsList is a custom kingpin parser that consumes all remaining
// arguments.
type RemainingArgsList []string
func (r *RemainingArgsList) Set(value string) error {
*r = append(*r, value)
return nil
}
func (r *RemainingArgsList) String() string {
return strings.Join([]string(*r), " ")
}
func (r *RemainingArgsList) IsCumulative() bool {
return true
}
// RemainingArgs returns a list of remaining arguments for the given command.
func RemainingArgs(s kingpin.Settings) (target *[]string) {
target = new([]string)
s.SetValue((*RemainingArgsList)(target))
return
}
// KingpinClause allows commands and flags to mount to either the root app
// (kingpin.Application) or a subcommand (kingpin.CmdClause)
type KingpinClause interface {
Command(name string, help string) *kingpin.CmdClause
Flag(name string, help string) *kingpin.FlagClause
}
+169
View File
@@ -0,0 +1,169 @@
/*
* 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 (
"log/slog"
"testing"
"github.com/alecthomas/kingpin/v2"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/utils"
)
func buildMinimalKingpinApp(subcommandName string) (app *kingpin.Application, subcommand *kingpin.CmdClause) {
app = utils.InitCLIParser("tbot", "test").Interspersed(false)
subcommand = app.Command(subcommandName, "subcommand")
return
}
func TestConfigCLIOnlySample(t *testing.T) {
// Test the sample config generated by `tctl bots add ...`
legacy := &LegacyCommand{
LegacyDestinationDirArgs: &LegacyDestinationDirArgs{
DestinationDir: "/tmp/foo",
},
AuthProxyArgs: NewStaticAuthServer("auth.example.com"),
Token: "foo",
CAPins: []string{"abc123"},
DiagAddr: "127.0.0.1:1337",
JoinMethod: string(types.JoinMethodToken),
}
cfg, err := LoadConfigWithMutators(&GlobalArgs{
Debug: true,
}, legacy)
require.NoError(t, err)
require.Equal(t, legacy.AuthServer, cfg.AuthServer)
require.NotNil(t, cfg.Onboarding)
token, err := cfg.Onboarding.Token()
require.NoError(t, err)
require.Equal(t, legacy.Token, token)
require.Equal(t, legacy.CAPins, cfg.Onboarding.CAPins)
// Storage is still default
storageImpl, ok := cfg.Storage.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, config.GetDefaultStoragePath(), storageImpl.Path)
// A single default Destination should exist
require.Len(t, cfg.Services, 1)
output := cfg.Services[0]
identOutput, ok := output.(*config.IdentityOutput)
require.True(t, ok)
destImpl := identOutput.GetDestination()
require.NoError(t, err)
destImplReal, ok := destImpl.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, legacy.DestinationDir, destImplReal.Path)
require.True(t, cfg.Debug)
require.Equal(t, legacy.DiagAddr, cfg.DiagAddr)
}
type basicCommand interface {
TryRun(cmd string) (match bool, err error)
}
type testCommandCase[O any] struct {
name string
args []string
assert func(t *testing.T, output O)
}
func testCommand[T basicCommand](
t *testing.T,
newCommand func(parentCommand KingpinClause, action func(T) error) T,
testCases []testCommandCase[T],
) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
app, _ := buildMinimalKingpinApp("sub")
actionCalled := false
var actionCalledOutput T
cmd := newCommand(app, func(output T) error {
actionCalled = true
actionCalledOutput = output
return nil
})
command, err := app.Parse(tt.args)
require.NoError(t, err)
match, err := cmd.TryRun(command)
require.NoError(t, err)
require.True(t, match)
require.True(t, actionCalled)
tt.assert(t, actionCalledOutput)
})
}
}
type startConfigureCommand interface {
ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error
basicCommand
}
type startConfigureTestCase struct {
name string
args []string
assertConfig func(t *testing.T, cfg *config.BotConfig)
}
func testStartConfigureCommand[T startConfigureCommand](
t *testing.T,
newCommand func(parentCmd *kingpin.CmdClause, action MutatorAction) T,
testCases []startConfigureTestCase,
) {
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
app, subcommand := buildMinimalKingpinApp("start")
actionCalled := false
cmd := newCommand(subcommand, func(mut ConfigMutator) error {
actionCalled = true
return nil
})
command, err := app.Parse(tt.args)
require.NoError(t, err)
match, err := cmd.TryRun(command)
require.NoError(t, err)
require.True(t, match)
require.True(t, actionCalled)
// Convert these args to a BotConfig and check it.
cfg, err := LoadConfigWithMutators(&GlobalArgs{}, cmd)
require.NoError(t, err)
tt.assertConfig(t, cfg)
})
}
}
+68
View File
@@ -0,0 +1,68 @@
/*
* 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 "context"
// DBCommand contains fields for `tbot db`
type DBCommand struct {
*genericExecutorHandler[DBCommand]
DestinationDir string
Cluster string
ProxyServer string
// LegacyProxy is the legacy --proxy flag.
// TODO(timothyb89): DELETE IN 17.0.0
// TODO(timothyb89): Or maybe remove in this PR.
LegacyProxyFlag string
RemainingArgs *[]string
}
// NewDBCommand initializes flags for `tbot db`
func NewDBCommand(app KingpinClause, action func(*DBCommand) error) *DBCommand {
cmd := app.Command("db", "Execute database commands through tsh.")
c := &DBCommand{}
c.genericExecutorHandler = newGenericExecutorHandler(cmd, c, func(c *DBCommand) error {
// Prepend an action to handle --proxy deprecation.
if c.LegacyProxyFlag != "" {
c.ProxyServer = c.LegacyProxyFlag
log.WarnContext(context.TODO(), "The --proxy flag is deprecated and will be removed in v17.0.0. Use --proxy-server instead")
}
return nil
}, action)
// We're migrating from --proxy to --proxy-server so this flag is hidden
// but still supported.
// TODO(strideynet): DELETE IN 17.0.0
cmd.Flag("proxy", "The Teleport proxy server to use, in host:port form.").Hidden().Envar(ProxyServerEnvVar).StringVar(&c.LegacyProxyFlag)
cmd.Flag("proxy-server", "The Teleport proxy server to use, in host:port form.").StringVar(&c.ProxyServer)
cmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&c.DestinationDir)
cmd.Flag("cluster", "The cluster name. Extracted from the certificate if unset.").StringVar(&c.Cluster)
c.RemainingArgs = RemainingArgs(cmd.Arg(
"args",
"Arguments to `tsh db ...`; prefix with `-- ` to ensure flags are passed correctly.",
))
return c
}
+48
View File
@@ -0,0 +1,48 @@
/*
* 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"
)
func TestDBCommand(t *testing.T) {
testCommand(t, NewDBCommand, []testCommandCase[*DBCommand]{
{
name: "success",
args: []string{
"db",
"--proxy-server=example.com:22",
"--destination-dir=/tmp",
"--cluster=example.com",
"bar",
"buzz",
"boo",
},
assert: func(t *testing.T, got *DBCommand) {
require.Equal(t, "example.com:22", got.ProxyServer)
require.Equal(t, "/tmp", got.DestinationDir)
require.Equal(t, "example.com", got.Cluster)
require.Equal(t, []string{"bar", "buzz", "boo"}, *got.RemainingArgs)
},
},
})
}
+110
View File
@@ -0,0 +1,110 @@
/*
* 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 (
"log/slog"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/utils"
)
// GlobalArgs is a set of arguments additionally passed to all command handlers.
// Some subset of these may be applied to configuration, and a ConfigMutator is
// implemented to handle them; other fields may be referenced ad hoc.
type GlobalArgs struct {
// FIPS instructs `tbot` to run in a mode designed to comply with FIPS
// regulations. This means the bot should:
// - Refuse to run if not compiled with boringcrypto
// - Use FIPS relevant endpoints for cloud providers (e.g AWS)
// - Restrict TLS / SSH cipher suites and TLS version
// - RSA2048 or ECDSA with NIST-P256 curve should be used for private key generation
FIPS bool
// ConfigPath is a path to a YAML configuration file to load, if any.
ConfigPath string
// Debug enables debug-level logging, when set
Debug bool
// LogFormat configures the output format of the logger
LogFormat string
// Trace indicates whether tracing should be enabled.
Trace bool
// TraceExporter is a manually provided URI to send traces to instead of
// forwarding them to the Auth service.
TraceExporter string
// Insecure instructs `tbot` to trust the Auth Server without verifying the CA.
Insecure bool
// staticConfigYAML allows tests to specify a configuration file statically
staticConfigYAML string
}
// NewGlobalArgs appends global flags to the application and returns a struct
// that will be populated at parse time.
func NewGlobalArgs(app *kingpin.Application) *GlobalArgs {
g := &GlobalArgs{}
app.Flag("debug", "Verbose logging to stdout.").Short('d').BoolVar(&g.Debug)
app.Flag("config", "Path to a configuration file.").Short('c').StringVar(&g.ConfigPath)
app.Flag("fips", "Runs tbot in FIPS compliance mode. This requires the FIPS binary is in use.").BoolVar(&g.FIPS)
app.Flag("trace", "Capture and export distributed traces.").Hidden().BoolVar(&g.Trace)
app.Flag("trace-exporter", "An OTLP exporter URL to send spans to.").Hidden().StringVar(&g.TraceExporter)
app.Flag("insecure", "Insecure configures the bot to trust the certificates from the Auth Server or Proxy on first connect without verification. Do not use in production.").BoolVar(&g.Insecure)
app.Flag("log-format", "Controls the format of output logs. Can be `json` or `text`. Defaults to `text`.").
Default(utils.LogFormatText).
EnumVar(&g.LogFormat, utils.LogFormatJSON, utils.LogFormatText)
return g
}
// NewGlobalArgsWithStaticConfig creates a new GlobalArgs instance with a static
// YAML config. This can be used in tests to preload a config file without
// writing to the filesystem. Note that this only works for codepaths that
// make use of `LoadConfigWithMutators`.
func NewGlobalArgsWithStaticConfig(staticYAML string) *GlobalArgs {
return &GlobalArgs{
staticConfigYAML: staticYAML,
}
}
func (g *GlobalArgs) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
// Note: g.ConfigPath is not checked here; the config must have already been
// loaded.
if g.FIPS {
cfg.FIPS = g.FIPS
}
if g.Debug {
cfg.Debug = g.Debug
}
if g.Insecure {
cfg.Insecure = true
}
return nil
}
+65
View File
@@ -0,0 +1,65 @@
/*
* 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"
)
// TestGlobals tests that GlobalArgs initialize and parse properly, and mutate
// BotConfig as expected.
func TestGlobalArgs(t *testing.T) {
app, _ := buildMinimalKingpinApp("start")
globals := NewGlobalArgs(app)
// Note: various flags here are already tested as part of sharedStartArgs.
_, err := app.Parse([]string{
"start",
"--debug",
"--config=foo.yaml",
"--fips",
"--trace",
"--trace-exporter=foo",
"--insecure",
"--log-format=json",
})
require.NoError(t, err)
require.True(t, globals.Debug)
require.True(t, globals.FIPS)
require.True(t, globals.Insecure)
require.True(t, globals.Trace)
require.Equal(t, "foo", globals.TraceExporter)
require.Equal(t, "foo.yaml", globals.ConfigPath)
// Clear the config path, otherwise LoadConfigWithMutators will try to load
// it.
globals.ConfigPath = ""
// Convert these args to a BotConfig and check it. Globals don't set many
// config flags, so not much to check here.
cfg, err := LoadConfigWithMutators(globals)
require.NoError(t, err)
require.True(t, cfg.Debug)
require.True(t, cfg.FIPS)
require.True(t, cfg.Insecure)
}
+75
View File
@@ -0,0 +1,75 @@
/*
* 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 (
"log/slog"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/config"
)
// InitCommand implements a command for `tbot init`
type InitCommand struct {
*AuthProxyArgs
*LegacyDestinationDirArgs
*genericExecutorHandler[InitCommand]
Owner string
BotUser string
ReaderUser string
InitDir string
Clean bool
}
// NewInitCommand constructs an InitCommand at the top level of the given
// application. It will execute `action` when selected by the user.
func NewInitCommand(app KingpinClause, action func(*InitCommand) error) *InitCommand {
cmd := app.Command("init", "Initialize a certificate destination directory for writes from a separate bot user.")
c := &InitCommand{}
c.AuthProxyArgs = newAuthProxyArgs(cmd)
c.LegacyDestinationDirArgs = newLegacyDestinationDirArgs(cmd)
c.genericExecutorHandler = newGenericExecutorHandler(cmd, c, action)
cmd.Flag("owner", "Defines Linux \"user:group\" owner of \"--destination-dir\". Defaults to the Linux user running tbot if unspecified.").StringVar(&c.Owner)
cmd.Flag("bot-user", "Enables POSIX ACLs and defines Linux user that can read/write short-lived certificates to \"--destination-dir\".").StringVar(&c.BotUser)
cmd.Flag("reader-user", "Enables POSIX ACLs and defines Linux user that will read short-lived certificates from \"--destination-dir\".").StringVar(&c.ReaderUser)
cmd.Flag("init-dir", "If using a config file and multiple destinations are configured, controls which destination dir to configure.").StringVar(&c.InitDir)
cmd.Flag("clean", "If set, remove unexpected files and directories from the destination.").BoolVar(&c.Clean)
return c
}
func (c *InitCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if c.AuthProxyArgs != nil {
if err := c.AuthProxyArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
}
if c.LegacyDestinationDirArgs != nil {
if err := c.LegacyDestinationDirArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
}
return nil
}
+48
View File
@@ -0,0 +1,48 @@
/*
* 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"
)
func TestInitCommand(t *testing.T) {
testCommand(t, NewInitCommand, []testCommandCase[*InitCommand]{
{
name: "success",
args: []string{
"init",
"--owner=jim:wheels",
"--bot-user=jeffrey",
"--reader-user=bob",
"--init-dir=/tmp",
"--clean",
},
assert: func(t *testing.T, got *InitCommand) {
require.Equal(t, "jim:wheels", got.Owner)
require.Equal(t, "jeffrey", got.BotUser)
require.Equal(t, "bob", got.ReaderUser)
require.Equal(t, "/tmp", got.InitDir)
require.True(t, got.Clean)
},
},
})
}
+38
View File
@@ -0,0 +1,38 @@
/*
* 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
type KubeCredentialsCommand struct {
*genericExecutorHandler[KubeCredentialsCommand]
DestinationDir string
}
// NewKubeCredentialsCommand initializes a kubernetes `credentials` command
// and returns a struct that will contain the parse result.
func NewKubeCredentialsCommand(parentCmd KingpinClause, action func(*KubeCredentialsCommand) error) *KubeCredentialsCommand {
cmd := parentCmd.Command("credentials", "Get credentials for kubectl access").Hidden()
c := &KubeCredentialsCommand{}
c.genericExecutorHandler = newGenericExecutorHandler(cmd, c, action)
cmd.Flag("destination-dir", "The destination directory with which to generate Kubernetes credentials").Required().StringVar(&c.DestinationDir)
return c
}
+40
View File
@@ -0,0 +1,40 @@
/*
* 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"
)
func TestKubeCredentialsCommand(t *testing.T) {
testCommand(t, NewKubeCredentialsCommand, []testCommandCase[*KubeCredentialsCommand]{
{
name: "success",
args: []string{
"credentials",
"--destination-dir=/tmp",
},
assert: func(t *testing.T, got *KubeCredentialsCommand) {
require.Equal(t, "/tmp", got.DestinationDir)
},
},
})
}
+40
View File
@@ -0,0 +1,40 @@
/*
* 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
// MigrateCommand contains fields parsed for `tbot migrate`
type MigrateCommand struct {
*genericExecutorHandler[MigrateCommand]
// ConfigureOutput is the path to write the file. If empty, writes to
// stdout.
ConfigureOutput string
}
// NewMigrateCommand initializes the `tbot migrate` command and its flags.
func NewMigrateCommand(app KingpinClause, action func(*MigrateCommand) error) *MigrateCommand {
cmd := app.Command("migrate", "Migrates a config file from an older version to the newest version. Outputs to stdout by default.")
c := &MigrateCommand{}
c.genericExecutorHandler = newGenericExecutorHandler(cmd, c, action)
cmd.Flag("output", "Path to write the generated configuration file to rather than write to stdout.").Short('o').StringVar(&c.ConfigureOutput)
return c
}
+40
View File
@@ -0,0 +1,40 @@
/*
* 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"
)
func TestMigrateCommand(t *testing.T) {
testCommand(t, NewMigrateCommand, []testCommandCase[*MigrateCommand]{
{
name: "success",
args: []string{
"migrate",
"--output=/tmp",
},
assert: func(t *testing.T, got *MigrateCommand) {
require.Equal(t, "/tmp", got.ConfigureOutput)
},
},
})
}
+70
View File
@@ -0,0 +1,70 @@
/*
* 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 (
"context"
)
// ProxyCommand supports `tbot proxy`
type ProxyCommand struct {
*genericExecutorHandler[ProxyCommand]
DestinationDir string
Cluster string
ProxyServer string
// LegacyProxy is the legacy --proxy flag.
// TODO(timothyb89): DELETE IN 17.0.0
// TODO(timothyb89): Or maybe remove in this PR.
LegacyProxyFlag string
ProxyRemaining *[]string
}
// NewProxyCommand initializes the subcommand for `tbot proxy`
func NewProxyCommand(app KingpinClause, action func(*ProxyCommand) error) *ProxyCommand {
cmd := app.Command("proxy", "Start a local TLS proxy via tsh to connect to Teleport in single-port mode.")
c := &ProxyCommand{}
c.genericExecutorHandler = newGenericExecutorHandler(cmd, c, func(c *ProxyCommand) error {
// Prepend an action to handle --proxy deprecation.
if c.LegacyProxyFlag != "" {
c.ProxyServer = c.LegacyProxyFlag
log.WarnContext(context.TODO(), "The --proxy flag is deprecated and will be removed in v17.0.0. Use --proxy-server instead")
}
return nil
}, action)
// We're migrating from --proxy to --proxy-server so this flag is hidden
// but still supported.
// TODO(strideynet): DELETE IN 17.0.0
cmd.Flag("proxy", "The Teleport proxy server to use, in host:port form.").Hidden().StringVar(&c.LegacyProxyFlag)
cmd.Flag("proxy-server", "The Teleport proxy server to use, in host:port form.").Envar(ProxyServerEnvVar).StringVar(&c.ProxyServer)
cmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&c.DestinationDir)
cmd.Flag("cluster", "The cluster name. Extracted from the certificate if unset.").StringVar(&c.Cluster)
c.ProxyRemaining = RemainingArgs(cmd.Arg(
"args",
"Arguments to `tsh proxy ...`; prefix with `-- ` to ensure flags are passed correctly.",
))
return c
}
+48
View File
@@ -0,0 +1,48 @@
/*
* 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"
)
func TestProxyCommand(t *testing.T) {
testCommand(t, NewProxyCommand, []testCommandCase[*ProxyCommand]{
{
name: "success",
args: []string{
"proxy",
"--proxy-server=example.com:22",
"--destination-dir=/tmp",
"--cluster=example.com",
"bar",
"buzz",
"boo",
},
assert: func(t *testing.T, got *ProxyCommand) {
require.Equal(t, "example.com:22", got.ProxyServer)
require.Equal(t, "/tmp", got.DestinationDir)
require.Equal(t, "example.com", got.Cluster)
require.Equal(t, []string{"bar", "buzz", "boo"}, *got.ProxyRemaining)
},
},
})
}
+43
View File
@@ -0,0 +1,43 @@
/*
* 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
// SSHMultiplexerProxyCommand includes fields for `tbot ssh-multiplexer-proxy-command`
type SSHMultiplexerProxyCommand struct {
*genericExecutorHandler[SSHMultiplexerProxyCommand]
Socket string
Data string
}
// NewSSHMultiplexerProxyCommand initializes and parses args for `tbot ssh-multiplexer-proxy-command`
func NewSSHMultiplexerProxyCommand(app KingpinClause, action func(*SSHMultiplexerProxyCommand) error) *SSHMultiplexerProxyCommand {
cmd := app.Command(
"ssh-multiplexer-proxy-command",
"An OpenSSH compatible ProxyCommand which connects to a long-lived tbot running the ssh-multiplexer service",
).Hidden()
c := &SSHMultiplexerProxyCommand{}
c.genericExecutorHandler = newGenericExecutorHandler(cmd, c, action)
cmd.Arg("path", "Path to the listener socket.").Required().StringVar(&c.Socket)
cmd.Arg("data", "Connection target.").Required().StringVar(&c.Data)
return c
}
@@ -0,0 +1,42 @@
/*
* 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"
)
func TestSSHMultiplexerProxyCommand(t *testing.T) {
testCommand(t, NewSSHMultiplexerProxyCommand, []testCommandCase[*SSHMultiplexerProxyCommand]{
{
name: "success",
args: []string{
"ssh-multiplexer-proxy-command",
"/tmp/sock",
"example.com:22",
},
assert: func(t *testing.T, got *SSHMultiplexerProxyCommand) {
require.Equal(t, "/tmp/sock", got.Socket)
require.Equal(t, "example.com:22", got.Data)
},
},
})
}
+81
View File
@@ -0,0 +1,81 @@
/*
* 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
// SSHProxyCommand includes fields for `tbot ssh-proxy-command`
type SSHProxyCommand struct {
*genericExecutorHandler[SSHProxyCommand]
// DestinationDir stores the generated end-user certificates.
DestinationDir string
// Cluster is the name of the Teleport cluster on which resources should
// be accessed.
Cluster string
// ProxyServer is the teleport proxy address. Unlike `AuthServer` this must
// explicitly point to a Teleport proxy.
// Example: "example.teleport.sh:443"
ProxyServer string
// User is the os login to use for ssh connections.
User string
// Host is the target ssh machine to connect to.
Host string
// Post is the post of the ssh machine to connect on.
Port string
// EnableResumption turns on automatic session resumption to prevent connections from
// being dropped if Proxy connectivity is lost.
EnableResumption bool
// TLSRoutingEnabled indicates whether the cluster has TLS routing enabled.
TLSRoutingEnabled bool
// ConnectionUpgradeRequired indicates that an ALPN connection upgrade is required
// for connections to the cluster.
ConnectionUpgradeRequired bool
// TSHConfigPath is the path to a tsh config file.
TSHConfigPath string
}
// NewSSHProxyCommand initializes the `tbot ssh-proxy-command` subcommand and
// its fields.
func NewSSHProxyCommand(app KingpinClause, action func(*SSHProxyCommand) error) *SSHProxyCommand {
cmd := app.Command("ssh-proxy-command", "An OpenSSH/PuTTY proxy command").Hidden()
c := &SSHProxyCommand{}
c.genericExecutorHandler = newGenericExecutorHandler(cmd, c, action)
cmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&c.DestinationDir)
cmd.Flag("cluster", "The cluster name. Extracted from the certificate if unset.").StringVar(&c.Cluster)
cmd.Flag("user", "The remote user name for the connection").Required().StringVar(&c.User)
cmd.Flag("host", "The remote host to connect to").Required().StringVar(&c.Host)
cmd.Flag("port", "The remote port to connect on.").StringVar(&c.Port)
cmd.Flag("proxy-server", "The Teleport proxy server to use, in host:port form.").Required().StringVar(&c.ProxyServer)
cmd.Flag("tls-routing", "Whether the Teleport cluster has tls routing enabled.").Required().BoolVar(&c.TLSRoutingEnabled)
cmd.Flag("connection-upgrade", "Whether the Teleport cluster requires an ALPN connection upgrade.").Required().BoolVar(&c.ConnectionUpgradeRequired)
cmd.Flag("proxy-templates", "The path to a file containing proxy templates to be evaluated.").StringVar(&c.TSHConfigPath)
cmd.Flag("resume", "Enable SSH connection resumption").BoolVar(&c.EnableResumption)
return c
}
+56
View File
@@ -0,0 +1,56 @@
/*
* 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"
)
func TestSSHProxyCommand(t *testing.T) {
testCommand(t, NewSSHProxyCommand, []testCommandCase[*SSHProxyCommand]{
{
name: "success",
args: []string{
"ssh-proxy-command",
"--destination-dir=/bar",
"--cluster=foo",
"--user=noah",
"--host=example.com",
"--proxy-server=example.com:443",
"--tls-routing",
"--connection-upgrade",
"--proxy-templates=/tmp/tsh.yaml",
"--resume",
},
assert: func(t *testing.T, got *SSHProxyCommand) {
require.Equal(t, "/bar", got.DestinationDir)
require.Equal(t, "foo", got.Cluster)
require.Equal(t, "noah", got.User)
require.Equal(t, "example.com", got.Host)
require.Equal(t, "example.com:443", got.ProxyServer)
require.True(t, got.TLSRoutingEnabled)
require.True(t, got.ConnectionUpgradeRequired)
require.Equal(t, "/tmp/tsh.yaml", got.TSHConfigPath)
require.True(t, got.EnableResumption)
},
},
})
}
+76
View File
@@ -0,0 +1,76 @@
/*
* 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 (
"log/slog"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/config"
)
// ApplicationCommand implements `tbot start application` and
// `tbot configure application`.
type ApplicationCommand struct {
*sharedStartArgs
*genericMutatorHandler
Destination string
AppName string
SpecificTLSExtensions bool
}
// NewApplicationCommand initializes a command and flag for application outputs
// and returns a struct that will contain the parse result.
func NewApplicationCommand(parentCmd *kingpin.CmdClause, action MutatorAction) *ApplicationCommand {
cmd := parentCmd.Command("application", "Starts with an application output").Alias("app")
c := &ApplicationCommand{}
c.sharedStartArgs = newSharedStartArgs(cmd)
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)
cmd.Flag("destination", "A destination URI, such as file:///foo/bar").Required().StringVar(&c.Destination)
cmd.Flag("app", "The name of the app in Teleport").Required().StringVar(&c.AppName)
cmd.Flag("specific-tls-extensions", "If set, include additional `tls.crt`, `tls.key`, and `tls.cas` for apps that require these file extensions").BoolVar(&c.SpecificTLSExtensions)
// Note: CLI will not support roles; all will be requested.
return c
}
func (c *ApplicationCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
dest, err := config.DestinationFromURI(c.Destination)
if err != nil {
return trace.Wrap(err)
}
cfg.Services = append(cfg.Services, &config.ApplicationOutput{
Destination: dest,
AppName: c.AppName,
SpecificTLSExtensions: c.SpecificTLSExtensions,
})
return nil
}
+60
View File
@@ -0,0 +1,60 @@
/*
* 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"
)
// TestApplicationCommand tests that the ApplicationCommand properly parses its
// arguments and applies as expected onto a BotConfig.
func TestApplicationCommand(t *testing.T) {
testStartConfigureCommand(t, NewApplicationCommand, []startConfigureTestCase{
{
name: "success",
args: []string{
"start",
"application",
"--destination=/bar",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--app=foo",
"--specific-tls-extensions",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
// It must configure a app output with a directory destination.
svc := cfg.Services[0]
appSvc, ok := svc.(*config.ApplicationOutput)
require.True(t, ok)
require.Equal(t, "foo", appSvc.AppName)
require.True(t, appSvc.SpecificTLSExtensions)
dir, ok := appSvc.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/bar", dir.Path)
},
},
})
}
+68
View File
@@ -0,0 +1,68 @@
/*
* 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 (
"log/slog"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/config"
)
// ApplicationTunnelCommand implements `tbot start application-tunnel` and
// `tbot configure application-tunnel`.
type ApplicationTunnelCommand struct {
*sharedStartArgs
*genericMutatorHandler
Listen string
AppName string
}
// NewApplicationTunnelCommand initializes flags for an app tunnel command and
// returns a struct to contain the parse result.
func NewApplicationTunnelCommand(parentCmd *kingpin.CmdClause, action MutatorAction) *ApplicationTunnelCommand {
cmd := parentCmd.Command("application-tunnel", "Starts an application tunnel").Alias("app-tunnel")
c := &ApplicationTunnelCommand{}
c.sharedStartArgs = newSharedStartArgs(cmd)
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)
cmd.Flag("listen", "A socket URI, such as tcp://0.0.0.0:8080").Required().StringVar(&c.Listen)
cmd.Flag("app", "The name of the app in Teleport").Required().StringVar(&c.AppName)
// Note: CLI will not support roles; all will be requested.
return c
}
func (c *ApplicationTunnelCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
cfg.Services = append(cfg.Services, &config.ApplicationTunnelService{
Listen: c.Listen,
AppName: c.AppName,
})
return nil
}
@@ -0,0 +1,57 @@
/*
* 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"
)
// TestApplicationTunnelCommand tests that the ApplicationTunnelCommand
// properly parses its arguments and applies as expected onto a BotConfig.
func TestApplicationTunnelCommand(t *testing.T) {
testStartConfigureCommand(t, NewApplicationTunnelCommand, []startConfigureTestCase{
{
name: "success",
args: []string{
"start",
"application-tunnel",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--app=foo",
"--listen=tcp://0.0.0.0:8000",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
require.Len(t, cfg.Services, 1)
// It must configure an app tunnel service
svc := cfg.Services[0]
appSvc, ok := svc.(*config.ApplicationTunnelService)
require.True(t, ok)
require.Equal(t, "foo", appSvc.AppName)
require.Equal(t, "tcp://0.0.0.0:8000", appSvc.Listen)
},
},
})
}
+80
View File
@@ -0,0 +1,80 @@
/*
* 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 (
"log/slog"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/config"
)
// DatabaseCommand implements `tbot start database` and
// `tbot configure database`.
type DatabaseCommand struct {
*sharedStartArgs
*genericMutatorHandler
Destination string
Format string
Service string
Username string
Database string
}
// NewDatabaseCommand initializes a command and flags for database outputs and
// returns a struct that will contain the parse result.
func NewDatabaseCommand(parentCmd *kingpin.CmdClause, action MutatorAction) *DatabaseCommand {
cmd := parentCmd.Command("database", "Starts with a database output").Alias("db")
c := &DatabaseCommand{}
c.sharedStartArgs = newSharedStartArgs(cmd)
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)
cmd.Flag("destination", "A destination URI, such as file:///foo/bar").Required().StringVar(&c.Destination)
cmd.Flag("format", "The database output format if necessary").Default("").EnumVar(&c.Format, config.SupportedDatabaseFormatStrings()...)
cmd.Flag("service", "The database service name").Required().StringVar(&c.Service)
cmd.Flag("username", "The database user name").Required().StringVar(&c.Username)
cmd.Flag("database", "The name of the database available in the requested database service").Required().StringVar(&c.Database)
return c
}
func (c *DatabaseCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
dest, err := config.DestinationFromURI(c.Destination)
if err != nil {
return trace.Wrap(err)
}
cfg.Services = append(cfg.Services, &config.DatabaseOutput{
Destination: dest,
Format: config.DatabaseFormat(c.Format),
Username: c.Username,
Database: c.Database,
Service: c.Service,
})
return nil
}
+66
View File
@@ -0,0 +1,66 @@
/*
* 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"
)
// TestDatabaseCommand tests that the DatabaseCommand properly parses its
// arguments and applies as expected onto a BotConfig.
func TestDatabaseCommand(t *testing.T) {
testStartConfigureCommand(t, NewDatabaseCommand, []startConfigureTestCase{
{
name: "success",
args: []string{
"start",
"database",
"--destination=/bar",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--service=foo",
"--username=bar",
"--database=baz",
"--format=tls",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
require.Len(t, cfg.Services, 1)
// It must configure a database output with a directory destination.
svc := cfg.Services[0]
db, ok := svc.(*config.DatabaseOutput)
require.True(t, ok)
require.Equal(t, "foo", db.Service)
require.Equal(t, "bar", db.Username)
require.Equal(t, "baz", db.Database)
require.Equal(t, config.TLSDatabaseFormat, db.Format)
dir, ok := db.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/bar", dir.Path)
},
},
})
}
+73
View File
@@ -0,0 +1,73 @@
/*
* 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 (
"log/slog"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/config"
)
// DatabaseTunnelCommand implements `tbot start database-tunnel` and
// `tbot configure database-tunnel`.
type DatabaseTunnelCommand struct {
*sharedStartArgs
*genericMutatorHandler
Listen string
Service string
Username string
Database string
}
// NewDatabaseTunnelCommand creates a command supporting `tbot start database-tunnel`
func NewDatabaseTunnelCommand(parentCmd *kingpin.CmdClause, action MutatorAction) *DatabaseTunnelCommand {
cmd := parentCmd.Command("database-tunnel", "Start a database tunnel listener").Alias("db-tunnel")
c := &DatabaseTunnelCommand{}
c.sharedStartArgs = newSharedStartArgs(cmd)
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)
cmd.Flag("listen", "A socket URI to listen on, such as tcp://0.0.0.0:3306").Required().StringVar(&c.Listen)
cmd.Flag("service", "The database service name").Required().StringVar(&c.Service)
cmd.Flag("username", "The database user name").Required().StringVar(&c.Username)
cmd.Flag("database", "The name of the database available in the requested database service").Required().StringVar(&c.Database)
// Note: excluding roles from the CLI; will default to all available.
return c
}
func (c *DatabaseTunnelCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
cfg.Services = append(cfg.Services, &config.DatabaseTunnelService{
Listen: c.Listen,
Username: c.Username,
Database: c.Database,
Service: c.Service,
})
return nil
}
@@ -0,0 +1,61 @@
/*
* 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"
)
// TestDatabaseTunnelCommand tests that the DatabaseTunnelCommand
// properly parses its arguments and applies as expected onto a BotConfig.
func TestDatabaseTunnelCommand(t *testing.T) {
testStartConfigureCommand(t, NewDatabaseTunnelCommand, []startConfigureTestCase{
{
name: "success",
args: []string{
"start",
"database-tunnel",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--listen=tcp://0.0.0.0:8000",
"--service=foo",
"--username=bar",
"--database=baz",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
require.Len(t, cfg.Services, 1)
// It must configure a db tunnel service
svc := cfg.Services[0]
db, ok := svc.(*config.DatabaseTunnelService)
require.True(t, ok)
require.Equal(t, "tcp://0.0.0.0:8000", db.Listen)
require.Equal(t, "foo", db.Service)
require.Equal(t, "bar", db.Username)
require.Equal(t, "baz", db.Database)
},
},
})
}
+73
View File
@@ -0,0 +1,73 @@
/*
* 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 (
"log/slog"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/config"
)
// IdentityCommand implements `tbot start identity` and
// `tbot configure identity`.
type IdentityCommand struct {
*sharedStartArgs
*genericMutatorHandler
Destination string
Cluster string
}
// NewIdentityCommand initializes the command and flags for identity outputs
// and returns a struct that will contain the parse result.
func NewIdentityCommand(parentCmd *kingpin.CmdClause, action MutatorAction) *IdentityCommand {
cmd := parentCmd.Command("identity", "Start with an identity output for SSH and Teleport API access").Alias("ssh").Alias("id")
c := &IdentityCommand{}
c.sharedStartArgs = newSharedStartArgs(cmd)
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)
cmd.Flag("destination", "A destination URI, such as file:///foo/bar").Required().StringVar(&c.Destination)
cmd.Flag("cluster", "The name of a specific cluster for which to issue an identity if using a leaf cluster").StringVar(&c.Cluster)
// Note: roles and ssh_config mode are excluded for now.
return c
}
func (c *IdentityCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
dest, err := config.DestinationFromURI(c.Destination)
if err != nil {
return trace.Wrap(err)
}
cfg.Services = append(cfg.Services, &config.IdentityOutput{
Destination: dest,
Cluster: c.Cluster,
})
return nil
}
+81
View File
@@ -0,0 +1,81 @@
/*
* 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"
"time"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/tbot/config"
)
// TestIdentityCommand tests that the IdentityCommand properly parses its arguments
// and applies as expected onto a BotConfig.
func TestIdentityCommand(t *testing.T) {
testStartConfigureCommand(t, NewIdentityCommand, []startConfigureTestCase{
{
name: "success",
args: []string{
"start",
"identity",
"--token=foo",
"--ca-pin=bar",
"--certificate-ttl=10m",
"--renewal-interval=5m",
"--join-method=github",
"--oneshot",
"--diag-addr=0.0.0.0:8080",
"--storage=/foo",
"--destination=file:///bar",
"--proxy-server=example.com:443",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
token, err := cfg.Onboarding.Token()
require.NoError(t, err)
require.Equal(t, "foo", token)
require.ElementsMatch(t, cfg.Onboarding.CAPins, []string{"bar"})
require.Equal(t, time.Minute*10, cfg.CertificateTTL)
require.Equal(t, time.Minute*5, cfg.RenewalInterval)
require.Equal(t, types.JoinMethodGitHub, cfg.Onboarding.JoinMethod)
require.True(t, cfg.Oneshot)
require.Equal(t, "0.0.0.0:8080", cfg.DiagAddr)
require.Equal(t, "example.com:443", cfg.ProxyServer)
dir, ok := cfg.Storage.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/foo", dir.Path)
require.Len(t, cfg.Services, 1)
// It must configure an identity output with a directory destination.
svc := cfg.Services[0]
ident, ok := svc.(*config.IdentityOutput)
require.True(t, ok)
dir, ok = ident.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/bar", dir.Path)
},
},
})
}
+76
View File
@@ -0,0 +1,76 @@
/*
* 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 (
"log/slog"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/config"
)
// KubernetesCommand implements `tbot start kubernetes` and
// `tbot configure kubernetes`.
type KubernetesCommand struct {
*sharedStartArgs
*genericMutatorHandler
Destination string
KubernetesCluster string
DisableExecPlugin bool
}
// NewKubernetesCommand initializes the command and flags for kubernetes outputs
// and returns a struct to contain the parse result.
func NewKubernetesCommand(parentCmd *kingpin.CmdClause, action MutatorAction) *KubernetesCommand {
cmd := parentCmd.Command("kubernetes", "Starts with a kubernetes output").Alias("k8s")
c := &KubernetesCommand{}
c.sharedStartArgs = newSharedStartArgs(cmd)
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)
cmd.Flag("destination", "A destination URI, such as file:///foo/bar").Required().StringVar(&c.Destination)
cmd.Flag("kubernetes-cluster", "The name of the Kubernetes cluster in Teleport for which to fetch credentials").Required().StringVar(&c.KubernetesCluster)
cmd.Flag("disable-exec-plugin", "If set, disables the exec plugin. This allows credentials to be used without the `tbot` binary.").BoolVar(&c.DisableExecPlugin)
// Note: excluding roles; the bot will fetch all available in CLI mode.
return c
}
func (c *KubernetesCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
dest, err := config.DestinationFromURI(c.Destination)
if err != nil {
return trace.Wrap(err)
}
cfg.Services = append(cfg.Services, &config.KubernetesOutput{
Destination: dest,
KubernetesCluster: c.KubernetesCluster,
DisableExecPlugin: c.DisableExecPlugin,
})
return nil
}
+62
View File
@@ -0,0 +1,62 @@
/*
* 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"
)
// TestKubernetesCommand tests that the KubernetesCommand properly parses its
// arguments and applies as expected onto a BotConfig.
func TestKubernetesCommand(t *testing.T) {
testStartConfigureCommand(t, NewKubernetesCommand, []startConfigureTestCase{
{
name: "success",
args: []string{
"start",
"kubernetes",
"--destination=/bar",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--kubernetes-cluster=demo",
"--disable-exec-plugin",
},
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.KubernetesOutput)
require.True(t, ok)
require.Equal(t, "demo", k8s.KubernetesCluster)
require.True(t, k8s.DisableExecPlugin)
dir, ok := k8s.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/bar", dir.Path)
},
},
})
}
+272
View File
@@ -0,0 +1,272 @@
/*
* 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 (
"context"
"fmt"
"log/slog"
"reflect"
"strings"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/tbot/config"
)
// LegacyDestinationDirArgs is an embeddable struct that provides legacy-style
// --destination-dir handling, largely for reuse by `start legacy` and other
// older subcommands like `init`.
type LegacyDestinationDirArgs struct {
// DestinationDir stores the generated end-user certificates.
DestinationDir string
}
// newLegacyDestinationDirArgs initializes the legacy --destination-dir flag on
// the given command, and returns a struct that will contain the parse result.
func newLegacyDestinationDirArgs(cmd *kingpin.CmdClause) *LegacyDestinationDirArgs {
args := &LegacyDestinationDirArgs{}
cmd.Flag("destination-dir", "Directory to write short-lived machine certificates.").StringVar(&args.DestinationDir)
return args
}
func (a *LegacyDestinationDirArgs) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if a.DestinationDir != "" {
// WARNING:
// See: https://github.com/gravitational/teleport/issues/27206 for
// potential gotchas that currently exist when dealing with this
// override behavior.
// CLI only supports a single filesystem Destination with SSH client config
// and all roles.
if len(cfg.Services) > 0 {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding output services",
"flag", "destination-dir",
"cli_value", a.DestinationDir,
)
}
// When using the CLI --destination-dir we configure an Identity type
// output for that directory.
cfg.Services = []config.ServiceConfig{
&config.IdentityOutput{
Destination: &config.DestinationDirectory{
Path: a.DestinationDir,
},
},
}
}
return nil
}
// LegacyCommand starts with legacy behavior. This handles flags somewhat
// differently and maintains support for certain deprecated flags, so does not
// use `sharedStartArgs`.
type LegacyCommand struct {
*AuthProxyArgs
*LegacyDestinationDirArgs
// cmd is the concrete command for this instance
cmd *kingpin.CmdClause
// action is the action that will be performed if this command is selected
action MutatorAction
// LogFormat controls the format of logging. Can be either `json` or `text`.
// By default, this is `text`.
LogFormat string
// DataDir stores the bot's internal data.
DataDir string
// CAPins is a list of pinned SKPI hashes of trusted auth server CAs, used
// only on first connect.
CAPins []string
// Token is a bot join token.
Token string
// RenewalInterval is the interval at which certificates are renewed, as a
// time.ParseDuration() string. It must be less than the certificate TTL.
RenewalInterval time.Duration
// CertificateTTL is the requested TTL of certificates. It should be some
// multiple of the renewal interval to allow for failed renewals.
CertificateTTL time.Duration
// JoinMethod is the method the bot should use to exchange a token for the
// initial certificate
JoinMethod string
// Oneshot controls whether the bot quits after a single renewal.
Oneshot bool
// DiagAddr is the address the diagnostics http service should listen on.
// If not set, no diagnostics listener is created.
DiagAddr string
}
// NewLegacyCommand initializes and returns a command supporting
// `tbot start legacy` and `tbot configure legacy`.
func NewLegacyCommand(parentCmd *kingpin.CmdClause, action MutatorAction) *LegacyCommand {
joinMethodList := fmt.Sprintf(
"(%s)",
strings.Join(config.SupportedJoinMethods, ", "),
)
c := &LegacyCommand{
action: action,
cmd: parentCmd.Command("legacy", "Start with either a config file or a legacy output").Default(),
}
c.AuthProxyArgs = newAuthProxyArgs(c.cmd)
c.LegacyDestinationDirArgs = newLegacyDestinationDirArgs(c.cmd)
c.cmd.Flag("data-dir", "Directory to store internal bot data. Access to this directory should be limited.").StringVar(&c.DataDir)
c.cmd.Flag("token", "A bot join token or path to file with token value, if attempting to onboard a new bot; used on first connect.").Envar(TokenEnvVar).StringVar(&c.Token)
c.cmd.Flag("ca-pin", "CA pin to validate the Teleport Auth Server; used on first connect.").StringsVar(&c.CAPins)
c.cmd.Flag("certificate-ttl", "TTL of short-lived machine certificates.").DurationVar(&c.CertificateTTL)
c.cmd.Flag("renewal-interval", "Interval at which short-lived certificates are renewed; must be less than the certificate TTL.").DurationVar(&c.RenewalInterval)
c.cmd.Flag("join-method", "Method to use to join the cluster. "+joinMethodList).EnumVar(&c.JoinMethod, config.SupportedJoinMethods...)
c.cmd.Flag("oneshot", "If set, quit after the first renewal.").BoolVar(&c.Oneshot)
c.cmd.Flag("diag-addr", "If set and the bot is in debug mode, a diagnostics service will listen on specified address.").StringVar(&c.DiagAddr)
return c
}
func (c *LegacyCommand) TryRun(cmd string) (match bool, err error) {
switch cmd {
case c.cmd.FullCommand():
err = c.action(c)
default:
return false, nil
}
return true, trace.Wrap(err)
}
func (c *LegacyCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
// Note: Debug, FIPS, and Insecure are included from globals
if c.AuthProxyArgs != nil {
if err := c.AuthProxyArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
}
if c.LegacyDestinationDirArgs != nil {
if err := c.LegacyDestinationDirArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
}
if c.Oneshot {
cfg.Oneshot = true
}
if c.CertificateTTL != 0 {
if cfg.CertificateTTL != 0 {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "certificate-ttl",
"config_value", cfg.CertificateTTL,
"cli_value", c.CertificateTTL,
)
}
cfg.CertificateTTL = c.CertificateTTL
}
if c.RenewalInterval != 0 {
if cfg.RenewalInterval != 0 {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "renewal-interval",
"config_value", cfg.RenewalInterval,
"cli_value", c.RenewalInterval,
)
}
cfg.RenewalInterval = c.RenewalInterval
}
// DataDir overrides any previously-configured storage config
if c.DataDir != "" {
if cfg.Storage != nil && cfg.Storage.Destination != nil {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "data-dir",
"config_value", cfg.Storage.Destination.String(),
"cli_value", c.DataDir,
)
}
dest, err := config.DestinationFromURI(c.DataDir)
if err != nil {
return trace.Wrap(err)
}
cfg.Storage = &config.StorageConfig{Destination: dest}
}
// If any onboarding flags are set, override the whole section.
// (CAPath, CAPins, etc follow different codepaths so we don't want a
// situation where different fields become set weirdly due to struct
// merging)
if c.Token != "" || c.JoinMethod != "" || len(c.CAPins) > 0 {
if !reflect.DeepEqual(cfg.Onboarding, config.OnboardingConfig{}) {
// To be safe, warn about possible confusion.
log.WarnContext(
context.TODO(),
"CLI parameters are overriding join configuration",
"cli_token", c.Token,
"cli_join_method", c.JoinMethod,
"cli_ca_pins_count", len(c.CAPins),
)
}
cfg.Onboarding = config.OnboardingConfig{
CAPins: c.CAPins,
JoinMethod: types.JoinMethod(c.JoinMethod),
}
cfg.Onboarding.SetToken(c.Token)
}
if c.DiagAddr != "" {
if cfg.DiagAddr != "" {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "diag-addr",
"config_value", cfg.DiagAddr,
"cli_value", c.DiagAddr,
)
}
cfg.DiagAddr = c.DiagAddr
}
return nil
}
+80
View File
@@ -0,0 +1,80 @@
/*
* 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"
"time"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/tbot/config"
)
// TestLegacyCommand tests that the LegacyCommand properly parses its arguments
// and applies as expected onto a BotConfig.
func TestLegacyCommand(t *testing.T) {
testStartConfigureCommand(t, NewLegacyCommand, []startConfigureTestCase{
{
name: "success",
args: []string{
"start", // Note: implied legacy, it should be the default.
"--token=foo",
"--ca-pin=bar",
"--certificate-ttl=10m",
"--renewal-interval=5m",
"--join-method=github",
"--oneshot",
"--diag-addr=0.0.0.0:8080",
"--data-dir=/foo",
"--destination-dir=/bar",
"--auth-server=example.com:3024",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
token, err := cfg.Onboarding.Token()
require.NoError(t, err)
require.Equal(t, "foo", token)
require.ElementsMatch(t, cfg.Onboarding.CAPins, []string{"bar"})
require.Equal(t, time.Minute*10, cfg.CertificateTTL)
require.Equal(t, time.Minute*5, cfg.RenewalInterval)
require.Equal(t, types.JoinMethodGitHub, cfg.Onboarding.JoinMethod)
require.True(t, cfg.Oneshot)
require.Equal(t, "0.0.0.0:8080", cfg.DiagAddr)
require.Equal(t, "example.com:3024", cfg.AuthServer)
dir, ok := cfg.Storage.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/foo", dir.Path)
require.Len(t, cfg.Services, 1)
// It must configure an identity output with a directory destination.
svc := cfg.Services[0]
ident, ok := svc.(*config.IdentityOutput)
require.True(t, ok)
dir, ok = ident.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/bar", dir.Path)
},
},
})
}
+235
View File
@@ -0,0 +1,235 @@
/*
* 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 (
"context"
"fmt"
"log/slog"
"reflect"
"strings"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/tbot/config"
)
// AuthProxyArgs is an embeddable struct that can add --auth-server and
// --proxy-server to arbitrary commands for reuse.
type AuthProxyArgs struct {
// AuthServer is a Teleport auth server address. It may either point
// directly to an auth server, or to a Teleport proxy server in which case
// a tunneled auth connection will be established.
// Prefer using Address() to pick an address.
AuthServer string
// ProxyServer is the teleport proxy address. Unlike `AuthServer` this must
// explicitly point to a Teleport proxy.
// Example: "example.teleport.sh:443"
ProxyServer string
}
// NewStaticAuthServer returns an AuthProxyArgs with the given AuthServer field
// configured. Used in tests.
func NewStaticAuthServer(authServer string) *AuthProxyArgs {
return &AuthProxyArgs{
AuthServer: authServer,
}
}
// newAuthProxyArgs initializes --auth-server and --proxy-server args on the
// given command. This can be embedded in any parent command that needs to
// accept an auth or proxy address. Note that `ApplyConfig` will need to be
// called in the parent's own `ApplyConfig`.
func newAuthProxyArgs(cmd *kingpin.CmdClause) *AuthProxyArgs {
args := &AuthProxyArgs{}
cmd.Flag("auth-server", "Address of the Teleport Auth Server. Prefer using --proxy-server where possible.").Short('a').Envar(AuthServerEnvVar).StringVar(&args.AuthServer)
cmd.Flag("proxy-server", "Address of the Teleport Proxy Server.").Envar(ProxyServerEnvVar).StringVar(&args.ProxyServer)
return args
}
func (a *AuthProxyArgs) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if a.AuthServer != "" {
if cfg.AuthServer != "" {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "auth-server",
"config_value", cfg.AuthServer,
"cli_value", a.AuthServer,
)
}
cfg.AuthServer = a.AuthServer
}
if a.ProxyServer != "" {
if cfg.ProxyServer != "" {
l.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "proxy-server",
"config_value", cfg.ProxyServer,
"cli_value", a.ProxyServer,
)
}
cfg.ProxyServer = a.ProxyServer
}
return nil
}
// sharedStartArgs are arguments that are shared between all modern `start` and
// `configure` subcommands.
type sharedStartArgs struct {
*AuthProxyArgs
JoinMethod string
Token string
CAPins []string
CertificateTTL time.Duration
RenewalInterval time.Duration
Storage string
Oneshot bool
DiagAddr string
}
// newSharedStartArgs initializes shared arguments on the given parent command.
func newSharedStartArgs(cmd *kingpin.CmdClause) *sharedStartArgs {
args := &sharedStartArgs{}
args.AuthProxyArgs = newAuthProxyArgs(cmd)
joinMethodList := fmt.Sprintf(
"(%s)",
strings.Join(config.SupportedJoinMethods, ", "),
)
cmd.Flag("token", "A bot join token or path to file with token value, if attempting to onboard a new bot; used on first connect.").Envar(TokenEnvVar).StringVar(&args.Token)
cmd.Flag("ca-pin", "CA pin to validate the Teleport Auth Server; used on first connect.").StringsVar(&args.CAPins)
cmd.Flag("certificate-ttl", "TTL of short-lived machine certificates.").DurationVar(&args.CertificateTTL)
cmd.Flag("renewal-interval", "Interval at which short-lived certificates are renewed; must be less than the certificate TTL.").DurationVar(&args.RenewalInterval)
cmd.Flag("join-method", "Method to use to join the cluster. "+joinMethodList).EnumVar(&args.JoinMethod, config.SupportedJoinMethods...)
cmd.Flag("oneshot", "If set, quit after the first renewal.").BoolVar(&args.Oneshot)
cmd.Flag("diag-addr", "If set and the bot is in debug mode, a diagnostics service will listen on specified address.").StringVar(&args.DiagAddr)
cmd.Flag("storage", "A destination URI for tbot's internal storage, e.g. file:///foo/bar").StringVar(&args.Storage)
return args
}
func (s *sharedStartArgs) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
// Note: Debug, FIPS, and Insecure are included from globals.
if s.AuthProxyArgs != nil {
if err := s.AuthProxyArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
}
if s.Oneshot {
cfg.Oneshot = true
}
if s.CertificateTTL != 0 {
if cfg.CertificateTTL != 0 {
l.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "certificate-ttl",
"config_value", cfg.CertificateTTL,
"cli_value", s.CertificateTTL,
)
}
cfg.CertificateTTL = s.CertificateTTL
}
if s.RenewalInterval != 0 {
if cfg.RenewalInterval != 0 {
l.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "renewal-interval",
"config_value", cfg.RenewalInterval,
"cli_value", s.RenewalInterval,
)
}
cfg.RenewalInterval = s.RenewalInterval
}
if s.DiagAddr != "" {
if cfg.DiagAddr != "" {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "diag-addr",
"config_value", cfg.DiagAddr,
"cli_value", s.DiagAddr,
)
}
cfg.DiagAddr = s.DiagAddr
}
// Storage overrides any previously-configured storage config
if s.Storage != "" {
if cfg.Storage != nil && cfg.Storage.Destination != nil {
l.WarnContext(
context.TODO(),
"CLI parameters are overriding configuration",
"flag", "storage",
"config_value", cfg.Storage.Destination.String(),
"cli_value", s.Storage,
)
}
dest, err := config.DestinationFromURI(s.Storage)
if err != nil {
return trace.Wrap(err)
}
cfg.Storage = &config.StorageConfig{Destination: dest}
}
// If any onboarding flags are set, override the whole section.
// (CAPath, CAPins, etc follow different codepaths so we don't want a
// situation where different fields become set weirdly due to struct
// merging)
if s.Token != "" || s.JoinMethod != "" || len(s.CAPins) > 0 {
if !reflect.DeepEqual(cfg.Onboarding, config.OnboardingConfig{}) {
// To be safe, warn about possible confusion.
l.WarnContext(
context.TODO(),
"CLI parameters are overriding join configuration",
"cli_token", s.Token,
"cli_join_method", s.JoinMethod,
"cli_ca_pins_count", len(s.CAPins),
)
}
cfg.Onboarding = config.OnboardingConfig{
CAPins: s.CAPins,
JoinMethod: types.JoinMethod(s.JoinMethod),
}
cfg.Onboarding.SetToken(s.Token)
}
return nil
}
+76
View File
@@ -0,0 +1,76 @@
/*
* 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"
"time"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/tbot/config"
)
func TestSharedStartArgs(t *testing.T) {
app, subcommand := buildMinimalKingpinApp("test")
args := newSharedStartArgs(subcommand)
_, err := app.Parse([]string{
"test",
"--token=foo",
"--ca-pin=bar",
"--certificate-ttl=10m",
"--renewal-interval=5m",
"--join-method=github",
"--oneshot",
"--diag-addr=0.0.0.0:8080",
"--storage=file:///foo/bar",
"--proxy-server=example.teleport.sh:443",
})
require.NoError(t, err)
require.Equal(t, "foo", args.Token)
require.Len(t, args.CAPins, 1)
require.Equal(t, "bar", args.CAPins[0])
require.Equal(t, time.Minute*10, args.CertificateTTL)
require.Equal(t, time.Minute*5, args.RenewalInterval)
require.True(t, args.Oneshot)
require.Equal(t, "0.0.0.0:8080", args.DiagAddr)
require.Equal(t, "file:///foo/bar", args.Storage)
require.Equal(t, "example.teleport.sh:443", args.ProxyServer)
// Convert these args to a BotConfig.
cfg, err := LoadConfigWithMutators(&GlobalArgs{}, args)
require.NoError(t, err)
token, err := cfg.Onboarding.Token()
require.NoError(t, err)
require.Equal(t, "foo", token)
require.ElementsMatch(t, cfg.Onboarding.CAPins, []string{"bar"})
require.Equal(t, time.Minute*10, cfg.CertificateTTL)
require.Equal(t, time.Minute*5, cfg.RenewalInterval)
require.Equal(t, types.JoinMethodGitHub, cfg.Onboarding.JoinMethod)
require.True(t, cfg.Oneshot)
require.Equal(t, "0.0.0.0:8080", cfg.DiagAddr)
dir, ok := cfg.Storage.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/foo/bar", dir.Path)
}
+89
View File
@@ -0,0 +1,89 @@
/*
* 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 (
"log/slog"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/config"
)
// SPIFFESVIDCommand implements `tbot start spiffe-svid` and
// `tbot configure spiffe-svid`.
type SPIFFESVIDCommand struct {
*sharedStartArgs
*genericMutatorHandler
Destination string
IncludeFederatedTrustBundles bool
SVIDPath string
SVIDHint string
DNSSANs []string
IPSANs []string
}
// NewSPIFFESVIDCommand initializes the command and flags for the
// `spiffe-svid` output and returns a struct that will contain the parse
// result.
func NewSPIFFESVIDCommand(parentCmd *kingpin.CmdClause, action MutatorAction) *SPIFFESVIDCommand {
cmd := parentCmd.Command("spiffe-svid", "Starts with a SPIFFE-compatible SVID output")
c := &SPIFFESVIDCommand{}
c.sharedStartArgs = newSharedStartArgs(cmd)
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)
cmd.Flag("destination", "A destination URI, such as file:///foo/bar").Required().StringVar(&c.Destination)
cmd.Flag("include-federated-trust-bundles", "If set, include federated trust bundles in the output").BoolVar(&c.IncludeFederatedTrustBundles)
cmd.Flag("svid-path", "A SPIFFE ID to request, prefixed with '/'").Required().StringVar(&c.SVIDPath)
cmd.Flag("svid-hint", "An optional hint for consumers of the SVID to aid in identification").StringVar(&c.SVIDHint)
cmd.Flag("dns-san", "A DNS name that should be included in the SVID. Repeatable.").StringsVar(&c.DNSSANs)
cmd.Flag("ip-san", "An IP address that should be included in the SVID. Repeatable.").StringsVar(&c.IPSANs)
return c
}
func (c *SPIFFESVIDCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error {
if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil {
return trace.Wrap(err)
}
dest, err := config.DestinationFromURI(c.Destination)
if err != nil {
return trace.Wrap(err)
}
cfg.Services = append(cfg.Services, &config.SPIFFESVIDOutput{
Destination: dest,
SVID: config.SVIDRequest{
Path: c.SVIDPath,
Hint: c.SVIDHint,
SANS: config.SVIDRequestSANs{
DNS: c.DNSSANs,
IP: c.IPSANs,
},
},
IncludeFederatedTrustBundles: c.IncludeFederatedTrustBundles,
})
return nil
}
+74
View File
@@ -0,0 +1,74 @@
/*
* 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"
)
// TestSPIFFESVIDCommand tests that the SPIFFESVIDCommand properly parses its
// arguments and applies as expected onto a BotConfig.
func TestSPIFFESVIDCommand(t *testing.T) {
testStartConfigureCommand(t, NewSPIFFESVIDCommand, []startConfigureTestCase{
{
name: "success",
args: []string{
"start",
"spiffe-svid",
"--destination=/bar",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--include-federated-trust-bundles",
"--svid-path=/foo/bar",
"--svid-hint=hello world",
"--dns-san=foo.example.com",
"--dns-san=bar.example.com",
"--ip-san=192.168.1.1",
"--ip-san=192.168.1.2",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
require.Len(t, cfg.Services, 1)
// It must configure a SPIFFE output with a directory destination.
svc := cfg.Services[0]
spiffe, ok := svc.(*config.SPIFFESVIDOutput)
require.True(t, ok)
require.True(t, spiffe.IncludeFederatedTrustBundles)
svid := spiffe.SVID
require.Equal(t, "/foo/bar", svid.Path)
require.Equal(t, "hello world", svid.Hint)
sans := svid.SANS
require.ElementsMatch(t, sans.DNS, []string{"foo.example.com", "bar.example.com"})
require.ElementsMatch(t, sans.IP, []string{"192.168.1.1", "192.168.1.2"})
dir, ok := spiffe.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/bar", dir.Path)
},
},
})
}
+3 -311
View File
@@ -23,12 +23,10 @@ import (
"fmt"
"io"
"net/url"
"reflect"
"slices"
"strings"
"time"
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"go.opentelemetry.io/otel"
"gopkg.in/yaml.v3"
@@ -64,155 +62,6 @@ var SupportedJoinMethods = []string{
var log = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentTBot)
// RemainingArgsList is a custom kingpin parser that consumes all remaining
// arguments.
type RemainingArgsList []string
func (r *RemainingArgsList) Set(value string) error {
*r = append(*r, value)
return nil
}
func (r *RemainingArgsList) String() string {
return strings.Join([]string(*r), " ")
}
func (r *RemainingArgsList) IsCumulative() bool {
return true
}
// RemainingArgs returns a list of remaining arguments for the given command.
func RemainingArgs(s kingpin.Settings) (target *[]string) {
target = new([]string)
s.SetValue((*RemainingArgsList)(target))
return
}
// CLIConf is configuration from the CLI.
type CLIConf struct {
ConfigPath string
Debug bool
// LogFormat controls the format of logging. Can be either `json` or `text`.
// By default, this is `text`.
LogFormat string
// AuthServer is a Teleport auth server address. It may either point
// directly to an auth server, or to a Teleport proxy server in which case
// a tunneled auth connection will be established.
// Prefer using Address() to pick an address.
AuthServer string
// DataDir stores the bot's internal data.
DataDir string
// DestinationDir stores the generated end-user certificates.
DestinationDir string
// CAPins is a list of pinned SKPI hashes of trusted auth server CAs, used
// only on first connect.
CAPins []string
// Token is a bot join token.
Token string
// RenewalInterval is the interval at which certificates are renewed, as a
// time.ParseDuration() string. It must be less than the certificate TTL.
RenewalInterval time.Duration
// CertificateTTL is the requested TTL of certificates. It should be some
// multiple of the renewal interval to allow for failed renewals.
CertificateTTL time.Duration
// JoinMethod is the method the bot should use to exchange a token for the
// initial certificate
JoinMethod string
// Oneshot controls whether the bot quits after a single renewal.
Oneshot bool
// InitDir specifies which Destination to initialize if multiple are
// configured.
InitDir string
// BotUser is a Unix username that should be given permission to write
BotUser string
// ReaderUser is the Unix username that will be reading the files
ReaderUser string
// Owner is the user:group that will own the Destination files. Due to SSH
// restrictions on key permissions, it cannot be the same as the reader
// user. If ACL support is unused or unavailable, the reader user will own
// files directly.
Owner string
// Clean is a flag that, if set, instructs `tbot init` to remove existing
// unexpected files.
Clean bool
// ConfigureOutput provides a path that the generated configuration file
// should be written to
ConfigureOutput string
// ProxyServer is the teleport proxy address. Unlike `AuthServer` this must
// explicitly point to a Teleport proxy.
// Example: "example.teleport.sh:443"
ProxyServer string
// Cluster is the name of the Teleport cluster on which resources should
// be accessed.
Cluster string
// RemainingArgs is the remaining string arguments for commands that
// require them.
RemainingArgs []string
// FIPS instructs `tbot` to run in a mode designed to comply with FIPS
// regulations. This means the bot should:
// - Refuse to run if not compiled with boringcrypto
// - Use FIPS relevant endpoints for cloud providers (e.g AWS)
// - Restrict TLS / SSH cipher suites and TLS version
// - RSA2048 or ECDSA with NIST-P256 curve should be used for private key generation
FIPS bool
// DiagAddr is the address the diagnostics http service should listen on.
// If not set, no diagnostics listener is created.
DiagAddr string
// Insecure instructs `tbot` to trust the Auth Server without verifying the CA.
Insecure bool
// Trace indicates whether tracing should be enabled.
Trace bool
// TraceExporter is a manually provided URI to send traces to instead of
// forwarding them to the Auth service.
TraceExporter string
// User is the os login to use for ssh connections.
User string
// Host is the target ssh machine to connect to.
Host string
// Post is the post of the ssh machine to connect on.
Port string
// EnableResumption turns on automatic session resumption to prevent connections from
// being dropped if Proxy connectivity is lost.
EnableResumption bool
// TLSRoutingEnabled indicates whether the cluster has TLS routing enabled.
TLSRoutingEnabled bool
// ConnectionUpgradeRequired indicates that an ALPN connection upgrade is required
// for connections to the cluster.
ConnectionUpgradeRequired bool
// TSHConfigPath is the path to a tsh config file.
TSHConfigPath string
}
// AzureOnboardingConfig holds configuration relevant to the "azure" join method.
type AzureOnboardingConfig struct {
// ClientID of the managed identity to use. Required if the VM has more
@@ -622,7 +471,9 @@ func (conf *BotConfig) GetInitables() []Initable {
return out
}
func destinationFromURI(uriString string) (bot.Destination, error) {
// DestinationFromURI parses a URI from the input string and returns a matching
// bot.Destination implementation, if possible.
func DestinationFromURI(uriString string) (bot.Destination, error) {
uri, err := url.Parse(uriString)
if err != nil {
return nil, trace.Wrap(err, "parsing --data-dir")
@@ -674,165 +525,6 @@ func destinationFromURI(uriString string) (bot.Destination, error) {
}
}
// FromCLIConf loads bot config from CLI parameters, potentially loading and
// merging a configuration file if specified. CheckAndSetDefaults() will
// be called. Note that CLI flags, if specified, will override file values.
func FromCLIConf(cf *CLIConf) (*BotConfig, error) {
var config *BotConfig
var err error
if cf.ConfigPath != "" {
config, err = ReadConfigFromFile(cf.ConfigPath, false)
if err != nil {
return nil, trace.Wrap(err, "loading bot config from path %s", cf.ConfigPath)
}
} else {
config = &BotConfig{}
}
if cf.Debug {
config.Debug = true
}
if cf.Oneshot {
config.Oneshot = true
}
if cf.AuthServer != "" {
if config.AuthServer != "" {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding destinations",
"config_path", cf.ConfigPath,
)
}
config.AuthServer = cf.AuthServer
}
if cf.ProxyServer != "" {
if config.ProxyServer != "" {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding destinations",
"config_path", cf.ConfigPath,
)
}
config.ProxyServer = cf.ProxyServer
}
if cf.CertificateTTL != 0 {
if config.CertificateTTL != 0 {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding destinations",
"config_path", cf.ConfigPath,
)
}
config.CertificateTTL = cf.CertificateTTL
}
if cf.RenewalInterval != 0 {
if config.RenewalInterval != 0 {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding destinations",
"config_path", cf.ConfigPath,
)
}
config.RenewalInterval = cf.RenewalInterval
}
// DataDir overrides any previously-configured storage config
if cf.DataDir != "" {
if config.Storage != nil && config.Storage.Destination != nil {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding destinations",
"config_path", cf.ConfigPath,
)
}
dest, err := destinationFromURI(cf.DataDir)
if err != nil {
return nil, trace.Wrap(err)
}
config.Storage = &StorageConfig{Destination: dest}
}
if cf.DestinationDir != "" {
// WARNING:
// See: https://github.com/gravitational/teleport/issues/27206 for
// potential gotchas that currently exist when dealing with this
// override behavior.
// CLI only supports a single filesystem Destination with SSH client config
// and all roles.
if len(config.Services) > 0 {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding destinations",
"config_path", cf.ConfigPath,
)
}
// When using the CLI --Destination-dir we configure an Identity type
// output for that directory.
config.Services = []ServiceConfig{
&IdentityOutput{
Destination: &DestinationDirectory{
Path: cf.DestinationDir,
},
},
}
}
// If any onboarding flags are set, override the whole section.
// (CAPath, CAPins, etc follow different codepaths so we don't want a
// situation where different fields become set weirdly due to struct
// merging)
if cf.Token != "" || cf.JoinMethod != "" || len(cf.CAPins) > 0 {
if !reflect.DeepEqual(config.Onboarding, OnboardingConfig{}) {
// To be safe, warn about possible confusion.
log.WarnContext(
context.TODO(),
"CLI parameters are overriding destinations",
"config_path", cf.ConfigPath,
)
}
config.Onboarding = OnboardingConfig{
CAPins: cf.CAPins,
JoinMethod: types.JoinMethod(cf.JoinMethod),
}
config.Onboarding.SetToken(cf.Token)
}
if cf.FIPS {
config.FIPS = cf.FIPS
}
if cf.DiagAddr != "" {
if config.DiagAddr != "" {
log.WarnContext(
context.TODO(),
"CLI parameters are overriding destinations",
"config_path", cf.ConfigPath,
)
}
config.DiagAddr = cf.DiagAddr
}
if err := config.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err, "validating merged bot config")
}
if cf.Insecure {
config.Insecure = true
}
return config, nil
}
// ReadConfigFromFile reads and parses a YAML config from a file.
func ReadConfigFromFile(filePath string, manualMigration bool) (*BotConfig, error) {
f, err := utils.OpenFileAllowingUnsafeLinks(filePath)
+5
View File
@@ -61,3 +61,8 @@ func (sc *StorageConfig) UnmarshalYAML(node *yaml.Node) error {
sc.Destination = dest
return nil
}
// GetDefaultStoragePath returns the default internal storage path for tbot.
func GetDefaultStoragePath() string {
return defaultStoragePath
}
+1 -46
View File
@@ -30,56 +30,11 @@ import (
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/tbot/bot"
"github.com/gravitational/teleport/lib/tbot/botfs"
"github.com/gravitational/teleport/lib/utils/golden"
)
func TestConfigCLIOnlySample(t *testing.T) {
// Test the sample config generated by `tctl bots add ...`
cf := CLIConf{
DestinationDir: "/tmp/foo",
Token: "foo",
CAPins: []string{"abc123"},
AuthServer: "auth.example.com",
DiagAddr: "127.0.0.1:1337",
Debug: true,
JoinMethod: string(types.JoinMethodToken),
}
cfg, err := FromCLIConf(&cf)
require.NoError(t, err)
require.Equal(t, cf.AuthServer, cfg.AuthServer)
require.NotNil(t, cfg.Onboarding)
token, err := cfg.Onboarding.Token()
require.NoError(t, err)
require.Equal(t, cf.Token, token)
require.Equal(t, cf.CAPins, cfg.Onboarding.CAPins)
// Storage is still default
storageImpl, ok := cfg.Storage.Destination.(*DestinationDirectory)
require.True(t, ok)
require.Equal(t, defaultStoragePath, storageImpl.Path)
// A single default Destination should exist
require.Len(t, cfg.Services, 1)
output := cfg.Services[0]
identOutput, ok := output.(*IdentityOutput)
require.True(t, ok)
destImpl := identOutput.GetDestination()
require.NoError(t, err)
destImplReal, ok := destImpl.(*DestinationDirectory)
require.True(t, ok)
require.Equal(t, cf.DestinationDir, destImplReal.Path)
require.Equal(t, cf.Debug, cfg.Debug)
require.Equal(t, cf.DiagAddr, cfg.DiagAddr)
}
func TestConfigFile(t *testing.T) {
configData := fmt.Sprintf(exampleConfigFile, "foo")
cfg, err := ReadConfig(strings.NewReader(configData), false)
@@ -214,7 +169,7 @@ func TestDestinationFromURI(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.in, func(t *testing.T) {
got, err := destinationFromURI(tt.in)
got, err := DestinationFromURI(tt.in)
if tt.wantErr {
require.Error(t, err)
return
+10
View File
@@ -196,3 +196,13 @@ func (o *DatabaseOutput) UnmarshalYAML(node *yaml.Node) error {
func (o *DatabaseOutput) Type() string {
return DatabaseOutputType
}
// SupportedDatabaseFormatStrings returns a constant list of all valid
// DatabaseFormat values as strings.
func SupportedDatabaseFormatStrings() (ret []string) {
for _, v := range databaseFormats {
ret = append(ret, string(v))
}
return
}
+12 -6
View File
@@ -23,12 +23,18 @@ import (
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot/cli"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/tshwrap"
"github.com/gravitational/teleport/lib/utils"
)
func onDBCommand(botConfig *config.BotConfig, cf *config.CLIConf) error {
func onDBCommand(globalCfg *cli.GlobalArgs, dbCmd *cli.DBCommand) error {
botConfig, err := cli.LoadConfigWithMutators(globalCfg)
if err != nil {
return trace.Wrap(err)
}
wrapper, err := tshwrap.New()
if err != nil {
return trace.Wrap(err)
@@ -50,16 +56,16 @@ func onDBCommand(botConfig *config.BotConfig, cf *config.CLIConf) error {
return trace.Wrap(err)
}
args := []string{"-i", identityPath, "db", "--proxy=" + cf.ProxyServer}
if cf.Cluster != "" {
args := []string{"-i", identityPath, "db", "--proxy=" + dbCmd.ProxyServer}
if dbCmd.Cluster != "" {
// If we caught --cluster in our args, pass it through.
args = append(args, "--cluster="+cf.Cluster)
} else if !utils.HasPrefixAny("--cluster", cf.RemainingArgs) {
args = append(args, "--cluster="+dbCmd.Cluster)
} else if !utils.HasPrefixAny("--cluster", *dbCmd.RemainingArgs) {
// If no `--cluster` was provided after a `--`, pass along the cluster
// name in the identity.
args = append(args, "--cluster="+identity.RouteToCluster)
}
args = append(args, cf.RemainingArgs...)
args = append(args, *dbCmd.RemainingArgs...)
// Pass through the debug flag, and prepend to satisfy argument ordering
// needs (`-d` must precede `db`).
+20 -15
View File
@@ -33,6 +33,7 @@ import (
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/lib/tbot/botfs"
"github.com/gravitational/teleport/lib/tbot/cli"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/identity"
)
@@ -372,16 +373,16 @@ func getOwner(cliOwner, defaultOwner string) (*user.User, *user.Group, error) {
// getAndTestACLOptions gets options needed to configure an ACL from CLI
// options and attempts to configure a test ACL to validate them. Ownership is
// not validated here.
func getAndTestACLOptions(cf *config.CLIConf, destDir string) (*user.User, *user.Group, *botfs.ACLOptions, error) {
if cf.BotUser == "" {
func getAndTestACLOptions(initCmd *cli.InitCommand, destDir string) (*user.User, *user.Group, *botfs.ACLOptions, error) {
if initCmd.BotUser == "" {
return nil, nil, nil, trace.BadParameter("--bot-user must be set")
}
if cf.ReaderUser == "" {
if initCmd.ReaderUser == "" {
return nil, nil, nil, trace.BadParameter("--reader-user must be set")
}
botUser, err := user.Lookup(cf.BotUser)
botUser, err := user.Lookup(initCmd.BotUser)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
@@ -391,7 +392,7 @@ func getAndTestACLOptions(cf *config.CLIConf, destDir string) (*user.User, *user
return nil, nil, nil, trace.Wrap(err)
}
readerUser, err := user.Lookup(cf.ReaderUser)
readerUser, err := user.Lookup(initCmd.ReaderUser)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
@@ -405,7 +406,7 @@ func getAndTestACLOptions(cf *config.CLIConf, destDir string) (*user.User, *user
// know the bot user definitely exists and is a reasonable owner choice.
defaultOwner := fmt.Sprintf("%s:%s", botUser.Username, botGroup.Name)
ownerUser, ownerGroup, err := getOwner(cf.Owner, defaultOwner)
ownerUser, ownerGroup, err := getOwner(initCmd.Owner, defaultOwner)
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
@@ -420,17 +421,21 @@ func getAndTestACLOptions(cf *config.CLIConf, destDir string) (*user.User, *user
return ownerUser, ownerGroup, &opts, nil
}
func onInit(botConfig *config.BotConfig, cf *config.CLIConf) error {
func onInit(globals *cli.GlobalArgs, init *cli.InitCommand) error {
botConfig, err := cli.LoadConfigWithMutators(globals, init)
if err != nil {
return trace.Wrap(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var err error
initables := botConfig.GetInitables()
var target config.Initable
// First, resolve the correct output/service. If using a config file with
// only 1 destination we can assume we want to init that one; otherwise,
// --init-dir is required.
if cf.InitDir == "" {
if init.InitDir == "" {
if len(initables) == 1 {
target = initables[0]
} else {
@@ -440,13 +445,13 @@ func onInit(botConfig *config.BotConfig, cf *config.CLIConf) error {
for _, v := range initables {
d := v.GetDestination()
dirDest, ok := d.(*config.DestinationDirectory)
if ok && dirDest.Path == cf.InitDir {
if ok && dirDest.Path == init.InitDir {
target = v
break
}
}
if target == nil {
return trace.NotFound("Could not find specified destination %q", cf.InitDir)
return trace.NotFound("Could not find specified destination %q", init.InitDir)
}
}
@@ -474,7 +479,7 @@ func onInit(botConfig *config.BotConfig, cf *config.CLIConf) error {
log.DebugContext(ctx, "Testing for ACL support")
// Awkward control flow here, but we want these to fail together.
ownerUser, ownerGroup, aclOpts, err = getAndTestACLOptions(cf, destDir.Path)
ownerUser, ownerGroup, aclOpts, err = getAndTestACLOptions(init, destDir.Path)
if err != nil {
if destDir.ACLs == botfs.ACLRequired {
// ACLs were specifically requested (vs "try" mode), so fail.
@@ -487,7 +492,7 @@ func onInit(botConfig *config.BotConfig, cf *config.CLIConf) error {
// We'll also need to re-fetch the owner as the defaults are
// different in the fallback case.
ownerUser, ownerGroup, err = getOwner(cf.Owner, "")
ownerUser, ownerGroup, err = getOwner(init.Owner, "")
if err != nil {
return trace.Wrap(err)
}
@@ -501,7 +506,7 @@ func onInit(botConfig *config.BotConfig, cf *config.CLIConf) error {
}
default:
log.InfoContext(ctx, "ACLs disabled for this destination")
ownerUser, ownerGroup, err = getOwner(cf.Owner, "")
ownerUser, ownerGroup, err = getOwner(init.Owner, "")
if err != nil {
return trace.Wrap(err)
}
@@ -534,7 +539,7 @@ func onInit(botConfig *config.BotConfig, cf *config.CLIConf) error {
}
// ... and warn about / remove any unneeded files.
if len(toRemove) > 0 && cf.Clean {
if len(toRemove) > 0 && init.Clean {
log.InfoContext(ctx, "Attempting to remove", "path", toRemove)
var errors []error
+50 -29
View File
@@ -30,9 +30,11 @@ import (
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/lib/tbot/botfs"
"github.com/gravitational/teleport/lib/tbot/cli"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/identity"
)
@@ -104,20 +106,25 @@ func getACLOptions() (*botfs.ACLOptions, error) {
}, nil
}
// testConfigFromCLI creates a BotConfig from the given CLI config.
func testConfigFromCLI(t *testing.T, cf *config.CLIConf) *config.BotConfig {
cfg, err := config.FromCLIConf(cf)
require.NoError(t, err)
return cfg
}
// testConfigFromString parses a YAML config file from a string.
func testConfigFromString(t *testing.T, yaml string) *config.BotConfig {
cfg, err := config.ReadConfig(strings.NewReader(yaml), false)
func testConfigFromString(t *testing.T, yamlStr string) *config.BotConfig {
// Load YAML to validate syntax.
cfg, err := config.ReadConfig(strings.NewReader(yamlStr), false)
require.NoError(t, err)
require.NoError(t, cfg.CheckAndSetDefaults())
// Reencode as a string
out := &strings.Builder{}
enc := yaml.NewEncoder(out)
enc.SetIndent(2)
err = enc.Encode(cfg)
require.NoError(t, err)
// Load and return the static config
globalArgs := cli.NewGlobalArgsWithStaticConfig(out.String())
cfg, err = cli.LoadConfigWithMutators(globalArgs)
require.NoError(t, err)
return cfg
}
@@ -144,14 +151,18 @@ func validateFileDestination(t *testing.T, svc config.Initable) *config.Destinat
// specified, this never tries to use ACLs.
func TestInit(t *testing.T) {
dir := t.TempDir()
cf := &config.CLIConf{
AuthServer: "example.com",
DestinationDir: dir,
cmd := &cli.InitCommand{
LegacyDestinationDirArgs: &cli.LegacyDestinationDirArgs{
DestinationDir: dir,
},
AuthProxyArgs: cli.NewStaticAuthServer("example.com"),
}
cfg := testConfigFromCLI(t, cf)
// Run init.
require.NoError(t, onInit(cfg, cf))
require.NoError(t, onInit(&cli.GlobalArgs{}, cmd))
cfg, err := cli.LoadConfigWithMutators(&cli.GlobalArgs{}, cmd)
require.NoError(t, err)
// Make sure everything was created.
_ = validateFileDestination(t, cfg.GetInitables()[0])
@@ -185,20 +196,25 @@ func TestInitMaybeACLs(t *testing.T) {
// Note: we'll use the current user as owner as that's the only way to
// guarantee ACL write access.
dir := t.TempDir()
cf := &config.CLIConf{
AuthServer: "example.com",
DestinationDir: dir,
BotUser: opts.BotUser.Username,
ReaderUser: opts.ReaderUser.Username,
cmd := &cli.InitCommand{
LegacyDestinationDirArgs: &cli.LegacyDestinationDirArgs{
DestinationDir: dir,
},
BotUser: opts.BotUser.Username,
ReaderUser: opts.ReaderUser.Username,
// This isn't a default, but unfortunately we need to specify a
// non-nobody owner for CI purposes.
Owner: fmt.Sprintf("%s:%s", currentUser.Username, currentGroup.Name),
AuthProxyArgs: cli.NewStaticAuthServer("example.com"),
}
cfg := testConfigFromCLI(t, cf)
cfg, err := cli.LoadConfigWithMutators(&cli.GlobalArgs{}, cmd)
require.NoError(t, err)
// Run init.
require.NoError(t, onInit(cfg, cf))
require.NoError(t, onInit(&cli.GlobalArgs{}, cmd))
// Make sure everything was created.
destDir := validateFileDestination(t, cfg.GetInitables()[0])
@@ -238,14 +254,18 @@ func TestInitSymlink(t *testing.T) {
require.NoError(t, os.Symlink(realPath, dataDir))
// Should fail due to symlink in path.
cfg := testConfigFromString(t, fmt.Sprintf(testInitSymlinksTemplate, dataDir, botfs.SymlinksSecure))
require.Error(t, onInit(cfg, &config.CLIConf{}))
cfgStr := fmt.Sprintf(testInitSymlinksTemplate, dataDir, botfs.SymlinksSecure)
globals := cli.NewGlobalArgsWithStaticConfig(cfgStr)
require.Error(t, onInit(globals, &cli.InitCommand{}))
// Should succeed when writing to the dir directly.
cfg = testConfigFromString(t, fmt.Sprintf(testInitSymlinksTemplate, realPath, botfs.SymlinksSecure))
require.NoError(t, onInit(cfg, &config.CLIConf{}))
cfgStr = fmt.Sprintf(testInitSymlinksTemplate, realPath, botfs.SymlinksSecure)
globals = cli.NewGlobalArgsWithStaticConfig(cfgStr)
require.NoError(t, onInit(globals, &cli.InitCommand{}))
// Make sure everything was created.
// Make sure everything was created. We'll have to rebuild the config from
// scratch since we don't have a copy available.
cfg := testConfigFromString(t, cfgStr)
_ = validateFileDestination(t, cfg.GetInitables()[0])
}
@@ -258,6 +278,7 @@ func TestInitSymlinkInsecure(t *testing.T) {
require.NoError(t, os.Symlink(realPath, dataDir))
// Should succeed due to SymlinksInsecure
cfg := testConfigFromString(t, fmt.Sprintf(testInitSymlinksTemplate, dataDir, botfs.SymlinksInsecure))
require.Error(t, onInit(cfg, &config.CLIConf{}))
globals := cli.NewGlobalArgsWithStaticConfig(fmt.Sprintf(testInitSymlinksTemplate, dataDir, botfs.SymlinksInsecure))
require.Error(t, onInit(globals, &cli.InitCommand{}))
}
+5 -3
View File
@@ -33,8 +33,8 @@ import (
clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"github.com/gravitational/teleport/api/identityfile"
"github.com/gravitational/teleport/lib/tbot/cli"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/tshwrap"
"github.com/gravitational/teleport/lib/tlsca"
)
@@ -65,8 +65,10 @@ func getCredentialData(idFile *identityfile.IdentityFile, currentTime time.Time)
return data, nil
}
func onKubeCredentialsCommand(ctx context.Context, cfg *config.BotConfig) error {
destination, err := tshwrap.GetDestinationDirectory(cfg)
func onKubeCredentialsCommand(
ctx context.Context, kubeCredentialsCmd *cli.KubeCredentialsCommand,
) error {
destination, err := config.DestinationFromURI(kubeCredentialsCmd.DestinationDir)
if err != nil {
return trace.Wrap(err)
}
+111 -171
View File
@@ -27,7 +27,6 @@ import (
"runtime"
"runtime/pprof"
runtimetrace "runtime/trace"
"strings"
"time"
"github.com/gravitational/trace"
@@ -38,6 +37,7 @@ import (
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/observability/tracing"
"github.com/gravitational/teleport/lib/tbot"
"github.com/gravitational/teleport/lib/tbot/cli"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tpm"
"github.com/gravitational/teleport/lib/utils"
@@ -46,12 +46,6 @@ import (
var log = logutils.NewPackageLogger(teleport.ComponentKey, teleport.ComponentTBot)
const (
authServerEnvVar = "TELEPORT_AUTH_SERVER"
tokenEnvVar = "TELEPORT_BOT_TOKEN"
proxyServerEnvVar = "TELEPORT_PROXY"
)
func main() {
if err := Run(os.Args[1:], os.Stdout); err != nil {
utils.FatalError(err)
@@ -66,126 +60,90 @@ access Teleport protected resources in the same way your engineers do!
Find out more at https://goteleport.com/docs/machine-id/introduction/`
func Run(args []string, stdout io.Writer) error {
var cf config.CLIConf
ctx := context.Background()
var cpuProfile, memProfile, traceProfile string
var cpuProfile, memProfile, traceProfile, configureOutPath string
app := utils.InitCLIParser("tbot", appHelp).Interspersed(false)
app.Flag("debug", "Verbose logging to stdout.").Short('d').BoolVar(&cf.Debug)
app.Flag("config", "Path to a configuration file.").Short('c').StringVar(&cf.ConfigPath)
app.Flag("fips", "Runs tbot in FIPS compliance mode. This requires the FIPS binary is in use.").BoolVar(&cf.FIPS)
app.Flag("trace", "Capture and export distributed traces.").Hidden().BoolVar(&cf.Trace)
app.Flag("trace-exporter", "An OTLP exporter URL to send spans to.").Hidden().StringVar(&cf.TraceExporter)
globalCfg := cli.NewGlobalArgs(app)
// Miscellaneous args exposed globally but handled here.
app.Flag("mem-profile", "Write memory profile to file").Hidden().StringVar(&memProfile)
app.Flag("cpu-profile", "Write CPU profile to file").Hidden().StringVar(&cpuProfile)
app.Flag("trace-profile", "Write trace profile to file").Hidden().StringVar(&traceProfile)
app.HelpFlag.Short('h')
joinMethodList := fmt.Sprintf(
"(%s)",
strings.Join(config.SupportedJoinMethods, ", "),
)
// Construct the top-level subcommands.
versionCmd := app.Command("version", "Print the version of your tbot binary.")
startCmd := app.Command("start", "Starts the renewal bot, writing certificates to the data dir at a set interval.")
startCmd.Flag("auth-server", "Address of the Teleport Auth Server. Prefer using --proxy-server where possible.").Short('a').Envar(authServerEnvVar).StringVar(&cf.AuthServer)
startCmd.Flag("proxy-server", "Address of the Teleport Proxy Server.").Envar(proxyServerEnvVar).StringVar(&cf.ProxyServer)
startCmd.Flag("token", "A bot join token or path to file with token value, if attempting to onboard a new bot; used on first connect.").Envar(tokenEnvVar).StringVar(&cf.Token)
startCmd.Flag("ca-pin", "CA pin to validate the Teleport Auth Server; used on first connect.").StringsVar(&cf.CAPins)
startCmd.Flag("data-dir", "Directory to store internal bot data. Access to this directory should be limited.").StringVar(&cf.DataDir)
startCmd.Flag("destination-dir", "Directory to write short-lived machine certificates.").StringVar(&cf.DestinationDir)
startCmd.Flag("certificate-ttl", "TTL of short-lived machine certificates.").DurationVar(&cf.CertificateTTL)
startCmd.Flag("renewal-interval", "Interval at which short-lived certificates are renewed; must be less than the certificate TTL.").DurationVar(&cf.RenewalInterval)
startCmd.Flag("insecure", "Insecure configures the bot to trust the certificates from the Auth Server or Proxy on first connect without verification. Do not use in production.").BoolVar(&cf.Insecure)
startCmd.Flag("join-method", "Method to use to join the cluster. "+joinMethodList).EnumVar(&cf.JoinMethod, config.SupportedJoinMethods...)
startCmd.Flag("oneshot", "If set, quit after the first renewal.").BoolVar(&cf.Oneshot)
startCmd.Flag("diag-addr", "If set and the bot is in debug mode, a diagnostics service will listen on specified address.").StringVar(&cf.DiagAddr)
startCmd.Flag("log-format", "Controls the format of output logs. Can be `json` or `text`. Defaults to `text`.").
Default(utils.LogFormatText).
EnumVar(&cf.LogFormat, utils.LogFormatJSON, utils.LogFormatText)
kubeCmd := app.Command("kube", "Kubernetes helpers").Hidden()
initCmd := app.Command("init", "Initialize a certificate destination directory for writes from a separate bot user.")
initCmd.Flag("destination-dir", "Directory to write short-lived machine certificates to.").StringVar(&cf.DestinationDir)
initCmd.Flag("owner", "Defines Linux \"user:group\" owner of \"--destination-dir\". Defaults to the Linux user running tbot if unspecified.").StringVar(&cf.Owner)
initCmd.Flag("bot-user", "Enables POSIX ACLs and defines Linux user that can read/write short-lived certificates to \"--destination-dir\".").StringVar(&cf.BotUser)
initCmd.Flag("reader-user", "Enables POSIX ACLs and defines Linux user that will read short-lived certificates from \"--destination-dir\".").StringVar(&cf.ReaderUser)
initCmd.Flag("init-dir", "If using a config file and multiple destinations are configured, controls which destination dir to configure.").StringVar(&cf.InitDir)
initCmd.Flag("clean", "If set, remove unexpected files and directories from the destination.").BoolVar(&cf.Clean)
initCmd.Flag("log-format", "Controls the format of output logs. Can be `json` or `text`. Defaults to `text`.").
Default(utils.LogFormatText).
EnumVar(&cf.LogFormat, utils.LogFormatJSON, utils.LogFormatText)
startCmd := app.Command("start", "Starts the renewal bot, writing certificates to the data dir at a set interval.")
configureCmd := app.Command("configure", "Creates a config file based on flags provided, and writes it to stdout or a file (-c <path>).")
configureCmd.Flag("auth-server", "Address of the Teleport Auth Server. Prefer using --proxy-server where possible.").Short('a').Envar(authServerEnvVar).StringVar(&cf.AuthServer)
configureCmd.Flag("proxy-server", "Address of the Teleport Proxy Server.").Envar(proxyServerEnvVar).StringVar(&cf.ProxyServer)
configureCmd.Flag("ca-pin", "CA pin to validate the Teleport Auth Server; used on first connect.").StringsVar(&cf.CAPins)
configureCmd.Flag("certificate-ttl", "TTL of short-lived machine certificates.").Default("60m").DurationVar(&cf.CertificateTTL)
configureCmd.Flag("data-dir", "Directory to store internal bot data. Access to this directory should be limited.").StringVar(&cf.DataDir)
configureCmd.Flag("insecure", "Insecure configures the bot to trust the certificates from the Auth Server or Proxy on first connect without verification. Do not use in production.").BoolVar(&cf.Insecure)
configureCmd.Flag("join-method", "Method to use to join the cluster. "+joinMethodList).EnumVar(&cf.JoinMethod, config.SupportedJoinMethods...)
configureCmd.Flag("oneshot", "If set, quit after the first renewal.").BoolVar(&cf.Oneshot)
configureCmd.Flag("renewal-interval", "Interval at which short-lived certificates are renewed; must be less than the certificate TTL.").DurationVar(&cf.RenewalInterval)
configureCmd.Flag("token", "A bot join token, if attempting to onboard a new bot; used on first connect.").Envar(tokenEnvVar).StringVar(&cf.Token)
configureCmd.Flag("output", "Path to write the generated configuration file to rather than write to stdout.").Short('o').StringVar(&cf.ConfigureOutput)
configureCmd.Flag("log-format", "Controls the format of output logs. Can be `json` or `text`. Defaults to `text`.").
Default(utils.LogFormatText).
EnumVar(&cf.LogFormat, utils.LogFormatJSON, utils.LogFormatText)
configureCmd.Flag("output", "Path to write the generated configuration file to rather than write to stdout.").Short('o').StringVar(&configureOutPath)
migrateCmd := app.Command("migrate", "Migrates a config file from an older version to the newest version. Outputs to stdout by default.")
migrateCmd.Flag("output", "Path to write the generated configuration file to rather than write to stdout.").Short('o').StringVar(&cf.ConfigureOutput)
// TODO: consider discarding config flag for non-legacy. These should always be self contained.
legacyProxyFlag := ""
// Initialize all new-style commands.
var commands []cli.CommandRunner
commands = append(commands,
cli.NewInitCommand(app, func(init *cli.InitCommand) error {
return onInit(globalCfg, init)
}),
dbCmd := app.Command("db", "Execute database commands through tsh.")
dbCmd.Flag("proxy-server", "The Teleport proxy server to use, in host:port form.").StringVar(&cf.ProxyServer)
// We're migrating from --proxy to --proxy-server so this flag is hidden
// but still supported.
// TODO(strideynet): DELETE IN 17.0.0
dbCmd.Flag("proxy", "The Teleport proxy server to use, in host:port form.").Hidden().Envar(proxyServerEnvVar).StringVar(&legacyProxyFlag)
dbCmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&cf.DestinationDir)
dbCmd.Flag("cluster", "The cluster name. Extracted from the certificate if unset.").StringVar(&cf.Cluster)
dbRemaining := config.RemainingArgs(dbCmd.Arg(
"args",
"Arguments to `tsh db ...`; prefix with `-- ` to ensure flags are passed correctly.",
))
cli.NewMigrateCommand(app, func(migrateCfg *cli.MigrateCommand) error {
return onMigrate(ctx, globalCfg, migrateCfg, stdout)
}),
proxyCmd := app.Command("proxy", "Start a local TLS proxy via tsh to connect to Teleport in single-port mode.")
proxyCmd.Flag("proxy-server", "The Teleport proxy server to use, in host:port form.").Envar(proxyServerEnvVar).StringVar(&cf.ProxyServer)
// We're migrating from --proxy to --proxy-server so this flag is hidden
// but still supported.
// TODO(strideynet): DELETE IN 17.0.0
proxyCmd.Flag("proxy", "The Teleport proxy server to use, in host:port form.").Hidden().StringVar(&legacyProxyFlag)
proxyCmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&cf.DestinationDir)
proxyCmd.Flag("cluster", "The cluster name. Extracted from the certificate if unset.").StringVar(&cf.Cluster)
proxyRemaining := config.RemainingArgs(proxyCmd.Arg(
"args",
"Arguments to `tsh proxy ...`; prefix with `-- ` to ensure flags are passed correctly.",
))
cli.NewSSHProxyCommand(app, func(sshProxyCommand *cli.SSHProxyCommand) error {
return onSSHProxyCommand(ctx, globalCfg, sshProxyCommand)
}),
sshProxyCmd := app.Command("ssh-proxy-command", "An OpenSSH/PuTTY proxy command").Hidden()
sshProxyCmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&cf.DestinationDir)
sshProxyCmd.Flag("cluster", "The cluster name. Extracted from the certificate if unset.").StringVar(&cf.Cluster)
sshProxyCmd.Flag("user", "The remote user name for the connection").Required().StringVar(&cf.User)
sshProxyCmd.Flag("host", "The remote host to connect to").Required().StringVar(&cf.Host)
sshProxyCmd.Flag("port", "The remote port to connect on.").StringVar(&cf.Port)
sshProxyCmd.Flag("proxy-server", "The Teleport proxy server to use, in host:port form.").Required().StringVar(&cf.ProxyServer)
sshProxyCmd.Flag("tls-routing", "Whether the Teleport cluster has tls routing enabled.").Required().BoolVar(&cf.TLSRoutingEnabled)
sshProxyCmd.Flag("connection-upgrade", "Whether the Teleport cluster requires an ALPN connection upgrade.").Required().BoolVar(&cf.ConnectionUpgradeRequired)
sshProxyCmd.Flag("proxy-templates", "The path to a file containing proxy templates to be evaluated.").StringVar(&cf.TSHConfigPath)
sshProxyCmd.Flag("resume", "Enable SSH connection resumption").BoolVar(&cf.EnableResumption)
cli.NewProxyCommand(app, func(proxyCmd *cli.ProxyCommand) error {
return onProxyCommand(ctx, globalCfg, proxyCmd)
}),
sshMultiplexProxyCmd := app.Command("ssh-multiplexer-proxy-command", "An OpenSSH compatible ProxyCommand which connects to a long-lived tbot running the ssh-multiplexer service").Hidden()
var sshMultiplexSocket string
var sshMultiplexData string
sshMultiplexProxyCmd.Arg("path", "Path to the listener socket.").Required().StringVar(&sshMultiplexSocket)
sshMultiplexProxyCmd.Arg("data", "Connection target.").Required().StringVar(&sshMultiplexData)
cli.NewDBCommand(app, func(dbCmd *cli.DBCommand) error {
return onDBCommand(globalCfg, dbCmd)
}),
kubeCmd := app.Command("kube", "Kubernetes helpers").Hidden()
kubeCredentialsCmd := kubeCmd.Command("credentials", "Get credentials for kubectl access").Hidden()
kubeCredentialsCmd.Flag("destination-dir", "The destination directory with which to generate Kubernetes credentials").Required().StringVar(&cf.DestinationDir)
cli.NewSSHMultiplexerProxyCommand(app, func(c *cli.SSHMultiplexerProxyCommand) error {
return onSSHMultiplexProxyCommand(ctx, c.Socket, c.Data)
}),
cli.NewKubeCredentialsCommand(kubeCmd, func(kubeCredentialsCmd *cli.KubeCredentialsCommand) error {
return onKubeCredentialsCommand(ctx, kubeCredentialsCmd)
}),
// `start` and `configure` commands
cli.NewLegacyCommand(startCmd, buildConfigAndStart(ctx, globalCfg)),
cli.NewLegacyCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout)),
cli.NewIdentityCommand(startCmd, buildConfigAndStart(ctx, globalCfg)),
cli.NewIdentityCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout)),
cli.NewDatabaseCommand(startCmd, buildConfigAndStart(ctx, globalCfg)),
cli.NewDatabaseCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout)),
cli.NewKubernetesCommand(startCmd, buildConfigAndStart(ctx, globalCfg)),
cli.NewKubernetesCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout)),
cli.NewApplicationCommand(startCmd, buildConfigAndStart(ctx, globalCfg)),
cli.NewApplicationCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout)),
cli.NewApplicationTunnelCommand(startCmd, buildConfigAndStart(ctx, globalCfg)),
cli.NewApplicationTunnelCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout)),
cli.NewDatabaseTunnelCommand(startCmd, buildConfigAndStart(ctx, globalCfg)),
cli.NewDatabaseTunnelCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout)),
cli.NewSPIFFESVIDCommand(startCmd, buildConfigAndStart(ctx, globalCfg)),
cli.NewSPIFFESVIDCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout)),
)
// Initialize legacy-style commands. These are simple enough to not really
// benefit from conversion to a new-style command.
spiffeInspectPath := ""
spiffeInspectCmd := app.Command("spiffe-inspect", "Inspects a SPIFFE Workload API endpoint to ensure it is working correctly.")
spiffeInspectCmd.Flag("path", "The path to the SPIFFE Workload API endpoint to test.").Required().StringVar(&spiffeInspectPath)
@@ -203,34 +161,17 @@ func Run(args []string, stdout io.Writer) error {
}
// Logging must be configured as early as possible to ensure all log
// message are formatted correctly.
if err := setupLogger(cf.Debug, cf.LogFormat); err != nil {
if err := setupLogger(globalCfg.Debug, globalCfg.LogFormat); err != nil {
return trace.Wrap(err, "setting up logger")
}
if legacyProxyFlag != "" {
cf.ProxyServer = legacyProxyFlag
log.WarnContext(
ctx,
"The --proxy flag is deprecated and will be removed in v17.0.0. Use --proxy-server instead",
)
}
// Remaining args are stored directly to a []string rather than written to
// a shared ref like most other kingpin args, so we'll need to manually
// move them to the remaining args field.
if len(*dbRemaining) > 0 {
cf.RemainingArgs = *dbRemaining
} else if len(*proxyRemaining) > 0 {
cf.RemainingArgs = *proxyRemaining
}
if cf.Trace {
if globalCfg.Trace {
log.InfoContext(
ctx,
"Initializing tracing provider. Traces will be exported",
"trace_exporter", cf.TraceExporter,
"trace_exporter", globalCfg.TraceExporter,
)
tp, err := initializeTracing(ctx, cf.TraceExporter)
tp, err := initializeTracing(ctx, globalCfg.TraceExporter)
if err != nil {
return trace.Wrap(err, "initializing tracing")
}
@@ -293,11 +234,8 @@ func Run(args []string, stdout io.Writer) error {
defer runtimetrace.Stop()
}
// Some commands do not need the full context of the config, so we'll
// run these first.
// Manually attempt to run all old-style commands.
switch command {
case migrateCmd.FullCommand():
return onMigrate(ctx, cf, stdout)
case versionCmd.FullCommand():
return onVersion()
case spiffeInspectCmd.FullCommand():
@@ -307,41 +245,49 @@ func Run(args []string, stdout io.Writer) error {
if err != nil {
return trace.Wrap(err, "querying TPM")
}
tpm.PrintQuery(query, cf.Debug, os.Stdout)
tpm.PrintQuery(query, globalCfg.Debug, os.Stdout)
return nil
case configureCmd.FullCommand():
return onConfigure(ctx, cf, stdout)
case sshProxyCmd.FullCommand():
return onSSHProxyCommand(ctx, &cf)
case sshMultiplexProxyCmd.FullCommand():
return onSSHMultiplexProxyCommand(ctx, sshMultiplexSocket, sshMultiplexData)
case installSystemdCmdStr:
return installSystemdCmdFn(ctx, log, cf.ConfigPath, os.Executable, os.Stdout)
return installSystemdCmdFn(ctx, log, globalCfg.ConfigPath, os.Executable, os.Stdout)
}
botConfig, err := config.FromCLIConf(&cf)
if err != nil {
// Attempt to run each new-style command.
for _, cmd := range commands {
match, err := cmd.TryRun(command)
if !match {
continue
}
return trace.Wrap(err)
}
// The rest of the commands rely on the full config
switch command {
case startCmd.FullCommand():
err = onStart(ctx, botConfig)
case initCmd.FullCommand():
err = onInit(botConfig, &cf)
case dbCmd.FullCommand():
err = onDBCommand(botConfig, &cf)
case proxyCmd.FullCommand():
err = onProxyCommand(ctx, botConfig, &cf)
case kubeCredentialsCmd.FullCommand():
err = onKubeCredentialsCommand(ctx, botConfig)
default:
// This should only happen when there's a missing switch case above.
err = trace.BadParameter("command %q not configured", command)
}
return trace.BadParameter("command %q not configured", command)
}
return err
// buildConfigAndStart returns a MutatorAction that will generate a config and
// run `onStart` with the result.
func buildConfigAndStart(ctx context.Context, globals *cli.GlobalArgs) cli.MutatorAction {
return func(mutator cli.ConfigMutator) error {
cfg, err := cli.LoadConfigWithMutators(globals, mutator)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(onStart(ctx, cfg))
}
}
// buildConfigAndConfigure returns a MutatorAction that will generate a config
// and run `onConfigure` with the result.
func buildConfigAndConfigure(ctx context.Context, globals *cli.GlobalArgs, outPath *string, stdout io.Writer) cli.MutatorAction {
return func(mutator cli.ConfigMutator) error {
cfg, err := cli.BaseConfigWithMutators(globals, mutator)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(onConfigure(ctx, cfg, *outPath, stdout))
}
}
func initializeTracing(
@@ -373,11 +319,11 @@ func onVersion() error {
func onConfigure(
ctx context.Context,
cf config.CLIConf,
cfg *config.BotConfig,
outPath string,
stdout io.Writer,
) error {
out := stdout
outPath := cf.ConfigureOutput
if outPath != "" {
f, err := os.Create(outPath)
if err != nil {
@@ -387,13 +333,6 @@ func onConfigure(
out = f
}
// We do not want to load an existing configuration file as this will cause
// it to be merged with the provided flags and defaults.
cf.ConfigPath = ""
cfg, err := config.FromCLIConf(&cf)
if err != nil {
return nil
}
// Ensure they have provided a join method to use in the configuration.
if cfg.Onboarding.JoinMethod == types.JoinMethodUnspecified {
return trace.BadParameter("join method must be provided")
@@ -422,17 +361,18 @@ func onConfigure(
func onMigrate(
ctx context.Context,
cf config.CLIConf,
globalCfg *cli.GlobalArgs,
migrateCmd *cli.MigrateCommand,
stdout io.Writer,
) error {
if cf.ConfigPath == "" {
if globalCfg.ConfigPath == "" {
return trace.BadParameter("source config file must be provided with -c")
}
out := stdout
outPath := cf.ConfigureOutput
outPath := migrateCmd.ConfigureOutput
if outPath != "" {
if outPath == cf.ConfigPath {
if outPath == globalCfg.ConfigPath {
return trace.BadParameter("migrated config output path should not be the same as the source config path")
}
@@ -446,7 +386,7 @@ func onMigrate(
// We do not want to load an existing configuration file as this will cause
// it to be merged with the provided flags and defaults.
cfg, err := config.ReadConfigFromFile(cf.ConfigPath, true)
cfg, err := config.ReadConfigFromFile(globalCfg.ConfigPath, true)
if err != nil {
return trace.Wrap(err)
}
+11 -5
View File
@@ -26,13 +26,19 @@ import (
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/tbot/cli"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/tshwrap"
)
func onProxyCommand(
ctx context.Context, botConfig *config.BotConfig, cf *config.CLIConf,
ctx context.Context, globalCfg *cli.GlobalArgs, proxyCmd *cli.ProxyCommand,
) error {
botConfig, err := cli.LoadConfigWithMutators(globalCfg)
if err != nil {
return trace.Wrap(err)
}
wrapper, err := tshwrap.New()
if err != nil {
return trace.Wrap(err)
@@ -52,8 +58,8 @@ func onProxyCommand(
// TODO(timothyb89): We could consider supporting a --cluster passthrough
// here as in `tbot db ...`.
args := []string{"-i", identityPath, "proxy", "--proxy=" + cf.ProxyServer}
args = append(args, cf.RemainingArgs...)
args := []string{"-i", identityPath, "proxy", "--proxy=" + proxyCmd.ProxyServer}
args = append(args, *proxyCmd.ProxyRemaining...)
// Pass through the debug flag, and prepend to satisfy argument ordering
// needs (`-d` must precede `proxy`).
@@ -63,7 +69,7 @@ func onProxyCommand(
// Handle a special case for `tbot proxy kube` where additional env vars
// need to be injected.
if slices.Contains(cf.RemainingArgs, "kube") {
if slices.Contains(*proxyCmd.ProxyRemaining, "kube") {
// `tsh kube proxy` uses teleport.EnvKubeConfig to determine the
// original kube config file.
env[teleport.EnvKubeConfig] = filepath.Join(
@@ -75,7 +81,7 @@ func onProxyCommand(
destination.Path, "kubeconfig-proxied.yaml",
)
}
if slices.Contains(cf.RemainingArgs, "ssh") {
if slices.Contains(*proxyCmd.ProxyRemaining, "ssh") {
log.WarnContext(ctx, "`tbot proxy ssh` is deprecated and will stop working in v17. See https://goteleport.com/docs/machine-id/reference/v16-upgrade-guide/")
}
+16 -16
View File
@@ -23,7 +23,7 @@ import (
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/tbot"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/tbot/cli"
)
// onSSHProxyCommand is meant to be used as an OpenSSH/PuTTY proxy command. While this
@@ -31,27 +31,27 @@ import (
// `tsh proxy ssh` which results in much less memory and cpu consumption. This will
// eventually supersede `tbot proxy ssh` as it becomes more feature rich and supports
// all the edge cases.
func onSSHProxyCommand(ctx context.Context, cf *config.CLIConf) error {
func onSSHProxyCommand(ctx context.Context, globalCfg *cli.GlobalArgs, sshProxyCmd *cli.SSHProxyCommand) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if cf.Port == "" {
cf.Port = "0"
if sshProxyCmd.Port == "" {
sshProxyCmd.Port = "0"
}
proxySSHConfig := tbot.ProxySSHConfig{
DestinationPath: cf.DestinationDir,
Insecure: cf.Insecure,
FIPS: cf.FIPS,
ProxyServer: cf.ProxyServer,
Cluster: cf.Cluster,
User: cf.User,
Host: cf.Host,
Port: cf.Port,
EnableResumption: cf.EnableResumption,
TLSRoutingEnabled: cf.TLSRoutingEnabled,
ConnectionUpgradeRequired: cf.ConnectionUpgradeRequired,
TSHConfigPath: cf.TSHConfigPath,
Insecure: globalCfg.Insecure,
FIPS: globalCfg.FIPS,
DestinationPath: sshProxyCmd.DestinationDir,
ProxyServer: sshProxyCmd.ProxyServer,
Cluster: sshProxyCmd.Cluster,
User: sshProxyCmd.User,
Host: sshProxyCmd.Host,
Port: sshProxyCmd.Port,
EnableResumption: sshProxyCmd.EnableResumption,
TLSRoutingEnabled: sshProxyCmd.TLSRoutingEnabled,
ConnectionUpgradeRequired: sshProxyCmd.ConnectionUpgradeRequired,
TSHConfigPath: sshProxyCmd.TSHConfigPath,
Log: log,
}