Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

355 lines
13 KiB
Go

// 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"
"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"
eventsapi "github.com/dolthub/eventsapi_schema/dolt/services/eventsapi/v1alpha1"
)
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}}. Returns an error if the profile already exists.
{{.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 && err != config.ErrConfigParamNotFound {
return errhand.BuildDError("error: failed to get profiles, %s", err).Build()
}
profilesJSON := ""
profileExists := false
if encodedProfiles != "" {
profilesJSON, profileExists, err = decodeProfileAndCheckExists(profileName, encodedProfiles)
if err != nil {
return errhand.BuildDError("error: failed to decode profiles, %s", err).Build()
}
}
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.SetRaw(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, ""),
}
}