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) +}