@cryptotaxi247 / kubo / commits / 5040fee90

implement support for --api option

This commit adds support for the --api option, which allows users to specify an API endpoint to run the cli command against. It enables much easier control of remote daemons. It also - ensures the API server version matches the API client - implements support for the $IPFS_PATH/api file Still TODO: - tests! - multiaddr to support /dns/ License: MIT Signed-off-by: Juan Batiz-Benet <juan@benet.ai>

Juan Batiz-Benet committed Aug 27, 2015 at 07:28 UTC 5040fee906305e074777d4e545c5a35fee35f5b0
6 files changed +210 -47
cmd/ipfs/daemon.go
+14 -3
@@ -292,9 +292,16 @@ func serveHTTPApi(req cmds.Request) (error, <-chan error) {
292 return fmt.Errorf("serveHTTPApi: GetConfig() failed: %s", err), nil
293 }
294
295 - apiMaddr, err := ma.NewMultiaddr(cfg.Addresses.API)
295 + apiAddr, _, err := req.Option(commands.ApiOption).String()
296 if err != nil {
297 - return fmt.Errorf("serveHTTPApi: invalid API address: %q (err: %s)", cfg.Addresses.API, err), nil
297 + return fmt.Errorf("serveHTTPApi: %s", err), nil
298 + }
299 + if apiAddr == "" {
300 + apiAddr = cfg.Addresses.API
301 + }
302 + apiMaddr, err := ma.NewMultiaddr(apiAddr)
303 + if err != nil {
304 + return fmt.Errorf("serveHTTPApi: invalid API address: %q (err: %s)", apiAddr, err), nil
305 }
306
307 apiLis, err := manet.Listen(apiMaddr)
@@ -344,7 +351,11 @@ func serveHTTPApi(req cmds.Request) (error, <-chan error) {
351
352 node, err := req.InvocContext().ConstructNode()
353 if err != nil {
347 - return fmt.Errorf("serveHTTPGateway: ConstructNode() failed: %s", err), nil
354 + return fmt.Errorf("serveHTTPApi: ConstructNode() failed: %s", err), nil
355 + }
356 +
357 + if err := node.Repo.SetAPIAddr(apiAddr); err != nil {
358 + return fmt.Errorf("serveHTTPApi: SetAPIAddr() failed: %s", err), nil
359 }
360
361 errc := make(chan error)
cmd/ipfs/main.go
+136 -40
@@ -23,6 +23,8 @@ import (
23 cmdsCli "github.com/ipfs/go-ipfs/commands/cli"
24 cmdsHttp "github.com/ipfs/go-ipfs/commands/http"
25 core "github.com/ipfs/go-ipfs/core"
26 + coreCmds "github.com/ipfs/go-ipfs/core/commands"
27 + repo "github.com/ipfs/go-ipfs/repo"
28 config "github.com/ipfs/go-ipfs/repo/config"
29 fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
30 eventlog "github.com/ipfs/go-ipfs/thirdparty/eventlog"
@@ -32,8 +34,10 @@ import (
34 // log is the command logger
35 var log = eventlog.Logger("cmd/ipfs")
36
35 -// signal to output help
36 -var errHelpRequested = errors.New("Help Requested")
37 +var (
38 + errUnexpectedApiOutput = errors.New("api returned unexpected output")
39 + errApiVersionMismatch = errors.New("api version mismatch")
40 +)
41
42 const (
43 EnvEnableProfiling = "IPFS_PROF"
@@ -292,8 +296,7 @@ func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd
296 return nil, err
297 }
298
295 - log.Debug("looking for running daemon...")
296 - useDaemon, err := commandShouldRunOnDaemon(*details, req, root)
299 + client, err := commandShouldRunOnDaemon(*details, req, root)
300 if err != nil {
301 return nil, err
302 }
@@ -310,28 +313,13 @@ func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd
313 }
314 }
315
313 - if useDaemon {
314 -
315 - cfg, err := req.InvocContext().GetConfig()
316 - if err != nil {
317 - return nil, err
318 - }
319 -
320 - addr, err := ma.NewMultiaddr(cfg.Addresses.API)
321 - if err != nil {
322 - return nil, err
323 - }
324 -
325 - log.Infof("Executing command on daemon running at %s", addr)
326 - _, host, err := manet.DialArgs(addr)
327 - if err != nil {
328 - return nil, err
329 - }
330 -
331 - client := cmdsHttp.NewClient(host)
332 -
316 + if client != nil {
317 + log.Debug("Executing command via API")
318 res, err = client.Send(req)
319 if err != nil {
320 + if isConnRefused(err) {
321 + err = repo.ErrApiNotRunning
322 + }
323 return nil, err
324 }
325
@@ -380,48 +368,67 @@ func commandDetails(path []string, root *cmds.Command) (*cmdDetails, error) {
368 // commandShouldRunOnDaemon determines, from commmand details, whether a
369 // command ought to be executed on an IPFS daemon.
370 //
383 -// It returns true if the command should be executed on a daemon and false if
371 +// It returns a client if the command should be executed on a daemon and nil if
372 // it should be executed on a client. It returns an error if the command must
373 // NOT be executed on either.
386 -func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (bool, error) {
374 +func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (cmdsHttp.Client, error) {
375 path := req.Path()
376 // root command.
377 if len(path) < 1 {
390 - return false, nil
378 + return nil, nil
379 }
380
381 if details.cannotRunOnClient && details.cannotRunOnDaemon {
394 - return false, fmt.Errorf("command disabled: %s", path[0])
382 + return nil, fmt.Errorf("command disabled: %s", path[0])
383 }
384
385 if details.doesNotUseRepo && details.canRunOnClient() {
398 - return false, nil
386 + return nil, nil
387 }
388
401 - // at this point need to know whether daemon is running. we defer
402 - // to this point so that some commands dont open files unnecessarily.
403 - daemonLocked, err := fsrepo.LockedByOtherProcess(req.InvocContext().ConfigRoot)
389 + // at this point need to know whether api is running. we defer
390 + // to this point so that we dont check unnecessarily
391 +
392 + // did user specify an api to use for this command?
393 + apiAddrStr, _, err := req.Option(coreCmds.ApiOption).String()
394 if err != nil {
405 - return false, err
395 + return nil, err
396 }
397
408 - if daemonLocked {
398 + client, err := getApiClient(req.InvocContext().ConfigRoot, apiAddrStr)
399 + if err == repo.ErrApiNotRunning {
400 + if apiAddrStr != "" && req.Command() != daemonCmd {
401 + // if user SPECIFIED an api, and this cmd is not daemon
402 + // we MUST use it. so error out.
403 + return nil, err
404 + }
405
410 - log.Info("a daemon is running...")
406 + // ok for api not to be running
407 + } else if err != nil { // some other api error
408 + return nil, err
409 + }
410
411 + if client != nil { // daemon is running
412 if details.cannotRunOnDaemon {
413 - e := "ipfs daemon is running. please stop it to run this command"
414 - return false, cmds.ClientError(e)
413 + e := "cannot use API with this command."
414 +
415 + // check if daemon locked. legacy error text, for now.
416 + daemonLocked, _ := fsrepo.LockedByOtherProcess(req.InvocContext().ConfigRoot)
417 + if daemonLocked {
418 + e = "ipfs daemon is running. please stop it to run this command"
419 + }
420 +
421 + return nil, cmds.ClientError(e)
422 }
423
417 - return true, nil
424 + return client, nil
425 }
426
427 if details.cannotRunOnClient {
421 - return false, cmds.ClientError("must run on the ipfs daemon")
428 + return nil, cmds.ClientError("must run on the ipfs daemon")
429 }
430
424 - return false, nil
431 + return nil, nil
432 }
433
434 func isClientError(err error) bool {
@@ -571,3 +578,92 @@ func profileIfEnabled() (func(), error) {
578 }
579 return func() {}, nil
580 }
581 +
582 +// getApiClient checks the repo, and the given options, checking for
583 +// a running API service. if there is one, it returns a client.
584 +// otherwise, it returns errApiNotRunning, or another error.
585 +func getApiClient(repoPath, apiAddrStr string) (cmdsHttp.Client, error) {
586 +
587 + if apiAddrStr == "" {
588 + var err error
589 + if apiAddrStr, err = fsrepo.APIAddr(repoPath); err != nil {
590 + return nil, err
591 + }
592 + }
593 +
594 + addr, err := ma.NewMultiaddr(apiAddrStr)
595 + if err != nil {
596 + return nil, err
597 + }
598 +
599 + client, err := apiClientForAddr(addr)
600 + if err != nil {
601 + return nil, err
602 + }
603 +
604 + // make sure the api is actually running.
605 + // this is slow, as it might mean an RTT to a remote server.
606 + // TODO: optimize some way
607 + if err := apiVersionMatches(client); err != nil {
608 + return nil, err
609 + }
610 +
611 + return client, nil
612 +}
613 +
614 +// apiVersionMatches checks whether the api server is running the
615 +// same version of go-ipfs. for now, only the exact same version of
616 +// client + server work. In the future, we should use semver for
617 +// proper API versioning! \o/
618 +func apiVersionMatches(client cmdsHttp.Client) (err error) {
619 + ver, err := doVersionRequest(client)
620 + if err != nil {
621 + return err
622 + }
623 +
624 + currv := config.CurrentVersionNumber
625 + if ver.Version != currv {
626 + return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, ver.Version, currv)
627 + }
628 + return nil
629 +}
630 +
631 +func doVersionRequest(client cmdsHttp.Client) (*coreCmds.VersionOutput, error) {
632 + cmd := coreCmds.VersionCmd
633 + optDefs, err := cmd.GetOptions([]string{})
634 + if err != nil {
635 + return nil, err
636 + }
637 +
638 + req, err := cmds.NewRequest([]string{"version"}, nil, nil, nil, cmd, optDefs)
639 + if err != nil {
640 + return nil, err
641 + }
642 +
643 + res, err := client.Send(req)
644 + if err != nil {
645 + if isConnRefused(err) {
646 + err = repo.ErrApiNotRunning
647 + }
648 + return nil, err
649 + }
650 +
651 + ver, ok := res.Output().(*coreCmds.VersionOutput)
652 + if !ok {
653 + return nil, errUnexpectedApiOutput
654 + }
655 + return ver, nil
656 +}
657 +
658 +func apiClientForAddr(addr ma.Multiaddr) (cmdsHttp.Client, error) {
659 + _, host, err := manet.DialArgs(addr)
660 + if err != nil {
661 + return nil, err
662 + }
663 +
664 + return cmdsHttp.NewClient(host), nil
665 +}
666 +
667 +func isConnRefused(err error) bool {
668 + return strings.Contains(err.Error(), "connection refused")
669 +}
core/commands/root.go
+5
@@ -16,6 +16,10 @@ type TestOutput struct {
16 Bar int
17 }
18
19 +const (
20 + ApiOption = "api"
21 +)
22 +
23 var Root = &cmds.Command{
24 Helptext: cmds.HelpText{
25 Tagline: "global p2p merkle-dag filesystem",
@@ -73,6 +77,7 @@ Use 'ipfs <command> --help' to learn more about each command.
77 cmds.BoolOption("help", "Show the full command help text"),
78 cmds.BoolOption("h", "Show a short version of the command help text"),
79 cmds.BoolOption("local", "L", "Run the command locally, instead of using the daemon"),
80 + cmds.StringOption(ApiOption, "Overrides the routing option (dht, supernode)"),
81 },
82 }
83
repo/fsrepo/fsrepo.go
+44 -4
@@ -58,6 +58,7 @@ func (err NoRepoError) Error() string {
58 const (
59 leveldbDirectory = "datastore"
60 flatfsDirectory = "blocks"
61 + apiFile = "api"
62 )
63
64 var (
@@ -285,14 +286,53 @@ func Remove(repoPath string) error {
286 // process. If true, then the repo cannot be opened by this process.
287 func LockedByOtherProcess(repoPath string) (bool, error) {
288 repoPath = path.Clean(repoPath)
288 -
289 - // TODO replace this with the "api" file
290 - // https://github.com/ipfs/specs/tree/master/repo/fs-repo
291 -
289 // NB: the lock is only held when repos are Open
290 return lockfile.Locked(repoPath)
291 }
292
293 +// APIAddr returns the registered API addr, according to the api file
294 +// in the fsrepo. This is a concurrent operation, meaning that any
295 +// process may read this file. modifying this file, therefore, should
296 +// use "mv" to replace the whole file and avoid interleaved read/writes.
297 +func APIAddr(repoPath string) (string, error) {
298 + repoPath = path.Clean(repoPath)
299 + apiFilePath := path.Join(repoPath, apiFile)
300 +
301 + // if there is no file, assume there is no api addr.
302 + f, err := os.Open(apiFilePath)
303 + if err != nil {
304 + if os.IsNotExist(err) {
305 + return "", repo.ErrApiNotRunning
306 + }
307 + return "", err
308 + }
309 + defer f.Close()
310 +
311 + // read up to 2048 bytes. io.ReadAll is a vulnerability, as
312 + // someone could hose the process by putting a massive file there.
313 + buf := make([]byte, 2048)
314 + n, err := f.Read(buf)
315 + if err != nil && err != io.EOF {
316 + return "", err
317 + }
318 +
319 + s := string(buf[:n])
320 + s = strings.TrimSpace(s)
321 + return s, nil
322 +}
323 +
324 +// SetAPIAddr writes the API Addr to the /api file.
325 +func (r *FSRepo) SetAPIAddr(addr string) error {
326 + f, err := os.Create(path.Join(r.path, apiFile))
327 + if err != nil {
328 + return err
329 + }
330 + defer f.Close()
331 +
332 + _, err = f.WriteString(addr)
333 + return err
334 +}
335 +
336 // openConfig returns an error if the config file is not present.
337 func (r *FSRepo) openConfig() error {
338 configFilename, err := config.Filename(r.path)
repo/mock.go
+2
@@ -35,3 +35,5 @@ func (m *Mock) GetConfigKey(key string) (interface{}, error) {
35 func (m *Mock) Datastore() ds.ThreadSafeDatastore { return m.D }
36
37 func (m *Mock) Close() error { return errTODO }
38 +
39 +func (m *Mock) SetAPIAddr(addr string) error { return errTODO }
repo/repo.go
+9
@@ -1,12 +1,18 @@
1 package repo
2
3 import (
4 + "errors"
5 "io"
6
7 datastore "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-datastore"
8 +
9 config "github.com/ipfs/go-ipfs/repo/config"
10 )
11
12 +var (
13 + ErrApiNotRunning = errors.New("api not running")
14 +)
15 +
16 type Repo interface {
17 Config() *config.Config
18 SetConfig(*config.Config) error
@@ -16,5 +22,8 @@ type Repo interface {
22
23 Datastore() datastore.ThreadSafeDatastore
24
25 + // SetAPIAddr sets the API address in the repo.
26 + SetAPIAddr(addr string) error
27 +
28 io.Closer
29 }