355 lines
13 KiB
Go
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, ""),
|
|
}
|
|
}
|