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:
@ -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)
|
||||||
|
182
cmd/ipfs/main.go
182
cmd/ipfs/main.go
@ -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")
|
||||||
|
}
|
||||||
|
@ -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)"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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 }
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user