Skip to content

Commit

Permalink
rdb: add instance connect (#1170)
Browse files Browse the repository at this point in the history
  • Loading branch information
remyleone authored Jul 13, 2020
1 parent 841acb9 commit 59677b5
Show file tree
Hide file tree
Showing 10 changed files with 1,426 additions and 4 deletions.
22 changes: 22 additions & 0 deletions cmd/scw/testdata/test-all-usage-rdb-instance-connect-usage.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
Connect to an instance using locally installed CLI such as psql or mysql.

USAGE:
scw rdb instance connect <instance-id ...> [arg=value ...]

ARGS:
instance-id UUID of the instance
username Name of the user to connect with to the database
[database=rdb] Name of the database
[cli-db] Command line tool to use, default to psql/mysql
[region=fr-par] Region to target. If none is passed will use default region from the config (fr-par | nl-ams)

FLAGS:
-h, --help help for connect

GLOBAL FLAGS:
-c, --config string The path to the config file
-D, --debug Enable debug mode
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
-p, --profile string The config profile to use
1 change: 1 addition & 0 deletions cmd/scw/testdata/test-all-usage-rdb-instance-usage.golden
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ USAGE:

AVAILABLE COMMANDS:
clone Clone an instance
connect Connect to an instance using locally installed CLI
create Create an instance
delete Delete an instance
get Get an instance
Expand Down
1 change: 1 addition & 0 deletions internal/namespaces/rdb/v1/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func GetCommands() *core.Commands {

cmds.Merge(core.NewCommands(
instanceWaitCommand(),
instanceConnectCommand(),
))
cmds.MustFind("rdb", "instance", "create").Override(instanceCreateBuilder)
cmds.MustFind("rdb", "instance", "clone").Override(instanceCloneBuilder)
Expand Down
192 changes: 192 additions & 0 deletions internal/namespaces/rdb/v1/custom_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ package rdb

import (
"context"
"fmt"
"os"
"os/exec"
"path"
"reflect"
"runtime"
"strings"
"time"

"github.com/scaleway/scaleway-cli/internal/core"
"github.com/scaleway/scaleway-cli/internal/human"
"github.com/scaleway/scaleway-cli/internal/interactive"
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
Expand Down Expand Up @@ -165,3 +171,189 @@ func instanceWaitCommand() *core.Command {
},
}
}

type instanceConnectArgs struct {
Region scw.Region
InstanceID string
Username string
Database *string
CliDB *string
}

type engineFamily string

const (
Unknown = engineFamily("Unknown")
PostgreSQL = engineFamily("PostgreSQL")
MySQL = engineFamily("MySQL")
postgreSQLHint = `
psql supports password file to avoid typing your password manually.
Learn more at: https://www.postgresql.org/docs/current/libpq-pgpass.html`
mySQLHint = `
mysql supports loading your password from a file to avoid typing them manually.
Learn more at: https://dev.mysql.com/doc/refman/8.0/en/option-files.html`
)

func passwordFileExist(ctx context.Context, family engineFamily) bool {
passwordFilePath := ""
switch family {
case PostgreSQL:
switch runtime.GOOS {
case "windows":
passwordFilePath = path.Join(core.ExtractUserHomeDir(ctx), core.ExtractEnv(ctx, "APPDATA"), "postgresql", "pgpass.conf")
default:
passwordFilePath = path.Join(core.ExtractUserHomeDir(ctx), ".pgpass")
}
case MySQL:
passwordFilePath = path.Join(core.ExtractUserHomeDir(ctx), ".my.cnf")
default:
return false
}
if passwordFilePath == "" {
return false
}
_, err := os.Stat(passwordFilePath)
return err == nil
}

func passwordFileHint(family engineFamily) string {
switch family {
case PostgreSQL:
return postgreSQLHint
case MySQL:
return mySQLHint
default:
return ""
}
}

func detectEngineFamily(instance *rdb.Instance) (engineFamily, error) {
if instance == nil {
return Unknown, fmt.Errorf("instance engine is nil")
}
if strings.HasPrefix(instance.Engine, string(PostgreSQL)) {
return PostgreSQL, nil
}
if strings.HasPrefix(instance.Engine, string(MySQL)) {
return MySQL, nil
}
return Unknown, fmt.Errorf("unknown engine: %s", instance.Engine)
}

func createConnectCommandLineArgs(instance *rdb.Instance, family engineFamily, args *instanceConnectArgs) ([]string, error) {
database := "rdb"
if args.Database != nil {
database = *args.Database
}

switch family {
case PostgreSQL:
clidb := "psql"
if args.CliDB != nil {
clidb = *args.CliDB
}

// psql -h 51.159.25.206 --port 13917 -d rdb -U username
return []string{
clidb,
"--host", instance.Endpoint.IP.String(),
"--port", fmt.Sprintf("%d", instance.Endpoint.Port),
"--username", args.Username,
"--dbname", database,
}, nil
case MySQL:
clidb := "mysql"
if args.CliDB != nil {
clidb = *args.CliDB
}

// mysql -h 195.154.69.163 --port 12210 -p -u username
return []string{
clidb,
"--host", instance.Endpoint.IP.String(),
"--port", fmt.Sprintf("%d", instance.Endpoint.Port),
"--database", database,
"--user", args.Username,
}, nil
}

return nil, fmt.Errorf("unrecognize database engine: %s", instance.Engine)
}

func instanceConnectCommand() *core.Command {
return &core.Command{
Namespace: "rdb",
Resource: "instance",
Verb: "connect",
Short: "Connect to an instance using locally installed CLI",
Long: "Connect to an instance using locally installed CLI such as psql or mysql.",
ArgsType: reflect.TypeOf(instanceConnectArgs{}),
ArgSpecs: core.ArgSpecs{
{
Name: "instance-id",
Short: `UUID of the instance`,
Required: true,
Positional: true,
},
{
Name: "username",
Short: "Name of the user to connect with to the database",
Required: true,
},
{
Name: "database",
Short: "Name of the database",
Default: core.DefaultValueSetter("rdb"),
},
{
Name: "cli-db",
Short: "Command line tool to use, default to psql/mysql",
},
core.RegionArgSpec(scw.RegionFrPar, scw.RegionNlAms),
},
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
args := argsI.(*instanceConnectArgs)

client := core.ExtractClient(ctx)
api := rdb.NewAPI(client)
instance, err := api.GetInstance(&rdb.GetInstanceRequest{
Region: args.Region,
InstanceID: args.InstanceID,
})
if err != nil {
return nil, err
}

engineFamily, err := detectEngineFamily(instance)
if err != nil {
return nil, err
}

cmdArgs, err := createConnectCommandLineArgs(instance, engineFamily, args)
if err != nil {
return nil, err
}

if !passwordFileExist(ctx, engineFamily) {
interactive.Println(passwordFileHint(engineFamily))
}

// Run command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) //nolint:gosec
//cmd.Stdin = os.Stdin
core.ExtractLogger(ctx).Debugf("executing: %s\n", cmd.Args)
exitCode, err := core.ExecCmd(ctx, cmd)

if err != nil {
return nil, err
}
if exitCode != 0 {
return nil, &core.CliError{Empty: true, Code: exitCode}
}

return &core.SuccessResult{
Empty: true, // the program will output the success message
}, nil
},
}
}
44 changes: 41 additions & 3 deletions internal/namespaces/rdb/v1/custom_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
func Test_CloneInstance(t *testing.T) {
t.Run("Simple", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: createInstance(),
BeforeFunc: createInstance("PostgreSQL-12"),
Cmd: "scw rdb instance clone {{ .Instance.ID }} node-type=DB-DEV-M name=foobar --wait",
Check: core.TestCheckGolden(),
AfterFunc: deleteInstance(),
Expand All @@ -29,7 +29,7 @@ func Test_CreateInstance(t *testing.T) {
func Test_GetInstance(t *testing.T) {
t.Run("Simple", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: createInstance(),
BeforeFunc: createInstance("PostgreSQL-12"),
Cmd: "scw rdb instance get {{ .Instance.ID }}",
Check: core.TestCheckGolden(),
AfterFunc: deleteInstance(),
Expand All @@ -39,9 +39,47 @@ func Test_GetInstance(t *testing.T) {
func Test_UpgradeInstance(t *testing.T) {
t.Run("Simple", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: createInstance(),
BeforeFunc: createInstance("PostgreSQL-12"),
Cmd: "scw rdb instance upgrade {{ .Instance.ID }} node-type=DB-DEV-M --wait",
Check: core.TestCheckGolden(),
AfterFunc: deleteInstance(),
}))
}

func Test_Connect(t *testing.T) {
t.Run("mysql", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.BeforeFuncCombine(
func(ctx *core.BeforeFuncCtx) error {
ctx.Meta["username"] = user
return nil
},
createInstance("MySQL-8"),
),
Cmd: "scw rdb instance connect {{ .Instance.ID }} username={{ .username }}",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(0),
),
OverrideExec: core.OverrideExecSimple("mysql --host {{ .Instance.Endpoint.IP }} --port {{ .Instance.Endpoint.Port }} --database rdb --user {{ .username }}", 0),
AfterFunc: deleteInstance(),
}))

t.Run("psql", core.Test(&core.TestConfig{
Commands: GetCommands(),
BeforeFunc: core.BeforeFuncCombine(
func(ctx *core.BeforeFuncCtx) error {
ctx.Meta["username"] = user
return nil
},
createInstance("PostgreSQL-12"),
),
Cmd: "scw rdb instance connect {{ .Instance.ID }} username={{ .username }}",
Check: core.TestCheckCombine(
core.TestCheckGolden(),
core.TestCheckExitCode(0),
),
OverrideExec: core.OverrideExecSimple("psql --host {{ .Instance.Endpoint.IP }} --port {{ .Instance.Endpoint.Port }} --username {{ .username }} --dbname rdb", 0),
AfterFunc: deleteInstance(),
}))
}
2 changes: 1 addition & 1 deletion internal/namespaces/rdb/v1/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const (
engine = "PostgreSQL-12"
)

func createInstance() core.BeforeFunc {
func createInstance(engine string) core.BeforeFunc {
return core.ExecStoreBeforeCmd(
"Instance",
fmt.Sprintf("scw rdb instance create node-type=DB-DEV-S is-ha-cluster=false name=%s engine=%s user-name=%s password=%s --wait", name, engine, user, password),
Expand Down
Loading

0 comments on commit 59677b5

Please sign in to comment.