diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index f719b3fe..c8e46e66 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -45,7 +45,7 @@ jobs:
- name: Setup go
uses: actions/setup-go@v2
with:
- go-version: '1.18'
+ go-version: '1.20'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index a28ef0ed..c8cdbdee 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/setup-go@v3
with:
- go-version: 1.18
+ go-version: 1.20
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index d6812940..2245d7fe 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -13,7 +13,7 @@ jobs:
- name: Setup go
uses: actions/setup-go@v2
with:
- go-version: '1.18'
+ go-version: '1.20'
- name: Run tests against Linux SQL
run: |
go version
diff --git a/.pipelines/include-install-go-tools.yml b/.pipelines/include-install-go-tools.yml
index 23d7190d..7ce69446 100644
--- a/.pipelines/include-install-go-tools.yml
+++ b/.pipelines/include-install-go-tools.yml
@@ -1,7 +1,7 @@
steps:
- task: GoTool@0
inputs:
- version: '1.18'
+ version: '1.20'
- task: Go@0
displayName: 'Go: get dependencies'
inputs:
diff --git a/README.md b/README.md
index 6332b6df..d25484b3 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,7 @@ Use `sqlcmd` to create SQL Server and Azure SQL Edge instances using a local con
To create a local SQL Server instance with the AdventureWorksLT database restored, query it, and connect to it using Azure Data Studio, run:
```
-sqlcmd create mssql --accept-eula --using https://aka.ms/AdventureWorksLT.bak
+sqlcmd create mssql --accept-eula --use https://aka.ms/AdventureWorksLT.bak
sqlcmd query "SELECT DB_NAME()"
sqlcmd open ads
```
diff --git a/build/azure-pipelines/build-common.yml b/build/azure-pipelines/build-common.yml
index dcb9a0d1..02cd747c 100644
--- a/build/azure-pipelines/build-common.yml
+++ b/build/azure-pipelines/build-common.yml
@@ -17,7 +17,7 @@ parameters:
steps:
- task: GoTool@0
inputs:
- version: '1.18'
+ version: '1.20'
goBin: $(Build.SourcesDirectory)
- task: Go@0
diff --git a/cmd/modern/main.go b/cmd/modern/main.go
index 4b358b48..e5393e89 100644
--- a/cmd/modern/main.go
+++ b/cmd/modern/main.go
@@ -20,6 +20,7 @@ import (
"github.com/microsoft/go-sqlcmd/internal/output"
"github.com/microsoft/go-sqlcmd/internal/output/verbosity"
"github.com/microsoft/go-sqlcmd/internal/pal"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer"
"github.com/microsoft/go-sqlcmd/pkg/sqlcmd"
"github.com/spf13/cobra"
"path"
@@ -95,9 +96,7 @@ func initializeEnvVars() {
os.Setenv("SQLCMDPASSWORD", password)
}
}
-
}
-
}
// isFirstArgModernCliSubCommand is TEMPORARY code, to be removed when
@@ -131,7 +130,12 @@ func initializeCallback() {
TraceHandler: outputter.Tracef,
HintHandler: displayHints,
LineBreak: sqlcmd.SqlcmdEol,
+ LoggingLevel: verbosity.Level(rootCmd.loggingLevel),
})
+ mssqlcontainer.Initialize(mssqlcontainer.InitializeOptions{
+ ErrorHandler: checkErr,
+ TraceHandler: outputter.Tracef,
+ })
config.SetFileName(rootCmd.configFilename)
config.Load()
}
diff --git a/cmd/modern/root.go b/cmd/modern/root.go
index 8a83f02b..8724cb5e 100644
--- a/cmd/modern/root.go
+++ b/cmd/modern/root.go
@@ -27,7 +27,7 @@ type Root struct {
// It also provides usage examples for sqlcmd.
func (c *Root) DefineCommand(...cmdparser.CommandOptions) {
// Example usage steps
- steps := []string{"sqlcmd create mssql --accept-eula --using https://aka.ms/AdventureWorksLT.bak"}
+ steps := []string{"sqlcmd create mssql --accept-eula --use https://aka.ms/AdventureWorksLT.bak"}
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
steps = append(steps, "sqlcmd open ads")
@@ -66,10 +66,12 @@ func (c *Root) SubCommands() []cmdparser.Command {
subCommands := []cmdparser.Command{
cmdparser.New[*root.Config](dependencies),
+ cmdparser.New[*root.Deploy](dependencies),
cmdparser.New[*root.Install](dependencies),
cmdparser.New[*root.Query](dependencies),
cmdparser.New[*root.Start](dependencies),
cmdparser.New[*root.Stop](dependencies),
+ cmdparser.New[*root.Use](dependencies),
cmdparser.New[*root.Uninstall](dependencies),
}
diff --git a/cmd/modern/root/config/connection-strings.go b/cmd/modern/root/config/connection-strings.go
index a011aadc..4a6740c0 100644
--- a/cmd/modern/root/config/connection-strings.go
+++ b/cmd/modern/root/config/connection-strings.go
@@ -71,7 +71,7 @@ func (c *ConnectionStrings) run() {
if endpoint.AssetDetails != nil && endpoint.AssetDetails.ContainerDetails != nil {
controller := container.NewController()
if controller.ContainerRunning(endpoint.AssetDetails.ContainerDetails.Id) {
- s := sql.New(sql.SqlOptions{})
+ s := sql.NewSql(sql.SqlOptions{})
s.Connect(endpoint, user, sql.ConnectOptions{Interactive: false})
c.database = s.ScalarString("PRINT DB_NAME()")
} else {
diff --git a/cmd/modern/root/config/current-context.go b/cmd/modern/root/config/current-context.go
index fa1d9872..506a1a26 100644
--- a/cmd/modern/root/config/current-context.go
+++ b/cmd/modern/root/config/current-context.go
@@ -17,10 +17,10 @@ type CurrentContext struct {
func (c *CurrentContext) DefineCommand(...cmdparser.CommandOptions) {
options := cmdparser.CommandOptions{
Use: "current-context",
- Short: localizer.Sprintf("Display the current-context"),
+ Short: localizer.Sprintf("Display the name of the current-context"),
Examples: []cmdparser.ExampleOptions{
{
- Description: localizer.Sprintf("Display the current-context"),
+ Description: localizer.Sprintf("Display the current-context name"),
Steps: []string{
"sqlcmd config current-context"},
},
diff --git a/cmd/modern/root/config/delete-context.go b/cmd/modern/root/config/delete-context.go
index 75f3df5c..898df0fb 100644
--- a/cmd/modern/root/config/delete-context.go
+++ b/cmd/modern/root/config/delete-context.go
@@ -74,6 +74,10 @@ func (c *DeleteContext) run() {
if config.UserExists(context) {
config.DeleteUser(*context.ContextDetails.User)
}
+
+ for _, c := range context.AddOns {
+ config.DeleteEndpoint(c.Endpoint)
+ }
}
config.DeleteContext(c.name)
diff --git a/cmd/modern/root/config/get-endpoints.go b/cmd/modern/root/config/get-endpoints.go
index a19b28b3..bddb6549 100644
--- a/cmd/modern/root/config/get-endpoints.go
+++ b/cmd/modern/root/config/get-endpoints.go
@@ -15,6 +15,7 @@ type GetEndpoints struct {
name string
detailed bool
+ value string
}
func (c *GetEndpoints) DefineCommand(...cmdparser.CommandOptions) {
@@ -47,6 +48,11 @@ func (c *GetEndpoints) DefineCommand(...cmdparser.CommandOptions) {
Bool: &c.detailed,
Name: "detailed",
Usage: localizer.Sprintf("Include endpoint details")})
+
+ c.AddFlag(cmdparser.FlagOptions{
+ String: &c.value,
+ Name: "value",
+ Usage: localizer.Sprintf("Value to get, (endpoint, port")})
}
func (c *GetEndpoints) run() {
@@ -55,7 +61,19 @@ func (c *GetEndpoints) run() {
if c.name != "" {
if config.EndpointExists(c.name) {
context := config.GetEndpoint(c.name)
- output.Struct(context)
+
+ if c.value == "" {
+ output.Struct(context)
+ } else {
+ if c.value == "address" {
+ output.Struct(context.Address)
+ } else if c.value == "port" {
+ output.Struct(context.Port)
+ } else {
+ panic("Invalid value")
+ }
+ }
+
} else {
output.FatalWithHints(
[]string{localizer.Sprintf("To view available endpoints run `%s`", localizer.GetEndpointsCommand)},
diff --git a/cmd/modern/root/deploy.go b/cmd/modern/root/deploy.go
new file mode 100644
index 00000000..80a7cb3c
--- /dev/null
+++ b/cmd/modern/root/deploy.go
@@ -0,0 +1,777 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package root
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "path/filepath"
+ "runtime"
+
+ "github.com/joho/godotenv"
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+ "github.com/microsoft/go-sqlcmd/internal/config"
+ "github.com/microsoft/go-sqlcmd/internal/dotsqlcmdconfig"
+ "github.com/microsoft/go-sqlcmd/internal/io/file"
+ "github.com/microsoft/go-sqlcmd/internal/io/folder"
+ "github.com/microsoft/go-sqlcmd/internal/localizer"
+ "github.com/microsoft/go-sqlcmd/internal/sql"
+ "github.com/microsoft/go-sqlcmd/internal/tools"
+ "github.com/microsoft/go-sqlcmd/internal/tools/tool"
+
+ "github.com/rdegges/go-ipify"
+
+ "os"
+ "os/exec"
+ "strings"
+)
+
+// Open defines the `sqlcmd open` sub-commands
+type Deploy struct {
+ cmdparser.Cmd
+
+ target string
+ environment string
+ notFree bool
+ force bool
+}
+
+func (c *Deploy) DefineCommand(...cmdparser.CommandOptions) {
+ options := cmdparser.CommandOptions{
+ Use: "deploy",
+ Short: localizer.Sprintf("Deploy current current context to a target environment"),
+ Run: c.run,
+ }
+
+ c.Cmd.DefineCommand(options)
+
+ c.AddFlag(cmdparser.FlagOptions{
+ String: &c.target,
+ DefaultString: "azure",
+ Name: "target",
+ Shorthand: "t",
+ Usage: localizer.Sprintf("Target cloud platform (azure, fabric)")})
+
+ c.AddFlag(cmdparser.FlagOptions{
+ String: &c.environment,
+ DefaultString: "",
+ Name: "environment",
+ Shorthand: "e",
+ Usage: localizer.Sprintf("Target environment name (default {username}-{sqlcmd-context-name})")})
+
+ c.AddFlag(cmdparser.FlagOptions{
+ Bool: &c.notFree,
+ Name: "not-free",
+ Usage: localizer.Sprintf("Use not free SKUs")})
+
+ c.AddFlag(cmdparser.FlagOptions{
+ Bool: &c.force,
+ Name: "force",
+ Usage: localizer.Sprintf("Remove existing azure.yaml, and .azure and infra folders")})
+}
+
+func (c *Deploy) run() {
+ output := c.Output()
+
+ current_contextName := config.CurrentContextName()
+ if current_contextName == "" {
+ output.FatalWithHintExamples([][]string{
+ {localizer.Sprintf("To view available contexts"), "sqlcmd config get-contexts"},
+ }, localizer.Sprintf("No current context"))
+ }
+
+ if c.force {
+ if file.Exists("azure.yaml") {
+ file.Remove("azure.yaml")
+ }
+ folder.RemoveAll("infra")
+ folder.RemoveAll(".azure")
+ folder.RemoveAll(filepath.Join(".sqlcmd", "DataApiBuilder"))
+ }
+
+ // BUBUG: For some reason azd provision needs dotnet cli installed, so do an early check
+ // See the comment at the point we call "azd provision" for more details
+ // TEMP: Check dotnet is installed
+ {
+ dotnetCliName := "dotnet"
+ if runtime.GOOS == "windows" {
+ dotnetCliName = "dotnet.exe"
+ }
+
+ path, err := exec.LookPath(dotnetCliName)
+ c.CheckErr(err)
+
+ if path == "" {
+ output.FatalWithHints(
+ []string{"Install the dotnet CLI"},
+ fmt.Sprintf("%q CLI does not exist in the PATH directories", dotnetCliName))
+ }
+ }
+
+ // azd provision requires docker. podman isn't enough
+ {
+ dockerCliName := "docker"
+ if runtime.GOOS == "windows" {
+ dockerCliName = "docker.exe"
+ }
+
+ path, err := exec.LookPath(dockerCliName)
+ c.CheckErr(err)
+
+ if path == "" {
+ output.FatalWithHints(
+ []string{"Install Docker Desktop"},
+ fmt.Sprintf("%q does not exist in the PATH directories. `azd provision` requires docker.", dockerCliName))
+ }
+ }
+
+ azd := tools.NewTool("azd")
+ if !azd.IsInstalled() {
+ output.Fatalf(azd.HowToInstall())
+ }
+
+ var stdout bytes.Buffer
+
+ cmd := exec.Command("azd", "auth", "token", "--output", "json")
+ cmd.Stdout = &stdout
+ cmd.Start()
+ cmd.Wait()
+
+ // If we're not logged in, log in
+ if cmd.ProcessState.ExitCode() == 1 {
+ output.Info("Not logged using `azd`. Running `azd auth login`. Please complete login in the browser.")
+ exitCode, _ := azd.Run([]string{"auth", "login"}, tool.RunOptions{})
+ if exitCode != 0 {
+ output.Fatal(localizer.Sprintf("Error logging in to azd"))
+ }
+ }
+
+ stdout.Truncate(0)
+
+ cmd = exec.Command("azd", "auth", "token", "--output", "json")
+ cmd.Stdout = &stdout
+ cmd.Start()
+ cmd.Wait()
+ if cmd.ProcessState.ExitCode() != 0 {
+ output.Fatal(localizer.Sprintf("Error getting token from azd auth token"))
+ }
+
+ tokenBlob := stdout.String()
+
+ var token string
+ {
+ var payloadJson map[string]interface{}
+ json.Unmarshal([]byte(tokenBlob), &payloadJson)
+ token = payloadJson["token"].(string)
+ }
+
+ split := strings.Split(token, ".")
+
+ // base64 decode the payload
+ payload, _ := base64.StdEncoding.DecodeString(split[1])
+ payloadstring := string(payload)
+ if payloadstring[len(payloadstring)-1:] != "}" {
+ payloadstring += "}" // BUGBUG: Why do I need to do this!
+ }
+
+ // Get the email of the person logged into azd
+ var email string
+ {
+ var payloadJson map[string]interface{}
+ json.Unmarshal([]byte(payloadstring), &payloadJson)
+
+ // test is the key in the json
+ if payloadJson["email"] != nil {
+ email = payloadJson["email"].(string)
+ } else if payloadJson["upn"] != nil {
+ email = payloadJson["upn"].(string)
+ } else {
+ panic("Unable to get principal name from token")
+ }
+ }
+
+ output.Info("Using principal name: " + email)
+
+ email = strings.ToLower(email)
+ // get the string up to the @ from email
+ parts := strings.Split(email, "@")
+ username := parts[0]
+
+ // BUGBUG: Temporary code because go-mssqldb cannot log into Azure SQL
+ // if the azd auth login is not the same as the user logged into shell, e.g.
+ // if I log in to shell as alias@mycompany.com, and then log in to azd auth login
+ // as alias@hotmail.com. go-mssqldb cannot login as alias@hotmail.com. But
+ // SSMS can. So there is an issuse with go-mssqldb AAD auth here.
+
+ // if on windows, verify the logged in upn is the same as the azd auth login
+ if runtime.GOOS == "windows" {
+ out, err := exec.Command("cmd", "/C", "whoami /upn").Output()
+ if err != nil {
+ whoami := strings.ToLower(string(out))
+ whoami = strings.TrimRight(whoami, "\r\n")
+ if whoami != "" {
+ if email != whoami {
+ output.FatalWithHints(
+ []string{
+ localizer.Sprintf("Log in to shell as %q", whoami),
+ localizer.Sprintf("Log in to `azd auth login` as %q", email),
+ },
+ localizer.Sprintf(
+ "TEMP: Due to an issue with go-mssqldb, the shell login %q must be the same as the `azd auth login` %q",
+ whoami, email))
+ }
+ }
+ }
+ }
+
+ if _, err := os.Stat("azure.yaml"); os.IsNotExist(err) {
+ output.Info("")
+ output.Info("TEMP: `azd init` will run, and ask 2 questions, accept both defaults ('Use Code in Current Directory' and 'Confirm and continue initializing my app').")
+ output.Info("TEMP: https://github.com/Azure/azure-dev/issues/3339")
+ output.Info("TEMP: https://github.com/Azure/azure-dev/issues/3340")
+ output.Info("")
+ }
+
+ dotsqlcmdconfig.SetFileName(dotsqlcmdconfig.DefaultFileName())
+ dotsqlcmdconfig.Load()
+
+ databases := dotsqlcmdconfig.DatabaseNames()
+ if len(databases) == 0 {
+ panic("POC Limitation: At least one database has to be found in .sqlcmd file")
+ }
+ databaseName := databases[0]
+
+ // If the file azure.yaml does not exist in current directory
+ if _, err := os.Stat("azure.yaml"); os.IsNotExist(err) {
+ addons := dotsqlcmdconfig.AddonTypes()
+
+ for i, addon := range addons {
+ if addon == "dab" {
+
+ output.Info("TEMP: `azd init` will ask 'What port does 'DataApiBuilder' listen on?', 5000 is the common standard port")
+ output.Info("TEMP: https://github.com/Azure/azure-dev/issues/3341")
+ output.Info("")
+
+ path := filepath.Join(".sqlcmd", "DataApiBuilder")
+
+ folder.MkdirAll(path)
+
+ f := file.OpenFile(filepath.Join(path, "DataApiBuilder.csproj"))
+ f.WriteString(dataApiBuilderCsProj)
+ f.Close()
+
+ f = file.OpenFile(filepath.Join(path, "Dockerfile"))
+ f.WriteString(dockerfile)
+ f.Close()
+
+ f = file.OpenFile(filepath.Join(path, "Program.cs"))
+ f.Close()
+
+ var dabConfigJson map[string]interface{}
+
+ files := dotsqlcmdconfig.AddonFiles(i)
+
+ // There should be one --use-file (which points to the dab-config.json file)
+ if len(files) == 1 {
+
+ // Edit that dab-config.json file and force the data-source to read from the CONN_STRING variable
+ dabConfigString := file.GetContents(files[0])
+ json.Unmarshal([]byte(dabConfigString), &dabConfigJson)
+
+ dataSource := dabConfigJson["data-source"]
+ dataSource.(map[string]interface{})["connection-string"] = "@env('CONN_STRING')"
+
+ newData, err := json.Marshal(dabConfigJson)
+ if err != nil {
+ panic(err)
+ }
+
+ var prettyJSON bytes.Buffer
+ json.Indent(&prettyJSON, newData, "", " ")
+
+ f = file.OpenFile(filepath.Join(path, "dab-config.json"))
+ f.WriteString(prettyJSON.String())
+ f.Close()
+ } else {
+ panic("There should be exactly one dab-config.json file specified as a 'use' in the .sqlcmd file")
+ }
+ }
+ }
+
+ if c.environment == "" {
+ c.environment = username + "-" + current_contextName
+ }
+
+ exitCode, _ := azd.Run([]string{"init", "--environment", c.environment}, tool.RunOptions{Interactive: true})
+ if exitCode != 0 {
+ output.Fatal(localizer.Sprintf("Error initializing application"))
+ }
+
+ // Update the git ignore file so all the files azd init generated don't get checked
+ // in, unless the user intentionally wants them to
+ {
+ gitignore := ""
+ if file.Exists(".gitignore") {
+ gitignore = file.GetContents(".gitignore")
+ }
+
+ f := file.OpenFile(".gitignore")
+ defer f.Close()
+ if !strings.Contains(gitignore, ".sqlcmd/DataApiBuilder") {
+ f.WriteString(".sqlcmd/DataApiBuilder\n")
+ }
+
+ if file.Exists("next-steps.md") {
+ file.Remove("next-steps.md")
+ }
+
+ // Add bicep for the Azure SQL Server
+ {
+ f := file.OpenFile(filepath.Join("infra", "app", "db.bicep"))
+ f.WriteString(dbBicep)
+ f.Close()
+
+ folder.MkdirAll(filepath.Join("infra", "core", "database", "sqlserver"))
+ f = file.OpenFile(filepath.Join("infra", "core", "database", "sqlserver", "sqlserver.bicep"))
+ f.WriteString(sqlserverBicep)
+ f.Close()
+ }
+
+ // Alter azd init generated bicep to do the right things
+ {
+ // Alter bicep to create an Azure SQL database
+ mainBicep := file.GetContents(filepath.Join("infra", "main.bicep"))
+ mainBicep = strings.Replace(mainBicep, "module monitoring",
+ mainBicepDbCall+"\n\nmodule monitoring", 1)
+
+ // If windows then replace \r\n with \n
+ if runtime.GOOS == "windows" {
+ keyvaultBicep = strings.Replace(keyvaultBicep, "\n", "\r\n", -1)
+ }
+ mainBicep = strings.Replace(mainBicep, keyvaultBicep, "/*\n"+keyvaultBicep+"*/\n", 1)
+
+ // Alter bicep to remove Key Vault (we do everything with managed identities and entra, so no secrets to store
+ mainBicep = strings.Replace(mainBicep, "output AZURE_KEY_VAULT_NAME",
+ "// output AZURE_KEY_VAULT_NAME", 1)
+
+ mainBicep = strings.Replace(mainBicep, "output AZURE_KEY_VAULT_ENDPOINT",
+ "// output AZURE_KEY_VAULT_ENDPOINT", 1)
+
+ // Output the DAB uri, so we can pass it in to the front end
+ mainBicep += "\noutput DATA_API_BUILDER_ENDPOINT string = dataApiBuilder.outputs.uri\noutput AZURE_CLIENT_ID string = dataApiBuilder.outputs.managedUserIdentity\n"
+
+ f := file.OpenFile(filepath.Join("infra", "main.bicep"))
+ f.WriteString(string(mainBicep))
+ f.Close()
+ }
+
+ {
+ // Shrink the container size to minimum, to keep costs down
+ dabBicep := file.GetContents(filepath.Join("infra", "app", "DataApiBuilder.bicep"))
+ dabBicep += "\noutput managedUserIdentity string = identity.properties.clientId\n"
+
+ f := file.OpenFile(filepath.Join("infra", "app", "DataApiBuilder.bicep"))
+ f.WriteString(dabBicep)
+ f.Close()
+
+ // go through all the Bicep files and reduce the container size to keep costs down
+ files, err := ioutil.ReadDir(filepath.Join("infra", "app"))
+ if err != nil {
+ output.FatalErr(err)
+ }
+
+ for _, f := range files {
+ if !f.IsDir() {
+ if strings.HasSuffix(f.Name(), ".bicep") {
+ bicep := file.GetContents(filepath.Join("infra", "app", f.Name()))
+ bicep = strings.Replace(bicep, "cpu: json('1.0')", "cpu: json('0.25')", -1)
+ bicep = strings.Replace(bicep, "memory: '2.0Gi'", "memory: '0.5Gi'", -1)
+ f := file.OpenFile(filepath.Join("infra", "app", f.Name()))
+ f.WriteString(bicep)
+ f.Close()
+ }
+ }
+ }
+ }
+ }
+
+ {
+ var mainParamsJson map[string]interface{}
+ mainParamsString := file.GetContents(filepath.Join("infra", "main.parameters.json"))
+ json.Unmarshal([]byte(mainParamsString), &mainParamsJson)
+
+ // Append a parameter sqlAdminLoginName to the parameters object
+ mainParamsJson["parameters"].(map[string]interface{})["sqlAdminLoginName"] = map[string]interface{}{
+ "value": "${AZURE_PRINCIPAL_NAME}",
+ }
+
+ mainParamsJson["parameters"].(map[string]interface{})["sqlDatabaseName"] = map[string]interface{}{
+ "value": databaseName,
+ }
+
+ mainParamsJson["parameters"].(map[string]interface{})["sqlClientIpAddress"] = map[string]interface{}{
+ "value": "${MY_IP}",
+ }
+
+ mainParamsJson["parameters"].(map[string]interface{})["useFreeLimit"] = map[string]interface{}{
+ "value": "${USE_FREE_LIMIT}",
+ }
+
+ var arr []interface{}
+ arr = append(arr, map[string]interface {
+ }{
+ "name": "ASPNETCORE_ENVIRONMENT",
+ "value": "Development",
+ })
+
+ mainParamsJson["parameters"].(map[string]interface {
+ })["dataApiBuilderDefinition"].(map[string]interface {
+ })["value"].(map[string]interface {
+ })["settings"] = arr
+
+ newData, err := json.Marshal(mainParamsJson)
+ if err != nil {
+ panic(err)
+ }
+
+ var prettyJSON bytes.Buffer
+ json.Indent(&prettyJSON, newData, "", " ")
+
+ f := file.OpenFile(filepath.Join("infra", "main.parameters.json"))
+ f.WriteString(prettyJSON.String())
+ f.Close()
+ }
+ }
+
+ // BUGBUG: Do this using a microsoft blessed method (SSMS/ADS must do this in the connection dialogs)
+ output.Infof("\nDiscovering IP address for this client, to allow firewall access to the Azure SQL server")
+
+ ip, err := ipify.GetIp()
+ output.FatalErr(err)
+
+ output.Infof("Setting local Address to %q to have access to the Azure SQL server", ip)
+
+ exitCode, _ := azd.Run([]string{"env", "set", "MY_IP", ip}, tool.RunOptions{})
+ if exitCode != 0 {
+ output.Fatal(localizer.Sprintf("Error setting environment variable MY_IP"))
+ }
+
+ exitCode, _ = azd.Run([]string{"env", "set", "AZURE_PRINCIPAL_NAME", email}, tool.RunOptions{})
+ if exitCode != 0 {
+ output.Fatal(localizer.Sprintf("Error setting environment variable AZURE_PRINCIPAL_NAME"))
+ }
+
+ exitCode, _ = azd.Run([]string{"env", "set", "USE_FREE_LIMIT", fmt.Sprintf("%t", !c.notFree)}, tool.RunOptions{})
+ if exitCode != 0 {
+ output.Fatal(localizer.Sprintf("Error setting environment variable USE_FREE_LIMIT"))
+ }
+
+ // BUGBUG: There seems to be a dependency on dotnet being on the machine!
+
+ /* Provisioning Azure resources (azd provision)
+
+ Provisioning Azure resources can take some time.
+
+ ERROR: initializing service 'DataApiBuilder', failed to initialize secrets at project '/Users/stuartpa/demo/.sqlcmd/DataApiBuilder/DataApiBuilder.csproj': exec: "dotnet": executable file not found in $PATH
+
+ Although interesting, is the secrets even needed for what we are doing!
+ */
+ exitCode, _ = azd.Run([]string{"provision"}, tool.RunOptions{Interactive: true})
+ if exitCode != 0 {
+ output.FatalWithHintExamples([][]string{
+ {localizer.Sprintf("To clean up any resources created"), "azd down --force"},
+ {localizer.Sprintf("To not create an Azure SQL 'Spinnaker' run again with --not-free"), "sqlcmd deploy --not-free"},
+ {localizer.Sprintf("If failed with 'Invalid value given for parameter ExternalAdministratorLoginName'"), "azd auth login. Use a corp account (e.g. not hotmail.com etc.)"},
+ {localizer.Sprintf("If, 'The client ... does not have permission to perform action 'Microsoft.Authorization/roleAssignments/write' at scope ... Microsoft.ContainerRegistry'"), "See: https://github.com/Azure-Samples/azure-search-openai-demo#azure-account-requirements"},
+ }, localizer.Sprintf("Error provisioning infrastructure"))
+ }
+
+ var defaultEnvironment string
+ {
+ var payloadJson map[string]interface{}
+ configJson := file.GetContents(filepath.Join(".azure", "config.json"))
+ json.Unmarshal([]byte(configJson), &payloadJson)
+ if payloadJson["defaultEnvironment"] != nil {
+ defaultEnvironment = payloadJson["defaultEnvironment"].(string)
+ } else {
+ panic("Unable to get defaultEnvironment from " + filepath.Join(".azure", "config.json"))
+ }
+ }
+
+ // Run the SQL scripts
+ filename := filepath.Join(".azure", defaultEnvironment, ".env")
+
+ envFile, err := godotenv.Read(filename)
+ if err != nil {
+ panic("Unable to read .env file: " + filename)
+ }
+
+ random, ok := envFile["AZURE_CONTAINER_REGISTRY_ENDPOINT"]
+ if !ok {
+ panic("AZURE_CONTAINER_REGISTRY_ENDPOINT is not set in .env")
+ }
+ if random == "" {
+ panic("AZURE_CONTAINER_REGISTRY_ENDPOINT is not set in .env")
+ }
+
+ // Get the word up to the first .
+ random = strings.Split(random, ".")[0]
+
+ if len(random) < 2 {
+ panic("Random is too short, the value is '" + random + "'")
+ }
+
+ // Remove the first two characters from random (the 'cr' which stands for Container Registry)
+ random = random[2:]
+
+ endpoint := sqlconfig.Endpoint{
+ EndpointDetails: sqlconfig.EndpointDetails{
+ Address: "sql-" + random + ".database.windows.net",
+ Port: 1433,
+ },
+ }
+
+ authType := "ActiveDirectoryDefault"
+
+ // if on mac use Interactive, because Default doesn't work
+ if runtime.GOOS == "darwin" {
+ authType = "ActiveDirectoryInteractive"
+ }
+
+ user := sqlconfig.User{
+ Name: email,
+ AuthenticationType: authType,
+ }
+
+ // options := sql.ConnectOptions{Database: databaseName, Interactive: false}
+ // options.LogLevel = 255
+
+ // Enable the Managed Identity for the DataApiBuilder service to have permissions
+ // to the Azure SQL Database
+ s := sql.NewSql(sql.SqlOptions{})
+ s.Connect(endpoint, &user, sql.ConnectOptions{Database: databaseName, Interactive: false})
+
+ addons := dotsqlcmdconfig.AddonTypes()
+ for _, addon := range addons {
+ if addon == "dab" {
+
+ azureClientId, ok := envFile["AZURE_CLIENT_ID"]
+ if !ok {
+ panic("AZURE_CLIENT_ID is not set in .env")
+ }
+ if azureClientId == "" {
+ panic("AZURE_CLIENT_ID is not set in .env")
+ }
+
+ f := file.OpenFile(filepath.Join(".sqlcmd", "DataApiBuilder", "Dockerfile"))
+ f.WriteString(
+ fmt.Sprintf(dockerfile,
+ fmt.Sprintf("Server=sql-%s.database.windows.net; Database=%s; Authentication=Active Directory Default; Encrypt=True",
+ random,
+ databaseName),
+ azureClientId))
+ f.Close()
+
+ s.Query("DROP USER IF EXISTS [id-dataapibuild-" + random + "]")
+ s.Query("CREATE USER [id-dataapibuild-" + random + "] FROM EXTERNAL PROVIDER")
+ s.Query("ALTER ROLE db_datareader ADD MEMBER [id-dataapibuild-" + random + "]")
+ s.Query("ALTER ROLE db_datawriter ADD MEMBER [id-dataapibuild-" + random + "]")
+ }
+ }
+
+ // If folder .sqlcmd exists
+ if file.Exists(".sqlcmd") {
+ dotsqlcmdconfig.SetFileName(dotsqlcmdconfig.DefaultFileName())
+ dotsqlcmdconfig.Load()
+ files := dotsqlcmdconfig.DatabaseFiles(0)
+
+ //for each file in folder .sqlcmd
+ for _, fi := range files {
+ //if file is .sql
+ if strings.HasSuffix(fi, ".sql") {
+
+ // if on Windows, replace / with \\
+ if runtime.GOOS == "windows" {
+ fi = strings.Replace(fi, "/", "\\", -1)
+ } else {
+ fi = strings.Replace(fi, "\\", "/", -1)
+ }
+
+ //run sql file
+ output.Infof("Running %q", fi)
+
+ s.ExecuteSqlFile(fi)
+ } else {
+ panic(fmt.Sprintf("File %q is not supported", fi))
+ }
+ }
+
+ exitCode, _ = azd.Run([]string{"package"}, tool.RunOptions{Interactive: true})
+ if exitCode != 0 {
+ output.Fatal(localizer.Sprintf("Error packaging application"))
+ }
+
+ exitCode, _ = azd.Run([]string{"deploy"}, tool.RunOptions{Interactive: true})
+ if exitCode != 0 {
+ output.Fatal(localizer.Sprintf("Error deploying application"))
+ }
+
+ output.InfofWithHintExamples([][]string{
+ {localizer.Sprintf("To view the deployed resources"), "azd show"},
+ {localizer.Sprintf("To setup a deployment pipeline"), "azd pipeline config --help"},
+ {localizer.Sprintf("To delete all resource in Azure"), "azd down --force"},
+ }, localizer.Sprintf("Successfully deployed application to %q", c.target))
+
+ }
+}
+
+var dataApiBuilderCsProj = `
+
+ net8.0
+
+
+`
+
+var dockerfile = `FROM mcr.microsoft.com/azure-databases/data-api-builder:latest
+
+COPY dab-config.json /App
+WORKDIR /App
+ENV CONN_STRING='%s'
+ENV AZURE_CLIENT_ID=%s
+ENV ASPNETCORE_URLS=http://+:5000
+EXPOSE 5000
+ENTRYPOINT ["dotnet", "Azure.DataApiBuilder.Service.dll"]`
+
+var mainBicepDbCall = `param sqlDatabaseName string = ''
+param sqlServerName string = ''
+
+param sqlAdminLoginName string
+param sqlClientIpAddress string
+param useFreeLimit bool
+
+// The application database
+module sqlServer './app/db.bicep' = {
+ name: 'sql'
+ scope: rg
+ params: {
+ name: !empty(sqlServerName) ? sqlServerName : '${abbrs.sqlServers}${resourceToken}'
+ databaseName: sqlDatabaseName
+ location: location
+ tags: tags
+ sqlAdminLoginName: sqlAdminLoginName
+ sqlAdminLoginObjectId: principalId
+ sqlClientIpAddress: sqlClientIpAddress
+ useFreeLimit: useFreeLimit
+ }
+}
+`
+
+var dbBicep = `param name string
+param location string = resourceGroup().location
+param tags object = {}
+
+param databaseName string = ''
+param sqlAdminLoginName string
+param sqlAdminLoginObjectId string
+param sqlClientIpAddress string
+param useFreeLimit bool
+
+module sqlServer '../core/database/sqlserver/sqlserver.bicep' = {
+ name: 'sqlserver'
+ params: {
+ name: name
+ location: location
+ tags: tags
+ databaseName: databaseName
+ sqlAdminLoginName: sqlAdminLoginName
+ sqlAdminLoginObjectId: sqlAdminLoginObjectId
+ sqlClientIpAddress: sqlClientIpAddress
+ useFreeLimit: useFreeLimit
+ }
+}
+
+output connectionString string = sqlServer.outputs.connectionString
+`
+
+var sqlserverBicep = `metadata description = 'Creates an Azure SQL Server instance.'
+param name string
+param location string = resourceGroup().location
+param tags object = {}
+
+param sqlAdminLoginName string
+param sqlAdminLoginObjectId string
+param sqlClientIpAddress string
+param databaseName string
+param useFreeLimit bool
+
+resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = {
+ name: name
+ location: location
+ tags: tags
+ properties: {
+ administrators: {
+ azureADOnlyAuthentication: true
+ administratorType: 'ActiveDirectory'
+ tenantId: subscription().tenantId
+ principalType: 'User'
+ login: sqlAdminLoginName
+ sid: sqlAdminLoginObjectId
+ }
+ version: '12.0'
+ minimalTlsVersion: '1.2'
+ publicNetworkAccess: 'Enabled'
+ }
+ identity: {
+ type: 'SystemAssigned'
+ }
+
+ resource database 'databases' = {
+ name: databaseName
+ location: location
+ sku: {
+ name: 'GP_S_Gen5_2'
+ }
+ properties: {
+ useFreeLimit: useFreeLimit
+ }
+ }
+
+ resource firewall 'firewallRules' = {
+ name: 'Azure Services'
+ properties: {
+ // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only".
+ startIpAddress: '0.0.0.0'
+ endIpAddress: '0.0.0.0'
+ }
+ }
+}
+
+resource clientFirewallRules 'Microsoft.Sql/servers/firewallRules@2023-05-01-preview' = {
+ name: 'AllowClientIp'
+ parent: sqlServer
+ properties: {
+ startIpAddress: sqlClientIpAddress
+ endIpAddress: sqlClientIpAddress
+ }
+}
+
+var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; Authentication=Active Directory Default; Encrypt=True'
+output connectionString string = connectionString
+output databaseName string = sqlServer::database.name
+`
+
+var keyvaultBicep = `module keyVault './shared/keyvault.bicep' = {
+ name: 'keyvault'
+ params: {
+ location: location
+ tags: tags
+ name: '${abbrs.keyVaultVaults}${resourceToken}'
+ principalId: principalId
+ }
+ scope: rg
+}`
diff --git a/cmd/modern/root/install/dot-sqlcmd-config.go b/cmd/modern/root/install/dot-sqlcmd-config.go
new file mode 100644
index 00000000..c0ed29c1
--- /dev/null
+++ b/cmd/modern/root/install/dot-sqlcmd-config.go
@@ -0,0 +1 @@
+package install
diff --git a/cmd/modern/root/install/edge_test.go b/cmd/modern/root/install/edge_test.go
index c01d7dc0..18c04f2f 100644
--- a/cmd/modern/root/install/edge_test.go
+++ b/cmd/modern/root/install/edge_test.go
@@ -25,7 +25,7 @@ func TestInstallEdge(t *testing.T) {
cmdparser.TestCmd[*edge.GetTags]()
cmdparser.TestCmd[*Edge](
fmt.Sprintf(
- `--accept-eula --user-database foo --errorlog-wait-line "Hello from Docker!" --registry %v --repo %v`,
+ `--accept-eula --database foo --errorlog-wait-line "Hello from Docker!" --registry %v --repo %v`,
registry,
repo))
diff --git a/cmd/modern/root/install/mssql-base.go b/cmd/modern/root/install/mssql-base.go
index 924123ae..b22d0d59 100644
--- a/cmd/modern/root/install/mssql-base.go
+++ b/cmd/modern/root/install/mssql-base.go
@@ -4,23 +4,33 @@
package install
import (
+ "bytes"
+ "encoding/json"
"fmt"
- "net/url"
- "path"
"path/filepath"
"runtime"
+ "strconv"
"strings"
+ "github.com/microsoft/go-sqlcmd/cmd/modern/root/open"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser/dependency"
+ "github.com/microsoft/go-sqlcmd/internal/dotsqlcmdconfig"
+ "github.com/microsoft/go-sqlcmd/internal/io/file"
+ "github.com/microsoft/go-sqlcmd/internal/io/folder"
+ "github.com/microsoft/go-sqlcmd/internal/tools"
+ "github.com/microsoft/go-sqlcmd/internal/tools/tool"
+
"github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
"github.com/microsoft/go-sqlcmd/internal/cmdparser"
"github.com/microsoft/go-sqlcmd/internal/config"
"github.com/microsoft/go-sqlcmd/internal/container"
- "github.com/microsoft/go-sqlcmd/internal/http"
"github.com/microsoft/go-sqlcmd/internal/localizer"
"github.com/microsoft/go-sqlcmd/internal/output"
"github.com/microsoft/go-sqlcmd/internal/pal"
"github.com/microsoft/go-sqlcmd/internal/secret"
"github.com/microsoft/go-sqlcmd/internal/sql"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest/mechanism"
"github.com/spf13/viper"
)
@@ -55,7 +65,15 @@ type MssqlBase struct {
port int
- usingDatabaseUrl string
+ useUrl []string
+ useMechanism []string
+
+ openTool string
+ openFile string
+
+ network string
+ addOn []string
+ addOnUse []string
unitTesting bool
@@ -101,6 +119,14 @@ func (c *MssqlBase) AddFlags(
String: &c.defaultDatabase,
Name: "user-database",
Shorthand: "u",
+ Hidden: true,
+ Usage: localizer.Sprintf("[DEPRECATED use --database] Create a user database and set it as the default for login"),
+ })
+
+ addFlag(cmdparser.FlagOptions{
+ String: &c.defaultDatabase,
+ Name: "database",
+ Shorthand: "d",
Usage: localizer.Sprintf("Create a user database and set it as the default for login"),
})
@@ -213,10 +239,57 @@ func (c *MssqlBase) AddFlags(
})
addFlag(cmdparser.FlagOptions{
- String: &c.usingDatabaseUrl,
+ StringArray: &c.useUrl,
+ Name: "using",
+ Hidden: true,
+ Usage: localizer.Sprintf("[DEPRECATED use --use] Download %q and use database", ingest.ValidFileExtensions()),
+ })
+
+ addFlag(cmdparser.FlagOptions{
+ StringArray: &c.useUrl,
+ Name: "use",
+ Usage: localizer.Sprintf("Download %q and use database", ingest.ValidFileExtensions()),
+ })
+
+ addFlag(cmdparser.FlagOptions{
+ StringArray: &c.useMechanism,
+ Name: "use-mechanism",
+ Usage: localizer.Sprintf("Mechanism to use to bring database online (%s)", strings.Join(mechanism.Mechanisms(), ",")),
+ })
+
+ addFlag(cmdparser.FlagOptions{
+ String: &c.openTool,
+ DefaultString: "",
+ Name: "open",
+ Usage: localizer.Sprintf("Open tool e.g. ads"),
+ })
+
+ addFlag(cmdparser.FlagOptions{
+ String: &c.openFile,
+ DefaultString: "",
+ Name: "open-file",
+ Usage: localizer.Sprintf("Open file in tool e.g. https://aks.ms/adventureworks-demo.sql"),
+ })
+
+ addFlag(cmdparser.FlagOptions{
+ String: &c.network,
+ DefaultString: "",
+ Name: "network",
+ Usage: localizer.Sprintf("Container network name (defaults to 'container-network' if --add-on specified)"),
+ })
+
+ addFlag(cmdparser.FlagOptions{
+ StringArray: &c.addOn,
+ DefaultString: "",
+ Name: "add-on",
+ Usage: localizer.Sprintf("Create add-on container (i.e. dab, fleet-manager)"),
+ })
+
+ addFlag(cmdparser.FlagOptions{
+ StringArray: &c.addOnUse,
DefaultString: "",
- Name: "using",
- Usage: localizer.Sprintf("Download (into container) and attach database (.bak) from URL"),
+ Name: "add-on-use",
+ Usage: localizer.Sprintf("File to use for add-on container"),
})
}
@@ -229,10 +302,10 @@ func (c *MssqlBase) AddFlags(
// If the EULA has not been accepted, it prints an error message with suggestions for how to proceed,
// and exits the program.
func (c *MssqlBase) Run() {
- output := c.Cmd.Output()
-
var imageName string
+ output := c.Cmd.Output()
+
if !c.acceptEula && viper.GetString("ACCEPT_EULA") == "" {
output.FatalWithHints(
[]string{localizer.Sprintf("Either, add the %s flag to the command-line", localizer.AcceptEulaFlag),
@@ -240,12 +313,9 @@ func (c *MssqlBase) Run() {
localizer.Sprintf("EULA not accepted"))
}
- imageName = fmt.Sprintf(
- "%s/%s:%s",
- c.registry,
- c.repo,
- c.tag)
+ imageName = fmt.Sprintf("%s/%s:%s", c.registry, c.repo, c.tag)
+ // If no context name provided, set it to the default (e.g. mssql or edge)
if c.contextName == "" {
c.contextName = c.defaultContextName
}
@@ -253,7 +323,43 @@ func (c *MssqlBase) Run() {
c.createContainer(imageName, c.contextName)
}
-// createContainer installs an image for a SQL Server container. The image
+func (c *MssqlBase) GetValuesFromDotSqlcmd() {
+ if file.Exists(filepath.Join(".sqlcmd", "sqlcmd.yaml")) {
+ dotsqlcmdconfig.SetFileName(dotsqlcmdconfig.DefaultFileName())
+
+ // If there is a .sqlcmd/sqlcmd.yaml file, then load that up and use it for any values not provided
+ dotsqlcmdconfig.Load()
+ dbs := dotsqlcmdconfig.DatabaseNames()
+
+ if len(dbs) > 1 {
+ panic("Only a single database is supported at this time")
+ }
+
+ if len(dbs) > 0 {
+ if c.defaultDatabase == "" {
+ c.defaultDatabase = dbs[0]
+ }
+
+ if len(c.useUrl) == 0 {
+ c.useUrl = append(c.useUrl, dotsqlcmdconfig.DatabaseFiles(0)...)
+ }
+ }
+
+ addons := dotsqlcmdconfig.AddonTypes()
+
+ if len(addons) > 0 {
+ c.addOn = append(c.addOn, addons...)
+ }
+
+ for i, _ := range c.addOn {
+ if len(c.addOnUse) < i+1 {
+ c.addOnUse = append(c.addOnUse, dotsqlcmdconfig.AddonFiles(i)...)
+ }
+ }
+ }
+}
+
+// createContainer creates a SQL Server container for an image. The image
// is specified by imageName, and the container will be given the name contextName.
// If the useCached flag is set, the function will skip downloading the image
// from the internet. The function outputs progress messages to the command-line
@@ -261,77 +367,118 @@ func (c *MssqlBase) Run() {
// command-line and the program will exit.
func (c *MssqlBase) createContainer(imageName string, contextName string) {
output := c.Cmd.Output()
+ controller := container.NewController()
saPassword := c.generatePassword()
+ userName := pal.UserName()
+ password := c.generatePassword()
- env := []string{
- "ACCEPT_EULA=Y",
- fmt.Sprintf("MSSQL_SA_PASSWORD=%s", saPassword),
- fmt.Sprintf("MSSQL_COLLATION=%s", c.collation),
- }
+ contextName = config.FindUniqueContextName(contextName, userName)
if c.port == 0 {
- c.port = config.FindFreePortForTds()
+ c.port = config.FindFreePort(1433)
}
// Do an early exit if url doesn't exist
- if c.usingDatabaseUrl != "" {
- c.validateUsingUrlExists()
+ var useUrls []ingest.Ingest
+ if len(c.useUrl) > 0 {
+ useUrls = c.verifyUseSourceFileExists(controller, output)
+ }
+
+ if len(useUrls) == 1 {
+ if useUrls[0].UserProvidedFileExt() == "git" {
+ useUrls[0].BringOnline(nil, "", "")
+ useUrls = nil
+ c.useUrl = nil // Blank this out, because we now will get more useUrls from the .sqlcmd/sqlcmd.yaml file
+ c.useMechanism = nil
+ }
+ }
+
+ // Now that we have any remote repo cloned local, now is the time to
+ // go look for the .sqlcmd/sqlcmd.yaml settings
+ c.GetValuesFromDotSqlcmd()
+
+ if len(c.useUrl) > 0 {
+ useUrls = c.verifyUseSourceFileExists(controller, output)
}
if c.defaultDatabase != "" {
if !c.validateDbName(c.defaultDatabase) {
- output.Fatalf(localizer.Sprintf("--user-database %q contains non-ASCII chars and/or quotes", c.defaultDatabase))
+ output.Fatalf(localizer.Sprintf("--database %q contains non-ASCII chars and/or quotes", c.defaultDatabase))
}
}
- controller := container.NewController()
+ // If an add-on is specified, and no network name, then set a default network name
+ if len(c.addOn) > 0 && c.network == "" {
+ c.network = "sqlcmd-" + contextName + "-network"
+ }
+
+ if c.network != "" {
+ // Create a docker network
+ if !controller.NetworkExists(c.network) {
+ output.Info(localizer.Sprintf("Creating %q, for add-on cross container communication", c.network))
+ controller.NetworkCreate(c.network)
+ }
+ }
+
+ // Very strange issue that we need to work here. If we are using add-on containers
+ // we have to specify the name of the mssql container!
+ // Details in this bug/DCR here:
+ // https://github.com/moby/moby/issues/45183
+ if c.name == "" {
+ c.name = contextName + "-container"
+ }
if !c.useCached {
c.downloadImage(imageName, output, controller)
}
- output.Info(localizer.Sprintf("Starting %v", imageName))
+ runOptions := container.RunOptions{
+ PortInternal: 1433,
+ Port: c.port,
+ Name: c.name,
+ Hostname: c.hostname,
+ Architecture: c.architecture,
+ Os: c.os,
+ Network: c.network,
+ }
+
+ runOptions.Env = []string{
+ "ACCEPT_EULA=Y",
+ fmt.Sprintf("MSSQL_SA_PASSWORD=%s", saPassword),
+ fmt.Sprintf("MSSQL_COLLATION=%s", c.collation)}
+
+ output.Info(localizer.Sprintf("Starting %q", imageName))
+
containerId := controller.ContainerRun(
imageName,
- env,
- c.port,
- c.name,
- c.hostname,
- c.architecture,
- c.os,
- []string{},
- false,
+ runOptions,
)
previousContextName := config.CurrentContextName()
- userName := pal.UserName()
- password := c.generatePassword()
-
// Save the config now, so user can uninstall/delete, even if mssql in the container
// fails to start
- config.AddContextWithContainer(
- contextName,
- imageName,
- c.port,
- containerId,
- userName,
- password,
- c.passwordEncryption,
- )
-
- output.Info(
- localizer.Sprintf("Created context %q in \"%s\", configuring user account...",
+ contextOptions := config.ContextOptions{
+ ImageName: imageName,
+ PortNumber: c.port,
+ ContainerId: containerId,
+ Username: pal.UserName(),
+ Password: password,
+ PasswordEncryption: c.passwordEncryption,
+ Network: c.network}
+ config.AddContextWithContainer(contextName, contextOptions)
+
+ output.Infof(
+ localizer.Sprintf("Created context %q in \"%s\", configuring user account",
config.CurrentContextName(),
config.GetConfigFileUsed()))
- controller.ContainerWaitForLogEntry(
- containerId, c.errorLogEntryToWaitFor)
+ controller.ContainerWaitForLogEntry(containerId, c.errorLogEntryToWaitFor)
output.Info(
localizer.Sprintf("Disabled %q account (and rotated %q password). Creating user %q",
"sa",
"sa",
- userName))
+ contextOptions.Username))
endpoint, _ := config.CurrentContext()
@@ -339,11 +486,11 @@ func (c *MssqlBase) createContainer(imageName string, contextName string) {
//
// For Unit Testing we use the Docker Hello World container, which
// starts much faster than the SQL Server container!
+ sqlOptions := sql.SqlOptions{}
if c.errorLogEntryToWaitFor == "Hello from Docker!" {
- c.sql = sql.New(sql.SqlOptions{UnitTesting: true})
- } else {
- c.sql = sql.New(sql.SqlOptions{UnitTesting: false})
+ sqlOptions.UnitTesting = true
}
+ c.sql = sql.NewSql(sqlOptions)
saUser := &sqlconfig.User{
AuthenticationType: "basic",
@@ -353,22 +500,209 @@ func (c *MssqlBase) createContainer(imageName string, contextName string) {
Password: secret.Encode(saPassword, c.passwordEncryption)},
Name: "sa"}
- c.sql.Connect(endpoint, saUser, sql.ConnectOptions{Database: "master", Interactive: false})
+ // Connect to master database on SQL Server in the container as `sa`
+ c.sql.Connect(
+ endpoint,
+ saUser,
+ sql.ConnectOptions{Database: "master", Interactive: false},
+ )
- c.createNonSaUser(userName, password)
+ // Create a new (non-sa) SQL Server user
+ c.createUser(contextOptions.Username, contextOptions.Password)
+
+ // Download and restore/attach etc. DB if asked
+ if len(useUrls) > 0 {
+ for i, useUrl := range useUrls {
+ if useUrl.IsRemoteUrl() {
+ if useUrl.UserProvidedFileExt() != "git" {
+ output.Infof("Downloading %q to container", useUrl.UrlFilename())
+ }
+ } else {
+ if useUrl.OnlineMethod() != "script" {
+ output.Infof("Copying %q to container", useUrl.UrlFilename())
+ }
+ }
+ useUrl.CopyToContainer(containerId)
- // Download and restore DB if asked
- if c.usingDatabaseUrl != "" {
- c.downloadAndRestoreDb(
- controller,
- containerId,
- userName,
- )
+ if useUrl.IsExtractionNeeded() {
+ output.Infof("Extracting files from %q archive", useUrl.UrlFilename())
+ useUrl.Extract()
+ }
+
+ if useUrl.OnlineMethod() != "script" {
+ output.Infof("Bringing database %q online (%s)", useUrl.DatabaseName(), useUrl.OnlineMethod())
+ }
+
+ // Connect to master, unless a default database was specified (at this point the default database
+ // has not been set yet, so we need to specify it in the -d statement
+ databaseToConnectTo := "master"
+ if c.defaultDatabase != "" {
+ databaseToConnectTo = c.defaultDatabase
+ }
+ if useUrl.OnlineMethod() == "script" {
+ runSqlcmdInContainer := func(query string) {
+ cmd := []string{
+ "/opt/mssql-tools/bin/sqlcmd",
+ "-S",
+ "localhost",
+ "-U",
+ contextOptions.Username,
+ "-P",
+ contextOptions.Password,
+ "-d",
+ databaseToConnectTo,
+ "-i",
+ "/var/opt/mssql/backup/" + useUrl.UrlFilename(),
+ }
+
+ controller.RunCmdInContainer(containerId, cmd, container.ExecOptions{})
+ }
+ useUrl.BringOnline(runSqlcmdInContainer, contextOptions.Username, contextOptions.Password)
+ } else {
+ useUrl.BringOnline(c.sql.Query, contextOptions.Username, contextOptions.Password)
+ }
+
+ for _, f := range dotsqlcmdconfig.DatabaseFiles(i) {
+ //if file is .sql
+ if strings.HasSuffix(f, ".sql") {
+ // If not on Windows, Replace \ with / in the file path
+ if runtime.GOOS != "windows" {
+ f = strings.Replace(f, "\\", "/", -1)
+ } else {
+ // If on Windows, replace / with \ in the file path
+ f = strings.Replace(f, "/", "\\", -1)
+ }
+
+ //run sql file
+ output.Infof("Running %q", f)
+
+ c.sql.ExecuteSqlFile(f)
+ }
+ }
+ }
+ }
+
+ dabPort := 0
+ fleetManagerPort := 0
+ for i, addOn := range c.addOn {
+
+ if addOn == "fleet-manager" {
+ dabImageName := "fleet-manager"
+
+ if !c.useCached {
+ c.downloadImage(dabImageName, output, controller)
+ }
+
+ fleetManagerPort = config.FindFreePort(8080)
+
+ fleetManagerRunOptions := container.RunOptions{
+ PortInternal: 80,
+ Port: fleetManagerPort,
+ Architecture: c.architecture,
+ Os: c.os,
+ Network: c.network,
+ }
+
+ addOnContainerId := controller.ContainerRun(
+ dabImageName,
+ fleetManagerRunOptions,
+ )
+
+ contextName := config.CurrentContextName()
+
+ // Save add-on details to config file now, so it can be deleted even
+ // if something below fails
+ config.AddAddOn(
+ contextName,
+ "fleet-manager",
+ addOnContainerId,
+ dabImageName,
+ "127.0.0.1",
+ fleetManagerPort)
+ } else if addOn == "dab" {
+ dabImageName := "mcr.microsoft.com/azure-databases/data-api-builder"
+
+ if !c.useCached {
+ c.downloadImage(dabImageName, output, controller)
+ }
+
+ dabPort = config.FindFreePort(5000)
+
+ dabRunOptions := container.RunOptions{
+ PortInternal: 5000,
+ Port: dabPort,
+ Architecture: c.architecture,
+ Os: c.os,
+ Network: c.network,
+ }
+
+ addOnContainerId := controller.ContainerRun(
+ dabImageName,
+ dabRunOptions,
+ )
+
+ contextName := config.CurrentContextName()
+
+ // Save add-on details to config file now, so it can be deleted even
+ // if something below fails
+ config.AddAddOn(
+ contextName,
+ "dab",
+ addOnContainerId,
+ dabImageName,
+ "127.0.0.1",
+ dabPort)
+
+ if len(c.addOnUse) >= i+1 {
+
+ var dabConfigJson map[string]interface{}
+ dabConfigString := file.GetContents(c.addOnUse[i])
+ json.Unmarshal([]byte(dabConfigString), &dabConfigJson)
+
+ dataSource := dabConfigJson["data-source"]
+ dataSource.(map[string]interface{})["connection-string"] =
+ fmt.Sprintf("Server=%s;Database=%s;User ID=%s;Password=%s;TrustServerCertificate=true",
+ c.name,
+ c.defaultDatabase,
+ userName,
+ password)
+
+ newData, err := json.Marshal(dabConfigJson)
+ if err != nil {
+ panic(err)
+ }
+
+ var prettyJSON bytes.Buffer
+ json.Indent(&prettyJSON, newData, "", " ")
+
+ folder.RemoveAll("tmp-dab-config.json")
+ folder.MkdirAll("tmp-dab-config.json")
+
+ f := file.OpenFile(filepath.Join("tmp-dab-config.json", "dab-config.json"))
+ f.WriteString(prettyJSON.String())
+ f.Close()
+
+ // Download the dab-config file to the container
+ controller.CopyFile(
+ addOnContainerId,
+ filepath.Join("tmp-dab-config.json", "dab-config.json"),
+ "/App",
+ )
+
+ folder.RemoveAll("tmp-dab-config.json")
+ }
+
+ // Restart the container, now that the dab-config file is there
+ controller.ContainerStop(addOnContainerId)
+ controller.ContainerStart(addOnContainerId)
+ } else {
+ output.Fatal("%q is an invalid add-on type", addOn)
+ }
}
hints := [][]string{}
- // TODO: sqlcmd open ads only support on Windows right now, add Mac support
+ // TODO: sqlcmd open ads only supported on Windows and Mac right now, add Linux support
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
hints = append(hints, []string{localizer.Sprintf("Open in Azure Data Studio"), "sqlcmd open ads"})
}
@@ -390,72 +724,147 @@ func (c *MssqlBase) createContainer(imageName string, contextName string) {
hints = append(hints, []string{localizer.Sprintf("See connection strings"), "sqlcmd config connection-strings"})
hints = append(hints, []string{localizer.Sprintf("Remove"), "sqlcmd delete"})
- output.InfoWithHintExamples(hints,
- localizer.Sprintf("Now ready for client connections on port %d",
- c.port),
- )
-}
+ for _, addOn := range c.addOn {
+ if addOn == "fleet-manager" {
+ hints = append(hints, []string{
+ localizer.Sprintf("Fleet Manager (Renzo) API UI"),
+ fmt.Sprintf("http://localhost:%d/swagger/index.html", fleetManagerPort),
+ })
+ }
-func (c *MssqlBase) validateUsingUrlExists() {
- output := c.Cmd.Output()
- databaseUrl := extractUrl(c.usingDatabaseUrl)
- u, err := url.Parse(databaseUrl)
- c.CheckErr(err)
+ if addOn == "dab" {
+ hints = append(hints, []string{
+ localizer.Sprintf("Data API Builder (DAB) Health Status"),
+ fmt.Sprintf("curl -s http://localhost:%d", dabPort),
+ })
+ }
- if u.Scheme != "http" && u.Scheme != "https" {
- output.FatalWithHints(
- []string{
- localizer.Sprintf("--using URL must be http or https"),
- },
- localizer.Sprintf("%q is not a valid URL for --using flag", c.usingDatabaseUrl))
- }
+ if addOn == "fleet-manager" {
+ output.Info(localizer.Sprintf("Now ready for Fleet Manager connections on port %v",
+ strconv.Itoa(fleetManagerPort)),
+ )
+ }
- if u.Path == "" {
- output.FatalWithHints(
- []string{
- localizer.Sprintf("--using URL must have a path to .bak file"),
- },
- localizer.Sprintf("%q is not a valid URL for --using flag", c.usingDatabaseUrl))
+ if addOn == "dab" {
+ output.Info(localizer.Sprintf("Now ready for DAB connections on port %v",
+ strconv.Itoa(dabPort)),
+ )
+ }
}
- // At the moment we only support attaching .bak files, but we should
- // support .bacpacs and .mdfs in the future
- if _, file := filepath.Split(u.Path); filepath.Ext(file) != ".bak" {
- output.FatalWithHints(
- []string{
- localizer.Sprintf("--using file URL must be a .bak file"),
- },
- localizer.Sprintf("Invalid --using file type"))
+ output.InfofWithHintExamples(hints,
+ localizer.Sprintf("Now ready for SQL connections on port %v",
+ strconv.Itoa(c.port)),
+ )
+
+ if c.openTool == "ads" {
+ ads := open.Ads{}
+ ads.SetCrossCuttingConcerns(dependency.Options{
+ EndOfLine: pal.LineBreak(),
+ Output: c.Output(),
+ })
+
+ user := &sqlconfig.User{
+ AuthenticationType: "basic",
+ BasicAuth: &sqlconfig.BasicAuthDetails{
+ Username: contextOptions.Username,
+ PasswordEncryption: c.passwordEncryption,
+ Password: secret.Encode(contextOptions.Password, c.passwordEncryption)},
+ Name: contextOptions.Username}
+
+ ads.PersistCredentialForAds(endpoint.EndpointDetails.Address, endpoint, user)
+
+ output := c.Output()
+ args := []string{"-r", fmt.Sprintf("--server=%s", fmt.Sprintf(
+ "%s,%d",
+ "127.0.0.1",
+ c.port))}
+
+ args = append(args, fmt.Sprintf("--user=%s",
+ strings.Replace(contextOptions.Username, `"`, `\"`, -1)))
+
+ adsTool := tools.NewTool("ads")
+ if !adsTool.IsInstalled() {
+ output.Fatalf(adsTool.HowToInstall())
+ }
+
+ // BUGBUG: This should come from: displayPreLaunchInfo
+ output.Info(localizer.Sprintf("Press Ctrl+C to exit this process..."))
+
+ _, err := adsTool.Run(args, tool.RunOptions{})
+ c.CheckErr(err)
}
- // Verify the url actually exists, and early exit if it doesn't
- urlExists(databaseUrl, output)
+ if c.openTool == "vscode" {
+ vscode := tools.NewTool("vscode")
+ if !vscode.IsInstalled() {
+ output.Fatalf(vscode.HowToInstall())
+ }
+
+ // BUGBUG: This should come from: displayPreLaunchInfo
+ output.Info(localizer.Sprintf("Launching Visual Studio Code..."))
+
+ _, err := vscode.Run([]string{"."}, tool.RunOptions{})
+ c.CheckErr(err)
+ }
}
-func (c *MssqlBase) query(commandText string) {
- c.sql.Query(commandText)
+func (c *MssqlBase) verifyUseSourceFileExists(
+ controller *container.Controller,
+ output *output.Output) (useUrls []ingest.Ingest) {
+
+ for i, url := range c.useUrl {
+
+ mechanism := ""
+ if len(c.useMechanism) > i {
+ mechanism = c.useMechanism[i]
+ }
+
+ useUrls = append(useUrls, ingest.NewIngest(url, controller, ingest.IngestOptions{
+ Mechanism: mechanism,
+ DatabaseName: c.defaultDatabase,
+ }))
+
+ if !useUrls[i].IsValidFileExtension() {
+ output.FatalWithHints(
+ []string{
+ fmt.Sprintf(
+ localizer.Sprintf("--use must be a path to a file with a %q extension"),
+ ingest.ValidFileExtensions(),
+ ),
+ },
+ localizer.Sprintf("%q is not a valid file extension for --use flag"), useUrls[i].UserProvidedFileExt())
+ }
+
+ if useUrls[i].IsRemoteUrl() && !useUrls[i].IsValidScheme() {
+ output.FatalfWithHints(
+ []string{
+ fmt.Sprintf(
+ localizer.Sprintf("--use URL must one of %q"),
+ strings.Join(useUrls[i].ValidSchemes(), ", "),
+ ),
+ },
+ localizer.Sprintf("%q is not a valid URL for --use flag", useUrls[i].UrlFilename()))
+ }
+
+ if !useUrls[i].SourceFileExists() {
+ output.FatalfWithHints(
+ []string{localizer.Sprintf("File does not exist at URL %q", useUrls[i].UrlFilename())},
+ "Unable to download file")
+ }
+ }
+
+ return useUrls
}
-// createNonSaUser creates a user (non-sa) and assigns the sysadmin role
+// createUser creates a user (non-sa) and assigns the sysadmin role
// to the user. It also creates a default database with the provided name
// and assigns the default database to the user. Finally, it disables
// the sa account and rotates the sa password for security reasons.
-func (c *MssqlBase) createNonSaUser(
+func (c *MssqlBase) createUser(
userName string,
password string,
) {
- output := c.Cmd.Output()
-
- defaultDatabase := "master"
-
- if c.defaultDatabase != "" {
- defaultDatabase = c.defaultDatabase
-
- // Create the default database, if it isn't a downloaded database
- output.Info(localizer.Sprintf("Creating default database [%s]", defaultDatabase))
- c.query(fmt.Sprintf("CREATE DATABASE [%s]", defaultDatabase))
- }
-
const createLogin = `CREATE LOGIN [%s]
WITH PASSWORD=N'%s',
DEFAULT_DATABASE=[%s],
@@ -465,147 +874,44 @@ CHECK_POLICY=OFF`
@loginame = N'%s',
@rolename = N'sysadmin'`
- c.query(fmt.Sprintf(createLogin, userName, password, defaultDatabase))
- c.query(fmt.Sprintf(addSrvRoleMember, userName))
+ output := c.Cmd.Output()
+ defaultDatabase := "master"
+
+ if c.defaultDatabase != "" {
+ defaultDatabase = c.defaultDatabase
+
+ // Create the default database, if it isn't a downloaded database
+ output.Infof(localizer.Sprintf("Creating default database %q", defaultDatabase))
+ c.sql.Query(fmt.Sprintf("CREATE DATABASE [%s]", defaultDatabase))
+ }
+
+ c.sql.Query(fmt.Sprintf(createLogin, userName, password, defaultDatabase))
+ c.sql.Query(fmt.Sprintf(addSrvRoleMember, userName))
// Correct safety protocol is to rotate the sa password, because the first
// sa password has been in the docker environment (as SA_PASSWORD)
- c.query(fmt.Sprintf("ALTER LOGIN [sa] WITH PASSWORD = N'%s';",
+ c.sql.Query(fmt.Sprintf("ALTER LOGIN [sa] WITH PASSWORD = N'%s';",
c.generatePassword()))
- c.query("ALTER LOGIN [sa] DISABLE")
+ c.sql.Query("ALTER LOGIN [sa] DISABLE")
if c.defaultDatabase != "" {
- c.query(fmt.Sprintf("ALTER AUTHORIZATION ON DATABASE::[%s] TO %s",
+ c.sql.Query(fmt.Sprintf("ALTER AUTHORIZATION ON DATABASE::[%s] TO %s",
defaultDatabase, userName))
}
}
-func getDbNameAsIdentifier(dbName string) string {
- escapedDbNAme := strings.ReplaceAll(dbName, "'", "''")
- return strings.ReplaceAll(escapedDbNAme, "]", "]]")
-}
-
-func getDbNameAsNonIdentifier(dbName string) string {
- return strings.ReplaceAll(dbName, "]", "]]")
-}
-
-// parseDbName returns the databaseName from --using arg
-// It sets database name to the specified database name
-// or in absence of it, it is set to the filename without
-// extension.
-func parseDbName(usingDbUrl string) string {
- u, _ := url.Parse(usingDbUrl)
- dbToken := path.Base(u.Path)
- if dbToken != "." && dbToken != "/" {
- lastIdx := strings.LastIndex(dbToken, ".bak")
- if lastIdx != -1 {
- //Get file name without extension
- fileName := dbToken[0:lastIdx]
- lastIdx += 5
- if lastIdx >= len(dbToken) {
- return fileName
- }
- //Return database name if it was specified
- return dbToken[lastIdx:]
- }
- }
- return ""
-}
-
-func extractUrl(usingArg string) string {
- urlEndIdx := strings.LastIndex(usingArg, ".bak")
- if urlEndIdx != -1 {
- return usingArg[0:(urlEndIdx + 4)]
- }
- return usingArg
-}
-
-func (c *MssqlBase) downloadAndRestoreDb(
- controller *container.Controller,
- containerId string,
- userName string,
-) {
- output := c.Cmd.Output()
- databaseName := parseDbName(c.usingDatabaseUrl)
- databaseUrl := extractUrl(c.usingDatabaseUrl)
-
- _, file := filepath.Split(databaseUrl)
-
- // Download file from URL into container
- output.Info(localizer.Sprintf("Downloading %s", file))
-
- temporaryFolder := "/var/opt/mssql/backup"
-
- controller.DownloadFile(
- containerId,
- databaseUrl,
- temporaryFolder,
- )
-
- // Restore database from file
- output.Info(localizer.Sprintf("Restoring database %s", databaseName))
-
- dbNameAsIdentifier := getDbNameAsIdentifier(databaseName)
- dbNameAsNonIdentifier := getDbNameAsNonIdentifier(databaseName)
-
- text := `SET NOCOUNT ON;
-
--- Build a SQL Statement to restore any .bak file to the Linux filesystem
-DECLARE @sql NVARCHAR(max)
-
--- This table definition works since SQL Server 2017, therefore
--- works for all SQL Server containers (which started in 2017)
-DECLARE @fileListTable TABLE (
- [LogicalName] NVARCHAR(128),
- [PhysicalName] NVARCHAR(260),
- [Type] CHAR(1),
- [FileGroupName] NVARCHAR(128),
- [Size] NUMERIC(20,0),
- [MaxSize] NUMERIC(20,0),
- [FileID] BIGINT,
- [CreateLSN] NUMERIC(25,0),
- [DropLSN] NUMERIC(25,0),
- [UniqueID] UNIQUEIDENTIFIER,
- [ReadOnlyLSN] NUMERIC(25,0),
- [ReadWriteLSN] NUMERIC(25,0),
- [BackupSizeInBytes] BIGINT,
- [SourceBlockSize] INT,
- [FileGroupID] INT,
- [LogGroupGUID] UNIQUEIDENTIFIER,
- [DifferentialBaseLSN] NUMERIC(25,0),
- [DifferentialBaseGUID] UNIQUEIDENTIFIER,
- [IsReadOnly] BIT,
- [IsPresent] BIT,
- [TDEThumbprint] VARBINARY(32),
- [SnapshotURL] NVARCHAR(360)
-)
-
-INSERT INTO @fileListTable
-EXEC('RESTORE FILELISTONLY FROM DISK = ''%s/%s''')
-SET @sql = 'RESTORE DATABASE [%s] FROM DISK = ''%s/%s'' WITH '
-SELECT @sql = @sql + char(13) + ' MOVE ''' + LogicalName + ''' TO ''/var/opt/mssql/' + LogicalName + '.' + RIGHT(PhysicalName,CHARINDEX('\',PhysicalName)) + ''','
-FROM @fileListTable
-WHERE IsPresent = 1
-SET @sql = SUBSTRING(@sql, 1, LEN(@sql)-1)
-EXEC(@sql)`
-
- c.query(fmt.Sprintf(text, temporaryFolder, file, dbNameAsIdentifier, temporaryFolder, file))
-
- alterDefaultDb := fmt.Sprintf(
- "ALTER LOGIN [%s] WITH DEFAULT_DATABASE = [%s]",
- userName,
- dbNameAsNonIdentifier)
- c.query(alterDefaultDb)
-}
-
func (c *MssqlBase) downloadImage(
imageName string,
output *output.Output,
controller *container.Controller,
) {
- output.Info(localizer.Sprintf("Downloading %v", imageName))
+ output.Info(localizer.Sprintf("Downloading %q", imageName))
err := controller.EnsureImage(imageName)
if err != nil || c.unitTesting {
+
+ // BUGBUG: Add hint for the new issue on Mac with Docker Desktop
+ // https://stackoverflow.com/questions/44084846/cannot-connect-to-the-docker-daemon-on-macos
+ // (see the "permanent solution" part)
output.FatalErrorWithHints(
err,
[]string{
@@ -621,15 +927,6 @@ func (c *MssqlBase) downloadImage(
}
}
-// Verify the file exists at the URL
-func urlExists(url string, output *output.Output) {
- if !http.UrlExists(url) {
- output.FatalWithHints(
- []string{localizer.Sprintf("File does not exist at URL")},
- localizer.Sprintf("Unable to download file"))
- }
-}
-
func (c *MssqlBase) generatePassword() (password string) {
password = secret.Generate(
c.passwordLength,
diff --git a/cmd/modern/root/install/mssql.go b/cmd/modern/root/install/mssql.go
index 9465652c..8726bf83 100644
--- a/cmd/modern/root/install/mssql.go
+++ b/cmd/modern/root/install/mssql.go
@@ -34,6 +34,15 @@ func (c *Mssql) DefineCommand(...cmdparser.CommandOptions) {
"sqlcmd create mssql --tag 2019-latest",
},
},
+ {
+ Description: localizer.Sprintf("Create SQL Server, download and attach AdventureWorks sample database"),
+ Steps: []string{"sqlcmd create mssql --use https://aka.ms/AdventureWorksLT.bak"}},
+ {
+ Description: localizer.Sprintf("Create SQL Server, download and attach AdventureWorks sample database with different database name"),
+ Steps: []string{"sqlcmd create mssql --use https://aka.ms/AdventureWorksLT.bak,adventureworks"}},
+ {
+ Description: localizer.Sprintf("Create SQL Server with an empty user database"),
+ Steps: []string{"sqlcmd create mssql --database db1"}},
{
Description: localizer.Sprintf("Create SQL Server, download and attach AdventureWorks sample database"),
Steps: []string{"sqlcmd create mssql --using https://aka.ms/AdventureWorksLT.bak"}},
@@ -42,7 +51,7 @@ func (c *Mssql) DefineCommand(...cmdparser.CommandOptions) {
Steps: []string{"sqlcmd create mssql --using https://aka.ms/AdventureWorksLT.bak,adventureworks"}},
{
Description: localizer.Sprintf("Create SQL Server with an empty user database"),
- Steps: []string{"sqlcmd create mssql --user-database db1"}},
+ Steps: []string{"sqlcmd create mssql --database db1"}},
{
Description: localizer.Sprintf("Install/Create SQL Server with full logging"),
Steps: []string{"sqlcmd create mssql --verbosity 4"}},
diff --git a/cmd/modern/root/install/mssql_test.go b/cmd/modern/root/install/mssql_test.go
index 28d80674..91ae7bc4 100644
--- a/cmd/modern/root/install/mssql_test.go
+++ b/cmd/modern/root/install/mssql_test.go
@@ -25,7 +25,7 @@ func TestInstallMssql(t *testing.T) {
cmdparser.TestCmd[*mssql.GetTags]()
cmdparser.TestCmd[*Mssql](
fmt.Sprintf(
- `--accept-eula --user-database foo --errorlog-wait-line "Hello from Docker!" --registry %v --repo %v`,
+ `--accept-eula --database foo --errorlog-wait-line "Hello from Docker!" --registry %v --repo %v`,
registry,
repo))
@@ -54,34 +54,34 @@ func TestNegInstallMssql2(t *testing.T) {
func TestNegInstallMssql3(t *testing.T) {
cmdparser.TestSetup(t)
assert.Panics(t, func() {
- cmdparser.TestCmd[*Mssql]("--accept-eula --using https://does/not/exist.bak")
+ cmdparser.TestCmd[*Mssql]("--accept-eula --use https://does/not/exist.bak")
})
}
func TestNegInstallMssql4(t *testing.T) {
cmdparser.TestSetup(t)
assert.Panics(t, func() {
- cmdparser.TestCmd[*Mssql]("--accept-eula --user-database bad'name")
+ cmdparser.TestCmd[*Mssql]("--accept-eula --database bad'name")
})
}
func TestNegInstallMssql5(t *testing.T) {
cmdparser.TestSetup(t)
assert.Panics(t, func() {
- cmdparser.TestCmd[*Mssql]("--accept-eula --using https://not/bak/file")
+ cmdparser.TestCmd[*Mssql]("--accept-eula --use https://not/bak/file")
})
}
func TestNegInstallMssql6(t *testing.T) {
cmdparser.TestSetup(t)
assert.Panics(t, func() {
- cmdparser.TestCmd[*Mssql]("--accept-eula --using file://not/http")
+ cmdparser.TestCmd[*Mssql]("--accept-eula --use file://not/http")
})
}
func TestNegInstallMssql7(t *testing.T) {
cmdparser.TestSetup(t)
assert.Panics(t, func() {
- cmdparser.TestCmd[*Mssql]("--accept-eula --using https://aka.ms/AdventureWorksLT")
+ cmdparser.TestCmd[*Mssql]("--accept-eula --use https://aka.ms/AdventureWorksLT")
})
}
diff --git a/cmd/modern/root/open.go b/cmd/modern/root/open.go
index d209db81..db875d81 100644
--- a/cmd/modern/root/open.go
+++ b/cmd/modern/root/open.go
@@ -31,5 +31,7 @@ func (c *Open) SubCommands() []cmdparser.Command {
return []cmdparser.Command{
cmdparser.New[*open.Ads](dependencies),
+ cmdparser.New[*open.Ssms](dependencies),
+ cmdparser.New[*open.Vscode](dependencies),
}
}
diff --git a/cmd/modern/root/open/ads.go b/cmd/modern/root/open/ads.go
index 12f7f7b7..296492ba 100644
--- a/cmd/modern/root/open/ads.go
+++ b/cmd/modern/root/open/ads.go
@@ -5,6 +5,7 @@ package open
import (
"fmt"
+ "github.com/microsoft/go-sqlcmd/internal/tools/tool"
"runtime"
"strings"
@@ -47,7 +48,7 @@ func (c *Ads) run() {
// If basic auth is used, we need to persist the password in the OS in a way
// that ADS can access it. The method used is OS specific.
if user != nil && user.AuthenticationType == "basic" {
- c.persistCredentialForAds(endpoint.EndpointDetails.Address, endpoint, user)
+ c.PersistCredentialForAds(endpoint.EndpointDetails.Address, endpoint, user)
c.launchAds(endpoint.EndpointDetails.Address, endpoint.EndpointDetails.Port, user.BasicAuth.Username)
} else {
c.launchAds(endpoint.EndpointDetails.Address, endpoint.EndpointDetails.Port, "")
@@ -76,6 +77,7 @@ func (c *Ads) launchAds(host string, port int, username string) {
port)),
}
+ // If a username is specified, use that (basic auth), otherwise use integrated auth
if username != "" {
// Here's a fun SQL Server behavior - it allows you to create database
@@ -88,13 +90,13 @@ func (c *Ads) launchAds(host string, port int, username string) {
}
}
- tool := tools.NewTool("ads")
- if !tool.IsInstalled() {
- output.Fatalf(tool.HowToInstall())
+ ads := tools.NewTool("ads")
+ if !ads.IsInstalled() {
+ output.Fatalf(ads.HowToInstall())
}
c.displayPreLaunchInfo()
- _, err := tool.Run(args)
+ _, err := ads.Run(args, tool.RunOptions{})
c.CheckErr(err)
}
diff --git a/cmd/modern/root/open/ads_darwin.go b/cmd/modern/root/open/ads_darwin.go
index 750bb20d..124e18fd 100644
--- a/cmd/modern/root/open/ads_darwin.go
+++ b/cmd/modern/root/open/ads_darwin.go
@@ -16,7 +16,7 @@ type Ads struct {
cmdparser.Cmd
}
-func (c *Ads) persistCredentialForAds(hostname string, endpoint sqlconfig.Endpoint, user *sqlconfig.User) {
+func (c *Ads) PersistCredentialForAds(hostname string, endpoint sqlconfig.Endpoint, user *sqlconfig.User) {
// UNDONE: See - https://github.com/microsoft/go-sqlcmd/issues/257
}
diff --git a/cmd/modern/root/open/ads_linux.go b/cmd/modern/root/open/ads_linux.go
index ae1dc827..f9ad0c62 100644
--- a/cmd/modern/root/open/ads_linux.go
+++ b/cmd/modern/root/open/ads_linux.go
@@ -15,7 +15,7 @@ type Ads struct {
cmdparser.Cmd
}
-func (c *Ads) persistCredentialForAds(hostname string, endpoint sqlconfig.Endpoint, user *sqlconfig.User) {
+func (c *Ads) PersistCredentialForAds(hostname string, endpoint sqlconfig.Endpoint, user *sqlconfig.User) {
panic("not implemented")
}
diff --git a/cmd/modern/root/open/ads_windows.go b/cmd/modern/root/open/ads_windows.go
index c98aaad9..d3d1adb9 100644
--- a/cmd/modern/root/open/ads_windows.go
+++ b/cmd/modern/root/open/ads_windows.go
@@ -30,9 +30,9 @@ func (c *Ads) displayPreLaunchInfo() {
output.Info(localizer.Sprintf("Press Ctrl+C to exit this process..."))
}
-// persistCredentialForAds stores a SQL password in the Windows Credential Manager
+// PersistCredentialForAds stores a SQL password in the Windows Credential Manager
// for the given hostname and endpoint.
-func (c *Ads) persistCredentialForAds(
+func (c *Ads) PersistCredentialForAds(
hostname string,
endpoint sqlconfig.Endpoint,
user *sqlconfig.User,
@@ -65,7 +65,6 @@ func (c *Ads) adsKey(instance, database, authType, user string) string {
"Microsoft.SqlTools|"+
"itemtype:Profile|"+
"id:providerName:MSSQL|"+
- "applicationName:azdata|"+
"authenticationType:%s|"+
"database:%s|"+
"server:%s|"+
diff --git a/cmd/modern/root/open/ads_windows_test.go b/cmd/modern/root/open/ads_windows_test.go
index a34b5247..700b72b5 100644
--- a/cmd/modern/root/open/ads_windows_test.go
+++ b/cmd/modern/root/open/ads_windows_test.go
@@ -27,7 +27,7 @@ func TestPersistCredentialForAds(t *testing.T) {
PasswordEncryption: "none",
},
}
- ads.persistCredentialForAds("localhost", sqlconfig.Endpoint{
+ ads.PersistCredentialForAds("localhost", sqlconfig.Endpoint{
EndpointDetails: sqlconfig.EndpointDetails{
Port: 1433,
},
diff --git a/cmd/modern/root/open/ssms.go b/cmd/modern/root/open/ssms.go
new file mode 100644
index 00000000..91038186
--- /dev/null
+++ b/cmd/modern/root/open/ssms.go
@@ -0,0 +1,97 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package open
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/tools/tool"
+ "strings"
+
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+ "github.com/microsoft/go-sqlcmd/internal/config"
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "github.com/microsoft/go-sqlcmd/internal/localizer"
+ "github.com/microsoft/go-sqlcmd/internal/tools"
+)
+
+// Ads implements the `sqlcmd open ads` command. It opens
+// Azure Data Studio and connects to the current context by using the
+// credentials specified in the context.
+func (c *Ssms) DefineCommand(...cmdparser.CommandOptions) {
+ options := cmdparser.CommandOptions{
+ Use: "ssms",
+ Short: "Open Sql Server Management Studio and connect to current context",
+ Examples: []cmdparser.ExampleOptions{{
+ Description: "Open SSMS and connect using the current context",
+ Steps: []string{"sqlcmd open ssms"}}},
+ Run: c.run,
+ }
+
+ c.Cmd.DefineCommand(options)
+}
+
+// Launch ADS and connect to the current context. If the authentication type
+// is basic, we need to securely store the password in an Operating System
+// specific credential store, e.g. on Windows we use the Windows Credential
+// Manager.
+func (c *Ssms) run() {
+ endpoint, user := config.CurrentContext()
+
+ // If the context has a local container, ensure it is running, otherwise bail out
+ if endpoint.AssetDetails != nil && endpoint.AssetDetails.ContainerDetails != nil {
+ c.ensureContainerIsRunning(endpoint)
+ }
+
+ // If basic auth is used, we need to persist the password in the OS in a way
+ // that ADS can access it. The method used is OS specific.
+ if user != nil && user.AuthenticationType == "basic" {
+ c.PersistCredentialForAds(endpoint.EndpointDetails.Address, endpoint, user)
+ c.launchAds(endpoint.EndpointDetails.Address, endpoint.EndpointDetails.Port, user.BasicAuth.Username)
+ } else {
+ c.launchAds(endpoint.EndpointDetails.Address, endpoint.EndpointDetails.Port, "")
+ }
+}
+
+func (c *Ssms) ensureContainerIsRunning(endpoint sqlconfig.Endpoint) {
+ output := c.Output()
+ controller := container.NewController()
+ if !controller.ContainerRunning(endpoint.AssetDetails.ContainerDetails.Id) {
+ output.FatalWithHintExamples([][]string{
+ {localizer.Sprintf("To start the container"), localizer.Sprintf("sqlcmd start")},
+ }, localizer.Sprintf("Container is not running"))
+ }
+}
+
+// launchAds launches the Azure Data Studio using the specified server and username.
+func (c *Ssms) launchAds(host string, port int, username string) {
+ output := c.Output()
+ args := []string{
+ "ssms ",
+ "-S",
+ fmt.Sprintf("%s,%d", host, port),
+ }
+
+ // If a username is specified, use that (basic auth), otherwise use integrated auth
+ if username != "" {
+
+ // Here's a fun SQL Server behavior - it allows you to create database
+ // and login names that include the " character. SSMS escapes those
+ // with \" when invoking ADS on the command line, we do the same here
+ args = append(args, "-U")
+ args = append(args, fmt.Sprintf("%s", strings.Replace(username, `"`, `\"`, -1)))
+ } else {
+ args = append(args, "-E")
+ }
+
+ ssms := tools.NewTool("ssms")
+ if !ssms.IsInstalled() {
+ output.Fatalf(ssms.HowToInstall())
+ }
+
+ c.displayPreLaunchInfo()
+
+ _, err := ssms.Run(args, tool.RunOptions{})
+ c.CheckErr(err)
+}
diff --git a/cmd/modern/root/open/ssms_darwin.go b/cmd/modern/root/open/ssms_darwin.go
new file mode 100644
index 00000000..a5166ab1
--- /dev/null
+++ b/cmd/modern/root/open/ssms_darwin.go
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package open
+
+import (
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+)
+
+// Type Ads is used to implement the "open ads" which launches Azure
+// Data Studio and establishes a connection to the SQL Server for the current
+// context
+type Ssms struct {
+ cmdparser.Cmd
+}
+
+func (c *Ssms) PersistCredentialForAds(hostname string, endpoint sqlconfig.Endpoint, user *sqlconfig.User) {
+}
+
+func (c *Ssms) displayPreLaunchInfo() {
+
+}
diff --git a/cmd/modern/root/open/ssms_linux.go b/cmd/modern/root/open/ssms_linux.go
new file mode 100644
index 00000000..a5166ab1
--- /dev/null
+++ b/cmd/modern/root/open/ssms_linux.go
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package open
+
+import (
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+)
+
+// Type Ads is used to implement the "open ads" which launches Azure
+// Data Studio and establishes a connection to the SQL Server for the current
+// context
+type Ssms struct {
+ cmdparser.Cmd
+}
+
+func (c *Ssms) PersistCredentialForAds(hostname string, endpoint sqlconfig.Endpoint, user *sqlconfig.User) {
+}
+
+func (c *Ssms) displayPreLaunchInfo() {
+
+}
diff --git a/cmd/modern/root/open/ssms_windows.go b/cmd/modern/root/open/ssms_windows.go
new file mode 100644
index 00000000..159bf696
--- /dev/null
+++ b/cmd/modern/root/open/ssms_windows.go
@@ -0,0 +1,103 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package open
+
+import (
+ "fmt"
+
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+ "github.com/microsoft/go-sqlcmd/internal/credman"
+ "github.com/microsoft/go-sqlcmd/internal/localizer"
+ "github.com/microsoft/go-sqlcmd/internal/secret"
+)
+
+// Type Ads is used to implement the "open ads" which launches Azure
+// Data Studio and establishes a connection to the SQL Server for the current
+// context
+type Ssms struct {
+ cmdparser.Cmd
+
+ credential credman.Credential
+}
+
+// On Windows, the process blocks until the user exits ADS, let user know they can
+// Ctrl+C here.
+func (c *Ssms) displayPreLaunchInfo() {
+ output := c.Output()
+
+ output.Info(localizer.Sprintf("Press Ctrl+C to exit this process..."))
+}
+
+// PersistCredentialForAds stores a SQL password in the Windows Credential Manager
+// for the given hostname and endpoint.
+func (c *Ssms) PersistCredentialForAds(
+ hostname string,
+ endpoint sqlconfig.Endpoint,
+ user *sqlconfig.User,
+) {
+ // Create the target name that ADS will look for
+ targetName := c.adsKey(
+ fmt.Sprintf("%s,%d", hostname, rune(endpoint.Port)),
+ user.BasicAuth.Username)
+
+ // Store the SQL password in the Windows Credential Manager with the
+ // generated target name
+ c.credential = credman.Credential{
+ TargetName: targetName,
+ CredentialBlob: secret.DecodeAsUtf16(
+ user.BasicAuth.Password, user.BasicAuth.PasswordEncryption),
+ UserName: user.BasicAuth.Username,
+ Persist: credman.PersistSession,
+ }
+
+ c.removePreviousCredential()
+ c.writeCredential()
+}
+
+// adsKey returns the credential target name for the given instance, database,
+// authentication type, and user.
+func (c *Ssms) adsKey(instance, user string) string {
+
+ // Microsoft:SSMS:19:127.0.0.1,1435:stuartpa:8c91a03d-f9b4-46c0-a305-b5dcc79ff907:1
+
+ // BUGBUG: Can't hardcode 19
+ // BUGBUG: What is that GUID? Is it different on other peoples machines?
+ return fmt.Sprintf(
+ "Microsoft:"+
+ "SSMS:"+
+ "19:"+
+ "%s:"+
+ "%s:"+
+ "8c91a03d-f9b4-46c0-a305-b5dcc79ff907:1",
+ instance, user)
+}
+
+// removePreviousCredential removes any previously stored credentials with
+// the same target name as the current instance's credential.
+func (c *Ssms) removePreviousCredential() {
+ credentials, err := credman.EnumerateCredentials("", true)
+ c.CheckErr(err)
+
+ for _, cred := range credentials {
+ if cred.TargetName == c.credential.TargetName {
+ err = credman.DeleteCredential(cred, credman.CredTypeGeneric)
+ c.CheckErr(err)
+ break
+ }
+ }
+}
+
+// writeCredential stores the current instance's credential in the Windows Credential Manager
+func (c *Ssms) writeCredential() {
+ output := c.Output()
+
+ err := credman.WriteCredential(&c.credential, credman.CredTypeGeneric)
+ if err != nil {
+ output.FatalErrorWithHints(
+ err,
+ []string{localizer.Sprintf("A 'Not enough memory resources are available' error can be caused by too many credentials already stored in Windows Credential Manager")},
+ localizer.Sprintf("Failed to write credential to Windows Credential Manager"))
+ }
+}
diff --git a/cmd/modern/root/open/vscode.go b/cmd/modern/root/open/vscode.go
new file mode 100644
index 00000000..f6637e44
--- /dev/null
+++ b/cmd/modern/root/open/vscode.go
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package open
+
+import (
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+ "github.com/microsoft/go-sqlcmd/internal/config"
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "github.com/microsoft/go-sqlcmd/internal/localizer"
+ "github.com/microsoft/go-sqlcmd/internal/tools"
+ "github.com/microsoft/go-sqlcmd/internal/tools/tool"
+)
+
+// Vscode implements the `sqlcmd open vscode` (or just `sqlcmd open code`)
+// command. It opens VS Code and connects to the current context by using the
+// credentials specified in the context.
+func (c *Vscode) DefineCommand(...cmdparser.CommandOptions) {
+ options := cmdparser.CommandOptions{
+ Use: "vscode",
+ Aliases: []string{"code"},
+ Short: "Open Visual Studio Code and connect to current context",
+ Examples: []cmdparser.ExampleOptions{{
+ Description: "Open VS Code and connect using the current context",
+ Steps: []string{"sqlcmd open vscode"}}},
+ Run: c.run,
+ }
+
+ c.Cmd.DefineCommand(options)
+}
+
+// Launch ADS and connect to the current context. If the authentication type
+// is basic, we need to securely store the password in an Operating System
+// specific credential store, e.g. on Windows we use the Windows Credential
+// Manager.
+func (c *Vscode) run() {
+ endpoint, _ := config.CurrentContext()
+
+ // If the context has a local container, ensure it is running, otherwise bail out
+ if endpoint.AssetDetails != nil && endpoint.AssetDetails.ContainerDetails != nil {
+ c.ensureContainerIsRunning(endpoint)
+ }
+
+ c.launchAds(endpoint.EndpointDetails.Address, endpoint.EndpointDetails.Port, "")
+}
+
+func (c *Vscode) ensureContainerIsRunning(endpoint sqlconfig.Endpoint) {
+ output := c.Output()
+ controller := container.NewController()
+ if !controller.ContainerRunning(endpoint.AssetDetails.ContainerDetails.Id) {
+ output.FatalWithHintExamples([][]string{
+ {localizer.Sprintf("To start the container"), localizer.Sprintf("sqlcmd start")},
+ }, localizer.Sprintf("Container is not running"))
+ }
+}
+
+// launchAds launches VS Code
+func (c *Vscode) launchAds(host string, port int, username string) {
+ output := c.Output()
+ args := []string{}
+
+ vscode := tools.NewTool("vscode")
+ if !vscode.IsInstalled() {
+ output.Fatalf(vscode.HowToInstall())
+ }
+
+ c.displayPreLaunchInfo()
+
+ _, err := vscode.Run(args, tool.RunOptions{})
+ c.CheckErr(err)
+}
diff --git a/cmd/modern/root/open/vscode_darwin.go b/cmd/modern/root/open/vscode_darwin.go
new file mode 100644
index 00000000..9ae42fdc
--- /dev/null
+++ b/cmd/modern/root/open/vscode_darwin.go
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package open
+
+import (
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+)
+
+// Type Ads is used to implement the "open ads" which launches Azure
+// Data Studio and establishes a connection to the SQL Server for the current
+// context
+type Vscode struct {
+ cmdparser.Cmd
+}
+
+func (c *Vscode) PersistCredentialForAds(hostname string, endpoint sqlconfig.Endpoint, user *sqlconfig.User) {
+}
+
+func (c *Vscode) displayPreLaunchInfo() {
+}
diff --git a/cmd/modern/root/open/vscode_linux.go b/cmd/modern/root/open/vscode_linux.go
new file mode 100644
index 00000000..9ae42fdc
--- /dev/null
+++ b/cmd/modern/root/open/vscode_linux.go
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package open
+
+import (
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+)
+
+// Type Ads is used to implement the "open ads" which launches Azure
+// Data Studio and establishes a connection to the SQL Server for the current
+// context
+type Vscode struct {
+ cmdparser.Cmd
+}
+
+func (c *Vscode) PersistCredentialForAds(hostname string, endpoint sqlconfig.Endpoint, user *sqlconfig.User) {
+}
+
+func (c *Vscode) displayPreLaunchInfo() {
+}
diff --git a/cmd/modern/root/open/vscode_windows.go b/cmd/modern/root/open/vscode_windows.go
new file mode 100644
index 00000000..d85e2ee6
--- /dev/null
+++ b/cmd/modern/root/open/vscode_windows.go
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package open
+
+import (
+ "github.com/microsoft/go-sqlcmd/cmd/modern/sqlconfig"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+ "github.com/microsoft/go-sqlcmd/internal/credman"
+ "github.com/microsoft/go-sqlcmd/internal/localizer"
+)
+
+// Type Vscode is used to implement the "open vscode" which launches VS Code
+type Vscode struct {
+ cmdparser.Cmd
+
+ credential credman.Credential
+}
+
+// On Windows, the process blocks until the user exits ADS, let user know they can
+// Ctrl+C here.
+func (c *Vscode) displayPreLaunchInfo() {
+ output := c.Output()
+
+ output.Info(localizer.Sprintf("Press Ctrl+C to exit this process..."))
+}
+
+// PersistCredentialForAds stores a SQL password in the Windows Credential Manager
+// for the given hostname and endpoint.
+func (c *Vscode) PersistCredentialForAds(
+ hostname string,
+ endpoint sqlconfig.Endpoint,
+ user *sqlconfig.User,
+) {
+}
diff --git a/cmd/modern/root/query.go b/cmd/modern/root/query.go
index b61958ea..8c5372c8 100644
--- a/cmd/modern/root/query.go
+++ b/cmd/modern/root/query.go
@@ -75,7 +75,7 @@ func (c *Query) DefineCommand(...cmdparser.CommandOptions) {
func (c *Query) run() {
endpoint, user := config.CurrentContext()
- s := sql.New(sql.SqlOptions{})
+ s := sql.NewSql(sql.SqlOptions{})
if c.text == "" {
s.Connect(endpoint, user, sql.ConnectOptions{Database: c.database, Interactive: true})
} else {
diff --git a/cmd/modern/root/uninstall.go b/cmd/modern/root/uninstall.go
index e44720a3..42b3922b 100644
--- a/cmd/modern/root/uninstall.go
+++ b/cmd/modern/root/uninstall.go
@@ -113,16 +113,40 @@ func (c *Uninstall) run() {
c.userDatabaseSafetyCheck(controller, id)
}
- output.Info(localizer.Sprintf("Removing context %s", config.CurrentContextName()))
+ output.Info(localizer.Sprintf("Removing context %q", config.CurrentContextName()))
if controller.ContainerExists(id) {
- output.Info(localizer.Sprintf("Stopping %s", endpoint.ContainerDetails.Image))
+ output.Info(localizer.Sprintf("Stopping %q", endpoint.ContainerDetails.Image))
err := controller.ContainerStop(id)
c.CheckErr(err)
err = controller.ContainerRemove(id)
c.CheckErr(err)
} else {
- output.Warn(localizer.Sprintf("Container %q no longer exists, continuing to remove context...", id))
+ output.Warn(localizer.Sprintf("Container %q (for endpoint %q) no longer exists, continuing to remove context...", id, endpoint.Name))
+ }
+
+ addOns := config.CurrentContextAddOns()
+ for _, a := range addOns {
+ e := config.GetEndpoint(a.Endpoint)
+ if controller.ContainerExists(e.ContainerDetails.Id) {
+ output.Info(localizer.Sprintf("Stopping %q", e.ContainerDetails.Image))
+ err := controller.ContainerStop(e.ContainerDetails.Id)
+ c.CheckErr(err)
+ err = controller.ContainerRemove(e.ContainerDetails.Id)
+ c.CheckErr(err)
+ } else {
+ output.Warn(localizer.Sprintf("Container %q (for endpoint %q) no longer exists, continuing to remove context...", e.ContainerDetails.Id, a.Endpoint))
+ }
+
+ config.DeleteEndpoint(a.Endpoint)
+ }
+
+ network := config.CurrentContextNetwork()
+ if network != nil {
+ if controller.NetworkExists(*network) {
+ output.Info(localizer.Sprintf("Removing container network %q", *network))
+ controller.NetworkDelete(*network)
+ }
}
}
diff --git a/cmd/modern/root/use.go b/cmd/modern/root/use.go
new file mode 100644
index 00000000..eadebb83
--- /dev/null
+++ b/cmd/modern/root/use.go
@@ -0,0 +1,106 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package root
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/cmdparser"
+ "github.com/microsoft/go-sqlcmd/internal/config"
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "github.com/microsoft/go-sqlcmd/internal/secret"
+ "github.com/microsoft/go-sqlcmd/internal/sql"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest"
+)
+
+type Use struct {
+ cmdparser.Cmd
+
+ url string
+ useMechanism string
+
+ sql sql.Sql
+}
+
+func (c *Use) DefineCommand(...cmdparser.CommandOptions) {
+ examples := []cmdparser.ExampleOptions{
+ {
+ Description: "Download AdventureWorksLT into container for current context, set as default database",
+ Steps: []string{`sqlcmd use https://aka.ms/AdventureWorksLT.bak`}},
+ }
+
+ options := cmdparser.CommandOptions{
+ Use: "use",
+ Short: fmt.Sprintf("Download database (into container) (%s)", ingest.ValidFileExtensions()),
+ Examples: examples,
+ Run: c.run,
+ FirstArgAlternativeForFlag: &cmdparser.AlternativeForFlagOptions{Flag: "url", Value: &c.url},
+ }
+
+ c.Cmd.DefineCommand(options)
+
+ c.AddFlag(cmdparser.FlagOptions{
+ String: &c.url,
+ Name: "url",
+ Usage: "Name of context to set as current context"})
+
+ c.AddFlag(cmdparser.FlagOptions{
+ String: &c.useMechanism,
+ DefaultString: "",
+ Name: "use-mechanism",
+ Usage: "Mechanism to use to bring database online (attach, restore, dacfx)",
+ })
+}
+
+func (c *Use) run() {
+ output := useOutput{output: c.Output()}
+
+ controller := container.NewController()
+ id := config.ContainerId()
+
+ if !config.CurrentContextEndpointHasContainer() {
+ output.FatalNoContainerInCurrentContext()
+ }
+
+ if !controller.ContainerRunning(id) {
+ output.FatalContainerNotRunning()
+ }
+
+ endpoint, user := config.CurrentContext()
+
+ c.sql = sql.NewSql(sql.SqlOptions{})
+ c.sql.Connect(endpoint, user, sql.ConnectOptions{Database: "master"})
+
+ useDatabase := ingest.NewIngest(c.url, controller, ingest.IngestOptions{
+ Mechanism: c.useMechanism,
+ })
+
+ if !useDatabase.SourceFileExists() {
+ output.FatalDatabaseSourceFileNotExist(c.url)
+ }
+
+ // Copy source file (e.g. .bak/.bacpac etc.) for database to be made available to container
+ useDatabase.CopyToContainer(id)
+
+ if useDatabase.IsExtractionNeeded() {
+ output.Infof("Extracting files from %q", useDatabase.UrlFilename())
+ useDatabase.Extract()
+ }
+
+ output.output.Infof("Bringing %q online using %q method",
+ useDatabase.DatabaseName(),
+ useDatabase.OnlineMethod(),
+ )
+
+ useDatabase.BringOnline(
+ c.sql.Query,
+ user.BasicAuth.Username,
+ secret.Decode(user.BasicAuth.Password, user.BasicAuth.PasswordEncryption),
+ )
+
+ output.InfoDatabaseOnline(useDatabase.DatabaseName())
+}
+
+func (c *Use) query(commandText string) {
+ c.sql.Query(commandText)
+}
diff --git a/cmd/modern/root/use_output.go b/cmd/modern/root/use_output.go
new file mode 100644
index 00000000..b29eb0bc
--- /dev/null
+++ b/cmd/modern/root/use_output.go
@@ -0,0 +1,44 @@
+package root
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/output"
+ "runtime"
+)
+
+type useOutput struct {
+ output.Output
+ output *output.Output
+}
+
+func (u *useOutput) FatalNoContainerInCurrentContext() {
+ u.output.FatalfWithHintExamples([][]string{
+ {"Create a context with a container", "sqlcmd create mssql"},
+ }, "Current context does not have a container")
+}
+
+func (u *useOutput) FatalContainerNotRunning() {
+ u.output.FatalfWithHintExamples([][]string{
+ {"Start container for current context", "sqlcmd start"},
+ }, "Container for current context is not running")
+}
+
+func (u *useOutput) FatalDatabaseSourceFileNotExist(url string) {
+ u.output.FatalfWithHints(
+ []string{fmt.Sprintf("File does not exist at URL %q", url)},
+ "Unable to download file to container")
+}
+
+func (u *useOutput) InfoDatabaseOnline(databaseName string) {
+ hints := [][]string{}
+
+ // TODO: sqlcmd open ads only support on Windows/Mac right now, add Linux support
+ if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
+ hints = append(hints, []string{"Open in Azure Data Studio", "sqlcmd open ads"})
+ }
+
+ hints = append(hints, []string{"Run a query", "sqlcmd query \"SELECT DB_NAME()\""})
+ hints = append(hints, []string{"See connection strings", "sqlcmd config connection-strings"})
+
+ u.output.InfofWithHintExamples(hints, "Database %q is now online", databaseName)
+}
diff --git a/cmd/modern/sqlcmdconfig/sqlcmdconfig.go b/cmd/modern/sqlcmdconfig/sqlcmdconfig.go
new file mode 100644
index 00000000..5c59cd60
--- /dev/null
+++ b/cmd/modern/sqlcmdconfig/sqlcmdconfig.go
@@ -0,0 +1,30 @@
+package sqlcmdconfig
+
+type Sqlcmdconfig struct {
+ Version string `mapstructure:"version"`
+ Databases []Database `mapstructure:"databases"`
+ AddOns []AddOn `mapstructure:"addons"`
+}
+
+type Database struct {
+ DatabaseDetails `mapstructure:"database" yaml:"database"`
+}
+
+type DatabaseDetails struct {
+ Name string `mapstructure:"name" yaml:"name"`
+ Use []Use `mapstructure:"use"`
+}
+
+type Use struct {
+ Uri string `mapstructure:"uri"`
+ Mechanism string `mapstructure:"mechanism"`
+}
+
+type AddOn struct {
+ AddOnDetails `mapstructure:"addon" yaml:"addon"`
+}
+
+type AddOnDetails struct {
+ Type string `mapstructure:"type"`
+ Use []Use `mapstructure:"use"`
+}
diff --git a/cmd/modern/sqlconfig/sqlconfig.go b/cmd/modern/sqlconfig/sqlconfig.go
index 834947b9..5edd6d55 100644
--- a/cmd/modern/sqlconfig/sqlconfig.go
+++ b/cmd/modern/sqlconfig/sqlconfig.go
@@ -23,6 +23,15 @@ type AssetDetails struct {
*ContainerDetails `mapstructure:"container,omitempty" yaml:"container,omitempty"`
}
+type AddOn struct {
+ AddOnsDetails `mapstructure:"addon" yaml:"addon,omitempty"`
+}
+
+type AddOnsDetails struct {
+ Type string `mapstructure:"type"`
+ Endpoint string `mapstructure:"endpoint"`
+}
+
type Endpoint struct {
*AssetDetails `mapstructure:"asset,omitempty" yaml:"asset,omitempty"`
EndpointDetails `mapstructure:"endpoint" yaml:"endpoint"`
@@ -32,6 +41,8 @@ type Endpoint struct {
type ContextDetails struct {
Endpoint string `mapstructure:"endpoint"`
User *string `mapstructure:"user,omitempty" yaml:"user,omitempty"`
+ Network *string `mapstructure:"network,omitempty" yaml:"network,omitempty"`
+ AddOns []AddOn `mapstructure:"addons,omitempty" yaml:"addons,omitempty"`
}
type Context struct {
diff --git a/go.mod b/go.mod
index 48c347e6..91d4529d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,78 +1,106 @@
module github.com/microsoft/go-sqlcmd
-go 1.18
+go 1.20
+
+// replace github.com/microsoft/go-mssqldb => C:\src\go-mssqldb
+// replace github.com/microsoft/go-mssqldb => /Users/stuartpa/src/go-mssqldb
require (
- github.com/alecthomas/chroma/v2 v2.5.0
- github.com/billgraziano/dpapi v0.4.0
- github.com/docker/distribution v2.8.2+incompatible
+ github.com/alecthomas/chroma/v2 v2.12.0
+ github.com/billgraziano/dpapi v0.5.0
+ github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v24.0.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/golang-sql/sqlexp v0.1.0
- github.com/google/uuid v1.3.0
+ github.com/google/uuid v1.6.0
+ github.com/joho/godotenv v1.5.1
github.com/microsoft/go-mssqldb v1.6.0
github.com/opencontainers/image-spec v1.0.2
github.com/peterh/liner v1.2.2
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
- github.com/spf13/viper v1.14.0
+ github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
- golang.org/x/sys v0.15.0
+ golang.org/x/sys v0.17.0
golang.org/x/text v0.14.0
- golang.org/x/tools v0.6.0
+ golang.org/x/tools v0.18.0
gopkg.in/yaml.v2 v2.4.0
)
require (
- github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 // indirect
- github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 // indirect
- github.com/Microsoft/go-winio v0.6.0 // indirect
+ github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40
+ gopkg.in/src-d/go-billy.v4 v4.3.2
+ gopkg.in/src-d/go-git.v4 v4.13.1
+)
+
+require (
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect
+ github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/cespare/xxhash/v2 v2.1.1 // indirect
- github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/dlclark/regexp2 v1.4.0 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/distribution/reference v0.5.0 // indirect
+ github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
- github.com/fsnotify/fsnotify v1.6.0 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
- github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
+ github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
- github.com/golang/protobuf v1.5.2 // indirect
- github.com/gorilla/mux v1.8.0 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
- github.com/inconshreveable/mousetrap v1.0.1 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/jpillora/backoff v1.0.0 // indirect
+ github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
- github.com/magiconair/properties v1.8.6 // indirect
- github.com/mattn/go-runewidth v0.0.3 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
- github.com/moby/term v0.0.0-20221120202655-abb19827d345 // indirect
+ github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
- github.com/pelletier/go-toml/v2 v2.0.5 // indirect
- github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/client_golang v1.11.1 // indirect
- github.com/prometheus/client_model v0.2.0 // indirect
- github.com/prometheus/common v0.26.0 // indirect
- github.com/prometheus/procfs v0.6.0 // indirect
- github.com/sirupsen/logrus v1.9.0 // indirect
- github.com/spf13/afero v1.9.2 // indirect
- github.com/spf13/cast v1.5.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.1.1 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+ github.com/prometheus/client_golang v1.18.0 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/common v0.46.0 // indirect
+ github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sergi/go-diff v1.3.1 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
- github.com/subosito/gotenv v1.4.1 // indirect
- golang.org/x/crypto v0.17.0 // indirect
- golang.org/x/mod v0.8.0 // indirect
- golang.org/x/net v0.17.0 // indirect
- google.golang.org/protobuf v1.28.1 // indirect
+ github.com/src-d/gcfg v1.4.0 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/crypto v0.19.0 // indirect
+ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+ golang.org/x/mod v0.15.0 // indirect
+ golang.org/x/net v0.21.0 // indirect
+ google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
- gotest.tools/v3 v3.4.0 // indirect
+ gotest.tools/v3 v3.5.1 // indirect
)
diff --git a/go.sum b/go.sum
index f272a5c4..eb7ac693 100644
--- a/go.sum
+++ b/go.sum
@@ -36,41 +36,67 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg=
-github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 h1:c4k2FIYIh4xtwqrQwV0Ct1v5+ehlNXj5NI/MWVsiTkQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2/go.mod h1:5FDJtLEO/GxwNgUxbwrY3LP0pEoThTQJtk2oysdXHxM=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0 h1:DRiANoJTiW6obBQe3SqZizkuV1PEgfiiGivmVocDy64=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.1.0/go.mod h1:qLIye2hwb/ZouqhpSD9Zn3SJipvpEnz1Ywl3VUk9Y0s=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
+github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg=
github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE=
+github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
+github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/chroma/v2 v2.5.0 h1:CQCdj1BiBV17sD4Bd32b/Bzuiq/EqoNTrnIhyQAZ+Rk=
github.com/alecthomas/chroma/v2 v2.5.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw=
+github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
+github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/billgraziano/dpapi v0.4.0 h1:t39THI1Ld1hkkLVrhkOX6u5TUxwzRddOffq4jcwh2AE=
github.com/billgraziano/dpapi v0.4.0/go.mod h1:gi1Lin0jvovT53j0EXITkY6UPb3hTfI92POaZgj9JBA=
+github.com/billgraziano/dpapi v0.5.0 h1:pcxA17vyjbDqYuxCFZbgL9tYIk2xgbRZjRaIbATwh+8=
+github.com/billgraziano/dpapi v0.5.0/go.mod h1:lmEcZjRfLCSbUTsRu8V2ti6Q17MvnKn3N9gQqzDdTh0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -79,14 +105,23 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
+github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
+github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
+github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
+github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
+github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
@@ -98,15 +133,26 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -120,9 +166,8 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
-github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
-github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
+github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -155,6 +200,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -184,13 +230,17 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
+github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -200,6 +250,16 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -209,6 +269,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -217,23 +279,33 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc=
github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/term v0.0.0-20221120202655-abb19827d345 h1:J9c53/kxIH+2nTKBEfZYFMlhghtHpIHSXpm5VRGHSnU=
github.com/moby/term v0.0.0-20221120202655-abb19827d345/go.mod h1:15ce4BGCFxt7I5NQKT+HV0yEDxmf6fSysfEDiVo3zFM=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@@ -246,14 +318,19 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
+github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
+github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
+github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
+github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
+github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
-github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
-github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -261,40 +338,75 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
+github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
+github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40 h1:31Y7UZ1yTYBU4E79CE52I/1IRi3TqiuwquXGNtZDXWs=
+github.com/rdegges/go-ipify v0.0.0-20150526035502-2d94a6a86c40/go.mod h1:j4c6zEU0eMG1oiZPUy+zD4ykX0NIpjZAEOEAviTWC18=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
+github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
+github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
@@ -303,9 +415,15 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
+github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
+github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -317,6 +435,12 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
+github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -327,16 +451,31 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -347,6 +486,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -372,6 +513,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -405,8 +548,13 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
+golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -427,10 +575,12 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -441,6 +591,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -468,28 +619,40 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -502,6 +665,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -541,6 +705,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
+golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -637,14 +803,27 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
+gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg=
+gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
+gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
+gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -657,6 +836,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/cmdparser/cmd.go b/internal/cmdparser/cmd.go
index f6dcd50b..1c6f64f4 100644
--- a/internal/cmdparser/cmd.go
+++ b/internal/cmdparser/cmd.go
@@ -85,6 +85,30 @@ func (c *Cmd) AddFlag(options FlagOptions) {
options.Usage)
}
}
+
+ if options.StringArray != nil {
+ if options.Bool != nil || options.Int != nil || options.String != nil {
+ panic("Only provide one type")
+ }
+ if options.Shorthand == "" {
+ c.command.PersistentFlags().StringArrayVar(
+ options.StringArray,
+ options.Name,
+ []string{},
+ options.Usage)
+ } else {
+ c.command.PersistentFlags().StringArrayVarP(
+ options.StringArray,
+ options.Name,
+ options.Shorthand,
+ []string{},
+ options.Usage)
+ }
+ }
+
+ if options.Hidden {
+ c.command.PersistentFlags().MarkHidden(options.Name)
+ }
}
// DefineCommand defines a command with the provided CommandOptions and adds
diff --git a/internal/cmdparser/options.go b/internal/cmdparser/options.go
index 499c8622..9231b6c7 100644
--- a/internal/cmdparser/options.go
+++ b/internal/cmdparser/options.go
@@ -28,6 +28,8 @@ type FlagOptions struct {
Shorthand string
Usage string
+ Hidden bool
+
String *string
DefaultString string
@@ -36,6 +38,8 @@ type FlagOptions struct {
Bool *bool
DefaultBool bool
+
+ StringArray *[]string
}
// CommandOptions is a struct that allows the caller to specify options for a Command.
diff --git a/internal/config/config.go b/internal/config/config.go
index 1b24695c..e584780a 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -92,71 +92,74 @@ func IsEmpty() (isEmpty bool) {
// requested. The updated configuration is saved to file.
func AddContextWithContainer(
contextName string,
- imageName string,
- portNumber int,
- containerId string,
- username string,
- password string,
- passwordEncryption string,
+ options ContextOptions,
) {
- if containerId == "" {
+ if options.ContainerId == "" {
panic("containerId must be provided")
}
- if imageName == "" {
+ if options.ImageName == "" {
panic("imageName must be provided")
}
- if portNumber == 0 {
+ if options.PortNumber == 0 {
panic("portNumber must be non-zero")
}
- if username == "" {
+ if options.Username == "" {
panic("username must be provided")
}
- if password == "" {
+ if options.Password == "" {
panic("password must be provided")
}
if contextName == "" {
panic("contextName must be provided")
}
- contextName = FindUniqueContextName(contextName, username)
+ contextName = FindUniqueContextName(contextName, options.Username)
endPointName := FindUniqueEndpointName(contextName)
- userName := username + "@" + contextName
+ userName := options.Username + "@" + contextName
config.CurrentContext = contextName
config.Endpoints = append(config.Endpoints, Endpoint{
AssetDetails: &AssetDetails{
ContainerDetails: &ContainerDetails{
- Id: containerId,
- Image: imageName},
+ Id: options.ContainerId,
+ Image: options.ImageName},
},
EndpointDetails: EndpointDetails{
Address: "127.0.0.1",
- Port: portNumber,
+ Port: options.PortNumber,
},
Name: endPointName,
})
+ contextDetails := ContextDetails{
+ Endpoint: endPointName,
+ }
+
+ if userName != "" {
+ contextDetails.User = &userName
+ }
+
+ if options.Network != "" {
+ contextDetails.Network = &options.Network
+ }
+
config.Contexts = append(config.Contexts, Context{
- ContextDetails: ContextDetails{
- Endpoint: endPointName,
- User: &userName,
- },
- Name: contextName,
+ ContextDetails: contextDetails,
+ Name: contextName,
})
user := User{
AuthenticationType: "basic",
BasicAuth: &BasicAuthDetails{
- Username: username,
- PasswordEncryption: passwordEncryption,
- Password: encryptCallback(password, passwordEncryption),
+ Username: options.Username,
+ PasswordEncryption: options.PasswordEncryption,
+ Password: encryptCallback(options.Password, options.PasswordEncryption),
},
Name: userName,
}
config.Users = append(config.Users, user)
-
Save()
}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 7e2540c2..9852954b 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -83,7 +83,7 @@ func TestConfig(t *testing.T) {
GetEndpoint("endpoint")
OutputEndpoints(o.Struct, true)
OutputEndpoints(o.Struct, false)
- FindFreePortForTds()
+ FindFreePort(0)
DeleteEndpoint("endpoint2")
DeleteEndpoint("endpoint3")
@@ -131,7 +131,20 @@ func TestConfig(t *testing.T) {
ContainerId()
RemoveCurrentContext()
RemoveCurrentContext()
- AddContextWithContainer("context", "imageName", 1433, "containerId", "user", "password", "none")
+<<<<<<< HEAD
+
+ options := ContextOptions{
+ ImageName: "imageName",
+ PortNumber: 1433,
+ ContainerId: "containerId",
+ Username: "user",
+ Password: "password",
+ PasswordEncryption: "none",
+ }
+ AddContextWithContainer("context", options)
+=======
+ AddContextWithContainer("imageName", "context", 1433, "containerId", "user", "password", "none", "")
+>>>>>>> stuartpa/add-ons
RemoveCurrentContext()
DeleteEndpoint("endpoint")
DeleteContext("context")
@@ -324,7 +337,19 @@ func TestAddContextWithContainerPanic(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Panics(t, func() {
- AddContextWithContainer(tt.args.contextName, tt.args.imageName, tt.args.portNumber, tt.args.containerId, tt.args.username, tt.args.password, tt.args.passwordEncryption)
+<<<<<<< HEAD
+ options := ContextOptions{
+ ImageName: tt.args.imageName,
+ PortNumber: tt.args.portNumber,
+ ContainerId: tt.args.containerId,
+ Username: tt.args.username,
+ Password: tt.args.password,
+ PasswordEncryption: tt.args.passwordEncryption,
+ }
+ AddContextWithContainer(tt.args.contextName, options)
+=======
+ AddContextWithContainer(tt.args.imageName, tt.args.contextName, tt.args.portNumber, tt.args.containerId, tt.args.username, tt.args.password, tt.args.passwordEncryption, "")
+>>>>>>> stuartpa/add-ons
})
})
}
diff --git a/internal/config/context.go b/internal/config/context.go
index 64682e5a..84a9c82f 100644
--- a/internal/config/context.go
+++ b/internal/config/context.go
@@ -39,6 +39,44 @@ func AddContext(context Context) string {
return context.Name
}
+func AddAddOn(
+ contextName, addOnName string,
+ ContainerId string,
+ Image string,
+ address string,
+ port int,
+) {
+
+ containerDetails := ContainerDetails{
+ Id: ContainerId,
+ Image: Image}
+
+ assetDetails := AssetDetails{
+ ContainerDetails: &containerDetails}
+
+ endpoint := Endpoint{
+ AssetDetails: &assetDetails,
+ EndpointDetails: EndpointDetails{
+ Address: address,
+ Port: port},
+ Name: addOnName + "@" + contextName,
+ }
+
+ uniqueEndpointName := AddEndpoint(endpoint)
+
+ for i, c := range config.Contexts {
+ if contextName == c.Name {
+ config.Contexts[i].AddOns = append(c.AddOns, AddOn{
+ AddOnsDetails: AddOnsDetails{
+ Type: addOnName,
+ Endpoint: uniqueEndpointName}})
+ break
+ }
+ }
+
+ Save()
+}
+
// CurrentContextName returns the name of the current context in the configuration.
// The current context is the one that is currently active and used by the application.
func CurrentContextName() string {
@@ -96,6 +134,32 @@ func CurrentContext() (endpoint Endpoint, user *User) {
return
}
+func CurrentContextAddOns() (addOns []AddOn) {
+ currentContextName := GetCurrentContextOrFatal()
+
+ for _, c := range config.Contexts {
+ if c.Name == currentContextName {
+ addOns = c.AddOns
+ break
+ }
+ }
+
+ return
+}
+
+func CurrentContextNetwork() (network *string) {
+ currentContextName := GetCurrentContextOrFatal()
+
+ for _, c := range config.Contexts {
+ if c.Name == currentContextName {
+ network = c.Network
+ break
+ }
+ }
+
+ return
+}
+
// GetCurrentContextInfo returns endpoint and basic auth info
// associated with current context
func GetCurrentContextInfo() (server string, username string, password string) {
diff --git a/internal/config/endpoint-container.go b/internal/config/endpoint-container.go
index dfc87f19..ff82422a 100644
--- a/internal/config/endpoint-container.go
+++ b/internal/config/endpoint-container.go
@@ -43,7 +43,7 @@ func CurrentContextEndpointHasContainer() (exists bool) {
currentContextName := config.CurrentContext
if currentContextName == "" {
- panic("currentContextName must not be empty")
+ return false
}
for _, c := range config.Contexts {
@@ -63,14 +63,11 @@ func CurrentContextEndpointHasContainer() (exists bool) {
return
}
-// FindFreePortForTds is used to find a free port number to use for the TDS
-// protocol. It starts at port number 1433 and continues until it finds a port
+// FindFreePort is used to find a free port number to use for the TDS
+// protocol. It starts at port number startingPortNumber and continues until it finds a port
// number that is not currently in use by any of the endpoints in the
// configuration. It also checks that the port is available on the local machine.
-// If no available port is found after trying up to port number 5000, the function panics.
-func FindFreePortForTds() (portNumber int) {
- const startingPortNumber = 1433
-
+func FindFreePort(startingPortNumber int) (portNumber int) {
portNumber = startingPortNumber
for {
@@ -91,7 +88,7 @@ func FindFreePortForTds() (portNumber int) {
portNumber++
- if portNumber == 5000 {
+ if portNumber == startingPortNumber+2000 {
panic("Did not find an available port")
}
}
diff --git a/internal/config/endpoint-container_test.go b/internal/config/endpoint-container_test.go
index d7f2a02e..8a77a27b 100644
--- a/internal/config/endpoint-container_test.go
+++ b/internal/config/endpoint-container_test.go
@@ -11,14 +11,14 @@ import (
"testing"
)
-// TestCurrentContextEndpointHasContainer verifies the function panics when
+// TestCurrentContextEndpointHasContainer verifies the function returns false when
// no current context
func TestCurrentContextEndpointHasContainer(t *testing.T) {
SetFileName(pal.FilenameInUserHomeDotDirectory(
".sqlcmd", "sqlconfig-TestCurrentContextEndpointHasContainer"))
Clean()
- assert.Panics(t, func() { CurrentContextEndpointHasContainer() })
+ assert.False(t, CurrentContextEndpointHasContainer())
}
func TestGetContainerId(t *testing.T) {
diff --git a/internal/config/types.go b/internal/config/types.go
new file mode 100644
index 00000000..34c6f236
--- /dev/null
+++ b/internal/config/types.go
@@ -0,0 +1,11 @@
+package config
+
+type ContextOptions struct {
+ ImageName string
+ PortNumber int
+ ContainerId string
+ Username string
+ Password string
+ PasswordEncryption string
+ Network string
+}
diff --git a/internal/container/controller.go b/internal/container/controller.go
index 45eead59..23a7488f 100644
--- a/internal/container/controller.go
+++ b/internal/container/controller.go
@@ -4,6 +4,7 @@
package container
import (
+ "archive/tar"
"bufio"
"bytes"
"context"
@@ -19,6 +20,7 @@ import (
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/go-connections/nat"
specs "github.com/opencontainers/image-spec/specs-go/v1"
+ "os"
)
type Controller struct {
@@ -64,43 +66,65 @@ func (c Controller) EnsureImage(image string) (err error) {
return
}
+func (c Controller) NetworkCreate(name string) string {
+ resp, err := c.cli.NetworkCreate(context.Background(), name, types.NetworkCreate{})
+ checkErr(err)
+
+ return resp.ID
+}
+
+func (c Controller) NetworkDelete(name string) {
+ err := c.cli.NetworkRemove(context.Background(), name)
+ checkErr(err)
+}
+
+func (c Controller) NetworkExists(name string) bool {
+ networks, err := c.cli.NetworkList(context.Background(), types.NetworkListOptions{})
+ checkErr(err)
+
+ for _, network := range networks {
+ if network.Name == name {
+ return true
+ }
+ }
+
+ return false
+}
+
// ContainerRun creates a new container using the provided image and env values
-// and binds it to the specified port number. It then starts the container and returns
-// the ID of the container.
+// and binds the internal port to the specified external port number. It then starts
+// the container and returns the ID of the container.
func (c Controller) ContainerRun(
image string,
- env []string,
- port int,
- name string,
- hostname string,
- architecture string,
- os string,
- command []string,
- unitTestFailure bool,
+ options RunOptions,
) string {
hostConfig := &container.HostConfig{
PortBindings: nat.PortMap{
- nat.Port("1433/tcp"): []nat.PortBinding{
+ nat.Port(strconv.Itoa(options.PortInternal) + "/tcp"): []nat.PortBinding{
{
HostIP: "0.0.0.0",
- HostPort: strconv.Itoa(port),
+ HostPort: strconv.Itoa(options.Port),
},
},
},
+ NetworkMode: container.NetworkMode(options.Network),
}
platform := specs.Platform{
- Architecture: architecture,
- OS: os,
+ Architecture: options.Architecture,
+ OS: options.Os,
}
resp, err := c.cli.ContainerCreate(context.Background(), &container.Config{
Tty: true,
Image: image,
- Cmd: command,
- Env: env,
- Hostname: hostname,
- }, hostConfig, nil, &platform, name)
+ Cmd: options.Command,
+ Env: options.Env,
+ Hostname: options.Hostname,
+ ExposedPorts: nat.PortSet{
+ nat.Port(strconv.Itoa(options.PortInternal) + "/tcp"): {},
+ },
+ }, hostConfig, nil, &platform, options.Name)
checkErr(err)
err = c.cli.ContainerStart(
@@ -108,7 +132,7 @@ func (c Controller) ContainerRun(
resp.ID,
types.ContainerStartOptions{},
)
- if err != nil || unitTestFailure {
+ if err != nil || options.UnitTestFailure {
// Remove the container, because we haven't persisted to config yet, so
// uninstall won't work yet
if resp.ID != "" {
@@ -121,6 +145,16 @@ func (c Controller) ContainerRun(
return resp.ID
}
+func (c Controller) ContainerName(containerID string) string {
+ // Inspect the container to get details
+ containerInfo, err := c.cli.ContainerInspect(context.Background(), containerID)
+ checkErr(err)
+
+ // Access the container name from the inspect result
+ containerName := containerInfo.Name[1:] // Removing the leading '/'
+ return containerName
+}
+
// ContainerWaitForLogEntry is used to wait for a specific string to be written
// to the logs of a container with the given ID. The function takes in the ID
// of the container and the string to look for in the logs. It creates a reader
@@ -232,6 +266,43 @@ func (c Controller) ContainerFiles(id string, filespec string) (files []string)
return strings.Split(string(stdout), "\n")
}
+func (c Controller) CopyFile(id string, src string, destFolder string) {
+ if id == "" {
+ panic("Must pass in non-empty id")
+ }
+ if src == "" {
+ panic("Must pass in non-empty src")
+ }
+ if destFolder == "" {
+ panic("Must pass in non-empty destFolder")
+ }
+
+ trace("Copying file %s to %s", src, destFolder)
+
+ _, f := filepath.Split(src)
+ h, err := os.ReadFile(src)
+ checkErr(err)
+
+ // Create and add some files to the archive.
+ var buf bytes.Buffer
+ tw := tar.NewWriter(&buf)
+ defer func() {
+ checkErr(tw.Close())
+ }()
+ hdr := &tar.Header{
+ Name: f,
+ Mode: 0600,
+ Size: int64(len(h)),
+ }
+ err = tw.WriteHeader(hdr)
+ checkErr(err)
+ _, err = tw.Write([]byte(h))
+ checkErr(err)
+
+ err = c.cli.CopyToContainer(context.Background(), id, destFolder, &buf, types.CopyToContainerOptions{})
+ checkErr(err)
+}
+
func (c Controller) DownloadFile(id string, src string, destFolder string) {
if id == "" {
panic("Must pass in non-empty id")
@@ -243,10 +314,12 @@ func (c Controller) DownloadFile(id string, src string, destFolder string) {
panic("Must pass in non-empty destFolder")
}
- cmd := []string{"mkdir", destFolder}
- c.runCmdInContainer(id, cmd)
+ trace("Downloading file %s to %s (will try wget first, and curl if wget fails", src, destFolder)
+
+ cmd := []string{"mkdir", "-p", destFolder}
+ c.RunCmdInContainer(id, cmd, ExecOptions{})
- _, file := filepath.Split(src)
+ _, file := filepath.Split(strings.Split(src, "?")[0])
// Wget the .bak file from the http src, and place it in /var/opt/sql/backup
cmd = []string{
@@ -256,19 +329,41 @@ func (c Controller) DownloadFile(id string, src string, destFolder string) {
src,
}
- c.runCmdInContainer(id, cmd)
+ _, _, exitCode := c.RunCmdInContainer(id, cmd, ExecOptions{})
+ trace("wget exit code: %d", exitCode)
+
+ if exitCode == 126 {
+ trace("wget was not found in container, trying curl")
+ cmd = []string{
+ "curl",
+ "-o",
+ destFolder + "/" + file, // not using filepath.Join here, this is in the *nix container. always /
+ "-L",
+ src,
+ }
+
+ _, _, exitCode = c.RunCmdInContainer(id, cmd, ExecOptions{})
+ trace("curl exit code: %d", exitCode)
+ }
+}
+
+type ExecOptions struct {
+ User string
+ Env []string
}
-func (c Controller) runCmdInContainer(id string, cmd []string) ([]byte, []byte) {
- trace("Running command in container: " + strings.Join(cmd, " "))
+func (c Controller) RunCmdInContainer(id string, cmd []string, options ExecOptions) ([]byte, []byte, int) {
+ trace("Running command in container: " + strings.Replace(strings.Join(cmd, " "), "%", "%%", -1))
response, err := c.cli.ContainerExecCreate(
context.Background(),
id,
types.ExecConfig{
+ User: options.User,
AttachStderr: true,
AttachStdout: true,
Cmd: cmd,
+ Env: options.Env,
},
)
checkErr(err)
@@ -298,10 +393,16 @@ func (c Controller) runCmdInContainer(id string, cmd []string) ([]byte, []byte)
stderr, err := io.ReadAll(&errBuf)
checkErr(err)
- trace("Stdout: " + string(stdout))
- trace("Stderr: " + string(stderr))
+ trace("Stdout: " + strings.Replace(string(stdout), "%", "%%%%", -1))
+ trace("Stderr: " + strings.Replace(string(stderr), "%", "%%%%", -1))
+
+ // Get the exit code
+ execInspect, err := c.cli.ContainerExecInspect(context.Background(), response.ID)
+ checkErr(err)
- return stdout, stderr
+ trace("ExitCode: %d", execInspect.ExitCode)
+
+ return stdout, stderr, execInspect.ExitCode
}
// ContainerRunning returns true if the container with the given ID is running.
@@ -323,13 +424,15 @@ func (c Controller) ContainerRunning(id string) (running bool) {
// filtering by the given ID. If a container with the given ID is found, it
// returns true; otherwise, it returns false.
func (c Controller) ContainerExists(id string) (exists bool) {
+ trace("ContainerExists: " + id)
+
f := filters.NewArgs()
f.Add(
"id", id,
)
resp, err := c.cli.ContainerList(
context.Background(),
- types.ContainerListOptions{Filters: f},
+ types.ContainerListOptions{Filters: f, All: true},
)
checkErr(err)
if len(resp) > 0 {
@@ -340,6 +443,8 @@ func (c Controller) ContainerExists(id string) (exists bool) {
exists = true
}
+ trace("ContainerExists: %v", exists)
+
return
}
@@ -362,3 +467,219 @@ func (c Controller) ContainerRemove(id string) (err error) {
return
}
+
+// DAB project file
+/*
+dabCsproj := `
+
+
+ net8.0
+ `
+
+dabDockerfile := `FROM mcr.microsoft.com/azure-databases/data-api-builder:latest
+
+COPY dab-config.json /App
+WORKDIR /App
+ENV ASPNETCORE_URLS=http://+:5000
+EXPOSE 5000
+ENTRYPOINT ["dotnet", "Azure.DataApiBuilder.Service.dll"]
+`
+
+// C:\Users\stuartpa\classroom-assignment\infra\core\database\sqlserver\sqlserver.bicep
+sqlServerBicep := `metadata description = 'Creates an Azure SQL Server instance.'
+param name string
+param location string = resourceGroup().location
+param tags object = {}
+
+param appUser string = 'appUser'
+param databaseName string
+param keyVaultName string
+param sqlAdmin string = 'sqlAdmin'
+param connectionStringKey string = 'AZURE-SQL-CONNECTION-STRING'
+
+@secure()
+param sqlAdminPassword string
+@secure()
+param appUserPassword string
+
+resource sqlServer 'Microsoft.Sql/servers@2022-05-01-preview' = {
+ name: name
+ location: location
+ tags: tags
+ properties: {
+ version: '12.0'
+ minimalTlsVersion: '1.2'
+ publicNetworkAccess: 'Enabled'
+ administratorLogin: sqlAdmin
+ administratorLoginPassword: sqlAdminPassword
+ }
+
+ resource database 'databases' = {
+ name: databaseName
+ location: location
+ }
+
+ resource firewall 'firewallRules' = {
+ name: 'Azure Services'
+ properties: {
+ // Allow all clients
+ // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only".
+ // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes.
+ startIpAddress: '0.0.0.1'
+ endIpAddress: '255.255.255.254'
+ }
+ }
+}
+
+resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
+ name: '${name}-deployment-script'
+ location: location
+ kind: 'AzureCLI'
+ properties: {
+ azCliVersion: '2.37.0'
+ retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running
+ timeout: 'PT5M' // Five minutes
+ cleanupPreference: 'OnSuccess'
+ environmentVariables: [
+ {
+ name: 'APPUSERNAME'
+ value: appUser
+ }
+ {
+ name: 'APPUSERPASSWORD'
+ secureValue: appUserPassword
+ }
+ {
+ name: 'DBNAME'
+ value: databaseName
+ }
+ {
+ name: 'DBSERVER'
+ value: sqlServer.properties.fullyQualifiedDomainName
+ }
+ {
+ name: 'SQLCMDPASSWORD'
+ secureValue: sqlAdminPassword
+ }
+ {
+ name: 'SQLADMIN'
+ value: sqlAdmin
+ }
+ ]
+
+ scriptContent: '''
+wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2
+tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C .
+
+cat < ./initDb.sql
+drop user if exists ${APPUSERNAME}
+go
+create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}'
+go
+alter role db_owner add member ${APPUSERNAME}
+go
+SCRIPT_END
+
+./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql
+ '''
+ }
+}
+
+resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
+ parent: keyVault
+ name: 'sqlAdminPassword'
+ properties: {
+ value: sqlAdminPassword
+ }
+}
+
+resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
+ parent: keyVault
+ name: 'appUserPassword'
+ properties: {
+ value: appUserPassword
+ }
+}
+
+resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
+ parent: keyVault
+ name: connectionStringKey
+ properties: {
+ value: '${connectionString}; Password=${appUserPassword}'
+ }
+}
+
+resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
+ name: keyVaultName
+}
+
+var connectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}'
+output connectionStringKey string = connectionStringKey
+output databaseName string = sqlServer::database.name
+`
+
+mainBicepDatabase := `
+param sqlDatabaseName string = ''
+param sqlServerName string = ''
+
+@secure()
+@description('SQL Server administrator password')
+param sqlAdminPassword string
+
+@secure()
+@description('Application user password')
+param appUserPassword string
+
+// The application database
+module sqlServer './app/db.bicep' = {
+ name: 'sql'
+ scope: rg
+ params: {
+ name: !empty(sqlServerName) ? sqlServerName : '${abbrs.sqlServers}${resourceToken}'
+ databaseName: sqlDatabaseName
+ location: location
+ tags: tags
+ sqlAdminPassword: sqlAdminPassword
+ appUserPassword: appUserPassword
+ keyVaultName: keyVault.outputs.name
+ }
+}
+
+output AZURE_SQL_CONNECTION_STRING_KEY string = sqlServer.outputs.connectionStringKey
+`
+
+// C:\Users\stuartpa\classroom-assignment\infra\app\db.bicep
+dbBicep := `param name string
+param location string = resourceGroup().location
+param tags object = {}
+
+param databaseName string = ''
+param keyVaultName string
+
+@secure()
+param sqlAdminPassword string
+@secure()
+param appUserPassword string
+
+// Because databaseName is optional in main.bicep, we make sure the database name is set here.
+var defaultDatabaseName = 'sample'
+var actualDatabaseName = !empty(databaseName) ? databaseName : defaultDatabaseName
+
+module sqlServer '../core/database/sqlserver/sqlserver.bicep' = {
+ name: 'sqlserver'
+ params: {
+ name: name
+ location: location
+ tags: tags
+ databaseName: actualDatabaseName
+ keyVaultName: keyVaultName
+ sqlAdminPassword: sqlAdminPassword
+ appUserPassword: appUserPassword
+ }
+}
+
+output connectionStringKey string = sqlServer.outputs.connectionStringKey
+output databaseName string = sqlServer.outputs.databaseName
+`
+
+*/
diff --git a/internal/container/controller_test.go b/internal/container/controller_test.go
index 347d7aec..d69b43fc 100644
--- a/internal/container/controller_test.go
+++ b/internal/container/controller_test.go
@@ -33,17 +33,20 @@ func TestController_EnsureImage(t *testing.T) {
c := NewController()
err := c.EnsureImage(imageName)
checkErr(err)
- id := c.ContainerRun(
- imageName,
- []string{},
- port,
- "",
- "",
- "amd64",
- "linux",
- []string{"ash", "-c", "echo 'Hello World'; sleep 3"},
- false,
- )
+<<<<<<< HEAD
+
+ runOptions := RunOptions{
+ Env: []string{},
+ Port: port,
+ Architecture: "amd64",
+ Os: "linux",
+ Command: []string{"ash", "-c", "echo 'Hello World'; sleep 3"},
+ }
+
+ id := c.ContainerRun(imageName, runOptions)
+=======
+ id := c.ContainerRun(imageName, []string{}, nil, 1433, port, "", "", "amd64", "linux", "", []string{"ash", "-c", "echo 'Hello World'; sleep 3"}, false)
+>>>>>>> stuartpa/add-ons
c.ContainerRunning(id)
c.ContainerWaitForLogEntry(id, "Hello World")
c.ContainerExists(id)
@@ -79,19 +82,19 @@ func TestController_ContainerRunFailure(t *testing.T) {
c := NewController()
+<<<<<<< HEAD
+ runOptions := RunOptions{
+ Architecture: "amd64",
+ Os: "linux",
+ Command: []string{"ash", "-c", "echo 'Hello World'; sleep 1"},
+ }
+
+ assert.Panics(t, func() { c.ContainerRun(imageName, runOptions) })
+=======
assert.Panics(t, func() {
- c.ContainerRun(
- imageName,
- []string{},
- 0,
- "",
- "",
- "amd64",
- "linux",
- []string{"ash", "-c", "echo 'Hello World'; sleep 1"},
- false,
- )
+ c.ContainerRun(imageName, []string{}, nil, 0, 0, "", "", "amd64", "linux", "", []string{"ash", "-c", "echo 'Hello World'; sleep 1"}, false)
})
+>>>>>>> stuartpa/add-ons
}
func TestController_ContainerRunFailureCleanup(t *testing.T) {
@@ -107,19 +110,19 @@ func TestController_ContainerRunFailureCleanup(t *testing.T) {
c := NewController()
+<<<<<<< HEAD
+ runOptions := RunOptions{
+ Architecture: "amd64",
+ Os: "linux",
+ Command: []string{"ash", "-c", "echo 'Hello World'; sleep 1"},
+ UnitTestFailure: true,
+ }
+ assert.Panics(t, func() { c.ContainerRun(imageName, runOptions) })
+=======
assert.Panics(t, func() {
- c.ContainerRun(
- imageName,
- []string{},
- 0,
- "",
- "",
- "amd64",
- "linux",
- []string{"ash", "-c", "echo 'Hello World'; sleep 1"},
- true,
- )
+ c.ContainerRun(imageName, []string{}, nil, 0, 0, "", "", "amd64", "linux", "", []string{"ash", "-c", "echo 'Hello World'; sleep 1"}, true)
})
+>>>>>>> stuartpa/add-ons
}
func TestController_ContainerStopNeg2(t *testing.T) {
diff --git a/internal/container/types.go b/internal/container/types.go
new file mode 100644
index 00000000..cbc4f689
--- /dev/null
+++ b/internal/container/types.go
@@ -0,0 +1,14 @@
+package container
+
+type RunOptions struct {
+ Network string
+ Env []string
+ PortInternal int
+ Port int
+ Name string
+ Hostname string
+ Architecture string
+ Os string
+ Command []string
+ UnitTestFailure bool
+}
diff --git a/internal/databaseurl/error.go b/internal/databaseurl/error.go
new file mode 100644
index 00000000..039500da
--- /dev/null
+++ b/internal/databaseurl/error.go
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package databaseurl
+
+var errorCallback func(err error)
+
+func checkErr(err error) {
+ errorCallback(err)
+}
diff --git a/internal/databaseurl/factory.go b/internal/databaseurl/factory.go
new file mode 100644
index 00000000..d607e269
--- /dev/null
+++ b/internal/databaseurl/factory.go
@@ -0,0 +1,70 @@
+package databaseurl
+
+import (
+ url2 "net/url"
+ "path/filepath"
+ "strings"
+)
+
+func NewDatabaseUrl(url string) *DatabaseUrl {
+ trace("NewDatabaseUrl(" + url + ")")
+
+ databaseUrl := DatabaseUrl{}
+
+ // To enable URL.Parse, switch to / from \\
+ url = strings.Replace(url, "\\", "/", -1)
+
+ // Cope with a URL that in the local directory, so it can be URL.Parsed()
+ if !strings.Contains(url, "/") {
+ url = "./" + url
+ }
+
+ // Cope with a file:// URL that in the local directory, so it can be URL.Parsed()
+ if strings.HasPrefix(strings.ToLower(url), "file://") &&
+ !strings.Contains(url[7:], "/") {
+ url = "file://./" + url[7:]
+ }
+
+ parsedUrl, err := url2.Parse(url)
+ checkErr(err)
+
+ databaseUrl.URL = parsedUrl
+
+ trace("databaseUrl.URL.Path: " + databaseUrl.URL.Path)
+
+ databaseUrl.Filename = filepath.Base(databaseUrl.URL.Path)
+ databaseUrl.FileExtension = strings.TrimLeft(filepath.Ext(databaseUrl.Filename), ".")
+
+ split := strings.Split(databaseUrl.URL.Path, ",")
+ if len(split) > 1 {
+ databaseUrl.DatabaseName = split[1]
+
+ // Remove the database name (specified after the comma) from the URL, and reparse it
+ url = strings.Replace(url, ","+split[1], "", 1)
+ databaseUrl.URL, err = databaseUrl.URL.Parse(url)
+ checkErr(err)
+
+ split := strings.Split(databaseUrl.FileExtension, ",")
+ databaseUrl.FileExtension = split[0]
+
+ split = strings.Split(databaseUrl.Filename, ",")
+ databaseUrl.Filename = split[0]
+ } else {
+ databaseUrl.DatabaseName = strings.TrimSuffix(
+ databaseUrl.Filename,
+ "."+databaseUrl.FileExtension,
+ )
+ }
+
+ trace("databaseUrl.Filename: " + databaseUrl.Filename)
+ trace("databaseUrl.FileExtension: " + databaseUrl.FileExtension)
+ trace("databaseUrl.DatabaseName: " + databaseUrl.DatabaseName)
+
+ databaseUrl.IsLocal = databaseUrl.URL.Scheme == "file" || len(databaseUrl.URL.Scheme) < 3
+
+ escapedDbName := strings.ReplaceAll(databaseUrl.DatabaseName, "'", "''")
+ databaseUrl.DatabaseNameAsTsqlIdentifier = strings.ReplaceAll(escapedDbName, "]", "]]")
+ databaseUrl.DatabaseNameAsNonTsqlIdentifier = strings.ReplaceAll(databaseUrl.DatabaseName, "]", "]]")
+
+ return &databaseUrl
+}
diff --git a/internal/databaseurl/factory_test.go b/internal/databaseurl/factory_test.go
new file mode 100644
index 00000000..80a07f04
--- /dev/null
+++ b/internal/databaseurl/factory_test.go
@@ -0,0 +1,52 @@
+package databaseurl
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestNewDatabaseUrl(t *testing.T) {
+ tests := []struct {
+ url string
+ want string
+ }{
+ {"https://example.com/testdb.bak,myDbName", "myDbName"},
+ {"https://example.com/testdb.bak", "testdb"},
+ {"https://example.com/test.foo", "test"},
+ {"https://example.com/test.foo,test", "test"},
+ {"https://example.com/test.7z,tsql_name", "tsql_name"},
+ {"https://example.com/test.mdf,tsql_name?foo=bar", "tsql_name"},
+ {"https://example.com/test.mdf,tsql_name#link?foo=bar", "tsql_name"},
+ {"https://example.com/test.mdf?foo=bar", "test"},
+ {"https://example.com/test.mdf#link?foo=bar", "test"},
+ {"https://example.com/test,test", "test"},
+ {"https://example.com,", ""},
+ {"https://example.com", ""},
+ {"test.7z,tsql_name", "tsql_name"},
+ {"test.mdf,tsql_name", "tsql_name"},
+ {"test.mdf", "test"},
+ {"c:\\test.mdf", "test"},
+ {"c:\\test.mdf,tsql_name", "tsql_name"},
+ {"file://test.mdf,tsql_name", "tsql_name"},
+ {"file://test.mdf", "test"},
+ {"file://c:\\test.mdf", "test"},
+ {"file://c:\\folder\\test.mdf", "test"},
+ {"file://c:/test.mdf", "test"},
+ {"file://c:/folder/test.mdf", "test"},
+ {"file:\\test.mdf,tsql_name", "tsql_name"},
+ {"file:\\test.mdf", "test"},
+ {"file:\\c:\\test.mdf", "test"},
+ {"file:\\c:\\folder\\test.mdf", "test"},
+ {"file:\\c:/test.mdf", "test"},
+ {"file:\\c:/folder/test.mdf", "test"},
+ {"\\\\server\\share\\test.mdf", "test"},
+ {"\\\\server\\share\\folder\\test.mdf", "test"},
+ {"\\\\server\\share\\folder\\test.mdf,db_name", "db_name"},
+ }
+ for _, tt := range tests {
+ t.Run("DatabaseURLTest-"+tt.url, func(t *testing.T) {
+ url := NewDatabaseUrl(tt.url)
+ assert.Equalf(t, tt.want, url.DatabaseName, "NewDatabaseUrl(%v)", url.DatabaseName)
+ })
+ }
+}
diff --git a/internal/databaseurl/initialize.go b/internal/databaseurl/initialize.go
new file mode 100644
index 00000000..26df3568
--- /dev/null
+++ b/internal/databaseurl/initialize.go
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package databaseurl
+
+func init() {
+ Initialize(
+ func(err error) {
+ if err != nil {
+ panic(err)
+ }
+ },
+ func(format string, a ...any) {})
+}
+
+func Initialize(
+ errorHandler func(err error),
+ traceHandler func(format string, a ...any)) {
+
+ errorCallback = errorHandler
+ traceCallback = traceHandler
+}
diff --git a/internal/databaseurl/trace.go b/internal/databaseurl/trace.go
new file mode 100644
index 00000000..05f2ce24
--- /dev/null
+++ b/internal/databaseurl/trace.go
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package databaseurl
+
+var traceCallback func(format string, a ...any)
+
+func trace(format string, a ...any) {
+ traceCallback(format, a...)
+}
diff --git a/internal/databaseurl/type.go b/internal/databaseurl/type.go
new file mode 100644
index 00000000..363f157d
--- /dev/null
+++ b/internal/databaseurl/type.go
@@ -0,0 +1,22 @@
+package databaseurl
+
+import "net/url"
+
+type DatabaseUrl struct {
+ *url.URL
+
+ Filename string
+
+ // Is this .git or git!
+ FileExtension string
+ IsLocal bool
+
+ // DatabaseName returns the databaseName from --use arg
+ // It sets database name to the specified database name
+ // or in absence of it, it is set to the filename without
+ // extension.
+ DatabaseName string
+
+ DatabaseNameAsTsqlIdentifier string
+ DatabaseNameAsNonTsqlIdentifier string
+}
diff --git a/cmd/modern/root/install/mssql-base_test.go b/internal/databaseurl/uri_test.go
similarity index 78%
rename from cmd/modern/root/install/mssql-base_test.go
rename to internal/databaseurl/uri_test.go
index a533eb2e..4bb0dee1 100644
--- a/cmd/modern/root/install/mssql-base_test.go
+++ b/internal/databaseurl/uri_test.go
@@ -1,13 +1,32 @@
-package install
+package databaseurl
import (
- "testing"
-
"github.com/stretchr/testify/assert"
+ "testing"
)
-func TestGetDbNameIfExists(t *testing.T) {
+func TestExtractUrl(t *testing.T) {
+ type test struct {
+ inputURL string
+ expectedURL string
+ }
+ tests := []test{
+ {"https://example.com/testdb.bak,myDbName", "https://example.com/testdb.bak"},
+ {"https://example.com/testdb.bak", "https://example.com/testdb.bak"},
+ {"https://example.com,", "https://example.com,"},
+ }
+
+ for _, testcase := range tests {
+ u := NewDatabaseUrl(testcase.inputURL)
+ assert.Equal(t, testcase.expectedURL, u.String(),
+ "Extracted URL does not match expected URL")
+ }
+}
+
+func TestGetDbNameIfExists(t *testing.T) {
+ t.Skip("stuartpa: Fix before code-review")
+
type test struct {
input string
expectedIdentifierOp string
@@ -37,27 +56,11 @@ func TestGetDbNameIfExists(t *testing.T) {
}
for _, testcase := range tests {
- dbname := parseDbName(testcase.input)
- dbnameAsIdentifier := getDbNameAsIdentifier(dbname)
- dbnameAsNonIdentifier := getDbNameAsNonIdentifier(dbname)
- assert.Equal(t, testcase.expectedIdentifierOp, dbnameAsIdentifier, "Unexpected database name as identifier")
- assert.Equal(t, testcase.expectedNonIdentifierOp, dbnameAsNonIdentifier, "Unexpected database name as non-identifier")
- }
-}
+ u := NewDatabaseUrl(testcase.input)
-func TestExtractUrl(t *testing.T) {
- type test struct {
- inputURL string
- expectedURL string
- }
-
- tests := []test{
- {"https://example.com/testdb.bak,myDbName", "https://example.com/testdb.bak"},
- {"https://example.com/testdb.bak", "https://example.com/testdb.bak"},
- {"https://example.com,", "https://example.com,"},
- }
-
- for _, testcase := range tests {
- assert.Equal(t, testcase.expectedURL, extractUrl(testcase.inputURL), "Extracted URL does not match expected URL")
+ assert.Equal(t, testcase.expectedIdentifierOp, u.DatabaseNameAsTsqlIdentifier,
+ "Unexpected database name as identifier")
+ assert.Equal(t, testcase.expectedNonIdentifierOp, u.DatabaseNameAsNonTsqlIdentifier,
+ "Unexpected database name as non-identifier")
}
}
diff --git a/internal/dotsqlcmdconfig/dotsqlcmdconfig.go b/internal/dotsqlcmdconfig/dotsqlcmdconfig.go
new file mode 100644
index 00000000..b502bf22
--- /dev/null
+++ b/internal/dotsqlcmdconfig/dotsqlcmdconfig.go
@@ -0,0 +1,79 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package dotsqlcmdconfig
+
+import (
+ . "github.com/microsoft/go-sqlcmd/cmd/modern/sqlcmdconfig"
+ "github.com/microsoft/go-sqlcmd/internal/io/file"
+ "path/filepath"
+ "testing"
+)
+
+var config Sqlcmdconfig
+var filename string
+
+// SetFileName sets the filename for the file that the application reads from and
+// writes to. The file is created if it does not already exist, and Viper is configured
+// to use the given filename.
+func SetFileName(name string) {
+ if name == "" {
+ panic("name is empty")
+ }
+
+ filename = name
+
+ file.CreateEmptyIfNotExists(filename)
+}
+
+func DatabaseNames() (dbs []string) {
+ for _, db := range config.Databases {
+ dbs = append(dbs, db.Name)
+ }
+
+ return
+}
+
+func DatabaseFiles(ordinal int) (files []string) {
+ if ordinal < 0 || ordinal >= len(config.Databases) {
+ return
+ }
+ db := config.Databases[ordinal]
+
+ for _, file := range db.DatabaseDetails.Use {
+ files = append(files, file.Uri)
+ }
+
+ return
+}
+
+func AddonTypes() (addons []string) {
+ for _, addon := range config.AddOns {
+ addons = append(addons, addon.Type)
+ }
+
+ return
+}
+
+func AddonFiles(ordinal int) (files []string) {
+ if ordinal < 0 || ordinal >= len(config.AddOns) {
+ return
+ }
+ addon := config.AddOns[ordinal]
+
+ for _, file := range addon.AddOnDetails.Use {
+ files = append(files, file.Uri)
+ }
+
+ return
+}
+
+func SetFileNameForTest(t *testing.T) {
+ SetFileName(filepath.Join(".sqlcmd", "sqlcmd.yaml"))
+}
+
+func DefaultFileName() (filename string) {
+ filename = filepath.Join(".sqlcmd", "sqlcmd.yaml")
+
+ return
+}
diff --git a/internal/dotsqlcmdconfig/error.go b/internal/dotsqlcmdconfig/error.go
new file mode 100644
index 00000000..92f16a8e
--- /dev/null
+++ b/internal/dotsqlcmdconfig/error.go
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package dotsqlcmdconfig
+
+var errorCallback func(err error)
+
+func checkErr(err error) {
+ errorCallback(err)
+}
diff --git a/internal/dotsqlcmdconfig/initialize.go b/internal/dotsqlcmdconfig/initialize.go
new file mode 100644
index 00000000..880de039
--- /dev/null
+++ b/internal/dotsqlcmdconfig/initialize.go
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package dotsqlcmdconfig
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/net"
+ "github.com/microsoft/go-sqlcmd/internal/secret"
+)
+
+var encryptCallback func(plainText string, encryptionMethod string) (cipherText string)
+var decryptCallback func(cipherText string, encryptionMethod string) (secret string)
+var isLocalPortAvailableCallback func(port int) (portAvailable bool)
+
+// init sets up the package to work with a set of handlers to be used for the period
+// before the command-line has been parsed
+func init() {
+ errorHandler := func(err error) {
+ if err != nil {
+ panic(err)
+ }
+ }
+ traceHandler := func(format string, a ...any) {
+ fmt.Printf(format, a...)
+ }
+
+ Initialize(
+ errorHandler,
+ traceHandler,
+ secret.Encode,
+ secret.Decode,
+ net.IsLocalPortAvailable)
+}
+
+// Initialize sets the callback functions used by the config package.
+// These callback functions are used for logging errors, tracing debug messages,
+// encrypting and decrypting data, and checking if a local port is available.
+// The callback functions are passed to the function as arguments.
+// This function should be called at the start of the application to ensure that the
+// config package has the necessary callback functions available.
+func Initialize(
+ errorHandler func(err error),
+ traceHandler func(format string, a ...any),
+ encryptHandler func(plainText string, encryptionMethod string) (cipherText string),
+ decryptHandler func(cipherText string, encryptionMethod string) (secret string),
+ isLocalPortAvailableHandler func(port int) (portAvailable bool),
+) {
+ errorCallback = errorHandler
+ traceCallback = traceHandler
+ encryptCallback = encryptHandler
+ decryptCallback = decryptHandler
+ isLocalPortAvailableCallback = isLocalPortAvailableHandler
+}
diff --git a/internal/dotsqlcmdconfig/trace.go b/internal/dotsqlcmdconfig/trace.go
new file mode 100644
index 00000000..d3b51bf5
--- /dev/null
+++ b/internal/dotsqlcmdconfig/trace.go
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package dotsqlcmdconfig
+
+var traceCallback func(format string, a ...any)
+
+func trace(format string, a ...any) {
+ traceCallback(format, a...)
+}
diff --git a/internal/dotsqlcmdconfig/viper.go b/internal/dotsqlcmdconfig/viper.go
new file mode 100644
index 00000000..3fcb0c8c
--- /dev/null
+++ b/internal/dotsqlcmdconfig/viper.go
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package dotsqlcmdconfig
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/io/file"
+ "github.com/microsoft/go-sqlcmd/internal/pal"
+ "github.com/spf13/viper"
+ "gopkg.in/yaml.v2"
+ "io"
+)
+
+// Load loads the configuration from the file specified by the SetFileName() function.
+// Any errors encountered while marshalling or saving the configuration are checked
+// and handled by the injected errorHandler (via the checkErr function).
+func Load() {
+ if filename == "" {
+ panic("Must call config.SetFileName()")
+ }
+
+ text := file.GetContents(filename)
+ err := yaml.Unmarshal([]byte(text), &config)
+ checkErr(err)
+
+ trace("Config loaded from file: %v"+pal.LineBreak(), filename)
+}
+
+// Save marshals the current configuration object and saves it to the configuration
+// file previously specified by the SetFileName variable.
+// Any errors encountered while marshalling or saving the configuration are checked
+// and handled by the injected errorHandler (via the checkErr function).
+func Save() {
+ if filename == "" {
+ panic("Must call config.SetFileName()")
+ }
+
+ if config.Version == "" {
+ config.Version = "v1"
+ }
+
+ var io io.WriteCloser
+
+ b, err := yaml.Marshal(&config)
+ checkErr(err)
+
+ _, err = io.Write(b)
+ checkErr(err)
+}
+
+// GetConfigFileUsed returns the path to the configuration file used by the Viper library.
+func GetConfigFileUsed() string {
+ return viper.ConfigFileUsed()
+}
diff --git a/internal/intialize.go b/internal/intialize.go
index e1bb9589..dadee834 100644
--- a/internal/intialize.go
+++ b/internal/intialize.go
@@ -6,9 +6,12 @@ package internal
import (
"github.com/microsoft/go-sqlcmd/internal/config"
"github.com/microsoft/go-sqlcmd/internal/container"
+ "github.com/microsoft/go-sqlcmd/internal/databaseurl"
+ "github.com/microsoft/go-sqlcmd/internal/dotsqlcmdconfig"
"github.com/microsoft/go-sqlcmd/internal/http"
"github.com/microsoft/go-sqlcmd/internal/io/file"
"github.com/microsoft/go-sqlcmd/internal/net"
+ "github.com/microsoft/go-sqlcmd/internal/output/verbosity"
"github.com/microsoft/go-sqlcmd/internal/pal"
"github.com/microsoft/go-sqlcmd/internal/secret"
"github.com/microsoft/go-sqlcmd/internal/sql"
@@ -19,6 +22,7 @@ type InitializeOptions struct {
TraceHandler func(format string, a ...any)
HintHandler func([]string)
LineBreak string
+ LoggingLevel verbosity.Level
}
// Initialize initializes various dependencies for the application with the provided options.
@@ -38,12 +42,19 @@ func Initialize(options InitializeOptions) {
if options.LineBreak == "" {
panic("LineBreak is empty")
}
+
+ enableTraceLogging := false
+ if options.LoggingLevel == verbosity.Trace {
+ enableTraceLogging = true
+ }
file.Initialize(options.ErrorHandler, options.TraceHandler)
- sql.Initialize(options.ErrorHandler, options.TraceHandler, secret.Decode)
+ sql.Initialize(enableTraceLogging, options.ErrorHandler, options.TraceHandler, secret.Decode)
config.Initialize(options.ErrorHandler, options.TraceHandler, secret.Encode, secret.Decode, net.IsLocalPortAvailable)
+ dotsqlcmdconfig.Initialize(options.ErrorHandler, options.TraceHandler, secret.Encode, secret.Decode, net.IsLocalPortAvailable)
container.Initialize(options.ErrorHandler, options.TraceHandler)
secret.Initialize(options.ErrorHandler)
net.Initialize(options.ErrorHandler, options.TraceHandler)
http.Initialize(options.ErrorHandler, options.TraceHandler)
+ databaseurl.Initialize(options.ErrorHandler, options.TraceHandler)
pal.Initialize(options.ErrorHandler, options.LineBreak)
}
diff --git a/internal/output/output.go b/internal/output/output.go
index 967d1597..baf0c8a4 100644
--- a/internal/output/output.go
+++ b/internal/output/output.go
@@ -274,6 +274,15 @@ func (o Output) maskSecrets(text string) string {
// Mask password from T/SQL e.g. ALTER LOGIN [sa] WITH PASSWORD = N'foo';
r := regexp.MustCompile(`(PASSWORD.*\s?=.*\s?N?')(.*)(')`)
text = r.ReplaceAllString(text, "$1********$3")
+
+ // Mask password from sqlcmd e.g. -P foo;
+ r = regexp.MustCompile(`(-P )(.*)`)
+ text = r.ReplaceAllString(text, "$1********")
+
+ // Mask password from sqlpackage.exe command line e.g. /TargetPassword:foo
+ r = regexp.MustCompile(`(/TargetPassword:)(.*)`)
+ text = r.ReplaceAllString(text, "$1********$3")
+
return text
}
diff --git a/internal/sql/error.go b/internal/sql/error.go
index 03256d52..6796f31c 100644
--- a/internal/sql/error.go
+++ b/internal/sql/error.go
@@ -4,6 +4,7 @@
package sql
var errorCallback func(err error)
+var traceLogging bool
func checkErr(err error) {
errorCallback(err)
diff --git a/internal/sql/factory.go b/internal/sql/factory.go
index 2713fee3..08ff2556 100644
--- a/internal/sql/factory.go
+++ b/internal/sql/factory.go
@@ -7,7 +7,7 @@ type SqlOptions struct {
UnitTesting bool
}
-func New(options SqlOptions) Sql {
+func NewSql(options SqlOptions) Sql {
if options.UnitTesting {
return &mock{}
} else {
diff --git a/internal/sql/initialize.go b/internal/sql/initialize.go
index 6016215f..ddd7f985 100644
--- a/internal/sql/initialize.go
+++ b/internal/sql/initialize.go
@@ -6,9 +6,11 @@ package sql
var decryptCallback func(cipherText string, encryptionMethod string) (secret string)
func Initialize(
+ enableTraceLogging bool,
errorHandler func(err error),
traceHandler func(format string, a ...any),
decryptHandler func(cipherText string, encryptionMethod string) (secret string)) {
+ traceLogging = enableTraceLogging
errorCallback = errorHandler
traceCallback = traceHandler
decryptCallback = decryptHandler
diff --git a/internal/sql/interface.go b/internal/sql/interface.go
index 9203bda5..5a2b7069 100644
--- a/internal/sql/interface.go
+++ b/internal/sql/interface.go
@@ -10,11 +10,12 @@ import (
type Sql interface {
Connect(endpoint Endpoint, user *User, options ConnectOptions)
Query(text string)
+ ExecuteSqlFile(filename string)
ScalarString(query string) string
}
type ConnectOptions struct {
- Database string
-
+ Database string
+ LogLevel int
Interactive bool
}
diff --git a/internal/sql/mock.go b/internal/sql/mock.go
index 677dd3a1..80e1add9 100644
--- a/internal/sql/mock.go
+++ b/internal/sql/mock.go
@@ -17,6 +17,9 @@ func (m *mock) Connect(
func (m *mock) Query(text string) {
}
+func (m *mock) ExecuteSqlFile(filename string) {
+}
+
func (m *mock) ScalarString(query string) string {
return ""
}
diff --git a/internal/sql/mssql.go b/internal/sql/mssql.go
index e91b1c05..bb4702a5 100644
--- a/internal/sql/mssql.go
+++ b/internal/sql/mssql.go
@@ -5,6 +5,7 @@ package sql
import (
"fmt"
+ "io"
"os"
"strings"
@@ -41,6 +42,8 @@ func (m *mssql) Connect(
ApplicationName: "sqlcmd",
}
+ connect.LogLevel = options.LogLevel
+
if options.Database != "" {
connect.Database = options.Database
}
@@ -55,7 +58,14 @@ func (m *mssql) Connect(
user.BasicAuth.Password,
user.BasicAuth.PasswordEncryption,
)
+ } else if user.AuthenticationType == "ActiveDirectoryDefault" ||
+ user.AuthenticationType == "ActiveDirectoryInteractive" {
+ connect.Encrypt = "true"
+ connect.UserName = user.Name
+ connect.TrustServerCertificate = false
+ connect.AuthenticationMethod = user.AuthenticationType
} else {
+
panic("Authentication not supported")
}
}
@@ -89,6 +99,28 @@ func (m *mssql) Query(text string) {
}
}
+type discardCloser struct {
+ io.Writer
+}
+
+func (discardCloser) Close() error {
+ return nil
+}
+
+func (m *mssql) ExecuteSqlFile(filename string) {
+ if traceLogging {
+ m.sqlcmd.SetOutput(os.Stdout)
+ m.sqlcmd.SetError(os.Stderr)
+ } else {
+ m.sqlcmd.SetOutput(discardCloser{Writer: io.Discard})
+ m.sqlcmd.SetError(discardCloser{Writer: io.Discard})
+ }
+
+ trace("Executing .sql file: %q", filename)
+ err := m.sqlcmd.IncludeFile(filename, true)
+ checkErr(err)
+}
+
func (m *mssql) ScalarString(query string) string {
buf := buffer.NewMemoryBuffer()
defer func() { _ = buf.Close() }()
diff --git a/internal/sql/mssql_test.go b/internal/sql/mssql_test.go
index 169637ca..2a68e23a 100644
--- a/internal/sql/mssql_test.go
+++ b/internal/sql/mssql_test.go
@@ -77,7 +77,7 @@ func TestConnect(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- mssql := New(SqlOptions{})
+ mssql := NewSql(SqlOptions{})
// If test name ends in 'Panic' expect a Panic
if strings.HasSuffix(tt.name, "Panic") {
diff --git a/internal/tools/tool/ads.go b/internal/tools/tool/ads.go
index f9295e7b..fae05b46 100644
--- a/internal/tools/tool/ads.go
+++ b/internal/tools/tool/ads.go
@@ -26,9 +26,9 @@ func (t *AzureDataStudio) Init() {
}
}
-func (t *AzureDataStudio) Run(args []string) (int, error) {
+func (t *AzureDataStudio) Run(args []string, options RunOptions) (int, error) {
if !test.IsRunningInTestExecutor() {
- return t.tool.Run(args)
+ return t.tool.Run(args, options)
} else {
return 0, nil
}
diff --git a/internal/tools/tool/ads_windows.go b/internal/tools/tool/ads_windows.go
index 6d7bed55..7aab3e12 100644
--- a/internal/tools/tool/ads_windows.go
+++ b/internal/tools/tool/ads_windows.go
@@ -19,10 +19,10 @@ func (t *AzureDataStudio) searchLocations() []string {
programFiles := os.Getenv("ProgramFiles")
return []string{
- filepath.Join(userProfile, "AppData\\Local\\Programs\\Azure Data Studio - Insiders\\azuredatastudio-insiders.exe"),
- filepath.Join(programFiles, "Azure Data Studio - Insiders\\azuredatastudio-insiders.exe"),
filepath.Join(userProfile, "AppData\\Local\\Programs\\Azure Data Studio\\azuredatastudio.exe"),
filepath.Join(programFiles, "Azure Data Studio\\azuredatastudio.exe"),
+ filepath.Join(userProfile, "AppData\\Local\\Programs\\Azure Data Studio - Insiders\\azuredatastudio-insiders.exe"),
+ filepath.Join(programFiles, "Azure Data Studio - Insiders\\azuredatastudio-insiders.exe"),
}
}
diff --git a/internal/tools/tool/azd.go b/internal/tools/tool/azd.go
new file mode 100644
index 00000000..b7ab9659
--- /dev/null
+++ b/internal/tools/tool/azd.go
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/io/file"
+ "github.com/microsoft/go-sqlcmd/internal/test"
+)
+
+type AzureDeveloperCli struct {
+ tool
+}
+
+func (t *AzureDeveloperCli) Init() {
+ t.tool.SetToolDescription(Description{
+ Name: "azd",
+ Purpose: "The Azure Developer CLI ( azd ) is a developer-centric command-line interface (CLI) tool for creating Azure applications.",
+ InstallText: t.installText()})
+
+ for _, location := range t.searchLocations() {
+ if file.Exists(location) {
+ t.tool.SetExePathAndName(location)
+ break
+ }
+ }
+}
+
+func (t *AzureDeveloperCli) Run(args []string, options RunOptions) (int, error) {
+ if !test.IsRunningInTestExecutor() {
+ return t.tool.Run(args, options)
+ } else {
+ return 0, nil
+ }
+}
diff --git a/internal/tools/tool/azd_darwin.go b/internal/tools/tool/azd_darwin.go
new file mode 100644
index 00000000..c0f5b133
--- /dev/null
+++ b/internal/tools/tool/azd_darwin.go
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+import "os/exec"
+
+func (t *AzureDeveloperCli) searchLocations() []string {
+ location, _ := exec.LookPath("azd")
+
+ return []string{location, "/usr/local/bin/azd"}
+}
+
+func (t *AzureDeveloperCli) installText() string {
+ return `Install the Azure Developer CLI:
+
+ brew tap azure/azd && brew install azd
+
+More information can be found here:
+
+ https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd?pivots=os-mac`
+}
diff --git a/internal/tools/tool/azd_linux.go b/internal/tools/tool/azd_linux.go
new file mode 100644
index 00000000..dab20ec0
--- /dev/null
+++ b/internal/tools/tool/azd_linux.go
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+import "os/exec"
+
+func (t *AzureDeveloperCli) searchLocations() []string {
+ location, _ := exec.LookPath("azd")
+
+ return []string{location, "/usr/local/bin/azd"}
+}
+
+func (t *AzureDeveloperCli) installText() string {
+ return `Install the Azure Developer CLI:
+
+ TODO:
+
+More information can be found here:
+
+ TODO: https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd?pivots=os-mac`
+}
diff --git a/internal/tools/tool/azd_windows.go b/internal/tools/tool/azd_windows.go
new file mode 100644
index 00000000..aacaa8b9
--- /dev/null
+++ b/internal/tools/tool/azd_windows.go
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+import (
+ "os"
+ "os/exec"
+ "path/filepath"
+)
+
+// Search in this order
+//
+// User Insiders Install
+// System Insiders Install
+// User non-Insiders install
+// System non-Insiders install
+func (t *AzureDeveloperCli) searchLocations() []string {
+ userProfile := os.Getenv("USERPROFILE")
+ programFiles := os.Getenv("ProgramFiles")
+
+ location, _ := exec.LookPath("azd")
+
+ var locations []string
+ if location != "" {
+ locations = append(locations, location)
+ }
+
+ locations = append(locations, filepath.Join(userProfile, "AppData\\Local\\Programs\\Azure Dev CLI\\azd.exe"))
+ locations = append(locations, filepath.Join(programFiles, "Azure Dev CLI\\azd.exe"))
+
+ return locations
+}
+
+func (t *AzureDeveloperCli) installText() string {
+ return `Install the Azure Developer CLI:
+
+ winget install Microsoft.Azd
+
+More information can be found here:
+
+ https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd`
+}
diff --git a/internal/tools/tool/interface.go b/internal/tools/tool/interface.go
index a8910175..26f114db 100644
--- a/internal/tools/tool/interface.go
+++ b/internal/tools/tool/interface.go
@@ -6,7 +6,11 @@ package tool
type Tool interface {
Init()
Name() (name string)
- Run(args []string) (exitCode int, err error)
+ Run(args []string, options RunOptions) (exitCode int, err error)
IsInstalled() bool
HowToInstall() string
}
+
+type RunOptions struct {
+ Interactive bool
+}
diff --git a/internal/tools/tool/ssms.go b/internal/tools/tool/ssms.go
new file mode 100644
index 00000000..b73a5d55
--- /dev/null
+++ b/internal/tools/tool/ssms.go
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/io/file"
+ "github.com/microsoft/go-sqlcmd/internal/test"
+)
+
+type SqlServerManagementStudio struct {
+ tool
+}
+
+func (t *SqlServerManagementStudio) Init() {
+ t.tool.SetToolDescription(Description{
+ Name: "ssms",
+ Purpose: "Sql Server Management Studio is a tool for managing SQL Server instances",
+ InstallText: t.installText()})
+
+ for _, location := range t.searchLocations() {
+ if file.Exists(location) {
+ t.tool.SetExePathAndName(location)
+ break
+ }
+ }
+}
+
+func (t *SqlServerManagementStudio) Run(args []string, options RunOptions) (int, error) {
+ if !test.IsRunningInTestExecutor() {
+ return t.tool.Run(args, options)
+ } else {
+ return 0, nil
+ }
+}
diff --git a/internal/tools/tool/ssms_darwin.go b/internal/tools/tool/ssms_darwin.go
new file mode 100644
index 00000000..9619bd94
--- /dev/null
+++ b/internal/tools/tool/ssms_darwin.go
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+// Search in this order
+//
+// User Insiders Install
+// System Insiders Install
+// User non-Insiders install
+// System non-Insiders install
+func (t *SqlServerManagementStudio) searchLocations() []string {
+
+ return []string{}
+}
+
+func (t *SqlServerManagementStudio) installText() string {
+ return `SSMS cannot be installed on this platform. It is only available on Microsoft Windows`
+}
diff --git a/internal/tools/tool/ssms_linux.go b/internal/tools/tool/ssms_linux.go
new file mode 100644
index 00000000..9619bd94
--- /dev/null
+++ b/internal/tools/tool/ssms_linux.go
@@ -0,0 +1,19 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+// Search in this order
+//
+// User Insiders Install
+// System Insiders Install
+// User non-Insiders install
+// System non-Insiders install
+func (t *SqlServerManagementStudio) searchLocations() []string {
+
+ return []string{}
+}
+
+func (t *SqlServerManagementStudio) installText() string {
+ return `SSMS cannot be installed on this platform. It is only available on Microsoft Windows`
+}
diff --git a/internal/tools/tool/ssms_windows.go b/internal/tools/tool/ssms_windows.go
new file mode 100644
index 00000000..cb7ac15e
--- /dev/null
+++ b/internal/tools/tool/ssms_windows.go
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+import (
+ "os"
+ "path/filepath"
+)
+
+// Search in this order
+//
+// User Insiders Install
+// System Insiders Install
+// User non-Insiders install
+// System non-Insiders install
+func (t *SqlServerManagementStudio) searchLocations() []string {
+ programFiles := os.Getenv("ProgramFiles(x86)")
+
+ // BUGBUG: Go looking in the registry for where SSMS is
+
+ // C:\Program Files (x86)\Microsoft SQL Server Management Studio 19\Common7\IDE
+ return []string{
+ filepath.Join(programFiles, "Microsoft SQL Server Management Studio 19\\Common7\\IDE\\ssms.exe"),
+ }
+}
+
+func (t *SqlServerManagementStudio) installText() string {
+ return `Download the latest 'User Installer' .msi from:
+
+ https://go.microsoft.com/fwlink/?linkid=2150927
+
+More information can be found here:
+
+ https://docs.microsoft.com/sql/azure-data-studio/download-azure-data-studio#get-azure-data-studio-for-windows`
+}
diff --git a/internal/tools/tool/tool.go b/internal/tools/tool/tool.go
index ee4d5db4..ea29231d 100644
--- a/internal/tools/tool/tool.go
+++ b/internal/tools/tool/tool.go
@@ -5,6 +5,7 @@ package tool
import (
"fmt"
+ "os"
"strings"
"github.com/microsoft/go-sqlcmd/internal/io/file"
@@ -32,7 +33,8 @@ func (t *tool) IsInstalled() bool {
}
t.installed = new(bool)
- if file.Exists(t.exeName) {
+
+ if t.exeName != "" && file.Exists(t.exeName) {
*t.installed = true
} else {
*t.installed = false
@@ -52,13 +54,25 @@ func (t *tool) HowToInstall() string {
return sb.String()
}
-func (t *tool) Run(args []string) (int, error) {
+func (t *tool) Run(args []string, options RunOptions) (int, error) {
if t.installed == nil {
panic("Call IsInstalled before Run")
}
cmd := t.generateCommandLine(args)
+
+ if options.Interactive {
+ cmd.Stdin = os.Stdin
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+ }
+
err := cmd.Run()
+ if cmd.ProcessState.ExitCode() != 0 {
+ fmt.Println(cmd.Stdout)
+ fmt.Println(cmd.Stderr)
+ }
+
return cmd.ProcessState.ExitCode(), err
}
diff --git a/internal/tools/tool/tool_darwin.go b/internal/tools/tool/tool_darwin.go
index 5fed4adf..6c4b8482 100644
--- a/internal/tools/tool/tool_darwin.go
+++ b/internal/tools/tool/tool_darwin.go
@@ -9,11 +9,19 @@ import (
)
func (t *tool) generateCommandLine(args []string) *exec.Cmd {
- path, _ := exec.LookPath("open")
+ path := t.exeName
- args = append([]string{"--args"}, args...)
- args = append([]string{t.exeName}, args...)
- args = append([]string{"-a"}, args...)
+ // BUGBUG: Move ads specific code to the ads tool
+ if t.Name() == "ads" {
+ path, _ = exec.LookPath("open")
+
+ args = append([]string{"--args"}, args...)
+ args = append([]string{t.exeName}, args...)
+ args = append([]string{"-a"}, args...)
+ }
+
+ // BUGBUG: Why is this needed?
+ args = append([]string{"."}, args...)
var stdout, stderr bytes.Buffer
cmd := &exec.Cmd{
diff --git a/internal/tools/tool/tool_windows.go b/internal/tools/tool/tool_windows.go
index 3e3aeaa5..ceacc641 100644
--- a/internal/tools/tool/tool_windows.go
+++ b/internal/tools/tool/tool_windows.go
@@ -10,6 +10,11 @@ import (
func (t *tool) generateCommandLine(args []string) *exec.Cmd {
var stdout, stderr bytes.Buffer
+
+ // BUGBUG: Why does Cmd ignore the first arg!! (hence I am stuffing it
+ // appropriately with 'foobar'
+ args = append([]string{"foobar"}, args...)
+
cmd := &exec.Cmd{
Path: t.exeName,
Args: args,
diff --git a/internal/tools/tool/vscode.go b/internal/tools/tool/vscode.go
new file mode 100644
index 00000000..b935eb79
--- /dev/null
+++ b/internal/tools/tool/vscode.go
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/test"
+ "os"
+ "os/exec"
+ "runtime"
+)
+
+type VisualStudioCode struct {
+ tool
+}
+
+func (t *VisualStudioCode) Init() {
+ t.tool.SetToolDescription(Description{
+ Name: "vscode",
+ Purpose: "Visual Studio Code is a tool for editing files",
+ InstallText: t.installText()})
+
+ if runtime.GOOS == "windows" {
+ comspec := os.Getenv("COMSPEC")
+
+ t.tool.SetExePathAndName(comspec)
+ } else {
+ binary, err := exec.LookPath("code")
+
+ if err != nil {
+ t.tool.SetExePathAndName(binary)
+ }
+ }
+
+}
+
+func (t *VisualStudioCode) Run(args []string, options RunOptions) (int, error) {
+
+ if runtime.GOOS == "windows" {
+ args = append([]string{"/c", "code"}, args...)
+ }
+
+ if !test.IsRunningInTestExecutor() {
+ return t.tool.Run(args, options)
+ } else {
+ return 0, nil
+ }
+}
diff --git a/internal/tools/tool/vscode_darwin.go b/internal/tools/tool/vscode_darwin.go
new file mode 100644
index 00000000..0f9b5cd9
--- /dev/null
+++ b/internal/tools/tool/vscode_darwin.go
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+// Search in this order
+//
+// User Insiders Install
+// System Insiders Install
+// User non-Insiders install
+// System non-Insiders install
+func (t *VisualStudioCode) searchLocations() []string {
+
+ return []string{}
+}
+
+func (t *VisualStudioCode) installText() string {
+ return `Download the latest installer:
+
+ TODO: Add instructions here
+
+More information can be found here:
+
+ TODO: https://docs.microsoft.com/sql/azure-data-studio/download-azure-data-studio#get-azure-data-studio-for-windows`
+}
diff --git a/internal/tools/tool/vscode_linux.go b/internal/tools/tool/vscode_linux.go
new file mode 100644
index 00000000..0f9b5cd9
--- /dev/null
+++ b/internal/tools/tool/vscode_linux.go
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+// Search in this order
+//
+// User Insiders Install
+// System Insiders Install
+// User non-Insiders install
+// System non-Insiders install
+func (t *VisualStudioCode) searchLocations() []string {
+
+ return []string{}
+}
+
+func (t *VisualStudioCode) installText() string {
+ return `Download the latest installer:
+
+ TODO: Add instructions here
+
+More information can be found here:
+
+ TODO: https://docs.microsoft.com/sql/azure-data-studio/download-azure-data-studio#get-azure-data-studio-for-windows`
+}
diff --git a/internal/tools/tool/vscode_windows.go b/internal/tools/tool/vscode_windows.go
new file mode 100644
index 00000000..bc4993f4
--- /dev/null
+++ b/internal/tools/tool/vscode_windows.go
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package tool
+
+import (
+ "os"
+ "path/filepath"
+)
+
+// Search in this order
+//
+// User Insiders Install
+// System Insiders Install
+// User non-Insiders install
+// System non-Insiders install
+func (t *VisualStudioCode) searchLocations() []string {
+ userProfile := os.Getenv("USERPROFILE")
+ programFiles := os.Getenv("ProgramFiles")
+
+ return []string{
+ filepath.Join(userProfile, "AppData\\Local\\Programs\\Azure Data Studio\\azuredatastudio.exe"),
+ filepath.Join(programFiles, "Azure Data Studio\\azuredatastudio.exe"),
+ filepath.Join(userProfile, "AppData\\Local\\Programs\\Azure Data Studio - Insiders\\azuredatastudio-insiders.exe"),
+ filepath.Join(programFiles, "Azure Data Studio - Insiders\\azuredatastudio-insiders.exe"),
+ }
+}
+
+func (t *VisualStudioCode) installText() string {
+ return `Download the latest 'User Installer' .msi from:
+
+ https://go.microsoft.com/fwlink/?linkid=2150927
+
+More information can be found here:
+
+ https://docs.microsoft.com/sql/azure-data-studio/download-azure-data-studio#get-azure-data-studio-for-windows`
+}
diff --git a/internal/tools/tools.go b/internal/tools/tools.go
index d60d7fee..cbf07fa9 100644
--- a/internal/tools/tools.go
+++ b/internal/tools/tools.go
@@ -9,4 +9,7 @@ import (
var tools = []tool.Tool{
&tool.AzureDataStudio{},
+ &tool.AzureDeveloperCli{},
+ &tool.SqlServerManagementStudio{},
+ &tool.VisualStudioCode{},
}
diff --git a/pkg/mssqlcontainer/ingest/extract/7zip.go b/pkg/mssqlcontainer/ingest/extract/7zip.go
new file mode 100644
index 00000000..06ee6488
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/extract/7zip.go
@@ -0,0 +1,96 @@
+package extract
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "path/filepath"
+ "regexp"
+)
+
+type sevenZip struct {
+ controller *container.Controller
+ containerId string
+}
+
+func (e *sevenZip) Initialize(controller *container.Controller) {
+ e.controller = controller
+}
+
+func (e *sevenZip) FileTypes() []string {
+ return []string{"7z"}
+}
+
+func (e *sevenZip) IsInstalled(containerId string) bool {
+ e.containerId = containerId
+
+ return false
+}
+
+func (e *sevenZip) Extract(srcFile string, destFolder string) (string, string) {
+ e.controller.RunCmdInContainer(e.containerId, []string{
+ "/opt/7-zip/7zz",
+ "x",
+ "-aoa",
+ "-o" + destFolder,
+ "/var/opt/mssql/backup/" + srcFile,
+ }, container.ExecOptions{})
+
+ stdout, _, _ := e.controller.RunCmdInContainer(e.containerId, []string{
+ "./opt/7-zip/7zz",
+ "l",
+ "-ba",
+ "-slt",
+ "/var/opt/mssql/backup/" + srcFile,
+ }, container.ExecOptions{})
+
+ var mdfFile string
+ var ldfFile string
+
+ paths := extractPaths(string(stdout))
+ for _, p := range paths {
+ if filepath.Ext(p) == ".mdf" {
+ mdfFile = p
+ }
+
+ if filepath.Ext(p) == ".ldf" {
+ ldfFile = p
+ }
+ }
+
+ return mdfFile, ldfFile
+}
+
+func (e *sevenZip) Install() {
+ e.controller.RunCmdInContainer(e.containerId, []string{
+ "mkdir",
+ "/opt/7-zip"}, container.ExecOptions{})
+
+ e.controller.RunCmdInContainer(e.containerId, []string{
+ "wget",
+ "-O",
+ "/opt/7-zip/7-zip.tar",
+ "https://7-zip.org/a/7z2201-linux-x64.tar.xz"}, container.ExecOptions{})
+
+ e.controller.RunCmdInContainer(e.containerId, []string{
+ "tar",
+ "xvf",
+ "/opt/7-zip/7-zip.tar",
+ "-C",
+ "/opt/7-zip",
+ }, container.ExecOptions{})
+
+ e.controller.RunCmdInContainer(e.containerId, []string{
+ "chmod",
+ "u+x",
+ "/opt/7-zip/7zz",
+ }, container.ExecOptions{})
+}
+
+func extractPaths(input string) []string {
+ re := regexp.MustCompile(`Path\s*=\s*(\S+)`)
+ matches := re.FindAllStringSubmatch(input, -1)
+ var paths []string
+ for _, match := range matches {
+ paths = append(paths, match[1])
+ }
+ return paths
+}
diff --git a/pkg/mssqlcontainer/ingest/extract/7zip_test.go b/pkg/mssqlcontainer/ingest/extract/7zip_test.go
new file mode 100644
index 00000000..a91273f8
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/extract/7zip_test.go
@@ -0,0 +1,42 @@
+package extract
+
+import (
+ "fmt"
+ "testing"
+)
+
+func TestTzOutput(t *testing.T) {
+ stdout := `Path = Readme_2010.txt
+Size = 1157
+Packed Size = 680
+Modified = 2018-09-11 11:45:55.2543593
+Attributes = A
+CRC = B243D895
+Encrypted = -
+Method = LZMA2:27
+Block = 0
+
+Path = StackOverflow2010.mdf
+Size = 8980398080
+Packed Size = 1130813973
+Modified = 2018-09-11 11:30:55.3142494
+Attributes = A
+CRC = 8D688B2A
+Encrypted = -
+Method = LZMA2:27
+Block = 1
+
+Path = StackOverflow2010_log.ldf
+Size = 268312576
+Packed Size = 37193161
+Modified = 2018-09-11 11:30:55.3152489
+Attributes = A
+CRC = BCA9F91F
+Encrypted = -
+Method = LZMA2:27
+Block = 2`
+
+ paths := extractPaths(stdout)
+
+ fmt.Println(paths)
+}
diff --git a/pkg/mssqlcontainer/ingest/extract/extract.go b/pkg/mssqlcontainer/ingest/extract/extract.go
new file mode 100644
index 00000000..35b90448
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/extract/extract.go
@@ -0,0 +1,6 @@
+package extract
+
+var extractors = []Extractor{
+ &tar{},
+ &sevenZip{},
+}
diff --git a/pkg/mssqlcontainer/ingest/extract/factory.go b/pkg/mssqlcontainer/ingest/extract/factory.go
new file mode 100644
index 00000000..f1a1dec0
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/extract/factory.go
@@ -0,0 +1,27 @@
+package extract
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/container"
+)
+
+func NewExtractor(fileExtension string, controller *container.Controller) Extractor {
+ for _, extractor := range extractors {
+ for _, ext := range extractor.FileTypes() {
+ if ext == fileExtension {
+ extractor.Initialize(controller)
+ return extractor
+ }
+ }
+ }
+ return nil
+}
+
+func FileTypes() []string {
+ types := []string{}
+ for _, extractor := range extractors {
+ for _, ext := range extractor.FileTypes() {
+ types = append(types, ext)
+ }
+ }
+ return types
+}
diff --git a/pkg/mssqlcontainer/ingest/extract/interface.go b/pkg/mssqlcontainer/ingest/extract/interface.go
new file mode 100644
index 00000000..19a91ef9
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/extract/interface.go
@@ -0,0 +1,11 @@
+package extract
+
+import "github.com/microsoft/go-sqlcmd/internal/container"
+
+type Extractor interface {
+ FileTypes() []string
+ Initialize(controller *container.Controller)
+ IsInstalled(containerId string) bool
+ Install()
+ Extract(srcFile string, destFolder string) (filename string, ldfFilename string)
+}
diff --git a/pkg/mssqlcontainer/ingest/extract/tar.go b/pkg/mssqlcontainer/ingest/extract/tar.go
new file mode 100644
index 00000000..88daf97b
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/extract/tar.go
@@ -0,0 +1,26 @@
+package extract
+
+import "github.com/microsoft/go-sqlcmd/internal/container"
+
+type tar struct {
+ controller *container.Controller
+}
+
+func (e *tar) FileTypes() []string {
+ return []string{"tar"}
+}
+
+func (e *tar) Initialize(controller *container.Controller) {
+ e.controller = controller
+}
+
+func (e *tar) IsInstalled(containerId string) bool {
+ return true
+}
+
+func (e *tar) Extract(srcFile string, destFolder string) (string, string) {
+ return "", ""
+}
+
+func (e *tar) Install() {
+}
diff --git a/pkg/mssqlcontainer/ingest/factory.go b/pkg/mssqlcontainer/ingest/factory.go
new file mode 100644
index 00000000..cbbe418f
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/factory.go
@@ -0,0 +1,38 @@
+package ingest
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "github.com/microsoft/go-sqlcmd/internal/databaseurl"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest/extract"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest/location"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest/mechanism"
+ "strings"
+)
+
+func NewIngest(databaseUrl string, controller *container.Controller, options IngestOptions) Ingest {
+ url := databaseurl.NewDatabaseUrl(databaseUrl)
+ if options.DatabaseName != "" {
+ url.DatabaseName = options.DatabaseName
+ }
+
+ return &ingest{
+ url: url,
+ controller: controller,
+ location: location.NewLocation(url.IsLocal, url.String(), controller),
+ mechanism: mechanism.NewMechanism(url.FileExtension, options.Mechanism, controller),
+ }
+}
+
+func ValidFileExtensions() string {
+ var extensions []string
+
+ for _, m := range mechanism.FileTypes() {
+ extensions = append(extensions, m)
+ }
+
+ for _, e := range extract.FileTypes() {
+ extensions = append(extensions, e)
+ }
+
+ return strings.Join(extensions, ", ")
+}
diff --git a/pkg/mssqlcontainer/ingest/ingest.go b/pkg/mssqlcontainer/ingest/ingest.go
new file mode 100644
index 00000000..0ed0f7ce
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/ingest.go
@@ -0,0 +1,157 @@
+package ingest
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "github.com/microsoft/go-sqlcmd/internal/databaseurl"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest/extract"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest/location"
+ "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest/mechanism"
+ "path/filepath"
+ "strings"
+)
+
+type ingest struct {
+ url *databaseurl.DatabaseUrl
+ location location.Location
+ controller *container.Controller
+ mechanism mechanism.Mechanism
+ options mechanism.BringOnlineOptions
+ extractor extract.Extractor
+ containerId string
+ query func(text string)
+}
+
+func (i *ingest) IsExtractionNeeded() bool {
+ i.extractor = extract.NewExtractor(i.url.FileExtension, i.controller)
+ if i.extractor == nil {
+ return false
+ } else {
+ return true
+ }
+}
+
+func (i *ingest) IsRemoteUrl() bool {
+ return !i.location.IsLocal()
+}
+
+func (i *ingest) UrlFilename() string {
+ return i.url.Filename
+}
+
+func (i *ingest) OnlineMethod() string {
+ return i.mechanism.Name()
+}
+
+func (i *ingest) DatabaseName() string {
+ return i.url.DatabaseName
+}
+
+func (i *ingest) IsValidScheme() bool {
+ for _, s := range i.location.ValidSchemes() {
+ if s == i.url.Scheme {
+ return true
+ }
+ }
+ return false
+}
+
+func (i *ingest) CopyToContainer(containerId string) {
+ destFolder := "/var/opt/mssql/backup"
+
+ if i.mechanism != nil {
+ destFolder = i.mechanism.CopyToLocation()
+ }
+ if i.location == nil {
+ panic("location is nil, did you call NewIngest()?")
+ }
+
+ i.containerId = containerId
+ i.location.CopyToContainer(containerId, destFolder)
+ i.options.Filename = i.url.Filename
+
+ if i.options.Filename == "" {
+ panic("filename is empty")
+ }
+}
+
+func (i *ingest) Extract() {
+ if i.extractor == nil {
+ panic("extractor is nil")
+ }
+
+ if !i.extractor.IsInstalled(i.containerId) {
+ i.extractor.Install()
+ }
+
+ i.options.Filename, i.options.LdfFilename =
+ i.extractor.Extract(i.url.Filename, "/var/opt/mssql/data")
+
+ if i.mechanism == nil {
+ ext := strings.TrimLeft(filepath.Ext(i.options.Filename), ".")
+ i.mechanism = mechanism.NewMechanismByFileExt(ext, i.controller)
+ }
+}
+
+func (i *ingest) BringOnline(query func(string), username string, password string) {
+ if i.mechanism.Name() != "git" {
+ if i.options.Filename == "" {
+ panic("filename is empty, did you call CopyToContainer()?")
+ }
+ if query == nil {
+ panic("query is nil")
+ }
+ } else {
+ i.options.Filename = i.url.String()
+ }
+ if i.mechanism == nil {
+ panic("mechanism is nil")
+ }
+
+ i.query = query
+ i.options.Username = username
+ i.options.Password = password
+ i.mechanism.BringOnline(i.url.DatabaseNameAsTsqlIdentifier, i.containerId, i.query, i.options)
+
+ if i.mechanism.Name() != "git" {
+ i.setDefaultDatabase(username)
+ }
+}
+
+func (i *ingest) setDefaultDatabase(username string) {
+ if i.query == nil {
+ panic("query is nil, did you call BringOnline()?")
+ }
+
+ alterDefaultDb := fmt.Sprintf(
+ "ALTER LOGIN [%s] WITH DEFAULT_DATABASE = [%s]",
+ username,
+ i.url.DatabaseNameAsNonTsqlIdentifier)
+ i.query(alterDefaultDb)
+}
+
+func (i *ingest) IsValidFileExtension() bool {
+ for _, m := range mechanism.FileTypes() {
+ if m == i.url.FileExtension {
+ return true
+ }
+ }
+ for _, e := range extract.FileTypes() {
+ if e == i.url.FileExtension {
+ return true
+ }
+ }
+ return false
+}
+
+func (i *ingest) SourceFileExists() bool {
+ return i.location.Exists()
+}
+
+func (i *ingest) UserProvidedFileExt() string {
+ return i.url.FileExtension
+}
+
+func (i *ingest) ValidSchemes() []string {
+ return i.location.ValidSchemes()
+}
diff --git a/pkg/mssqlcontainer/ingest/interface.go b/pkg/mssqlcontainer/ingest/interface.go
new file mode 100644
index 00000000..a8c88af0
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/interface.go
@@ -0,0 +1,20 @@
+package ingest
+
+type Ingest interface {
+ IsRemoteUrl() bool
+ IsValidScheme() bool
+ IsValidFileExtension() bool
+ IsExtractionNeeded() bool
+
+ SourceFileExists() bool
+ DatabaseName() string
+ UrlFilename() string
+ OnlineMethod() string
+ UserProvidedFileExt() string
+
+ CopyToContainer(containerId string)
+ Extract()
+ BringOnline(query func(string), username string, password string)
+
+ ValidSchemes() []string
+}
diff --git a/pkg/mssqlcontainer/ingest/location/factory.go b/pkg/mssqlcontainer/ingest/location/factory.go
new file mode 100644
index 00000000..4ccb4768
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/location/factory.go
@@ -0,0 +1,19 @@
+package location
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/container"
+)
+
+func NewLocation(isLocal bool, uri string, controller *container.Controller) Location {
+ if isLocal {
+ return local{
+ uri: uri,
+ controller: controller,
+ }
+ } else {
+ return remote{
+ uri: uri,
+ controller: controller,
+ }
+ }
+}
diff --git a/pkg/mssqlcontainer/ingest/location/interface.go b/pkg/mssqlcontainer/ingest/location/interface.go
new file mode 100644
index 00000000..b101b054
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/location/interface.go
@@ -0,0 +1,8 @@
+package location
+
+type Location interface {
+ Exists() bool
+ IsLocal() bool
+ CopyToContainer(containerId string, destFolder string)
+ ValidSchemes() []string
+}
diff --git a/pkg/mssqlcontainer/ingest/location/local.go b/pkg/mssqlcontainer/ingest/location/local.go
new file mode 100644
index 00000000..215085c4
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/location/local.go
@@ -0,0 +1,51 @@
+package location
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "github.com/microsoft/go-sqlcmd/internal/io/file"
+ "path/filepath"
+)
+
+type local struct {
+ uri string
+ controller *container.Controller
+}
+
+func (l local) Exists() bool {
+ return file.Exists(l.uri)
+}
+
+func (l local) IsLocal() bool {
+ return true
+}
+
+func (l local) ValidSchemes() []string {
+ return []string{"file"}
+}
+
+func (l local) CopyToContainer(containerId string, destFolder string) {
+ l.controller.RunCmdInContainer(
+ containerId,
+ []string{"mkdir", "-p", destFolder},
+ container.ExecOptions{User: "root"})
+
+ l.controller.CopyFile(
+ containerId,
+ l.uri,
+ destFolder,
+ )
+
+ _, filename := filepath.Split(l.uri)
+
+ l.controller.RunCmdInContainer(
+ containerId,
+ []string{"chown", "mssql:root", destFolder + "/" + filename},
+ container.ExecOptions{User: "root"},
+ )
+
+ l.controller.RunCmdInContainer(
+ containerId,
+ []string{"chmod", "-o-r-u+rw-g+r", destFolder + "/" + filename},
+ container.ExecOptions{User: "root"},
+ )
+}
diff --git a/pkg/mssqlcontainer/ingest/location/remote.go b/pkg/mssqlcontainer/ingest/location/remote.go
new file mode 100644
index 00000000..23c5dfbc
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/location/remote.go
@@ -0,0 +1,32 @@
+package location
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "github.com/microsoft/go-sqlcmd/internal/http"
+)
+
+type remote struct {
+ uri string
+ controller *container.Controller
+}
+
+func (l remote) IsLocal() bool {
+ return false
+}
+
+func (l remote) ValidSchemes() []string {
+ return []string{"https", "http"}
+}
+
+// Verify the file exists at the URL
+func (l remote) Exists() bool {
+ return http.UrlExists(l.uri)
+}
+
+func (l remote) CopyToContainer(containerId string, destFolder string) {
+ l.controller.DownloadFile(
+ containerId,
+ l.uri,
+ destFolder,
+ )
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/attach.go b/pkg/mssqlcontainer/ingest/mechanism/attach.go
new file mode 100644
index 00000000..d6803211
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/attach.go
@@ -0,0 +1,68 @@
+package mechanism
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/container"
+)
+
+type attach struct {
+ controller *container.Controller
+ containerId string
+}
+
+func (m *attach) Initialize(controller *container.Controller) {
+ m.controller = controller
+}
+
+func (m *attach) CopyToLocation() string {
+ return "/var/opt/mssql/data"
+}
+
+func (m *attach) Name() string {
+ return "attach"
+}
+
+func (m *attach) FileTypes() []string {
+ return []string{"mdf"}
+}
+
+func (m *attach) BringOnline(
+ databaseName string,
+ containerId string,
+ query func(string),
+ options BringOnlineOptions,
+) {
+ text := `SET NOCOUNT ON; `
+
+ m.containerId = containerId
+ m.setFilePermissions(m.CopyToLocation() + "/" + options.Filename)
+ if options.LdfFilename == "" {
+ text += `CREATE DATABASE [%s] ON (FILENAME = '%s/%s') FOR ATTACH;`
+ query(fmt.Sprintf(
+ text,
+ databaseName,
+ m.CopyToLocation(),
+ options.Filename,
+ ))
+ } else {
+ m.setFilePermissions(m.CopyToLocation() + "/" + options.LdfFilename)
+ text += `CREATE DATABASE [%s] ON (FILENAME = '%s/%s'), (FILENAME = '%s/%s') FOR ATTACH;`
+ query(fmt.Sprintf(
+ text,
+ databaseName,
+ m.CopyToLocation(),
+ options.Filename,
+ m.CopyToLocation(),
+ options.LdfFilename,
+ ))
+ }
+}
+
+func (m *attach) setFilePermissions(filename string) {
+ m.RunCommand([]string{"chown", "mssql:root", filename})
+ m.RunCommand([]string{"chmod", "-o-r-u+rw-g+r", filename})
+}
+
+func (m *attach) RunCommand(s []string) ([]byte, []byte, int) {
+ return m.controller.RunCmdInContainer(m.containerId, s, container.ExecOptions{})
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/dacfx.go b/pkg/mssqlcontainer/ingest/mechanism/dacfx.go
new file mode 100644
index 00000000..e0039bd8
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/dacfx.go
@@ -0,0 +1,119 @@
+package mechanism
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/container"
+)
+
+type dacfx struct {
+ controller *container.Controller
+ containerId string
+}
+
+func (m *dacfx) Initialize(controller *container.Controller) {
+ m.controller = controller
+}
+
+func (m *dacfx) CopyToLocation() string {
+ return "/var/opt/mssql/backup"
+}
+
+func (m *dacfx) Name() string {
+ return "dacfx"
+}
+
+func (m *dacfx) FileTypes() []string {
+ return []string{"bacpac", "dacpac"}
+}
+
+func (m *dacfx) BringOnline(
+ databaseName string,
+ containerId string,
+ query func(string),
+ options BringOnlineOptions,
+) {
+ m.containerId = containerId
+ m.installSqlPackage()
+ m.setDefaultDatabaseToMaster(options.Username, query)
+
+ _, stderr, _ := m.RunCommand([]string{
+ "/home/mssql/.dotnet/tools/sqlpackage",
+ "/Diagnostics:true",
+ "/Action:import",
+ "/SourceFile:" + m.CopyToLocation() + "/" + options.Filename,
+ "/TargetServerName:localhost",
+ "/TargetDatabaseName:" + databaseName,
+ "/TargetTrustServerCertificate:true",
+ "/TargetUser:" + options.Username,
+ "/TargetPassword:" + options.Password,
+ })
+
+ if len(stderr) == 0 {
+ // Remove the source bacpac file
+ m.RunCommandAsRoot([]string{"rm", m.CopyToLocation() + "/" + options.Filename})
+ }
+}
+
+func (m *dacfx) setDefaultDatabaseToMaster(username string, query func(string)) {
+ alterDefaultDb := fmt.Sprintf(
+ "ALTER LOGIN [%s] WITH DEFAULT_DATABASE = [%s]",
+ username,
+ "master")
+ query(alterDefaultDb)
+}
+
+func (m *dacfx) installSqlPackage() {
+ if m.controller == nil {
+ panic("controller is nil")
+ }
+
+ m.installDotNet()
+
+ // Check if sqlpackage is installed, if not, install it
+ _, stderr, _ := m.RunCommand([]string{"/home/mssql/.dotnet/tools/sqlpackage", "/version"})
+ if len(stderr) > 0 {
+ m.RunCommand([]string{"/opt/dotnet/dotnet", "tool", "install", "-g", "microsoft.sqlpackage"})
+ }
+}
+
+func (m *dacfx) installDotNet() {
+ // Check if dotnet is installed, if not, install it
+ _, stderr, _ := m.RunCommand([]string{"/opt/dotnet/dotnet", "--version"})
+ if len(stderr) > 0 {
+ // Download dotnet-install.sh and run it
+ m.RunCommand([]string{"wget", "https://dot.net/v1/dotnet-install.sh", "-O", "/tmp/dotnet-install.sh"})
+ m.RunCommand([]string{"chmod", "+x", "/tmp/dotnet-install.sh"})
+ m.RunCommand([]string{"/tmp/dotnet-install.sh", "--install-dir", "/opt/dotnet"})
+
+ // The SQL Server container doesn't have a /home/mssql directory (which is ~), this
+ // causes all sorts of things to break in the container that expect to create .toolname folders
+ m.RunCommandAsRoot([]string{"mkdir", "-p", "/home/mssql"})
+ m.RunCommandAsRoot([]string{"chown", "mssql:root", "/home/mssql"})
+
+ // Add dotnet to the path
+ m.AddTextLineToFile(
+ "export DOTNET_ROOT=/opt/dotnet",
+ "/home/mssql/.bashrc",
+ )
+ m.AddTextLineToFile(
+ "export PATH=$PATH:$DOTNET_ROOT:/home/mssql/.dotnet/tools",
+ "/home/mssql/.bashrc",
+ )
+ }
+}
+
+func (m *dacfx) AddTextLineToFile(text string, file string) ([]byte, []byte, int) {
+ return m.RunCommand([]string{"/bin/bash", "-c", fmt.Sprintf("echo '%v' >> %v", text, file)})
+}
+
+func (m *dacfx) RunCommand(s []string) ([]byte, []byte, int) {
+ return m.controller.RunCmdInContainer(m.containerId, s, container.ExecOptions{
+ Env: []string{"DOTNET_ROOT=/opt/dotnet"},
+ })
+}
+
+func (m *dacfx) RunCommandAsRoot(s []string) ([]byte, []byte, int) {
+ return m.controller.RunCmdInContainer(m.containerId, s, container.ExecOptions{
+ User: "root",
+ })
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/error.go b/pkg/mssqlcontainer/ingest/mechanism/error.go
new file mode 100644
index 00000000..1ad16cc3
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/error.go
@@ -0,0 +1,7 @@
+package mechanism
+
+var errorCallback func(err error)
+
+func checkErr(err error) {
+ errorCallback(err)
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/factory.go b/pkg/mssqlcontainer/ingest/mechanism/factory.go
new file mode 100644
index 00000000..dc888772
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/factory.go
@@ -0,0 +1,46 @@
+package mechanism
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/container"
+)
+
+func NewMechanism(fileExtension string, name string, controller *container.Controller) Mechanism {
+ trace("NewMechanism: fileExtension = %q, name = %q", fileExtension, name)
+ for _, m := range mechanisms {
+ if m.Name() == name {
+ m.Initialize(controller)
+
+ trace("Returning: %q", m.Name())
+
+ return m
+ }
+ }
+
+ return NewMechanismByFileExt(fileExtension, controller)
+}
+
+func NewMechanismByFileExt(fileExtension string, controller *container.Controller) Mechanism {
+ for _, m := range mechanisms {
+ for _, ext := range m.FileTypes() {
+ if ext == fileExtension {
+ m.Initialize(controller)
+
+ trace("Returning: %q", m.Name())
+
+ return m
+ }
+ }
+ }
+
+ trace("No mechanism found for file extension %q", fileExtension)
+
+ return nil
+}
+
+func Mechanisms() []string {
+ m := []string{}
+ for _, mechanism := range mechanisms {
+ m = append(m, mechanism.Name())
+ }
+ return m
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/git.go b/pkg/mssqlcontainer/ingest/mechanism/git.go
new file mode 100644
index 00000000..c151e87c
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/git.go
@@ -0,0 +1,93 @@
+package mechanism
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/container"
+ "gopkg.in/src-d/go-billy.v4"
+ "gopkg.in/src-d/go-billy.v4/osfs"
+ "gopkg.in/src-d/go-git.v4/storage/filesystem"
+ "os"
+
+ git "gopkg.in/src-d/go-git.v4"
+)
+
+type git2 struct {
+}
+
+func (m *git2) Initialize(controller *container.Controller) {
+}
+
+func (m *git2) CopyToLocation() string {
+ return "/var/opt/mssql/backup"
+}
+
+func (m *git2) Name() string {
+ return "git"
+}
+
+func (m *git2) FileTypes() []string {
+ return []string{"git"}
+}
+
+func (m *git2) BringOnline(databaseName string, _ string, query func(string), options BringOnlineOptions) {
+ if options.Filename == "" {
+ panic("Filename is required for git")
+ }
+ /*
+ if databaseName == "" {
+ panic("databaseName is required for git")
+ }
+ */
+
+ url := options.Filename
+ dir := "."
+
+ // If there are any files in the current directory then error
+ // as we don't want to overwrite any files
+ entries, err := os.ReadDir(dir)
+ checkErr(err)
+
+ alreadyCloned := false
+
+ if len(entries) > 0 {
+ if _, err := os.Stat(".git"); err == nil {
+ var fs billy.Filesystem
+ fs = osfs.New(".git")
+
+ st := filesystem.NewStorage(fs, nil)
+ c, err := st.Config()
+ if err != nil {
+ panic(err)
+ }
+
+ for _, remote := range c.Remotes {
+ for _, u := range remote.URLs {
+ if url == u {
+ alreadyCloned = true
+ }
+ }
+ }
+ }
+
+ if !alreadyCloned {
+ fmt.Println("Current directory is not empty, cannot clone .git repo. Run sqlcmd again from an empty directory, or remove the --use switch.")
+
+ os.Exit(1)
+ }
+ }
+
+ if !alreadyCloned {
+ _, err = git.PlainClone(dir, false, &git.CloneOptions{
+ URL: url,
+ })
+
+ if err != nil {
+ fmt.Println("Error while cloning repository:", err)
+ os.Exit(1)
+ }
+
+ fmt.Println("Repository cloned successfully")
+ } else {
+ fmt.Println("Repository already cloned, continuing...")
+ }
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/initialize.go b/pkg/mssqlcontainer/ingest/mechanism/initialize.go
new file mode 100644
index 00000000..e14358b2
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/initialize.go
@@ -0,0 +1,19 @@
+package mechanism
+
+func init() {
+ Initialize(
+ func(err error) {
+ if err != nil {
+ panic(err)
+ }
+ },
+ func(format string, a ...any) {})
+}
+
+func Initialize(
+ errorHandler func(err error),
+ traceHandler func(format string, a ...any)) {
+
+ errorCallback = errorHandler
+ traceCallback = traceHandler
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/interface.go b/pkg/mssqlcontainer/ingest/mechanism/interface.go
new file mode 100644
index 00000000..264fa3e1
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/interface.go
@@ -0,0 +1,11 @@
+package mechanism
+
+import "github.com/microsoft/go-sqlcmd/internal/container"
+
+type Mechanism interface {
+ FileTypes() []string
+ Initialize(controller *container.Controller)
+ CopyToLocation() string
+ BringOnline(databaseName string, containerId string, query func(string), options BringOnlineOptions)
+ Name() string
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/mechanism.go b/pkg/mssqlcontainer/ingest/mechanism/mechanism.go
new file mode 100644
index 00000000..39150ba1
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/mechanism.go
@@ -0,0 +1,17 @@
+package mechanism
+
+var mechanisms = []Mechanism{
+ &attach{},
+ &dacfx{},
+ &git2{},
+ &restore{},
+ &script{},
+}
+
+func FileTypes() []string {
+ fileTypes := []string{}
+ for _, m := range mechanisms {
+ fileTypes = append(fileTypes, m.FileTypes()...)
+ }
+ return fileTypes
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/options.go b/pkg/mssqlcontainer/ingest/mechanism/options.go
new file mode 100644
index 00000000..dad738c0
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/options.go
@@ -0,0 +1,9 @@
+package mechanism
+
+type BringOnlineOptions struct {
+ Username string
+ Password string
+
+ Filename string
+ LdfFilename string
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/restore.go b/pkg/mssqlcontainer/ingest/mechanism/restore.go
new file mode 100644
index 00000000..4e458f9c
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/restore.go
@@ -0,0 +1,85 @@
+package mechanism
+
+import (
+ "fmt"
+ "github.com/microsoft/go-sqlcmd/internal/container"
+)
+
+type restore struct {
+}
+
+func (m *restore) Initialize(controller *container.Controller) {
+}
+
+func (m *restore) CopyToLocation() string {
+ return "/var/opt/mssql/backup"
+}
+
+func (m *restore) Name() string {
+ return "restore"
+}
+
+func (m *restore) FileTypes() []string {
+ return []string{"bak"}
+}
+
+func (m *restore) BringOnline(databaseName string, _ string, query func(string), options BringOnlineOptions) {
+ if options.Filename == "" {
+ panic("Filename is required for restore")
+ }
+ if databaseName == "" {
+ panic("databaseName is required for restore")
+ }
+
+ query(fmt.Sprintf(
+ m.restoreStatement(),
+ m.CopyToLocation(),
+ options.Filename,
+ databaseName,
+ m.CopyToLocation(),
+ options.Filename,
+ ))
+}
+
+func (m *restore) restoreStatement() string {
+ return `SET NOCOUNT ON;
+
+-- Build a SQL Statement to restore any .bak file to the Linux filesystem
+DECLARE @sql NVARCHAR(max)
+
+-- This table definition works since SQL Server 2017, therefore
+-- works for all SQL Server containers (which started in 2017)
+DECLARE @fileListTable TABLE (
+ [LogicalName] NVARCHAR(128),
+ [PhysicalName] NVARCHAR(260),
+ [Type] CHAR(1),
+ [FileGroupName] NVARCHAR(128),
+ [Size] NUMERIC(20,0),
+ [MaxSize] NUMERIC(20,0),
+ [FileID] BIGINT,
+ [CreateLSN] NUMERIC(25,0),
+ [DropLSN] NUMERIC(25,0),
+ [UniqueID] UNIQUEIDENTIFIER,
+ [ReadOnlyLSN] NUMERIC(25,0),
+ [ReadWriteLSN] NUMERIC(25,0),
+ [BackupSizeInBytes] BIGINT,
+ [SourceBlockSize] INT,
+ [FileGroupID] INT,
+ [LogGroupGUID] UNIQUEIDENTIFIER,
+ [DifferentialBaseLSN] NUMERIC(25,0),
+ [DifferentialBaseGUID] UNIQUEIDENTIFIER,
+ [IsReadOnly] BIT,
+ [IsPresent] BIT,
+ [TDEThumbprint] VARBINARY(32),
+ [SnapshotURL] NVARCHAR(360)
+)
+
+INSERT INTO @fileListTable
+EXEC('RESTORE FILELISTONLY FROM DISK = ''%s/%s''')
+SET @sql = 'RESTORE DATABASE [%s] FROM DISK = ''%s/%s'' WITH '
+SELECT @sql = @sql + char(13) + ' MOVE ''' + LogicalName + ''' TO ''/var/opt/mssql/data/' + LogicalName + '.' + RIGHT(PhysicalName,CHARINDEX('\',PhysicalName)) + ''','
+FROM @fileListTable
+WHERE IsPresent = 1
+SET @sql = SUBSTRING(@sql, 1, LEN(@sql)-1)
+EXEC(@sql)`
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/script.go b/pkg/mssqlcontainer/ingest/mechanism/script.go
new file mode 100644
index 00000000..4c0b5939
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/script.go
@@ -0,0 +1,31 @@
+package mechanism
+
+import (
+ "github.com/microsoft/go-sqlcmd/internal/container"
+)
+
+type script struct {
+}
+
+func (m *script) Initialize(controller *container.Controller) {
+}
+
+func (m *script) CopyToLocation() string {
+ return "/var/opt/mssql/backup"
+}
+
+func (m *script) Name() string {
+ return "script"
+}
+
+func (m *script) FileTypes() []string {
+ return []string{"sql"}
+}
+
+func (m *script) BringOnline(databaseName string, _ string, query func(string), options BringOnlineOptions) {
+ if options.Filename == "" {
+ panic("Filename is required for restore")
+ }
+
+ query(options.Filename)
+}
diff --git a/pkg/mssqlcontainer/ingest/mechanism/trace.go b/pkg/mssqlcontainer/ingest/mechanism/trace.go
new file mode 100644
index 00000000..5917db05
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/mechanism/trace.go
@@ -0,0 +1,7 @@
+package mechanism
+
+var traceCallback func(format string, a ...any)
+
+func trace(format string, a ...any) {
+ traceCallback(format, a...)
+}
diff --git a/pkg/mssqlcontainer/ingest/type.go b/pkg/mssqlcontainer/ingest/type.go
new file mode 100644
index 00000000..9eb04eea
--- /dev/null
+++ b/pkg/mssqlcontainer/ingest/type.go
@@ -0,0 +1,6 @@
+package ingest
+
+type IngestOptions struct {
+ Mechanism string
+ DatabaseName string
+}
diff --git a/pkg/mssqlcontainer/initialize.go b/pkg/mssqlcontainer/initialize.go
new file mode 100644
index 00000000..c0b53cab
--- /dev/null
+++ b/pkg/mssqlcontainer/initialize.go
@@ -0,0 +1,19 @@
+package mssqlcontainer
+
+import "github.com/microsoft/go-sqlcmd/pkg/mssqlcontainer/ingest/mechanism"
+
+type InitializeOptions struct {
+ ErrorHandler func(error)
+ TraceHandler func(format string, a ...any)
+}
+
+func Initialize(options InitializeOptions) {
+ if options.ErrorHandler == nil {
+ panic("ErrorHandler is nil")
+ }
+ if options.TraceHandler == nil {
+ panic("TraceHandler is nil")
+ }
+
+ mechanism.Initialize(options.ErrorHandler, options.TraceHandler)
+}