@@ -2,12 +2,18 @@ package rdb
22
33import (
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+ }
0 commit comments