mirror of
https://github.com/ipfs/kubo.git
synced 2025-05-18 15:36:16 +08:00

Importantly: * fixes a bunch of MFS bugs * pulls in some bitswap improvements License: MIT Signed-off-by: Steven Allen <steven@stebalien.com>
488 lines
12 KiB
Go
488 lines
12 KiB
Go
package commands
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/ipfs/go-ipfs/core/commands/cmdenv"
|
|
"github.com/ipfs/go-ipfs/repo"
|
|
"github.com/ipfs/go-ipfs/repo/fsrepo"
|
|
|
|
"gx/ipfs/QmP2i47tnU23ijdshrZtuvrSkQPtf9HhsMb9fwGVe8owj2/jsondiff"
|
|
"gx/ipfs/QmaAP56JAwdjwisPTu4yx17whcjTr6y5JCSCF77Y1rahWV/go-ipfs-cmds"
|
|
"gx/ipfs/QmcRKBUqc2p3L1ZraoJjbXfs9E6xzvEuyK9iypb5RGwfsr/go-ipfs-config"
|
|
"gx/ipfs/Qmde5VP1qUkyQXKCfmEUA7bP64V2HAptbJ7phuPp7jXWwg/go-ipfs-cmdkit"
|
|
)
|
|
|
|
// ConfigUpdateOutput is config profile apply command's output
|
|
type ConfigUpdateOutput struct {
|
|
OldCfg map[string]interface{}
|
|
NewCfg map[string]interface{}
|
|
}
|
|
|
|
type ConfigField struct {
|
|
Key string
|
|
Value interface{}
|
|
}
|
|
|
|
const (
|
|
configBoolOptionName = "bool"
|
|
configJSONOptionName = "json"
|
|
)
|
|
|
|
var ConfigCmd = &cmds.Command{
|
|
Helptext: cmdkit.HelpText{
|
|
Tagline: "Get and set ipfs config values.",
|
|
ShortDescription: `
|
|
'ipfs config' controls configuration variables. It works like 'git config'.
|
|
The configuration values are stored in a config file inside your ipfs
|
|
repository.`,
|
|
LongDescription: `
|
|
'ipfs config' controls configuration variables. It works
|
|
much like 'git config'. The configuration values are stored in a config
|
|
file inside your IPFS repository.
|
|
|
|
Examples:
|
|
|
|
Get the value of the 'Datastore.Path' key:
|
|
|
|
$ ipfs config Datastore.Path
|
|
|
|
Set the value of the 'Datastore.Path' key:
|
|
|
|
$ ipfs config Datastore.Path ~/.ipfs/datastore
|
|
`,
|
|
},
|
|
Subcommands: map[string]*cmds.Command{
|
|
"show": configShowCmd,
|
|
"edit": configEditCmd,
|
|
"replace": configReplaceCmd,
|
|
"profile": configProfileCmd,
|
|
},
|
|
Arguments: []cmdkit.Argument{
|
|
cmdkit.StringArg("key", true, false, "The key of the config entry (e.g. \"Addresses.API\")."),
|
|
cmdkit.StringArg("value", false, false, "The value to set the config entry to."),
|
|
},
|
|
Options: []cmdkit.Option{
|
|
cmdkit.BoolOption(configBoolOptionName, "Set a boolean value."),
|
|
cmdkit.BoolOption(configJSONOptionName, "Parse stringified JSON."),
|
|
},
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
args := req.Arguments
|
|
key := args[0]
|
|
|
|
var output *ConfigField
|
|
|
|
// This is a temporary fix until we move the private key out of the config file
|
|
switch strings.ToLower(key) {
|
|
case "identity", "identity.privkey":
|
|
return fmt.Errorf("cannot show or change private key through API")
|
|
default:
|
|
}
|
|
|
|
cfgRoot, err := cmdenv.GetConfigRoot(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r, err := fsrepo.Open(cfgRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
if len(args) == 2 {
|
|
value := args[1]
|
|
|
|
if parseJSON, _ := req.Options[configJSONOptionName].(bool); parseJSON {
|
|
var jsonVal interface{}
|
|
if err := json.Unmarshal([]byte(value), &jsonVal); err != nil {
|
|
err = fmt.Errorf("failed to unmarshal json. %s", err)
|
|
return err
|
|
}
|
|
|
|
output, err = setConfig(r, key, jsonVal)
|
|
} else if isbool, _ := req.Options[configBoolOptionName].(bool); isbool {
|
|
output, err = setConfig(r, key, value == "true")
|
|
} else {
|
|
output, err = setConfig(r, key, value)
|
|
}
|
|
} else {
|
|
output, err = getConfig(r, key)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return res.Emit(output)
|
|
},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *ConfigField) error {
|
|
if len(req.Arguments) == 2 {
|
|
return nil
|
|
}
|
|
|
|
buf, err := config.HumanOutput(out.Value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf = append(buf, byte('\n'))
|
|
|
|
w.Write(buf)
|
|
return nil
|
|
}),
|
|
},
|
|
Type: ConfigField{},
|
|
}
|
|
|
|
var configShowCmd = &cmds.Command{
|
|
Helptext: cmdkit.HelpText{
|
|
Tagline: "Output config file contents.",
|
|
ShortDescription: `
|
|
NOTE: For security reasons, this command will omit your private key. If you would like to make a full backup of your config (private key included), you must copy the config file from your repo.
|
|
`,
|
|
},
|
|
Type: map[string]interface{}{},
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
cfgRoot, err := cmdenv.GetConfigRoot(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fname, err := config.Filename(cfgRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data, err := ioutil.ReadFile(fname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var cfg map[string]interface{}
|
|
err = json.Unmarshal(data, &cfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = scrubValue(cfg, []string{config.IdentityTag, config.PrivKeyTag})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return cmds.EmitOnce(res, &cfg)
|
|
},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *map[string]interface{}) error {
|
|
buf, err := config.HumanOutput(out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf = append(buf, byte('\n'))
|
|
w.Write(buf)
|
|
|
|
return nil
|
|
}),
|
|
},
|
|
}
|
|
|
|
func scrubValue(m map[string]interface{}, key []string) error {
|
|
find := func(m map[string]interface{}, k string) (string, interface{}, bool) {
|
|
lckey := strings.ToLower(k)
|
|
for mkey, val := range m {
|
|
lcmkey := strings.ToLower(mkey)
|
|
if lckey == lcmkey {
|
|
return mkey, val, true
|
|
}
|
|
}
|
|
return "", nil, false
|
|
}
|
|
|
|
cur := m
|
|
for _, k := range key[:len(key)-1] {
|
|
foundk, val, ok := find(cur, k)
|
|
if !ok {
|
|
return fmt.Errorf("failed to find specified key")
|
|
}
|
|
|
|
if foundk != k {
|
|
// case mismatch, calling this an error
|
|
return fmt.Errorf("case mismatch in config, expected %q but got %q", k, foundk)
|
|
}
|
|
|
|
mval, mok := val.(map[string]interface{})
|
|
if !mok {
|
|
return fmt.Errorf("%s was not a map", foundk)
|
|
}
|
|
|
|
cur = mval
|
|
}
|
|
|
|
todel, _, ok := find(cur, key[len(key)-1])
|
|
if !ok {
|
|
return fmt.Errorf("%s, not found", strings.Join(key, "."))
|
|
}
|
|
|
|
delete(cur, todel)
|
|
return nil
|
|
}
|
|
|
|
var configEditCmd = &cmds.Command{
|
|
Helptext: cmdkit.HelpText{
|
|
Tagline: "Open the config file for editing in $EDITOR.",
|
|
ShortDescription: `
|
|
To use 'ipfs config edit', you must have the $EDITOR environment
|
|
variable set to your preferred text editor.
|
|
`,
|
|
},
|
|
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
cfgRoot, err := cmdenv.GetConfigRoot(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
filename, err := config.Filename(cfgRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return editConfig(filename)
|
|
},
|
|
}
|
|
|
|
var configReplaceCmd = &cmds.Command{
|
|
Helptext: cmdkit.HelpText{
|
|
Tagline: "Replace the config with <file>.",
|
|
ShortDescription: `
|
|
Make sure to back up the config file first if necessary, as this operation
|
|
can't be undone.
|
|
`,
|
|
},
|
|
|
|
Arguments: []cmdkit.Argument{
|
|
cmdkit.FileArg("file", true, false, "The file to use as the new config."),
|
|
},
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
cfgRoot, err := cmdenv.GetConfigRoot(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r, err := fsrepo.Open(cfgRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
|
|
file, err := cmdenv.GetFileArg(req.Files.Entries())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
return replaceConfig(r, file)
|
|
},
|
|
}
|
|
|
|
var configProfileCmd = &cmds.Command{
|
|
Helptext: cmdkit.HelpText{
|
|
Tagline: "Apply profiles to config.",
|
|
ShortDescription: fmt.Sprintf(`
|
|
Available profiles:
|
|
%s
|
|
`, buildProfileHelp()),
|
|
},
|
|
|
|
Subcommands: map[string]*cmds.Command{
|
|
"apply": configProfileApplyCmd,
|
|
},
|
|
}
|
|
|
|
var configProfileApplyCmd = &cmds.Command{
|
|
Helptext: cmdkit.HelpText{
|
|
Tagline: "Apply profile to config.",
|
|
},
|
|
Options: []cmdkit.Option{
|
|
cmdkit.BoolOption("dry-run", "print difference between the current config and the config that would be generated"),
|
|
},
|
|
Arguments: []cmdkit.Argument{
|
|
cmdkit.StringArg("profile", true, false, "The profile to apply to the config."),
|
|
},
|
|
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
|
|
profile, ok := config.Profiles[req.Arguments[0]]
|
|
if !ok {
|
|
return fmt.Errorf("%s is not a profile", req.Arguments[0])
|
|
}
|
|
|
|
dryRun, _ := req.Options["dry-run"].(bool)
|
|
cfgRoot, err := cmdenv.GetConfigRoot(env)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldCfg, newCfg, err := transformConfig(cfgRoot, req.Arguments[0], profile.Transform, dryRun)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
oldCfgMap, err := scrubPrivKey(oldCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newCfgMap, err := scrubPrivKey(newCfg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return cmds.EmitOnce(res, &ConfigUpdateOutput{
|
|
OldCfg: oldCfgMap,
|
|
NewCfg: newCfgMap,
|
|
})
|
|
},
|
|
Encoders: cmds.EncoderMap{
|
|
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *ConfigUpdateOutput) error {
|
|
diff := jsondiff.Compare(out.OldCfg, out.NewCfg)
|
|
buf := jsondiff.Format(diff)
|
|
|
|
w.Write(buf)
|
|
|
|
return nil
|
|
}),
|
|
},
|
|
Type: ConfigUpdateOutput{},
|
|
}
|
|
|
|
func buildProfileHelp() string {
|
|
var out string
|
|
|
|
for name, profile := range config.Profiles {
|
|
dlines := strings.Split(profile.Description, "\n")
|
|
for i := range dlines {
|
|
dlines[i] = " " + dlines[i]
|
|
}
|
|
|
|
out = out + fmt.Sprintf(" '%s':\n%s\n", name, strings.Join(dlines, "\n"))
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// scrubPrivKey scrubs private key for security reasons.
|
|
func scrubPrivKey(cfg *config.Config) (map[string]interface{}, error) {
|
|
cfgMap, err := config.ToMap(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = scrubValue(cfgMap, []string{config.IdentityTag, config.PrivKeyTag})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfgMap, nil
|
|
}
|
|
|
|
// transformConfig returns old config and new config instead of difference between they,
|
|
// because apply command can provide stable API through this way.
|
|
// If dryRun is true, repo's config should not be updated and persisted
|
|
// to storage. Otherwise, repo's config should be updated and persisted
|
|
// to storage.
|
|
func transformConfig(configRoot string, configName string, transformer config.Transformer, dryRun bool) (*config.Config, *config.Config, error) {
|
|
r, err := fsrepo.Open(configRoot)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer r.Close()
|
|
|
|
oldCfg, err := r.Config()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// make a copy to avoid updating repo's config unintentionally
|
|
newCfg, err := oldCfg.Clone()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = transformer(newCfg)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if !dryRun {
|
|
_, err = r.BackupConfig("pre-" + configName + "-")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
err = r.SetConfig(newCfg)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
return oldCfg, newCfg, nil
|
|
}
|
|
|
|
func getConfig(r repo.Repo, key string) (*ConfigField, error) {
|
|
value, err := r.GetConfigKey(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get config value: %q", err)
|
|
}
|
|
return &ConfigField{
|
|
Key: key,
|
|
Value: value,
|
|
}, nil
|
|
}
|
|
|
|
func setConfig(r repo.Repo, key string, value interface{}) (*ConfigField, error) {
|
|
err := r.SetConfigKey(key, value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to set config value: %s (maybe use --json?)", err)
|
|
}
|
|
return getConfig(r, key)
|
|
}
|
|
|
|
func editConfig(filename string) error {
|
|
editor := os.Getenv("EDITOR")
|
|
if editor == "" {
|
|
return errors.New("ENV variable $EDITOR not set")
|
|
}
|
|
|
|
cmd := exec.Command("sh", "-c", editor+" "+filename)
|
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
|
return cmd.Run()
|
|
}
|
|
|
|
func replaceConfig(r repo.Repo, file io.Reader) error {
|
|
var cfg config.Config
|
|
if err := json.NewDecoder(file).Decode(&cfg); err != nil {
|
|
return errors.New("failed to decode file as config")
|
|
}
|
|
if len(cfg.Identity.PrivKey) != 0 {
|
|
return errors.New("setting private key with API is not supported")
|
|
}
|
|
|
|
keyF, err := getConfig(r, config.PrivKeySelector)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get PrivKey")
|
|
}
|
|
|
|
pkstr, ok := keyF.Value.(string)
|
|
if !ok {
|
|
return fmt.Errorf("private key in config was not a string")
|
|
}
|
|
|
|
cfg.Identity.PrivKey = pkstr
|
|
|
|
return r.SetConfig(&cfg)
|
|
}
|