Replace CORE_CLUSTER_JOIN_ADDRESS with CORE_CLUSTER_PEERS. This is a comma separated list of cluster members with their IDs of the form ID@host:port On startup the node tries to connect to all the peers. In case of sudden deaths of a node this will allow to find back into the cluster. The list in CLUSTER_PEERS is a starting point of known peers. Other node that are not in that list can still join the cluster. File and stream proxy has been moved to the Proxy type.
166 lines
4.0 KiB
Go
166 lines
4.0 KiB
Go
package cluster
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
httpapi "github.com/datarhei/core/v16/http/api"
|
|
"github.com/datarhei/core/v16/http/errorhandler"
|
|
"github.com/datarhei/core/v16/http/handler/util"
|
|
httplog "github.com/datarhei/core/v16/http/log"
|
|
mwlog "github.com/datarhei/core/v16/http/middleware/log"
|
|
"github.com/datarhei/core/v16/http/validator"
|
|
"github.com/datarhei/core/v16/log"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
)
|
|
|
|
type api struct {
|
|
id string
|
|
address string
|
|
router *echo.Echo
|
|
cluster Cluster
|
|
logger log.Logger
|
|
}
|
|
|
|
type API interface {
|
|
Start() error
|
|
Shutdown(ctx context.Context) error
|
|
}
|
|
|
|
type APIConfig struct {
|
|
ID string
|
|
Cluster Cluster
|
|
Logger log.Logger
|
|
}
|
|
|
|
type JoinRequest struct {
|
|
Origin string `json:"origin"`
|
|
ID string `json:"id"`
|
|
RaftAddress string `json:"raft_address"`
|
|
APIAddress string `json:"api_address"`
|
|
}
|
|
|
|
type LeaveRequest struct {
|
|
Origin string `json:"origin"`
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
func NewAPI(config APIConfig) (API, error) {
|
|
a := &api{
|
|
id: config.ID,
|
|
cluster: config.Cluster,
|
|
logger: config.Logger,
|
|
}
|
|
|
|
if a.logger == nil {
|
|
a.logger = log.New("")
|
|
}
|
|
|
|
address, err := config.Cluster.APIAddr("")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
a.address = address
|
|
|
|
a.router = echo.New()
|
|
a.router.Debug = true
|
|
a.router.HTTPErrorHandler = errorhandler.HTTPErrorHandler
|
|
a.router.Validator = validator.New()
|
|
a.router.HideBanner = true
|
|
a.router.HidePort = true
|
|
|
|
mwlog.NewWithConfig(mwlog.Config{
|
|
Logger: a.logger,
|
|
})
|
|
|
|
a.router.Use(mwlog.NewWithConfig(mwlog.Config{
|
|
Logger: a.logger,
|
|
}))
|
|
a.router.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
|
|
LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
|
|
rows := strings.Split(string(stack), "\n")
|
|
a.logger.Error().WithField("stack", rows).Log("recovered from a panic")
|
|
return nil
|
|
},
|
|
}))
|
|
a.router.Logger.SetOutput(httplog.NewWrapper(a.logger))
|
|
|
|
a.router.POST("/v1/join", func(c echo.Context) error {
|
|
r := JoinRequest{}
|
|
|
|
if err := util.ShouldBindJSON(c, &r); err != nil {
|
|
return httpapi.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
|
|
}
|
|
|
|
a.logger.Debug().Log("got join request: %+v", r)
|
|
|
|
if r.Origin == a.id {
|
|
return httpapi.Err(http.StatusLoopDetected, "", "breaking circuit")
|
|
}
|
|
|
|
err := a.cluster.Join(r.Origin, r.ID, r.RaftAddress, r.APIAddress, "")
|
|
if err != nil {
|
|
a.logger.Debug().WithError(err).Log("unable to join cluster")
|
|
return httpapi.Err(http.StatusInternalServerError, "unable to join cluster", "%s", err)
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, "OK")
|
|
})
|
|
|
|
a.router.POST("/v1/leave", func(c echo.Context) error {
|
|
r := LeaveRequest{}
|
|
|
|
if err := util.ShouldBindJSON(c, &r); err != nil {
|
|
return httpapi.Err(http.StatusBadRequest, "Invalid JSON", "%s", err)
|
|
}
|
|
|
|
a.logger.Debug().Log("got leave request: %+v", r)
|
|
|
|
if r.Origin == a.id {
|
|
return httpapi.Err(http.StatusLoopDetected, "", "breaking circuit")
|
|
}
|
|
|
|
err := a.cluster.Leave(r.Origin, r.ID)
|
|
if err != nil {
|
|
a.logger.Debug().WithError(err).Log("unable to leave cluster")
|
|
return httpapi.Err(http.StatusInternalServerError, "unable to leave cluster", "%s", err)
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, "OK")
|
|
})
|
|
|
|
a.router.GET("/v1/snaphot", func(c echo.Context) error {
|
|
data, err := a.cluster.Snapshot()
|
|
if err != nil {
|
|
a.logger.Debug().WithError(err).Log("unable to create snaphot")
|
|
return httpapi.Err(http.StatusInternalServerError, "unable to create snapshot", "%s", err)
|
|
}
|
|
|
|
return c.Stream(http.StatusOK, "application/octet-stream", bytes.NewReader(data))
|
|
})
|
|
|
|
a.router.POST("/v1/process", func(c echo.Context) error {
|
|
return httpapi.Err(http.StatusNotImplemented, "")
|
|
})
|
|
|
|
a.router.DELETE("/v1/process/:id", func(c echo.Context) error {
|
|
return httpapi.Err(http.StatusNotImplemented, "")
|
|
})
|
|
|
|
return a, nil
|
|
}
|
|
|
|
func (a *api) Start() error {
|
|
a.logger.Debug().Log("starting api at %s", a.address)
|
|
return a.router.Start(a.address)
|
|
}
|
|
|
|
func (a *api) Shutdown(ctx context.Context) error {
|
|
return a.router.Shutdown(ctx)
|
|
}
|