Merge branch 'vod' into vod-auto-remove-node

This commit is contained in:
Ingo Oppermann 2024-07-09 14:08:57 +02:00
commit 8dcda07fc1
No known key found for this signature in database
GPG Key ID: 2AB32426E9DD229E
144 changed files with 7178 additions and 7949 deletions

View File

@ -544,6 +544,7 @@ func (a *api) start(ctx context.Context) error {
CoreSkills: a.ffmpeg.Skills(),
IPLimiter: a.sessionsLimiter,
Logger: a.log.logger.core.WithComponent("Cluster"),
Resources: a.resources,
Debug: cluster.DebugConfig{
DisableFFmpegCheck: cfg.Cluster.Debug.DisableFFmpegCheck,
},
@ -1338,7 +1339,7 @@ func (a *api) start(ctx context.Context) error {
}
if a.cluster != nil {
config.Proxy = a.cluster.ProxyReader()
config.Proxy = a.cluster.Manager()
}
if cfg.RTMP.EnableTLS {
@ -1369,7 +1370,7 @@ func (a *api) start(ctx context.Context) error {
}
if a.cluster != nil {
config.Proxy = a.cluster.ProxyReader()
config.Proxy = a.cluster.Manager()
}
if cfg.SRT.Log.Enable {

173
cluster/about.go Normal file
View File

@ -0,0 +1,173 @@
package cluster
import (
"errors"
"time"
"github.com/datarhei/core/v16/cluster/raft"
"github.com/datarhei/core/v16/slices"
)
type ClusterRaft struct {
Address string
State string
LastContact time.Duration
NumPeers uint64
LogTerm uint64
LogIndex uint64
}
type ClusterNodeResources struct {
IsThrottling bool // Whether this core is currently throttling
NCPU float64 // Number of CPU on this node
CPU float64 // Current CPU load, 0-100*ncpu
CPULimit float64 // Defined CPU load limit, 0-100*ncpu
Mem uint64 // Currently used memory in bytes
MemLimit uint64 // Defined memory limit in bytes
Error error
}
type ClusterNode struct {
ID string
Name string
Version string
State string
Error error
Voter bool
Leader bool
Address string
CreatedAt time.Time
Uptime time.Duration
LastContact time.Duration
Latency time.Duration
Core ClusterNodeCore
Resources ClusterNodeResources
}
type ClusterNodeCore struct {
Address string
State string
Error error
LastContact time.Duration
Latency time.Duration
Version string
}
type ClusterAboutLeader struct {
ID string
Address string
ElectedSince time.Duration
}
type ClusterAbout struct {
ID string
Domains []string
Leader ClusterAboutLeader
State string
Raft ClusterRaft
Nodes []ClusterNode
Version ClusterVersion
Error error
}
func (c *cluster) About() (ClusterAbout, error) {
c.stateLock.RLock()
hasLeader := c.hasRaftLeader
domains := slices.Copy(c.hostnames)
c.stateLock.RUnlock()
about := ClusterAbout{
ID: c.id,
Leader: ClusterAboutLeader{},
State: "online",
Version: Version,
Domains: domains,
}
if !hasLeader {
about.State = "offline"
about.Error = errors.New("no raft leader elected")
}
stats := c.raft.Stats()
about.Raft.Address = stats.Address
about.Raft.State = stats.State
about.Raft.LastContact = stats.LastContact
about.Raft.NumPeers = stats.NumPeers
about.Raft.LogIndex = stats.LogIndex
about.Raft.LogTerm = stats.LogTerm
servers, err := c.raft.Servers()
if err != nil {
c.logger.Warn().WithError(err).Log("Raft configuration")
}
serversMap := map[string]raft.Server{}
for _, s := range servers {
serversMap[s.ID] = s
if s.Leader {
about.Leader.ID = s.ID
about.Leader.Address = s.Address
about.Leader.ElectedSince = s.LastChange
}
}
storeNodes := c.ListNodes()
nodes := c.manager.NodeList()
for _, node := range nodes {
nodeAbout := node.About()
node := ClusterNode{
ID: nodeAbout.ID,
Name: nodeAbout.Name,
Version: nodeAbout.Version,
State: nodeAbout.State,
Error: nodeAbout.Error,
Address: nodeAbout.Address,
LastContact: time.Since(nodeAbout.LastContact),
Latency: nodeAbout.Latency,
CreatedAt: nodeAbout.Core.CreatedAt,
Uptime: nodeAbout.Core.Uptime,
Core: ClusterNodeCore{
Address: nodeAbout.Core.Address,
State: nodeAbout.Core.State,
Error: nodeAbout.Core.Error,
LastContact: time.Since(nodeAbout.Core.LastContact),
Latency: nodeAbout.Core.Latency,
Version: nodeAbout.Core.Version.Number,
},
Resources: ClusterNodeResources{
IsThrottling: nodeAbout.Resources.IsThrottling,
NCPU: nodeAbout.Resources.NCPU,
CPU: nodeAbout.Resources.CPU,
CPULimit: nodeAbout.Resources.CPULimit,
Mem: nodeAbout.Resources.Mem,
MemLimit: nodeAbout.Resources.MemLimit,
Error: nodeAbout.Resources.Error,
},
}
if s, ok := serversMap[nodeAbout.ID]; ok {
node.Voter = s.Voter
node.Leader = s.Leader
}
if storeNode, hasStoreNode := storeNodes[nodeAbout.ID]; hasStoreNode {
if storeNode.State == "maintenance" {
node.State = storeNode.State
}
}
if about.State == "online" && node.State != "online" {
about.State = "degraded"
}
about.Nodes = append(about.Nodes, node)
}
return about, nil
}

181
cluster/affinity.go Normal file
View File

@ -0,0 +1,181 @@
package cluster
import (
"sort"
"github.com/datarhei/core/v16/cluster/node"
)
type referenceAffinityNodeCount struct {
nodeid string
count uint64
}
type referenceAffinity struct {
m map[string][]referenceAffinityNodeCount
}
// NewReferenceAffinity returns a referenceAffinity. This is a map of references (per domain) to an array of
// nodes this reference is found on and their count.
func NewReferenceAffinity(processes []node.Process) *referenceAffinity {
ra := &referenceAffinity{
m: map[string][]referenceAffinityNodeCount{},
}
for _, p := range processes {
if len(p.Config.Reference) == 0 {
continue
}
key := p.Config.Reference + "@" + p.Config.Domain
// Here we count how often a reference is present on a node. When
// moving processes to a different node, the node with the highest
// count of same references will be the first candidate.
found := false
arr := ra.m[key]
for i, count := range arr {
if count.nodeid == p.NodeID {
count.count++
arr[i] = count
found = true
break
}
}
if !found {
arr = append(arr, referenceAffinityNodeCount{
nodeid: p.NodeID,
count: 1,
})
}
ra.m[key] = arr
}
// Sort every reference count in decreasing order for each reference.
for ref, count := range ra.m {
sort.SliceStable(count, func(a, b int) bool {
return count[a].count > count[b].count
})
ra.m[ref] = count
}
return ra
}
// Nodes returns a list of node IDs for the provided reference and domain. The list
// is ordered by how many references are on the nodes in descending order.
func (ra *referenceAffinity) Nodes(reference, domain string) []string {
if len(reference) == 0 {
return nil
}
key := reference + "@" + domain
counts, ok := ra.m[key]
if !ok {
return nil
}
nodes := []string{}
for _, count := range counts {
nodes = append(nodes, count.nodeid)
}
return nodes
}
// Add adds a reference on a node to an existing reference affinity.
func (ra *referenceAffinity) Add(reference, domain, nodeid string) {
if len(reference) == 0 {
return
}
key := reference + "@" + domain
counts, ok := ra.m[key]
if !ok {
ra.m[key] = []referenceAffinityNodeCount{
{
nodeid: nodeid,
count: 1,
},
}
return
}
found := false
for i, count := range counts {
if count.nodeid == nodeid {
count.count++
counts[i] = count
found = true
break
}
}
if !found {
counts = append(counts, referenceAffinityNodeCount{
nodeid: nodeid,
count: 1,
})
}
ra.m[key] = counts
}
// Move moves a reference from one node to another node in an existing reference affinity.
func (ra *referenceAffinity) Move(reference, domain, fromnodeid, tonodeid string) {
if len(reference) == 0 {
return
}
key := reference + "@" + domain
counts, ok := ra.m[key]
if !ok {
ra.m[key] = []referenceAffinityNodeCount{
{
nodeid: tonodeid,
count: 1,
},
}
return
}
found := false
for i, count := range counts {
if count.nodeid == tonodeid {
count.count++
counts[i] = count
found = true
} else if count.nodeid == fromnodeid {
count.count--
counts[i] = count
}
}
if !found {
counts = append(counts, referenceAffinityNodeCount{
nodeid: tonodeid,
count: 1,
})
}
newCounts := []referenceAffinityNodeCount{}
for _, count := range counts {
if count.count == 0 {
continue
}
newCounts = append(newCounts, count)
}
ra.m[key] = newCounts
}

File diff suppressed because it is too large Load Diff

View File

@ -4,15 +4,14 @@ import (
"bytes"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"strings"
"time"
"github.com/datarhei/core/v16/config"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/ffmpeg/skills"
httpapi "github.com/datarhei/core/v16/http/api"
iamaccess "github.com/datarhei/core/v16/iam/access"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/restream/app"
@ -39,6 +38,10 @@ type SetProcessMetadataRequest struct {
Metadata interface{} `json:"metadata"`
}
type RelocateProcessesRequest struct {
Map map[app.ProcessID]string `json:"map"`
}
type AddIdentityRequest struct {
Identity iamidentity.User `json:"identity"`
}
@ -66,6 +69,28 @@ type GetKVResponse struct {
UpdatedAt time.Time `json:"updated_at"`
}
type AboutResponse struct {
ID string `json:"id"`
Version string `json:"version"`
Address string `json:"address"`
StartedAt time.Time `json:"started_at"`
Resources AboutResponseResources `json:"resources"`
}
type AboutResponseResources struct {
IsThrottling bool `json:"is_throttling"` // Whether this core is currently throttling
NCPU float64 `json:"ncpu"` // Number of CPU on this node
CPU float64 `json:"cpu"` // Current CPU load, 0-100*ncpu
CPULimit float64 `json:"cpu_limit"` // Defined CPU load limit, 0-100*ncpu
Mem uint64 `json:"memory_bytes"` // Currently used memory in bytes
MemLimit uint64 `json:"memory_limit_bytes"` // Defined memory limit in bytes
Error string `json:"error"` // Last error
}
type SetNodeStateRequest struct {
State string `json:"state"`
}
type APIClient struct {
Address string
Client *http.Client
@ -86,8 +111,23 @@ func (c *APIClient) Version() (string, error) {
return version, nil
}
func (c *APIClient) About() (AboutResponse, error) {
data, err := c.call(http.MethodGet, "/v1/about", "", nil, "")
if err != nil {
return AboutResponse{}, err
}
var about AboutResponse
err = json.Unmarshal(data, &about)
if err != nil {
return AboutResponse{}, err
}
return about, nil
}
func (c *APIClient) Barrier(name string) (bool, error) {
data, err := c.call(http.MethodGet, "/v1/barrier/"+url.PathEscape(name), "application/json", nil, "")
data, err := c.call(http.MethodGet, "/v1/barrier/"+url.PathEscape(name), "", nil, "")
if err != nil {
return false, err
}
@ -169,155 +209,6 @@ func (c *APIClient) TransferLeadership(origin, id string) error {
return err
}
func (c *APIClient) AddProcess(origin string, r AddProcessRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/v1/process", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) RemoveProcess(origin string, id app.ProcessID) error {
_, err := c.call(http.MethodDelete, "/v1/process/"+url.PathEscape(id.ID)+"?domain="+url.QueryEscape(id.Domain), "application/json", nil, origin)
return err
}
func (c *APIClient) UpdateProcess(origin string, id app.ProcessID, r UpdateProcessRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/process/"+url.PathEscape(id.ID)+"?domain="+url.QueryEscape(id.Domain), "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) SetProcessCommand(origin string, id app.ProcessID, r SetProcessCommandRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/process/"+url.PathEscape(id.ID)+"/command?domain="+url.QueryEscape(id.Domain), "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) SetProcessMetadata(origin string, id app.ProcessID, key string, r SetProcessMetadataRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/process/"+url.PathEscape(id.ID)+"/metadata/"+url.PathEscape(key)+"?domain="+url.QueryEscape(id.Domain), "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) AddIdentity(origin string, r AddIdentityRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/v1/iam/user", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) UpdateIdentity(origin, name string, r UpdateIdentityRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/iam/user/"+url.PathEscape(name), "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) SetPolicies(origin, name string, r SetPoliciesRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/iam/user/"+url.PathEscape(name)+"/policies", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) RemoveIdentity(origin string, name string) error {
_, err := c.call(http.MethodDelete, "/v1/iam/user/"+url.PathEscape(name), "application/json", nil, origin)
return err
}
func (c *APIClient) Lock(origin string, r LockRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/v1/lock", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) Unlock(origin string, name string) error {
_, err := c.call(http.MethodDelete, "/v1/lock/"+url.PathEscape(name), "application/json", nil, origin)
return err
}
func (c *APIClient) SetKV(origin string, r SetKVRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/v1/kv", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) UnsetKV(origin string, key string) error {
_, err := c.call(http.MethodDelete, "/v1/kv/"+url.PathEscape(key), "application/json", nil, origin)
if err != nil {
e, ok := err.(httpapi.Error)
if ok && e.Code == 404 {
return fs.ErrNotExist
}
}
return err
}
func (c *APIClient) GetKV(origin string, key string) (string, time.Time, error) {
data, err := c.call(http.MethodGet, "/v1/kv/"+url.PathEscape(key), "application/json", nil, origin)
if err != nil {
e, ok := err.(httpapi.Error)
if ok && e.Code == 404 {
return "", time.Time{}, fs.ErrNotExist
}
return "", time.Time{}, err
}
res := GetKVResponse{}
err = json.Unmarshal(data, &res)
if err != nil {
return "", time.Time{}, err
}
return res.Value, res.UpdatedAt, nil
}
func (c *APIClient) Snapshot(origin string) (io.ReadCloser, error) {
return c.stream(http.MethodGet, "/v1/snapshot", "", nil, origin)
}
@ -346,7 +237,7 @@ func (c *APIClient) stream(method, path, contentType string, data io.Reader, ori
}
if status < 200 || status >= 300 {
e := httpapi.Error{}
e := Error{}
defer body.Close()
@ -392,3 +283,14 @@ func (c *APIClient) request(req *http.Request) (int, io.ReadCloser, error) {
return resp.StatusCode, resp.Body, nil
}
// Error represents an error response of the API
type Error struct {
Code int `json:"code" jsonschema:"required" format:"int"`
Message string `json:"message" jsonschema:""`
Details []string `json:"details" jsonschema:""`
}
func (e Error) Error() string {
return strings.Join(e.Details, ", ")
}

48
cluster/client/iam.go Normal file
View File

@ -0,0 +1,48 @@
package client
import (
"bytes"
"net/http"
"net/url"
"github.com/datarhei/core/v16/encoding/json"
)
func (c *APIClient) IAMIdentityAdd(origin string, r AddIdentityRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/v1/iam/user", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) IAMIdentityUpdate(origin, name string, r UpdateIdentityRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/iam/user/"+url.PathEscape(name), "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) IAMPoliciesSet(origin, name string, r SetPoliciesRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/iam/user/"+url.PathEscape(name)+"/policies", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) IAMIdentityRemove(origin string, name string) error {
_, err := c.call(http.MethodDelete, "/v1/iam/user/"+url.PathEscape(name), "application/json", nil, origin)
return err
}

54
cluster/client/kvs.go Normal file
View File

@ -0,0 +1,54 @@
package client
import (
"bytes"
"io/fs"
"net/http"
"net/url"
"time"
"github.com/datarhei/core/v16/encoding/json"
)
func (c *APIClient) KVSet(origin string, r SetKVRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/v1/kv", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) KVUnset(origin string, key string) error {
_, err := c.call(http.MethodDelete, "/v1/kv/"+url.PathEscape(key), "application/json", nil, origin)
if err != nil {
e, ok := err.(Error)
if ok && e.Code == 404 {
return fs.ErrNotExist
}
}
return err
}
func (c *APIClient) KVGet(origin string, key string) (string, time.Time, error) {
data, err := c.call(http.MethodGet, "/v1/kv/"+url.PathEscape(key), "application/json", nil, origin)
if err != nil {
e, ok := err.(Error)
if ok && e.Code == 404 {
return "", time.Time{}, fs.ErrNotExist
}
return "", time.Time{}, err
}
res := GetKVResponse{}
err = json.Unmarshal(data, &res)
if err != nil {
return "", time.Time{}, err
}
return res.Value, res.UpdatedAt, nil
}

26
cluster/client/lock.go Normal file
View File

@ -0,0 +1,26 @@
package client
import (
"bytes"
"net/http"
"net/url"
"github.com/datarhei/core/v16/encoding/json"
)
func (c *APIClient) LockCreate(origin string, r LockRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/v1/lock", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) LockDelete(origin string, name string) error {
_, err := c.call(http.MethodDelete, "/v1/lock/"+url.PathEscape(name), "application/json", nil, origin)
return err
}

20
cluster/client/node.go Normal file
View File

@ -0,0 +1,20 @@
package client
import (
"bytes"
"net/http"
"net/url"
"github.com/datarhei/core/v16/encoding/json"
)
func (c *APIClient) NodeSetState(origin string, nodeid string, r SetNodeStateRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/node/"+url.PathEscape(nodeid)+"/state", "application/json", bytes.NewReader(data), origin)
return err
}

71
cluster/client/proces.go Normal file
View File

@ -0,0 +1,71 @@
package client
import (
"bytes"
"net/http"
"net/url"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/restream/app"
)
func (c *APIClient) ProcessAdd(origin string, r AddProcessRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPost, "/v1/process", "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) ProcessRemove(origin string, id app.ProcessID) error {
_, err := c.call(http.MethodDelete, "/v1/process/"+url.PathEscape(id.ID)+"?domain="+url.QueryEscape(id.Domain), "application/json", nil, origin)
return err
}
func (c *APIClient) ProcessUpdate(origin string, id app.ProcessID, r UpdateProcessRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/process/"+url.PathEscape(id.ID)+"?domain="+url.QueryEscape(id.Domain), "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) ProcessSetCommand(origin string, id app.ProcessID, r SetProcessCommandRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/process/"+url.PathEscape(id.ID)+"/command?domain="+url.QueryEscape(id.Domain), "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) ProcessSetMetadata(origin string, id app.ProcessID, key string, r SetProcessMetadataRequest) error {
data, err := json.Marshal(r)
if err != nil {
return err
}
_, err = c.call(http.MethodPut, "/v1/process/"+url.PathEscape(id.ID)+"/metadata/"+url.PathEscape(key)+"?domain="+url.QueryEscape(id.Domain), "application/json", bytes.NewReader(data), origin)
return err
}
func (c *APIClient) ProcessesRelocate(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
}

File diff suppressed because it is too large Load Diff

View File

@ -626,6 +626,62 @@ const docTemplateClusterAPI = `{
}
}
},
"/v1/node/{id}/state": {
"get": {
"description": "Set a state for a node",
"produces": [
"application/json"
],
"tags": [
"v1.0.0"
],
"summary": "Set a state for a node",
"operationId": "cluster-1-node-set-state",
"parameters": [
{
"description": "Set node state request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/client.SetNodeStateRequest"
}
},
{
"type": "string",
"description": "Origin ID of request",
"name": "X-Cluster-Origin",
"in": "header"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
},
"508": {
"description": "Loop Detected",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
}
}
}
},
"/v1/process": {
"post": {
"description": "Add a process to the cluster DB",
@ -918,6 +974,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": {
"post": {
"description": "Add a new server to the cluster",
@ -1281,6 +1381,17 @@ const docTemplateClusterAPI = `{
}
}
},
"client.RelocateProcessesRequest": {
"type": "object",
"properties": {
"map": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"client.SetKVRequest": {
"type": "object",
"properties": {
@ -1292,6 +1403,14 @@ const docTemplateClusterAPI = `{
}
}
},
"client.SetNodeStateRequest": {
"type": "object",
"properties": {
"state": {
"type": "string"
}
}
},
"client.SetPoliciesRequest": {
"type": "object",
"properties": {

View File

@ -618,6 +618,62 @@
}
}
},
"/v1/node/{id}/state": {
"get": {
"description": "Set a state for a node",
"produces": [
"application/json"
],
"tags": [
"v1.0.0"
],
"summary": "Set a state for a node",
"operationId": "cluster-1-node-set-state",
"parameters": [
{
"description": "Set node state request",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/client.SetNodeStateRequest"
}
},
{
"type": "string",
"description": "Origin ID of request",
"name": "X-Cluster-Origin",
"in": "header"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
},
"508": {
"description": "Loop Detected",
"schema": {
"$ref": "#/definitions/cluster.Error"
}
}
}
}
},
"/v1/process": {
"post": {
"description": "Add a process to the cluster DB",
@ -910,6 +966,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": {
"post": {
"description": "Add a new server to the cluster",
@ -1273,6 +1373,17 @@
}
}
},
"client.RelocateProcessesRequest": {
"type": "object",
"properties": {
"map": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
},
"client.SetKVRequest": {
"type": "object",
"properties": {
@ -1284,6 +1395,14 @@
}
}
},
"client.SetNodeStateRequest": {
"type": "object",
"properties": {
"state": {
"type": "string"
}
}
},
"client.SetPoliciesRequest": {
"type": "object",
"properties": {

View File

@ -122,6 +122,13 @@ definitions:
valid_until:
type: string
type: object
client.RelocateProcessesRequest:
properties:
map:
additionalProperties:
type: string
type: object
type: object
client.SetKVRequest:
properties:
key:
@ -129,6 +136,11 @@ definitions:
value:
type: string
type: object
client.SetNodeStateRequest:
properties:
state:
type: string
type: object
client.SetPoliciesRequest:
properties:
policies:
@ -1268,6 +1280,43 @@ paths:
summary: Remove a lock
tags:
- v1.0.0
/v1/node/{id}/state:
get:
description: Set a state for a node
operationId: cluster-1-node-set-state
parameters:
- description: Set node state request
in: body
name: data
required: true
schema:
$ref: '#/definitions/client.SetNodeStateRequest'
- description: Origin ID of request
in: header
name: X-Cluster-Origin
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"400":
description: Bad Request
schema:
$ref: '#/definitions/cluster.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/cluster.Error'
"508":
description: Loop Detected
schema:
$ref: '#/definitions/cluster.Error'
summary: Set a state for a node
tags:
- v1.0.0
/v1/process:
post:
consumes:
@ -1466,6 +1515,35 @@ paths:
summary: Add JSON metadata with a process under the given key
tags:
- 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:
post:
consumes:

View File

@ -0,0 +1,21 @@
package forwarder
import (
"fmt"
apiclient "github.com/datarhei/core/v16/cluster/client"
"github.com/datarhei/core/v16/cluster/store"
)
func reconstructError(err error) error {
if cerr, ok := err.(apiclient.Error); ok {
switch cerr.Code {
case 400:
err = fmt.Errorf("%s%w", err.Error(), store.ErrBadRequest)
case 404:
err = fmt.Errorf("%s%w", err.Error(), store.ErrNotFound)
}
}
return err
}

View File

@ -7,63 +7,31 @@ import (
"time"
apiclient "github.com/datarhei/core/v16/cluster/client"
iamaccess "github.com/datarhei/core/v16/iam/access"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/restream/app"
)
// Forwarder forwards any HTTP request from a follower to the leader
type Forwarder interface {
SetLeader(address string)
HasLeader() bool
Join(origin, id, raftAddress, peerAddress string) error
Leave(origin, id string) error
TransferLeadership(origin, id string) error
Snapshot(origin string) (io.ReadCloser, error)
AddProcess(origin string, config *app.Config) error
UpdateProcess(origin string, id app.ProcessID, config *app.Config) error
RemoveProcess(origin string, id app.ProcessID) error
SetProcessCommand(origin string, id app.ProcessID, command string) error
SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error
AddIdentity(origin string, identity iamidentity.User) error
UpdateIdentity(origin, name string, identity iamidentity.User) error
SetPolicies(origin, name string, policies []iamaccess.Policy) error
RemoveIdentity(origin string, name string) error
CreateLock(origin string, name string, validUntil time.Time) error
DeleteLock(origin string, name string) error
SetKV(origin, key, value string) error
UnsetKV(origin, key string) error
GetKV(origin, key string) (string, time.Time, error)
}
type forwarder struct {
id string
lock sync.RWMutex
type Forwarder struct {
ID string
Logger log.Logger
lock sync.RWMutex
client apiclient.APIClient
logger log.Logger
}
type ForwarderConfig struct {
type Config struct {
ID string
Logger log.Logger
}
func New(config ForwarderConfig) (Forwarder, error) {
f := &forwarder{
id: config.ID,
logger: config.Logger,
func New(config Config) (*Forwarder, error) {
f := &Forwarder{
ID: config.ID,
Logger: config.Logger,
}
if f.logger == nil {
f.logger = log.New("")
if f.Logger == nil {
f.Logger = log.New("")
}
tr := http.DefaultTransport.(*http.Transport).Clone()
@ -82,7 +50,7 @@ func New(config ForwarderConfig) (Forwarder, error) {
return f, nil
}
func (f *forwarder) SetLeader(address string) {
func (f *Forwarder) SetLeader(address string) {
f.lock.Lock()
defer f.lock.Unlock()
@ -90,18 +58,18 @@ func (f *forwarder) SetLeader(address string) {
return
}
f.logger.Debug().Log("Setting leader address to %s", address)
f.Logger.Debug().Log("Setting leader address to %s", address)
f.client.Address = address
}
func (f *forwarder) HasLeader() bool {
func (f *Forwarder) HasLeader() bool {
return len(f.client.Address) != 0
}
func (f *forwarder) Join(origin, id, raftAddress, peerAddress string) error {
func (f *Forwarder) Join(origin, id, raftAddress, peerAddress string) error {
if origin == "" {
origin = f.id
origin = f.ID
}
r := apiclient.JoinRequest{
@ -109,7 +77,7 @@ func (f *forwarder) Join(origin, id, raftAddress, peerAddress string) error {
RaftAddress: raftAddress,
}
f.logger.Debug().WithField("request", r).Log("Forwarding to leader")
f.Logger.Debug().WithField("request", r).Log("Forwarding to leader")
f.lock.RLock()
client := f.client
@ -125,12 +93,12 @@ func (f *forwarder) Join(origin, id, raftAddress, peerAddress string) error {
return client.Join(origin, r)
}
func (f *forwarder) Leave(origin, id string) error {
func (f *Forwarder) Leave(origin, id string) error {
if origin == "" {
origin = f.id
origin = f.ID
}
f.logger.Debug().WithField("id", id).Log("Forwarding to leader")
f.Logger.Debug().WithField("id", id).Log("Forwarding to leader")
f.lock.RLock()
client := f.client
@ -139,12 +107,12 @@ func (f *forwarder) Leave(origin, id string) error {
return client.Leave(origin, id)
}
func (f *forwarder) TransferLeadership(origin, id string) error {
func (f *Forwarder) TransferLeadership(origin, id string) error {
if origin == "" {
origin = f.id
origin = f.ID
}
f.logger.Debug().WithField("id", id).Log("Transferring leadership")
f.Logger.Debug().WithField("id", id).Log("Transferring leadership")
f.lock.RLock()
client := f.client
@ -153,216 +121,10 @@ func (f *forwarder) TransferLeadership(origin, id string) error {
return client.TransferLeadership(origin, id)
}
func (f *forwarder) Snapshot(origin string) (io.ReadCloser, error) {
func (f *Forwarder) Snapshot(origin string) (io.ReadCloser, error) {
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.Snapshot(origin)
}
func (f *forwarder) AddProcess(origin string, config *app.Config) error {
if origin == "" {
origin = f.id
}
r := apiclient.AddProcessRequest{
Config: *config,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.AddProcess(origin, r)
}
func (f *forwarder) UpdateProcess(origin string, id app.ProcessID, config *app.Config) error {
if origin == "" {
origin = f.id
}
r := apiclient.UpdateProcessRequest{
Config: *config,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.UpdateProcess(origin, id, r)
}
func (f *forwarder) SetProcessCommand(origin string, id app.ProcessID, command string) error {
if origin == "" {
origin = f.id
}
r := apiclient.SetProcessCommandRequest{
Command: command,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.SetProcessCommand(origin, id, r)
}
func (f *forwarder) SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error {
if origin == "" {
origin = f.id
}
r := apiclient.SetProcessMetadataRequest{
Metadata: data,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.SetProcessMetadata(origin, id, key, r)
}
func (f *forwarder) RemoveProcess(origin string, id app.ProcessID) error {
if origin == "" {
origin = f.id
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.RemoveProcess(origin, id)
}
func (f *forwarder) AddIdentity(origin string, identity iamidentity.User) error {
if origin == "" {
origin = f.id
}
r := apiclient.AddIdentityRequest{
Identity: identity,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.AddIdentity(origin, r)
}
func (f *forwarder) UpdateIdentity(origin, name string, identity iamidentity.User) error {
if origin == "" {
origin = f.id
}
r := apiclient.UpdateIdentityRequest{
Identity: identity,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.UpdateIdentity(origin, name, r)
}
func (f *forwarder) SetPolicies(origin, name string, policies []iamaccess.Policy) error {
if origin == "" {
origin = f.id
}
r := apiclient.SetPoliciesRequest{
Policies: policies,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.SetPolicies(origin, name, r)
}
func (f *forwarder) RemoveIdentity(origin string, name string) error {
if origin == "" {
origin = f.id
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.RemoveIdentity(origin, name)
}
func (f *forwarder) CreateLock(origin string, name string, validUntil time.Time) error {
if origin == "" {
origin = f.id
}
r := apiclient.LockRequest{
Name: name,
ValidUntil: validUntil,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.Lock(origin, r)
}
func (f *forwarder) DeleteLock(origin string, name string) error {
if origin == "" {
origin = f.id
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.Unlock(origin, name)
}
func (f *forwarder) SetKV(origin, key, value string) error {
if origin == "" {
origin = f.id
}
r := apiclient.SetKVRequest{
Key: key,
Value: value,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.SetKV(origin, r)
}
func (f *forwarder) UnsetKV(origin, key string) error {
if origin == "" {
origin = f.id
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.UnsetKV(origin, key)
}
func (f *forwarder) GetKV(origin, key string) (string, time.Time, error) {
if origin == "" {
origin = f.id
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return client.GetKV(origin, key)
}

67
cluster/forwarder/iam.go Normal file
View File

@ -0,0 +1,67 @@
package forwarder
import (
apiclient "github.com/datarhei/core/v16/cluster/client"
iamaccess "github.com/datarhei/core/v16/iam/access"
iamidentity "github.com/datarhei/core/v16/iam/identity"
)
func (f *Forwarder) IAMIdentityAdd(origin string, identity iamidentity.User) error {
if origin == "" {
origin = f.ID
}
r := apiclient.AddIdentityRequest{
Identity: identity,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.IAMIdentityAdd(origin, r))
}
func (f *Forwarder) IAMIdentityUpdate(origin, name string, identity iamidentity.User) error {
if origin == "" {
origin = f.ID
}
r := apiclient.UpdateIdentityRequest{
Identity: identity,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.IAMIdentityUpdate(origin, name, r))
}
func (f *Forwarder) IAMPoliciesSet(origin, name string, policies []iamaccess.Policy) error {
if origin == "" {
origin = f.ID
}
r := apiclient.SetPoliciesRequest{
Policies: policies,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.IAMPoliciesSet(origin, name, r))
}
func (f *Forwarder) IAMIdentityRemove(origin string, name string) error {
if origin == "" {
origin = f.ID
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.IAMIdentityRemove(origin, name))
}

50
cluster/forwarder/kvs.go Normal file
View File

@ -0,0 +1,50 @@
package forwarder
import (
"time"
apiclient "github.com/datarhei/core/v16/cluster/client"
)
func (f *Forwarder) KVSet(origin, key, value string) error {
if origin == "" {
origin = f.ID
}
r := apiclient.SetKVRequest{
Key: key,
Value: value,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.KVSet(origin, r))
}
func (f *Forwarder) KVUnset(origin, key string) error {
if origin == "" {
origin = f.ID
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.KVUnset(origin, key))
}
func (f *Forwarder) KVGet(origin, key string) (string, time.Time, error) {
if origin == "" {
origin = f.ID
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
value, at, err := client.KVGet(origin, key)
return value, at, reconstructError(err)
}

36
cluster/forwarder/lock.go Normal file
View File

@ -0,0 +1,36 @@
package forwarder
import (
"time"
apiclient "github.com/datarhei/core/v16/cluster/client"
)
func (f *Forwarder) LockCreate(origin string, name string, validUntil time.Time) error {
if origin == "" {
origin = f.ID
}
r := apiclient.LockRequest{
Name: name,
ValidUntil: validUntil,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.LockCreate(origin, r))
}
func (f *Forwarder) LockDelete(origin string, name string) error {
if origin == "" {
origin = f.ID
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.LockDelete(origin, name))
}

21
cluster/forwarder/node.go Normal file
View File

@ -0,0 +1,21 @@
package forwarder
import (
apiclient "github.com/datarhei/core/v16/cluster/client"
)
func (f *Forwarder) NodeSetState(origin, nodeid, state string) error {
if origin == "" {
origin = f.ID
}
r := apiclient.SetNodeStateRequest{
State: state,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.NodeSetState(origin, nodeid, r))
}

View File

@ -0,0 +1,98 @@
package forwarder
import (
apiclient "github.com/datarhei/core/v16/cluster/client"
"github.com/datarhei/core/v16/restream/app"
)
func (f *Forwarder) ProcessAdd(origin string, config *app.Config) error {
if origin == "" {
origin = f.ID
}
r := apiclient.AddProcessRequest{
Config: *config,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.ProcessAdd(origin, r))
}
func (f *Forwarder) ProcessUpdate(origin string, id app.ProcessID, config *app.Config) error {
if origin == "" {
origin = f.ID
}
r := apiclient.UpdateProcessRequest{
Config: *config,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.ProcessUpdate(origin, id, r))
}
func (f *Forwarder) ProcessSetCommand(origin string, id app.ProcessID, command string) error {
if origin == "" {
origin = f.ID
}
r := apiclient.SetProcessCommandRequest{
Command: command,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.ProcessSetCommand(origin, id, r))
}
func (f *Forwarder) ProcessSetMetadata(origin string, id app.ProcessID, key string, data interface{}) error {
if origin == "" {
origin = f.ID
}
r := apiclient.SetProcessMetadataRequest{
Metadata: data,
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.ProcessSetMetadata(origin, id, key, r))
}
func (f *Forwarder) ProcessRemove(origin string, id app.ProcessID) error {
if origin == "" {
origin = f.ID
}
f.lock.RLock()
client := f.client
f.lock.RUnlock()
return reconstructError(client.ProcessRemove(origin, id))
}
func (f *Forwarder) ProcessesRelocate(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 reconstructError(client.ProcessesRelocate(origin, r))
}

View File

@ -39,13 +39,13 @@ func (c *cluster) IAM(superuser iamidentity.User, jwtRealm, jwtSecret string) (i
}
func (c *cluster) ListIdentities() (time.Time, []iamidentity.User) {
users := c.store.ListUsers()
users := c.store.IAMIdentityList()
return users.UpdatedAt, users.Users
}
func (c *cluster) ListIdentity(name string) (time.Time, iamidentity.User, error) {
user := c.store.GetUser(name)
user := c.store.IAMIdentityGet(name)
if len(user.Users) == 0 {
return time.Time{}, iamidentity.User{}, fmt.Errorf("not found")
@ -55,28 +55,24 @@ func (c *cluster) ListIdentity(name string) (time.Time, iamidentity.User, error)
}
func (c *cluster) ListPolicies() (time.Time, []iamaccess.Policy) {
policies := c.store.ListPolicies()
policies := c.store.IAMPolicyList()
return policies.UpdatedAt, policies.Policies
}
func (c *cluster) ListUserPolicies(name string) (time.Time, []iamaccess.Policy) {
policies := c.store.ListUserPolicies(name)
policies := c.store.IAMIdentityPolicyList(name)
return policies.UpdatedAt, policies.Policies
}
func (c *cluster) AddIdentity(origin string, identity iamidentity.User) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
func (c *cluster) IAMIdentityAdd(origin string, identity iamidentity.User) error {
if err := identity.Validate(); err != nil {
return fmt.Errorf("invalid identity: %w", err)
}
if !c.IsRaftLeader() {
return c.forwarder.AddIdentity(origin, identity)
return c.forwarder.IAMIdentityAdd(origin, identity)
}
cmd := &store.Command{
@ -89,13 +85,9 @@ func (c *cluster) AddIdentity(origin string, identity iamidentity.User) error {
return c.applyCommand(cmd)
}
func (c *cluster) UpdateIdentity(origin, name string, identity iamidentity.User) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
func (c *cluster) IAMIdentityUpdate(origin, name string, identity iamidentity.User) error {
if !c.IsRaftLeader() {
return c.forwarder.UpdateIdentity(origin, name, identity)
return c.forwarder.IAMIdentityUpdate(origin, name, identity)
}
cmd := &store.Command{
@ -109,13 +101,9 @@ func (c *cluster) UpdateIdentity(origin, name string, identity iamidentity.User)
return c.applyCommand(cmd)
}
func (c *cluster) SetPolicies(origin, name string, policies []iamaccess.Policy) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
func (c *cluster) IAMPoliciesSet(origin, name string, policies []iamaccess.Policy) error {
if !c.IsRaftLeader() {
return c.forwarder.SetPolicies(origin, name, policies)
return c.forwarder.IAMPoliciesSet(origin, name, policies)
}
cmd := &store.Command{
@ -129,13 +117,9 @@ func (c *cluster) SetPolicies(origin, name string, policies []iamaccess.Policy)
return c.applyCommand(cmd)
}
func (c *cluster) RemoveIdentity(origin string, name string) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
func (c *cluster) IAMIdentityRemove(origin string, name string) error {
if !c.IsRaftLeader() {
return c.forwarder.RemoveIdentity(origin, name)
return c.forwarder.IAMIdentityRemove(origin, name)
}
cmd := &store.Command{

View File

@ -18,7 +18,7 @@ func NewIdentityAdapter(store store.Store) (iamidentity.Adapter, error) {
}
func (a *identityAdapter) LoadIdentities() ([]iamidentity.User, error) {
users := a.store.ListUsers()
users := a.store.IAMIdentityList()
return users.Users, nil
}

View File

@ -25,7 +25,7 @@ func NewPolicyAdapter(store store.Store) (iamaccess.Adapter, error) {
}
func (a *policyAdapter) LoadPolicy(model model.Model) error {
policies := a.store.ListPolicies()
policies := a.store.IAMPolicyList()
rules := [][]string{}
domains := map[string]struct{}{}

View File

@ -10,13 +10,9 @@ import (
"github.com/datarhei/core/v16/log"
)
func (c *cluster) CreateLock(origin string, name string, validUntil time.Time) (*kvs.Lock, error) {
if ok, _ := c.IsClusterDegraded(); ok {
return nil, ErrDegraded
}
func (c *cluster) LockCreate(origin string, name string, validUntil time.Time) (*kvs.Lock, error) {
if !c.IsRaftLeader() {
err := c.forwarder.CreateLock(origin, name, validUntil)
err := c.forwarder.LockCreate(origin, name, validUntil)
if err != nil {
return nil, err
}
@ -28,7 +24,7 @@ func (c *cluster) CreateLock(origin string, name string, validUntil time.Time) (
return l, nil
}
if c.store.HasLock(name) {
if c.store.LockHasLock(name) {
return nil, fmt.Errorf("the lock '%s' already exists", name)
}
@ -52,13 +48,9 @@ func (c *cluster) CreateLock(origin string, name string, validUntil time.Time) (
return l, nil
}
func (c *cluster) DeleteLock(origin string, name string) error {
if ok, _ := c.IsClusterDegraded(); ok {
return ErrDegraded
}
func (c *cluster) LockDelete(origin string, name string) error {
if !c.IsRaftLeader() {
return c.forwarder.DeleteLock(origin, name)
return c.forwarder.LockDelete(origin, name)
}
cmd := &store.Command{
@ -71,17 +63,9 @@ func (c *cluster) DeleteLock(origin string, name string) error {
return c.applyCommand(cmd)
}
func (c *cluster) ListLocks() map[string]time.Time {
return c.store.ListLocks()
}
func (c *cluster) SetKV(origin, key, value string) error {
if ok, _ := c.IsClusterDegraded(); ok {
return ErrDegraded
}
func (c *cluster) KVSet(origin, key, value string) error {
if !c.IsRaftLeader() {
return c.forwarder.SetKV(origin, key, value)
return c.forwarder.KVSet(origin, key, value)
}
cmd := &store.Command{
@ -95,13 +79,9 @@ func (c *cluster) SetKV(origin, key, value string) error {
return c.applyCommand(cmd)
}
func (c *cluster) UnsetKV(origin, key string) error {
if ok, _ := c.IsClusterDegraded(); ok {
return ErrDegraded
}
func (c *cluster) KVUnset(origin, key string) error {
if !c.IsRaftLeader() {
return c.forwarder.UnsetKV(origin, key)
return c.forwarder.KVUnset(origin, key)
}
cmd := &store.Command{
@ -114,18 +94,14 @@ func (c *cluster) UnsetKV(origin, key string) error {
return c.applyCommand(cmd)
}
func (c *cluster) GetKV(origin, key string, stale bool) (string, time.Time, error) {
func (c *cluster) KVGet(origin, key string, stale bool) (string, time.Time, error) {
if !stale {
if ok, _ := c.IsClusterDegraded(); ok {
return "", time.Time{}, ErrDegraded
}
if !c.IsRaftLeader() {
return c.forwarder.GetKV(origin, key)
return c.forwarder.KVGet(origin, key)
}
}
value, err := c.store.GetFromKVS(key)
value, err := c.store.KVSGetValue(key)
if err != nil {
return "", time.Time{}, err
}
@ -133,12 +109,6 @@ func (c *cluster) GetKV(origin, key string, stale bool) (string, time.Time, erro
return value.Value, value.UpdatedAt, nil
}
func (c *cluster) ListKV(prefix string) map[string]store.Value {
storeValues := c.store.ListKVS(prefix)
return storeValues
}
type ClusterKVS interface {
kvs.KVS
@ -178,17 +148,17 @@ func (s *clusterKVS) CreateLock(name string, validUntil time.Time) (*kvs.Lock, e
"name": name,
"valid_until": validUntil,
}).Log("Create lock")
return s.cluster.CreateLock("", name, validUntil)
return s.cluster.LockCreate("", name, validUntil)
}
func (s *clusterKVS) DeleteLock(name string) error {
s.logger.Debug().WithField("name", name).Log("Delete lock")
return s.cluster.DeleteLock("", name)
return s.cluster.LockDelete("", name)
}
func (s *clusterKVS) ListLocks() map[string]time.Time {
s.logger.Debug().Log("List locks")
return s.cluster.ListLocks()
return s.cluster.Store().LockList()
}
func (s *clusterKVS) SetKV(key, value string) error {
@ -196,12 +166,12 @@ func (s *clusterKVS) SetKV(key, value string) error {
"key": key,
"value": value,
}).Log("Set KV")
return s.cluster.SetKV("", key, value)
return s.cluster.KVSet("", key, value)
}
func (s *clusterKVS) UnsetKV(key string) error {
s.logger.Debug().WithField("key", key).Log("Unset KV")
return s.cluster.UnsetKV("", key)
return s.cluster.KVUnset("", key)
}
func (s *clusterKVS) GetKV(key string) (string, time.Time, error) {
@ -213,10 +183,10 @@ func (s *clusterKVS) GetKV(key string) (string, time.Time, error) {
"key": key,
"stale": stale,
}).Log("Get KV")
return s.cluster.GetKV("", key, stale)
return s.cluster.KVGet("", key, stale)
}
func (s *clusterKVS) ListKV(prefix string) map[string]store.Value {
s.logger.Debug().Log("List KV")
return s.cluster.ListKV(prefix)
return s.cluster.Store().KVSList(prefix)
}

File diff suppressed because it is too large Load Diff

185
cluster/leader_rebalance.go Normal file
View File

@ -0,0 +1,185 @@
package cluster
import (
"github.com/datarhei/core/v16/cluster/node"
"github.com/datarhei/core/v16/cluster/store"
"github.com/datarhei/core/v16/log"
)
func (c *cluster) doRebalance(emergency bool, term uint64) {
if emergency {
// Don't rebalance in emergency mode.
return
}
logger := c.logger.WithField("term", term)
logger.Debug().WithField("emergency", emergency).Log("Rebalancing")
storeNodes := c.store.NodeList()
have := c.manager.ClusterProcessList()
nodes := c.manager.NodeList()
nodesMap := map[string]node.About{}
for _, node := range nodes {
about := node.About()
if storeNode, hasStoreNode := storeNodes[about.ID]; hasStoreNode {
about.State = storeNode.State
}
nodesMap[about.ID] = about
}
logger.Debug().WithFields(log.Fields{
"have": have,
"nodes": nodesMap,
}).Log("Rebalance")
opStack, _ := rebalance(have, nodesMap)
errors := c.applyOpStack(opStack, term)
for _, e := range errors {
// Only apply the command if the error is different.
process, err := c.store.ProcessGet(e.processid)
if err != nil {
continue
}
var errmessage string = ""
if e.err != nil {
if process.Error == e.err.Error() {
continue
}
errmessage = e.err.Error()
} else {
if len(process.Error) == 0 {
continue
}
}
cmd := &store.Command{
Operation: store.OpSetProcessError,
Data: store.CommandSetProcessError{
ID: e.processid,
Error: errmessage,
},
}
c.applyCommand(cmd)
}
}
// rebalance returns a list of operations that will move running processes away from nodes that are overloaded.
func rebalance(have []node.Process, nodes map[string]node.About) ([]interface{}, map[string]node.Resources) {
resources := NewResourcePlanner(nodes)
// Mark nodes as throttling where at least one process is still throttling
for _, haveP := range have {
if haveP.Throttling {
resources.Throttling(haveP.NodeID, true)
}
}
// Group all running processes by node and sort them by their runtime in ascending order.
nodeProcessMap := createNodeProcessMap(have)
// A map from the process reference to the nodes it is running on.
haveReferenceAffinity := NewReferenceAffinity(have)
opStack := []interface{}{}
// Check if any of the nodes is overloaded.
for id, r := range resources.Map() {
// Ignore this node if the resource values are not reliable.
if r.Error != nil {
continue
}
// Check if node is overloaded.
if r.CPU < r.CPULimit && r.Mem < r.MemLimit && !r.IsThrottling {
continue
}
// Move processes from this node to another node with enough free resources.
// The processes are ordered ascending by their runtime.
processes := nodeProcessMap[id]
if len(processes) == 0 {
// If there are no processes on that node, we can't do anything.
continue
}
overloadedNodeid := id
for i, p := range processes {
availableNodeid := ""
// Try to move the process to a node where other processes with the same
// reference currently reside.
if len(p.Config.Reference) != 0 {
raNodes := haveReferenceAffinity.Nodes(p.Config.Reference, p.Config.Domain)
for _, raNodeid := range raNodes {
// Do not move the process to the node it is currently on.
if raNodeid == overloadedNodeid {
continue
}
if resources.HasNodeEnough(raNodeid, p.Config.LimitCPU, p.Config.LimitMemory) {
availableNodeid = raNodeid
break
}
}
}
// Find the best node with enough resources available.
if len(availableNodeid) == 0 {
nodes := resources.FindBestNodes(p.Config.LimitCPU, p.Config.LimitMemory)
for _, nodeid := range nodes {
if nodeid == overloadedNodeid {
continue
}
availableNodeid = nodeid
break
}
}
if len(availableNodeid) == 0 {
// There's no other node with enough resources to take over this process.
opStack = append(opStack, processOpSkip{
nodeid: overloadedNodeid,
processid: p.Config.ProcessID(),
err: errNotEnoughResourcesForRebalancing,
})
continue
}
opStack = append(opStack, processOpMove{
fromNodeid: overloadedNodeid,
toNodeid: availableNodeid,
config: p.Config,
metadata: p.Metadata,
order: p.Order,
})
// Adjust the process.
p.NodeID = availableNodeid
processes[i] = p
// Adjust the resources.
resources.Move(availableNodeid, overloadedNodeid, p.CPU, p.Mem)
// Adjust the reference affinity.
haveReferenceAffinity.Move(p.Config.Reference, p.Config.Domain, overloadedNodeid, availableNodeid)
// Move only one process at a time.
break
}
}
return opStack, resources.Map()
}

206
cluster/leader_relocate.go Normal file
View File

@ -0,0 +1,206 @@
package cluster
import (
"github.com/datarhei/core/v16/cluster/node"
"github.com/datarhei/core/v16/cluster/store"
"github.com/datarhei/core/v16/log"
"github.com/datarhei/core/v16/restream/app"
)
func (c *cluster) doRelocate(emergency bool, term uint64) {
if emergency {
// Don't relocate in emergency mode.
return
}
logger := c.logger.WithField("term", term)
logger.Debug().WithField("emergency", emergency).Log("Relocating")
relocateMap := c.store.ProcessGetRelocateMap()
storeNodes := c.store.NodeList()
have := c.manager.ClusterProcessList()
nodes := c.manager.NodeList()
nodesMap := map[string]node.About{}
for _, node := range nodes {
about := node.About()
if storeNode, hasStoreNode := storeNodes[about.ID]; hasStoreNode {
about.State = storeNode.State
}
nodesMap[about.ID] = about
}
logger.Debug().WithFields(log.Fields{
"relocate": relocate,
"have": have,
"nodes": nodesMap,
}).Log("Rebalance")
opStack, _, relocatedProcessIDs := relocate(have, nodesMap, relocateMap)
errors := c.applyOpStack(opStack, term)
for _, e := range errors {
// Only apply the command if the error is different.
process, err := c.store.ProcessGet(e.processid)
if err != nil {
continue
}
var errmessage string = ""
if e.err != nil {
if process.Error == e.err.Error() {
continue
}
errmessage = e.err.Error()
} else {
if len(process.Error) == 0 {
continue
}
}
cmd := &store.Command{
Operation: store.OpSetProcessError,
Data: store.CommandSetProcessError{
ID: e.processid,
Error: errmessage,
},
}
c.applyCommand(cmd)
}
cmd := store.CommandUnsetRelocateProcess{
ID: []app.ProcessID{},
}
for _, processid := range relocatedProcessIDs {
cmd.ID = append(cmd.ID, app.ParseProcessID(processid))
}
if len(cmd.ID) != 0 {
c.applyCommand(&store.Command{
Operation: store.OpUnsetRelocateProcess,
Data: cmd,
})
}
}
// relocate returns a list of operations that will move deployed processes to different nodes.
func relocate(have []node.Process, nodes map[string]node.About, relocateMap map[string]string) ([]interface{}, map[string]node.Resources, []string) {
resources := NewResourcePlanner(nodes)
// Mark nodes as throttling where at least one process is still throttling
for _, haveP := range have {
if haveP.Throttling {
resources.Throttling(haveP.NodeID, true)
}
}
relocatedProcessIDs := []string{}
// A map from the process reference to the nodes it is running on.
haveReferenceAffinity := NewReferenceAffinity(have)
opStack := []interface{}{}
// Check for any requested relocations.
for processid, targetNodeid := range relocateMap {
process := node.Process{}
found := false
for _, p := range have {
if processid == p.Config.ProcessID().String() {
process = p
found = true
break
}
}
if !found {
relocatedProcessIDs = append(relocatedProcessIDs, processid)
continue
}
sourceNodeid := process.NodeID
if sourceNodeid == targetNodeid {
relocatedProcessIDs = append(relocatedProcessIDs, processid)
continue
}
if len(targetNodeid) != 0 {
_, hasNode := nodes[targetNodeid]
if !hasNode || !resources.HasNodeEnough(targetNodeid, process.Config.LimitCPU, process.Config.LimitMemory) {
targetNodeid = ""
}
}
if len(targetNodeid) == 0 {
// Try to move the process to a node where other processes with the same
// reference currently reside.
if len(process.Config.Reference) != 0 {
raNodes := haveReferenceAffinity.Nodes(process.Config.Reference, process.Config.Domain)
for _, raNodeid := range raNodes {
// Do not move the process to the node it is currently on.
if raNodeid == sourceNodeid {
continue
}
if resources.HasNodeEnough(raNodeid, process.Config.LimitCPU, process.Config.LimitMemory) {
targetNodeid = raNodeid
break
}
}
}
// Find the best node with enough resources available.
if len(targetNodeid) == 0 {
nodes := resources.FindBestNodes(process.Config.LimitCPU, process.Config.LimitMemory)
for _, nodeid := range nodes {
if nodeid == sourceNodeid {
continue
}
targetNodeid = nodeid
break
}
}
if len(targetNodeid) == 0 {
// There's no other node with enough resources to take over this process.
opStack = append(opStack, processOpSkip{
nodeid: sourceNodeid,
processid: process.Config.ProcessID(),
err: errNotEnoughResourcesForRelocating,
})
continue
}
}
opStack = append(opStack, processOpMove{
fromNodeid: sourceNodeid,
toNodeid: targetNodeid,
config: process.Config,
metadata: process.Metadata,
order: process.Order,
})
// Adjust the resources.
resources.Move(targetNodeid, sourceNodeid, process.CPU, process.Mem)
// Adjust the reference affinity.
haveReferenceAffinity.Move(process.Config.Reference, process.Config.Domain, sourceNodeid, targetNodeid)
relocatedProcessIDs = append(relocatedProcessIDs, processid)
}
return opStack, resources.Map(), relocatedProcessIDs
}

View File

@ -0,0 +1,377 @@
package cluster
import (
"bytes"
"maps"
"time"
"github.com/datarhei/core/v16/cluster/node"
"github.com/datarhei/core/v16/cluster/store"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/log"
)
func (c *cluster) doSynchronize(emergency bool, term uint64) {
wish := c.store.ProcessGetNodeMap()
want := c.store.ProcessList()
storeNodes := c.store.NodeList()
have := c.manager.ClusterProcessList()
nodes := c.manager.NodeList()
logger := c.logger.WithField("term", term)
logger.Debug().WithField("emergency", emergency).Log("Synchronizing")
nodesMap := map[string]node.About{}
for _, node := range nodes {
about := node.About()
if storeNode, hasStoreNode := storeNodes[about.ID]; hasStoreNode {
about.State = storeNode.State
}
nodesMap[about.ID] = about
}
logger.Debug().WithFields(log.Fields{
"want": want,
"have": have,
"nodes": nodesMap,
}).Log("Synchronize")
opStack, _, reality := synchronize(wish, want, have, nodesMap, c.nodeRecoverTimeout)
if !emergency && !maps.Equal(wish, reality) {
cmd := &store.Command{
Operation: store.OpSetProcessNodeMap,
Data: store.CommandSetProcessNodeMap{
Map: reality,
},
}
c.applyCommand(cmd)
}
errors := c.applyOpStack(opStack, term)
if !emergency {
for _, e := range errors {
// Only apply the command if the error is different.
process, err := c.store.ProcessGet(e.processid)
if err != nil {
continue
}
var errmessage string = ""
if e.err != nil {
if process.Error == e.err.Error() {
continue
}
errmessage = e.err.Error()
} else {
if len(process.Error) == 0 {
continue
}
}
cmd := &store.Command{
Operation: store.OpSetProcessError,
Data: store.CommandSetProcessError{
ID: e.processid,
Error: errmessage,
},
}
c.applyCommand(cmd)
}
}
}
// isMetadataUpdateRequired compares two metadata. It relies on the documented property that json.Marshal
// sorts the map keys prior encoding.
func isMetadataUpdateRequired(wantMap map[string]interface{}, haveMap map[string]interface{}) (bool, map[string]interface{}) {
hasChanges := false
changeMap := map[string]interface{}{}
haveMapKeys := map[string]struct{}{}
for key := range haveMap {
haveMapKeys[key] = struct{}{}
}
for key, wantMapValue := range wantMap {
haveMapValue, ok := haveMap[key]
if !ok {
// A key in map1 exists, that doesn't exist in map2, we need to update.
hasChanges = true
}
// Compare the values
changesData, err := json.Marshal(wantMapValue)
if err != nil {
continue
}
completeData, err := json.Marshal(haveMapValue)
if err != nil {
continue
}
if !bytes.Equal(changesData, completeData) {
// The values are not equal, we need to update.
hasChanges = true
}
delete(haveMapKeys, key)
changeMap[key] = wantMapValue
}
for key := range haveMapKeys {
// If there keys in map2 that are not in map1, we have to update.
hasChanges = true
changeMap[key] = nil
}
return hasChanges, changeMap
}
// synchronize returns a list of operations in order to adjust the "have" list to the "want" list
// with taking the available resources on each node into account.
func synchronize(wish map[string]string, want []store.Process, have []node.Process, nodes map[string]node.About, nodeRecoverTimeout time.Duration) ([]interface{}, map[string]node.Resources, map[string]string) {
resources := NewResourcePlanner(nodes)
// Mark nodes as throttling where at least one process is still throttling
for _, haveP := range have {
if haveP.Throttling {
resources.Throttling(haveP.NodeID, true)
}
}
// A map same as wish, but reflecting the actual situation.
reality := map[string]string{}
// A map from the process ID to the process config of the processes
// we want to be running on the nodes.
wantMap := map[string]store.Process{}
for _, wantP := range want {
pid := wantP.Config.ProcessID().String()
wantMap[pid] = wantP
}
opStack := []interface{}{}
// Now we iterate through the processes we actually have running on the nodes
// and remove them from the wantMap. We also make sure that they have the correct order.
// If a process cannot be found on the wantMap, it will be deleted from the nodes.
haveAfterRemove := []node.Process{}
wantOrderStart := []node.Process{}
for _, haveP := range have {
pid := haveP.Config.ProcessID().String()
wantP, ok := wantMap[pid]
if !ok {
// The process is not on the wantMap. Delete it and adjust the resources.
opStack = append(opStack, processOpDelete{
nodeid: haveP.NodeID,
processid: haveP.Config.ProcessID(),
})
resources.Remove(haveP.NodeID, haveP.CPU, haveP.Mem)
continue
}
// The process is on the wantMap. Update the process if the configuration and/or metadata differ.
hasConfigChanges := !wantP.Config.Equal(haveP.Config)
hasMetadataChanges, metadata := isMetadataUpdateRequired(wantP.Metadata, haveP.Metadata)
if hasConfigChanges || hasMetadataChanges {
// TODO: When the required resources increase, should we move this process to a node
// that has them available? Otherwise, this node might start throttling. However, this
// will result in rebalancing.
opStack = append(opStack, processOpUpdate{
nodeid: haveP.NodeID,
processid: haveP.Config.ProcessID(),
config: wantP.Config,
metadata: metadata,
})
}
delete(wantMap, pid)
reality[pid] = haveP.NodeID
if haveP.Order != wantP.Order {
if wantP.Order == "start" {
// Delay pushing them to the stack in order to have
// all resources released first.
wantOrderStart = append(wantOrderStart, haveP)
} else {
opStack = append(opStack, processOpStop{
nodeid: haveP.NodeID,
processid: haveP.Config.ProcessID(),
})
// Release the resources.
resources.Remove(haveP.NodeID, haveP.CPU, haveP.Mem)
}
}
haveAfterRemove = append(haveAfterRemove, haveP)
}
for _, haveP := range wantOrderStart {
nodeid := haveP.NodeID
resources.Add(nodeid, haveP.Config.LimitCPU, haveP.Config.LimitMemory)
// TODO: check if the current node has actually enough resources available,
// otherwise it needs to be moved somewhere else. If the node doesn't
// have enough resources available, the process will be prevented
// from starting.
/*
if hasNodeEnoughResources(r, haveP.Config.LimitCPU, haveP.Config.LimitMemory) {
// Consume the resources
r.CPU += haveP.Config.LimitCPU
r.Mem += haveP.Config.LimitMemory
resources[nodeid] = r
} else {
nodeid = findBestNodeForProcess(resources, haveP.Config.LimitCPU, haveP.Config.LimitMemory)
if len(nodeid) == 0 {
// Start it anyways and let it run into an error
opStack = append(opStack, processOpStart{
nodeid: nodeid,
processid: haveP.Config.ProcessID(),
})
continue
}
if nodeid != haveP.NodeID {
opStack = append(opStack, processOpMove{
fromNodeid: haveP.NodeID,
toNodeid: nodeid,
config: haveP.Config,
metadata: haveP.Metadata,
order: haveP.Order,
})
}
// Consume the resources
r, ok := resources[nodeid]
if ok {
r.CPU += haveP.Config.LimitCPU
r.Mem += haveP.Config.LimitMemory
resources[nodeid] = r
}
}
*/
opStack = append(opStack, processOpStart{
nodeid: nodeid,
processid: haveP.Config.ProcessID(),
})
}
have = haveAfterRemove
// In case a node didn't respond, some PID are still on the wantMap, that would run on
// the currently not responding nodes. We use the wish map to assign them to the node.
// If the node is unavailable for too long, keep these processes on the wantMap, otherwise
// remove them and hope that they will reappear during the nodeRecoverTimeout.
for pid := range wantMap {
// Check if this PID is be assigned to a node.
if nodeid, ok := wish[pid]; ok {
// Check for how long the node hasn't been contacted, or if it still exists.
if node, ok := nodes[nodeid]; ok {
if node.State == "online" {
continue
}
if time.Since(node.LastContact) <= nodeRecoverTimeout {
reality[pid] = nodeid
delete(wantMap, pid)
}
}
}
}
// The wantMap now contains only those processes that need to be installed on a node.
// We will rebuild the "want" array from the wantMap in the same order as the original
// "want" array to make the resulting opStack deterministic.
wantReduced := []store.Process{}
for _, wantP := range want {
pid := wantP.Config.ProcessID().String()
if _, ok := wantMap[pid]; !ok {
continue
}
wantReduced = append(wantReduced, wantP)
}
// Create a map from the process reference to the node it is running on.
haveReferenceAffinity := NewReferenceAffinity(have)
// Now, all remaining processes in the wantMap must be added to one of the nodes.
for _, wantP := range wantReduced {
pid := wantP.Config.ProcessID().String()
// If a process doesn't have any limits defined, reject that process.
if wantP.Config.LimitCPU <= 0 || wantP.Config.LimitMemory <= 0 {
opStack = append(opStack, processOpReject{
processid: wantP.Config.ProcessID(),
err: errNoLimitsDefined,
})
continue
}
// Check if there are already processes with the same reference, and if so
// choose this node. Then check the node if it has enough resources left. If
// not, then select a node with the most available resources.
nodeid := ""
// Try to add the process to a node where other processes with the same reference currently reside.
raNodes := haveReferenceAffinity.Nodes(wantP.Config.Reference, wantP.Config.Domain)
for _, raNodeid := range raNodes {
if resources.HasNodeEnough(raNodeid, wantP.Config.LimitCPU, wantP.Config.LimitMemory) {
nodeid = raNodeid
break
}
}
// Find the node with the most resources available.
if len(nodeid) == 0 {
nodes := resources.FindBestNodes(wantP.Config.LimitCPU, wantP.Config.LimitMemory)
if len(nodes) > 0 {
nodeid = nodes[0]
}
}
if len(nodeid) != 0 {
opStack = append(opStack, processOpAdd{
nodeid: nodeid,
config: wantP.Config,
metadata: wantP.Metadata,
order: wantP.Order,
})
// Consume the resources
resources.Add(nodeid, wantP.Config.LimitCPU, wantP.Config.LimitMemory)
reality[pid] = nodeid
haveReferenceAffinity.Add(wantP.Config.Reference, wantP.Config.Domain, nodeid)
} else {
opStack = append(opStack, processOpReject{
processid: wantP.Config.ProcessID(),
err: errNotEnoughResourcesForDeployment,
})
}
}
return opStack, resources.Map(), reality
}

File diff suppressed because it is too large Load Diff

37
cluster/node.go Normal file
View File

@ -0,0 +1,37 @@
package cluster
import (
"errors"
"github.com/datarhei/core/v16/cluster/store"
)
func (c *cluster) ListNodes() map[string]store.Node {
return c.store.NodeList()
}
var ErrUnsupportedNodeState = errors.New("unsupported node state")
func (c *cluster) NodeSetState(origin, id, state string) error {
switch state {
case "online":
case "maintenance":
case "leave":
default:
return ErrUnsupportedNodeState
}
if !c.IsRaftLeader() {
return c.forwarder.NodeSetState(origin, id, state)
}
cmd := &store.Command{
Operation: store.OpSetNodeState,
Data: &store.CommandSetNodeState{
NodeID: id,
State: state,
},
}
return c.applyCommand(cmd)
}

View File

@ -1,4 +1,4 @@
package proxy
package node
import (
"errors"

View File

@ -1,4 +1,4 @@
package proxy
package node
import (
"testing"

806
cluster/node/core.go Normal file

File diff suppressed because it is too large Load Diff

601
cluster/node/manager.go Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,21 +7,9 @@ import (
"github.com/datarhei/core/v16/restream/app"
)
func (c *cluster) ListProcesses() []store.Process {
return c.store.ListProcesses()
}
func (c *cluster) GetProcess(id app.ProcessID) (store.Process, error) {
return c.store.GetProcess(id)
}
func (c *cluster) AddProcess(origin string, config *app.Config) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
func (c *cluster) ProcessAdd(origin string, config *app.Config) error {
if !c.IsRaftLeader() {
return c.forwarder.AddProcess(origin, config)
return c.forwarder.ProcessAdd(origin, config)
}
cmd := &store.Command{
@ -34,13 +22,9 @@ func (c *cluster) AddProcess(origin string, config *app.Config) error {
return c.applyCommand(cmd)
}
func (c *cluster) RemoveProcess(origin string, id app.ProcessID) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
func (c *cluster) ProcessRemove(origin string, id app.ProcessID) error {
if !c.IsRaftLeader() {
return c.forwarder.RemoveProcess(origin, id)
return c.forwarder.ProcessRemove(origin, id)
}
cmd := &store.Command{
@ -53,13 +37,9 @@ func (c *cluster) RemoveProcess(origin string, id app.ProcessID) error {
return c.applyCommand(cmd)
}
func (c *cluster) UpdateProcess(origin string, id app.ProcessID, config *app.Config) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
func (c *cluster) ProcessUpdate(origin string, id app.ProcessID, config *app.Config) error {
if !c.IsRaftLeader() {
return c.forwarder.UpdateProcess(origin, id, config)
return c.forwarder.ProcessUpdate(origin, id, config)
}
cmd := &store.Command{
@ -73,14 +53,10 @@ func (c *cluster) UpdateProcess(origin string, id app.ProcessID, config *app.Con
return c.applyCommand(cmd)
}
func (c *cluster) SetProcessCommand(origin string, id app.ProcessID, command string) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
}
func (c *cluster) ProcessSetCommand(origin string, id app.ProcessID, command string) error {
if command == "start" || command == "stop" {
if !c.IsRaftLeader() {
return c.forwarder.SetProcessCommand(origin, id, command)
return c.forwarder.ProcessSetCommand(origin, id, command)
}
cmd := &store.Command{
@ -94,21 +70,32 @@ func (c *cluster) SetProcessCommand(origin string, id app.ProcessID, command str
return c.applyCommand(cmd)
}
nodeid, err := c.proxy.FindNodeFromProcess(id)
nodeid, err := c.manager.ProcessFindNodeID(id)
if err != nil {
return fmt.Errorf("the process '%s' is not registered with any node: %w", id.String(), err)
}
return c.proxy.CommandProcess(nodeid, id, command)
return c.manager.ProcessCommand(nodeid, id, command)
}
func (c *cluster) SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error {
if ok, _ := c.IsDegraded(); ok {
return ErrDegraded
func (c *cluster) ProcessesRelocate(origin string, relocations map[app.ProcessID]string) error {
if !c.IsRaftLeader() {
return c.forwarder.ProcessesRelocate(origin, relocations)
}
cmd := &store.Command{
Operation: store.OpSetRelocateProcess,
Data: &store.CommandSetRelocateProcess{
Map: relocations,
},
}
return c.applyCommand(cmd)
}
func (c *cluster) ProcessSetMetadata(origin string, id app.ProcessID, key string, data interface{}) error {
if !c.IsRaftLeader() {
return c.forwarder.SetProcessMetadata(origin, id, key, data)
return c.forwarder.ProcessSetMetadata(origin, id, key, data)
}
cmd := &store.Command{
@ -123,12 +110,8 @@ func (c *cluster) SetProcessMetadata(origin string, id app.ProcessID, key string
return c.applyCommand(cmd)
}
func (c *cluster) GetProcessMetadata(origin string, id app.ProcessID, key string) (interface{}, error) {
if ok, _ := c.IsDegraded(); ok {
return nil, ErrDegraded
}
p, err := c.store.GetProcess(id)
func (c *cluster) ProcessGetMetadata(origin string, id app.ProcessID, key string) (interface{}, error) {
p, err := c.store.ProcessGet(id)
if err != nil {
return nil, err
}
@ -144,7 +127,3 @@ func (c *cluster) GetProcessMetadata(origin string, id app.ProcessID, key string
return data, nil
}
func (c *cluster) GetProcessNodeMap() map[string]string {
return c.store.GetProcessNodeMap()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

123
cluster/resources.go Normal file
View File

@ -0,0 +1,123 @@
package cluster
import (
"sort"
"github.com/datarhei/core/v16/cluster/node"
)
type resourcePlanner struct {
nodes map[string]node.Resources
blocked map[string]struct{}
}
func NewResourcePlanner(nodes map[string]node.About) *resourcePlanner {
r := &resourcePlanner{
nodes: map[string]node.Resources{},
blocked: map[string]struct{}{},
}
for nodeid, about := range nodes {
r.nodes[nodeid] = about.Resources
if about.State != "online" {
r.blocked[nodeid] = struct{}{}
}
}
return r
}
func (r *resourcePlanner) Throttling(nodeid string, throttling bool) {
res, hasNode := r.nodes[nodeid]
if !hasNode {
return
}
res.IsThrottling = throttling
r.nodes[nodeid] = res
}
// HasNodeEnough returns whether a node has enough resources available for the
// requested cpu and memory consumption.
func (r *resourcePlanner) HasNodeEnough(nodeid string, cpu float64, mem uint64) bool {
res, hasNode := r.nodes[nodeid]
if !hasNode {
return false
}
if _, hasNode := r.blocked[nodeid]; hasNode {
return false
}
if res.Error == nil && res.CPU+cpu < res.CPULimit && res.Mem+mem < res.MemLimit && !res.IsThrottling {
return true
}
return false
}
// FindBestNodes returns an array of nodeids that can fit the requested cpu and memory requirements. If no
// such node is available, an empty array is returned. The array is sorted by the most suitable node first.
func (r *resourcePlanner) FindBestNodes(cpu float64, mem uint64) []string {
nodes := []string{}
for id := range r.nodes {
if r.HasNodeEnough(id, cpu, mem) {
nodes = append(nodes, id)
}
}
sort.SliceStable(nodes, func(i, j int) bool {
nodeA, nodeB := nodes[i], nodes[j]
if r.nodes[nodeA].CPU != r.nodes[nodeB].CPU {
return r.nodes[nodeA].CPU < r.nodes[nodeB].CPU
}
return r.nodes[nodeA].Mem <= r.nodes[nodeB].Mem
})
return nodes
}
// Add adds the resources of the node according to the cpu and memory utilization.
func (r *resourcePlanner) Add(nodeid string, cpu float64, mem uint64) {
res, hasRes := r.nodes[nodeid]
if !hasRes {
return
}
res.CPU += cpu
res.Mem += mem
r.nodes[nodeid] = res
}
// Remove subtracts the resources from the node according to the cpu and memory utilization.
func (r *resourcePlanner) Remove(nodeid string, cpu float64, mem uint64) {
res, hasRes := r.nodes[nodeid]
if !hasRes {
return
}
res.CPU -= cpu
if res.CPU < 0 {
res.CPU = 0
}
if mem >= res.Mem {
res.Mem = 0
} else {
res.Mem -= mem
}
r.nodes[nodeid] = res
}
// Move adjusts the resources from the target and source node according to the cpu and memory utilization.
func (r *resourcePlanner) Move(target, source string, cpu float64, mem uint64) {
r.Add(target, cpu, mem)
r.Remove(source, cpu, mem)
}
func (r *resourcePlanner) Map() map[string]node.Resources {
return r.nodes
}

6
cluster/store/errors.go Normal file
View File

@ -0,0 +1,6 @@
package store
import "errors"
var ErrNotFound = errors.New("")
var ErrBadRequest = errors.New("")

View File

@ -11,7 +11,7 @@ func (s *store) addIdentity(cmd CommandAddIdentity) error {
err := s.data.Users.userlist.Add(cmd.Identity)
if err != nil {
return fmt.Errorf("the identity with the name '%s' already exists", cmd.Identity.Name)
return fmt.Errorf("the identity with the name '%s' already exists%w", cmd.Identity.Name, ErrBadRequest)
}
now := time.Now()
@ -30,17 +30,17 @@ func (s *store) updateIdentity(cmd CommandUpdateIdentity) error {
defer s.lock.Unlock()
if cmd.Name == "$anon" {
return fmt.Errorf("the identity with the name '%s' can't be updated", cmd.Name)
return fmt.Errorf("the identity with the name '%s' can't be updated%w", cmd.Name, ErrBadRequest)
}
oldUser, err := s.data.Users.userlist.Get(cmd.Name)
if err != nil {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
return fmt.Errorf("the identity with the name '%s' doesn't exist%w", cmd.Name, ErrNotFound)
}
o, ok := s.data.Users.Users[oldUser.Name]
if !ok {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
return fmt.Errorf("the identity with the name '%s' doesn't exist%w", cmd.Name, ErrNotFound)
}
err = s.data.Users.userlist.Update(cmd.Name, cmd.Identity)
@ -50,7 +50,7 @@ func (s *store) updateIdentity(cmd CommandUpdateIdentity) error {
user, err := s.data.Users.userlist.Get(cmd.Identity.Name)
if err != nil {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Identity.Name)
return fmt.Errorf("the identity with the name '%s' doesn't exist%w", cmd.Identity.Name, ErrNotFound)
}
now := time.Now()
@ -89,7 +89,7 @@ func (s *store) removeIdentity(cmd CommandRemoveIdentity) error {
return nil
}
func (s *store) ListUsers() Users {
func (s *store) IAMIdentityList() Users {
s.lock.RLock()
defer s.lock.RUnlock()
@ -104,7 +104,7 @@ func (s *store) ListUsers() Users {
return u
}
func (s *store) GetUser(name string) Users {
func (s *store) IAMIdentityGet(name string) Users {
s.lock.RLock()
defer s.lock.RUnlock()

View File

@ -49,7 +49,7 @@ func TestAddIdentity(t *testing.T) {
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
u := s.GetUser("foobar")
u := s.IAMIdentityGet("foobar")
require.Equal(t, 1, len(u.Users))
user := u.Users[0]
@ -254,7 +254,7 @@ func TestUpdateIdentity(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
foobar := s.GetUser("foobar1").Users[0]
foobar := s.IAMIdentityGet("foobar1").Users[0]
require.True(t, foobar.CreatedAt.Equal(foobar.UpdatedAt))
require.False(t, time.Time{}.Equal(foobar.CreatedAt))
@ -287,13 +287,13 @@ func TestUpdateIdentity(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
u := s.GetUser("foobar1")
u := s.IAMIdentityGet("foobar1")
require.Empty(t, u.Users)
u = s.GetUser("foobar2")
u = s.IAMIdentityGet("foobar2")
require.NotEmpty(t, u.Users)
u = s.GetUser("foobaz")
u = s.IAMIdentityGet("foobaz")
require.NotEmpty(t, u.Users)
require.True(t, u.Users[0].CreatedAt.Equal(foobar.CreatedAt))
@ -367,22 +367,22 @@ func TestUpdateIdentityWithAlias(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Users.Users))
u := s.GetUser("foobar1")
u := s.IAMIdentityGet("foobar1")
require.Empty(t, u.Users)
u = s.GetUser("fooalias1")
u = s.IAMIdentityGet("fooalias1")
require.Empty(t, u.Users)
u = s.GetUser("foobar2")
u = s.IAMIdentityGet("foobar2")
require.NotEmpty(t, u.Users)
u = s.GetUser("fooalias2")
u = s.IAMIdentityGet("fooalias2")
require.NotEmpty(t, u.Users)
u = s.GetUser("foobaz")
u = s.IAMIdentityGet("foobaz")
require.NotEmpty(t, u.Users)
u = s.GetUser("fooalias")
u = s.IAMIdentityGet("fooalias")
require.NotEmpty(t, u.Users)
}

View File

@ -1,6 +1,7 @@
package store
import (
"errors"
"io/fs"
"strings"
"time"
@ -25,7 +26,7 @@ func (s *store) unsetKV(cmd CommandUnsetKV) error {
defer s.lock.Unlock()
if _, ok := s.data.KVS[cmd.Key]; !ok {
return fs.ErrNotExist
return errors.Join(fs.ErrNotExist, ErrNotFound)
}
delete(s.data.KVS, cmd.Key)
@ -33,7 +34,7 @@ func (s *store) unsetKV(cmd CommandUnsetKV) error {
return nil
}
func (s *store) ListKVS(prefix string) map[string]Value {
func (s *store) KVSList(prefix string) map[string]Value {
s.lock.RLock()
defer s.lock.RUnlock()
@ -50,13 +51,13 @@ func (s *store) ListKVS(prefix string) map[string]Value {
return m
}
func (s *store) GetFromKVS(key string) (Value, error) {
func (s *store) KVSGetValue(key string) (Value, error) {
s.lock.RLock()
defer s.lock.RUnlock()
value, ok := s.data.KVS[key]
if !ok {
return Value{}, fs.ErrNotExist
return Value{}, errors.Join(fs.ErrNotExist, ErrNotFound)
}
return value, nil

View File

@ -34,7 +34,7 @@ func TestSetKV(t *testing.T) {
})
require.NoError(t, err)
value, err := s.GetFromKVS("foo")
value, err := s.KVSGetValue("foo")
require.NoError(t, err)
require.Equal(t, "bar", value.Value)
@ -46,7 +46,7 @@ func TestSetKV(t *testing.T) {
})
require.NoError(t, err)
value, err = s.GetFromKVS("foo")
value, err = s.KVSGetValue("foo")
require.NoError(t, err)
require.Equal(t, "baz", value.Value)
require.Greater(t, value.UpdatedAt, updatedAt)
@ -86,7 +86,7 @@ func TestUnsetKVCommand(t *testing.T) {
},
})
require.Error(t, err)
require.Equal(t, fs.ErrNotExist, err)
require.ErrorIs(t, err, fs.ErrNotExist)
}
func TestUnsetKV(t *testing.T) {
@ -99,7 +99,7 @@ func TestUnsetKV(t *testing.T) {
})
require.NoError(t, err)
_, err = s.GetFromKVS("foo")
_, err = s.KVSGetValue("foo")
require.NoError(t, err)
err = s.unsetKV(CommandUnsetKV{
@ -107,7 +107,7 @@ func TestUnsetKV(t *testing.T) {
})
require.NoError(t, err)
_, err = s.GetFromKVS("foo")
_, err = s.KVSGetValue("foo")
require.Error(t, err)
require.Equal(t, fs.ErrNotExist, err)
require.ErrorIs(t, err, fs.ErrNotExist)
}

View File

@ -13,7 +13,7 @@ func (s *store) createLock(cmd CommandCreateLock) error {
if ok {
if time.Now().Before(validUntil) {
return fmt.Errorf("the lock with the ID '%s' already exists", cmd.Name)
return fmt.Errorf("the lock with the ID '%s' already exists%w", cmd.Name, ErrBadRequest)
}
}
@ -35,7 +35,7 @@ func (s *store) deleteLock(cmd CommandDeleteLock) error {
return nil
}
func (s *store) clearLocks(cmd CommandClearLocks) error {
func (s *store) clearLocks(_ CommandClearLocks) error {
s.lock.Lock()
defer s.lock.Unlock()
@ -51,7 +51,7 @@ func (s *store) clearLocks(cmd CommandClearLocks) error {
return nil
}
func (s *store) HasLock(name string) bool {
func (s *store) LockHasLock(name string) bool {
s.lock.RLock()
defer s.lock.RUnlock()
@ -60,7 +60,7 @@ func (s *store) HasLock(name string) bool {
return ok
}
func (s *store) ListLocks() map[string]time.Time {
func (s *store) LockList() map[string]time.Time {
s.lock.RLock()
defer s.lock.RUnlock()

35
cluster/store/node.go Normal file
View File

@ -0,0 +1,35 @@
package store
import "time"
func (s *store) setNodeState(cmd CommandSetNodeState) error {
s.lock.Lock()
defer s.lock.Unlock()
if cmd.State == "online" {
delete(s.data.Nodes, cmd.NodeID)
return nil
}
node := s.data.Nodes[cmd.NodeID]
node.State = cmd.State
node.UpdatedAt = time.Now()
s.data.Nodes[cmd.NodeID] = node
return nil
}
func (s *store) NodeList() map[string]Node {
s.lock.RLock()
defer s.lock.RUnlock()
m := map[string]Node{}
for id, node := range s.data.Nodes {
m[id] = node
}
return m
}

View File

@ -16,12 +16,12 @@ func (s *store) setPolicies(cmd CommandSetPolicies) error {
if cmd.Name != "$anon" {
user, err := s.data.Users.userlist.Get(cmd.Name)
if err != nil {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
return fmt.Errorf("unknown identity %s%w", cmd.Name, ErrNotFound)
}
u, ok := s.data.Users.Users[user.Name]
if !ok {
return fmt.Errorf("the identity with the name '%s' doesn't exist", cmd.Name)
return fmt.Errorf("unknown identity %s%w", cmd.Name, ErrNotFound)
}
u.UpdatedAt = now
@ -45,7 +45,7 @@ func (s *store) setPolicies(cmd CommandSetPolicies) error {
return nil
}
func (s *store) ListPolicies() Policies {
func (s *store) IAMPolicyList() Policies {
s.lock.RLock()
defer s.lock.RUnlock()
@ -60,7 +60,7 @@ func (s *store) ListPolicies() Policies {
return p
}
func (s *store) ListUserPolicies(name string) Policies {
func (s *store) IAMIdentityPolicyList(name string) Policies {
s.lock.RLock()
defer s.lock.RUnlock()

View File

@ -88,7 +88,7 @@ func TestSetPolicies(t *testing.T) {
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 0, len(s.data.Policies.Policies))
users := s.GetUser("foobar")
users := s.IAMIdentityGet("foobar")
require.NotEmpty(t, users.Users)
updatedAt := users.Users[0].UpdatedAt
@ -101,7 +101,7 @@ func TestSetPolicies(t *testing.T) {
require.Equal(t, 1, len(s.data.Users.Users))
require.Equal(t, 2, len(s.data.Policies.Policies["foobar"]))
users = s.GetUser("foobar")
users = s.IAMIdentityGet("foobar")
require.NotEmpty(t, users.Users)
require.False(t, updatedAt.Equal(users.Users[0].UpdatedAt))

View File

@ -14,12 +14,12 @@ func (s *store) addProcess(cmd CommandAddProcess) error {
id := cmd.Config.ProcessID().String()
if cmd.Config.LimitCPU <= 0 || cmd.Config.LimitMemory <= 0 {
return fmt.Errorf("the process with the ID '%s' must have limits defined", id)
return fmt.Errorf("the process with the ID '%s' must have limits defined%w", id, ErrBadRequest)
}
_, ok := s.data.Process[id]
if ok {
return fmt.Errorf("the process with the ID '%s' already exists", id)
return fmt.Errorf("the process with the ID '%s' already exists%w", id, ErrBadRequest)
}
order := "stop"
@ -48,7 +48,7 @@ func (s *store) removeProcess(cmd CommandRemoveProcess) error {
_, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exist", id)
return fmt.Errorf("the process with the ID '%s' doesn't exist%w", id, ErrNotFound)
}
delete(s.data.Process, id)
@ -64,12 +64,12 @@ func (s *store) updateProcess(cmd CommandUpdateProcess) error {
dstid := cmd.Config.ProcessID().String()
if cmd.Config.LimitCPU <= 0 || cmd.Config.LimitMemory <= 0 {
return fmt.Errorf("the process with the ID '%s' must have limits defined", dstid)
return fmt.Errorf("the process with the ID '%s' must have limits defined%w", dstid, ErrBadRequest)
}
p, ok := s.data.Process[srcid]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", srcid)
return fmt.Errorf("the process with the ID '%s' doesn't exists%w", srcid, ErrNotFound)
}
if p.Config.Equal(cmd.Config) {
@ -87,7 +87,7 @@ func (s *store) updateProcess(cmd CommandUpdateProcess) error {
_, ok = s.data.Process[dstid]
if ok {
return fmt.Errorf("the process with the ID '%s' already exists", dstid)
return fmt.Errorf("the process with the ID '%s' already exists%w", dstid, ErrBadRequest)
}
now := time.Now()
@ -102,6 +102,30 @@ func (s *store) updateProcess(cmd CommandUpdateProcess) error {
return nil
}
func (s *store) setRelocateProcess(cmd CommandSetRelocateProcess) error {
s.lock.Lock()
defer s.lock.Unlock()
for processid, targetNodeid := range cmd.Map {
id := processid.String()
s.data.ProcessRelocateMap[id] = targetNodeid
}
return nil
}
func (s *store) unsetRelocateProcess(cmd CommandUnsetRelocateProcess) error {
s.lock.Lock()
defer s.lock.Unlock()
for _, processid := range cmd.ID {
id := processid.String()
delete(s.data.ProcessRelocateMap, id)
}
return nil
}
func (s *store) setProcessOrder(cmd CommandSetProcessOrder) error {
s.lock.Lock()
defer s.lock.Unlock()
@ -110,7 +134,7 @@ func (s *store) setProcessOrder(cmd CommandSetProcessOrder) error {
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
return fmt.Errorf("the process with the ID '%s' doesn't exists%w", cmd.ID, ErrNotFound)
}
p.Order = cmd.Order
@ -129,7 +153,7 @@ func (s *store) setProcessMetadata(cmd CommandSetProcessMetadata) error {
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
return fmt.Errorf("the process with the ID '%s' doesn't exists%w", cmd.ID, ErrNotFound)
}
if p.Metadata == nil {
@ -156,7 +180,7 @@ func (s *store) setProcessError(cmd CommandSetProcessError) error {
p, ok := s.data.Process[id]
if !ok {
return fmt.Errorf("the process with the ID '%s' doesn't exists", cmd.ID)
return fmt.Errorf("the process with the ID '%s' doesn't exists%w", cmd.ID, ErrNotFound)
}
p.Error = cmd.Error
@ -175,7 +199,7 @@ func (s *store) setProcessNodeMap(cmd CommandSetProcessNodeMap) error {
return nil
}
func (s *store) ListProcesses() []Process {
func (s *store) ProcessList() []Process {
s.lock.RLock()
defer s.lock.RUnlock()
@ -195,13 +219,13 @@ func (s *store) ListProcesses() []Process {
return processes
}
func (s *store) GetProcess(id app.ProcessID) (Process, error) {
func (s *store) ProcessGet(id app.ProcessID) (Process, error) {
s.lock.RLock()
defer s.lock.RUnlock()
process, ok := s.data.Process[id.String()]
if !ok {
return Process{}, fmt.Errorf("not found")
return Process{}, fmt.Errorf("not found%w", ErrNotFound)
}
return Process{
@ -214,7 +238,7 @@ func (s *store) GetProcess(id app.ProcessID) (Process, error) {
}, nil
}
func (s *store) GetProcessNodeMap() map[string]string {
func (s *store) ProcessGetNodeMap() map[string]string {
s.lock.RLock()
defer s.lock.RUnlock()
@ -226,3 +250,16 @@ func (s *store) GetProcessNodeMap() map[string]string {
return m
}
func (s *store) ProcessGetRelocateMap() map[string]string {
s.lock.RLock()
defer s.lock.RUnlock()
m := map[string]string{}
for key, value := range s.data.ProcessRelocateMap {
m[key] = value
}
return m
}

View File

@ -301,13 +301,13 @@ func TestUpdateProcess(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 2, len(s.data.Process))
_, err = s.GetProcess(config1.ProcessID())
_, err = s.ProcessGet(config1.ProcessID())
require.Error(t, err)
_, err = s.GetProcess(config2.ProcessID())
_, err = s.ProcessGet(config2.ProcessID())
require.NoError(t, err)
_, err = s.GetProcess(config.ProcessID())
_, err = s.ProcessGet(config.ProcessID())
require.NoError(t, err)
}
@ -330,7 +330,7 @@ func TestSetProcessOrderCommand(t *testing.T) {
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
p, err := s.GetProcess(config.ProcessID())
p, err := s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "stop", p.Order)
@ -343,7 +343,7 @@ func TestSetProcessOrderCommand(t *testing.T) {
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
p, err = s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "start", p.Order)
}
@ -382,7 +382,7 @@ func TestSetProcessOrder(t *testing.T) {
})
require.NoError(t, err)
p, err := s.GetProcess(config.ProcessID())
p, err := s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "stop", p.Order)
@ -392,7 +392,7 @@ func TestSetProcessOrder(t *testing.T) {
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
p, err = s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "start", p.Order)
}
@ -416,7 +416,7 @@ func TestSetProcessMetadataCommand(t *testing.T) {
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
p, err := s.GetProcess(config.ProcessID())
p, err := s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Empty(t, p.Metadata)
@ -432,7 +432,7 @@ func TestSetProcessMetadataCommand(t *testing.T) {
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
p, err = s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.NotEmpty(t, p.Metadata)
@ -477,7 +477,7 @@ func TestSetProcessMetadata(t *testing.T) {
})
require.NoError(t, err)
p, err := s.GetProcess(config.ProcessID())
p, err := s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.NotEmpty(t, p.Metadata)
@ -492,7 +492,7 @@ func TestSetProcessMetadata(t *testing.T) {
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
p, err = s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.NotEmpty(t, p.Metadata)
@ -506,7 +506,7 @@ func TestSetProcessMetadata(t *testing.T) {
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
p, err = s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.NotEmpty(t, p.Metadata)
@ -533,7 +533,7 @@ func TestSetProcessErrorCommand(t *testing.T) {
require.NoError(t, err)
require.NotEmpty(t, s.data.Process)
p, err := s.GetProcess(config.ProcessID())
p, err := s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "", p.Error)
@ -546,7 +546,7 @@ func TestSetProcessErrorCommand(t *testing.T) {
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
p, err = s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "foobar", p.Error)
}
@ -585,7 +585,7 @@ func TestSetProcessError(t *testing.T) {
})
require.NoError(t, err)
p, err := s.GetProcess(config.ProcessID())
p, err := s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "", p.Error)
@ -595,7 +595,7 @@ func TestSetProcessError(t *testing.T) {
})
require.NoError(t, err)
p, err = s.GetProcess(config.ProcessID())
p, err = s.ProcessGet(config.ProcessID())
require.NoError(t, err)
require.Equal(t, "foobar", p.Error)
}
@ -642,6 +642,6 @@ func TestSetProcessNodeMap(t *testing.T) {
require.NoError(t, err)
require.Equal(t, m2, s.data.ProcessNodeMap)
m := s.GetProcessNodeMap()
m := s.ProcessGetNodeMap()
require.Equal(t, m2, m)
}

View File

@ -20,20 +20,23 @@ type Store interface {
OnApply(func(op Operation))
ListProcesses() []Process
GetProcess(id app.ProcessID) (Process, error)
GetProcessNodeMap() map[string]string
ProcessList() []Process
ProcessGet(id app.ProcessID) (Process, error)
ProcessGetNodeMap() map[string]string
ProcessGetRelocateMap() map[string]string
ListUsers() Users
GetUser(name string) Users
ListPolicies() Policies
ListUserPolicies(name string) Policies
IAMIdentityList() Users
IAMIdentityGet(name string) Users
IAMIdentityPolicyList(name string) Policies
IAMPolicyList() Policies
HasLock(name string) bool
ListLocks() map[string]time.Time
LockHasLock(name string) bool
LockList() map[string]time.Time
ListKVS(prefix string) map[string]Value
GetFromKVS(key string) (Value, error)
KVSList(prefix string) map[string]Value
KVSGetValue(key string) (Value, error)
NodeList() map[string]Node
}
type Process struct {
@ -60,25 +63,33 @@ type Value struct {
UpdatedAt time.Time
}
type Node struct {
State string
UpdatedAt time.Time
}
type Operation string
const (
OpAddProcess Operation = "addProcess"
OpRemoveProcess Operation = "removeProcess"
OpUpdateProcess Operation = "updateProcess"
OpSetProcessOrder Operation = "setProcessOrder"
OpSetProcessMetadata Operation = "setProcessMetadata"
OpSetProcessError Operation = "setProcessError"
OpAddIdentity Operation = "addIdentity"
OpUpdateIdentity Operation = "updateIdentity"
OpRemoveIdentity Operation = "removeIdentity"
OpSetPolicies Operation = "setPolicies"
OpSetProcessNodeMap Operation = "setProcessNodeMap"
OpCreateLock Operation = "createLock"
OpDeleteLock Operation = "deleteLock"
OpClearLocks Operation = "clearLocks"
OpSetKV Operation = "setKV"
OpUnsetKV Operation = "unsetKV"
OpAddProcess Operation = "addProcess"
OpRemoveProcess Operation = "removeProcess"
OpUpdateProcess Operation = "updateProcess"
OpSetRelocateProcess Operation = "setRelocateProcess"
OpUnsetRelocateProcess Operation = "unsetRelocateProcess"
OpSetProcessOrder Operation = "setProcessOrder"
OpSetProcessMetadata Operation = "setProcessMetadata"
OpSetProcessError Operation = "setProcessError"
OpAddIdentity Operation = "addIdentity"
OpUpdateIdentity Operation = "updateIdentity"
OpRemoveIdentity Operation = "removeIdentity"
OpSetPolicies Operation = "setPolicies"
OpSetProcessNodeMap Operation = "setProcessNodeMap"
OpCreateLock Operation = "createLock"
OpDeleteLock Operation = "deleteLock"
OpClearLocks Operation = "clearLocks"
OpSetKV Operation = "setKV"
OpUnsetKV Operation = "unsetKV"
OpSetNodeState Operation = "setNodeState"
)
type Command struct {
@ -90,13 +101,21 @@ type CommandAddProcess struct {
Config *app.Config
}
type CommandRemoveProcess struct {
ID app.ProcessID
}
type CommandUpdateProcess struct {
ID app.ProcessID
Config *app.Config
}
type CommandRemoveProcess struct {
ID app.ProcessID
type CommandSetRelocateProcess struct {
Map map[app.ProcessID]string
}
type CommandUnsetRelocateProcess struct {
ID []app.ProcessID
}
type CommandSetProcessOrder struct {
@ -115,6 +134,10 @@ type CommandSetProcessError struct {
Error string
}
type CommandSetProcessNodeMap struct {
Map map[string]string
}
type CommandAddIdentity struct {
Identity identity.User
}
@ -133,10 +156,6 @@ type CommandSetPolicies struct {
Policies []access.Policy
}
type CommandSetProcessNodeMap struct {
Map map[string]string
}
type CommandCreateLock struct {
Name string
ValidUntil time.Time
@ -157,10 +176,16 @@ type CommandUnsetKV struct {
Key string
}
type CommandSetNodeState struct {
NodeID string
State string
}
type storeData struct {
Version uint64
Process map[string]Process
ProcessNodeMap map[string]string
Version uint64
Process map[string]Process // processid -> process
ProcessNodeMap map[string]string // processid -> nodeid
ProcessRelocateMap map[string]string // processid -> nodeid
Users struct {
UpdatedAt time.Time
@ -176,6 +201,8 @@ type storeData struct {
Locks map[string]time.Time
KVS map[string]Value
Nodes map[string]Node
}
func (s *storeData) init() {
@ -184,6 +211,7 @@ func (s *storeData) init() {
s.Version = 1
s.Process = map[string]Process{}
s.ProcessNodeMap = map[string]string{}
s.ProcessRelocateMap = map[string]string{}
s.Users.UpdatedAt = now
s.Users.Users = map[string]identity.User{}
s.Users.userlist = identity.NewUserList()
@ -191,6 +219,7 @@ func (s *storeData) init() {
s.Policies.Policies = map[string][]access.Policy{}
s.Locks = map[string]time.Time{}
s.KVS = map[string]Value{}
s.Nodes = map[string]Node{}
}
// store implements a raft.FSM
@ -297,6 +326,22 @@ func (s *store) applyCommand(c Command) error {
}
err = s.updateProcess(cmd)
case OpSetRelocateProcess:
cmd := CommandSetRelocateProcess{}
err = decodeCommand(&cmd, c.Data)
if err != nil {
break
}
err = s.setRelocateProcess(cmd)
case OpUnsetRelocateProcess:
cmd := CommandUnsetRelocateProcess{}
err = decodeCommand(&cmd, c.Data)
if err != nil {
break
}
err = s.unsetRelocateProcess(cmd)
case OpSetProcessOrder:
cmd := CommandSetProcessOrder{}
err = decodeCommand(&cmd, c.Data)
@ -401,6 +446,14 @@ func (s *store) applyCommand(c Command) error {
}
err = s.unsetKV(cmd)
case OpSetNodeState:
cmd := CommandSetNodeState{}
err = decodeCommand(&cmd, c.Data)
if err != nil {
break
}
err = s.setNodeState(cmd)
default:
s.logger.Warn().WithField("operation", c.Operation).Log("Unknown operation")
err = fmt.Errorf("unknown operation: %s", c.Operation)
@ -448,6 +501,14 @@ func (s *store) Restore(snapshot io.ReadCloser) error {
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 {
if p.Metadata != nil {
continue

View File

@ -38,6 +38,6 @@ func ParseClusterVersion(version string) (ClusterVersion, error) {
// Version of the cluster
var Version = ClusterVersion{
Major: 2,
Minor: 0,
Minor: 1,
Patch: 0,
}

View File

@ -209,6 +209,35 @@ const docTemplate = `{
}
}
},
"/api/v3/cluster/db/node": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "List of nodes in the cluster DB",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "List nodes in the cluster DB",
"operationId": "cluster-3-db-list-nodes",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.ClusterStoreNode"
}
}
}
}
}
},
"/api/v3/cluster/db/policies": {
"get": {
"security": [
@ -1381,6 +1410,107 @@ const docTemplate = `{
}
}
},
"/api/v3/cluster/node/{id}/state": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get the state of a node with the given ID",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Get the state of a node with the given ID",
"operationId": "cluster-3-get-node-state",
"parameters": [
{
"type": "string",
"description": "Node ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.ClusterNodeState"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Set the state of a node with the given ID",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Set the state of a node with the given ID",
"operationId": "cluster-3-set-node-state",
"parameters": [
{
"type": "string",
"description": "Node ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "State",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.ClusterNodeState"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/v3/cluster/node/{id}/version": {
"get": {
"security": [
@ -2039,6 +2169,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": {
"get": {
"security": [
@ -5092,12 +5265,42 @@ const docTemplate = `{
}
}
},
"api.ClusterNodeState": {
"type": "object",
"required": [
"state"
],
"properties": {
"state": {
"type": "string",
"enum": [
"online",
"maintenance",
"leave"
]
}
}
},
"api.ClusterProcessMap": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"api.ClusterProcessReallocate": {
"type": "object",
"properties": {
"process_ids": {
"type": "array",
"items": {
"$ref": "#/definitions/api.ProcessID"
}
},
"target_node_id": {
"type": "string"
}
}
},
"api.ClusterRaft": {
"type": "object",
"properties": {
@ -5122,6 +5325,20 @@ const docTemplate = `{
}
}
},
"api.ClusterStoreNode": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"state": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"api.Command": {
"type": "object",
"required": [
@ -6614,6 +6831,17 @@ const docTemplate = `{
}
}
},
"api.ProcessID": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"api.ProcessReport": {
"type": "object",
"properties": {

View File

@ -201,6 +201,35 @@
}
}
},
"/api/v3/cluster/db/node": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "List of nodes in the cluster DB",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "List nodes in the cluster DB",
"operationId": "cluster-3-db-list-nodes",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/api.ClusterStoreNode"
}
}
}
}
}
},
"/api/v3/cluster/db/policies": {
"get": {
"security": [
@ -1373,6 +1402,107 @@
}
}
},
"/api/v3/cluster/node/{id}/state": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Get the state of a node with the given ID",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Get the state of a node with the given ID",
"operationId": "cluster-3-get-node-state",
"parameters": [
{
"type": "string",
"description": "Node ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.ClusterNodeState"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
},
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Set the state of a node with the given ID",
"produces": [
"application/json"
],
"tags": [
"v16.?.?"
],
"summary": "Set the state of a node with the given ID",
"operationId": "cluster-3-set-node-state",
"parameters": [
{
"type": "string",
"description": "Node ID",
"name": "id",
"in": "path",
"required": true
},
{
"description": "State",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.ClusterNodeState"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/api.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/api.Error"
}
}
}
}
},
"/api/v3/cluster/node/{id}/version": {
"get": {
"security": [
@ -2031,6 +2161,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": {
"get": {
"security": [
@ -5084,12 +5257,42 @@
}
}
},
"api.ClusterNodeState": {
"type": "object",
"required": [
"state"
],
"properties": {
"state": {
"type": "string",
"enum": [
"online",
"maintenance",
"leave"
]
}
}
},
"api.ClusterProcessMap": {
"type": "object",
"additionalProperties": {
"type": "string"
}
},
"api.ClusterProcessReallocate": {
"type": "object",
"properties": {
"process_ids": {
"type": "array",
"items": {
"$ref": "#/definitions/api.ProcessID"
}
},
"target_node_id": {
"type": "string"
}
}
},
"api.ClusterRaft": {
"type": "object",
"properties": {
@ -5114,6 +5317,20 @@
}
}
},
"api.ClusterStoreNode": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"state": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"api.Command": {
"type": "object",
"required": [
@ -6606,6 +6823,17 @@
}
}
},
"api.ProcessID": {
"type": "object",
"properties": {
"domain": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"api.ProcessReport": {
"type": "object",
"properties": {

View File

@ -207,10 +207,30 @@ definitions:
ncpu:
type: number
type: object
api.ClusterNodeState:
properties:
state:
enum:
- online
- maintenance
- leave
type: string
required:
- state
type: object
api.ClusterProcessMap:
additionalProperties:
type: string
type: object
api.ClusterProcessReallocate:
properties:
process_ids:
items:
$ref: '#/definitions/api.ProcessID'
type: array
target_node_id:
type: string
type: object
api.ClusterRaft:
properties:
address:
@ -227,6 +247,15 @@ definitions:
state:
type: string
type: object
api.ClusterStoreNode:
properties:
id:
type: string
state:
type: string
updated_at:
type: string
type: object
api.Command:
properties:
command:
@ -1231,6 +1260,13 @@ definitions:
format: uint64
type: integer
type: object
api.ProcessID:
properties:
domain:
type: string
id:
type: string
type: object
api.ProcessReport:
properties:
created_at:
@ -2689,6 +2725,24 @@ paths:
summary: List locks in the cluster DB
tags:
- v16.?.?
/api/v3/cluster/db/node:
get:
description: List of nodes in the cluster DB
operationId: cluster-3-db-list-nodes
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/api.ClusterStoreNode'
type: array
security:
- ApiKeyAuth: []
summary: List nodes in the cluster DB
tags:
- v16.?.?
/api/v3/cluster/db/policies:
get:
description: List of policies in the cluster
@ -3451,6 +3505,71 @@ paths:
summary: List of processes in the cluster on a node
tags:
- v16.?.?
/api/v3/cluster/node/{id}/state:
get:
description: Get the state of a node with the given ID
operationId: cluster-3-get-node-state
parameters:
- description: Node ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.ClusterNodeState'
"404":
description: Not Found
schema:
$ref: '#/definitions/api.Error'
security:
- ApiKeyAuth: []
summary: Get the state of a node with the given ID
tags:
- v16.?.?
put:
description: Set the state of a node with the given ID
operationId: cluster-3-set-node-state
parameters:
- description: Node ID
in: path
name: id
required: true
type: string
- description: State
in: body
name: config
required: true
schema:
$ref: '#/definitions/api.ClusterNodeState'
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
"400":
description: Bad Request
schema:
$ref: '#/definitions/api.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/api.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/api.Error'
security:
- ApiKeyAuth: []
summary: Set the state of a node with the given ID
tags:
- v16.?.?
/api/v3/cluster/node/{id}/version:
get:
description: List a proxy node by its ID
@ -3893,6 +4012,33 @@ paths:
summary: Probe a process in the cluster
tags:
- 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:
get:
description: Retrieve snapshot of the cluster DB

View File

@ -23,24 +23,24 @@ type Codec struct {
Decoders []string
}
func (a Codec) Equal(b Codec) bool {
func (a Codec) Equal(b Codec) error {
if a.Id != b.Id {
return false
return fmt.Errorf("id expected: %s, actual: %s", a.Id, b.Id)
}
if a.Name != b.Name {
return false
return fmt.Errorf("name expected: %s, actual: %s", a.Name, b.Name)
}
if !slices.EqualComparableElements(a.Encoders, b.Encoders) {
return false
if err := slices.EqualComparableElements(a.Encoders, b.Encoders); err != nil {
return fmt.Errorf("codec %s encoders: %w", a.Name, err)
}
if !slices.EqualComparableElements(a.Decoders, b.Decoders) {
return false
if err := slices.EqualComparableElements(a.Decoders, b.Decoders); err != nil {
return fmt.Errorf("codec %s decoders: %w", a.Name, err)
}
return true
return nil
}
type ffCodecs struct {
@ -49,20 +49,20 @@ type ffCodecs struct {
Subtitle []Codec
}
func (a ffCodecs) Equal(b ffCodecs) bool {
if !slices.EqualEqualerElements(a.Audio, b.Audio) {
return false
func (a ffCodecs) Equal(b ffCodecs) error {
if err := slices.EqualEqualerElements(a.Audio, b.Audio); err != nil {
return fmt.Errorf("audio: %w", err)
}
if !slices.EqualEqualerElements(a.Video, b.Video) {
return false
if err := slices.EqualEqualerElements(a.Video, b.Video); err != nil {
return fmt.Errorf("video: %w", err)
}
if !slices.EqualEqualerElements(a.Subtitle, b.Subtitle) {
return false
if err := slices.EqualEqualerElements(a.Subtitle, b.Subtitle); err != nil {
return fmt.Errorf("subtitle: %w", err)
}
return true
return nil
}
// HWDevice represents a hardware device (e.g. USB device)
@ -73,24 +73,24 @@ type HWDevice struct {
Media string
}
func (a HWDevice) Equal(b HWDevice) bool {
func (a HWDevice) Equal(b HWDevice) error {
if a.Id != b.Id {
return false
return fmt.Errorf("id expected: %s, actual: %s", a.Id, b.Id)
}
if a.Name != b.Name {
return false
return fmt.Errorf("name expected: %s, actual: %s", a.Name, b.Name)
}
if a.Extra != b.Extra {
return false
return fmt.Errorf("extra expected: %s, actual: %s", a.Extra, b.Extra)
}
if a.Media != b.Media {
return false
return fmt.Errorf("media expected: %s, actual: %s", a.Media, b.Media)
}
return true
return nil
}
// Device represents a type of device (e.g. V4L2) including connected actual devices
@ -100,20 +100,20 @@ type Device struct {
Devices []HWDevice
}
func (a Device) Equal(b Device) bool {
func (a Device) Equal(b Device) error {
if a.Id != b.Id {
return false
return fmt.Errorf("id expected: %s, actual: %s", a.Id, b.Id)
}
if a.Name != b.Name {
return false
return fmt.Errorf("name expected: %s, actual: %s", a.Name, b.Name)
}
if !slices.EqualEqualerElements(a.Devices, b.Devices) {
return false
if err := slices.EqualEqualerElements(a.Devices, b.Devices); err != nil {
return fmt.Errorf("hwdevice: %w", err)
}
return true
return nil
}
type ffDevices struct {
@ -121,16 +121,16 @@ type ffDevices struct {
Muxers []Device
}
func (a ffDevices) Equal(b ffDevices) bool {
if !slices.EqualEqualerElements(a.Demuxers, b.Demuxers) {
return false
func (a ffDevices) Equal(b ffDevices) error {
if err := slices.EqualEqualerElements(a.Demuxers, b.Demuxers); err != nil {
return fmt.Errorf("demuxers: %w", err)
}
if !slices.EqualEqualerElements(a.Muxers, b.Muxers) {
return false
if err := slices.EqualEqualerElements(a.Muxers, b.Muxers); err != nil {
return fmt.Errorf("muxers: %w", err)
}
return true
return nil
}
// Format represents a supported format (e.g. flv)
@ -144,16 +144,16 @@ type ffFormats struct {
Muxers []Format
}
func (a ffFormats) Equal(b ffFormats) bool {
if !slices.EqualComparableElements(a.Demuxers, b.Demuxers) {
return false
func (a ffFormats) Equal(b ffFormats) error {
if err := slices.EqualComparableElements(a.Demuxers, b.Demuxers); err != nil {
return fmt.Errorf("demuxers: %w", err)
}
if !slices.EqualComparableElements(a.Muxers, b.Muxers) {
return false
if err := slices.EqualComparableElements(a.Muxers, b.Muxers); err != nil {
return fmt.Errorf("muxers: %w", err)
}
return true
return nil
}
// Protocol represents a supported protocol (e.g. rtsp)
@ -167,16 +167,16 @@ type ffProtocols struct {
Output []Protocol
}
func (a ffProtocols) Equal(b ffProtocols) bool {
if !slices.EqualComparableElements(a.Input, b.Input) {
return false
func (a ffProtocols) Equal(b ffProtocols) error {
if err := slices.EqualComparableElements(a.Input, b.Input); err != nil {
return fmt.Errorf("input: %w", err)
}
if !slices.EqualComparableElements(a.Output, b.Output) {
return false
if err := slices.EqualComparableElements(a.Output, b.Output); err != nil {
return fmt.Errorf("output: %w", err)
}
return true
return nil
}
type HWAccel struct {
@ -204,24 +204,24 @@ type ffmpeg struct {
Libraries []Library
}
func (a ffmpeg) Equal(b ffmpeg) bool {
func (a ffmpeg) Equal(b ffmpeg) error {
if a.Version != b.Version {
return false
return fmt.Errorf("version expected: %s, actual: %s", a.Version, b.Version)
}
if a.Compiler != b.Compiler {
return false
return fmt.Errorf("compiler expected: %s, actual: %s", a.Compiler, b.Compiler)
}
if a.Configuration != b.Configuration {
return false
return fmt.Errorf("configuration expected: %s, actual: %s", a.Configuration, b.Configuration)
}
if !slices.EqualComparableElements(a.Libraries, b.Libraries) {
return false
if err := slices.EqualComparableElements(a.Libraries, b.Libraries); err != nil {
return fmt.Errorf("libraries: %w", err)
}
return true
return nil
}
// Skills are the detected capabilities of a ffmpeg binary
@ -237,36 +237,36 @@ type Skills struct {
Protocols ffProtocols
}
func (a Skills) Equal(b Skills) bool {
if !a.FFmpeg.Equal(b.FFmpeg) {
return false
func (a Skills) Equal(b Skills) error {
if err := a.FFmpeg.Equal(b.FFmpeg); err != nil {
return fmt.Errorf("ffmpeg: %w", err)
}
if !slices.EqualComparableElements(a.Filters, b.Filters) {
return false
if err := slices.EqualComparableElements(a.Filters, b.Filters); err != nil {
return fmt.Errorf("filters: %w", err)
}
if !slices.EqualComparableElements(a.HWAccels, b.HWAccels) {
return false
if err := slices.EqualComparableElements(a.HWAccels, b.HWAccels); err != nil {
return fmt.Errorf("hwaccels: %w", err)
}
if !a.Codecs.Equal(b.Codecs) {
return false
if err := a.Codecs.Equal(b.Codecs); err != nil {
return fmt.Errorf("codecs: %w", err)
}
if !a.Devices.Equal(b.Devices) {
return false
if err := a.Devices.Equal(b.Devices); err != nil {
return fmt.Errorf("devices: %w", err)
}
if !a.Formats.Equal(b.Formats) {
return false
if err := a.Formats.Equal(b.Formats); err != nil {
return fmt.Errorf("formats: %w", err)
}
if !a.Protocols.Equal(b.Protocols) {
return false
if err := a.Protocols.Equal(b.Protocols); err != nil {
return fmt.Errorf("protocols: %w", err)
}
return true
return nil
}
// New returns all skills that ffmpeg provides

View File

@ -321,28 +321,28 @@ func TestNew(t *testing.T) {
func TestEqualEmptySkills(t *testing.T) {
s := Skills{}
ok := s.Equal(s)
require.True(t, ok)
err := s.Equal(s)
require.NoError(t, err)
}
func TestEuqalSkills(t *testing.T) {
func TestEqualSkills(t *testing.T) {
binary, err := testhelper.BuildBinary("ffmpeg", "../../internal/testhelper")
require.NoError(t, err, "Failed to build helper program")
s1, err := New(binary)
require.NoError(t, err)
ok := s1.Equal(s1)
require.True(t, ok)
err = s1.Equal(s1)
require.NoError(t, err)
s2, err := New(binary)
require.NoError(t, err)
ok = s1.Equal(s2)
require.True(t, ok)
err = s1.Equal(s2)
require.NoError(t, err)
ok = s1.Equal(Skills{})
require.False(t, ok)
err = s1.Equal(Skills{})
require.Error(t, err)
}
func TestPatchVersion(t *testing.T) {

View File

@ -10,6 +10,12 @@ type globber struct {
glob glob.Glob
}
func MustCompile(pattern string, separators ...rune) Glob {
g := glob.MustCompile(pattern, separators...)
return &globber{glob: g}
}
func Compile(pattern string, separators ...rune) (Glob, error) {
g, err := glob.Compile(pattern, separators...)
if err != nil {

3
go.mod
View File

@ -12,13 +12,13 @@ require (
github.com/atrox/haikunatorgo/v2 v2.0.1
github.com/caddyserver/certmagic v0.21.3
github.com/casbin/casbin/v2 v2.90.0
github.com/datarhei/core-client-go/v16 v16.11.1-0.20240429143858-23ad5985b894
github.com/datarhei/gosrt v0.6.0
github.com/datarhei/joy4 v0.0.0-20240603190808-b1407345907e
github.com/fujiwara/shapeio v1.0.0
github.com/go-playground/validator/v10 v10.21.0
github.com/gobwas/glob v0.2.3
github.com/goccy/go-json v0.10.3
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/gops v0.3.28
github.com/google/uuid v1.6.0
@ -76,7 +76,6 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect

2
go.sum
View File

@ -51,8 +51,6 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20240429143858-23ad5985b894 h1:ZQCTobOGpzfuZxgMWsZviFSXfI5QuttkTgPQz1PKbhU=
github.com/datarhei/core-client-go/v16 v16.11.1-0.20240429143858-23ad5985b894/go.mod h1:Mu2bHqvGJe46KvAhY2ElohuQYhHB64PZeaCNDv6C5b8=
github.com/datarhei/gosrt v0.6.0 h1:HrrXAw90V78ok4WMIhX6se1aTHPCn82Sg2hj+PhdmGc=
github.com/datarhei/gosrt v0.6.0/go.mod h1:fsOWdLSHUHShHjgi/46h6wjtdQrtnSdAQFnlas8ONxs=
github.com/datarhei/joy4 v0.0.0-20240603190808-b1407345907e h1:Qc/0D4xvXrazFkoi/4UGqO15yQ1JN5I8h7RwdzCLgTY=

View File

@ -25,6 +25,10 @@ type ClusterNode struct {
Resources ClusterNodeResources `json:"resources"`
}
type ClusterNodeState struct {
State string `json:"state" validate:"required" enums:"online,maintenance,leave" jsonschema:"enum=online,enum=maintenance,enum=leave"`
}
type ClusterNodeCore struct {
Address string `json:"address"`
Status string `json:"status"`
@ -89,3 +93,14 @@ type ClusterKVSValue struct {
type ClusterKVS map[string]ClusterKVSValue
type ClusterProcessMap map[string]string
type ClusterProcessReallocate struct {
TargetNodeID string `json:"target_node_id"`
Processes []ProcessID `json:"process_ids"`
}
type ClusterStoreNode struct {
ID string `json:"id"`
State string `json:"state"`
UpdatedAt time.Time `json:"updated_at"`
}

View File

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

View File

@ -29,7 +29,7 @@ type ProcessReport struct {
}
// Unmarshal converts a restream log to a report
func (report *ProcessReport) Unmarshal(l *app.Log) {
func (report *ProcessReport) Unmarshal(l *app.Report) {
if l == nil {
return
}

View File

@ -1,4 +1,4 @@
package coreclient
package client
import (
"bytes"
@ -12,12 +12,12 @@ import (
"sync"
"time"
"github.com/goccy/go-json"
"github.com/datarhei/core-client-go/v16/api"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/glob"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/restream/app"
"github.com/Masterminds/semver/v3"
"github.com/gobwas/glob"
jwtgo "github.com/golang-jwt/jwt/v4"
"github.com/klauspost/compress/gzip"
"github.com/klauspost/compress/zstd"
@ -50,24 +50,6 @@ type RestClient interface {
About(cached bool) (api.About, error) // GET /
Config() (int64, api.Config, error) // GET /v3/config
ConfigSet(config interface{}) error // POST /v3/config
ConfigReload() error // GET /v3/config/reload
Graph(query api.GraphQuery) (api.GraphResponse, error) // POST /graph
DiskFSList(sort, order string) ([]api.FileInfo, error) // GET /v3/fs/disk
DiskFSHasFile(path string) bool // HEAD /v3/fs/disk/{path}
DiskFSGetFile(path string) (io.ReadCloser, error) // GET /v3/fs/disk/{path}
DiskFSDeleteFile(path string) error // DELETE /v3/fs/disk/{path}
DiskFSAddFile(path string, data io.Reader) error // PUT /v3/fs/disk/{path}
MemFSList(sort, order string) ([]api.FileInfo, error) // GET /v3/fs/mem
MemFSHasFile(path string) bool // HEAD /v3/fs/mem/{path}
MemFSGetFile(path string) (io.ReadCloser, error) // GET /v3/fs/mem/{path}
MemFSDeleteFile(path string) error // DELETE /v3/fs/mem/{path}
MemFSAddFile(path string, data io.Reader) error // PUT /v3/fs/mem/{path}
FilesystemList(storage, pattern, sort, order string) ([]api.FileInfo, error) // GET /v3/fs/{storage}
FilesystemHasFile(storage, path string) bool // HEAD /v3/fs/{storage}/{path}
FilesystemGetFile(storage, path string) (io.ReadCloser, error) // GET /v3/fs/{storage}/{path}
@ -75,96 +57,28 @@ type RestClient interface {
FilesystemDeleteFile(storage, path string) error // DELETE /v3/fs/{storage}/{path}
FilesystemAddFile(storage, path string, data io.Reader) error // PUT /v3/fs/{storage}/{path}
Log() ([]api.LogEvent, error) // GET /v3/log
Events(ctx context.Context, filters api.EventFilters) (<-chan api.Event, error) // POST /v3/events
Metadata(key string) (api.Metadata, error) // GET /v3/metadata/{key}
MetadataSet(key string, metadata api.Metadata) error // PUT /v3/metadata/{key}
MetricsList() ([]api.MetricsDescription, error) // GET /v3/metrics
Metrics(query api.MetricsQuery) (api.MetricsResponse, error) // POST /v3/metrics
ProcessList(opts ProcessListOptions) ([]api.Process, error) // GET /v3/process
ProcessAdd(p api.ProcessConfig) error // POST /v3/process
Process(id ProcessID, filter []string) (api.Process, error) // GET /v3/process/{id}
ProcessUpdate(id ProcessID, p api.ProcessConfig) error // PUT /v3/process/{id}
ProcessDelete(id ProcessID) error // DELETE /v3/process/{id}
ProcessCommand(id ProcessID, command string) error // PUT /v3/process/{id}/command
ProcessProbe(id ProcessID) (api.Probe, error) // GET /v3/process/{id}/probe
ProcessProbeConfig(config api.ProcessConfig) (api.Probe, error) // POST /v3/process/probe
ProcessConfig(id ProcessID) (api.ProcessConfig, error) // GET /v3/process/{id}/config
ProcessReport(id ProcessID) (api.ProcessReport, error) // GET /v3/process/{id}/report
ProcessState(id ProcessID) (api.ProcessState, error) // GET /v3/process/{id}/state
ProcessMetadata(id ProcessID, key string) (api.Metadata, error) // GET /v3/process/{id}/metadata/{key}
ProcessMetadataSet(id ProcessID, key string, metadata api.Metadata) error // PUT /v3/process/{id}/metadata/{key}
PlayoutStatus(id ProcessID, inputID string) (api.PlayoutStatus, error) // GET /v3/process/{id}/playout/{inputid}/status
IdentitiesList() ([]api.IAMUser, error) // GET /v3/iam/user
Identity(name string) (api.IAMUser, error) // GET /v3/iam/user/{name}
IdentityAdd(u api.IAMUser) error // POST /v3/iam/user
IdentityUpdate(name string, u api.IAMUser) error // PUT /v3/iam/user/{name}
IdentitySetPolicies(name string, p []api.IAMPolicy) error // PUT /v3/iam/user/{name}/policy
IdentityDelete(name string) error // DELETE /v3/iam/user/{name}
Cluster() (*api.ClusterAboutV1, *api.ClusterAboutV2, error) // GET /v3/cluster
ClusterHealthy() (bool, error) // GET /v3/cluster/healthy
ClusterSnapshot() (io.ReadCloser, error) // GET /v3/cluster/snapshot
ClusterLeave() error // PUT /v3/cluster/leave
ClusterTransferLeadership(id string) error // PUT /v3/cluster/transfer/{id}
ClusterNodeList() ([]api.ClusterNode, error) // GET /v3/cluster/node
ClusterNode(id string) (api.ClusterNode, error) // GET /v3/cluster/node/{id}
ClusterNodeFiles(id string) (api.ClusterNodeFiles, error) // GET /v3/cluster/node/{id}/files
ClusterNodeProcessList(id string, opts ProcessListOptions) ([]api.Process, error) // GET /v3/cluster/node/{id}/process
ClusterNodeVersion(id string) (api.Version, error) // GET /v3/cluster/node/{id}/version
ClusterNodeFilesystemList(id, storage, pattern, sort, order string) ([]api.FileInfo, error) // GET /v3/cluster/node/{id}/fs/{storage}
ClusterNodeFilesystemDeleteFile(id, storage, path string) error // DELETE /v3/cluster/node/{id}/fs/{storage}/{path}
ClusterNodeFilesystemPutFile(id, storage, path string, data io.Reader) error // PUT /v3/cluster/node/{id}/fs/{storage}/{path}
ClusterNodeFilesystemGetFile(id, storage, path string) (io.ReadCloser, error) // GET /v3/cluster/node/{id}/fs/{storage}/{path}
ClusterDBProcessList() ([]api.Process, error) // GET /v3/cluster/db/process
ClusterDBProcess(id ProcessID) (api.Process, error) // GET /v3/cluster/db/process/{id}
ClusterDBUserList() ([]api.IAMUser, error) // GET /v3/cluster/db/user
ClusterDBUser(name string) (api.IAMUser, error) // GET /v3/cluster/db/user/{name}
ClusterDBPolicies() ([]api.IAMPolicy, error) // GET /v3/cluster/db/policies
ClusterDBLocks() ([]api.ClusterLock, error) // GET /v3/cluster/db/locks
ClusterDBKeyValues() (api.ClusterKVS, error) // GET /v3/cluster/db/kv
ClusterDBProcessMap() (api.ClusterProcessMap, error) // GET /v3/cluster/db/map/process
ClusterFilesystemList(name, pattern, sort, order string) ([]api.FileInfo, error) // GET /v3/cluster/fs/{storage}
ClusterProcessList(opts ProcessListOptions) ([]api.Process, error) // GET /v3/cluster/process
ClusterProcess(id ProcessID, filter []string) (api.Process, error) // POST /v3/cluster/process
ClusterProcessAdd(p api.ProcessConfig) error // GET /v3/cluster/process/{id}
ClusterProcessUpdate(id ProcessID, p api.ProcessConfig) error // PUT /v3/cluster/process/{id}
ClusterProcessDelete(id ProcessID) error // DELETE /v3/cluster/process/{id}
ClusterProcessCommand(id ProcessID, command string) error // PUT /v3/cluster/process/{id}/command
ClusterProcessMetadata(id ProcessID, key string) (api.Metadata, error) // GET /v3/cluster/process/{id}/metadata/{key}
ClusterProcessMetadataSet(id ProcessID, key string, metadata api.Metadata) error // PUT /v3/cluster/process/{id}/metadata/{key}
ClusterProcessProbe(id ProcessID) (api.Probe, error) // GET /v3/cluster/process/{id}/probe
ClusterProcessProbeConfig(config api.ProcessConfig, coreid string) (api.Probe, error) // POST /v3/cluster/process/probe
ClusterIdentitiesList() ([]api.IAMUser, error) // GET /v3/cluster/iam/user
ClusterIdentity(name string) (api.IAMUser, error) // GET /v3/cluster/iam/user/{name}
ClusterIdentityAdd(u api.IAMUser) error // POST /v3/cluster/iam/user
ClusterIdentityUpdate(name string, u api.IAMUser) error // PUT /v3/cluster/iam/user/{name}
ClusterIdentitySetPolicies(name string, p []api.IAMPolicy) error // PUT /v3/cluster/iam/user/{name}/policy
ClusterIdentityDelete(name string) error // DELETE /v3/cluster/iam/user/{name}
ClusterIAMReload() error // PUT /v3/cluster/iam/reload
ProcessList(opts ProcessListOptions) ([]api.Process, error) // GET /v3/process
ProcessAdd(p *app.Config, metadata map[string]interface{}) error // POST /v3/process
Process(id app.ProcessID, filter []string) (api.Process, error) // GET /v3/process/{id}
ProcessUpdate(id app.ProcessID, p *app.Config, metadata map[string]interface{}) error // PUT /v3/process/{id}
ProcessDelete(id app.ProcessID) error // DELETE /v3/process/{id}
ProcessCommand(id app.ProcessID, command string) error // PUT /v3/process/{id}/command
ProcessProbe(id app.ProcessID) (api.Probe, error) // GET /v3/process/{id}/probe
ProcessProbeConfig(config *app.Config) (api.Probe, error) // POST /v3/process/probe
ProcessConfig(id app.ProcessID) (api.ProcessConfig, error) // GET /v3/process/{id}/config
ProcessReport(id app.ProcessID) (api.ProcessReport, error) // GET /v3/process/{id}/report
ProcessState(id app.ProcessID) (api.ProcessState, error) // GET /v3/process/{id}/state
ProcessMetadata(id app.ProcessID, key string) (api.Metadata, error) // GET /v3/process/{id}/metadata/{key}
ProcessMetadataSet(id app.ProcessID, key string, metadata api.Metadata) error // PUT /v3/process/{id}/metadata/{key}
RTMPChannels() ([]api.RTMPChannel, error) // GET /v3/rtmp
SRTChannels() ([]api.SRTChannel, error) // GET /v3/srt
SRTChannelsRaw() ([]byte, error) // GET /v3/srt
Sessions(collectors []string) (api.SessionsSummary, error) // GET /v3/session
SessionsActive(collectors []string) (api.SessionsActive, error) // GET /v3/session/active
SessionToken(name string, req []api.SessionTokenRequest) ([]api.SessionTokenRequest, error) // PUT /v3/session/token/{username}
Skills() (api.Skills, error) // GET /v3/skills
SkillsReload() error // GET /v3/skills/reload
WidgetProcess(id ProcessID) (api.WidgetProcess, error) // GET /v3/widget/process/{id}
}
type Token struct {
@ -448,6 +362,14 @@ func New(config Config) (RestClient, error) {
path: mustNewGlob("/v3/cluster/node/*/fs/*/**"),
constraint: mustNewConstraint("^16.14.0"),
},
{
path: mustNewGlob("/v3/cluster/db/node"),
constraint: mustNewConstraint("^16.14.0"),
},
{
path: mustNewGlob("/v3/cluster/node/*/state"),
constraint: mustNewConstraint("^16.14.0"),
},
},
"POST": {
{
@ -524,6 +446,14 @@ func New(config Config) (RestClient, error) {
path: mustNewGlob("/v3/cluster/node/*/fs/*/**"),
constraint: mustNewConstraint("^16.14.0"),
},
{
path: mustNewGlob("/v3/cluster/reallocate"),
constraint: mustNewConstraint("^16.14.0"),
},
{
path: mustNewGlob("/v3/cluster/node/*/state"),
constraint: mustNewConstraint("^16.14.0"),
},
},
"DELETE": {
{
@ -614,6 +544,9 @@ func (r *restclient) Address() string {
func (r *restclient) About(cached bool) (api.About, error) {
if cached {
r.aboutLock.RLock()
defer r.aboutLock.RUnlock()
return r.about, nil
}
@ -622,7 +555,22 @@ func (r *restclient) About(cached bool) (api.About, error) {
return api.About{}, err
}
if r.accessToken.IsSet() && len(about.ID) == 0 {
if err := r.refresh(); err != nil {
if err := r.login(); err != nil {
return api.About{}, err
}
}
about, err = r.info()
if err != nil {
return api.About{}, err
}
}
r.aboutLock.Lock()
r.about = about
r.aboutLock.Unlock()
return about, nil
}
@ -821,7 +769,15 @@ func (r *restclient) info() (api.About, error) {
return api.About{}, err
}
if r.accessToken.IsSet() && !r.accessToken.IsExpired() {
if r.accessToken.IsSet() {
if r.accessToken.IsExpired() {
if err := r.refresh(); err != nil {
if err := r.login(); err != nil {
return api.About{}, err
}
}
}
req.Header.Add("Authorization", "Bearer "+r.accessToken.String())
}
@ -942,7 +898,7 @@ func (r *restclient) stream(ctx context.Context, method, path string, query *url
return nil, e
}
e.Body = data
//e.Body = data
err = json.Unmarshal(data, &e)
if err != nil {
@ -967,7 +923,7 @@ func (r *restclient) call(method, path string, query *url.Values, header http.He
body, err := r.stream(ctx, method, path, query, header, contentType, data)
if err != nil {
return nil, err
return nil, fmt.Errorf("%s %s: %w", method, path, err)
}
defer body.Close()

View File

@ -1,4 +1,4 @@
package coreclient
package client
import (
"bytes"
@ -6,9 +6,8 @@ import (
"io"
"net/http"
"github.com/goccy/go-json"
"github.com/datarhei/core-client-go/v16/api"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
)
func (r *restclient) Events(ctx context.Context, filters api.EventFilters) (<-chan api.Event, error) {

View File

@ -1,4 +1,4 @@
package coreclient
package client
import (
"context"
@ -8,9 +8,8 @@ import (
"path/filepath"
"strconv"
"github.com/goccy/go-json"
"github.com/datarhei/core-client-go/v16/api"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
)
const (

256
http/client/process.go Normal file
View File

@ -0,0 +1,256 @@
package client
import (
"bytes"
"net/url"
"strings"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/restream/app"
)
type ProcessListOptions struct {
ID []string
Filter []string
Domain string
Reference string
IDPattern string
RefPattern string
OwnerPattern string
DomainPattern string
}
func (p *ProcessListOptions) Query() *url.Values {
values := &url.Values{}
values.Set("id", strings.Join(p.ID, ","))
values.Set("filter", strings.Join(p.Filter, ","))
values.Set("domain", p.Domain)
values.Set("reference", p.Reference)
values.Set("idpattern", p.IDPattern)
values.Set("refpattern", p.RefPattern)
values.Set("ownerpattern", p.OwnerPattern)
values.Set("domainpattern", p.DomainPattern)
return values
}
func (r *restclient) ProcessList(opts ProcessListOptions) ([]api.Process, error) {
var processes []api.Process
data, err := r.call("GET", "/v3/process", opts.Query(), nil, "", nil)
if err != nil {
return processes, err
}
err = json.Unmarshal(data, &processes)
return processes, err
}
func (r *restclient) Process(id app.ProcessID, filter []string) (api.Process, error) {
var info api.Process
values := &url.Values{}
values.Set("filter", strings.Join(filter, ","))
values.Set("domain", id.Domain)
data, err := r.call("GET", "/v3/process/"+url.PathEscape(id.ID), values, nil, "", nil)
if err != nil {
return info, err
}
err = json.Unmarshal(data, &info)
return info, err
}
func (r *restclient) ProcessAdd(p *app.Config, metadata map[string]interface{}) error {
var buf bytes.Buffer
config := api.ProcessConfig{}
config.Unmarshal(p)
config.Metadata = metadata
e := json.NewEncoder(&buf)
e.Encode(config)
_, err := r.call("POST", "/v3/process", nil, nil, "application/json", &buf)
if err != nil {
return err
}
return nil
}
func (r *restclient) ProcessUpdate(id app.ProcessID, p *app.Config, metadata map[string]interface{}) error {
var buf bytes.Buffer
config := api.ProcessConfig{}
config.Unmarshal(p)
config.Metadata = metadata
e := json.NewEncoder(&buf)
e.Encode(config)
query := &url.Values{}
query.Set("domain", id.Domain)
_, err := r.call("PUT", "/v3/process/"+url.PathEscape(id.ID), query, nil, "application/json", &buf)
if err != nil {
return err
}
return nil
}
func (r *restclient) ProcessDelete(id app.ProcessID) error {
query := &url.Values{}
query.Set("domain", id.Domain)
r.call("DELETE", "/v3/process/"+url.PathEscape(id.ID), query, nil, "", nil)
return nil
}
func (r *restclient) ProcessCommand(id app.ProcessID, command string) error {
var buf bytes.Buffer
e := json.NewEncoder(&buf)
e.Encode(api.Command{
Command: command,
})
query := &url.Values{}
query.Set("domain", id.Domain)
_, err := r.call("PUT", "/v3/process/"+url.PathEscape(id.ID)+"/command", query, nil, "application/json", &buf)
if err != nil {
return err
}
return nil
}
func (r *restclient) ProcessMetadata(id app.ProcessID, key string) (api.Metadata, error) {
var m api.Metadata
path := "/v3/process/" + url.PathEscape(id.ID) + "/metadata"
if len(key) != 0 {
path += "/" + url.PathEscape(key)
}
query := &url.Values{}
query.Set("domain", id.Domain)
data, err := r.call("GET", path, query, nil, "", nil)
if err != nil {
return m, err
}
err = json.Unmarshal(data, &m)
return m, err
}
func (r *restclient) ProcessMetadataSet(id app.ProcessID, key string, metadata api.Metadata) error {
var buf bytes.Buffer
e := json.NewEncoder(&buf)
e.Encode(metadata)
query := &url.Values{}
query.Set("domain", id.Domain)
_, err := r.call("PUT", "/v3/process/"+url.PathEscape(id.ID)+"/metadata/"+url.PathEscape(key), query, nil, "application/json", &buf)
if err != nil {
return err
}
return nil
}
func (r *restclient) ProcessProbe(id app.ProcessID) (api.Probe, error) {
var p api.Probe
query := &url.Values{}
query.Set("domain", id.Domain)
data, err := r.call("GET", "/v3/process/"+url.PathEscape(id.ID)+"/probe", query, nil, "", nil)
if err != nil {
return p, err
}
err = json.Unmarshal(data, &p)
return p, err
}
func (r *restclient) ProcessProbeConfig(p *app.Config) (api.Probe, error) {
var probe api.Probe
var buf bytes.Buffer
config := api.ProcessConfig{}
config.Unmarshal(p)
e := json.NewEncoder(&buf)
e.Encode(config)
data, err := r.call("POST", "/v3/process/probe", nil, nil, "application/json", &buf)
if err != nil {
return probe, err
}
err = json.Unmarshal(data, &p)
return probe, err
}
func (r *restclient) ProcessConfig(id app.ProcessID) (api.ProcessConfig, error) {
var p api.ProcessConfig
query := &url.Values{}
query.Set("domain", id.Domain)
data, err := r.call("GET", "/v3/process/"+url.PathEscape(id.ID)+"/config", query, nil, "", nil)
if err != nil {
return p, err
}
err = json.Unmarshal(data, &p)
return p, err
}
func (r *restclient) ProcessReport(id app.ProcessID) (api.ProcessReport, error) {
var p api.ProcessReport
query := &url.Values{}
query.Set("domain", id.Domain)
data, err := r.call("GET", "/v3/process/"+url.PathEscape(id.ID)+"/report", query, nil, "", nil)
if err != nil {
return p, err
}
err = json.Unmarshal(data, &p)
return p, err
}
func (r *restclient) ProcessState(id app.ProcessID) (api.ProcessState, error) {
var p api.ProcessState
query := &url.Values{}
query.Set("domain", id.Domain)
data, err := r.call("GET", "/v3/process/"+url.PathEscape(id.ID)+"/state", query, nil, "", nil)
if err != nil {
return p, err
}
err = json.Unmarshal(data, &p)
return p, err
}

View File

@ -1,9 +1,8 @@
package coreclient
package client
import (
"github.com/goccy/go-json"
"github.com/datarhei/core-client-go/v16/api"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
)
func (r *restclient) RTMPChannels() ([]api.RTMPChannel, error) {

View File

@ -1,9 +1,8 @@
package coreclient
package client
import (
"github.com/goccy/go-json"
"github.com/datarhei/core-client-go/v16/api"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
)
func (r *restclient) Skills() (api.Skills, error) {

View File

@ -1,9 +1,8 @@
package coreclient
package client
import (
"github.com/goccy/go-json"
"github.com/datarhei/core-client-go/v16/api"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
)
func (r *restclient) SRTChannels() ([]api.SRTChannel, error) {

View File

@ -6,7 +6,7 @@ import (
gofs "io/fs"
"time"
"github.com/datarhei/core/v16/cluster/proxy"
"github.com/datarhei/core/v16/cluster/node"
"github.com/datarhei/core/v16/io/fs"
)
@ -18,10 +18,10 @@ type filesystem struct {
fs.Filesystem
name string
proxy proxy.ProxyReader
proxy *node.Manager
}
func NewClusterFS(name string, fs fs.Filesystem, proxy proxy.ProxyReader) Filesystem {
func NewClusterFS(name string, fs fs.Filesystem, proxy *node.Manager) Filesystem {
if proxy == nil {
return fs
}
@ -42,14 +42,14 @@ func (fs *filesystem) Open(path string) fs.File {
}
// Check if the file is available in the cluster
size, lastModified, err := fs.proxy.GetFileInfo(fs.name, path)
size, lastModified, err := fs.proxy.FilesystemGetFileInfo(fs.name, path)
if err != nil {
return nil
}
file := &file{
getFile: func(offset int64) (io.ReadCloser, error) {
return fs.proxy.GetFile(fs.name, path, offset)
return fs.proxy.FilesystemGetFile(fs.name, path, offset)
},
name: path,
size: size,

View File

@ -44,7 +44,7 @@ func (s *RawAVstreamSwap) UnmarshalPlayout(status playout.Status) {
s.Lasterror = status.Swap.LastError
}
func (p *Process) UnmarshalRestream(process *app.Process, state *app.State, report *app.Log, metadata map[string]interface{}) {
func (p *Process) UnmarshalRestream(process *app.Process, state *app.State, report *app.Report, metadata map[string]interface{}) {
p.ID = process.ID
p.Type = "ffmpeg"
p.Reference = process.Reference
@ -189,7 +189,7 @@ func (a *AVStreamIo) UnmarshalRestream(io app.AVstreamIO) {
a.SizeKb = scalars.Uint64(io.Size)
}
func (r *ProcessReport) UnmarshalRestream(report *app.Log) {
func (r *ProcessReport) UnmarshalRestream(report *app.Report) {
r.CreatedAt = report.CreatedAt
r.Prelude = report.Prelude
r.Log = []*ProcessReportLogEntry{}
@ -210,7 +210,7 @@ func (r *ProcessReport) UnmarshalRestream(report *app.Log) {
}
}
func (h *ProcessReportHistoryEntry) UnmarshalRestream(entry app.LogHistoryEntry) {
func (h *ProcessReportHistoryEntry) UnmarshalRestream(entry app.ReportHistoryEntry) {
h.CreatedAt = entry.CreatedAt
h.Prelude = entry.Prelude
h.Log = []*ProcessReportLogEntry{}

View File

@ -8,11 +8,12 @@ import (
"time"
"github.com/datarhei/core/v16/cluster"
"github.com/datarhei/core/v16/cluster/proxy"
"github.com/datarhei/core/v16/cluster/node"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/iam"
"github.com/datarhei/core/v16/restream/app"
"github.com/labstack/echo/v4"
)
@ -20,7 +21,7 @@ import (
// The ClusterHandler type provides handler functions for manipulating the cluster config.
type ClusterHandler struct {
cluster cluster.Cluster
proxy proxy.ProxyReader
proxy *node.Manager
iam iam.IAM
}
@ -28,7 +29,7 @@ type ClusterHandler struct {
func NewCluster(cluster cluster.Cluster, iam iam.IAM) (*ClusterHandler, error) {
h := &ClusterHandler{
cluster: cluster,
proxy: cluster.ProxyReader(),
proxy: cluster.Manager(),
iam: iam,
}
@ -67,7 +68,7 @@ func (h *ClusterHandler) About(c echo.Context) error {
Address: state.Leader.Address,
ElectedSince: uint64(state.Leader.ElectedSince.Seconds()),
},
Status: state.Status,
Status: state.State,
Raft: api.ClusterRaft{
Address: state.Raft.Address,
State: state.Raft.State,
@ -76,13 +77,13 @@ func (h *ClusterHandler) About(c echo.Context) error {
LogTerm: state.Raft.LogTerm,
LogIndex: state.Raft.LogIndex,
},
Nodes: []api.ClusterNode{},
Version: state.Version.String(),
Degraded: state.Degraded,
Nodes: []api.ClusterNode{},
Version: state.Version.String(),
}
if state.DegradedErr != nil {
about.DegradedErr = state.DegradedErr.Error()
if state.Error != nil {
about.Degraded = true
about.DegradedErr = state.Error.Error()
}
for _, node := range state.Nodes {
@ -97,7 +98,7 @@ func (h *ClusterHandler) marshalClusterNode(node cluster.ClusterNode) api.Cluste
ID: node.ID,
Name: node.Name,
Version: node.Version,
Status: node.Status,
Status: node.State,
Voter: node.Voter,
Leader: node.Leader,
Address: node.Address,
@ -107,7 +108,7 @@ func (h *ClusterHandler) marshalClusterNode(node cluster.ClusterNode) api.Cluste
Latency: node.Latency.Seconds() * 1000,
Core: api.ClusterNodeCore{
Address: node.Core.Address,
Status: node.Core.Status,
Status: node.Core.State,
LastContact: node.Core.LastContact.Seconds() * 1000,
Latency: node.Core.Latency.Seconds() * 1000,
Version: node.Core.Version,
@ -147,12 +148,12 @@ func (h *ClusterHandler) marshalClusterNode(node cluster.ClusterNode) api.Cluste
// @Security ApiKeyAuth
// @Router /api/v3/cluster/healthy [get]
func (h *ClusterHandler) Healthy(c echo.Context) error {
degraded, _ := h.cluster.IsDegraded()
hasLeader := h.cluster.HasRaftLeader()
return c.JSON(http.StatusOK, !degraded)
return c.JSON(http.StatusOK, hasLeader)
}
// Transfer the leadership to another node
// TransferLeadership transfers the leadership to another node
// @Summary Transfer the leadership to another node
// @Description Transfer the leadership to another node
// @Tags v16.?.?
@ -228,3 +229,47 @@ func (h *ClusterHandler) GetSnapshot(c echo.Context) error {
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.ProcessesRelocate("", relocations)
if err != nil {
return api.Err(http.StatusInternalServerError, "", "%s", err.Error())
}
return c.JSON(http.StatusOK, "OK")
}

View File

@ -9,7 +9,7 @@ import (
"github.com/labstack/echo/v4"
)
// ListFiles lists all files on a filesystem
// FilesystemListFiles lists all files on a filesystem
// @Summary List all files on a filesystem
// @Description List all files on a filesystem. The listing can be ordered by name, size, or date of last modification in ascending or descending order.
// @Tags v16.?.?
@ -23,13 +23,13 @@ import (
// @Success 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/fs/{storage} [get]
func (h *ClusterHandler) ListFiles(c echo.Context) error {
func (h *ClusterHandler) FilesystemListFiles(c echo.Context) error {
name := util.PathParam(c, "storage")
pattern := util.DefaultQuery(c, "glob", "")
sortby := util.DefaultQuery(c, "sort", "none")
order := util.DefaultQuery(c, "order", "asc")
files := h.proxy.ListFiles(name, pattern)
files := h.proxy.FilesystemList(name, pattern)
var sortFunc func(i, j int) bool

View File

@ -1,8 +1,10 @@
package api
import (
"errors"
"net/http"
"github.com/datarhei/core/v16/cluster/store"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/datarhei/core/v16/iam/access"
@ -23,7 +25,7 @@ import (
// @Failure 403 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/iam/user [post]
func (h *ClusterHandler) AddIdentity(c echo.Context) error {
func (h *ClusterHandler) IAMIdentityAdd(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
domain := util.DefaultQuery(c, "domain", "")
@ -50,18 +52,18 @@ func (h *ClusterHandler) AddIdentity(c echo.Context) error {
return api.Err(http.StatusForbidden, "", "Only superusers can add superusers")
}
if err := h.cluster.AddIdentity("", iamuser); err != nil {
if err := h.cluster.IAMIdentityAdd("", iamuser); err != nil {
return api.Err(http.StatusBadRequest, "", "invalid identity: %s", err.Error())
}
if err := h.cluster.SetPolicies("", iamuser.Name, iampolicies); err != nil {
if err := h.cluster.IAMPoliciesSet("", iamuser.Name, iampolicies); err != nil {
return api.Err(http.StatusBadRequest, "", "Invalid policies: %s", err.Error())
}
return c.JSON(http.StatusOK, user)
}
// UpdateIdentity replaces an existing user
// IAMIdentityUpdate replaces an existing user
// @Summary Replace an existing user
// @Description Replace an existing user.
// @Tags v16.?.?
@ -78,7 +80,7 @@ func (h *ClusterHandler) AddIdentity(c echo.Context) error {
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/iam/user/{name} [put]
func (h *ClusterHandler) UpdateIdentity(c echo.Context) error {
func (h *ClusterHandler) IAMIdentityUpdate(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
domain := util.DefaultQuery(c, "domain", "")
@ -128,13 +130,13 @@ func (h *ClusterHandler) UpdateIdentity(c echo.Context) error {
}
if name != "$anon" {
err = h.cluster.UpdateIdentity("", name, iamuser)
err = h.cluster.IAMIdentityUpdate("", name, iamuser)
if err != nil {
return api.Err(http.StatusBadRequest, "", "%s", err.Error())
}
}
err = h.cluster.SetPolicies("", iamuser.Name, iampolicies)
err = h.cluster.IAMPoliciesSet("", iamuser.Name, iampolicies)
if err != nil {
return api.Err(http.StatusInternalServerError, "", "set policies: %s", err.Error())
}
@ -142,7 +144,7 @@ func (h *ClusterHandler) UpdateIdentity(c echo.Context) error {
return c.JSON(http.StatusOK, user)
}
// UpdateIdentityPolicies replaces existing user policies
// IAMIdentityUpdatePolicies replaces existing user policies
// @Summary Replace policies of an user
// @Description Replace policies of an user
// @Tags v16.?.?
@ -159,7 +161,7 @@ func (h *ClusterHandler) UpdateIdentity(c echo.Context) error {
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/iam/user/{name}/policy [put]
func (h *ClusterHandler) UpdateIdentityPolicies(c echo.Context) error {
func (h *ClusterHandler) IAMIdentityUpdatePolicies(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
domain := util.DefaultQuery(c, "domain", "")
@ -216,15 +218,18 @@ func (h *ClusterHandler) UpdateIdentityPolicies(c echo.Context) error {
return api.Err(http.StatusForbidden, "", "only superusers can modify superusers")
}
err = h.cluster.SetPolicies("", name, accessPolicies)
err = h.cluster.IAMPoliciesSet("", name, accessPolicies)
if err != nil {
if errors.Is(err, store.ErrNotFound) {
return api.Err(http.StatusNotFound, "", "set policies: %s", err.Error())
}
return api.Err(http.StatusInternalServerError, "", "set policies: %s", err.Error())
}
return c.JSON(http.StatusOK, policies)
}
// ReloadIAM reloads the identities and policies from the cluster store to IAM
// IAMReload reloads the identities and policies from the cluster store to IAM
// @Summary Reload identities and policies
// @Description Reload identities and policies
// @Tags v16.?.?
@ -234,7 +239,7 @@ func (h *ClusterHandler) UpdateIdentityPolicies(c echo.Context) error {
// @Success 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/iam/reload [get]
func (h *ClusterHandler) ReloadIAM(c echo.Context) error {
func (h *ClusterHandler) IAMReload(c echo.Context) error {
err := h.iam.ReloadIndentities()
if err != nil {
return api.Err(http.StatusInternalServerError, "", "reload identities: %w", err.Error())
@ -248,7 +253,7 @@ func (h *ClusterHandler) ReloadIAM(c echo.Context) error {
return c.JSON(http.StatusOK, "OK")
}
// ListIdentities returns the list of identities stored in IAM
// IAMIdentityList returns the list of identities stored in IAM
// @Summary List of identities in IAM
// @Description List of identities in IAM
// @Tags v16.?.?
@ -257,7 +262,7 @@ func (h *ClusterHandler) ReloadIAM(c echo.Context) error {
// @Success 200 {array} api.IAMUser
// @Security ApiKeyAuth
// @Router /api/v3/cluster/iam/user [get]
func (h *ClusterHandler) ListIdentities(c echo.Context) error {
func (h *ClusterHandler) IAMIdentityList(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
@ -292,7 +297,7 @@ func (h *ClusterHandler) ListIdentities(c echo.Context) error {
return c.JSON(http.StatusOK, users)
}
// ListIdentity returns the identity stored in IAM
// IAMIdentityGet returns the identity stored in IAM
// @Summary Identity in IAM
// @Description Identity in IAM
// @Tags v16.?.?
@ -303,7 +308,7 @@ func (h *ClusterHandler) ListIdentities(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/iam/user/{name} [get]
func (h *ClusterHandler) ListIdentity(c echo.Context) error {
func (h *ClusterHandler) IAMIdentityGet(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
name := util.PathParam(c, "name")
@ -342,7 +347,7 @@ func (h *ClusterHandler) ListIdentity(c echo.Context) error {
return c.JSON(http.StatusOK, user)
}
// ListPolicies returns the list of policies stored in IAM
// IAMPolicyList returns the list of policies stored in IAM
// @Summary List of policies in IAM
// @Description List of policies IAM
// @Tags v16.?.?
@ -351,7 +356,7 @@ func (h *ClusterHandler) ListIdentity(c echo.Context) error {
// @Success 200 {array} api.IAMPolicy
// @Security ApiKeyAuth
// @Router /api/v3/cluster/iam/policies [get]
func (h *ClusterHandler) ListPolicies(c echo.Context) error {
func (h *ClusterHandler) IAMPolicyList(c echo.Context) error {
iampolicies := h.iam.ListPolicies("", "", nil, "", nil)
policies := []api.IAMPolicy{}
@ -381,7 +386,7 @@ func (h *ClusterHandler) ListPolicies(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/iam/user/{name} [delete]
func (h *ClusterHandler) RemoveIdentity(c echo.Context) error {
func (h *ClusterHandler) IAMIdentityRemove(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
domain := util.DefaultQuery(c, "domain", "$none")
@ -400,7 +405,7 @@ func (h *ClusterHandler) RemoveIdentity(c echo.Context) error {
return api.Err(http.StatusForbidden, "", "Only superusers can remove superusers")
}
if err := h.cluster.RemoveIdentity("", name); err != nil {
if err := h.cluster.IAMIdentityRemove("", name); err != nil {
return api.Err(http.StatusBadRequest, "", "invalid identity: %s", err.Error())
}

View File

@ -1,19 +1,20 @@
package api
import (
"errors"
"net/http"
"sort"
"strings"
"time"
clientapi "github.com/datarhei/core-client-go/v16/api"
"github.com/datarhei/core/v16/cluster/proxy"
"github.com/datarhei/core/v16/cluster"
"github.com/datarhei/core/v16/cluster/node"
"github.com/datarhei/core/v16/http/api"
"github.com/datarhei/core/v16/http/handler/util"
"github.com/labstack/echo/v4"
)
// GetNodes returns the list of proxy nodes in the cluster
// NodeList returns the list of proxy nodes in the cluster
// @Summary List of proxy nodes in the cluster
// @Description List of proxy nodes in the cluster
// @Tags v16.?.?
@ -23,19 +24,27 @@ import (
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/node [get]
func (h *ClusterHandler) GetNodes(c echo.Context) error {
func (h *ClusterHandler) NodeList(c echo.Context) error {
about, _ := h.cluster.About()
nodes := h.cluster.Store().NodeList()
list := []api.ClusterNode{}
for _, node := range about.Nodes {
if dbnode, hasNode := nodes[node.ID]; hasNode {
if dbnode.State == "maintenance" {
node.State = dbnode.State
}
}
list = append(list, h.marshalClusterNode(node))
}
return c.JSON(http.StatusOK, list)
}
// GetNode returns the proxy node with the given ID
// NodeGet returns the proxy node with the given ID
// @Summary List a proxy node by its ID
// @Description List a proxy node by its ID
// @Tags v16.?.?
@ -46,23 +55,31 @@ func (h *ClusterHandler) GetNodes(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/node/{id} [get]
func (h *ClusterHandler) GetNode(c echo.Context) error {
func (h *ClusterHandler) NodeGet(c echo.Context) error {
id := util.PathParam(c, "id")
about, _ := h.cluster.About()
nodes := h.cluster.Store().NodeList()
for _, node := range about.Nodes {
if node.ID != id {
continue
}
if dbnode, hasNode := nodes[node.ID]; hasNode {
if dbnode.State == "maintenance" {
node.State = dbnode.State
}
}
return c.JSON(http.StatusOK, h.marshalClusterNode(node))
}
return api.Err(http.StatusNotFound, "", "node not found")
}
// GetNodeVersion returns the proxy node version with the given ID
// NodeGetVersion returns the proxy node version with the given ID
// @Summary List a proxy node by its ID
// @Description List a proxy node by its ID
// @Tags v16.?.?
@ -73,29 +90,29 @@ func (h *ClusterHandler) GetNode(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/node/{id}/version [get]
func (h *ClusterHandler) GetNodeVersion(c echo.Context) error {
func (h *ClusterHandler) NodeGetVersion(c echo.Context) error {
id := util.PathParam(c, "id")
peer, err := h.proxy.GetNodeReader(id)
peer, err := h.proxy.NodeGet(id)
if err != nil {
return api.Err(http.StatusNotFound, "", "node not found: %s", err.Error())
}
v := peer.Version()
v := peer.CoreAbout()
version := api.Version{
Number: v.Number,
Commit: v.Commit,
Branch: v.Branch,
Build: v.Build.Format(time.RFC3339),
Arch: v.Arch,
Compiler: v.Compiler,
Number: v.Version.Number,
Commit: v.Version.Commit,
Branch: v.Version.Branch,
Build: v.Version.Build.Format(time.RFC3339),
Arch: v.Version.Arch,
Compiler: v.Version.Compiler,
}
return c.JSON(http.StatusOK, version)
}
// GetNodeResources returns the resources from the proxy node with the given ID
// NodeGetMedia returns the resources from the proxy node with the given ID
// @Summary List the resources of a proxy node by its ID
// @Description List the resources of a proxy node by its ID
// @Tags v16.?.?
@ -106,10 +123,10 @@ func (h *ClusterHandler) GetNodeVersion(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/node/{id}/files [get]
func (h *ClusterHandler) GetNodeResources(c echo.Context) error {
func (h *ClusterHandler) NodeGetMedia(c echo.Context) error {
id := util.PathParam(c, "id")
peer, err := h.proxy.GetNodeReader(id)
peer, err := h.proxy.NodeGet(id)
if err != nil {
return api.Err(http.StatusNotFound, "", "node not found: %s", err.Error())
}
@ -118,7 +135,7 @@ func (h *ClusterHandler) GetNodeResources(c echo.Context) error {
Files: make(map[string][]string),
}
peerFiles := peer.ListResources()
peerFiles := peer.Core().MediaList()
files.LastUpdate = peerFiles.LastUpdate.Unix()
@ -158,12 +175,12 @@ func (h *ClusterHandler) NodeFSListFiles(c echo.Context) error {
sortby := util.DefaultQuery(c, "sort", "none")
order := util.DefaultQuery(c, "order", "asc")
peer, err := h.proxy.GetNodeReader(id)
peer, err := h.proxy.NodeGet(id)
if err != nil {
return api.Err(http.StatusNotFound, "", "node not found: %s", err.Error())
}
files, err := peer.ListFiles(name, pattern)
files, err := peer.Core().FilesystemList(name, pattern)
if err != nil {
return api.Err(http.StatusInternalServerError, "", "retrieving file list: %s", err.Error())
}
@ -216,12 +233,12 @@ func (h *ClusterHandler) NodeFSGetFile(c echo.Context) error {
storage := util.PathParam(c, "storage")
path := util.PathWildcardParam(c)
peer, err := h.proxy.GetNodeReader(id)
peer, err := h.proxy.NodeGet(id)
if err != nil {
return api.Err(http.StatusNotFound, "", "node not found: %s", err.Error())
}
file, err := peer.GetFile(storage, path, 0)
file, err := peer.Core().FilesystemGetFile(storage, path, 0)
if err != nil {
return api.Err(http.StatusNotFound, "", "%s", err.Error())
}
@ -252,14 +269,14 @@ func (h *ClusterHandler) NodeFSPutFile(c echo.Context) error {
storage := util.PathParam(c, "storage")
path := util.PathWildcardParam(c)
peer, err := h.proxy.GetNodeReader(id)
peer, err := h.proxy.NodeGet(id)
if err != nil {
return api.Err(http.StatusNotFound, "", "node not found: %s", err.Error())
}
req := c.Request()
err = peer.PutFile(storage, path, req.Body)
err = peer.Core().FilesystemPutFile(storage, path, req.Body)
if err != nil {
return api.Err(http.StatusBadRequest, "", "%s", err.Error())
}
@ -285,12 +302,12 @@ func (h *ClusterHandler) NodeFSDeleteFile(c echo.Context) error {
storage := util.PathParam(c, "storage")
path := util.PathWildcardParam(c)
peer, err := h.proxy.GetNodeReader(id)
peer, err := h.proxy.NodeGet(id)
if err != nil {
return api.Err(http.StatusNotFound, "", "node not found: %s", err.Error())
}
err = peer.DeleteFile(storage, path)
err = peer.Core().FilesystemDeleteFile(storage, path)
if err != nil {
return api.Err(http.StatusNotFound, "", "%s", err.Error())
}
@ -298,7 +315,7 @@ func (h *ClusterHandler) NodeFSDeleteFile(c echo.Context) error {
return c.JSON(http.StatusOK, nil)
}
// ListNodeProcesses returns the list of processes running on a node of the cluster
// NodeListProcesses returns the list of processes running on a node of the cluster
// @Summary List of processes in the cluster on a node
// @Description List of processes in the cluster on a node
// @Tags v16.?.?
@ -318,7 +335,7 @@ func (h *ClusterHandler) NodeFSDeleteFile(c echo.Context) error {
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/node/{id}/process [get]
func (h *ClusterHandler) ListNodeProcesses(c echo.Context) error {
func (h *ClusterHandler) NodeListProcesses(c echo.Context) error {
id := util.PathParam(c, "id")
ctxuser := util.DefaultContext(c, "user", "")
filter := strings.FieldsFunc(util.DefaultQuery(c, "filter", ""), func(r rune) bool {
@ -334,12 +351,12 @@ func (h *ClusterHandler) ListNodeProcesses(c echo.Context) error {
ownerpattern := util.DefaultQuery(c, "ownerpattern", "")
domainpattern := util.DefaultQuery(c, "domainpattern", "")
peer, err := h.proxy.GetNodeReader(id)
peer, err := h.proxy.NodeGet(id)
if err != nil {
return api.Err(http.StatusNotFound, "", "node not found: %s", err.Error())
}
procs, err := peer.ProcessList(proxy.ProcessListOptions{
procs, err := peer.Core().ProcessList(node.ProcessListOptions{
ID: wantids,
Filter: filter,
Domain: domain,
@ -353,7 +370,7 @@ func (h *ClusterHandler) ListNodeProcesses(c echo.Context) error {
return api.Err(http.StatusInternalServerError, "", "node not available: %s", err.Error())
}
processes := []clientapi.Process{}
processes := []api.Process{}
for _, p := range procs {
if !h.iam.Enforce(ctxuser, domain, "process", p.Config.ID, "read") {
@ -365,3 +382,108 @@ func (h *ClusterHandler) ListNodeProcesses(c echo.Context) error {
return c.JSON(http.StatusOK, processes)
}
// NodeGetState returns the state of a node with the given ID
// @Summary Get the state of a node with the given ID
// @Description Get the state of a node with the given ID
// @Tags v16.?.?
// @ID cluster-3-get-node-state
// @Produce json
// @Param id path string true "Node ID"
// @Success 200 {object} api.ClusterNodeState
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/node/{id}/state [get]
func (h *ClusterHandler) NodeGetState(c echo.Context) error {
id := util.PathParam(c, "id")
about, _ := h.cluster.About()
state := ""
for _, node := range about.Nodes {
if node.ID != id {
continue
}
state = node.State
break
}
if len(state) == 0 {
return api.Err(http.StatusNotFound, "", "node not found")
}
nodes := h.cluster.Store().NodeList()
if node, hasNode := nodes[id]; hasNode {
if node.State == "maintenance" {
state = node.State
}
}
return c.JSON(http.StatusOK, api.ClusterNodeState{
State: state,
})
}
// NodeSetState sets the state of a node with the given ID
// @Summary Set the state of a node with the given ID
// @Description Set the state of a node with the given ID
// @Tags v16.?.?
// @ID cluster-3-set-node-state
// @Produce json
// @Param id path string true "Node ID"
// @Param config body api.ClusterNodeState true "State"
// @Success 200 {string} string
// @Failure 400 {object} api.Error
// @Failure 404 {object} api.Error
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/node/{id}/state [put]
func (h *ClusterHandler) NodeSetState(c echo.Context) error {
id := util.PathParam(c, "id")
about, _ := h.cluster.About()
found := false
for _, node := range about.Nodes {
if node.ID != id {
continue
}
found = true
break
}
if !found {
return api.Err(http.StatusNotFound, "", "node not found")
}
state := api.ClusterNodeState{}
if err := util.ShouldBindJSON(c, &state); err != nil {
return api.Err(http.StatusBadRequest, "", "invalid JSON: %s", err.Error())
}
if state.State == "leave" {
err := h.cluster.Leave("", id)
if err != nil {
if errors.Is(err, cluster.ErrUnknownNode) {
return api.Err(http.StatusNotFound, "", "node not found")
}
return api.Err(http.StatusInternalServerError, "", "%s", err.Error())
}
return c.JSON(http.StatusOK, "OK")
}
err := h.cluster.NodeSetState("", id, state.State)
if err != nil {
if errors.Is(err, cluster.ErrUnsupportedNodeState) {
return api.Err(http.StatusBadRequest, "", "%s", err.Error())
}
return api.Err(http.StatusInternalServerError, "", "%s", err.Error())
}
return c.JSON(http.StatusOK, "OK")
}

View File

@ -7,8 +7,7 @@ import (
"strconv"
"strings"
clientapi "github.com/datarhei/core-client-go/v16/api"
"github.com/datarhei/core/v16/cluster/proxy"
"github.com/datarhei/core/v16/cluster/node"
"github.com/datarhei/core/v16/cluster/store"
"github.com/datarhei/core/v16/encoding/json"
"github.com/datarhei/core/v16/glob"
@ -20,7 +19,7 @@ import (
"github.com/lithammer/shortuuid/v4"
)
// GetAllProcesses returns the list of processes running on all nodes of the cluster
// ProcessList returns the list of processes running on all nodes of the cluster
// @Summary List of processes in the cluster
// @Description List of processes in the cluster
// @Tags v16.?.?
@ -37,7 +36,7 @@ import (
// @Success 200 {array} api.Process
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process [get]
func (h *ClusterHandler) GetAllProcesses(c echo.Context) error {
func (h *ClusterHandler) ProcessList(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
filter := newFilter(util.DefaultQuery(c, "filter", ""))
reference := util.DefaultQuery(c, "reference", "")
@ -50,7 +49,7 @@ func (h *ClusterHandler) GetAllProcesses(c echo.Context) error {
ownerpattern := util.DefaultQuery(c, "ownerpattern", "")
domainpattern := util.DefaultQuery(c, "domainpattern", "")
procs := h.proxy.ListProcesses(proxy.ProcessListOptions{
procs := h.proxy.ProcessList(node.ProcessListOptions{
ID: wantids,
Filter: filter.Slice(),
Domain: domain,
@ -61,7 +60,7 @@ func (h *ClusterHandler) GetAllProcesses(c echo.Context) error {
DomainPattern: domainpattern,
})
processes := []clientapi.Process{}
processes := []api.Process{}
pmap := map[app.ProcessID]struct{}{}
for _, p := range procs {
@ -77,7 +76,7 @@ func (h *ClusterHandler) GetAllProcesses(c echo.Context) error {
// Here we have to add those processes that are in the cluster DB and couldn't be deployed
{
processes := h.cluster.ListProcesses()
processes := h.cluster.Store().ProcessList()
filtered := h.getFilteredStoreProcesses(processes, wantids, domain, reference, idpattern, refpattern, ownerpattern, domainpattern)
for _, p := range filtered {
@ -139,7 +138,7 @@ func (h *ClusterHandler) GetAllProcesses(c echo.Context) error {
return c.Stream(http.StatusOK, "application/json", buf)
}
func (h *ClusterHandler) getFilteredStoreProcesses(processes []store.Process, wantids []string, domain, reference, idpattern, refpattern, ownerpattern, domainpattern string) []store.Process {
func (h *ClusterHandler) getFilteredStoreProcesses(processes []store.Process, wantids []string, _, reference, idpattern, refpattern, ownerpattern, domainpattern string) []store.Process {
filtered := []store.Process{}
count := 0
@ -293,7 +292,7 @@ func (h *ClusterHandler) convertStoreProcessToAPIProcess(p store.Process, filter
return process
}
// GetProcess returns the process with the given ID whereever it's running on the cluster
// ProcessGet returns the process with the given ID whereever it's running on the cluster
// @Summary List a process by its ID
// @Description List a process by its ID. Use the filter parameter to specifiy the level of detail of the output.
// @Tags v16.?.?
@ -307,7 +306,7 @@ func (h *ClusterHandler) convertStoreProcessToAPIProcess(p store.Process, filter
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/{id} [get]
func (h *ClusterHandler) GetProcess(c echo.Context) error {
func (h *ClusterHandler) ProcessGet(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
id := util.PathParam(c, "id")
filter := newFilter(util.DefaultQuery(c, "filter", ""))
@ -317,7 +316,7 @@ func (h *ClusterHandler) GetProcess(c echo.Context) error {
return api.Err(http.StatusForbidden, "")
}
procs := h.proxy.ListProcesses(proxy.ProcessListOptions{
procs := h.proxy.ProcessList(node.ProcessListOptions{
ID: []string{id},
Filter: filter.Slice(),
Domain: domain,
@ -325,7 +324,7 @@ func (h *ClusterHandler) GetProcess(c echo.Context) error {
if len(procs) == 0 {
// Check the store in the cluster for an undeployed process
p, err := h.cluster.GetProcess(app.NewProcessID(id, domain))
p, err := h.cluster.Store().ProcessGet(app.NewProcessID(id, domain))
if err != nil {
return api.Err(http.StatusNotFound, "", "Unknown process ID: %s", id)
}
@ -355,7 +354,7 @@ func (h *ClusterHandler) GetProcess(c echo.Context) error {
// @Failure 403 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process [post]
func (h *ClusterHandler) AddProcess(c echo.Context) error {
func (h *ClusterHandler) ProcessAdd(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
@ -390,12 +389,12 @@ func (h *ClusterHandler) AddProcess(c echo.Context) error {
config, metadata := process.Marshal()
if err := h.cluster.AddProcess("", config); err != nil {
if err := h.cluster.ProcessAdd("", config); err != nil {
return api.Err(http.StatusBadRequest, "", "adding process config: %s", err.Error())
}
for key, value := range metadata {
h.cluster.SetProcessMetadata("", config.ProcessID(), key, value)
h.cluster.ProcessSetMetadata("", config.ProcessID(), key, value)
}
return c.JSON(http.StatusOK, process)
@ -417,7 +416,7 @@ func (h *ClusterHandler) AddProcess(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/{id} [put]
func (h *ClusterHandler) UpdateProcess(c echo.Context) error {
func (h *ClusterHandler) ProcessUpdate(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
superuser := util.DefaultContext(c, "superuser", false)
domain := util.DefaultQuery(c, "domain", "")
@ -437,7 +436,7 @@ func (h *ClusterHandler) UpdateProcess(c echo.Context) error {
pid := process.ProcessID()
current, err := h.cluster.GetProcess(pid)
current, err := h.cluster.Store().ProcessGet(pid)
if err != nil {
return api.Err(http.StatusNotFound, "", "process not found: %s in domain '%s'", pid.ID, pid.Domain)
}
@ -461,7 +460,7 @@ func (h *ClusterHandler) UpdateProcess(c echo.Context) error {
config, metadata := process.Marshal()
if err := h.cluster.UpdateProcess("", pid, config); err != nil {
if err := h.cluster.ProcessUpdate("", pid, config); err != nil {
if err == restream.ErrUnknownProcess {
return api.Err(http.StatusNotFound, "", "process not found: %s in domain '%s'", pid.ID, pid.Domain)
}
@ -472,7 +471,7 @@ func (h *ClusterHandler) UpdateProcess(c echo.Context) error {
pid = process.ProcessID()
for key, value := range metadata {
h.cluster.SetProcessMetadata("", pid, key, value)
h.cluster.ProcessSetMetadata("", pid, key, value)
}
return c.JSON(http.StatusOK, process)
@ -494,7 +493,7 @@ func (h *ClusterHandler) UpdateProcess(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/{id}/command [put]
func (h *ClusterHandler) SetProcessCommand(c echo.Context) error {
func (h *ClusterHandler) ProcessSetCommand(c echo.Context) error {
id := util.PathParam(c, "id")
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
@ -523,14 +522,14 @@ func (h *ClusterHandler) SetProcessCommand(c echo.Context) error {
return api.Err(http.StatusBadRequest, "", "unknown command provided. known commands are: start, stop, reload, restart")
}
if err := h.cluster.SetProcessCommand("", pid, command.Command); err != nil {
if err := h.cluster.ProcessSetCommand("", pid, command.Command); err != nil {
return api.Err(http.StatusNotFound, "", "command failed: %s", err.Error())
}
return c.JSON(http.StatusOK, "OK")
}
// SetProcessMetadata stores metadata with a process
// ProcessSetMetadata stores metadata with a process
// @Summary Add JSON metadata with a process under the given key
// @Description Add arbitrary JSON metadata under the given key. If the key exists, all already stored metadata with this key will be overwritten. If the key doesn't exist, it will be created.
// @Tags v16.?.?
@ -546,7 +545,7 @@ func (h *ClusterHandler) SetProcessCommand(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/{id}/metadata/{key} [put]
func (h *ClusterHandler) SetProcessMetadata(c echo.Context) error {
func (h *ClusterHandler) ProcessSetMetadata(c echo.Context) error {
id := util.PathParam(c, "id")
key := util.PathParam(c, "key")
ctxuser := util.DefaultContext(c, "user", "")
@ -571,14 +570,14 @@ func (h *ClusterHandler) SetProcessMetadata(c echo.Context) error {
Domain: domain,
}
if err := h.cluster.SetProcessMetadata("", pid, key, data); err != nil {
if err := h.cluster.ProcessSetMetadata("", pid, key, data); err != nil {
return api.Err(http.StatusNotFound, "", "setting metadata failed: %s", err.Error())
}
return c.JSON(http.StatusOK, data)
}
// GetProcessMetadata returns the metadata stored with a process
// ProcessGetMetadata returns the metadata stored with a process
// @Summary Retrieve JSON metadata stored with a process under a key
// @Description Retrieve the previously stored JSON metadata under the given key. If the key is empty, all metadata will be returned.
// @Tags v16.?.?
@ -593,7 +592,7 @@ func (h *ClusterHandler) SetProcessMetadata(c echo.Context) error {
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/{id}/metadata/{key} [get]
func (h *ClusterHandler) GetProcessMetadata(c echo.Context) error {
func (h *ClusterHandler) ProcessGetMetadata(c echo.Context) error {
id := util.PathParam(c, "id")
key := util.PathParam(c, "key")
ctxuser := util.DefaultContext(c, "user", "")
@ -608,7 +607,7 @@ func (h *ClusterHandler) GetProcessMetadata(c echo.Context) error {
Domain: domain,
}
data, err := h.cluster.GetProcessMetadata("", pid, key)
data, err := h.cluster.ProcessGetMetadata("", pid, key)
if err != nil {
return api.Err(http.StatusNotFound, "", "unknown process ID: %s", err.Error())
}
@ -628,7 +627,7 @@ func (h *ClusterHandler) GetProcessMetadata(c echo.Context) error {
// @Failure 403 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/{id}/probe [get]
func (h *ClusterHandler) ProbeProcess(c echo.Context) error {
func (h *ClusterHandler) ProcessProbe(c echo.Context) error {
id := util.PathParam(c, "id")
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
@ -642,14 +641,14 @@ func (h *ClusterHandler) ProbeProcess(c echo.Context) error {
Domain: domain,
}
nodeid, err := h.proxy.FindNodeFromProcess(pid)
nodeid, err := h.proxy.ProcessFindNodeID(pid)
if err != nil {
return c.JSON(http.StatusOK, api.Probe{
Log: []string{fmt.Sprintf("the process can't be found: %s", err.Error())},
})
}
probe, _ := h.proxy.ProbeProcess(nodeid, pid)
probe, _ := h.proxy.ProcessProbe(nodeid, pid)
return c.JSON(http.StatusOK, probe)
}
@ -669,7 +668,7 @@ func (h *ClusterHandler) ProbeProcess(c echo.Context) error {
// @Failure 500 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/probe [post]
func (h *ClusterHandler) ProbeProcessConfig(c echo.Context) error {
func (h *ClusterHandler) ProcessProbeConfig(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
coreid := util.DefaultQuery(c, "coreid", "")
@ -702,12 +701,12 @@ func (h *ClusterHandler) ProbeProcessConfig(c echo.Context) error {
config, _ := process.Marshal()
coreid = h.proxy.FindNodeFromResources(coreid, config.LimitCPU, config.LimitMemory)
coreid = h.proxy.FindNodeForResources(coreid, config.LimitCPU, config.LimitMemory)
if len(coreid) == 0 {
return api.Err(http.StatusInternalServerError, "", "Not enough available resources available to execute probe")
return api.Err(http.StatusInternalServerError, "", "Not enough resources available to execute probe")
}
probe, _ := h.proxy.ProbeProcessConfig(coreid, config)
probe, _ := h.proxy.ProcessProbeConfig(coreid, config)
return c.JSON(http.StatusOK, probe)
}
@ -724,7 +723,7 @@ func (h *ClusterHandler) ProbeProcessConfig(c echo.Context) error {
// @Failure 403 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/process/{id} [delete]
func (h *ClusterHandler) DeleteProcess(c echo.Context) error {
func (h *ClusterHandler) ProcessDelete(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
id := util.PathParam(c, "id")
@ -738,7 +737,7 @@ func (h *ClusterHandler) DeleteProcess(c echo.Context) error {
Domain: domain,
}
if err := h.cluster.RemoveProcess("", pid); err != nil {
if err := h.cluster.ProcessRemove("", pid); err != nil {
return api.Err(http.StatusBadRequest, "", "%s", err.Error())
}

View File

@ -12,7 +12,7 @@ import (
"github.com/labstack/echo/v4"
)
// ListStoreProcesses returns the list of processes stored in the DB of the cluster
// StoreListProcesses returns the list of processes stored in the DB of the cluster
// @Summary List of processes in the cluster DB
// @Description List of processes in the cluster DB
// @Tags v16.?.?
@ -21,10 +21,10 @@ import (
// @Success 200 {array} api.Process
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/process [get]
func (h *ClusterHandler) ListStoreProcesses(c echo.Context) error {
func (h *ClusterHandler) StoreListProcesses(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
procs := h.cluster.ListProcesses()
procs := h.cluster.Store().ProcessList()
processes := []api.Process{}
@ -52,7 +52,7 @@ func (h *ClusterHandler) ListStoreProcesses(c echo.Context) error {
// @Success 200 {object} api.Process
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/process/:id [get]
func (h *ClusterHandler) GetStoreProcess(c echo.Context) error {
func (h *ClusterHandler) StoreGetProcess(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
id := util.PathParam(c, "id")
@ -66,7 +66,7 @@ func (h *ClusterHandler) GetStoreProcess(c echo.Context) error {
return api.Err(http.StatusForbidden, "", "API user %s is not allowed to read this process", ctxuser)
}
p, err := h.cluster.GetProcess(pid)
p, err := h.cluster.Store().ProcessGet(pid)
if err != nil {
return api.Err(http.StatusNotFound, "", "process not found: %s in domain '%s'", pid.ID, pid.Domain)
}
@ -76,7 +76,7 @@ func (h *ClusterHandler) GetStoreProcess(c echo.Context) error {
return c.JSON(http.StatusOK, process)
}
// GetStoreProcessNodeMap returns a map of which process is running on which node
// StoreGetProcessNodeMap returns a map of which process is running on which node
// @Summary Retrieve a map of which process is running on which node
// @Description Retrieve a map of which process is running on which node
// @Tags v16.?.?
@ -85,13 +85,13 @@ func (h *ClusterHandler) GetStoreProcess(c echo.Context) error {
// @Success 200 {object} api.ClusterProcessMap
// @Security ApiKeyAuth
// @Router /api/v3/cluster/map/process [get]
func (h *ClusterHandler) GetStoreProcessNodeMap(c echo.Context) error {
m := h.cluster.GetProcessNodeMap()
func (h *ClusterHandler) StoreGetProcessNodeMap(c echo.Context) error {
m := h.cluster.Store().ProcessGetNodeMap()
return c.JSON(http.StatusOK, m)
}
// ListStoreIdentities returns the list of identities stored in the DB of the cluster
// StoreListIdentities returns the list of identities stored in the DB of the cluster
// @Summary List of identities in the cluster
// @Description List of identities in the cluster
// @Tags v16.?.?
@ -100,15 +100,15 @@ func (h *ClusterHandler) GetStoreProcessNodeMap(c echo.Context) error {
// @Success 200 {array} api.IAMUser
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/user [get]
func (h *ClusterHandler) ListStoreIdentities(c echo.Context) error {
func (h *ClusterHandler) StoreListIdentities(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
updatedAt, identities := h.cluster.ListIdentities()
identities := h.cluster.Store().IAMIdentityList()
users := make([]api.IAMUser, len(identities))
users := make([]api.IAMUser, len(identities.Users))
for i, iamuser := range identities {
for i, iamuser := range identities.Users {
if !h.iam.Enforce(ctxuser, domain, "iam", iamuser.Name, "read") {
continue
}
@ -119,27 +119,27 @@ func (h *ClusterHandler) ListStoreIdentities(c echo.Context) error {
}
}
_, policies := h.cluster.ListUserPolicies(iamuser.Name)
users[i].Marshal(iamuser, policies)
policies := h.cluster.Store().IAMIdentityPolicyList(iamuser.Name)
users[i].Marshal(iamuser, policies.Policies)
}
c.Response().Header().Set("Last-Modified", updatedAt.UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
c.Response().Header().Set("Last-Modified", identities.UpdatedAt.UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
return c.JSON(http.StatusOK, users)
}
// ListStoreIdentity returns the list of identities stored in the DB of the cluster
// StoreGetIdentity returns the list of identities stored in the DB of the cluster
// @Summary List of identities in the cluster
// @Description List of identities in the cluster
// @Tags v16.?.?
// @ID cluster-3-db-list-identity
// @ID cluster-3-db-get-identity
// @Produce json
// @Success 200 {object} api.IAMUser
// @Failure 403 {object} api.Error
// @Failure 404 {object} api.Error
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/user/{name} [get]
func (h *ClusterHandler) ListStoreIdentity(c echo.Context) error {
func (h *ClusterHandler) StoreGetIdentity(c echo.Context) error {
ctxuser := util.DefaultContext(c, "user", "")
domain := util.DefaultQuery(c, "domain", "")
name := util.PathParam(c, "name")
@ -150,14 +150,15 @@ func (h *ClusterHandler) ListStoreIdentity(c echo.Context) error {
var updatedAt time.Time
var iamuser identity.User
var err error
if name != "$anon" {
updatedAt, iamuser, err = h.cluster.ListIdentity(name)
if err != nil {
return api.Err(http.StatusNotFound, "", "%s", err.Error())
user := h.cluster.Store().IAMIdentityGet(name)
if len(user.Users) == 0 {
return api.Err(http.StatusNotFound, "")
}
updatedAt, iamuser = user.UpdatedAt, user.Users[0]
if ctxuser != iamuser.Name {
if !h.iam.Enforce(ctxuser, domain, "iam", name, "write") {
iamuser = identity.User{
@ -171,20 +172,20 @@ func (h *ClusterHandler) ListStoreIdentity(c echo.Context) error {
}
}
policiesUpdatedAt, policies := h.cluster.ListUserPolicies(name)
policies := h.cluster.Store().IAMIdentityPolicyList(name)
if updatedAt.IsZero() {
updatedAt = policiesUpdatedAt
updatedAt = policies.UpdatedAt
}
user := api.IAMUser{}
user.Marshal(iamuser, policies)
user.Marshal(iamuser, policies.Policies)
c.Response().Header().Set("Last-Modified", updatedAt.UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
return c.JSON(http.StatusOK, user)
}
// ListStorePolicies returns the list of policies stored in the DB of the cluster
// StoreListPolicies returns the list of policies stored in the DB of the cluster
// @Summary List of policies in the cluster
// @Description List of policies in the cluster
// @Tags v16.?.?
@ -193,26 +194,27 @@ func (h *ClusterHandler) ListStoreIdentity(c echo.Context) error {
// @Success 200 {array} api.IAMPolicy
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/policies [get]
func (h *ClusterHandler) ListStorePolicies(c echo.Context) error {
updatedAt, clusterpolicies := h.cluster.ListPolicies()
func (h *ClusterHandler) StoreListPolicies(c echo.Context) error {
clusterpolicies := h.cluster.Store().IAMPolicyList()
policies := []api.IAMPolicy{}
for _, pol := range clusterpolicies {
for _, pol := range clusterpolicies.Policies {
policies = append(policies, api.IAMPolicy{
Name: pol.Name,
Domain: pol.Domain,
Resource: pol.Resource,
Types: pol.Types,
Actions: pol.Actions,
})
}
c.Response().Header().Set("Last-Modified", updatedAt.UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
c.Response().Header().Set("Last-Modified", clusterpolicies.UpdatedAt.UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
return c.JSON(http.StatusOK, policies)
}
// ListStoreLocks returns the list of currently stored locks
// StoreListLocks returns the list of currently stored locks
// @Summary List locks in the cluster DB
// @Description List of locks in the cluster DB
// @Tags v16.?.?
@ -221,8 +223,8 @@ func (h *ClusterHandler) ListStorePolicies(c echo.Context) error {
// @Success 200 {array} api.ClusterLock
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/locks [get]
func (h *ClusterHandler) ListStoreLocks(c echo.Context) error {
clusterlocks := h.cluster.ListLocks()
func (h *ClusterHandler) StoreListLocks(c echo.Context) error {
clusterlocks := h.cluster.Store().LockList()
locks := []api.ClusterLock{}
@ -236,7 +238,7 @@ func (h *ClusterHandler) ListStoreLocks(c echo.Context) error {
return c.JSON(http.StatusOK, locks)
}
// ListStoreKV returns the list of currently stored key/value pairs
// StoreListKV returns the list of currently stored key/value pairs
// @Summary List KV in the cluster DB
// @Description List of KV in the cluster DB
// @Tags v16.?.?
@ -245,8 +247,8 @@ func (h *ClusterHandler) ListStoreLocks(c echo.Context) error {
// @Success 200 {object} api.ClusterKVS
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/kv [get]
func (h *ClusterHandler) ListStoreKV(c echo.Context) error {
clusterkv := h.cluster.ListKV("")
func (h *ClusterHandler) StoreListKV(c echo.Context) error {
clusterkv := h.cluster.Store().KVSList("")
kvs := api.ClusterKVS{}
@ -259,3 +261,28 @@ func (h *ClusterHandler) ListStoreKV(c echo.Context) error {
return c.JSON(http.StatusOK, kvs)
}
// StoreListNodes returns the list of stored node metadata
// @Summary List nodes in the cluster DB
// @Description List of nodes in the cluster DB
// @Tags v16.?.?
// @ID cluster-3-db-list-nodes
// @Produce json
// @Success 200 {array} api.ClusterStoreNode
// @Security ApiKeyAuth
// @Router /api/v3/cluster/db/node [get]
func (h *ClusterHandler) StoreListNodes(c echo.Context) error {
clusternodes := h.cluster.Store().NodeList()
nodes := []api.ClusterStoreNode{}
for nodeid, v := range clusternodes {
nodes = append(nodes, api.ClusterStoreNode{
ID: nodeid,
State: v.State,
UpdatedAt: v.UpdatedAt,
})
}
return c.JSON(http.StatusOK, nodes)
}

View File

@ -84,6 +84,7 @@ func (h *IAMHandler) AddIdentity(c echo.Context) error {
// @Param name path string true "Username"
// @Param domain query string false "Domain of the acting user"
// @Success 200 {string} string
// @Failure 400 {object} api.Error
// @Failure 403 {object} api.Error
// @Failure 404 {object} api.Error
// @Failure 500 {object} api.Error

View File

@ -206,7 +206,7 @@ func NewServer(config Config) (serverhandler.Server, error) {
if config.Cluster != nil {
if httpfs.Filesystem.Type() == "disk" || httpfs.Filesystem.Type() == "mem" {
httpfs.Filesystem = fs.NewClusterFS(httpfs.Filesystem.Name(), httpfs.Filesystem, config.Cluster.ProxyReader())
httpfs.Filesystem = fs.NewClusterFS(httpfs.Filesystem.Name(), httpfs.Filesystem, config.Cluster.Manager())
}
}
@ -728,54 +728,59 @@ func (s *server) setRoutesV3(v3 *echo.Group) {
v3.GET("/cluster/snapshot", s.v3handler.cluster.GetSnapshot)
v3.GET("/cluster/db/process", s.v3handler.cluster.ListStoreProcesses)
v3.GET("/cluster/db/process/:id", s.v3handler.cluster.GetStoreProcess)
v3.GET("/cluster/db/user", s.v3handler.cluster.ListStoreIdentities)
v3.GET("/cluster/db/user/:name", s.v3handler.cluster.ListStoreIdentity)
v3.GET("/cluster/db/policies", s.v3handler.cluster.ListStorePolicies)
v3.GET("/cluster/db/locks", s.v3handler.cluster.ListStoreLocks)
v3.GET("/cluster/db/kv", s.v3handler.cluster.ListStoreKV)
v3.GET("/cluster/db/map/process", s.v3handler.cluster.GetStoreProcessNodeMap)
v3.GET("/cluster/db/process", s.v3handler.cluster.StoreListProcesses)
v3.GET("/cluster/db/process/:id", s.v3handler.cluster.StoreGetProcess)
v3.GET("/cluster/db/user", s.v3handler.cluster.StoreListIdentities)
v3.GET("/cluster/db/user/:name", s.v3handler.cluster.StoreGetIdentity)
v3.GET("/cluster/db/policies", s.v3handler.cluster.StoreListPolicies)
v3.GET("/cluster/db/locks", s.v3handler.cluster.StoreListLocks)
v3.GET("/cluster/db/kv", s.v3handler.cluster.StoreListKV)
v3.GET("/cluster/db/map/process", s.v3handler.cluster.StoreGetProcessNodeMap)
v3.GET("/cluster/db/node", s.v3handler.cluster.StoreListNodes)
v3.GET("/cluster/iam/user", s.v3handler.cluster.ListIdentities)
v3.GET("/cluster/iam/user/:name", s.v3handler.cluster.ListIdentity)
v3.GET("/cluster/iam/policies", s.v3handler.cluster.ListPolicies)
v3.GET("/cluster/iam/user", s.v3handler.cluster.IAMIdentityList)
v3.GET("/cluster/iam/user/:name", s.v3handler.cluster.IAMIdentityGet)
v3.GET("/cluster/iam/policies", s.v3handler.cluster.IAMPolicyList)
v3.GET("/cluster/process", s.v3handler.cluster.GetAllProcesses)
v3.GET("/cluster/process/:id", s.v3handler.cluster.GetProcess)
v3.GET("/cluster/process/:id/metadata", s.v3handler.cluster.GetProcessMetadata)
v3.GET("/cluster/process/:id/metadata/:key", s.v3handler.cluster.GetProcessMetadata)
v3.GET("/cluster/process", s.v3handler.cluster.ProcessList)
v3.GET("/cluster/process/:id", s.v3handler.cluster.ProcessGet)
v3.GET("/cluster/process/:id/metadata", s.v3handler.cluster.ProcessGetMetadata)
v3.GET("/cluster/process/:id/metadata/:key", s.v3handler.cluster.ProcessGetMetadata)
v3.GET("/cluster/node", s.v3handler.cluster.GetNodes)
v3.GET("/cluster/node/:id", s.v3handler.cluster.GetNode)
v3.GET("/cluster/node/:id/files", s.v3handler.cluster.GetNodeResources)
v3.GET("/cluster/node", s.v3handler.cluster.NodeList)
v3.GET("/cluster/node/:id", s.v3handler.cluster.NodeGet)
v3.GET("/cluster/node/:id/files", s.v3handler.cluster.NodeGetMedia)
v3.GET("/cluster/node/:id/fs/:storage", s.v3handler.cluster.NodeFSListFiles)
v3.GET("/cluster/node/:id/fs/:storage/*", s.v3handler.cluster.NodeFSGetFile)
v3.GET("/cluster/node/:id/process", s.v3handler.cluster.ListNodeProcesses)
v3.GET("/cluster/node/:id/version", s.v3handler.cluster.GetNodeVersion)
v3.GET("/cluster/node/:id/process", s.v3handler.cluster.NodeListProcesses)
v3.GET("/cluster/node/:id/version", s.v3handler.cluster.NodeGetVersion)
v3.GET("/cluster/node/:id/state", s.v3handler.cluster.NodeGetState)
v3.GET("/cluster/fs/:storage", s.v3handler.cluster.ListFiles)
v3.GET("/cluster/fs/:storage", s.v3handler.cluster.FilesystemListFiles)
if !s.readOnly {
v3.PUT("/cluster/transfer/:id", s.v3handler.cluster.TransferLeadership)
v3.PUT("/cluster/leave", s.v3handler.cluster.Leave)
v3.POST("/cluster/process", s.v3handler.cluster.AddProcess)
v3.POST("/cluster/process/probe", s.v3handler.cluster.ProbeProcessConfig)
v3.PUT("/cluster/process/:id", s.v3handler.cluster.UpdateProcess)
v3.GET("/cluster/process/:id/probe", s.v3handler.cluster.ProbeProcess)
v3.DELETE("/cluster/process/:id", s.v3handler.cluster.DeleteProcess)
v3.PUT("/cluster/process/:id/command", s.v3handler.cluster.SetProcessCommand)
v3.PUT("/cluster/process/:id/metadata/:key", s.v3handler.cluster.SetProcessMetadata)
v3.POST("/cluster/process", s.v3handler.cluster.ProcessAdd)
v3.POST("/cluster/process/probe", s.v3handler.cluster.ProcessProbeConfig)
v3.PUT("/cluster/process/:id", s.v3handler.cluster.ProcessUpdate)
v3.GET("/cluster/process/:id/probe", s.v3handler.cluster.ProcessProbe)
v3.DELETE("/cluster/process/:id", s.v3handler.cluster.ProcessDelete)
v3.PUT("/cluster/process/:id/command", s.v3handler.cluster.ProcessSetCommand)
v3.PUT("/cluster/process/:id/metadata/:key", s.v3handler.cluster.ProcessSetMetadata)
v3.PUT("/cluster/reallocation", s.v3handler.cluster.Reallocation)
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/state", s.v3handler.cluster.NodeSetState)
v3.PUT("/cluster/iam/reload", s.v3handler.cluster.ReloadIAM)
v3.POST("/cluster/iam/user", s.v3handler.cluster.AddIdentity)
v3.PUT("/cluster/iam/user/:name", s.v3handler.cluster.UpdateIdentity)
v3.PUT("/cluster/iam/user/:name/policy", s.v3handler.cluster.UpdateIdentityPolicies)
v3.DELETE("/cluster/iam/user/:name", s.v3handler.cluster.RemoveIdentity)
v3.PUT("/cluster/iam/reload", s.v3handler.cluster.IAMReload)
v3.POST("/cluster/iam/user", s.v3handler.cluster.IAMIdentityAdd)
v3.PUT("/cluster/iam/user/:name", s.v3handler.cluster.IAMIdentityUpdate)
v3.PUT("/cluster/iam/user/:name/policy", s.v3handler.cluster.IAMIdentityUpdatePolicies)
v3.DELETE("/cluster/iam/user/:name", s.v3handler.cluster.IAMIdentityRemove)
}
}

View File

@ -139,7 +139,7 @@ Output #0, hls, to './data/testsrc.m3u8':
os.Exit(2)
}
if slices.EqualComparableElements(os.Args[1:], []string{"-f", "avfoundation", "-list_devices", "true", "-i", ""}) {
if err := slices.EqualComparableElements(os.Args[1:], []string{"-f", "avfoundation", "-list_devices", "true", "-i", ""}); err == nil {
fmt.Fprintf(os.Stderr, "%s\n", avfoundation)
os.Exit(0)
}

View File

@ -10,6 +10,31 @@ import (
"github.com/datarhei/core/v16/psutil"
)
type Info struct {
Mem MemoryInfo
CPU CPUInfo
}
type MemoryInfo struct {
Total uint64 // bytes
Available uint64 // bytes
Used uint64 // bytes
Limit uint64 // bytes
Throttling bool
Error error
}
type CPUInfo struct {
NCPU float64 // number of cpus
System float64 // percent 0-100
User float64 // percent 0-100
Idle float64 // percent 0-100
Other float64 // percent 0-100
Limit float64 // percent 0-100
Throttling bool
Error error
}
type resources struct {
psutil psutil.Util
@ -34,16 +59,20 @@ type Resources interface {
Start()
Stop()
// HasLimits returns whether any limits have been set
// HasLimits returns whether any limits have been set.
HasLimits() bool
// Limits returns the CPU (percent 0-100) and memory (bytes) limits
// Limits returns the CPU (percent 0-100) and memory (bytes) limits.
Limits() (float64, uint64)
// ShouldLimit returns whether cpu and/or memory is currently limited
// ShouldLimit returns whether cpu and/or memory is currently limited.
ShouldLimit() (bool, bool)
// Request checks whether the requested resources are available.
Request(cpu float64, memory uint64) error
// Info returns the current resource usage
Info() Info
}
type Config struct {
@ -290,3 +319,38 @@ func (r *resources) Request(cpu float64, memory uint64) error {
return nil
}
func (r *resources) Info() Info {
cpulimit, memlimit := r.Limits()
cputhrottling, memthrottling := r.ShouldLimit()
cpustat, cpuerr := r.psutil.CPUPercent()
memstat, memerr := r.psutil.VirtualMemory()
cpuinfo := CPUInfo{
NCPU: r.ncpu,
System: cpustat.System,
User: cpustat.User,
Idle: cpustat.Idle,
Other: cpustat.Other,
Limit: cpulimit,
Throttling: cputhrottling,
Error: cpuerr,
}
meminfo := MemoryInfo{
Total: memstat.Total,
Available: memstat.Available,
Used: memstat.Used,
Limit: memlimit,
Throttling: memthrottling,
Error: memerr,
}
i := Info{
CPU: cpuinfo,
Mem: meminfo,
}
return i
}

3
restream/app/metadata.go Normal file
View File

@ -0,0 +1,3 @@
package app
type Metadata interface{}

View File

@ -3,6 +3,7 @@ package app
import (
"bytes"
"crypto/md5"
"encoding/json"
"strconv"
"strings"
@ -143,6 +144,15 @@ func (config *Config) CreateCommand() []string {
return command
}
func (config *Config) String() string {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err.Error()
}
return string(data)
}
func (config *Config) Hash() []byte {
b := bytes.Buffer{}
@ -316,3 +326,12 @@ func (p *ProcessID) Parse(pid string) {
p.ID = pid[:i]
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
}

View File

@ -9,15 +9,15 @@ type LogLine struct {
Data string
}
type LogEntry struct {
type ReportEntry struct {
CreatedAt time.Time
Prelude []string
Log []LogLine
Matches []string
}
type LogHistoryEntry struct {
LogEntry
type ReportHistoryEntry struct {
ReportEntry
ExitedAt time.Time
ExitState string
@ -25,12 +25,12 @@ type LogHistoryEntry struct {
Usage ProcessUsage
}
type Log struct {
LogEntry
History []LogHistoryEntry
type Report struct {
ReportEntry
History []ReportHistoryEntry
}
type LogHistorySearchResult struct {
type ReportHistorySearchResult struct {
ProcessID string
Reference string
ExitState string

View File

@ -45,21 +45,21 @@ type Restreamer interface {
SetMetadata(key string, data interface{}) error // Set general metadata
GetMetadata(key string) (interface{}, error) // Get previously set general metadata
AddProcess(config *app.Config) error // Add a new process
GetProcessIDs(idpattern, refpattern, ownerpattern, domainpattern string) []app.ProcessID // Get a list of process IDs based on patterns for ID and reference
DeleteProcess(id app.ProcessID) error // Delete a process
UpdateProcess(id app.ProcessID, config *app.Config) error // Update a process
StartProcess(id app.ProcessID) error // Start a process
StopProcess(id app.ProcessID) error // Stop a process
RestartProcess(id app.ProcessID) error // Restart a process
ReloadProcess(id app.ProcessID) error // Reload a process
GetProcess(id app.ProcessID) (*app.Process, error) // Get a process
GetProcessState(id app.ProcessID) (*app.State, error) // Get the state of a process
GetProcessLog(id app.ProcessID) (*app.Log, error) // Get the logs of a process
SearchProcessLogHistory(idpattern, refpattern, state string, from, to *time.Time) []app.LogHistorySearchResult // Search the log history of all processes
GetPlayout(id app.ProcessID, inputid string) (string, error) // Get the URL of the playout API for a process
SetProcessMetadata(id app.ProcessID, key string, data interface{}) error // Set metatdata to a process
GetProcessMetadata(id app.ProcessID, key string) (interface{}, error) // Get previously set metadata from a process
AddProcess(config *app.Config) error // Add a new process
GetProcessIDs(idpattern, refpattern, ownerpattern, domainpattern string) []app.ProcessID // Get a list of process IDs based on patterns for ID and reference
DeleteProcess(id app.ProcessID) error // Delete a process
UpdateProcess(id app.ProcessID, config *app.Config) error // Update a process
StartProcess(id app.ProcessID) error // Start a process
StopProcess(id app.ProcessID) error // Stop a process
RestartProcess(id app.ProcessID) error // Restart a process
ReloadProcess(id app.ProcessID) error // Reload a process
GetProcess(id app.ProcessID) (*app.Process, error) // Get a process
GetProcessState(id app.ProcessID) (*app.State, error) // Get the state of a process
GetProcessLog(id app.ProcessID) (*app.Report, error) // Get the logs of a process
SearchProcessLogHistory(idpattern, refpattern, state string, from, to *time.Time) []app.ReportHistorySearchResult // Search the log history of all processes
GetPlayout(id app.ProcessID, inputid string) (string, error) // Get the URL of the playout API for a process
SetProcessMetadata(id app.ProcessID, key string, data interface{}) error // Set metatdata to a process
GetProcessMetadata(id app.ProcessID, key string) (interface{}, error) // Get previously set metadata from a process
Probe(config *app.Config, timeout time.Duration) app.Probe // Probe a process with specific timeout
}
@ -1777,8 +1777,8 @@ func convertProgressFromParser(progress *app.Progress, pprogress parse.Progress)
}
}
func (r *restream) GetProcessLog(id app.ProcessID) (*app.Log, error) {
log := &app.Log{}
func (r *restream) GetProcessLog(id app.ProcessID) (*app.Report, error) {
log := &app.Report{}
r.lock.RLock()
defer r.lock.RUnlock()
@ -1808,8 +1808,8 @@ func (r *restream) GetProcessLog(id app.ProcessID) (*app.Log, error) {
history := task.parser.ReportHistory()
for _, h := range history {
e := app.LogHistoryEntry{
LogEntry: app.LogEntry{
e := app.ReportHistoryEntry{
ReportEntry: app.ReportEntry{
CreatedAt: h.CreatedAt,
Prelude: h.Prelude,
Matches: h.Matches,
@ -1849,9 +1849,9 @@ func (r *restream) GetProcessLog(id app.ProcessID) (*app.Log, error) {
e.Progress.Output[i].ID = task.process.Config.Output[p.Index].ID
}
e.LogEntry.Log = make([]app.LogLine, len(h.Log))
e.ReportEntry.Log = make([]app.LogLine, len(h.Log))
for i, line := range h.Log {
e.LogEntry.Log[i] = app.LogLine{
e.ReportEntry.Log[i] = app.LogLine{
Timestamp: line.Timestamp,
Data: line.Data,
}
@ -1863,8 +1863,8 @@ func (r *restream) GetProcessLog(id app.ProcessID) (*app.Log, error) {
return log, nil
}
func (r *restream) SearchProcessLogHistory(idpattern, refpattern, state string, from, to *time.Time) []app.LogHistorySearchResult {
result := []app.LogHistorySearchResult{}
func (r *restream) SearchProcessLogHistory(idpattern, refpattern, state string, from, to *time.Time) []app.ReportHistorySearchResult {
result := []app.ReportHistorySearchResult{}
ids := r.GetProcessIDs(idpattern, refpattern, "", "")
@ -1880,7 +1880,7 @@ func (r *restream) SearchProcessLogHistory(idpattern, refpattern, state string,
presult := task.parser.SearchReportHistory(state, from, to)
for _, f := range presult {
result = append(result, app.LogHistorySearchResult{
result = append(result, app.ReportHistorySearchResult{
ProcessID: task.id,
Reference: task.reference,
ExitState: f.ExitState,

View File

@ -7,7 +7,7 @@ import (
"github.com/datarhei/core/v16/iam"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/rtmp"
rtmpurl "github.com/datarhei/core/v16/rtmp/url"
srturl "github.com/datarhei/core/v16/srt/url"
)
@ -124,7 +124,7 @@ func (g *rewrite) rtmpURL(u *url.URL, _ Access, identity iamidentity.Verifier) s
token := identity.GetServiceToken()
// Remove the existing token from the path
path, _, _ := rtmp.GetToken(u)
path, _, _ := rtmpurl.GetToken(u)
u.Path = path
q := u.Query()

View File

@ -5,17 +5,17 @@ import (
"crypto/tls"
"fmt"
"net"
"net/url"
"path/filepath"
"strings"
"sync"
"time"
"github.com/datarhei/core/v16/cluster/proxy"
"github.com/datarhei/core/v16/cluster/node"
enctoken "github.com/datarhei/core/v16/encoding/token"
"github.com/datarhei/core/v16/iam"
iamidentity "github.com/datarhei/core/v16/iam/identity"
"github.com/datarhei/core/v16/log"
rtmpurl "github.com/datarhei/core/v16/rtmp/url"
"github.com/datarhei/core/v16/session"
"github.com/datarhei/joy4/av/avutil"
@ -61,7 +61,7 @@ type Config struct {
// with methods like tls.Config.SetSessionTicketKeys.
TLSConfig *tls.Config
Proxy proxy.ProxyReader
Proxy *node.Manager
IAM iam.IAM
}
@ -98,7 +98,7 @@ type server struct {
channels map[string]*channel
lock sync.RWMutex
proxy proxy.ProxyReader
proxy *node.Manager
iam iam.IAM
}
@ -203,68 +203,14 @@ func (s *server) log(who, handler, action, resource, message string, client net.
}).Log(message)
}
// GetToken returns the path without the token and the token found in the URL and whether
// it was found in the path. If the token was part of the path, the token is removed from
// the path. The token in the query string takes precedence. The token in the path is
// assumed to be the last path element.
func GetToken(u *url.URL) (string, string, bool) {
q := u.Query()
if q.Has("token") {
// The token was in the query. Return the unmomdified path and the token.
return u.Path, q.Get("token"), false
}
pathElements := splitPath(u.EscapedPath())
nPathElements := len(pathElements)
if nPathElements <= 1 {
return u.Path, "", false
}
rawPath := "/" + strings.Join(pathElements[:nPathElements-1], "/")
rawToken := pathElements[nPathElements-1]
path, err := url.PathUnescape(rawPath)
if err != nil {
path = rawPath
}
token, err := url.PathUnescape(rawToken)
if err != nil {
token = rawToken
}
// Return the path without the token
return path, token, true
}
func splitPath(path string) []string {
pathElements := strings.Split(filepath.Clean(path), "/")
if len(pathElements) == 0 {
return pathElements
}
if len(pathElements[0]) == 0 {
pathElements = pathElements[1:]
}
return pathElements
}
func removePathPrefix(path, prefix string) (string, string) {
prefix = filepath.Join("/", prefix)
return filepath.Join("/", strings.TrimPrefix(path, prefix+"/")), prefix
}
// handlePlay is called when a RTMP client wants to play a stream
func (s *server) handlePlay(conn *rtmp.Conn) {
defer conn.Close()
remote := conn.NetConn().RemoteAddr()
playpath, token, isStreamkey := GetToken(conn.URL)
playpath, token, isStreamkey := rtmpurl.GetToken(conn.URL)
playpath, _ = removePathPrefix(playpath, s.app)
playpath, _ = rtmpurl.RemovePathPrefix(playpath, s.app)
identity, err := s.findIdentityFromStreamKey(token)
if err != nil {
@ -293,7 +239,7 @@ func (s *server) handlePlay(conn *rtmp.Conn) {
if ch == nil && s.proxy != nil {
// Check in the cluster for that stream
url, err := s.proxy.GetURL("rtmp", playpath)
url, err := s.proxy.MediaGetURL("rtmp", playpath)
if err != nil {
s.log(identity, "PLAY", "NOTFOUND", playpath, "", remote)
return
@ -390,9 +336,9 @@ func (s *server) handlePublish(conn *rtmp.Conn) {
defer conn.Close()
remote := conn.NetConn().RemoteAddr()
playpath, token, isStreamkey := GetToken(conn.URL)
playpath, token, isStreamkey := rtmpurl.GetToken(conn.URL)
playpath, app := removePathPrefix(playpath, s.app)
playpath, app := rtmpurl.RemovePathPrefix(playpath, s.app)
identity, err := s.findIdentityFromStreamKey(token)
if err != nil {
@ -534,7 +480,7 @@ func (s *server) findIdentityFromStreamKey(key string) (string, error) {
// considered the domain. It is assumed that the app is not part of
// the provided path.
func (s *server) findDomainFromPlaypath(path string) string {
elements := splitPath(path)
elements := rtmpurl.SplitPath(path)
if len(elements) == 1 {
return "$none"
}

View File

@ -4,6 +4,8 @@ import (
"net/url"
"testing"
rtmpurl "github.com/datarhei/core/v16/rtmp/url"
"github.com/stretchr/testify/require"
)
@ -20,7 +22,7 @@ func TestToken(t *testing.T) {
u, err := url.Parse(d[0])
require.NoError(t, err)
path, token, _ := GetToken(u)
path, token, _ := rtmpurl.GetToken(u)
require.Equal(t, d[1], path, "url=%s", u.String())
require.Equal(t, d[2], token, "url=%s", u.String())
@ -35,7 +37,7 @@ func TestSplitPath(t *testing.T) {
}
for path, split := range data {
elms := splitPath(path)
elms := rtmpurl.SplitPath(path)
require.ElementsMatch(t, split, elms, "%s", path)
}
@ -49,7 +51,7 @@ func TestRemovePathPrefix(t *testing.T) {
}
for _, d := range data {
x, _ := removePathPrefix(d[0], d[1])
x, _ := rtmpurl.RemovePathPrefix(d[0], d[1])
require.Equal(t, d[2], x, "path=%s prefix=%s", d[0], d[1])
}

61
rtmp/url/url.go Normal file
View File

@ -0,0 +1,61 @@
package url
import (
"net/url"
"path/filepath"
"strings"
)
// GetToken returns the path without the token and the token found in the URL and whether
// it was found in the path. If the token was part of the path, the token is removed from
// the path. The token in the query string takes precedence. The token in the path is
// assumed to be the last path element.
func GetToken(u *url.URL) (string, string, bool) {
q := u.Query()
if q.Has("token") {
// The token was in the query. Return the unmomdified path and the token.
return u.Path, q.Get("token"), false
}
pathElements := SplitPath(u.EscapedPath())
nPathElements := len(pathElements)
if nPathElements <= 1 {
return u.Path, "", false
}
rawPath := "/" + strings.Join(pathElements[:nPathElements-1], "/")
rawToken := pathElements[nPathElements-1]
path, err := url.PathUnescape(rawPath)
if err != nil {
path = rawPath
}
token, err := url.PathUnescape(rawToken)
if err != nil {
token = rawToken
}
// Return the path without the token
return path, token, true
}
func SplitPath(path string) []string {
pathElements := strings.Split(filepath.Clean(path), "/")
if len(pathElements) == 0 {
return pathElements
}
if len(pathElements[0]) == 0 {
pathElements = pathElements[1:]
}
return pathElements
}
func RemovePathPrefix(path, prefix string) (string, string) {
prefix = filepath.Join("/", prefix)
return filepath.Join("/", strings.TrimPrefix(path, prefix+"/")), prefix
}

View File

@ -59,7 +59,7 @@ func DiffEqualer[T any, X Equaler[T]](listA []T, listB []X) ([]T, []X) {
if visited[j] {
continue
}
if listB[j].Equal(element) {
if listB[j].Equal(element) == nil {
visited[j] = true
found = true
break

View File

@ -1,28 +1,54 @@
package slices
import (
"errors"
"fmt"
"strings"
)
// EqualComparableElements returns whether two slices have the same elements.
func EqualComparableElements[T comparable](a, b []T) bool {
func EqualComparableElements[T comparable](a, b []T) error {
extraA, extraB := DiffComparable(a, b)
if len(extraA) == 0 && len(extraB) == 0 {
return true
return nil
}
return false
diff := []string{}
for _, e := range extraA {
diff = append(diff, fmt.Sprintf("+ %v", e))
}
for _, e := range extraB {
diff = append(diff, fmt.Sprintf("- %v", e))
}
return errors.New(strings.Join(diff, ","))
}
// Equaler defines a type that implements the Equal function.
type Equaler[T any] interface {
Equal(T) bool
Equal(T) error
}
// EqualEqualerElements returns whether two slices of Equaler have the same elements.
func EqualEqualerElements[T any, X Equaler[T]](a []T, b []X) bool {
func EqualEqualerElements[T any, X Equaler[T]](a []T, b []X) error {
extraA, extraB := DiffEqualer(a, b)
if len(extraA) == 0 && len(extraB) == 0 {
return true
return nil
}
return false
diff := []string{}
for _, e := range extraA {
diff = append(diff, fmt.Sprintf("- %v", e))
}
for _, e := range extraB {
diff = append(diff, fmt.Sprintf("+ %v", e))
}
return errors.New(strings.Join(diff, ","))
}

View File

@ -1,6 +1,7 @@
package slices
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
@ -10,42 +11,46 @@ func TestEqualComparableElements(t *testing.T) {
a := []string{"a", "b", "c", "d"}
b := []string{"b", "c", "a", "d"}
ok := EqualComparableElements(a, b)
require.True(t, ok)
err := EqualComparableElements(a, b)
require.NoError(t, err)
ok = EqualComparableElements(b, a)
require.True(t, ok)
err = EqualComparableElements(b, a)
require.NoError(t, err)
a = append(a, "z")
ok = EqualComparableElements(a, b)
require.False(t, ok)
err = EqualComparableElements(a, b)
require.Error(t, err)
ok = EqualComparableElements(b, a)
require.False(t, ok)
err = EqualComparableElements(b, a)
require.Error(t, err)
}
type String string
func (a String) Equal(b String) bool {
return string(a) == string(b)
func (a String) Equal(b String) error {
if string(a) == string(b) {
return nil
}
return fmt.Errorf("%s != %s", a, b)
}
func TestEqualEqualerElements(t *testing.T) {
a := []String{"a", "b", "c", "d"}
b := []String{"b", "c", "a", "d"}
ok := EqualEqualerElements(a, b)
require.True(t, ok)
err := EqualEqualerElements(a, b)
require.NoError(t, err)
ok = EqualEqualerElements(b, a)
require.True(t, ok)
err = EqualEqualerElements(b, a)
require.NoError(t, err)
a = append(a, "z")
ok = EqualEqualerElements(a, b)
require.False(t, ok)
err = EqualEqualerElements(a, b)
require.Error(t, err)
ok = EqualEqualerElements(b, a)
require.False(t, ok)
err = EqualEqualerElements(b, a)
require.Error(t, err)
}

View File

@ -11,7 +11,7 @@ import (
"sync"
"time"
"github.com/datarhei/core/v16/cluster/proxy"
"github.com/datarhei/core/v16/cluster/node"
enctoken "github.com/datarhei/core/v16/encoding/token"
"github.com/datarhei/core/v16/iam"
iamidentity "github.com/datarhei/core/v16/iam/identity"
@ -45,7 +45,7 @@ type Config struct {
SRTLogTopics []string
Proxy proxy.ProxyReader
Proxy *node.Manager
IAM iam.IAM
}
@ -84,7 +84,7 @@ type server struct {
srtlog map[string]*ring.Ring // Per logtopic a dedicated ring buffer
srtlogLock sync.RWMutex
proxy proxy.ProxyReader
proxy *node.Manager
iam iam.IAM
}
@ -423,7 +423,7 @@ func (s *server) handleSubscribe(conn srt.Conn) {
}
// Check in the cluster for the stream and proxy it
srturl, err := s.proxy.GetURL("srt", si.Resource)
srturl, err := s.proxy.MediaGetURL("srt", si.Resource)
if err != nil {
s.log(identity, "PLAY", "NOTFOUND", si.Resource, "no publisher for this resource found", client)
return

View File

@ -2,7 +2,6 @@ package url
import (
"fmt"
"net/url"
neturl "net/url"
"regexp"
"strings"
@ -111,7 +110,7 @@ func (si *StreamInfo) String() string {
func ParseStreamId(streamid string) (StreamInfo, error) {
si := StreamInfo{Mode: "request"}
if decodedStreamid, err := url.QueryUnescape(streamid); err == nil {
if decodedStreamid, err := neturl.QueryUnescape(streamid); err == nil {
streamid = decodedStreamid
}

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 FOSS GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,312 +0,0 @@
# core-client-go
A golang client for the `github.com/datarhei/core` API.
---
- [Quick Start](#quick-start)
- [API definitions](#api-definitions)
- [General](#general)
- [Config](#config)
- [Disk filesystem](#disk-filesystem)
- [In-memory filesystem](#in-memory-filesystem)
- [Log](#log)
- [Metadata](#metadata)
- [Metrics](#metrics)
- [Process](#process)
- [RTMP](#rtmp)
- [Session](#session)
- [Skills](#skills)
- [Versioning](#versioning)
- [Contributing](#contributing)
- [Licence](#licence)
## Quick Start
Example for retrieving a list of all processes:
```
import "github.com/datarhei/core-client-go/v16"
client, err := coreclient.New(coreclient.Config{
Address: "https://example.com:8080",
Username: "foo",
Password: "bar",
})
if err != nil {
...
}
processes, err := client.ProcessList(coreclient.ProcessListOptions{})
if err != nil {
...
}
```
## API definitions
### General
- `GET` /api
```golang
About() api.About
```
### Config
- `GET` /api/v3/config
```golang
Config() (api.Config, error)
```
- `PUT` /api/v3/config
```golang
ConfigSet(config api.ConfigSet) error
```
- `GET` /api/v3/config/reload
```golang
ConfigReload() error
```
### Disk filesystem
- `GET` /api/v3/fs/disk
```golang
DiskFSList(sort, order string) ([]api.FileInfo, error)
```
- `HEAD` /api/v3/fs/disk/{path}
```golang
DiskFSHasFile(path string) bool
```
- `GET` /api/v3/fs/disk/{path}
```golang
DiskFSGetFile(path string) (io.ReadCloser, error)
```
- `DELETE` /api/v3/fs/disk/{path}
```golang
DiskFSDeleteFile(path string) error
```
- `PUT` /api/v3/fs/disk/{path}
```golang
DiskFSAddFile(path string, data io.Reader) error
```
### In-memory filesystem
- `GET` /api/v3/fs/mem
```golang
MemFSList(sort, order string) ([]api.FileInfo, error)
```
- `HEAD` /api/v3/fs/mem/{path}
```golang
MemFSHasFile(path string) bool
```
- `GET` /api/v3/fs/mem/{path}
```golang
MemFSGetFile(path string) (io.ReadCloser, error)
```
- `DELETE` /api/v3/fs/mem/{path}
```golang
MemFSDeleteFile(path string) error
```
- `PUT` /api/v3/fs/mem/{path}
```golang
MemFSAddFile(path string, data io.Reader) error
```
### Log
- `GET` /api/v3/log
```golang
Log() ([]api.LogEvent, error)
```
### Metadata
- `GET` /api/v3/metadata/{key}
```golang
Metadata(id, key string) (api.Metadata, error)
```
- `PUT` /api/v3/metadata/{key}
```golang
MetadataSet(id, key string, metadata api.Metadata) error
```
### Metrics
- `GET` /api/v3/metrics
```golang
MetricsList() ([]api.MetricsDescription, error)
```
- `POST` /api/v3/metrics
```golang
Metrics(query api.MetricsQuery) (api.MetricsResponse, error)
```
### Process
- `GET` /api/v3/process
```golang
ProcessList(opts ProcessListOptions) ([]api.Process, error)
```
- `POST` /api/v3/process
```golang
ProcessAdd(p api.ProcessConfig) error
```
- `GET` /api/v3/process/{id}
```golang
Process(id string, filter []string) (api.Process, error)
```
- `PUT` /api/v3/process/{id}
```golang
ProcessUpdate(id string, p api.ProcessConfig) error
```
- `DELETE` /api/v3/process/{id}
```golang
ProcessDelete(id string) error
```
- `PUT` /api/v3/process/{id}/command
```golang
ProcessCommand(id, command string) error
```
- `GET` /api/v3/process/{id}/probe
```golang
ProcessProbe(id string) (api.Probe, error)
```
- `GET` /api/v3/process/{id}/config
```golang
ProcessConfig(id string) (api.ProcessConfig, error)
```
- `GET` /api/v3/process/{id}/report
```golang
ProcessReport(id string) (api.ProcessReport, error)
```
- `GET` /api/v3/process/{id}/state
```golang
ProcessState(id string) (api.ProcessState, error)
```
- `GET` /api/v3/process/{id}/metadata/{key}
```golang
ProcessMetadata(id, key string) (api.Metadata, error)
```
- `PUT` /api/v3/process/{id}/metadata/{key}
```golang
ProcessMetadataSet(id, key string, metadata api.Metadata) error
```
### RTMP
- `GET` /api/v3/rtmp
```golang
RTMPChannels() ([]api.RTMPChannel, error)
```
### SRT
- `GET` /api/v3/srt
```golang
SRTChannels() (api.SRTChannels, error)
```
### Session
- `GET` /api/v3/session
```golang
Sessions(collectors []string) (api.SessionsSummary, error)
```
- `GET` /api/v3/session/active
```golang
SessionsActive(collectors []string) (api.SessionsActive, error)
```
### Skills
- `GET` /api/v3/skills
```golang
Skills() (api.Skills, error)
```
- `GET` /api/v3/skills/reload
```golang
SkillsReload() error
```
### Widget
- `GET` /api/v3/widget
```golang
WidgetProcess(id string) (api.WidgetProcess, error)
```
## Versioning
The version of this module is according to which version of the datarhei Core API
you want to connect to. Check the branches to find out which other versions are
implemented. If you want to connect to an API version 12, you have to import the client
module of the version 12, i.e. `import "github.com/datarhei/core-client-go/v12"`.
The latest implementation is on the `main` branch.
## Contributing
Found a mistake or misconduct? Create a [issue](https://github.com/datarhei/core-client-go/issues) or send a pull-request.
Suggestions for improvement are welcome.
## Licence
[MIT](https://github.com/datarhei/core-client-go/blob/main/LICENSE)

View File

@ -1,33 +0,0 @@
package api
// About is some general information about the API
type About struct {
App string `json:"app"`
Auths []string `json:"auths"`
Name string `json:"name"`
ID string `json:"id"`
CreatedAt string `json:"created_at"`
Uptime uint64 `json:"uptime_seconds"`
Version Version `json:"version"`
}
// Version is some information about the binary
type Version struct {
Number string `json:"number"`
Commit string `json:"repository_commit"`
Branch string `json:"repository_branch"`
Build string `json:"build_date"`
Arch string `json:"arch"`
Compiler string `json:"compiler"`
}
// MinimalAbout is the minimal information about the API
type MinimalAbout struct {
App string `json:"app"`
Auths []string `json:"auths"`
Version VersionMinimal `json:"version"`
}
type VersionMinimal struct {
Number string `json:"number"`
}

View File

@ -1,23 +0,0 @@
package api
type AVstreamIO struct {
State string `json:"state" enums:"running,idle" jsonschema:"enum=running,enum=idle"`
Packet uint64 `json:"packet" format:"uint64"`
Time uint64 `json:"time"`
Size uint64 `json:"size_kb"`
}
type AVstream struct {
Input AVstreamIO `json:"input"`
Output AVstreamIO `json:"output"`
Aqueue uint64 `json:"aqueue" format:"uint64"`
Queue uint64 `json:"queue" format:"uint64"`
Dup uint64 `json:"dup" format:"uint64"`
Drop uint64 `json:"drop" format:"uint64"`
Enc uint64 `json:"enc" format:"uint64"`
Looping bool `json:"looping"`
LoopingRuntime uint64 `json:"looping_runtime" format:"uint64"`
Duplicating bool `json:"duplicating"`
GOP string `json:"gop"`
Mode string `json:"mode"`
}

Some files were not shown because too many files have changed in this diff Show More