Skip to content

Commit 08e5588

Browse files
committed
Prototype .bacpac support
1 parent 135a976 commit 08e5588

File tree

6 files changed

+220
-45
lines changed

6 files changed

+220
-45
lines changed

cmd/modern/main.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
package main
1313

1414
import (
15+
"encoding/base64"
16+
"encoding/json"
1517
"github.com/microsoft/go-sqlcmd/internal"
1618
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
1719
"github.com/microsoft/go-sqlcmd/internal/cmdparser/dependency"
@@ -22,9 +24,9 @@ import (
2224
"github.com/microsoft/go-sqlcmd/internal/pal"
2325
"github.com/microsoft/go-sqlcmd/pkg/sqlcmd"
2426
"github.com/spf13/cobra"
25-
"path"
26-
2727
"os"
28+
"path"
29+
"strings"
2830

2931
legacyCmd "github.com/microsoft/go-sqlcmd/cmd/sqlcmd"
3032
)
@@ -44,6 +46,29 @@ func main() {
4446
ErrorHandler: checkErr,
4547
HintHandler: displayHints})}
4648
rootCmd = cmdparser.New[*Root](dependencies)
49+
50+
//var input string
51+
52+
if strings.HasPrefix(os.Args[1], "sqlcmd://") {
53+
sqlcmdUrl := strings.TrimRight(os.Args[1][9:], "/")
54+
55+
//fmt.Printf("%q", sqlcmdUrl)
56+
57+
//_, _ = fmt.Scanln(&input)
58+
59+
data2, err := base64.StdEncoding.DecodeString(sqlcmdUrl)
60+
if err == nil {
61+
var genre2 []string
62+
if err = json.Unmarshal(data2, &genre2); err == nil {
63+
cobra.MousetrapHelpText = ""
64+
cmdparser.Initialize(initializeCallback)
65+
rootCmd.SetArgsForUnitTesting(genre2)
66+
rootCmd.Execute()
67+
os.Exit(0)
68+
}
69+
}
70+
}
71+
4772
if isFirstArgModernCliSubCommand() {
4873
cmdparser.Initialize(initializeCallback)
4974
rootCmd.Execute()

cmd/modern/root/install/mssql-base.go

Lines changed: 185 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,20 @@
44
package install
55

66
import (
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

398500
func (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

522636
func (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
553673
DECLARE @sql NVARCHAR(max)
@@ -588,7 +708,36 @@ WHERE IsPresent = 1
588708
SET @sql = SUBSTRING(@sql, 1, LEN(@sql)-1)
589709
EXEC(@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]",

cmd/modern/root/open/ads.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (c *Ads) run() {
4646
// If basic auth is used, we need to persist the password in the OS in a way
4747
// that ADS can access it. The method used is OS specific.
4848
if user != nil && user.AuthenticationType == "basic" {
49-
c.persistCredentialForAds(endpoint.EndpointDetails.Address, endpoint, user)
49+
c.PersistCredentialForAds(endpoint.EndpointDetails.Address, endpoint, user)
5050
c.launchAds(endpoint.EndpointDetails.Address, endpoint.EndpointDetails.Port, user.BasicAuth.Username)
5151
} else {
5252
c.launchAds(endpoint.EndpointDetails.Address, endpoint.EndpointDetails.Port, "")

cmd/modern/root/open/ads_windows.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ func (c *Ads) displayPreLaunchInfo() {
2828
output.Infof("Press Ctrl+C to exit this process...")
2929
}
3030

31-
// persistCredentialForAds stores a SQL password in the Windows Credential Manager
31+
// PersistCredentialForAds stores a SQL password in the Windows Credential Manager
3232
// for the given hostname and endpoint.
33-
func (c *Ads) persistCredentialForAds(
33+
func (c *Ads) PersistCredentialForAds(
3434
hostname string,
3535
endpoint sqlconfig.Endpoint,
3636
user *sqlconfig.User,

0 commit comments

Comments
 (0)