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
}