Skip to content

Commit 59677b5

Browse files
authored
rdb: add instance connect (#1170)
1 parent 841acb9 commit 59677b5

10 files changed

+1426
-4
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
Connect to an instance using locally installed CLI such as psql or mysql.
4+
5+
USAGE:
6+
scw rdb instance connect <instance-id ...> [arg=value ...]
7+
8+
ARGS:
9+
instance-id UUID of the instance
10+
username Name of the user to connect with to the database
11+
[database=rdb] Name of the database
12+
[cli-db] Command line tool to use, default to psql/mysql
13+
[region=fr-par] Region to target. If none is passed will use default region from the config (fr-par | nl-ams)
14+
15+
FLAGS:
16+
-h, --help help for connect
17+
18+
GLOBAL FLAGS:
19+
-c, --config string The path to the config file
20+
-D, --debug Enable debug mode
21+
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
22+
-p, --profile string The config profile to use

cmd/scw/testdata/test-all-usage-rdb-instance-usage.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ USAGE:
77

88
AVAILABLE COMMANDS:
99
clone Clone an instance
10+
connect Connect to an instance using locally installed CLI
1011
create Create an instance
1112
delete Delete an instance
1213
get Get an instance

internal/namespaces/rdb/v1/custom.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func GetCommands() *core.Commands {
2626

2727
cmds.Merge(core.NewCommands(
2828
instanceWaitCommand(),
29+
instanceConnectCommand(),
2930
))
3031
cmds.MustFind("rdb", "instance", "create").Override(instanceCreateBuilder)
3132
cmds.MustFind("rdb", "instance", "clone").Override(instanceCloneBuilder)

internal/namespaces/rdb/v1/custom_instance.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ package rdb
22

33
import (
44
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path"
59
"reflect"
10+
"runtime"
611
"strings"
712
"time"
813

914
"github.com/scaleway/scaleway-cli/internal/core"
1015
"github.com/scaleway/scaleway-cli/internal/human"
16+
"github.com/scaleway/scaleway-cli/internal/interactive"
1117
"github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
1218
"github.com/scaleway/scaleway-sdk-go/scw"
1319
)
@@ -165,3 +171,189 @@ func instanceWaitCommand() *core.Command {
165171
},
166172
}
167173
}
174+
175+
type instanceConnectArgs struct {
176+
Region scw.Region
177+
InstanceID string
178+
Username string
179+
Database *string
180+
CliDB *string
181+
}
182+
183+
type engineFamily string
184+
185+
const (
186+
Unknown = engineFamily("Unknown")
187+
PostgreSQL = engineFamily("PostgreSQL")
188+
MySQL = engineFamily("MySQL")
189+
postgreSQLHint = `
190+
psql supports password file to avoid typing your password manually.
191+
Learn more at: https://www.postgresql.org/docs/current/libpq-pgpass.html`
192+
mySQLHint = `
193+
mysql supports loading your password from a file to avoid typing them manually.
194+
Learn more at: https://dev.mysql.com/doc/refman/8.0/en/option-files.html`
195+
)
196+
197+
func passwordFileExist(ctx context.Context, family engineFamily) bool {
198+
passwordFilePath := ""
199+
switch family {
200+
case PostgreSQL:
201+
switch runtime.GOOS {
202+
case "windows":
203+
passwordFilePath = path.Join(core.ExtractUserHomeDir(ctx), core.ExtractEnv(ctx, "APPDATA"), "postgresql", "pgpass.conf")
204+
default:
205+
passwordFilePath = path.Join(core.ExtractUserHomeDir(ctx), ".pgpass")
206+
}
207+
case MySQL:
208+
passwordFilePath = path.Join(core.ExtractUserHomeDir(ctx), ".my.cnf")
209+
default:
210+
return false
211+
}
212+
if passwordFilePath == "" {
213+
return false
214+
}
215+
_, err := os.Stat(passwordFilePath)
216+
return err == nil
217+
}
218+
219+
func passwordFileHint(family engineFamily) string {
220+
switch family {
221+
case PostgreSQL:
222+
return postgreSQLHint
223+
case MySQL:
224+
return mySQLHint
225+
default:
226+
return ""
227+
}
228+
}
229+
230+
func detectEngineFamily(instance *rdb.Instance) (engineFamily, error) {
231+
if instance == nil {
232+
return Unknown, fmt.Errorf("instance engine is nil")
233+
}
234+
if strings.HasPrefix(instance.Engine, string(PostgreSQL)) {
235+
return PostgreSQL, nil
236+
}
237+
if strings.HasPrefix(instance.Engine, string(MySQL)) {
238+
return MySQL, nil
239+
}
240+
return Unknown, fmt.Errorf("unknown engine: %s", instance.Engine)
241+
}
242+
243+
func createConnectCommandLineArgs(instance *rdb.Instance, family engineFamily, args *instanceConnectArgs) ([]string, error) {
244+
database := "rdb"
245+
if args.Database != nil {
246+
database = *args.Database
247+
}
248+
249+
switch family {
250+
case PostgreSQL:
251+
clidb := "psql"
252+
if args.CliDB != nil {
253+
clidb = *args.CliDB
254+
}
255+
256+
// psql -h 51.159.25.206 --port 13917 -d rdb -U username
257+
return []string{
258+
clidb,
259+
"--host", instance.Endpoint.IP.String(),
260+
"--port", fmt.Sprintf("%d", instance.Endpoint.Port),
261+
"--username", args.Username,
262+
"--dbname", database,
263+
}, nil
264+
case MySQL:
265+
clidb := "mysql"
266+
if args.CliDB != nil {
267+
clidb = *args.CliDB
268+
}
269+
270+
// mysql -h 195.154.69.163 --port 12210 -p -u username
271+
return []string{
272+
clidb,
273+
"--host", instance.Endpoint.IP.String(),
274+
"--port", fmt.Sprintf("%d", instance.Endpoint.Port),
275+
"--database", database,
276+
"--user", args.Username,
277+
}, nil
278+
}
279+
280+
return nil, fmt.Errorf("unrecognize database engine: %s", instance.Engine)
281+
}
282+
283+
func instanceConnectCommand() *core.Command {
284+
return &core.Command{
285+
Namespace: "rdb",
286+
Resource: "instance",
287+
Verb: "connect",
288+
Short: "Connect to an instance using locally installed CLI",
289+
Long: "Connect to an instance using locally installed CLI such as psql or mysql.",
290+
ArgsType: reflect.TypeOf(instanceConnectArgs{}),
291+
ArgSpecs: core.ArgSpecs{
292+
{
293+
Name: "instance-id",
294+
Short: `UUID of the instance`,
295+
Required: true,
296+
Positional: true,
297+
},
298+
{
299+
Name: "username",
300+
Short: "Name of the user to connect with to the database",
301+
Required: true,
302+
},
303+
{
304+
Name: "database",
305+
Short: "Name of the database",
306+
Default: core.DefaultValueSetter("rdb"),
307+
},
308+
{
309+
Name: "cli-db",
310+
Short: "Command line tool to use, default to psql/mysql",
311+
},
312+
core.RegionArgSpec(scw.RegionFrPar, scw.RegionNlAms),
313+
},
314+
Run: func(ctx context.Context, argsI interface{}) (interface{}, error) {
315+
args := argsI.(*instanceConnectArgs)
316+
317+
client := core.ExtractClient(ctx)
318+
api := rdb.NewAPI(client)
319+
instance, err := api.GetInstance(&rdb.GetInstanceRequest{
320+
Region: args.Region,
321+
InstanceID: args.InstanceID,
322+
})
323+
if err != nil {
324+
return nil, err
325+
}
326+
327+
engineFamily, err := detectEngineFamily(instance)
328+
if err != nil {
329+
return nil, err
330+
}
331+
332+
cmdArgs, err := createConnectCommandLineArgs(instance, engineFamily, args)
333+
if err != nil {
334+
return nil, err
335+
}
336+
337+
if !passwordFileExist(ctx, engineFamily) {
338+
interactive.Println(passwordFileHint(engineFamily))
339+
}
340+
341+
// Run command
342+
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) //nolint:gosec
343+
//cmd.Stdin = os.Stdin
344+
core.ExtractLogger(ctx).Debugf("executing: %s\n", cmd.Args)
345+
exitCode, err := core.ExecCmd(ctx, cmd)
346+
347+
if err != nil {
348+
return nil, err
349+
}
350+
if exitCode != 0 {
351+
return nil, &core.CliError{Empty: true, Code: exitCode}
352+
}
353+
354+
return &core.SuccessResult{
355+
Empty: true, // the program will output the success message
356+
}, nil
357+
},
358+
}
359+
}

internal/namespaces/rdb/v1/custom_instance_test.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
func Test_CloneInstance(t *testing.T) {
1111
t.Run("Simple", core.Test(&core.TestConfig{
1212
Commands: GetCommands(),
13-
BeforeFunc: createInstance(),
13+
BeforeFunc: createInstance("PostgreSQL-12"),
1414
Cmd: "scw rdb instance clone {{ .Instance.ID }} node-type=DB-DEV-M name=foobar --wait",
1515
Check: core.TestCheckGolden(),
1616
AfterFunc: deleteInstance(),
@@ -29,7 +29,7 @@ func Test_CreateInstance(t *testing.T) {
2929
func Test_GetInstance(t *testing.T) {
3030
t.Run("Simple", core.Test(&core.TestConfig{
3131
Commands: GetCommands(),
32-
BeforeFunc: createInstance(),
32+
BeforeFunc: createInstance("PostgreSQL-12"),
3333
Cmd: "scw rdb instance get {{ .Instance.ID }}",
3434
Check: core.TestCheckGolden(),
3535
AfterFunc: deleteInstance(),
@@ -39,9 +39,47 @@ func Test_GetInstance(t *testing.T) {
3939
func Test_UpgradeInstance(t *testing.T) {
4040
t.Run("Simple", core.Test(&core.TestConfig{
4141
Commands: GetCommands(),
42-
BeforeFunc: createInstance(),
42+
BeforeFunc: createInstance("PostgreSQL-12"),
4343
Cmd: "scw rdb instance upgrade {{ .Instance.ID }} node-type=DB-DEV-M --wait",
4444
Check: core.TestCheckGolden(),
4545
AfterFunc: deleteInstance(),
4646
}))
4747
}
48+
49+
func Test_Connect(t *testing.T) {
50+
t.Run("mysql", core.Test(&core.TestConfig{
51+
Commands: GetCommands(),
52+
BeforeFunc: core.BeforeFuncCombine(
53+
func(ctx *core.BeforeFuncCtx) error {
54+
ctx.Meta["username"] = user
55+
return nil
56+
},
57+
createInstance("MySQL-8"),
58+
),
59+
Cmd: "scw rdb instance connect {{ .Instance.ID }} username={{ .username }}",
60+
Check: core.TestCheckCombine(
61+
core.TestCheckGolden(),
62+
core.TestCheckExitCode(0),
63+
),
64+
OverrideExec: core.OverrideExecSimple("mysql --host {{ .Instance.Endpoint.IP }} --port {{ .Instance.Endpoint.Port }} --database rdb --user {{ .username }}", 0),
65+
AfterFunc: deleteInstance(),
66+
}))
67+
68+
t.Run("psql", core.Test(&core.TestConfig{
69+
Commands: GetCommands(),
70+
BeforeFunc: core.BeforeFuncCombine(
71+
func(ctx *core.BeforeFuncCtx) error {
72+
ctx.Meta["username"] = user
73+
return nil
74+
},
75+
createInstance("PostgreSQL-12"),
76+
),
77+
Cmd: "scw rdb instance connect {{ .Instance.ID }} username={{ .username }}",
78+
Check: core.TestCheckCombine(
79+
core.TestCheckGolden(),
80+
core.TestCheckExitCode(0),
81+
),
82+
OverrideExec: core.OverrideExecSimple("psql --host {{ .Instance.Endpoint.IP }} --port {{ .Instance.Endpoint.Port }} --username {{ .username }} --dbname rdb", 0),
83+
AfterFunc: deleteInstance(),
84+
}))
85+
}

internal/namespaces/rdb/v1/helper_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const (
1313
engine = "PostgreSQL-12"
1414
)
1515

16-
func createInstance() core.BeforeFunc {
16+
func createInstance(engine string) core.BeforeFunc {
1717
return core.ExecStoreBeforeCmd(
1818
"Instance",
1919
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),

0 commit comments

Comments
 (0)