From b809d3f12a7090ec2fe6d494ec5084578d977cd3 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Mon, 14 Dec 2020 16:52:11 -0500 Subject: [PATCH] Add `kube` connect subcommand (#816) --- internal/cmd/commands.go | 14 +++- internal/cmd/commands/connect/connect.go | 19 +++++ internal/cmd/commands/connect/kube.go | 76 +++++++++++++++++++ .../getting-started/connect-to-target.mdx | 20 ++++- 4 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 internal/cmd/commands/connect/kube.go diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 7f23188920..6f9f959586 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -257,10 +257,16 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "http", }, nil }, - "connect ssh": func() (cli.Command, error) { + "connect kube": func() (cli.Command, error) { return &connect.Command{ Command: base.NewCommand(ui), - Func: "ssh", + Func: "kube", + }, nil + }, + "connect postgres": func() (cli.Command, error) { + return &connect.Command{ + Command: base.NewCommand(ui), + Func: "postgres", }, nil }, "connect rdp": func() (cli.Command, error) { @@ -269,10 +275,10 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "rdp", }, nil }, - "connect postgres": func() (cli.Command, error) { + "connect ssh": func() (cli.Command, error) { return &connect.Command{ Command: base.NewCommand(ui), - Func: "postgres", + Func: "ssh", }, nil }, diff --git a/internal/cmd/commands/connect/connect.go b/internal/cmd/commands/connect/connect.go index 7eb9bca179..8400326ee3 100644 --- a/internal/cmd/commands/connect/connect.go +++ b/internal/cmd/commands/connect/connect.go @@ -72,6 +72,9 @@ type Command struct { // HTTP httpFlags + // Kube + kubeFlags + // Postgres postgresFlags @@ -110,6 +113,8 @@ func (c *Command) Synopsis() string { return rdpSynopsis case "ssh": return sshSynopsis + case "kube": + return kubeSynopsis default: return "" } @@ -237,6 +242,9 @@ func (c *Command) Flags() *base.FlagSets { case "ssh": sshOptions(c, set) + + case "kube": + kubeOptions(c, set) } return set @@ -300,6 +308,8 @@ func (c *Command) Run(args []string) (retCode int) { c.flagExec = c.postgresFlags.defaultExec() case "rdp": c.flagExec = c.rdpFlags.defaultExec() + case "kube": + c.flagExec = c.kubeFlags.defaultExec() } } @@ -750,6 +760,15 @@ func (c *Command) handleExec(passthroughArgs []string) { case "ssh": args = append(args, c.sshFlags.buildArgs(c, port, ip, addr)...) + + case "kube": + kubeArgs, err := c.kubeFlags.buildArgs(c, port, ip, addr) + if err != nil { + c.Error(fmt.Sprintf("Error parsing session args: %s", err)) + c.execCmdReturnValue.Store(int32(3)) + return + } + args = append(args, kubeArgs...) } args = append(passthroughArgs, args...) diff --git a/internal/cmd/commands/connect/kube.go b/internal/cmd/commands/connect/kube.go new file mode 100644 index 0000000000..ebc0fae0f3 --- /dev/null +++ b/internal/cmd/commands/connect/kube.go @@ -0,0 +1,76 @@ +package connect + +import ( + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/posener/complete" +) + +const ( + kubeSynopsis = "Authorize a session against a target and invoke a Kubernetes client to connect" +) + +func kubeOptions(c *Command, set *base.FlagSets) { + f := set.NewFlagSet("Kubernetes Options") + + f.StringVar(&base.StringVar{ + Name: "style", + Target: &c.flagKubeStyle, + EnvVar: fmt.Sprintf("BOUNDARY_CONNECT_%s_STYLE", strings.ToUpper(c.Func)), + Completion: complete.PredictSet("kubectl"), + Default: "kubectl", + Usage: `Specifies how the CLI will attempt to invoke a Kubernetes client. This will also set a suitable default for -exec if a value was not specified. Currently-understood values are "kubectl".`, + }) + + f.StringVar(&base.StringVar{ + Name: "host", + Target: &c.flagKubeHost, + EnvVar: fmt.Sprintf("BOUNDARY_CONNECT_%s_HOST", strings.ToUpper(c.Func)), + Completion: complete.PredictNothing, + Usage: `Specifies the host value to use, overriding the endpoint address from the session information. The specified hostname will be passed through to the client (if supported) for use in the TLS SNI value.`, + }) + + f.StringVar(&base.StringVar{ + Name: "scheme", + Target: &c.flagKubeScheme, + Default: "https", + EnvVar: fmt.Sprintf("BOUNDARY_CONNECT_%s_SCHEME", strings.ToUpper(c.Func)), + Completion: complete.PredictNothing, + Usage: `Specifies the scheme to use.`, + }) +} + +type kubeFlags struct { + flagKubeStyle string + flagKubeHost string + flagKubeScheme string +} + +func (f *kubeFlags) defaultExec() string { + return strings.ToLower(f.flagKubeStyle) +} + +func (f *kubeFlags) buildArgs(c *Command, port, ip, addr string) ([]string, error) { + var args []string + host := f.flagKubeHost + if host == "" && c.sessionAuthzData.GetEndpoint() != "" { + hostUrl := c.sessionAuthzData.GetEndpoint() + u, err := url.Parse(hostUrl) + if err != nil { + return nil, fmt.Errorf("error parsing endpoint URL: %w", err) + } + host = u.Hostname() + } + switch f.flagKubeStyle { + case "kubectl": + if host != "" && f.flagKubeScheme == "https" { + host = strings.TrimSuffix(host, "/") + args = append(args, "--tls-server-name", host) + } + args = append(args, "--server", fmt.Sprintf("%s://%s", f.flagKubeScheme, addr)) + } + return args, nil +} diff --git a/website/content/docs/getting-started/connect-to-target.mdx b/website/content/docs/getting-started/connect-to-target.mdx index 18e605459e..6d2b062472 100644 --- a/website/content/docs/getting-started/connect-to-target.mdx +++ b/website/content/docs/getting-started/connect-to-target.mdx @@ -13,7 +13,8 @@ sets for this target contain the default host, which has the address `127.0.0.1`. When we run `boundary connect` against this target, the single available host will be selected and we'll open a local authenticated proxy to the target host on the target's default port (`127.0.0.1:22`). Because this -target is proxying to our local SSH server, we can use our built-in `connect ssh` command to wrap the proxied TCP connection and SSH via Boundary: +target is proxying to our local SSH server, we can use our built-in `connect +ssh` command to wrap the proxied TCP connection and SSH via Boundary: ``` $ boundary connect ssh -target-id ttcp_1234567890 @@ -37,6 +38,21 @@ different style expected by different SSH clients. At the moment, besides `ssh` (the default), the `boundary connect ssh` command supports `-style putty` to support passing connection information to PuTTY. +## Selecting Targets + +When using `boundary connect` you must identify the target used for connecting. +Convention in this documentation is to use the target ID as that's a single +value and most explicit; however, other flags are supported: + +- `target-name`: The name of the target +- `target-scope-id`: The ID of the scope in which the target lives +- `target-scope-name`: The name of the scope in which the target lives + +Note however that these are not uniquely identifying as names can be re-used +across scopes. As a result, when not using the target ID, you must use the +target's name in conjunction with the scope name or scope ID so that Boundary +can correctly identify the desired target. + ## Built-In vs. Exec Boundary comes with built-in wrappers for popular layer 7 connection protocols, @@ -45,6 +61,8 @@ such as: - `ssh`: defaults to the local SSH client (`ssh`) - `postgres`: defaults to the official Postgres CLI client (`psql`) - `rdp`: defaults to the built-in Windows RDP client (`mstsc`) +- `http`: defaults to `curl` +- `kube`: defaults to `kubectl` However, `boundary connect` can accommodate executing clients even when there is no built-in support for a specific client using `-exec`. The `-exec` flag is a