ipfs-cluster/rest_api.go
Hector Sanjuan 2512ecb701 Issue #41: Add Replication factor
New PeerManager, Allocator, Informer components have been added along
with a new "replication_factor" configuration option.

First, cluster peers collect and push metrics (Informer) to the Cluster
leader regularly. The Informer is an interface that can be implemented
in custom wayts to support custom metrics.

Second, on a pin operation, using the information from the collected metrics,
an Allocator can provide a list of preferences as to where the new pin
should be assigned. The Allocator is an interface allowing to provide
different allocation strategies.

Both Allocator and Informer are Cluster Componenets, and have access
to the RPC API.

The allocations are kept in the shared state. Cluster peer failure
detection is still missing and re-allocation is still missing, although
re-pinning something when a node is down/metrics missing does re-allocate
the pin somewhere else.

License: MIT
Signed-off-by: Hector Sanjuan <hector@protocol.ai>
2017-02-14 19:13:08 +01:00

484 lines
9.7 KiB
Go

package ipfscluster
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/ipfs/ipfs-cluster/api"
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
}
// 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 (rest *RESTAPI) routes() []route {
return []route{
{
"ID",
"GET",
"/id",
rest.idHandler,
},
{
"Version",
"GET",
"/version",
rest.versionHandler,
},
{
"Peers",
"GET",
"/peers",
rest.peerListHandler,
},
{
"PeerAdd",
"POST",
"/peers",
rest.peerAddHandler,
},
{
"PeerRemove",
"DELETE",
"/peers/{peer}",
rest.peerRemoveHandler,
},
{
"Pins",
"GET",
"/pinlist",
rest.pinListHandler,
},
{
"StatusAll",
"GET",
"/pins",
rest.statusAllHandler,
},
{
"SyncAll",
"POST",
"/pins/sync",
rest.syncAllHandler,
},
{
"Status",
"GET",
"/pins/{hash}",
rest.statusHandler,
},
{
"Pin",
"POST",
"/pins/{hash}",
rest.pinHandler,
},
{
"Unpin",
"DELETE",
"/pins/{hash}",
rest.unpinHandler,
},
{
"Sync",
"POST",
"/pins/{hash}/sync",
rest.syncHandler,
},
{
"Recover",
"POST",
"/pins/{hash}/recover",
rest.recoverHandler,
},
}
}
func (rest *RESTAPI) run() {
rest.wg.Add(1)
go func() {
defer rest.wg.Done()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
rest.ctx = ctx
<-rest.rpcReady
logger.Infof("REST API: %s", rest.apiAddr)
err := rest.server.Serve(rest.listener)
if err != nil && !strings.Contains(err.Error(), "closed network connection") {
logger.Error(err)
}
}()
}
// Shutdown stops any API listeners.
func (rest *RESTAPI) Shutdown() error {
rest.shutdownLock.Lock()
defer rest.shutdownLock.Unlock()
if rest.shutdown {
logger.Debug("already shutdown")
return nil
}
logger.Info("stopping Cluster API")
close(rest.rpcReady)
// Cancel any outstanding ops
rest.server.SetKeepAlivesEnabled(false)
rest.listener.Close()
rest.wg.Wait()
rest.shutdown = true
return nil
}
// SetClient makes the component ready to perform RPC
// requests.
func (rest *RESTAPI) SetClient(c *rpc.Client) {
rest.rpcClient = c
rest.rpcReady <- struct{}{}
}
func (rest *RESTAPI) idHandler(w http.ResponseWriter, r *http.Request) {
idSerial := api.IDSerial{}
err := rest.rpcClient.Call("",
"Cluster",
"ID",
struct{}{},
&idSerial)
sendResponse(w, err, idSerial)
}
func (rest *RESTAPI) versionHandler(w http.ResponseWriter, r *http.Request) {
var v api.Version
err := rest.rpcClient.Call("",
"Cluster",
"Version",
struct{}{},
&v)
sendResponse(w, err, v)
}
func (rest *RESTAPI) peerListHandler(w http.ResponseWriter, r *http.Request) {
var peersSerial []api.IDSerial
err := rest.rpcClient.Call("",
"Cluster",
"Peers",
struct{}{},
&peersSerial)
sendResponse(w, err, peersSerial)
}
func (rest *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 api.IDSerial
err = rest.rpcClient.Call("",
"Cluster",
"PeerAdd",
api.MultiaddrToSerial(mAddr),
&ids)
sendResponse(w, err, ids)
}
func (rest *RESTAPI) peerRemoveHandler(w http.ResponseWriter, r *http.Request) {
if p := parsePidOrError(w, r); p != "" {
err := rest.rpcClient.Call("",
"Cluster",
"PeerRemove",
p,
&struct{}{})
sendEmptyResponse(w, err)
}
}
func (rest *RESTAPI) pinHandler(w http.ResponseWriter, r *http.Request) {
if c := parseCidOrError(w, r); c.Cid != "" {
err := rest.rpcClient.Call("",
"Cluster",
"Pin",
c,
&struct{}{})
sendAcceptedResponse(w, err)
}
}
func (rest *RESTAPI) unpinHandler(w http.ResponseWriter, r *http.Request) {
if c := parseCidOrError(w, r); c.Cid != "" {
err := rest.rpcClient.Call("",
"Cluster",
"Unpin",
c,
&struct{}{})
sendAcceptedResponse(w, err)
}
}
func (rest *RESTAPI) pinListHandler(w http.ResponseWriter, r *http.Request) {
var pins []api.CidArgSerial
err := rest.rpcClient.Call("",
"Cluster",
"PinList",
struct{}{},
&pins)
sendResponse(w, err, pins)
}
func (rest *RESTAPI) statusAllHandler(w http.ResponseWriter, r *http.Request) {
var pinInfos []api.GlobalPinInfoSerial
err := rest.rpcClient.Call("",
"Cluster",
"StatusAll",
struct{}{},
&pinInfos)
sendResponse(w, err, pinInfos)
}
func (rest *RESTAPI) statusHandler(w http.ResponseWriter, r *http.Request) {
if c := parseCidOrError(w, r); c.Cid != "" {
var pinInfo api.GlobalPinInfoSerial
err := rest.rpcClient.Call("",
"Cluster",
"Status",
c,
&pinInfo)
sendResponse(w, err, pinInfo)
}
}
func (rest *RESTAPI) syncAllHandler(w http.ResponseWriter, r *http.Request) {
var pinInfos []api.GlobalPinInfoSerial
err := rest.rpcClient.Call("",
"Cluster",
"SyncAll",
struct{}{},
&pinInfos)
sendResponse(w, err, pinInfos)
}
func (rest *RESTAPI) syncHandler(w http.ResponseWriter, r *http.Request) {
if c := parseCidOrError(w, r); c.Cid != "" {
var pinInfo api.GlobalPinInfoSerial
err := rest.rpcClient.Call("",
"Cluster",
"Sync",
c,
&pinInfo)
sendResponse(w, err, pinInfo)
}
}
func (rest *RESTAPI) recoverHandler(w http.ResponseWriter, r *http.Request) {
if c := parseCidOrError(w, r); c.Cid != "" {
var pinInfo api.GlobalPinInfoSerial
err := rest.rpcClient.Call("",
"Cluster",
"Recover",
c,
&pinInfo)
sendResponse(w, err, pinInfo)
}
}
func parseCidOrError(w http.ResponseWriter, r *http.Request) api.CidArgSerial {
vars := mux.Vars(r)
hash := vars["hash"]
_, err := cid.Decode(hash)
if err != nil {
sendErrorResponse(w, 400, "error decoding Cid: "+err.Error())
return api.CidArgSerial{Cid: ""}
}
return api.CidArgSerial{Cid: 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
}
func sendResponse(w http.ResponseWriter, rpcErr error, resp interface{}) {
if checkRPCErr(w, rpcErr) {
sendJSONResponse(w, 200, resp)
}
}
// 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, rpcErr error) {
if checkRPCErr(w, rpcErr) {
w.WriteHeader(http.StatusNoContent)
}
}
func sendAcceptedResponse(w http.ResponseWriter, rpcErr error) {
if checkRPCErr(w, rpcErr) {
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)
}