Merge pull request #6390 from dolthub/steph/dolt-profile

Adds new `--profile` global arg
This commit is contained in:
stephanie
2023-08-07 13:30:35 -07:00
committed by GitHub
11 changed files with 867 additions and 34 deletions

View File

@@ -310,6 +310,24 @@ func CreateCountCommitsArgParser() *argparser.ArgParser {
return ap
}
func CreateGlobalArgParser(name string) *argparser.ArgParser {
ap := argparser.NewArgParserWithVariableArgs(name)
if name == "dolt" {
ap.SupportsString("profile", "", "profile", "The name of the profile to use when executing SQL queries. Run `dolt profile --help` for more information.")
}
ap.SupportsString("user", "u", "user", "Defines the local superuser.")
ap.SupportsString("password", "p", "password", "Defines the password for the user.")
ap.SupportsString("host", "", "host", "Defines the host to connect to.")
ap.SupportsString("port", "", "port", "Defines the port to connect to.")
ap.SupportsFlag("no-tls", "", "Disables TLS for the connection to remote databases.")
ap.SupportsString("data-dir", "", "data-dir", "Defines a data directory whose subdirectories should all be dolt data repositories accessible as independent databases.")
ap.SupportsString("doltcfg-dir", "", "doltcfg-dir", "Defines a directory that contains configuration files for dolt.")
ap.SupportsString("privilege-file", "", "privilege-file", "DePath to a file to load and store users and grants.")
ap.SupportsString("branch-control-file", "", "branch-control-file", "Path to a file to load and store branch control permissions.")
ap.SupportsString("use-db", "", "use-db", "The name of the database to use when executing SQL queries.")
return ap
}
var awsParams = []string{dbfactory.AWSRegionParam, dbfactory.AWSCredsTypeParam, dbfactory.AWSCredsFileParam, dbfactory.AWSCredsProfile}
var ossParams = []string{dbfactory.OSSCredsFileParam, dbfactory.OSSCredsProfile}

View File

@@ -0,0 +1,363 @@
// Copyright 2023 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fatih/color"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"github.com/dolthub/dolt/go/cmd/dolt/cli"
"github.com/dolthub/dolt/go/cmd/dolt/errhand"
eventsapi "github.com/dolthub/dolt/go/gen/proto/dolt/services/eventsapi/v1alpha1"
"github.com/dolthub/dolt/go/libraries/doltcore/dbfactory"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
"github.com/dolthub/dolt/go/libraries/utils/config"
)
var profileDocs = cli.CommandDocumentationContent{
ShortDesc: "Manage dolt profiles for CLI global options.",
LongDesc: `With no arguments, shows a list of existing profiles. Two subcommands are available to perform operations on the profiles.
{{.EmphasisLeft}}add{{.EmphasisRight}}
Adds a profile named {{.LessThan}}name{{.GreaterThan}}. If the profile already exists, it will be overwritten.
{{.EmphasisLeft}}remove{{.EmphasisRight}}, {{.EmphasisLeft}}rm{{.EmphasisRight}}
Remove the profile named {{.LessThan}}name{{.GreaterThan}}.`,
Synopsis: []string{
"[-v | --verbose]",
"add [-u {{.LessThan}}user{{.GreaterThan}}] [-p {{.LessThan}}password{{.GreaterThan}}] [--host {{.LessThan}}host{{.GreaterThan}}] [--port {{.LessThan}}port{{.GreaterThan}}] [--no-tls] [--data-dir {{.LessThan}}directory{{.GreaterThan}}] [--doltcfg-dir {{.LessThan}}directory{{.GreaterThan}}] [--privilege-file {{.LessThan}}privilege file{{.GreaterThan}}] [--branch-control-file {{.LessThan}}branch control file{{.GreaterThan}}] [--use-db {{.LessThan}}database{{.GreaterThan}}] {{.LessThan}}name{{.GreaterThan}}",
"remove {{.LessThan}}name{{.GreaterThan}}",
},
}
const (
addProfileId = "add"
removeProfileId = "remove"
GlobalCfgProfileKey = "profile"
DefaultProfileName = "default"
defaultProfileWarning = "Default profile has been added. All dolt commands taking global arguments will use this default profile until it is removed.\nWARNING: This will alter the behavior of commands which specify no `--profile`.\nIf you are using dolt in contexts where you expect a `.dolt` directory to be accessed, the default profile will be used instead."
)
type ProfileCmd struct{}
// Name returns the name of the Dolt cli command. This is what is used on the command line to invoke the command
func (cmd ProfileCmd) Name() string {
return "profile"
}
// Description returns a description of the command
func (cmd ProfileCmd) Description() string {
return "Manage dolt profiles for CLI global options."
}
func (cmd ProfileCmd) Docs() *cli.CommandDocumentation {
ap := cmd.ArgParser()
return cli.NewCommandDocumentation(profileDocs, ap)
}
func (cmd ProfileCmd) ArgParser() *argparser.ArgParser {
ap := cli.CreateGlobalArgParser("profile")
ap.ArgListHelp = append(ap.ArgListHelp, [2]string{"name", "Defines the name of the profile to add or remove."})
ap.SupportsFlag(cli.VerboseFlag, "v", "Includes full details when printing list of profiles.")
return ap
}
// EventType returns the type of the event to log
func (cmd ProfileCmd) EventType() eventsapi.ClientEventType {
return eventsapi.ClientEventType_PROFILE
}
func (cmd ProfileCmd) RequiresRepo() bool {
return false
}
// Exec executes the command
func (cmd ProfileCmd) Exec(ctx context.Context, commandStr string, args []string, dEnv *env.DoltEnv, cliCtx cli.CliContext) int {
ap := cmd.ArgParser()
help, usage := cli.HelpAndUsagePrinters(cli.CommandDocsForCommandString(commandStr, profileDocs, ap))
apr := cli.ParseArgsOrDie(ap, args, help)
var verr errhand.VerboseError
switch {
case apr.NArg() == 0:
verr = printProfiles(dEnv, apr)
case apr.Arg(0) == addProfileId:
verr = addProfile(dEnv, apr)
case apr.Arg(0) == removeProfileId:
verr = removeProfile(dEnv, apr)
default:
verr = errhand.BuildDError("").SetPrintUsage().Build()
}
return HandleVErrAndExitCode(verr, usage)
}
func addProfile(dEnv *env.DoltEnv, apr *argparser.ArgParseResults) errhand.VerboseError {
if apr.NArg() != 2 {
return errhand.BuildDError("Only one profile name can be specified").SetPrintUsage().Build()
}
profileName := strings.TrimSpace(apr.Arg(1))
p := newProfile(apr)
profStr := p.String()
cfg, ok := dEnv.Config.GetConfig(env.GlobalConfig)
if !ok {
return errhand.BuildDError("error: failed to get global config").Build()
}
//TODO: enable config to retrieve json objects instead of just strings
encodedProfiles, err := cfg.GetString(GlobalCfgProfileKey)
if err != nil {
if err != config.ErrConfigParamNotFound {
return errhand.BuildDError("error: failed to get profiles, %s", err).Build()
} else {
profilesJSON := "{\"" + profileName + "\"" + ": " + profStr + "}"
err = writeProfileToGlobalConfig(profilesJSON, cfg)
if err != nil {
return errhand.BuildDError("error: failed to write profile to config, %s", err).Build()
}
if profileName == DefaultProfileName {
cli.Println(color.YellowString(defaultProfileWarning))
}
err = setGlobalConfigPermissions(dEnv)
if err != nil {
return errhand.BuildDError("error: failed to set permissions, %s", err).Build()
}
return nil
}
}
profilesJSON, profileExists, err := decodeProfileAndCheckExists(profileName, encodedProfiles)
if profileExists {
return errhand.BuildDError("error: profile %s already exists, please delete this profile and re-add it if you want to edit any values.", profileName).Build()
}
profilesJSON, err = sjson.Set(profilesJSON, profileName, profStr)
if err != nil {
return errhand.BuildDError("error: failed to add profile, %s", err).Build()
}
err = writeProfileToGlobalConfig(profilesJSON, cfg)
if err != nil {
return errhand.BuildDError("error: failed to write profile to config, %s", err).Build()
}
if profileName == DefaultProfileName {
cli.Println(color.YellowString(defaultProfileWarning))
}
err = setGlobalConfigPermissions(dEnv)
if err != nil {
return errhand.BuildDError("error: failed to set permissions, %s", err).Build()
}
return nil
}
func removeProfile(dEnv *env.DoltEnv, apr *argparser.ArgParseResults) errhand.VerboseError {
if apr.NArg() != 2 {
return errhand.BuildDError("Only one profile name can be specified").SetPrintUsage().Build()
}
profileName := strings.TrimSpace(apr.Arg(1))
cfg, ok := dEnv.Config.GetConfig(env.GlobalConfig)
if !ok {
return errhand.BuildDError("error: failed to get global config").Build()
}
encodedProfiles, err := cfg.GetString(GlobalCfgProfileKey)
if err != nil {
if err == config.ErrConfigParamNotFound {
return errhand.BuildDError("error: no existing profiles").Build()
}
return errhand.BuildDError("error: failed to get profiles, %s", err).Build()
}
profilesJSON, profileExists, err := decodeProfileAndCheckExists(profileName, encodedProfiles)
if !profileExists {
return errhand.BuildDError("error: profile %s does not exist", profileName).Build()
}
profilesJSON, err = sjson.Delete(profilesJSON, profileName)
if err != nil {
return errhand.BuildDError("error: failed to remove profile, %s", err).Build()
}
if profilesJSON == "{}" {
err = cfg.Unset([]string{GlobalCfgProfileKey})
if err != nil {
return errhand.BuildDError("error: failed to remove profile, %s", err).Build()
}
} else {
err = writeProfileToGlobalConfig(profilesJSON, cfg)
if err != nil {
return errhand.BuildDError("error: failed to write profile to config, %s", err).Build()
}
}
err = setGlobalConfigPermissions(dEnv)
if err != nil {
return errhand.BuildDError("error: failed to set permissions, %s", err).Build()
}
return nil
}
func printProfiles(dEnv *env.DoltEnv, apr *argparser.ArgParseResults) errhand.VerboseError {
cfg, ok := dEnv.Config.GetConfig(env.GlobalConfig)
if !ok {
return errhand.BuildDError("error: failed to get global config").Build()
}
encodedProfiles, err := cfg.GetString(GlobalCfgProfileKey)
if err != nil {
if err == config.ErrConfigParamNotFound {
return nil
}
return errhand.BuildDError("error: failed to get profiles, %s", err).Build()
}
profilesJSON, err := DecodeProfile(encodedProfiles)
if err != nil {
return errhand.BuildDError("error: failed to decode profiles, %s", err).Build()
}
profileMap := gjson.Parse(profilesJSON)
if !profileMap.Exists() {
return nil
}
for profileName, profile := range profileMap.Map() {
var p Profile
var val []byte = []byte(profile.String())
err := json.Unmarshal([]byte(val), &p)
if err != nil {
return errhand.BuildDError("error: failed to unmarshal profile, %s", err).Build()
}
prettyPrintProfile(profileName, p, apr.Contains(cli.VerboseFlag))
}
return nil
}
func prettyPrintProfile(profileName string, profile Profile, verbose bool) {
cli.Println(profileName)
if verbose {
if profile.HasPassword {
cli.Println(fmt.Sprintf("\tuser: %s\n\tpassword: %s\n\thost: %s\n\tport: %s\n\tno-tls: %t\n\tdata-dir: %s\n\tdoltcfg-dir: %s\n\tprivilege-file: %s\n\tbranch-control-file: %s\n\tuse-db: %s\n",
profile.User, profile.Password, profile.Host, profile.Port, profile.NoTLS, profile.DataDir, profile.DoltCfgDir, profile.PrivilegeFile, profile.BranchControl, profile.UseDB))
} else {
cli.Println(fmt.Sprintf("\tuser: %s\n\thost: %s\n\tport: %s\n\tno-tls: %t\n\tdata-dir: %s\n\tdoltcfg-dir: %s\n\tprivilege-file: %s\n\tbranch-control-file: %s\n\tuse-db: %s\n",
profile.User, profile.Host, profile.Port, profile.NoTLS, profile.DataDir, profile.DoltCfgDir, profile.PrivilegeFile, profile.BranchControl, profile.UseDB))
}
}
}
// setGlobalConfigPermissions sets permissions on global config file to 0600 to protect potentially sensitive information (credentials)
func setGlobalConfigPermissions(dEnv *env.DoltEnv) error {
homeDir, err := env.GetCurrentUserHomeDir()
if err != nil {
return errhand.BuildDError("error: failed to get home directory: %s", err).Build()
}
path, err := dEnv.FS.Abs(filepath.Join(homeDir, dbfactory.DoltDir, env.GlobalConfigFile))
if err != nil {
return errhand.BuildDError("error: failed to get global config path: %s", err).Build()
}
err = os.Chmod(path, 0600)
if err != nil {
return errhand.BuildDError("error: failed to set permissions on global config: %s", err).Build()
}
return nil
}
// writeProfileToGlobalConfig encodes a given profile JSON (represented by a string) to base64 and writes that encoded profile to the global config
func writeProfileToGlobalConfig(profile string, config config.ReadWriteConfig) error {
profilesData := []byte(profile)
encodedProfiles := make([]byte, base64.StdEncoding.EncodedLen(len(profilesData)))
base64.StdEncoding.Encode(encodedProfiles, profilesData)
err := config.SetStrings(map[string]string{GlobalCfgProfileKey: string(encodedProfiles)})
if err != nil {
return err
}
return nil
}
// DecodeProfile decodes a given base64 encoded profile string to a string representing a JSON
func DecodeProfile(encodedProfile string) (string, error) {
decodedProfile := make([]byte, base64.StdEncoding.DecodedLen(len(encodedProfile)))
n, err := base64.StdEncoding.Decode(decodedProfile, []byte(encodedProfile))
if err != nil {
return "", err
}
decodedProfile = decodedProfile[:n]
return string(decodedProfile), nil
}
// decodeProfileAndCheckExists decodes the given profiles and retrieves the profile named profileName. Returns a
// string representing the profile JSON and a bool indicating whether the profile exists
func decodeProfileAndCheckExists(profileName, encodedProfiles string) (string, bool, error) {
profilesJSON, err := DecodeProfile(encodedProfiles)
if err != nil {
return "", false, err
}
profileCheck := gjson.Get(profilesJSON, profileName)
return profilesJSON, profileCheck.Exists(), nil
}
type Profile struct {
User string `json:"user"`
Password string `json:"password"`
HasPassword bool `json:"has-password"`
Host string `json:"host"`
Port string `json:"port"`
NoTLS bool `json:"no-tls"`
DataDir string `json:"data-dir"`
DoltCfgDir string `json:"doltcfg-dir"`
PrivilegeFile string `json:"privilege-file"`
BranchControl string `json:"branch-control-file"`
UseDB string `json:"use-db"`
}
func (p Profile) String() string {
b, err := json.Marshal(p)
if err != nil {
panic(err)
}
return string(b)
}
func newProfile(apr *argparser.ArgParseResults) Profile {
return Profile{
User: apr.GetValueOrDefault(cli.UserFlag, ""),
Password: apr.GetValueOrDefault(cli.PasswordFlag, ""),
HasPassword: apr.Contains(cli.PasswordFlag),
Host: apr.GetValueOrDefault(cli.HostFlag, ""),
Port: apr.GetValueOrDefault(cli.PortFlag, ""),
NoTLS: apr.Contains(cli.NoTLSFlag),
DataDir: apr.GetValueOrDefault(DataDirFlag, ""),
DoltCfgDir: apr.GetValueOrDefault(CfgDirFlag, ""),
PrivilegeFile: apr.GetValueOrDefault(PrivsFilePathFlag, ""),
BranchControl: apr.GetValueOrDefault(BranchCtrlPathFlag, ""),
UseDB: apr.GetValueOrDefault(UseDbFlag, ""),
}
}

View File

@@ -69,7 +69,7 @@ const (
type RemoteCmd struct{}
// Name is returns the name of the Dolt cli command. This is what is used on the command line to invoke the command
// Name returns the name of the Dolt cli command. This is what is used on the command line to invoke the command
func (cmd RemoteCmd) Name() string {
return "remote"
}

View File

@@ -92,6 +92,7 @@ const (
DefaultUser = "root"
DefaultHost = "localhost"
UseDbFlag = "use-db"
ProfileFlag = "profile"
welcomeMsg = `# Welcome to the DoltSQL shell.
# Statements must be terminated with ';'.

View File

@@ -30,6 +30,7 @@ import (
"github.com/dolthub/go-mysql-server/sql"
"github.com/fatih/color"
"github.com/pkg/profile"
"github.com/tidwall/gjson"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
@@ -55,6 +56,7 @@ import (
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
"github.com/dolthub/dolt/go/libraries/events"
"github.com/dolthub/dolt/go/libraries/utils/argparser"
"github.com/dolthub/dolt/go/libraries/utils/config"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
"github.com/dolthub/dolt/go/store/nbs"
"github.com/dolthub/dolt/go/store/util/tempfiles"
@@ -118,6 +120,7 @@ var doltSubCommands = []cli.Command{
docscmds.Commands,
stashcmds.StashCommands,
&commands.Assist{},
commands.ProfileCmd{},
}
var commandsWithoutCliCtx = []cli.Command{
@@ -150,6 +153,7 @@ var commandsWithoutCliCtx = []cli.Command{
dumpZshCommand,
docscmds.Commands,
&commands.Assist{},
commands.ProfileCmd{},
}
var commandsWithoutGlobalArgSupport = []cli.Command{
@@ -185,7 +189,7 @@ func supportsGlobalArgs(commandName string) bool {
}
var doltCommand = cli.NewSubCommandHandler("dolt", "it's git for data", doltSubCommands)
var globalArgParser = buildGlobalArgs()
var globalArgParser = cli.CreateGlobalArgParser("dolt")
var globalDocs = cli.CommandDocsForCommandString("dolt", doc, globalArgParser)
var globalSpecialMsg = `
@@ -420,8 +424,24 @@ func runMain() int {
_, usage := cli.HelpAndUsagePrinters(globalDocs)
apr, remainingArgs, err := globalArgParser.ParseGlobalArgs(args)
var fs filesys.Filesys
fs = filesys.LocalFS
dEnv := env.Load(ctx, env.GetCurrentUserHomeDir, fs, doltdb.LocalDirDoltDB, Version)
dEnv.IgnoreLockFile = ignoreLockFile
root, err := env.GetCurrentUserHomeDir()
if err != nil {
cli.PrintErrln(color.RedString("Failed to load the HOME directory: %v", err))
return 1
}
globalConfig, ok := dEnv.Config.GetConfig(env.GlobalConfig)
if !ok {
cli.PrintErrln(color.RedString("Failed to get global config"))
return 1
}
apr, remainingArgs, subcommandName, err := parseGlobalArgsAndSubCommandName(globalConfig, args)
if err == argparser.ErrHelp {
doltCommand.PrintUsage("dolt")
cli.Println(globalSpecialMsg)
@@ -433,10 +453,6 @@ func runMain() int {
return 1
}
subcommandName := remainingArgs[0]
var fs filesys.Filesys
fs = filesys.LocalFS
dataDir, hasDataDir := apr.GetValue(commands.DataDirFlag)
if hasDataDir {
// If a relative path was provided, this ensures we have an absolute path everywhere.
@@ -450,14 +466,6 @@ func runMain() int {
return 1
}
}
dEnv := env.Load(ctx, env.GetCurrentUserHomeDir, fs, doltdb.LocalDirDoltDB, Version)
dEnv.IgnoreLockFile = ignoreLockFile
root, err := env.GetCurrentUserHomeDir()
if err != nil {
cli.PrintErrln(color.RedString("Failed to load the HOME directory: %v", err))
return 1
}
if dEnv.CfgLoadErr != nil {
cli.PrintErrln(color.RedString("Failed to load the global config. %v", dEnv.CfgLoadErr))
@@ -729,19 +737,92 @@ func interceptSendMetrics(ctx context.Context, args []string) (bool, int) {
return true, doltCommand.Exec(ctx, "dolt", args, dEnv, nil)
}
func buildGlobalArgs() *argparser.ArgParser {
ap := argparser.NewArgParserWithVariableArgs("dolt")
// parseGlobalArgsAndSubCommandName parses the global arguments, including a profile if given or a default profile if exists. Also returns the subcommand name.
func parseGlobalArgsAndSubCommandName(globalConfig config.ReadWriteConfig, args []string) (apr *argparser.ArgParseResults, remaining []string, subcommandName string, err error) {
apr, remaining, err = globalArgParser.ParseGlobalArgs(args)
if err != nil {
return nil, nil, "", err
}
ap.SupportsString(cli.UserFlag, "u", "user", fmt.Sprintf("Defines the local superuser (defaults to `%v`). If the specified user exists, will take on permissions of that user.", commands.DefaultUser))
ap.SupportsString(cli.PasswordFlag, "p", "password", "Defines the password for the user. Defaults to empty string.")
ap.SupportsString(cli.HostFlag, "", "host", "Defines the host to connect to.")
ap.SupportsInt(cli.PortFlag, "", "port", "Defines the port to connect to. Only used when the --host flag is also provided. Defaults to `3306`.")
ap.SupportsFlag(cli.NoTLSFlag, "", "Disables TLS for the connection to remote databases.")
ap.SupportsString(commands.DataDirFlag, "", "directory", "Defines a directory whose subdirectories should all be dolt data repositories accessible as independent databases within. Defaults to the current directory.")
ap.SupportsString(commands.CfgDirFlag, "", "directory", "Defines a directory that contains configuration files for dolt. Defaults to `$data-dir/.doltcfg`. Will only be created if there is a change to configuration settings.")
ap.SupportsString(commands.PrivsFilePathFlag, "", "privilege file", "Path to a file to load and store users and grants. Defaults to `$doltcfg-dir/privileges.db`. Will only be created if there is a change to privileges.")
ap.SupportsString(commands.BranchCtrlPathFlag, "", "branch control file", "Path to a file to load and store branch control permissions. Defaults to `$doltcfg-dir/branch_control.db`. Will only be created if there is a change to branch control permissions.")
ap.SupportsString(commands.UseDbFlag, "", "database", "The name of the database to use when executing SQL queries. Defaults the database of the root directory, if it exists, and the first alphabetically if not.")
subcommandName = remaining[0]
return ap
useDefaultProfile := false
profileName, hasProfile := apr.GetValue(commands.ProfileFlag)
encodedProfiles, err := globalConfig.GetString(commands.GlobalCfgProfileKey)
if err != nil {
if err == config.ErrConfigParamNotFound {
if hasProfile {
return nil, nil, "", fmt.Errorf("no profiles found")
} else {
return apr, remaining, subcommandName, nil
}
} else {
return nil, nil, "", err
}
}
profiles, err := commands.DecodeProfile(encodedProfiles)
if err != nil {
return nil, nil, "", err
}
if !hasProfile && supportsGlobalArgs(subcommandName) {
defaultProfile := gjson.Get(profiles, commands.DefaultProfileName)
if defaultProfile.Exists() {
args = append([]string{"--profile", commands.DefaultProfileName}, args...)
apr, remaining, err = globalArgParser.ParseGlobalArgs(args)
if err != nil {
return nil, nil, "", err
}
profileName, _ = apr.GetValue(commands.ProfileFlag)
useDefaultProfile = true
}
}
if hasProfile || useDefaultProfile {
profileArgs, err := getProfile(apr, profileName, profiles)
if err != nil {
return nil, nil, "", err
}
args = append(profileArgs, args...)
apr, remaining, err = globalArgParser.ParseGlobalArgs(args)
if err != nil {
return nil, nil, "", err
}
}
return
}
// getProfile retrieves the given profile from the provided list of profiles and returns the args (as flags) and values
// for that profile in a []string. If the profile is not found, an error is returned.
func getProfile(apr *argparser.ArgParseResults, profileName, profiles string) (result []string, err error) {
prof := gjson.Get(profiles, profileName)
if prof.Exists() {
hasPassword := false
password := ""
for flag, value := range prof.Map() {
if !apr.Contains(flag) {
if flag == cli.PasswordFlag {
password = value.Str
} else if flag == "has-password" {
hasPassword = value.Bool()
} else if flag == cli.NoTLSFlag {
if value.Bool() {
result = append(result, "--"+flag)
continue
}
} else {
if value.Str != "" {
result = append(result, "--"+flag, value.Str)
}
}
}
}
if !apr.Contains(cli.PasswordFlag) && hasPassword {
result = append(result, "--"+cli.PasswordFlag, password)
}
return result, nil
} else {
return nil, fmt.Errorf("profile %s not found", profileName)
}
}

View File

@@ -154,6 +154,7 @@ const (
ClientEventType_STASH_LIST ClientEventType = 59
ClientEventType_STASH_POP ClientEventType = 60
ClientEventType_SHOW ClientEventType = 61
ClientEventType_PROFILE ClientEventType = 62
)
// Enum value maps for ClientEventType.

View File

@@ -71,6 +71,8 @@ require (
github.com/prometheus/client_golang v1.13.0
github.com/rs/zerolog v1.28.0
github.com/shirou/gopsutil/v3 v3.22.1
github.com/tidwall/gjson v1.14.4
github.com/tidwall/sjson v1.2.5
github.com/vbauerster/mpb v3.4.0+incompatible
github.com/vbauerster/mpb/v8 v8.0.2
github.com/xitongsys/parquet-go v1.6.1
@@ -134,10 +136,8 @@ require (
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rs/xid v1.4.0 // indirect
github.com/tetratelabs/wazero v1.1.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/tklauser/numcpus v0.3.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.opencensus.io v0.24.0 // indirect

View File

@@ -28,8 +28,8 @@ const (
doltRootPathEnvVar = "DOLT_ROOT_PATH"
credsDir = "creds"
configFile = "config.json"
globalConfig = "config_global.json"
configFile = "config.json"
GlobalConfigFile = "config_global.json"
repoStateFile = "repo_state.json"
)
@@ -77,7 +77,7 @@ func getGlobalCfgPath(hdp HomeDirProvider) (string, error) {
return "", err
}
return filepath.Join(homeDir, dbfactory.DoltDir, globalConfig), nil
return filepath.Join(homeDir, dbfactory.DoltDir, GlobalConfigFile), nil
}
func getLocalConfigPath() string {

View File

@@ -23,7 +23,7 @@ import (
func TestGetGlobalCfgPath(t *testing.T) {
homeDir := "/user/bheni"
expected := filepath.Join(homeDir, dbfactory.DoltDir, globalConfig)
expected := filepath.Join(homeDir, dbfactory.DoltDir, GlobalConfigFile)
actual, _ := getGlobalCfgPath(func() (string, error) {
return homeDir, nil
})

View File

@@ -135,6 +135,7 @@ SKIP_SERVER_TESTS=$(cat <<-EOM
~sql-reset.bats~
~sql-checkout.bats~
~cli-hosted.bats~
~profile.bats~
EOM
)

View File

@@ -0,0 +1,368 @@
#!/usr/bin/env bats
load $BATS_TEST_DIRNAME/helper/common.bash
load $BATS_TEST_DIRNAME/helper/query-server-common.bash
make_repo() {
mkdir "$1"
cd "$1"
dolt init
dolt sql -q "create table $1_tbl (id int)"
dolt sql <<SQL
CREATE TABLE table1 (pk int PRIMARY KEY);
CREATE TABLE table2 (pk int PRIMARY KEY);
SQL
dolt add -A && dolt commit -m "tables table1, table2"
cd -
}
setup() {
setup_no_dolt_init
make_repo defaultDB
make_repo altDB
unset DOLT_CLI_PASSWORD
unset DOLT_SILENCE_USER_REQ_FOR_TESTING
}
teardown() {
stop_sql_server 1
teardown_common
}
@test "profile: --profile exists and isn't empty" {
cd defaultDB
dolt sql -q "create table test (pk int primary key)"
dolt sql -q "insert into test values (999)"
dolt add test
dolt commit -m "insert initial value into test"
cd -
dolt profile add --use-db defaultDB defaultTest
run dolt --profile defaultTest sql -q "select * from test"
[ "$status" -eq 0 ] || false
[[ "$output" =~ "999" ]] || false
}
@test "profile: --profile doesn't exist" {
dolt profile add --use-db defaultDB defaultTest
run dolt --profile nonExistentProfile sql -q "select * from altDB_tbl"
[ "$status" -eq 1 ] || false
[[ "$output" =~ "Failure to parse arguments: profile nonExistentProfile not found" ]] || false
}
@test "profile: additional flag gets used" {
cd altDB
dolt sql -q "create table test (pk int primary key)"
dolt sql -q "insert into test values (999)"
dolt add test
dolt commit -m "insert initial value into test"
cd -
dolt profile add --user dolt --password "" userProfile
run dolt --profile userProfile --use-db altDB sql -q "select * from test"
[ "$status" -eq 0 ] || false
[[ "$output" =~ "999" ]] || false
}
@test "profile: duplicate flag overrides correctly" {
cd altDB
dolt sql -q "create table test (pk int primary key)"
dolt sql -q "insert into test values (999)"
dolt add test
dolt commit -m "insert initial value into test"
cd -
dolt profile add --use-db defaultDB defaultTest
run dolt --profile defaultTest --use-db altDB sql -q "select * from test"
[ "$status" -eq 0 ] || false
[[ "$output" =~ "999" ]] || false
}
@test "profile: duplicate flag with non-duplicate flags in profile overrides correctly" {
cd altDB
dolt sql -q "create table test (pk int primary key)"
dolt sql -q "insert into test values (999)"
dolt add test
dolt commit -m "insert initial value into test"
cd -
start_sql_server altDb
dolt --user dolt --password "" sql -q "CREATE USER 'steph' IDENTIFIED BY 'pass'; GRANT ALL PRIVILEGES ON altDB.* TO 'steph' WITH GRANT OPTION;";
dolt profile add --user "not-steph" --password "pass" --use-db altDB userWithDBProfile
run dolt --profile userWithDBProfile --user steph sql -q "select * from test"
[ "$status" -eq 0 ] || false
[[ "$output" =~ "999" ]] || false
}
@test "profile: duplicate flag with non-duplicate flags overrides correctly" {
cd altDB
dolt sql -q "create table test (pk int primary key)"
dolt sql -q "insert into test values (999)"
dolt add test
dolt commit -m "insert initial value into test"
cd -
start_sql_server altDb
dolt --user dolt --password "" sql -q "CREATE USER 'steph' IDENTIFIED BY 'pass'; GRANT ALL PRIVILEGES ON altDB.* TO 'steph' WITH GRANT OPTION;";
dolt profile add --user "not-steph" --password "pass" userProfile
run dolt --profile userProfile --user steph --use-db altDB sql -q "select * from test"
[ "$status" -eq 0 ] || false
[[ "$output" =~ "999" ]] || false
}
@test "profile: dolt profile add adds a profile" {
run dolt profile add --use-db altDB altTest
[ "$status" -eq 0 ] || false
run dolt profile
[ "$status" -eq 0 ] || false
[[ "$output" =~ "altTest" ]] || false
}
@test "profile: dolt profile add does not overwrite an existing profile" {
run dolt profile add --user "steph" --password "password123" userProfile
[ "$status" -eq 0 ] || false
run dolt profile -v
[ "$status" -eq 0 ] || false
[[ "$output" =~ "userProfile" ]] || false
[[ "$output" =~ "steph" ]] || false
[[ "$output" =~ "password123" ]] || false
run dolt profile add --user "joe" --password "password123" userProfile
[ "$status" -eq 1 ] || false
[[ "$output" =~ "profile userProfile already exists, please delete this profile and re-add it if you want to edit any values" ]] || false
run dolt profile -v
[ "$status" -eq 0 ] || false
[[ "$output" =~ "userProfile" ]] || false
[[ "$output" =~ "steph" ]] || false
[[ ! "$output" =~ "joe" ]] || false
[[ "$output" =~ "password123" ]] || false
}
@test "profile: dolt profile add adds a profile with existing profiles" {
dolt profile add --use-db altDB altTest
dolt profile add --use-db defaultDB defaultTest
run dolt profile
[ "$status" -eq 0 ] || false
[[ "$output" =~ "altTest" ]] || false
[[ "$output" =~ "defaultTest" ]] || false
}
@test "profile: dolt profile add with multiple names errors" {
run dolt profile add --use-db altDB altTest altTest2
[ "$status" -eq 1 ] || false
[[ "$output" =~ "Only one profile name can be specified" ]] || false
}
@test "profile: adding default profile prints warning message" {
run dolt profile add --use-db defaultDB default
[ "$status" -eq 0 ] || false
[[ "$output" =~ "Default profile has been added. All dolt commands taking global arguments will use this default profile until it is removed." ]] || false
[[ "$output" =~ "WARNING: This will alter the behavior of commands which specify no \`--profile\`." ]] || false
[[ "$output" =~ "If you are using dolt in contexts where you expect a \`.dolt\` directory to be accessed, the default profile will be used instead." ]] || false
}
@test "profile: dolt profile add encodes profiles in config" {
run dolt profile add --use-db altDB altTest
[ "$status" -eq 0 ] || false
run cat "$BATS_TMPDIR/config-$$/.dolt/config_global.json"
[ "$status" -eq 0 ] || false
[[ ! "$output" =~ "altTest" ]] || false
}
@test "profile: dolt profile add locks global config with 0600" {
run dolt profile add --use-db altDB altTest
[ "$status" -eq 0 ] || false
run stat "$BATS_TMPDIR/config-$$/.dolt/config_global.json"
[[ "$output" =~ "-rw-------" ]] || false
}
@test "profile: dolt profile remove removes a profile" {
dolt profile add --use-db altDB altTest
run dolt profile
[ "$status" -eq 0 ] || false
[[ "$output" =~ "altTest" ]] || false
run dolt profile remove altTest
[ "$status" -eq 0 ] || false
run dolt profile
[ "$status" -eq 0 ] || false
[[ ! "$output" =~ "altTest:" ]] || false
}
@test "profile: dolt profile remove leaves existing profiles" {
dolt profile add --use-db altDB altTest
dolt profile add --use-db defaultDB defaultTest
run dolt profile
[ "$status" -eq 0 ] || false
[[ "$output" =~ "altTest" ]] || false
[[ "$output" =~ "defaultTest" ]] || false
run dolt profile remove altTest
[ "$status" -eq 0 ] || false
run dolt profile
[ "$status" -eq 0 ] || false
[[ ! "$output" =~ "altTest:" ]] || false
[[ "$output" =~ "defaultTest" ]] || false
}
@test "profile: dolt profile remove last profile also removes profile param from global config" {
dolt profile add --use-db altDB altTest
run dolt profile
[ "$status" -eq 0 ] || false
[[ "$output" =~ "altTest" ]] || false
run dolt profile remove altTest
[ "$status" -eq 0 ] || false
run dolt profile
[ "$status" -eq 0 ] || false
[[ ! "$output" =~ "altTest" ]] || false
run dolt config --list --global
[ "$status" -eq 0 ] || false
[[ ! "$output" =~ "profile" ]] || false
}
@test "profile: dolt profile remove with no existing profiles errors" {
run dolt profile
[ "$status" -eq 0 ] || false
[[ "$output" = "" ]] || false
run dolt profile remove altTest
[ "$status" -eq 1 ] || false
[[ "$output" =~ "no existing profiles" ]] || false
}
@test "profile: dolt profile remove with non-existent profile errors" {
dolt profile add --use-db altDB altTest
run dolt profile
[ "$status" -eq 0 ] || false
[[ "$output" =~ "altTest" ]] || false
run dolt profile remove defaultTest
[ "$status" -eq 1 ] || false
[[ "$output" =~ "profile defaultTest does not exist" ]] || false
}
@test "profile: dolt profile remove with multiple names errors" {
dolt profile add --use-db altDB altTest
run dolt profile remove altTest altTest2
[ "$status" -eq 1 ] || false
[[ "$output" =~ "Only one profile name can be specified" ]] || false
}
@test "profile: dolt profile remove locks global config with 0600" {
dolt profile add --use-db altDB altTest
run dolt profile remove altTest
[ "$status" -eq 0 ] || false
run stat "$BATS_TMPDIR/config-$$/.dolt/config_global.json"
[[ "$output" =~ "-rw-------" ]] || false
}
@test "profile: dolt profile lists all profiles" {
dolt profile add --use-db altDB altTest
dolt profile add --use-db defaultDB -u "steph" --password "pass" defaultTest
run dolt profile
[ "$status" -eq 0 ] || false
[[ "$output" =~ "altTest" ]] || false
[[ ! "$output" =~ "use-db: altDB" ]] || false
[[ "$output" =~ "defaultTest" ]] || false
[[ ! "$output" =~ "user: steph" ]] || false
[[ ! "$output" =~ "password: pass" ]] || false
[[ ! "$output" =~ "use-db: defaultDB" ]] || false
}
@test "profile: dolt profile --verbose lists all profiles and all details" {
dolt profile add --use-db altDB altTest
dolt profile add --use-db defaultDB -u "steph" --password "pass" defaultTest
run dolt profile -v
[ "$status" -eq 0 ] || false
[[ "$output" =~ "altTest" ]] || false
[[ "$output" =~ "use-db: altDB" ]] || false
[[ "$output" =~ "defaultTest" ]] || false
[[ "$output" =~ "user: steph" ]] || false
[[ "$output" =~ "password: pass" ]] || false
[[ "$output" =~ "use-db: defaultDB" ]] || false
}
@test "profile: default profile used when none specified" {
cd defaultDB
dolt sql -q "create table test (pk int primary key)"
dolt sql -q "insert into test values (999)"
dolt add test
dolt commit -m "insert initial value into test"
cd -
dolt profile add --use-db defaultDB default
run dolt sql -q "select * from test"
[ "$status" -eq 0 ] || false
[[ "$output" =~ "999" ]] || false
}
@test "profile: no profile used when none specified and no default set" {
cd defaultDB
dolt sql -q "insert into table1 values (999)"
dolt add table1
dolt commit -m "insert initial value into table1"
cd -
dolt profile add --use-db defaultDB defaultTest
run dolt sql -q "select * from table1"
[ "$status" -eq 0 ] || false
[[ ! "$output" =~ "999" ]] || false
}
@test "profile: correct default profile used when none specified" {
cd defaultDB
dolt sql -q "create table test (pk int primary key)"
dolt sql -q "insert into test values (999)"
dolt add test
dolt commit -m "insert initial value into test"
cd -
dolt profile add --use-db defaultDB default
dolt profile add --use-db altDB altTest
run dolt sql -q "select * from test"
[ "$status" -eq 0 ] || false
[[ "$output" =~ "999" ]] || false
}
@test "profile: commands that don't support global args work with a default profile set" {
cd altDB
dolt sql -q "create table test (pk int primary key)"
dolt sql -q "insert into test values (999)"
dolt add test
dolt commit -m "insert initial value into test"
dolt profile add --use-db defaultDB default
run dolt log
[ "$status" -eq 0 ] || false
[[ "$output" =~ "insert initial value into test" ]] || false
}
@test "profile: profile with user but not password waits for password prompt" {
dolt profile add --use-db defaultDB -u "steph" defaultTest
run dolt --profile defaultTest sql -q "select * from table1" <<< ""
[ "$status" -eq 1 ] || false
[[ "$output" =~ "Enter password:" ]] || false
}
@test "profile: profile with user and empty password doesn't wait for password prompt" {
dolt profile add --use-db defaultDB -u "steph" -p "" defaultTest
run dolt --profile defaultTest sql -q "show tables"
[ "$status" -eq 0 ] || false
}