Skip to content

feat: add MySQL command support #5749

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
Func: "postgres",
}
}),
"connect mysql": wrapper.Wrap(func() wrapper.WrappableCommand {
return &connect.Command{
Command: base.NewCommand(ui, opts...),
Func: "mysql",
}
}),
"connect rdp": wrapper.Wrap(func() wrapper.WrappableCommand {
return &connect.Command{
Command: base.NewCommand(ui, opts...),
Expand Down
20 changes: 20 additions & 0 deletions internal/cmd/commands/connect/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ type Command struct {
// Postgres
postgresFlags

// MySQL
mysqlFlags

// RDP
rdpFlags

Expand All @@ -103,6 +106,8 @@ func (c *Command) Synopsis() string {
return httpSynopsis
case "postgres":
return postgresSynopsis
case "mysql":
return mysqlSynopsis
case "rdp":
return rdpSynopsis
case "ssh":
Expand Down Expand Up @@ -222,6 +227,9 @@ func (c *Command) Flags() *base.FlagSets {
case "postgres":
postgresOptions(c, set)

case "mysql":
mysqlOptions(c, set)

case "rdp":
rdpOptions(c, set)

Expand Down Expand Up @@ -309,6 +317,8 @@ func (c *Command) Run(args []string) (retCode int) {
c.flagExec = c.sshFlags.defaultExec()
case "postgres":
c.flagExec = c.postgresFlags.defaultExec()
case "mysql":
c.flagExec = c.mysqlFlags.defaultExec()
case "rdp":
c.flagExec = c.rdpFlags.defaultExec()
case "kube":
Expand Down Expand Up @@ -641,6 +651,16 @@ func (c *Command) handleExec(clientProxy *apiproxy.ClientProxy, passthroughArgs
envs = append(envs, pgEnvs...)
creds = pgCreds

case "mysql":
mysqlArgs, mysqlEnvs, mysqlCreds, mysqlErr := c.mysqlFlags.buildArgs(c, port, host, addr, creds)
if mysqlErr != nil {
argsErr = mysqlErr
break
}
args = append(args, mysqlArgs...)
envs = append(envs, mysqlEnvs...)
creds = mysqlCreds

case "rdp":
args = append(args, c.rdpFlags.buildArgs(c, port, host, addr)...)

Expand Down
114 changes: 114 additions & 0 deletions internal/cmd/commands/connect/mysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package connect

import (
"fmt"
"os"
"strings"

"github.com/hashicorp/boundary/api/proxy"
"github.com/hashicorp/boundary/internal/cmd/base"
"github.com/posener/complete"
)

const (
mysqlSynopsis = "Authorize a session against a target and invoke a MySQL client to connect"
)

func mysqlOptions(c *Command, set *base.FlagSets) {
f := set.NewFlagSet("MySQL Options")

f.StringVar(&base.StringVar{
Name: "style",
Target: &c.flagMySQLStyle,
EnvVar: "BOUNDARY_CONNECT_MYSQL_STYLE",
Completion: complete.PredictSet("mysql"),
Default: "mysql",
Usage: `Specifies how the CLI will attempt to invoke a MySQL client. This will also set a suitable default for -exec if a value was not specified. Currently-understood values are "mysql".`,
})

f.StringVar(&base.StringVar{
Name: "username",
Target: &c.flagUsername,
EnvVar: "BOUNDARY_CONNECT_USERNAME",
Completion: complete.PredictNothing,
Usage: `Specifies the username to pass through to the client. May be overridden by credentials sourced from a credential store.`,
})

f.StringVar(&base.StringVar{
Name: "dbname",
Target: &c.flagDbname,
EnvVar: "BOUNDARY_CONNECT_DBNAME",
Completion: complete.PredictNothing,
Usage: `Specifies the database name to pass through to the client.`,
})
}

type mysqlFlags struct {
flagMySQLStyle string
}

func (m *mysqlFlags) defaultExec() string {
return strings.ToLower(m.flagMySQLStyle)
}

func (m *mysqlFlags) buildArgs(c *Command, port, ip, _ string, creds proxy.Credentials) (args, envs []string, retCreds proxy.Credentials, retErr error) {
var username, password string

retCreds = creds
if len(retCreds.UsernamePassword) > 0 {
// Mark credential as consumed so it is not printed to user
retCreds.UsernamePassword[0].Consumed = true

// For now just grab the first username password credential brokered
username = retCreds.UsernamePassword[0].Username
password = retCreds.UsernamePassword[0].Password
}

switch m.flagMySQLStyle {
case "mysql":
if port != "" {
args = append(args, "-P", port)
}
args = append(args, "-h", ip)

if c.flagDbname != "" {
args = append(args, "-D", c.flagDbname)
}

switch {
case username != "":
args = append(args, "-u", username)
case c.flagUsername != "":
args = append(args, "-u", c.flagUsername)
}

if password != "" {
passfile, err := os.CreateTemp("", "*")
if err != nil {
return nil, nil, proxy.Credentials{}, fmt.Errorf("Error saving MySQL password to tmp file: %w", err)
}
c.cleanupFuncs = append(c.cleanupFuncs, func() error {
if err := os.Remove(passfile.Name()); err != nil {
return fmt.Errorf("Error removing temporary password file; consider removing %s manually: %w", passfile.Name(), err)
}
return nil
})
_, err = passfile.WriteString(fmt.Sprintf("[client]\npassword=%s", password))
if err != nil {
return nil, nil, proxy.Credentials{}, fmt.Errorf("Error writing password file to %s: %w", passfile.Name(), err)
}
if err := passfile.Close(); err != nil {
return nil, nil, proxy.Credentials{}, fmt.Errorf("Error closing password file after writing to %s: %w", passfile.Name(), err)
}
args = append(args, "-p"+password)

if c.flagDbname == "" {
c.UI.Warn("Credentials are being brokered but no -dbname parameter provided. mysql may misinterpret another parameter as the database name.")
}
}
}
return
}