1
0
mirror of https://github.com/ipfs/kubo.git synced 2025-09-10 09:52:20 +08:00

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>
This commit is contained in:
Juan Batiz-Benet
2015-08-27 07:28:27 +02:00
parent 7abebb1653
commit 5040fee906
6 changed files with 213 additions and 50 deletions

View File

@ -292,9 +292,16 @@ func serveHTTPApi(req cmds.Request) (error, <-chan error) {
return fmt.Errorf("serveHTTPApi: GetConfig() failed: %s", err), nil return fmt.Errorf("serveHTTPApi: GetConfig() failed: %s", err), nil
} }
apiMaddr, err := ma.NewMultiaddr(cfg.Addresses.API) apiAddr, _, err := req.Option(commands.ApiOption).String()
if err != nil { if err != nil {
return fmt.Errorf("serveHTTPApi: invalid API address: %q (err: %s)", cfg.Addresses.API, err), nil return fmt.Errorf("serveHTTPApi: %s", err), nil
}
if apiAddr == "" {
apiAddr = cfg.Addresses.API
}
apiMaddr, err := ma.NewMultiaddr(apiAddr)
if err != nil {
return fmt.Errorf("serveHTTPApi: invalid API address: %q (err: %s)", apiAddr, err), nil
} }
apiLis, err := manet.Listen(apiMaddr) apiLis, err := manet.Listen(apiMaddr)
@ -344,7 +351,11 @@ func serveHTTPApi(req cmds.Request) (error, <-chan error) {
node, err := req.InvocContext().ConstructNode() node, err := req.InvocContext().ConstructNode()
if err != nil { if err != nil {
return fmt.Errorf("serveHTTPGateway: ConstructNode() failed: %s", err), nil return fmt.Errorf("serveHTTPApi: ConstructNode() failed: %s", err), nil
}
if err := node.Repo.SetAPIAddr(apiAddr); err != nil {
return fmt.Errorf("serveHTTPApi: SetAPIAddr() failed: %s", err), nil
} }
errc := make(chan error) errc := make(chan error)

View File

@ -23,6 +23,8 @@ import (
cmdsCli "github.com/ipfs/go-ipfs/commands/cli" cmdsCli "github.com/ipfs/go-ipfs/commands/cli"
cmdsHttp "github.com/ipfs/go-ipfs/commands/http" cmdsHttp "github.com/ipfs/go-ipfs/commands/http"
core "github.com/ipfs/go-ipfs/core" core "github.com/ipfs/go-ipfs/core"
coreCmds "github.com/ipfs/go-ipfs/core/commands"
repo "github.com/ipfs/go-ipfs/repo"
config "github.com/ipfs/go-ipfs/repo/config" config "github.com/ipfs/go-ipfs/repo/config"
fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo" fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
eventlog "github.com/ipfs/go-ipfs/thirdparty/eventlog" eventlog "github.com/ipfs/go-ipfs/thirdparty/eventlog"
@ -32,8 +34,10 @@ import (
// log is the command logger // log is the command logger
var log = eventlog.Logger("cmd/ipfs") var log = eventlog.Logger("cmd/ipfs")
// signal to output help var (
var errHelpRequested = errors.New("Help Requested") errUnexpectedApiOutput = errors.New("api returned unexpected output")
errApiVersionMismatch = errors.New("api version mismatch")
)
const ( const (
EnvEnableProfiling = "IPFS_PROF" EnvEnableProfiling = "IPFS_PROF"
@ -292,8 +296,7 @@ func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd
return nil, err return nil, err
} }
log.Debug("looking for running daemon...") client, err := commandShouldRunOnDaemon(*details, req, root)
useDaemon, err := commandShouldRunOnDaemon(*details, req, root)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -310,28 +313,13 @@ func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd
} }
} }
if useDaemon { if client != nil {
log.Debug("Executing command via API")
cfg, err := req.InvocContext().GetConfig()
if err != nil {
return nil, err
}
addr, err := ma.NewMultiaddr(cfg.Addresses.API)
if err != nil {
return nil, err
}
log.Infof("Executing command on daemon running at %s", addr)
_, host, err := manet.DialArgs(addr)
if err != nil {
return nil, err
}
client := cmdsHttp.NewClient(host)
res, err = client.Send(req) res, err = client.Send(req)
if err != nil { if err != nil {
if isConnRefused(err) {
err = repo.ErrApiNotRunning
}
return nil, err return nil, err
} }
@ -380,48 +368,67 @@ func commandDetails(path []string, root *cmds.Command) (*cmdDetails, error) {
// commandShouldRunOnDaemon determines, from commmand details, whether a // commandShouldRunOnDaemon determines, from commmand details, whether a
// command ought to be executed on an IPFS daemon. // command ought to be executed on an IPFS daemon.
// //
// It returns true if the command should be executed on a daemon and false if // It returns a client if the command should be executed on a daemon and nil if
// it should be executed on a client. It returns an error if the command must // it should be executed on a client. It returns an error if the command must
// NOT be executed on either. // NOT be executed on either.
func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (bool, error) { func commandShouldRunOnDaemon(details cmdDetails, req cmds.Request, root *cmds.Command) (cmdsHttp.Client, error) {
path := req.Path() path := req.Path()
// root command. // root command.
if len(path) < 1 { if len(path) < 1 {
return false, nil return nil, nil
} }
if details.cannotRunOnClient && details.cannotRunOnDaemon { if details.cannotRunOnClient && details.cannotRunOnDaemon {
return false, fmt.Errorf("command disabled: %s", path[0]) return nil, fmt.Errorf("command disabled: %s", path[0])
} }
if details.doesNotUseRepo && details.canRunOnClient() { if details.doesNotUseRepo && details.canRunOnClient() {
return false, nil return nil, nil
} }
// at this point need to know whether daemon is running. we defer // at this point need to know whether api is running. we defer
// to this point so that some commands dont open files unnecessarily. // to this point so that we dont check unnecessarily
daemonLocked, err := fsrepo.LockedByOtherProcess(req.InvocContext().ConfigRoot)
// did user specify an api to use for this command?
apiAddrStr, _, err := req.Option(coreCmds.ApiOption).String()
if err != nil { if err != nil {
return false, err return nil, err
} }
if daemonLocked { client, err := getApiClient(req.InvocContext().ConfigRoot, apiAddrStr)
if err == repo.ErrApiNotRunning {
log.Info("a daemon is running...") if apiAddrStr != "" && req.Command() != daemonCmd {
// if user SPECIFIED an api, and this cmd is not daemon
if details.cannotRunOnDaemon { // we MUST use it. so error out.
e := "ipfs daemon is running. please stop it to run this command" return nil, err
return false, cmds.ClientError(e)
} }
return true, nil // ok for api not to be running
} else if err != nil { // some other api error
return nil, err
}
if client != nil { // daemon is running
if details.cannotRunOnDaemon {
e := "cannot use API with this command."
// check if daemon locked. legacy error text, for now.
daemonLocked, _ := fsrepo.LockedByOtherProcess(req.InvocContext().ConfigRoot)
if daemonLocked {
e = "ipfs daemon is running. please stop it to run this command"
}
return nil, cmds.ClientError(e)
}
return client, nil
} }
if details.cannotRunOnClient { if details.cannotRunOnClient {
return false, cmds.ClientError("must run on the ipfs daemon") return nil, cmds.ClientError("must run on the ipfs daemon")
} }
return false, nil return nil, nil
} }
func isClientError(err error) bool { func isClientError(err error) bool {
@ -571,3 +578,92 @@ func profileIfEnabled() (func(), error) {
} }
return func() {}, nil return func() {}, nil
} }
// getApiClient checks the repo, and the given options, checking for
// a running API service. if there is one, it returns a client.
// otherwise, it returns errApiNotRunning, or another error.
func getApiClient(repoPath, apiAddrStr string) (cmdsHttp.Client, error) {
if apiAddrStr == "" {
var err error
if apiAddrStr, err = fsrepo.APIAddr(repoPath); err != nil {
return nil, err
}
}
addr, err := ma.NewMultiaddr(apiAddrStr)
if err != nil {
return nil, err
}
client, err := apiClientForAddr(addr)
if err != nil {
return nil, err
}
// make sure the api is actually running.
// this is slow, as it might mean an RTT to a remote server.
// TODO: optimize some way
if err := apiVersionMatches(client); err != nil {
return nil, err
}
return client, nil
}
// apiVersionMatches checks whether the api server is running the
// same version of go-ipfs. for now, only the exact same version of
// client + server work. In the future, we should use semver for
// proper API versioning! \o/
func apiVersionMatches(client cmdsHttp.Client) (err error) {
ver, err := doVersionRequest(client)
if err != nil {
return err
}
currv := config.CurrentVersionNumber
if ver.Version != currv {
return fmt.Errorf("%s (%s != %s)", errApiVersionMismatch, ver.Version, currv)
}
return nil
}
func doVersionRequest(client cmdsHttp.Client) (*coreCmds.VersionOutput, error) {
cmd := coreCmds.VersionCmd
optDefs, err := cmd.GetOptions([]string{})
if err != nil {
return nil, err
}
req, err := cmds.NewRequest([]string{"version"}, nil, nil, nil, cmd, optDefs)
if err != nil {
return nil, err
}
res, err := client.Send(req)
if err != nil {
if isConnRefused(err) {
err = repo.ErrApiNotRunning
}
return nil, err
}
ver, ok := res.Output().(*coreCmds.VersionOutput)
if !ok {
return nil, errUnexpectedApiOutput
}
return ver, nil
}
func apiClientForAddr(addr ma.Multiaddr) (cmdsHttp.Client, error) {
_, host, err := manet.DialArgs(addr)
if err != nil {
return nil, err
}
return cmdsHttp.NewClient(host), nil
}
func isConnRefused(err error) bool {
return strings.Contains(err.Error(), "connection refused")
}

View File

@ -16,6 +16,10 @@ type TestOutput struct {
Bar int Bar int
} }
const (
ApiOption = "api"
)
var Root = &cmds.Command{ var Root = &cmds.Command{
Helptext: cmds.HelpText{ Helptext: cmds.HelpText{
Tagline: "global p2p merkle-dag filesystem", Tagline: "global p2p merkle-dag filesystem",
@ -73,6 +77,7 @@ Use 'ipfs <command> --help' to learn more about each command.
cmds.BoolOption("help", "Show the full command help text"), cmds.BoolOption("help", "Show the full command help text"),
cmds.BoolOption("h", "Show a short version of the command help text"), cmds.BoolOption("h", "Show a short version of the command help text"),
cmds.BoolOption("local", "L", "Run the command locally, instead of using the daemon"), cmds.BoolOption("local", "L", "Run the command locally, instead of using the daemon"),
cmds.StringOption(ApiOption, "Overrides the routing option (dht, supernode)"),
}, },
} }

View File

@ -58,6 +58,7 @@ func (err NoRepoError) Error() string {
const ( const (
leveldbDirectory = "datastore" leveldbDirectory = "datastore"
flatfsDirectory = "blocks" flatfsDirectory = "blocks"
apiFile = "api"
) )
var ( var (
@ -285,14 +286,53 @@ func Remove(repoPath string) error {
// process. If true, then the repo cannot be opened by this process. // process. If true, then the repo cannot be opened by this process.
func LockedByOtherProcess(repoPath string) (bool, error) { func LockedByOtherProcess(repoPath string) (bool, error) {
repoPath = path.Clean(repoPath) repoPath = path.Clean(repoPath)
// TODO replace this with the "api" file
// https://github.com/ipfs/specs/tree/master/repo/fs-repo
// NB: the lock is only held when repos are Open // NB: the lock is only held when repos are Open
return lockfile.Locked(repoPath) return lockfile.Locked(repoPath)
} }
// APIAddr returns the registered API addr, according to the api file
// in the fsrepo. This is a concurrent operation, meaning that any
// process may read this file. modifying this file, therefore, should
// use "mv" to replace the whole file and avoid interleaved read/writes.
func APIAddr(repoPath string) (string, error) {
repoPath = path.Clean(repoPath)
apiFilePath := path.Join(repoPath, apiFile)
// if there is no file, assume there is no api addr.
f, err := os.Open(apiFilePath)
if err != nil {
if os.IsNotExist(err) {
return "", repo.ErrApiNotRunning
}
return "", err
}
defer f.Close()
// read up to 2048 bytes. io.ReadAll is a vulnerability, as
// someone could hose the process by putting a massive file there.
buf := make([]byte, 2048)
n, err := f.Read(buf)
if err != nil && err != io.EOF {
return "", err
}
s := string(buf[:n])
s = strings.TrimSpace(s)
return s, nil
}
// SetAPIAddr writes the API Addr to the /api file.
func (r *FSRepo) SetAPIAddr(addr string) error {
f, err := os.Create(path.Join(r.path, apiFile))
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(addr)
return err
}
// openConfig returns an error if the config file is not present. // openConfig returns an error if the config file is not present.
func (r *FSRepo) openConfig() error { func (r *FSRepo) openConfig() error {
configFilename, err := config.Filename(r.path) configFilename, err := config.Filename(r.path)

View File

@ -35,3 +35,5 @@ func (m *Mock) GetConfigKey(key string) (interface{}, error) {
func (m *Mock) Datastore() ds.ThreadSafeDatastore { return m.D } func (m *Mock) Datastore() ds.ThreadSafeDatastore { return m.D }
func (m *Mock) Close() error { return errTODO } func (m *Mock) Close() error { return errTODO }
func (m *Mock) SetAPIAddr(addr string) error { return errTODO }

View File

@ -1,12 +1,18 @@
package repo package repo
import ( import (
"errors"
"io" "io"
datastore "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-datastore" datastore "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-datastore"
config "github.com/ipfs/go-ipfs/repo/config" config "github.com/ipfs/go-ipfs/repo/config"
) )
var (
ErrApiNotRunning = errors.New("api not running")
)
type Repo interface { type Repo interface {
Config() *config.Config Config() *config.Config
SetConfig(*config.Config) error SetConfig(*config.Config) error
@ -16,5 +22,8 @@ type Repo interface {
Datastore() datastore.ThreadSafeDatastore Datastore() datastore.ThreadSafeDatastore
// SetAPIAddr sets the API address in the repo.
SetAPIAddr(addr string) error
io.Closer io.Closer
} }