Add API endpoints for relocating processes

This commit is contained in:
Ingo Oppermann
2024-06-19 15:28:30 +02:00
parent de6a267fd4
commit a9d6b1ec49
18 changed files with 511 additions and 13 deletions

View File

@@ -113,6 +113,8 @@ func NewAPI(config APIConfig) (API, error) {
a.router.PUT("/v1/process/:id/command", a.SetProcessCommand) a.router.PUT("/v1/process/:id/command", a.SetProcessCommand)
a.router.PUT("/v1/process/:id/metadata/:key", a.SetProcessMetadata) a.router.PUT("/v1/process/:id/metadata/:key", a.SetProcessMetadata)
a.router.PUT("/v1/relocate", a.RelocateProcesses)
a.router.POST("/v1/iam/user", a.AddIdentity) a.router.POST("/v1/iam/user", a.AddIdentity)
a.router.PUT("/v1/iam/user/:name", a.UpdateIdentity) a.router.PUT("/v1/iam/user/:name", a.UpdateIdentity)
a.router.PUT("/v1/iam/user/:name/policies", a.SetIdentityPolicies) a.router.PUT("/v1/iam/user/:name/policies", a.SetIdentityPolicies)
@@ -508,6 +510,39 @@ func (a *api) SetProcessMetadata(c echo.Context) error {
return c.JSON(http.StatusOK, "OK") return c.JSON(http.StatusOK, "OK")
} }
// RelocateProcesses relocates processes to another node
// @Summary Relocate processes to another node
// @Description Relocate processes to another node.
// @Tags v1.0.0
// @ID cluster-3-relocate-processes
// @Produce json
// @Param data body client.RelocateProcessesRequest true "List of processes to relocate"
// @Success 200 {string} string
// @Failure 500 {object} Error
// @Failure 508 {object} Error
// @Router /v1/relocate [put]
func (a *api) RelocateProcesses(c echo.Context) error {
r := client.RelocateProcessesRequest{}
if err := util.ShouldBindJSON(c, &r); err != nil {
return Err(http.StatusBadRequest, "", "invalid JSON: %s", err.Error())
}
origin := c.Request().Header.Get("X-Cluster-Origin")
if origin == a.id {
return Err(http.StatusLoopDetected, "", "breaking circuit")
}
err := a.cluster.RelocateProcesses(origin, r.Map)
if err != nil {
a.logger.Debug().WithError(err).Log("Unable to apply process relocation request")
return Err(http.StatusInternalServerError, "", "unable to apply process relocation request: %s", err.Error())
}
return c.JSON(http.StatusOK, "OK")
}
// AddIdentity adds an identity to the cluster DB // AddIdentity adds an identity to the cluster DB
// @Summary Add an identity // @Summary Add an identity
// @Description Add an identity to the cluster DB // @Description Add an identity to the cluster DB

View File

@@ -39,6 +39,10 @@ type SetProcessMetadataRequest struct {
Metadata interface{} `json:"metadata"` Metadata interface{} `json:"metadata"`
} }
type RelocateProcessesRequest struct {
Map map[app.ProcessID]string
}
type AddIdentityRequest struct { type AddIdentityRequest struct {
Identity iamidentity.User `json:"identity"` Identity iamidentity.User `json:"identity"`
} }
@@ -219,6 +223,17 @@ func (c *APIClient) SetProcessMetadata(origin string, id app.ProcessID, key stri
return err return err
} }
func (c *APIClient) RelocateProcesses(origin string, r RelocateProcessesRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/relocate", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) AddIdentity(origin string, r AddIdentityRequest) error { func (c *APIClient) AddIdentity(origin string, r AddIdentityRequest) error {
data, err := json.Marshal(r) data, err := json.Marshal(r)
if err != nil { if err != nil {

View File

@@ -69,6 +69,7 @@ type Cluster interface {
SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error
GetProcessMetadata(origin string, id app.ProcessID, key string) (interface{}, error) GetProcessMetadata(origin string, id app.ProcessID, key string) (interface{}, error)
GetProcessNodeMap() map[string]string GetProcessNodeMap() map[string]string
RelocateProcesses(origin string, relocations map[app.ProcessID]string) error
IAM(superuser iamidentity.User, jwtRealm, jwtSecret string) (iam.IAM, error) IAM(superuser iamidentity.User, jwtRealm, jwtSecret string) (iam.IAM, error)
ListIdentities() (time.Time, []iamidentity.User) ListIdentities() (time.Time, []iamidentity.User)

View File

@@ -918,6 +918,50 @@ const docTemplateClusterAPI = `{
} }
} }
}, },
"/v1/relocate": {
"put": {
"description": "Relocate processes to another node.",
"produces": [
"application/json"
],
"tags": [
"v1.0.0"
],
"summary": "Relocate processes to another node",
"operationId": "cluster-3-relocate-processes",
"parameters": [
{
"description": "List of processes to relocate",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/client.RelocateProcessesRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
},
"508": {
"description": "Loop Detected",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
}
}
}
},
"/v1/server": { "/v1/server": {
"post": { "post": {
"description": "Add a new server to the cluster", "description": "Add a new server to the cluster",
@@ -1281,6 +1325,17 @@ const docTemplateClusterAPI = `{
} }
} }
}, },
"client.RelocateProcessesRequest": {
"type": "object",
"properties": {
"map": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"client.SetKVRequest": { "client.SetKVRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -910,6 +910,50 @@
} }
} }
}, },
"/v1/relocate": {
"put": {
"description": "Relocate processes to another node.",
"produces": [
"application/json"
],
"tags": [
"v1.0.0"
],
"summary": "Relocate processes to another node",
"operationId": "cluster-3-relocate-processes",
"parameters": [
{
"description": "List of processes to relocate",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/client.RelocateProcessesRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
},
"508": {
"description": "Loop Detected",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
}
}
}
},
"/v1/server": { "/v1/server": {
"post": { "post": {
"description": "Add a new server to the cluster", "description": "Add a new server to the cluster",
@@ -1273,6 +1317,17 @@
} }
} }
}, },
"client.RelocateProcessesRequest": {
"type": "object",
"properties": {
"map": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"client.SetKVRequest": { "client.SetKVRequest": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -122,6 +122,13 @@ definitions:
valid_until: valid_until:
type: string type: string
type: object type: object
client.RelocateProcessesRequest:
properties:
map:
additionalProperties:
type: string
type: object
type: object
client.SetKVRequest: client.SetKVRequest:
properties: properties:
key: key:
@@ -1466,6 +1473,35 @@ paths:
summary: Add JSON metadata with a process under the given key summary: Add JSON metadata with a process under the given key
tags: tags:
- v1.0.0 - v1.0.0
/v1/relocate:
put:
description: Relocate processes to another node.
operationId: cluster-3-relocate-processes
parameters:
- description: List of processes to relocate
in: body
name: data
required: true
schema:
$ref: '#/definitions/client.RelocateProcessesRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/cluster.Error'
"508":
description: Loop Detected
schema:
$ref: '#/definitions/cluster.Error'
summary: Relocate processes to another node
tags:
- v1.0.0
/v1/server: /v1/server:
post: post:
consumes: consumes:

View File

@@ -28,6 +28,7 @@ type Forwarder interface {
RemoveProcess(origin string, id app.ProcessID) error RemoveProcess(origin string, id app.ProcessID) error
SetProcessCommand(origin string, id app.ProcessID, command string) error SetProcessCommand(origin string, id app.ProcessID, command string) error
SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error
RelocateProcesses(origin string, relocations map[app.ProcessID]string) error
AddIdentity(origin string, identity iamidentity.User) error AddIdentity(origin string, identity iamidentity.User) error
UpdateIdentity(origin, name string, identity iamidentity.User) error UpdateIdentity(origin, name string, identity iamidentity.User) error
@@ -237,6 +238,22 @@ func (f *forwarder) RemoveProcess(origin string, id app.ProcessID) error {
return client.RemoveProcess(origin, id) return client.RemoveProcess(origin, id)
} }
func (f *forwarder) RelocateProcesses(origin string, relocations map[app.ProcessID]string) error {
if origin == "" {
origin = f.id
}
r := apiclient.RelocateProcessesRequest{
Map: relocations,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.RelocateProcesses(origin, r)
}
func (f *forwarder) AddIdentity(origin string, identity iamidentity.User) error { func (f *forwarder) AddIdentity(origin string, identity iamidentity.User) error {
if origin == "" { if origin == "" {
origin = f.id origin = f.id

View File

@@ -412,6 +412,7 @@ type processOpMove struct {
toNodeid string toNodeid string
config *app.Config config *app.Config
metadata map[string]interface{} metadata map[string]interface{}
order string
} }
type processOpStart struct { type processOpStart struct {
@@ -574,6 +575,7 @@ func (c *cluster) applyOpStack(stack []interface{}, term uint64) []processOpErro
break break
} }
if v.order == "start" {
err = c.proxy.CommandProcess(v.toNodeid, v.config.ProcessID(), "start") err = c.proxy.CommandProcess(v.toNodeid, v.config.ProcessID(), "start")
if err != nil { if err != nil {
errors = append(errors, processOpError{ errors = append(errors, processOpError{
@@ -587,6 +589,7 @@ func (c *cluster) applyOpStack(stack []interface{}, term uint64) []processOpErro
}).Log("Moving process, starting process") }).Log("Moving process, starting process")
break break
} }
}
errors = append(errors, processOpError{ errors = append(errors, processOpError{
processid: v.config.ProcessID(), processid: v.config.ProcessID(),
@@ -1055,6 +1058,7 @@ func synchronize(wish map[string]string, want []store.Process, have []proxy.Proc
toNodeid: nodeid, toNodeid: nodeid,
config: haveP.Config, config: haveP.Config,
metadata: haveP.Metadata, metadata: haveP.Metadata,
order: haveP.Order,
}) })
} }
@@ -1475,6 +1479,7 @@ func rebalance(have []proxy.Process, nodes map[string]proxy.NodeAbout) ([]interf
toNodeid: availableNodeid, toNodeid: availableNodeid,
config: p.Config, config: p.Config,
metadata: p.Metadata, metadata: p.Metadata,
order: p.Order,
}) })
// Adjust the process. // Adjust the process.
@@ -1529,6 +1534,11 @@ func relocate(have []proxy.Process, nodes map[string]proxy.NodeAbout, relocateMa
sourceNodeid := process.NodeID sourceNodeid := process.NodeID
if sourceNodeid == targetNodeid {
relocatedProcessIDs = append(relocatedProcessIDs, processid)
continue
}
if len(targetNodeid) != 0 { if len(targetNodeid) != 0 {
_, hasNode := nodes[targetNodeid] _, hasNode := nodes[targetNodeid]
@@ -1585,6 +1595,7 @@ func relocate(have []proxy.Process, nodes map[string]proxy.NodeAbout, relocateMa
toNodeid: targetNodeid, toNodeid: targetNodeid,
config: process.Config, config: process.Config,
metadata: process.Metadata, metadata: process.Metadata,
order: process.Order,
}) })
// Adjust the resources. // Adjust the resources.

View File

@@ -102,6 +102,25 @@ func (c *cluster) SetProcessCommand(origin string, id app.ProcessID, command str
return c.proxy.CommandProcess(nodeid, id, command) return c.proxy.CommandProcess(nodeid, id, command)
} }
func (c *cluster) RelocateProcesses(origin string, relocations map[app.ProcessID]string) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
if !c.IsRaftLeader() {
return c.forwarder.RelocateProcesses(origin, relocations)
}
cmd := &store.Command{
Operation: store.OpSetRelocateProcess,
Data: &store.CommandSetRelocateProcess{
Map: relocations,
},
}
return c.applyCommand(cmd)
}
func (c *cluster) SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error { func (c *cluster) SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error {
if ok, _ := c.IsDegraded(); ok { if ok, _ := c.IsDegraded(); ok {
return ErrDegraded return ErrDegraded

View File

@@ -196,6 +196,7 @@ func (s *storeData) init() {
s.Version = 1 s.Version = 1
s.Process = map[string]Process{} s.Process = map[string]Process{}
s.ProcessNodeMap = map[string]string{} s.ProcessNodeMap = map[string]string{}
s.ProcessRelocateMap = map[string]string{}
s.Users.UpdatedAt = now s.Users.UpdatedAt = now
s.Users.Users = map[string]identity.User{} s.Users.Users = map[string]identity.User{}
s.Users.userlist = identity.NewUserList() s.Users.userlist = identity.NewUserList()
@@ -476,6 +477,14 @@ func (s *store) Restore(snapshot io.ReadCloser) error {
return err return err
} }
if data.ProcessNodeMap == nil {
data.ProcessNodeMap = map[string]string{}
}
if data.ProcessRelocateMap == nil {
data.ProcessRelocateMap = map[string]string{}
}
for id, p := range data.Process { for id, p := range data.Process {
if p.Metadata != nil { if p.Metadata != nil {
continue continue

View File

@@ -2039,6 +2039,49 @@ const docTemplate = `{
} }
} }
}, },
"/api/v3/cluster/reallocation": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Retrieve snapshot of the cluster DB",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Retrieve snapshot of the cluster DB",
"operationId": "cluster-3-reallocation",
"parameters": [
{
"description": "Process reallocations",
"name": "reallocations",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.ClusterProcessReallocate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/v3/cluster/snapshot": { "/api/v3/cluster/snapshot": {
"get": { "get": {
"security": [ "security": [
@@ -5098,6 +5141,20 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
}, },
"api.ClusterProcessReallocate": {
"type": "object",
"properties": {
"process_ids": {
"type": "array",
"items": {
"$ref": "#/definitions/api.ProcessID"
}
},
"target_node_id": {
"type": "string"
}
}
},
"api.ClusterRaft": { "api.ClusterRaft": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6614,6 +6671,17 @@ const docTemplate = `{
} }
} }
}, },
"api.ProcessID": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"api.ProcessReport": { "api.ProcessReport": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -2031,6 +2031,49 @@
} }
} }
}, },
"/api/v3/cluster/reallocation": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Retrieve snapshot of the cluster DB",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Retrieve snapshot of the cluster DB",
"operationId": "cluster-3-reallocation",
"parameters": [
{
"description": "Process reallocations",
"name": "reallocations",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.ClusterProcessReallocate"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/v3/cluster/snapshot": { "/api/v3/cluster/snapshot": {
"get": { "get": {
"security": [ "security": [
@@ -5090,6 +5133,20 @@
"type": "string" "type": "string"
} }
}, },
"api.ClusterProcessReallocate": {
"type": "object",
"properties": {
"process_ids": {
"type": "array",
"items": {
"$ref": "#/definitions/api.ProcessID"
}
},
"target_node_id": {
"type": "string"
}
}
},
"api.ClusterRaft": { "api.ClusterRaft": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6606,6 +6663,17 @@
} }
} }
}, },
"api.ProcessID": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"api.ProcessReport": { "api.ProcessReport": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -211,6 +211,15 @@ definitions:
additionalProperties: additionalProperties:
type: string type: string
type: object type: object
api.ClusterProcessReallocate:
properties:
process_ids:
items:
$ref: '#/definitions/api.ProcessID'
type: array
target_node_id:
type: string
type: object
api.ClusterRaft: api.ClusterRaft:
properties: properties:
address: address:
@@ -1231,6 +1240,13 @@ definitions:
format: uint64 format: uint64
type: integer type: integer
type: object type: object
api.ProcessID:
properties:
domain:
type: string
id:
type: string
type: object
api.ProcessReport: api.ProcessReport:
properties: properties:
created_at: created_at:
@@ -3893,6 +3909,33 @@ paths:
summary: Probe a process in the cluster summary: Probe a process in the cluster
tags: tags:
- v16.?.? - v16.?.?
/api/v3/cluster/reallocation:
put:
description: Retrieve snapshot of the cluster DB
operationId: cluster-3-reallocation
parameters:
- description: Process reallocations
in: body
name: reallocations
required: true
schema:
$ref: '#/definitions/api.ClusterProcessReallocate'
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.Error'
security:
- ApiKeyAuth: []
summary: Retrieve snapshot of the cluster DB
tags:
- v16.?.?
/api/v3/cluster/snapshot: /api/v3/cluster/snapshot:
get: get:
description: Retrieve snapshot of the cluster DB description: Retrieve snapshot of the cluster DB

View File

@@ -89,3 +89,8 @@ type ClusterKVSValue struct {
type ClusterKVS map[string]ClusterKVSValue type ClusterKVS map[string]ClusterKVSValue
type ClusterProcessMap map[string]string type ClusterProcessMap map[string]string
type ClusterProcessReallocate struct {
TargetNodeID string `json:"target_node_id"`
Processes []ProcessID `json:"process_ids"`
}

View File

@@ -7,6 +7,11 @@ import (
"github.com/lithammer/shortuuid/v4" "github.com/lithammer/shortuuid/v4"
) )
type ProcessID struct {
ID string `json:"id"`
Domain string `json:"domain"`
}
// Process represents all information on a process // Process represents all information on a process
type Process struct { type Process struct {
ID string `json:"id" jsonschema:"minLength=1"` ID string `json:"id" jsonschema:"minLength=1"`

View File

@@ -13,6 +13,7 @@ import (
"github.com/datarhei/core/v16/http/api" "github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util" "github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/iam" "github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/restream/app"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@@ -152,7 +153,7 @@ func (h *ClusterHandler) Healthy(c echo.Context) error {
return c.JSON(http.StatusOK, !degraded) return c.JSON(http.StatusOK, !degraded)
} }
// Transfer the leadership to another node // TransferLeadership transfers the leadership to another node
// @Summary Transfer the leadership to another node // @Summary Transfer the leadership to another node
// @Description Transfer the leadership to another node // @Description Transfer the leadership to another node
// @Tags v16.?.? // @Tags v16.?.?
@@ -228,3 +229,47 @@ func (h *ClusterHandler) GetSnapshot(c echo.Context) error {
return c.Stream(http.StatusOK, "application/octet-stream", r) return c.Stream(http.StatusOK, "application/octet-stream", r)
} }
// Reallocation issues reallocation requests of processes
// @Summary Retrieve snapshot of the cluster DB
// @Description Retrieve snapshot of the cluster DB
// @Tags v16.?.?
// @ID cluster-3-reallocation
// @Produce json
// @Param reallocations body api.ClusterProcessReallocate true "Process reallocations"
// @Success 200 {string} string
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/reallocation [put]
func (h *ClusterHandler) Reallocation(c echo.Context) error {
reallocations := []api.ClusterProcessReallocate{}
if err := util.ShouldBindJSONValidation(c, &reallocations, false); err != nil {
return api.Err(http.StatusBadRequest, "", "invalid JSON: %s", err.Error())
}
for _, r := range reallocations {
err := c.Validate(r)
if err != nil {
return api.Err(http.StatusBadRequest, "", "invalid JSON: %s", err.Error())
}
}
relocations := map[app.ProcessID]string{}
for _, r := range reallocations {
for _, p := range r.Processes {
relocations[app.ProcessID{
ID: p.ID,
Domain: p.Domain,
}] = r.TargetNodeID
}
}
err := h.cluster.RelocateProcesses("", relocations)
if err != nil {
return api.Err(http.StatusInternalServerError, "", "%s", err.Error())
}
return c.JSON(http.StatusOK, "OK")
}

View File

@@ -768,6 +768,8 @@ func (s *server) setRoutesV3(v3 *echo.Group) {
v3.PUT("/cluster/process/:id/command", s.v3handler.cluster.SetProcessCommand) v3.PUT("/cluster/process/:id/command", s.v3handler.cluster.SetProcessCommand)
v3.PUT("/cluster/process/:id/metadata/:key", s.v3handler.cluster.SetProcessMetadata) v3.PUT("/cluster/process/:id/metadata/:key", s.v3handler.cluster.SetProcessMetadata)
v3.PUT("/cluster/reallocation", s.v3handler.cluster.Reallocation)
v3.DELETE("/cluster/node/:id/fs/:storage/*", s.v3handler.cluster.NodeFSDeleteFile) v3.DELETE("/cluster/node/:id/fs/:storage/*", s.v3handler.cluster.NodeFSDeleteFile)
v3.PUT("/cluster/node/:id/fs/:storage/*", s.v3handler.cluster.NodeFSPutFile) v3.PUT("/cluster/node/:id/fs/:storage/*", s.v3handler.cluster.NodeFSPutFile)

View File

@@ -316,3 +316,12 @@ func (p *ProcessID) Parse(pid string) {
p.ID = pid[:i] p.ID = pid[:i]
p.Domain = pid[i+1:] p.Domain = pid[i+1:]
} }
func (p ProcessID) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}
func (p *ProcessID) UnmarshalText(text []byte) error {
p.Parse(string(text))
return nil
}