config.go 9.92 KB
Newer Older
1 2 3
package commands

import (
4
	"bytes"
5
	"encoding/json"
6 7 8
	"errors"
	"fmt"
	"io"
9
	"io/ioutil"
10 11
	"os"
	"os/exec"
12
	"strings"
13

14
	cmds "github.com/ipfs/go-ipfs/commands"
Jan Winkelmann's avatar
Jan Winkelmann committed
15
	e "github.com/ipfs/go-ipfs/core/commands/e"
16 17 18
	repo "github.com/ipfs/go-ipfs/repo"
	config "github.com/ipfs/go-ipfs/repo/config"
	fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
19

Steven Allen's avatar
Steven Allen committed
20
	"gx/ipfs/QmQp2a2Hhb7F6eK2A5hN8f9aJy4mtkEikL9Zj4cgB7d1dD/go-ipfs-cmdkit"
21 22 23 24 25 26 27
)

type ConfigField struct {
	Key   string
	Value interface{}
}

28
var ConfigCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
29
	Helptext: cmdkit.HelpText{
30
		Tagline: "Get and set ipfs config values.",
31
		ShortDescription: `
Richard Littauer's avatar
Richard Littauer committed
32
'ipfs config' controls configuration variables. It works like 'git config'.
33
The configuration values are stored in a config file inside your ipfs
34 35
repository.`,
		LongDescription: `
Richard Littauer's avatar
Richard Littauer committed
36
'ipfs config' controls configuration variables. It works
37
much like 'git config'. The configuration values are stored in a config
38
file inside your IPFS repository.
39

40
Examples:
41

David Brennan's avatar
David Brennan committed
42
Get the value of the 'Datastore.Path' key:
43

David Brennan's avatar
David Brennan committed
44
  $ ipfs config Datastore.Path
45

David Brennan's avatar
David Brennan committed
46
Set the value of the 'Datastore.Path' key:
47

David Brennan's avatar
David Brennan committed
48
  $ ipfs config Datastore.Path ~/.ipfs/datastore
49
`,
50
	},
51

Jan Winkelmann's avatar
Jan Winkelmann committed
52 53 54
	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."),
55
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
56
	Options: []cmdkit.Option{
57 58
		cmdkit.BoolOption("bool", "Set a boolean value."),
		cmdkit.BoolOption("json", "Parse stringified JSON."),
59
	},
60
	Run: func(req cmds.Request, res cmds.Response) {
61
		args := req.Arguments()
62
		key := args[0]
63

Jan Winkelmann's avatar
Jan Winkelmann committed
64 65 66 67 68 69 70 71 72
		var output *ConfigField
		defer func() {
			if output != nil {
				res.SetOutput(output)
			} else {
				res.SetOutput(nil)
			}
		}()

73
		// This is a temporary fix until we move the private key out of the config file
74 75
		switch strings.ToLower(key) {
		case "identity", "identity.privkey":
Jan Winkelmann's avatar
Jan Winkelmann committed
76
			res.SetError(fmt.Errorf("cannot show or change private key through API"), cmdkit.ErrNormal)
77 78 79 80
			return
		default:
		}

Jeromy's avatar
Jeromy committed
81
		r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
82
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
83
			res.SetError(err, cmdkit.ErrNormal)
84
			return
85
		}
86
		defer r.Close()
87
		if len(args) == 2 {
88
			value := args[1]
89 90 91 92 93

			if parseJson, _, _ := req.Option("json").Bool(); parseJson {
				var jsonVal interface{}
				if err := json.Unmarshal([]byte(value), &jsonVal); err != nil {
					err = fmt.Errorf("failed to unmarshal json. %s", err)
Jan Winkelmann's avatar
Jan Winkelmann committed
94
					res.SetError(err, cmdkit.ErrNormal)
95 96 97 98 99
					return
				}

				output, err = setConfig(r, key, jsonVal)
			} else if isbool, _, _ := req.Option("bool").Bool(); isbool {
100 101 102 103
				output, err = setConfig(r, key, value == "true")
			} else {
				output, err = setConfig(r, key, value)
			}
104
		} else {
105 106 107
			output, err = getConfig(r, key)
		}
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
108
			res.SetError(err, cmdkit.ErrNormal)
109
			return
110 111
		}
	},
112
	Marshalers: cmds.MarshalerMap{
113
		cmds.Text: func(res cmds.Response) (io.Reader, error) {
114
			if len(res.Request().Arguments()) == 2 {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
115
				return nil, nil // dont output anything
116 117
			}

Jan Winkelmann's avatar
Jan Winkelmann committed
118 119
			if res.Error() != nil {
				return nil, res.Error()
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
120
			}
Jan Winkelmann's avatar
Jan Winkelmann committed
121 122 123 124 125 126

			v, err := unwrapOutput(res.Output())
			if err != nil {
				return nil, err
			}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
127 128
			vf, ok := v.(*ConfigField)
			if !ok {
Jan Winkelmann's avatar
Jan Winkelmann committed
129
				return nil, e.TypeErr(vf, v)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
130 131 132
			}

			buf, err := config.HumanOutput(vf.Value)
133 134 135
			if err != nil {
				return nil, err
			}
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
136
			buf = append(buf, byte('\n'))
137
			return bytes.NewReader(buf), nil
138 139
		},
	},
140
	Type: ConfigField{},
141
	Subcommands: map[string]*cmds.Command{
142 143 144
		"show":    configShowCmd,
		"edit":    configEditCmd,
		"replace": configReplaceCmd,
145
		"profile": configProfileCmd,
146 147 148 149
	},
}

var configShowCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
150
	Helptext: cmdkit.HelpText{
151
		Tagline: "Output config file contents.",
152 153
		ShortDescription: `
WARNING: Your private key is stored in the config file, and it will be
154 155
included in the output of this command.
`,
156
	},
157

158
	Run: func(req cmds.Request, res cmds.Response) {
keks's avatar
keks committed
159 160
		cfgPath := req.InvocContext().ConfigRoot
		fname, err := config.Filename(cfgPath)
161
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
162
			res.SetError(err, cmdkit.ErrNormal)
163
			return
164 165
		}

166
		data, err := ioutil.ReadFile(fname)
167
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
168
			res.SetError(err, cmdkit.ErrNormal)
169 170
			return
		}
171 172 173 174

		var cfg map[string]interface{}
		err = json.Unmarshal(data, &cfg)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
175
			res.SetError(err, cmdkit.ErrNormal)
176 177 178
			return
		}

179 180
		err = scrubValue(cfg, []string{config.IdentityTag, config.PrivKeyTag})
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
181
			res.SetError(err, cmdkit.ErrNormal)
Jeromy's avatar
Jeromy committed
182 183 184
			return
		}

185 186
		output, err := config.HumanOutput(cfg)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
187
			res.SetError(err, cmdkit.ErrNormal)
188 189 190 191
			return
		}

		res.SetOutput(bytes.NewReader(output))
192 193 194
	},
}

195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
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
}

236
var configEditCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
237
	Helptext: cmdkit.HelpText{
238
		Tagline: "Open the config file for editing in $EDITOR.",
239 240
		ShortDescription: `
To use 'ipfs config edit', you must have the $EDITOR environment
241 242
variable set to your preferred text editor.
`,
243
	},
244

245
	Run: func(req cmds.Request, res cmds.Response) {
Jeromy's avatar
Jeromy committed
246
		filename, err := config.Filename(req.InvocContext().ConfigRoot)
247
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
248
			res.SetError(err, cmdkit.ErrNormal)
249
			return
250 251
		}

252 253
		err = editConfig(filename)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
254
			res.SetError(err, cmdkit.ErrNormal)
255
		}
256 257 258
	},
}

259
var configReplaceCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
260
	Helptext: cmdkit.HelpText{
261
		Tagline: "Replace the config with <file>.",
262
		ShortDescription: `
263
Make sure to back up the config file first if necessary, as this operation
264 265 266 267
can't be undone.
`,
	},

Jan Winkelmann's avatar
Jan Winkelmann committed
268 269
	Arguments: []cmdkit.Argument{
		cmdkit.FileArg("file", true, false, "The file to use as the new config."),
270
	},
271
	Run: func(req cmds.Request, res cmds.Response) {
Jan Winkelmann's avatar
Jan Winkelmann committed
272 273 274
		// has to be called
		res.SetOutput(nil)

Jeromy's avatar
Jeromy committed
275
		r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
276
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
277
			res.SetError(err, cmdkit.ErrNormal)
278
			return
279 280 281 282 283
		}
		defer r.Close()

		file, err := req.Files().NextFile()
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
284
			res.SetError(err, cmdkit.ErrNormal)
285
			return
286 287 288
		}
		defer file.Close()

289 290
		err = replaceConfig(r, file)
		if err != nil {
Jan Winkelmann's avatar
Jan Winkelmann committed
291
			res.SetError(err, cmdkit.ErrNormal)
292 293
			return
		}
294 295 296
	},
}

297 298 299 300 301 302
var configProfileCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Apply profiles to config.",
	},

	Subcommands: map[string]*cmds.Command{
303 304
		"apply":  configProfileApplyCmd,
		"revert": configProfileRevertCmd,
305 306 307 308 309 310 311 312 313 314 315
	},
}

var configProfileApplyCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Apply profile to config.",
	},
	Arguments: []cmds.Argument{
		cmds.StringArg("profile", true, false, "The profile to apply to the config."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
Łukasz Magiera's avatar
Łukasz Magiera committed
316
		profile, ok := config.Profiles[req.Arguments()[0]]
317
		if !ok {
Łukasz Magiera's avatar
Łukasz Magiera committed
318
			res.SetError(fmt.Errorf("%s is not a profile", req.Arguments()[0]), cmds.ErrNormal)
319 320 321
			return
		}

322
		err := transformConfig(req.InvocContext().ConfigRoot, profile.Apply)
323 324 325 326
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
327 328
	},
}
329

330 331 332 333
var configProfileRevertCmd = &cmds.Command{
	Helptext: cmds.HelpText{
		Tagline: "Revert profile changes.",
		ShortDescription: `Reverts profile-related changes to the config.
334

335 336 337 338 339 340 341
Reverting some profiles may damage the configuration or not be possible.
Backing up the config before running this command is advised.`,
	},
	Arguments: []cmds.Argument{
		cmds.StringArg("profile", true, false, "The profile to apply to the config."),
	},
	Run: func(req cmds.Request, res cmds.Response) {
Łukasz Magiera's avatar
Łukasz Magiera committed
342
		profile, ok := config.Profiles[req.Arguments()[0]]
343
		if !ok {
Łukasz Magiera's avatar
Łukasz Magiera committed
344
			res.SetError(fmt.Errorf("%s is not a profile", req.Arguments()[0]), cmds.ErrNormal)
345 346 347
			return
		}

348
		err := transformConfig(req.InvocContext().ConfigRoot, profile.Unapply)
349 350 351 352 353 354 355
		if err != nil {
			res.SetError(err, cmds.ErrNormal)
			return
		}
	},
}

356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
func transformConfig(configRoot string, transformer config.Transformer) error {
	r, err := fsrepo.Open(configRoot)
	if err != nil {
		return err
	}
	defer r.Close()

	cfg, err := r.Config()
	if err != nil {
		return err
	}

	err = transformer(cfg)
	if err != nil {
		return err
	}

	return r.SetConfig(cfg)
}

376
func getConfig(r repo.Repo, key string) (*ConfigField, error) {
377
	value, err := r.GetConfigKey(key)
378
	if err != nil {
Richard Littauer's avatar
Richard Littauer committed
379
		return nil, fmt.Errorf("Failed to get config value: %q", err)
380 381 382 383 384 385 386
	}
	return &ConfigField{
		Key:   key,
		Value: value,
	}, nil
}

387
func setConfig(r repo.Repo, key string, value interface{}) (*ConfigField, error) {
388
	err := r.SetConfigKey(key, value)
389
	if err != nil {
390
		return nil, fmt.Errorf("failed to set config value: %s (maybe use --json?)", err)
391
	}
392
	return getConfig(r, key)
393 394 395 396 397 398 399 400 401 402 403 404
}

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()
}
405 406 407 408

func replaceConfig(r repo.Repo, file io.Reader) error {
	var cfg config.Config
	if err := json.NewDecoder(file).Decode(&cfg); err != nil {
409 410 411 412
		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")
413 414
	}

415
	keyF, err := getConfig(r, config.PrivKeySelector)
416 417 418
	if err != nil {
		return fmt.Errorf("Failed to get PrivKey")
	}
419 420 421 422 423 424 425

	pkstr, ok := keyF.Value.(string)
	if !ok {
		return fmt.Errorf("private key in config was not a string")
	}

	cfg.Identity.PrivKey = pkstr
426

427 428
	return r.SetConfig(&cfg)
}