6c18c02106
This commit adds PeerAdd() and PeerRemove() endpoints, CLI support, tests. Peer management is a delicate issue because of how the consensus works underneath and the places that need to track such peers. When adding a peer the procedure is as follows: * Try to open a connection to the new peer and abort if not reachable * Broadcast a PeerManagerAddPeer operation which tells all cluster members to add the new Peer. The Raft leader will add it to Raft's peerset and the multiaddress will be saved in the ClusterPeers configuration key. * If the above fails because some cluster node is not responding, broadcast a PeerRemove() and try to undo any damage. * If the broadcast succeeds, send our ClusterPeers to the new Peer along with the local multiaddress we are using in the connection opened in the first step (that is the multiaddress through which the other peer can reach us) * The new peer updates its configuration with the new list and joins the consensus License: MIT Signed-off-by: Hector Sanjuan <hector@protocol.ai>
617 lines
13 KiB
Go
617 lines
13 KiB
Go
package ipfscluster
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
mux "github.com/gorilla/mux"
|
|
rpc "github.com/hsanjuan/go-libp2p-gorpc"
|
|
cid "github.com/ipfs/go-cid"
|
|
peer "github.com/libp2p/go-libp2p-peer"
|
|
ma "github.com/multiformats/go-multiaddr"
|
|
)
|
|
|
|
// Server settings
|
|
var (
|
|
// maximum duration before timing out read of the request
|
|
RESTAPIServerReadTimeout = 5 * time.Second
|
|
// maximum duration before timing out write of the response
|
|
RESTAPIServerWriteTimeout = 10 * time.Second
|
|
// server-side the amount of time a Keep-Alive connection will be
|
|
// kept idle before being reused
|
|
RESTAPIServerIdleTimeout = 60 * time.Second
|
|
)
|
|
|
|
// RESTAPI implements an API and aims to provides
|
|
// a RESTful HTTP API for Cluster.
|
|
type RESTAPI struct {
|
|
ctx context.Context
|
|
apiAddr ma.Multiaddr
|
|
listenAddr string
|
|
listenPort int
|
|
rpcClient *rpc.Client
|
|
rpcReady chan struct{}
|
|
router *mux.Router
|
|
|
|
listener net.Listener
|
|
server *http.Server
|
|
|
|
shutdownLock sync.Mutex
|
|
shutdown bool
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
type route struct {
|
|
Name string
|
|
Method string
|
|
Pattern string
|
|
HandlerFunc http.HandlerFunc
|
|
}
|
|
|
|
type peerAddBody struct {
|
|
PeerMultiaddr string `json:"peer_multiaddress"`
|
|
}
|
|
|
|
type errorResp struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func (e errorResp) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
type versionResp struct {
|
|
Version string `json:"version"`
|
|
}
|
|
|
|
type pinResp struct {
|
|
Pinned string `json:"pinned"`
|
|
}
|
|
|
|
type unpinResp struct {
|
|
Unpinned string `json:"unpinned"`
|
|
}
|
|
|
|
type statusInfo struct {
|
|
Status string `json:"status"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
type statusCidResp struct {
|
|
Cid string `json:"cid"`
|
|
PeerMap map[string]statusInfo `json:"peer_map"`
|
|
}
|
|
|
|
type restIPFSIDResp struct {
|
|
ID string `json:"id"`
|
|
Addresses []string `json:"addresses"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
func newRestIPFSIDResp(id IPFSID) *restIPFSIDResp {
|
|
addrs := make([]string, len(id.Addresses), len(id.Addresses))
|
|
for i, a := range id.Addresses {
|
|
addrs[i] = a.String()
|
|
}
|
|
|
|
return &restIPFSIDResp{
|
|
ID: id.ID.Pretty(),
|
|
Addresses: addrs,
|
|
Error: id.Error,
|
|
}
|
|
}
|
|
|
|
type restIDResp struct {
|
|
ID string `json:"id"`
|
|
PublicKey string `json:"public_key"`
|
|
Addresses []string `json:"addresses"`
|
|
ClusterPeers []string `json:"cluster_peers"`
|
|
Version string `json:"version"`
|
|
Commit string `json:"commit"`
|
|
RPCProtocolVersion string `json:"rpc_protocol_version"`
|
|
Error string `json:"error,omitempty"`
|
|
IPFS *restIPFSIDResp `json:"ipfs"`
|
|
}
|
|
|
|
func newRestIDResp(id ID) *restIDResp {
|
|
pubKey := ""
|
|
if id.PublicKey != nil {
|
|
keyBytes, err := id.PublicKey.Bytes()
|
|
if err == nil {
|
|
pubKey = base64.StdEncoding.EncodeToString(keyBytes)
|
|
}
|
|
}
|
|
addrs := make([]string, len(id.Addresses), len(id.Addresses))
|
|
for i, a := range id.Addresses {
|
|
addrs[i] = a.String()
|
|
}
|
|
peers := make([]string, len(id.ClusterPeers), len(id.ClusterPeers))
|
|
for i, a := range id.ClusterPeers {
|
|
peers[i] = a.String()
|
|
}
|
|
return &restIDResp{
|
|
ID: id.ID.Pretty(),
|
|
PublicKey: pubKey,
|
|
Addresses: addrs,
|
|
ClusterPeers: peers,
|
|
Version: id.Version,
|
|
Commit: id.Commit,
|
|
RPCProtocolVersion: string(id.RPCProtocolVersion),
|
|
Error: id.Error,
|
|
IPFS: newRestIPFSIDResp(id.IPFS),
|
|
}
|
|
}
|
|
|
|
type statusResp []statusCidResp
|
|
|
|
// NewRESTAPI creates a new object which is ready to be
|
|
// started.
|
|
func NewRESTAPI(cfg *Config) (*RESTAPI, error) {
|
|
ctx := context.Background()
|
|
|
|
listenAddr, err := cfg.APIAddr.ValueForProtocol(ma.P_IP4)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
listenPortStr, err := cfg.APIAddr.ValueForProtocol(ma.P_TCP)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
listenPort, err := strconv.Atoi(listenPortStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d",
|
|
listenAddr, listenPort))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
router := mux.NewRouter().StrictSlash(true)
|
|
s := &http.Server{
|
|
ReadTimeout: RESTAPIServerReadTimeout,
|
|
WriteTimeout: RESTAPIServerWriteTimeout,
|
|
//IdleTimeout: RESTAPIServerIdleTimeout, // TODO: Go 1.8
|
|
Handler: router,
|
|
}
|
|
s.SetKeepAlivesEnabled(true) // A reminder that this can be changed
|
|
|
|
api := &RESTAPI{
|
|
ctx: ctx,
|
|
apiAddr: cfg.APIAddr,
|
|
listenAddr: listenAddr,
|
|
listenPort: listenPort,
|
|
listener: l,
|
|
server: s,
|
|
rpcReady: make(chan struct{}, 1),
|
|
}
|
|
|
|
for _, route := range api.routes() {
|
|
router.
|
|
Methods(route.Method).
|
|
Path(route.Pattern).
|
|
Name(route.Name).
|
|
Handler(route.HandlerFunc)
|
|
}
|
|
|
|
api.router = router
|
|
api.run()
|
|
return api, nil
|
|
}
|
|
|
|
func (api *RESTAPI) routes() []route {
|
|
return []route{
|
|
{
|
|
"ID",
|
|
"GET",
|
|
"/id",
|
|
api.idHandler,
|
|
},
|
|
|
|
{
|
|
"Version",
|
|
"GET",
|
|
"/version",
|
|
api.versionHandler,
|
|
},
|
|
|
|
{
|
|
"Peers",
|
|
"GET",
|
|
"/peers",
|
|
api.peerListHandler,
|
|
},
|
|
{
|
|
"PeerAdd",
|
|
"POST",
|
|
"/peers",
|
|
api.peerAddHandler,
|
|
},
|
|
{
|
|
"PeerRemove",
|
|
"DELETE",
|
|
"/peers/{peer}",
|
|
api.peerRemoveHandler,
|
|
},
|
|
|
|
{
|
|
"Pins",
|
|
"GET",
|
|
"/pinlist",
|
|
api.pinListHandler,
|
|
},
|
|
|
|
{
|
|
"StatusAll",
|
|
"GET",
|
|
"/pins",
|
|
api.statusAllHandler,
|
|
},
|
|
{
|
|
"SyncAll",
|
|
"POST",
|
|
"/pins/sync",
|
|
api.syncAllHandler,
|
|
},
|
|
{
|
|
"Status",
|
|
"GET",
|
|
"/pins/{hash}",
|
|
api.statusHandler,
|
|
},
|
|
{
|
|
"Pin",
|
|
"POST",
|
|
"/pins/{hash}",
|
|
api.pinHandler,
|
|
},
|
|
{
|
|
"Unpin",
|
|
"DELETE",
|
|
"/pins/{hash}",
|
|
api.unpinHandler,
|
|
},
|
|
{
|
|
"Sync",
|
|
"POST",
|
|
"/pins/{hash}/sync",
|
|
api.syncHandler,
|
|
},
|
|
{
|
|
"Recover",
|
|
"POST",
|
|
"/pins/{hash}/recover",
|
|
api.recoverHandler,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) run() {
|
|
api.wg.Add(1)
|
|
go func() {
|
|
defer api.wg.Done()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
api.ctx = ctx
|
|
|
|
<-api.rpcReady
|
|
|
|
logger.Infof("REST API: %s", api.apiAddr)
|
|
err := api.server.Serve(api.listener)
|
|
if err != nil && !strings.Contains(err.Error(), "closed network connection") {
|
|
logger.Error(err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Shutdown stops any API listeners.
|
|
func (api *RESTAPI) Shutdown() error {
|
|
api.shutdownLock.Lock()
|
|
defer api.shutdownLock.Unlock()
|
|
|
|
if api.shutdown {
|
|
logger.Debug("already shutdown")
|
|
return nil
|
|
}
|
|
|
|
logger.Info("stopping Cluster API")
|
|
|
|
close(api.rpcReady)
|
|
// Cancel any outstanding ops
|
|
api.server.SetKeepAlivesEnabled(false)
|
|
api.listener.Close()
|
|
|
|
api.wg.Wait()
|
|
api.shutdown = true
|
|
return nil
|
|
}
|
|
|
|
// SetClient makes the component ready to perform RPC
|
|
// requests.
|
|
func (api *RESTAPI) SetClient(c *rpc.Client) {
|
|
api.rpcClient = c
|
|
api.rpcReady <- struct{}{}
|
|
}
|
|
|
|
func (api *RESTAPI) idHandler(w http.ResponseWriter, r *http.Request) {
|
|
idSerial := IDSerial{}
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"ID",
|
|
struct{}{},
|
|
&idSerial)
|
|
if checkRPCErr(w, err) {
|
|
resp := newRestIDResp(idSerial.ToID())
|
|
sendJSONResponse(w, 200, resp)
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) versionHandler(w http.ResponseWriter, r *http.Request) {
|
|
var v string
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"Version",
|
|
struct{}{},
|
|
&v)
|
|
|
|
if checkRPCErr(w, err) {
|
|
sendJSONResponse(w, 200, versionResp{v})
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) peerListHandler(w http.ResponseWriter, r *http.Request) {
|
|
var peersSerial []IDSerial
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"Peers",
|
|
struct{}{},
|
|
&peersSerial)
|
|
|
|
if checkRPCErr(w, err) {
|
|
var resp []*restIDResp
|
|
for _, pS := range peersSerial {
|
|
p := pS.ToID()
|
|
resp = append(resp, newRestIDResp(p))
|
|
}
|
|
sendJSONResponse(w, 200, resp)
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) peerAddHandler(w http.ResponseWriter, r *http.Request) {
|
|
dec := json.NewDecoder(r.Body)
|
|
defer r.Body.Close()
|
|
|
|
var addInfo peerAddBody
|
|
err := dec.Decode(&addInfo)
|
|
if err != nil {
|
|
sendErrorResponse(w, 400, "error decoding request body")
|
|
return
|
|
}
|
|
|
|
mAddr, err := ma.NewMultiaddr(addInfo.PeerMultiaddr)
|
|
if err != nil {
|
|
sendErrorResponse(w, 400, "error decoding peer_multiaddress")
|
|
return
|
|
}
|
|
|
|
var ids IDSerial
|
|
err = api.rpcClient.Call("",
|
|
"Cluster",
|
|
"PeerAdd",
|
|
MultiaddrToSerial(mAddr),
|
|
&ids)
|
|
if checkRPCErr(w, err) {
|
|
resp := newRestIDResp(ids.ToID())
|
|
sendJSONResponse(w, 200, resp)
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) peerRemoveHandler(w http.ResponseWriter, r *http.Request) {
|
|
if p := parsePidOrError(w, r); p != "" {
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"PeerRemove",
|
|
p,
|
|
&struct{}{})
|
|
if checkRPCErr(w, err) {
|
|
sendEmptyResponse(w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) pinHandler(w http.ResponseWriter, r *http.Request) {
|
|
if c := parseCidOrError(w, r); c != nil {
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"Pin",
|
|
c,
|
|
&struct{}{})
|
|
if checkRPCErr(w, err) {
|
|
sendAcceptedResponse(w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) unpinHandler(w http.ResponseWriter, r *http.Request) {
|
|
if c := parseCidOrError(w, r); c != nil {
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"Unpin",
|
|
c,
|
|
&struct{}{})
|
|
if checkRPCErr(w, err) {
|
|
sendAcceptedResponse(w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) pinListHandler(w http.ResponseWriter, r *http.Request) {
|
|
var pins []string
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"PinList",
|
|
struct{}{},
|
|
&pins)
|
|
if checkRPCErr(w, err) {
|
|
sendJSONResponse(w, 200, pins)
|
|
}
|
|
|
|
}
|
|
|
|
func (api *RESTAPI) statusAllHandler(w http.ResponseWriter, r *http.Request) {
|
|
var pinInfos []GlobalPinInfo
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"StatusAll",
|
|
struct{}{},
|
|
&pinInfos)
|
|
if checkRPCErr(w, err) {
|
|
sendStatusResponse(w, http.StatusOK, pinInfos)
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) statusHandler(w http.ResponseWriter, r *http.Request) {
|
|
if c := parseCidOrError(w, r); c != nil {
|
|
var pinInfo GlobalPinInfo
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"Status",
|
|
c,
|
|
&pinInfo)
|
|
if checkRPCErr(w, err) {
|
|
sendStatusCidResponse(w, http.StatusOK, pinInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) syncAllHandler(w http.ResponseWriter, r *http.Request) {
|
|
var pinInfos []GlobalPinInfo
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"SyncAll",
|
|
struct{}{},
|
|
&pinInfos)
|
|
if checkRPCErr(w, err) {
|
|
sendStatusResponse(w, http.StatusAccepted, pinInfos)
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) syncHandler(w http.ResponseWriter, r *http.Request) {
|
|
if c := parseCidOrError(w, r); c != nil {
|
|
var pinInfo GlobalPinInfo
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"Sync",
|
|
c,
|
|
&pinInfo)
|
|
if checkRPCErr(w, err) {
|
|
sendStatusCidResponse(w, http.StatusOK, pinInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (api *RESTAPI) recoverHandler(w http.ResponseWriter, r *http.Request) {
|
|
if c := parseCidOrError(w, r); c != nil {
|
|
var pinInfo GlobalPinInfo
|
|
err := api.rpcClient.Call("",
|
|
"Cluster",
|
|
"Recover",
|
|
c,
|
|
&pinInfo)
|
|
if checkRPCErr(w, err) {
|
|
sendStatusCidResponse(w, http.StatusOK, pinInfo)
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseCidOrError(w http.ResponseWriter, r *http.Request) *CidArg {
|
|
vars := mux.Vars(r)
|
|
hash := vars["hash"]
|
|
_, err := cid.Decode(hash)
|
|
if err != nil {
|
|
sendErrorResponse(w, 400, "error decoding Cid: "+err.Error())
|
|
return nil
|
|
}
|
|
return &CidArg{hash}
|
|
}
|
|
|
|
func parsePidOrError(w http.ResponseWriter, r *http.Request) peer.ID {
|
|
vars := mux.Vars(r)
|
|
idStr := vars["peer"]
|
|
pid, err := peer.IDB58Decode(idStr)
|
|
if err != nil {
|
|
sendErrorResponse(w, 400, "error decoding Peer ID: "+err.Error())
|
|
return ""
|
|
}
|
|
return pid
|
|
}
|
|
|
|
// checkRPCErr takes care of returning standard error responses if we
|
|
// pass an error to it. It returns true when everythings OK (no error
|
|
// was handled), or false otherwise.
|
|
func checkRPCErr(w http.ResponseWriter, err error) bool {
|
|
if err != nil {
|
|
sendErrorResponse(w, 500, err.Error())
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func sendEmptyResponse(w http.ResponseWriter) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func sendAcceptedResponse(w http.ResponseWriter) {
|
|
w.WriteHeader(http.StatusAccepted)
|
|
}
|
|
|
|
func sendJSONResponse(w http.ResponseWriter, code int, resp interface{}) {
|
|
w.WriteHeader(code)
|
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func sendErrorResponse(w http.ResponseWriter, code int, msg string) {
|
|
errorResp := errorResp{code, msg}
|
|
logger.Errorf("sending error response: %d: %s", code, msg)
|
|
sendJSONResponse(w, code, errorResp)
|
|
}
|
|
|
|
func transformPinToStatusCid(p GlobalPinInfo) statusCidResp {
|
|
s := statusCidResp{}
|
|
s.Cid = p.Cid.String()
|
|
s.PeerMap = make(map[string]statusInfo)
|
|
for k, v := range p.PeerMap {
|
|
s.PeerMap[k.Pretty()] = statusInfo{
|
|
Status: v.Status.String(),
|
|
Error: v.Error,
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func sendStatusResponse(w http.ResponseWriter, code int, data []GlobalPinInfo) {
|
|
pins := make(statusResp, 0, len(data))
|
|
|
|
for _, d := range data {
|
|
pins = append(pins, transformPinToStatusCid(d))
|
|
}
|
|
sendJSONResponse(w, code, pins)
|
|
}
|
|
|
|
func sendStatusCidResponse(w http.ResponseWriter, code int, data GlobalPinInfo) {
|
|
st := transformPinToStatusCid(data)
|
|
sendJSONResponse(w, code, st)
|
|
}
|