44package install
55
66import (
7+ "encoding/base64"
8+ "encoding/json"
79 "fmt"
10+ "github.com/microsoft/go-sqlcmd/internal/cmdparser/dependency"
11+ "github.com/microsoft/go-sqlcmd/internal/tools"
812 "net/url"
13+ "os"
914 "path"
1015 "path/filepath"
1116 "runtime"
1217 "strings"
1318
19+ "github.com/microsoft/go-sqlcmd/cmd/modern/root/open"
20+
1421 "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
1522 "github.com/microsoft/go-sqlcmd/internal/cmdparser"
1623 "github.com/microsoft/go-sqlcmd/internal/config"
@@ -55,6 +62,8 @@ type MssqlBase struct {
5562 port int
5663
5764 usingDatabaseUrl string
65+ openTool string
66+ openFile string
5867
5968 unitTesting bool
6069
@@ -217,6 +226,20 @@ func (c *MssqlBase) AddFlags(
217226 Name : "using" ,
218227 Usage : "Download (into container) and attach database (.bak) from URL" ,
219228 })
229+
230+ addFlag (cmdparser.FlagOptions {
231+ String : & c .openTool ,
232+ DefaultString : "ads" ,
233+ Name : "open" ,
234+ Usage : "Open tool e.g. ads" ,
235+ })
236+
237+ addFlag (cmdparser.FlagOptions {
238+ String : & c .openFile ,
239+ DefaultString : "" ,
240+ Name : "open-file" ,
241+ Usage : "Open file in tool e.g. https://aks.ms/adventureworks-demo.sql" ,
242+ })
220243}
221244
222245// Run checks that the end-user license agreement has been accepted,
@@ -358,41 +381,120 @@ func (c *MssqlBase) createContainer(imageName string, contextName string) {
358381
359382 // Download and restore DB if asked
360383 if c .usingDatabaseUrl != "" {
361- c .downloadAndRestoreDb (
362- controller ,
363- containerId ,
364- userName ,
365- )
384+ c .downloadAndRestoreDb (controller , containerId , userName , password )
366385 }
367386
368- hints := [][] string {}
387+ if c . openTool == "" {
369388
370- // TODO: sqlcmd open ads only support on Windows right now, add Mac support
371- if runtime .GOOS == "windows" || runtime .GOOS == "darwin" {
372- hints = append (hints , []string {"Open in Azure Data Studio" , "sqlcmd open ads" })
373- }
389+ hints := [][]string {}
374390
375- hints = append (hints , []string {"Run a query" , "sqlcmd query \" SELECT @@version\" " })
376- hints = append (hints , []string {"Start interactive session" , "sqlcmd query" })
391+ // TODO: sqlcmd open ads only support on Windows right now, add Mac support
392+ if runtime .GOOS == "windows" || runtime .GOOS == "darwin" {
393+ hints = append (hints , []string {"Open in Azure Data Studio" , "sqlcmd open ads" })
394+ }
377395
378- if previousContextName != "" {
379- hints = append (
380- hints ,
381- []string {"Change current context" , fmt .Sprintf (
382- "sqlcmd config use-context %v" ,
383- previousContextName ,
384- )},
396+ hints = append (hints , []string {"Run a query" , "sqlcmd query \" SELECT @@version\" " })
397+ hints = append (hints , []string {"Start interactive session" , "sqlcmd query" })
398+
399+ if previousContextName != "" {
400+ hints = append (
401+ hints ,
402+ []string {"Change current context" , fmt .Sprintf (
403+ "sqlcmd config use-context %v" ,
404+ previousContextName ,
405+ )},
406+ )
407+ }
408+
409+ hints = append (hints , []string {"View sqlcmd configuration" , "sqlcmd config view" })
410+ hints = append (hints , []string {"See connection strings" , "sqlcmd config connection-strings" })
411+ hints = append (hints , []string {"Remove" , "sqlcmd delete" })
412+
413+ output .InfofWithHintExamples (hints ,
414+ "Now ready for client connections on port %d" ,
415+ c .port ,
385416 )
386417 }
387418
388- hints = append (hints , []string {"View sqlcmd configuration" , "sqlcmd config view" })
389- hints = append (hints , []string {"See connection strings" , "sqlcmd config connection-strings" })
390- hints = append (hints , []string {"Remove" , "sqlcmd delete" })
419+ if c .openTool == "ads" {
420+ ads := open.Ads {}
421+ ads .SetCrossCuttingConcerns (dependency.Options {
422+ EndOfLine : pal .LineBreak (),
423+ Output : c .Output (),
424+ })
425+
426+ user := & sqlconfig.User {
427+ AuthenticationType : "basic" ,
428+ BasicAuth : & sqlconfig.BasicAuthDetails {
429+ Username : userName ,
430+ PasswordEncryption : c .passwordEncryption ,
431+ Password : secret .Encode (password , c .passwordEncryption )},
432+ Name : userName }
433+
434+ ads .PersistCredentialForAds (endpoint .EndpointDetails .Address , endpoint , user )
435+
436+ output := c .Output ()
437+ args := []string {
438+ "-r" ,
439+ fmt .Sprintf (
440+ "--server=%s" , fmt .Sprintf (
441+ "%s,%d" ,
442+ "127.0.0.1" ,
443+ c .port )),
444+ }
445+
446+ args = append (args , fmt .Sprintf ("--user=%s" ,
447+ strings .Replace (userName , `"` , `\"` , - 1 )))
391448
392- output .InfofWithHintExamples (hints ,
393- "Now ready for client connections on port %d" ,
394- c .port ,
395- )
449+ tool := tools .NewTool ("ads" )
450+ if ! tool .IsInstalled () {
451+ output .Fatalf (tool .HowToInstall ())
452+ }
453+
454+ if c .openFile != "" {
455+ args = append (args , c .openFile )
456+
457+ /*
458+ var k registry.Key
459+ prefix := "SOFTWARE\\Classes\\"
460+ urlScheme := "sqlcmd"
461+ basePath := prefix + urlScheme
462+ permission := uint32(registry.QUERY_VALUE | registry.SET_VALUE)
463+ baseKey := registry.CURRENT_USER
464+
465+ programLocation := "\"C:\\Windows\\notepad.exe\""
466+
467+ // create key
468+ registry.CreateKey(baseKey, basePath, permission)
469+
470+ // set description
471+ k.SetStringValue("", "Notepad app")
472+ k.SetStringValue("URL Protocol", "")
473+
474+ // set icon
475+ registry.CreateKey(registry.CURRENT_USER, "lumiere\\DefaultIcon", registry.ALL_ACCESS)
476+ k.SetStringValue("", programLocation+",1")
477+
478+ // create tree
479+ registry.CreateKey(baseKey, basePath+"\\shell", permission)
480+ registry.CreateKey(baseKey, basePath+"\\shell\\open", permission)
481+ registry.CreateKey(baseKey, basePath+"\\shell\\open\\command", permission)
482+
483+ // set open command
484+ k.SetStringValue("", programLocation+" \"%1\"")
485+ */
486+
487+ a := os .Args [1 :]
488+ data , _ := json .Marshal (& a )
489+
490+ fmt .Printf ("The URL for sharing this `sqlcmd create` is:\n \n " )
491+ sEnc := base64 .StdEncoding .EncodeToString (data )
492+ fmt .Printf ("sqlcmd://%s\n " , sEnc )
493+ }
494+
495+ _ , err := tool .Run (args )
496+ c .CheckErr (err )
497+ }
396498}
397499
398500func (c * MssqlBase ) validateUsingUrlExists () {
@@ -412,19 +514,20 @@ func (c *MssqlBase) validateUsingUrlExists() {
412514 if u .Path == "" {
413515 output .FatalfWithHints (
414516 []string {
415- "--using URL must have a path to .bak file" ,
517+ "--using URL must have a path to .bak or .bacpac file" ,
416518 },
417519 "%q is not a valid URL for --using flag" , c .usingDatabaseUrl )
418520 }
419521
420522 // At the moment we only support attaching .bak files, but we should
421523 // support .bacpacs and .mdfs in the future
422- if _ , file := filepath .Split (u .Path ); filepath .Ext (file ) != ".bak" {
524+ _ , f := filepath .Split (u .Path )
525+ if filepath .Ext (f ) != ".bak" && filepath .Ext (f ) != ".bacpac" {
423526 output .FatalfWithHints (
424527 []string {
425- "--using file URL must be a .bak file" ,
528+ "--using file URL must be a .bak or .bacpac file" ,
426529 },
427- "Invalid --using file type" )
530+ "Invalid --using file type, extension %q is not supported" , filepath . Ext ( f ) )
428531 }
429532
430533 // Verify the url actually exists, and early exit if it doesn't
@@ -488,7 +591,7 @@ func getDbNameAsNonIdentifier(dbName string) string {
488591 return strings .ReplaceAll (dbName , "]" , "]]" )
489592}
490593
491- //parseDbName returns the databaseName from --using arg
594+ // parseDbName returns the databaseName from --using arg
492595// It sets database name to the specified database name
493596// or in absence of it, it is set to the filename without
494597// extension.
@@ -497,6 +600,9 @@ func parseDbName(usingDbUrl string) string {
497600 dbToken := path .Base (u .Path )
498601 if dbToken != "." && dbToken != "/" {
499602 lastIdx := strings .LastIndex (dbToken , ".bak" )
603+ if lastIdx == - 1 {
604+ lastIdx = strings .LastIndex (dbToken , ".bacpac" )
605+ }
500606 if lastIdx != - 1 {
501607 //Get file name without extension
502608 fileName := dbToken [0 :lastIdx ]
@@ -516,13 +622,22 @@ func extractUrl(usingArg string) string {
516622 if urlEndIdx != - 1 {
517623 return usingArg [0 :(urlEndIdx + 4 )]
518624 }
625+
626+ if urlEndIdx == - 1 {
627+ urlEndIdx = strings .LastIndex (usingArg , ".bacpac" )
628+ if urlEndIdx != - 1 {
629+ return usingArg [0 :(urlEndIdx + 7 )]
630+ }
631+ }
632+
519633 return usingArg
520634}
521635
522636func (c * MssqlBase ) downloadAndRestoreDb (
523637 controller * container.Controller ,
524638 containerId string ,
525639 userName string ,
640+ password string ,
526641) {
527642 output := c .Cmd .Output ()
528643 databaseName := parseDbName (c .usingDatabaseUrl )
@@ -541,13 +656,18 @@ func (c *MssqlBase) downloadAndRestoreDb(
541656 temporaryFolder ,
542657 )
543658
544- // Restore database from file
545- output .Infof ("Restoring database %s" , databaseName )
546-
547659 dbNameAsIdentifier := getDbNameAsIdentifier (databaseName )
548660 dbNameAsNonIdentifier := getDbNameAsNonIdentifier (databaseName )
549661
550- text := `SET NOCOUNT ON;
662+ u , err := url .Parse (databaseUrl )
663+ c .CheckErr (err )
664+
665+ _ , f := filepath .Split (u .Path )
666+ if filepath .Ext (f ) == ".bak" {
667+ // Restore database from file
668+ output .Infof ("Restoring database %s" , databaseName )
669+
670+ text := `SET NOCOUNT ON;
551671
552672-- Build a SQL Statement to restore any .bak file to the Linux filesystem
553673DECLARE @sql NVARCHAR(max)
@@ -588,7 +708,36 @@ WHERE IsPresent = 1
588708SET @sql = SUBSTRING(@sql, 1, LEN(@sql)-1)
589709EXEC(@sql)`
590710
591- c .query (fmt .Sprintf (text , temporaryFolder , file , dbNameAsIdentifier , temporaryFolder , file ))
711+ c .query (fmt .Sprintf (text , temporaryFolder , file , dbNameAsIdentifier , temporaryFolder , file ))
712+ } else if filepath .Ext (f ) == ".bacpac" {
713+ // sqlpackage /a:import /SourceFile:DaasMM.bacpac
714+ // /tu:"joey"
715+ // /tp:"6%oO3L#X%d%8hAH5E*y6plD6#IV1$PEESS$ZAhC8uL#@Q@40e3"
716+ // /tsn:"localhost,1433"
717+ // /tdn:"db1"
718+
719+ controller .DownloadFile (
720+ containerId ,
721+ "https://aka.ms/sqlpackage-linux" ,
722+ "/tmp" ,
723+ )
724+
725+ controller .RunCmdInContainer (containerId , []string {"apt-get" , "update" })
726+ controller .RunCmdInContainer (containerId , []string {"apt-get" , "install" , "-y" , "unzip" })
727+ controller .RunCmdInContainer (containerId , []string {"unzip" , "/tmp/sqlpackage-linux" , "-d" , "/opt/sqlpackage" })
728+ controller .RunCmdInContainer (containerId , []string {"chmod" , "+x" , "/opt/sqlpackage/sqlpackage" })
729+ //controller.RunCmdInContainer(containerId, []string{"echo", `'export PATH="$PATH:/opt/sqlpackage"' >> ~/.bash_profile`})
730+ //controller.RunCmdInContainer(containerId, []string{"source", "~/.bash_profile"})
731+ controller .RunCmdInContainer (containerId , []string {
732+ "/opt/sqlpackage/sqlpackage" ,
733+ "/a:import" ,
734+ "/SourceFile:" + temporaryFolder + "/" + file ,
735+ "/tu:" + userName ,
736+ "/tp:" + password ,
737+ "/tsn:127.0.0.1,1433" ,
738+ "/tdn:" + dbNameAsIdentifier ,
739+ "/TargetTrustServerCertificate:true" })
740+ }
592741
593742 alterDefaultDb := fmt .Sprintf (
594743 "ALTER LOGIN [%s] WITH DEFAULT_DATABASE = [%s]" ,
0 commit comments