Refactor cluster node code
This commit is contained in:
parent
28603aab98
commit
480dbb7f53
@ -543,6 +543,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,
|
||||
},
|
||||
@ -1337,7 +1338,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 {
|
||||
@ -1368,7 +1369,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
173
cluster/about.go
Normal 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
181
cluster/affinity.go
Normal 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
|
||||
}
|
||||
249
cluster/api.go
249
cluster/api.go
@ -17,11 +17,12 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/datarhei/core/v16/cluster/client"
|
||||
"github.com/datarhei/core/v16/cluster/store"
|
||||
"github.com/datarhei/core/v16/encoding/json"
|
||||
"github.com/datarhei/core/v16/http/handler/util"
|
||||
httplog "github.com/datarhei/core/v16/http/log"
|
||||
@ -38,11 +39,12 @@ import (
|
||||
)
|
||||
|
||||
type api struct {
|
||||
id string
|
||||
address string
|
||||
router *echo.Echo
|
||||
cluster Cluster
|
||||
logger log.Logger
|
||||
id string
|
||||
address string
|
||||
router *echo.Echo
|
||||
cluster *cluster
|
||||
logger log.Logger
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
type API interface {
|
||||
@ -52,15 +54,16 @@ type API interface {
|
||||
|
||||
type APIConfig struct {
|
||||
ID string
|
||||
Cluster Cluster
|
||||
Cluster *cluster
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func NewAPI(config APIConfig) (API, error) {
|
||||
a := &api{
|
||||
id: config.ID,
|
||||
cluster: config.Cluster,
|
||||
logger: config.Logger,
|
||||
id: config.ID,
|
||||
cluster: config.Cluster,
|
||||
logger: config.Logger,
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
|
||||
if a.logger == nil {
|
||||
@ -98,6 +101,7 @@ func NewAPI(config APIConfig) (API, error) {
|
||||
doc.GET("", echoSwagger.EchoWrapHandler(echoSwagger.InstanceName("ClusterAPI")))
|
||||
|
||||
a.router.GET("/", a.Version)
|
||||
a.router.GET("/v1/about", a.About)
|
||||
|
||||
a.router.GET("/v1/barrier/:name", a.Barrier)
|
||||
|
||||
@ -108,27 +112,27 @@ func NewAPI(config APIConfig) (API, error) {
|
||||
|
||||
a.router.GET("/v1/snaphot", a.Snapshot)
|
||||
|
||||
a.router.POST("/v1/process", a.AddProcess)
|
||||
a.router.DELETE("/v1/process/:id", a.RemoveProcess)
|
||||
a.router.PUT("/v1/process/:id", a.UpdateProcess)
|
||||
a.router.PUT("/v1/process/:id/command", a.SetProcessCommand)
|
||||
a.router.PUT("/v1/process/:id/metadata/:key", a.SetProcessMetadata)
|
||||
a.router.POST("/v1/process", a.ProcessAdd)
|
||||
a.router.DELETE("/v1/process/:id", a.ProcessRemove)
|
||||
a.router.PUT("/v1/process/:id", a.ProcessUpdate)
|
||||
a.router.PUT("/v1/process/:id/command", a.ProcessSetCommand)
|
||||
a.router.PUT("/v1/process/:id/metadata/:key", a.ProcessSetMetadata)
|
||||
|
||||
a.router.PUT("/v1/relocate", a.RelocateProcesses)
|
||||
a.router.PUT("/v1/relocate", a.ProcessesRelocate)
|
||||
|
||||
a.router.POST("/v1/iam/user", a.AddIdentity)
|
||||
a.router.PUT("/v1/iam/user/:name", a.UpdateIdentity)
|
||||
a.router.PUT("/v1/iam/user/:name/policies", a.SetIdentityPolicies)
|
||||
a.router.DELETE("/v1/iam/user/:name", a.RemoveIdentity)
|
||||
a.router.POST("/v1/iam/user", a.IAMIdentityAdd)
|
||||
a.router.PUT("/v1/iam/user/:name", a.IAMIdentityUpdate)
|
||||
a.router.PUT("/v1/iam/user/:name/policies", a.IAMPoliciesSet)
|
||||
a.router.DELETE("/v1/iam/user/:name", a.IAMIdentityRemove)
|
||||
|
||||
a.router.POST("/v1/lock", a.Lock)
|
||||
a.router.DELETE("/v1/lock/:name", a.Unlock)
|
||||
a.router.POST("/v1/lock", a.LockCreate)
|
||||
a.router.DELETE("/v1/lock/:name", a.LockDelete)
|
||||
|
||||
a.router.POST("/v1/kv", a.SetKV)
|
||||
a.router.GET("/v1/kv/:key", a.GetKV)
|
||||
a.router.DELETE("/v1/kv/:key", a.UnsetKV)
|
||||
a.router.POST("/v1/kv", a.KVSet)
|
||||
a.router.GET("/v1/kv/:key", a.KVGet)
|
||||
a.router.DELETE("/v1/kv/:key", a.KVUnset)
|
||||
|
||||
a.router.PUT("/v1/node/:id/state", a.SetNodeState)
|
||||
a.router.PUT("/v1/node/:id/state", a.NodeSetState)
|
||||
|
||||
a.router.GET("/v1/core", a.CoreAPIAddress)
|
||||
a.router.GET("/v1/core/config", a.CoreConfig)
|
||||
@ -160,6 +164,40 @@ func (a *api) Version(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, Version.String())
|
||||
}
|
||||
|
||||
// About returns the version of the cluster
|
||||
// @Summary The cluster version
|
||||
// @Description The cluster version
|
||||
// @Tags v1.0.0
|
||||
// @ID cluster-1-about
|
||||
// @Produce json
|
||||
// @Success 200 {string} About
|
||||
// @Success 500 {object} Error
|
||||
// @Router /v1/about [get]
|
||||
func (a *api) About(c echo.Context) error {
|
||||
resources, err := a.cluster.Resources()
|
||||
|
||||
about := client.AboutResponse{
|
||||
ID: a.id,
|
||||
Version: Version.String(),
|
||||
Address: a.cluster.Address(),
|
||||
StartedAt: a.startedAt,
|
||||
Resources: client.AboutResponseResources{
|
||||
IsThrottling: resources.CPU.Throttling,
|
||||
NCPU: resources.CPU.NCPU,
|
||||
CPU: (100 - resources.CPU.Idle) * resources.CPU.NCPU,
|
||||
CPULimit: resources.CPU.Limit * resources.CPU.NCPU,
|
||||
Mem: resources.Mem.Total - resources.Mem.Available,
|
||||
MemLimit: resources.Mem.Total,
|
||||
},
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
about.Resources.Error = err.Error()
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, about)
|
||||
}
|
||||
|
||||
// Barrier returns if the barrier already has been passed
|
||||
// @Summary Has the barrier already has been passed
|
||||
// @Description Has the barrier already has been passed
|
||||
@ -210,7 +248,7 @@ func (a *api) AddServer(c echo.Context) error {
|
||||
err := a.cluster.Join(origin, r.ID, r.RaftAddress, "")
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("id", r.ID).Log("Unable to join cluster")
|
||||
return Err(http.StatusInternalServerError, "", "unable to join cluster: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
@ -244,7 +282,7 @@ func (a *api) RemoveServer(c echo.Context) error {
|
||||
err := a.cluster.Leave(origin, id)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("id", id).Log("Unable to leave cluster")
|
||||
return Err(http.StatusInternalServerError, "", "unable to leave cluster: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
@ -278,7 +316,7 @@ func (a *api) TransferLeadership(c echo.Context) error {
|
||||
err := a.cluster.TransferLeadership(origin, id)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("id", id).Log("Unable to transfer leadership")
|
||||
return Err(http.StatusInternalServerError, "", "unable to transfer leadership: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
@ -303,7 +341,7 @@ func (a *api) Snapshot(c echo.Context) error {
|
||||
data, err := a.cluster.Snapshot(origin)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).Log("Unable to create snaphot")
|
||||
return Err(http.StatusInternalServerError, "", "unable to create snapshot: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
defer data.Close()
|
||||
@ -311,7 +349,7 @@ func (a *api) Snapshot(c echo.Context) error {
|
||||
return c.Stream(http.StatusOK, "application/octet-stream", data)
|
||||
}
|
||||
|
||||
// AddProcess adds a process to the cluster DB
|
||||
// ProcessAdd adds a process to the cluster DB
|
||||
// @Summary Add a process
|
||||
// @Description Add a process to the cluster DB
|
||||
// @Tags v1.0.0
|
||||
@ -325,7 +363,7 @@ func (a *api) Snapshot(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/process [post]
|
||||
func (a *api) AddProcess(c echo.Context) error {
|
||||
func (a *api) ProcessAdd(c echo.Context) error {
|
||||
r := client.AddProcessRequest{}
|
||||
|
||||
if err := util.ShouldBindJSON(c, &r); err != nil {
|
||||
@ -340,16 +378,16 @@ func (a *api) AddProcess(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("id", r.Config.ID).Log("Add process request")
|
||||
|
||||
err := a.cluster.AddProcess(origin, &r.Config)
|
||||
err := a.cluster.ProcessAdd(origin, &r.Config)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("id", r.Config.ID).Log("Unable to add process")
|
||||
return Err(http.StatusInternalServerError, "", "unable to add process: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// RemoveProcess removes a process from the cluster DB
|
||||
// ProcessRemove removes a process from the cluster DB
|
||||
// @Summary Remove a process
|
||||
// @Description Remove a process from the cluster DB
|
||||
// @Tags v1.0.0
|
||||
@ -362,7 +400,7 @@ func (a *api) AddProcess(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/process/{id} [delete]
|
||||
func (a *api) RemoveProcess(c echo.Context) error {
|
||||
func (a *api) ProcessRemove(c echo.Context) error {
|
||||
id := util.PathParam(c, "id")
|
||||
domain := util.DefaultQuery(c, "domain", "")
|
||||
|
||||
@ -376,16 +414,16 @@ func (a *api) RemoveProcess(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("id", pid).Log("Remove process request")
|
||||
|
||||
err := a.cluster.RemoveProcess(origin, pid)
|
||||
err := a.cluster.ProcessRemove(origin, pid)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("id", pid).Log("Unable to remove process")
|
||||
return Err(http.StatusInternalServerError, "", "unable to remove process: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// UpdateProcess replaces an existing process in the cluster DB
|
||||
// ProcessUpdate replaces an existing process in the cluster DB
|
||||
// @Summary Replace an existing process
|
||||
// @Description Replace an existing process in the cluster DB
|
||||
// @Tags v1.0.0
|
||||
@ -399,7 +437,7 @@ func (a *api) RemoveProcess(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/process/{id} [put]
|
||||
func (a *api) UpdateProcess(c echo.Context) error {
|
||||
func (a *api) ProcessUpdate(c echo.Context) error {
|
||||
id := util.PathParam(c, "id")
|
||||
domain := util.DefaultQuery(c, "domain", "")
|
||||
|
||||
@ -422,16 +460,16 @@ func (a *api) UpdateProcess(c echo.Context) error {
|
||||
"new_id": r.Config.ProcessID(),
|
||||
}).Log("Update process request")
|
||||
|
||||
err := a.cluster.UpdateProcess(origin, pid, &r.Config)
|
||||
err := a.cluster.ProcessUpdate(origin, pid, &r.Config)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("id", pid).Log("Unable to update process")
|
||||
return Err(http.StatusInternalServerError, "", "unable to update process: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// SetProcessCommand sets the order for a process
|
||||
// ProcessSetCommand sets the order for a process
|
||||
// @Summary Set the order for a process
|
||||
// @Description Set the order for a process.
|
||||
// @Tags v1.0.0
|
||||
@ -444,7 +482,7 @@ func (a *api) UpdateProcess(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/process/{id}/command [put]
|
||||
func (a *api) SetProcessCommand(c echo.Context) error {
|
||||
func (a *api) ProcessSetCommand(c echo.Context) error {
|
||||
id := util.PathParam(c, "id")
|
||||
domain := util.DefaultQuery(c, "domain", "")
|
||||
|
||||
@ -462,16 +500,16 @@ func (a *api) SetProcessCommand(c echo.Context) error {
|
||||
|
||||
pid := app.ProcessID{ID: id, Domain: domain}
|
||||
|
||||
err := a.cluster.SetProcessCommand(origin, pid, r.Command)
|
||||
err := a.cluster.ProcessSetCommand(origin, pid, r.Command)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("id", pid).Log("Unable to set order")
|
||||
return Err(http.StatusInternalServerError, "", "unable to set order: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
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 v1.0.0
|
||||
@ -485,7 +523,7 @@ func (a *api) SetProcessCommand(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/process/{id}/metadata/{key} [put]
|
||||
func (a *api) SetProcessMetadata(c echo.Context) error {
|
||||
func (a *api) ProcessSetMetadata(c echo.Context) error {
|
||||
id := util.PathParam(c, "id")
|
||||
key := util.PathParam(c, "key")
|
||||
domain := util.DefaultQuery(c, "domain", "")
|
||||
@ -504,16 +542,16 @@ func (a *api) SetProcessMetadata(c echo.Context) error {
|
||||
|
||||
pid := app.ProcessID{ID: id, Domain: domain}
|
||||
|
||||
err := a.cluster.SetProcessMetadata(origin, pid, key, r.Metadata)
|
||||
err := a.cluster.ProcessSetMetadata(origin, pid, key, r.Metadata)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("id", pid).Log("Unable to update metadata")
|
||||
return Err(http.StatusInternalServerError, "", "unable to update metadata: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// RelocateProcesses relocates processes to another node
|
||||
// ProcessesRelocate relocates processes to another node
|
||||
// @Summary Relocate processes to another node
|
||||
// @Description Relocate processes to another node.
|
||||
// @Tags v1.0.0
|
||||
@ -524,7 +562,7 @@ func (a *api) SetProcessMetadata(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/relocate [put]
|
||||
func (a *api) RelocateProcesses(c echo.Context) error {
|
||||
func (a *api) ProcessesRelocate(c echo.Context) error {
|
||||
r := client.RelocateProcessesRequest{}
|
||||
|
||||
if err := util.ShouldBindJSON(c, &r); err != nil {
|
||||
@ -537,16 +575,16 @@ func (a *api) RelocateProcesses(c echo.Context) error {
|
||||
return Err(http.StatusLoopDetected, "", "breaking circuit")
|
||||
}
|
||||
|
||||
err := a.cluster.RelocateProcesses(origin, r.Map)
|
||||
err := a.cluster.ProcessesRelocate(origin, r.Map)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).Log("Unable to apply process relocation request")
|
||||
return Err(http.StatusInternalServerError, "", "unable to apply process relocation request: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// AddIdentity adds an identity to the cluster DB
|
||||
// IAMIdentityAdd adds an identity to the cluster DB
|
||||
// @Summary Add an identity
|
||||
// @Description Add an identity to the cluster DB
|
||||
// @Tags v1.0.0
|
||||
@ -560,7 +598,7 @@ func (a *api) RelocateProcesses(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/iam/user [post]
|
||||
func (a *api) AddIdentity(c echo.Context) error {
|
||||
func (a *api) IAMIdentityAdd(c echo.Context) error {
|
||||
r := client.AddIdentityRequest{}
|
||||
|
||||
if err := util.ShouldBindJSON(c, &r); err != nil {
|
||||
@ -575,16 +613,16 @@ func (a *api) AddIdentity(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("identity", r.Identity).Log("Add identity request")
|
||||
|
||||
err := a.cluster.AddIdentity(origin, r.Identity)
|
||||
err := a.cluster.IAMIdentityAdd(origin, r.Identity)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("identity", r.Identity).Log("Unable to add identity")
|
||||
return Err(http.StatusInternalServerError, "", "unable to add identity: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// UpdateIdentity replaces an existing identity in the cluster DB
|
||||
// IAMIdentityUpdate replaces an existing identity in the cluster DB
|
||||
// @Summary Replace an existing identity
|
||||
// @Description Replace an existing identity in the cluster DB
|
||||
// @Tags v1.0.0
|
||||
@ -597,7 +635,7 @@ func (a *api) AddIdentity(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/iam/user/{name} [put]
|
||||
func (a *api) UpdateIdentity(c echo.Context) error {
|
||||
func (a *api) IAMIdentityUpdate(c echo.Context) error {
|
||||
name := util.PathParam(c, "name")
|
||||
|
||||
r := client.UpdateIdentityRequest{}
|
||||
@ -617,32 +655,32 @@ func (a *api) UpdateIdentity(c echo.Context) error {
|
||||
"identity": r.Identity,
|
||||
}).Log("Update identity request")
|
||||
|
||||
err := a.cluster.UpdateIdentity(origin, name, r.Identity)
|
||||
err := a.cluster.IAMIdentityUpdate(origin, name, r.Identity)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithFields(log.Fields{
|
||||
"name": name,
|
||||
"identity": r.Identity,
|
||||
}).Log("Unable to add identity")
|
||||
return Err(http.StatusInternalServerError, "", "unable to update identity: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// SetIdentityPolicies set policies for an identity in the cluster DB
|
||||
// IAMPoliciesSet set policies for an identity in the cluster DB
|
||||
// @Summary Set identity policies
|
||||
// @Description Set policies for an identity in the cluster DB. Any existing policies will be replaced.
|
||||
// @Tags v1.0.0
|
||||
// @ID cluster-3-set-identity-policies
|
||||
// @Produce json
|
||||
// @Param id path string true "Process ID"SetPoliciesRequest
|
||||
// @Param id path string true "Process ID" SetPoliciesRequest
|
||||
// @Param data body client.SetPoliciesRequest true "Policies for that user"
|
||||
// @Success 200 {string} string
|
||||
// @Failure 400 {object} Error
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/iam/user/{name}/policies [put]
|
||||
func (a *api) SetIdentityPolicies(c echo.Context) error {
|
||||
func (a *api) IAMPoliciesSet(c echo.Context) error {
|
||||
name := util.PathParam(c, "name")
|
||||
|
||||
r := client.SetPoliciesRequest{}
|
||||
@ -659,16 +697,16 @@ func (a *api) SetIdentityPolicies(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("policies", r.Policies).Log("Set policiesrequest")
|
||||
|
||||
err := a.cluster.SetPolicies(origin, name, r.Policies)
|
||||
err := a.cluster.IAMPoliciesSet(origin, name, r.Policies)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("policies", r.Policies).Log("Unable to set policies")
|
||||
return Err(http.StatusInternalServerError, "", "unable to add identity: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// RemoveIdentity removes an identity from the cluster DB
|
||||
// IAMIdentityRemove removes an identity from the cluster DB
|
||||
// @Summary Remove an identity
|
||||
// @Description Remove an identity from the cluster DB
|
||||
// @Tags v1.0.0
|
||||
@ -680,7 +718,7 @@ func (a *api) SetIdentityPolicies(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/iam/user/{name} [delete]
|
||||
func (a *api) RemoveIdentity(c echo.Context) error {
|
||||
func (a *api) IAMIdentityRemove(c echo.Context) error {
|
||||
name := util.PathParam(c, "name")
|
||||
|
||||
origin := c.Request().Header.Get("X-Cluster-Origin")
|
||||
@ -691,10 +729,10 @@ func (a *api) RemoveIdentity(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("identity", name).Log("Remove identity request")
|
||||
|
||||
err := a.cluster.RemoveIdentity(origin, name)
|
||||
err := a.cluster.IAMIdentityRemove(origin, name)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("identity", name).Log("Unable to remove identity")
|
||||
return Err(http.StatusInternalServerError, "", "unable to remove identity: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
@ -739,19 +777,19 @@ func (a *api) CoreSkills(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, skills)
|
||||
}
|
||||
|
||||
// Lock tries to acquire a named lock
|
||||
// LockCreate tries to acquire a named lock
|
||||
// @Summary Acquire a named lock
|
||||
// @Description Acquire a named lock
|
||||
// @Tags v1.0.0
|
||||
// @ID cluster-1-lock
|
||||
// @Produce json
|
||||
// @Param data body client.LockRequest true "Lock request"
|
||||
// @Param data body client.LockRequest true "LockCreate request"
|
||||
// @Param X-Cluster-Origin header string false "Origin ID of request"
|
||||
// @Success 200 {string} string
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/lock [post]
|
||||
func (a *api) Lock(c echo.Context) error {
|
||||
func (a *api) LockCreate(c echo.Context) error {
|
||||
r := client.LockRequest{}
|
||||
|
||||
if err := util.ShouldBindJSON(c, &r); err != nil {
|
||||
@ -766,16 +804,16 @@ func (a *api) Lock(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("name", r.Name).Log("Acquire lock")
|
||||
|
||||
_, err := a.cluster.CreateLock(origin, r.Name, r.ValidUntil)
|
||||
_, err := a.cluster.LockCreate(origin, r.Name, r.ValidUntil)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("name", r.Name).Log("Unable to acquire lock")
|
||||
return Err(http.StatusInternalServerError, "", "unable to acquire lock: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// Unlock removes a named lock
|
||||
// LockDelete removes a named lock
|
||||
// @Summary Remove a lock
|
||||
// @Description Remove a lock
|
||||
// @Tags v1.0.0
|
||||
@ -788,7 +826,7 @@ func (a *api) Lock(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/lock/{name} [delete]
|
||||
func (a *api) Unlock(c echo.Context) error {
|
||||
func (a *api) LockDelete(c echo.Context) error {
|
||||
name := util.PathParam(c, "name")
|
||||
|
||||
origin := c.Request().Header.Get("X-Cluster-Origin")
|
||||
@ -799,16 +837,16 @@ func (a *api) Unlock(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("name", name).Log("Remove lock request")
|
||||
|
||||
err := a.cluster.DeleteLock(origin, name)
|
||||
err := a.cluster.LockDelete(origin, name)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("name", name).Log("Unable to remove lock")
|
||||
return Err(http.StatusInternalServerError, "", "unable to remove lock: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// SetKV stores the value under key
|
||||
// KVSet stores the value under key
|
||||
// @Summary Store value under key
|
||||
// @Description Store value under key
|
||||
// @Tags v1.0.0
|
||||
@ -820,7 +858,7 @@ func (a *api) Unlock(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/kv [post]
|
||||
func (a *api) SetKV(c echo.Context) error {
|
||||
func (a *api) KVSet(c echo.Context) error {
|
||||
r := client.SetKVRequest{}
|
||||
|
||||
if err := util.ShouldBindJSON(c, &r); err != nil {
|
||||
@ -835,16 +873,16 @@ func (a *api) SetKV(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("key", r.Key).Log("Store value")
|
||||
|
||||
err := a.cluster.SetKV(origin, r.Key, r.Value)
|
||||
err := a.cluster.KVSet(origin, r.Key, r.Value)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithField("key", r.Key).Log("Unable to store value")
|
||||
return Err(http.StatusInternalServerError, "", "unable to store value: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// UnsetKV removes a key
|
||||
// KVUnset removes a key
|
||||
// @Summary Remove a key
|
||||
// @Description Remove a key
|
||||
// @Tags v1.0.0
|
||||
@ -857,7 +895,7 @@ func (a *api) SetKV(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/kv/{key} [delete]
|
||||
func (a *api) UnsetKV(c echo.Context) error {
|
||||
func (a *api) KVUnset(c echo.Context) error {
|
||||
key := util.PathParam(c, "key")
|
||||
|
||||
origin := c.Request().Header.Get("X-Cluster-Origin")
|
||||
@ -868,20 +906,16 @@ func (a *api) UnsetKV(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("key", key).Log("Delete key")
|
||||
|
||||
err := a.cluster.UnsetKV(origin, key)
|
||||
err := a.cluster.KVUnset(origin, key)
|
||||
if err != nil {
|
||||
if err == fs.ErrNotExist {
|
||||
a.logger.Debug().WithError(err).WithField("key", key).Log("Delete key: not found")
|
||||
return Err(http.StatusNotFound, "", "%s", err.Error())
|
||||
}
|
||||
a.logger.Debug().WithError(err).WithField("key", key).Log("Unable to remove key")
|
||||
return Err(http.StatusInternalServerError, "", "unable to remove key: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
// GetKV fetches a key
|
||||
// KVGet fetches a key
|
||||
// @Summary Fetch a key
|
||||
// @Description Fetch a key
|
||||
// @Tags v1.0.0
|
||||
@ -894,7 +928,7 @@ func (a *api) UnsetKV(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/kv/{key} [get]
|
||||
func (a *api) GetKV(c echo.Context) error {
|
||||
func (a *api) KVGet(c echo.Context) error {
|
||||
key := util.PathParam(c, "key")
|
||||
|
||||
origin := c.Request().Header.Get("X-Cluster-Origin")
|
||||
@ -905,14 +939,10 @@ func (a *api) GetKV(c echo.Context) error {
|
||||
|
||||
a.logger.Debug().WithField("key", key).Log("Get key")
|
||||
|
||||
value, updatedAt, err := a.cluster.GetKV(origin, key, false)
|
||||
value, updatedAt, err := a.cluster.KVGet(origin, key, false)
|
||||
if err != nil {
|
||||
if err == fs.ErrNotExist {
|
||||
a.logger.Debug().WithError(err).WithField("key", key).Log("Get key: not found")
|
||||
return Err(http.StatusNotFound, "", "%s", err.Error())
|
||||
}
|
||||
a.logger.Debug().WithError(err).WithField("key", key).Log("Unable to retrieve key")
|
||||
return Err(http.StatusInternalServerError, "", "unable to retrieve key: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
res := client.GetKVResponse{
|
||||
@ -923,7 +953,7 @@ func (a *api) GetKV(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, res)
|
||||
}
|
||||
|
||||
// SetNodeState sets a state for a node
|
||||
// NodeSetState sets a state for a node
|
||||
// @Summary Set a state for a node
|
||||
// @Description Set a state for a node
|
||||
// @Tags v1.0.0
|
||||
@ -936,7 +966,7 @@ func (a *api) GetKV(c echo.Context) error {
|
||||
// @Failure 500 {object} Error
|
||||
// @Failure 508 {object} Error
|
||||
// @Router /v1/node/{id}/state [get]
|
||||
func (a *api) SetNodeState(c echo.Context) error {
|
||||
func (a *api) NodeSetState(c echo.Context) error {
|
||||
nodeid := util.PathParam(c, "id")
|
||||
|
||||
r := client.SetNodeStateRequest{}
|
||||
@ -956,7 +986,7 @@ func (a *api) SetNodeState(c echo.Context) error {
|
||||
"state": r.State,
|
||||
}).Log("Set node state")
|
||||
|
||||
err := a.cluster.SetNodeState(origin, nodeid, r.State)
|
||||
err := a.cluster.NodeSetState(origin, nodeid, r.State)
|
||||
if err != nil {
|
||||
a.logger.Debug().WithError(err).WithFields(log.Fields{
|
||||
"node": nodeid,
|
||||
@ -966,7 +996,7 @@ func (a *api) SetNodeState(c echo.Context) error {
|
||||
if errors.Is(err, ErrUnsupportedNodeState) {
|
||||
return Err(http.StatusBadRequest, "", "%s: %s", err.Error(), r.State)
|
||||
}
|
||||
return Err(http.StatusInternalServerError, "", "unable to set state: %s", err.Error())
|
||||
return ErrFromClusterError(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
@ -1007,6 +1037,17 @@ func Err(code int, message string, args ...interface{}) Error {
|
||||
return e
|
||||
}
|
||||
|
||||
func ErrFromClusterError(err error) Error {
|
||||
status := http.StatusInternalServerError
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
status = http.StatusNotFound
|
||||
} else if errors.Is(err, store.ErrBadRequest) {
|
||||
status = http.StatusBadRequest
|
||||
}
|
||||
|
||||
return Err(status, "", "%s", err.Error())
|
||||
}
|
||||
|
||||
// ErrorHandler is a genral handler for echo handler errors
|
||||
func ErrorHandler(err error, c echo.Context) {
|
||||
var code int = 0
|
||||
|
||||
@ -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"
|
||||
@ -40,7 +39,7 @@ type SetProcessMetadataRequest struct {
|
||||
}
|
||||
|
||||
type RelocateProcessesRequest struct {
|
||||
Map map[app.ProcessID]string
|
||||
Map map[app.ProcessID]string `json:"map"`
|
||||
}
|
||||
|
||||
type AddIdentityRequest struct {
|
||||
@ -70,6 +69,24 @@ 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"`
|
||||
}
|
||||
@ -94,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
|
||||
}
|
||||
@ -177,177 +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) RelocateProcesses(origin string, r RelocateProcessesRequest) error {
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = c.call(http.MethodPut, "/v1/relocate", "application/json", bytes.NewReader(data), origin)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *APIClient) AddIdentity(origin string, r AddIdentityRequest) error {
|
||||
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) SetNodeState(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
|
||||
}
|
||||
|
||||
func (c *APIClient) Snapshot(origin string) (io.ReadCloser, error) {
|
||||
return c.stream(http.MethodGet, "/v1/snapshot", "", nil, origin)
|
||||
}
|
||||
@ -376,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()
|
||||
|
||||
@ -422,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
48
cluster/client/iam.go
Normal 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
54
cluster/client/kvs.go
Normal 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
26
cluster/client/lock.go
Normal 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
20
cluster/client/node.go
Normal 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
71
cluster/client/proces.go
Normal 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
|
||||
}
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
gonet "net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@ -18,7 +17,6 @@ import (
|
||||
"github.com/datarhei/core/v16/cluster/forwarder"
|
||||
"github.com/datarhei/core/v16/cluster/kvs"
|
||||
clusternode "github.com/datarhei/core/v16/cluster/node"
|
||||
"github.com/datarhei/core/v16/cluster/proxy"
|
||||
"github.com/datarhei/core/v16/cluster/raft"
|
||||
"github.com/datarhei/core/v16/cluster/store"
|
||||
"github.com/datarhei/core/v16/config"
|
||||
@ -29,8 +27,8 @@ import (
|
||||
iamidentity "github.com/datarhei/core/v16/iam/identity"
|
||||
"github.com/datarhei/core/v16/log"
|
||||
"github.com/datarhei/core/v16/net"
|
||||
"github.com/datarhei/core/v16/resources"
|
||||
"github.com/datarhei/core/v16/restream/app"
|
||||
"github.com/datarhei/core/v16/slices"
|
||||
)
|
||||
|
||||
type Cluster interface {
|
||||
@ -51,50 +49,42 @@ type Cluster interface {
|
||||
CoreSkills() skills.Skills
|
||||
|
||||
About() (ClusterAbout, error)
|
||||
IsClusterDegraded() (bool, error)
|
||||
IsDegraded() (bool, error)
|
||||
GetBarrier(name string) bool
|
||||
|
||||
Join(origin, id, raftAddress, peerAddress string) error
|
||||
Leave(origin, id string) error // gracefully remove a node from the cluster
|
||||
TransferLeadership(origin, id string) error // transfer leadership to another node
|
||||
Snapshot(origin string) (io.ReadCloser, error)
|
||||
HasRaftLeader() bool
|
||||
|
||||
ListProcesses() []store.Process
|
||||
GetProcess(id app.ProcessID) (store.Process, error)
|
||||
AddProcess(origin string, config *app.Config) error
|
||||
RemoveProcess(origin string, id app.ProcessID) error
|
||||
UpdateProcess(origin string, id app.ProcessID, config *app.Config) error
|
||||
SetProcessCommand(origin string, id app.ProcessID, order string) error
|
||||
SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error
|
||||
GetProcessMetadata(origin string, id app.ProcessID, key string) (interface{}, error)
|
||||
GetProcessNodeMap() map[string]string
|
||||
RelocateProcesses(origin string, relocations map[app.ProcessID]string) error
|
||||
ProcessAdd(origin string, config *app.Config) error
|
||||
ProcessRemove(origin string, id app.ProcessID) error
|
||||
ProcessUpdate(origin string, id app.ProcessID, config *app.Config) error
|
||||
ProcessSetCommand(origin string, id app.ProcessID, order string) error
|
||||
ProcessSetMetadata(origin string, id app.ProcessID, key string, data interface{}) error
|
||||
ProcessGetMetadata(origin string, id app.ProcessID, key string) (interface{}, error)
|
||||
ProcessesRelocate(origin string, relocations map[app.ProcessID]string) error
|
||||
|
||||
IAM(superuser iamidentity.User, jwtRealm, jwtSecret string) (iam.IAM, error)
|
||||
ListIdentities() (time.Time, []iamidentity.User)
|
||||
ListIdentity(name string) (time.Time, iamidentity.User, error)
|
||||
ListPolicies() (time.Time, []iamaccess.Policy)
|
||||
ListUserPolicies(name string) (time.Time, []iamaccess.Policy)
|
||||
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
|
||||
IAMIdentityAdd(origin string, identity iamidentity.User) error
|
||||
IAMIdentityUpdate(origin, name string, identity iamidentity.User) error
|
||||
IAMIdentityRemove(origin string, name string) error
|
||||
IAMPoliciesSet(origin, name string, policies []iamaccess.Policy) error
|
||||
|
||||
CreateLock(origin string, name string, validUntil time.Time) (*kvs.Lock, error)
|
||||
DeleteLock(origin string, name string) error
|
||||
ListLocks() map[string]time.Time
|
||||
LockCreate(origin string, name string, validUntil time.Time) (*kvs.Lock, error)
|
||||
LockDelete(origin string, name string) error
|
||||
|
||||
SetKV(origin, key, value string) error
|
||||
UnsetKV(origin, key string) error
|
||||
GetKV(origin, key string, stale bool) (string, time.Time, error)
|
||||
ListKV(prefix string) map[string]store.Value
|
||||
KVSet(origin, key, value string) error
|
||||
KVUnset(origin, key string) error
|
||||
KVGet(origin, key string, stale bool) (string, time.Time, error)
|
||||
|
||||
ListNodes() map[string]store.Node
|
||||
SetNodeState(origin, id, state string) error
|
||||
NodeSetState(origin, id, state string) error
|
||||
|
||||
ProxyReader() proxy.ProxyReader
|
||||
Manager() *clusternode.Manager
|
||||
CertManager() autocert.Manager
|
||||
Store() store.Store
|
||||
|
||||
Resources() (resources.Info, error)
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
@ -122,6 +112,7 @@ type Config struct {
|
||||
CoreSkills skills.Skills
|
||||
|
||||
IPLimiter net.IPLimiter
|
||||
Resources resources.Resources
|
||||
Logger log.Logger
|
||||
|
||||
Debug DebugConfig
|
||||
@ -154,20 +145,16 @@ type cluster struct {
|
||||
nodeRecoverTimeout time.Duration
|
||||
emergencyLeaderTimeout time.Duration
|
||||
|
||||
forwarder forwarder.Forwarder
|
||||
forwarder *forwarder.Forwarder
|
||||
api API
|
||||
proxy proxy.Proxy
|
||||
manager *clusternode.Manager
|
||||
|
||||
config *config.Config
|
||||
skills skills.Skills
|
||||
coreAddress string
|
||||
|
||||
isDegraded bool
|
||||
isDegradedErr error
|
||||
isCoreDegraded bool
|
||||
isCoreDegradedErr error
|
||||
hostnames []string
|
||||
stateLock sync.RWMutex
|
||||
hostnames []string
|
||||
stateLock sync.RWMutex
|
||||
|
||||
isRaftLeader bool
|
||||
hasRaftLeader bool
|
||||
@ -178,14 +165,13 @@ type cluster struct {
|
||||
clusterKVS ClusterKVS
|
||||
certManager autocert.Manager
|
||||
|
||||
nodes map[string]clusternode.Node
|
||||
nodesLock sync.RWMutex
|
||||
|
||||
barrier map[string]bool
|
||||
barrierLock sync.RWMutex
|
||||
|
||||
limiter net.IPLimiter
|
||||
|
||||
resources resources.Resources
|
||||
|
||||
debugDisableFFmpegCheck bool
|
||||
}
|
||||
|
||||
@ -209,20 +195,15 @@ func New(config Config) (Cluster, error) {
|
||||
nodeRecoverTimeout: config.NodeRecoverTimeout,
|
||||
emergencyLeaderTimeout: config.EmergencyLeaderTimeout,
|
||||
|
||||
isDegraded: true,
|
||||
isDegradedErr: fmt.Errorf("cluster not yet startet"),
|
||||
|
||||
isCoreDegraded: true,
|
||||
isCoreDegradedErr: fmt.Errorf("cluster not yet started"),
|
||||
|
||||
config: config.CoreConfig,
|
||||
skills: config.CoreSkills,
|
||||
nodes: map[string]clusternode.Node{},
|
||||
|
||||
barrier: map[string]bool{},
|
||||
|
||||
limiter: config.IPLimiter,
|
||||
|
||||
resources: config.Resources,
|
||||
|
||||
debugDisableFFmpegCheck: config.Debug.DisableFFmpegCheck,
|
||||
}
|
||||
|
||||
@ -301,7 +282,7 @@ func New(config Config) (Cluster, error) {
|
||||
|
||||
c.api = api
|
||||
|
||||
nodeproxy, err := proxy.NewProxy(proxy.ProxyConfig{
|
||||
nodemanager, err := clusternode.NewManager(clusternode.ManagerConfig{
|
||||
ID: c.nodeID,
|
||||
Logger: c.logger.WithField("logname", "proxy"),
|
||||
})
|
||||
@ -310,13 +291,9 @@ func New(config Config) (Cluster, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func(nodeproxy proxy.Proxy) {
|
||||
nodeproxy.Start()
|
||||
}(nodeproxy)
|
||||
c.manager = nodemanager
|
||||
|
||||
c.proxy = nodeproxy
|
||||
|
||||
if forwarder, err := forwarder.New(forwarder.ForwarderConfig{
|
||||
if forwarder, err := forwarder.New(forwarder.Config{
|
||||
ID: c.nodeID,
|
||||
Logger: c.logger.WithField("logname", "forwarder"),
|
||||
}); err != nil {
|
||||
@ -475,12 +452,12 @@ func (c *cluster) setup(ctx context.Context) error {
|
||||
c.logger.Info().Log("Waiting for cluster to become operational ...")
|
||||
|
||||
for {
|
||||
ok, err := c.IsClusterDegraded()
|
||||
ok, err := c.isClusterOperational()
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
c.logger.Warn().WithError(err).Log("Cluster is in degraded state")
|
||||
c.logger.Warn().WithError(err).Log("Waiting for all nodes to be registered")
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@ -644,15 +621,9 @@ func (c *cluster) Shutdown() error {
|
||||
c.shutdown = true
|
||||
close(c.shutdownCh)
|
||||
|
||||
for id, node := range c.nodes {
|
||||
node.Stop()
|
||||
if c.proxy != nil {
|
||||
c.proxy.RemoveNode(id)
|
||||
}
|
||||
}
|
||||
|
||||
if c.proxy != nil {
|
||||
c.proxy.Stop()
|
||||
if c.manager != nil {
|
||||
c.manager.NodesClear()
|
||||
c.manager = nil
|
||||
}
|
||||
|
||||
if c.api != nil {
|
||||
@ -677,56 +648,35 @@ func (c *cluster) IsRaftLeader() bool {
|
||||
return c.isRaftLeader
|
||||
}
|
||||
|
||||
func (c *cluster) IsDegraded() (bool, error) {
|
||||
c.stateLock.RLock()
|
||||
defer c.stateLock.RUnlock()
|
||||
func (c *cluster) HasRaftLeader() bool {
|
||||
c.leaderLock.Lock()
|
||||
defer c.leaderLock.Unlock()
|
||||
|
||||
if c.isDegraded {
|
||||
return c.isDegraded, c.isDegradedErr
|
||||
}
|
||||
|
||||
return c.isCoreDegraded, c.isCoreDegradedErr
|
||||
return c.hasRaftLeader
|
||||
}
|
||||
|
||||
func (c *cluster) IsClusterDegraded() (bool, error) {
|
||||
c.stateLock.Lock()
|
||||
isDegraded, isDegradedErr := c.isDegraded, c.isDegradedErr
|
||||
c.stateLock.Unlock()
|
||||
|
||||
if isDegraded {
|
||||
return isDegraded, isDegradedErr
|
||||
}
|
||||
|
||||
func (c *cluster) isClusterOperational() (bool, error) {
|
||||
servers, err := c.raft.Servers()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
c.nodesLock.RLock()
|
||||
nodes := len(c.nodes)
|
||||
c.nodesLock.RUnlock()
|
||||
serverCount := len(servers)
|
||||
nodeCount := c.manager.NodeCount()
|
||||
|
||||
if len(servers) != nodes {
|
||||
return true, fmt.Errorf("not all nodes are connected")
|
||||
if serverCount != nodeCount {
|
||||
return true, fmt.Errorf("%d of %d nodes registered", nodeCount, serverCount)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *cluster) Leave(origin, id string) error {
|
||||
if ok, _ := c.IsDegraded(); ok {
|
||||
return ErrDegraded
|
||||
}
|
||||
|
||||
if len(id) == 0 {
|
||||
id = c.nodeID
|
||||
}
|
||||
|
||||
c.nodesLock.RLock()
|
||||
_, hasNode := c.nodes[id]
|
||||
c.nodesLock.RUnlock()
|
||||
|
||||
if !hasNode {
|
||||
if !c.manager.NodeHasNode(id) {
|
||||
return ErrUnknownNode
|
||||
}
|
||||
|
||||
@ -860,10 +810,6 @@ func (c *cluster) Leave(origin, id string) error {
|
||||
}
|
||||
|
||||
func (c *cluster) Join(origin, id, raftAddress, peerAddress string) error {
|
||||
if ok, _ := c.IsDegraded(); ok {
|
||||
return ErrDegraded
|
||||
}
|
||||
|
||||
if !c.IsRaftLeader() {
|
||||
c.logger.Debug().Log("Not leader, forwarding to leader")
|
||||
return c.forwarder.Join(origin, id, raftAddress, peerAddress)
|
||||
@ -923,10 +869,6 @@ func (c *cluster) Join(origin, id, raftAddress, peerAddress string) error {
|
||||
}
|
||||
|
||||
func (c *cluster) TransferLeadership(origin, id string) error {
|
||||
if ok, _ := c.IsDegraded(); ok {
|
||||
return ErrDegraded
|
||||
}
|
||||
|
||||
if !c.IsRaftLeader() {
|
||||
c.logger.Debug().Log("Not leader, forwarding to leader")
|
||||
return c.forwarder.TransferLeadership(origin, id)
|
||||
@ -936,10 +878,6 @@ func (c *cluster) TransferLeadership(origin, id string) error {
|
||||
}
|
||||
|
||||
func (c *cluster) Snapshot(origin string) (io.ReadCloser, error) {
|
||||
if ok, _ := c.IsDegraded(); ok {
|
||||
return nil, ErrDegraded
|
||||
}
|
||||
|
||||
if !c.IsRaftLeader() {
|
||||
c.logger.Debug().Log("Not leader, forwarding to leader")
|
||||
return c.forwarder.Snapshot(origin)
|
||||
@ -962,18 +900,15 @@ func (c *cluster) trackNodeChanges() {
|
||||
continue
|
||||
}
|
||||
|
||||
c.nodesLock.Lock()
|
||||
|
||||
removeNodes := map[string]struct{}{}
|
||||
for id := range c.nodes {
|
||||
for _, id := range c.manager.NodeIDs() {
|
||||
removeNodes[id] = struct{}{}
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
id := server.ID
|
||||
|
||||
_, ok := c.nodes[id]
|
||||
if !ok {
|
||||
if !c.manager.NodeHasNode(id) {
|
||||
logger := c.logger.WithFields(log.Fields{
|
||||
"id": server.ID,
|
||||
"address": server.Address,
|
||||
@ -993,18 +928,12 @@ func (c *cluster) trackNodeChanges() {
|
||||
}),
|
||||
})
|
||||
|
||||
if err := verifyClusterVersion(node.Version()); err != nil {
|
||||
logger.Warn().Log("Version mismatch. Cluster will end up in degraded mode")
|
||||
}
|
||||
|
||||
if _, err := c.proxy.AddNode(id, node.Proxy()); err != nil {
|
||||
if _, err := c.manager.NodeAdd(id, node); err != nil {
|
||||
logger.Warn().WithError(err).Log("Adding node")
|
||||
node.Stop()
|
||||
continue
|
||||
}
|
||||
|
||||
c.nodes[id] = node
|
||||
|
||||
ips := node.IPs()
|
||||
for _, ip := range ips {
|
||||
c.limiter.AddBlock(ip)
|
||||
@ -1015,57 +944,21 @@ func (c *cluster) trackNodeChanges() {
|
||||
}
|
||||
|
||||
for id := range removeNodes {
|
||||
node, ok := c.nodes[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
c.proxy.RemoveNode(id)
|
||||
node.Stop()
|
||||
|
||||
ips := node.IPs()
|
||||
for _, ip := range ips {
|
||||
c.limiter.RemoveBlock(ip)
|
||||
}
|
||||
|
||||
delete(c.nodes, id)
|
||||
/*
|
||||
if id == c.nodeID {
|
||||
c.logger.Warn().WithField("id", id).Log("This node left the cluster. Shutting down.")
|
||||
// We got removed from the cluster, shutdown
|
||||
c.Shutdown()
|
||||
if node, err := c.manager.NodeRemove(id); err != nil {
|
||||
ips := node.IPs()
|
||||
for _, ip := range ips {
|
||||
c.limiter.RemoveBlock(ip)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
c.nodesLock.Unlock()
|
||||
|
||||
// Put the cluster in "degraded" mode in case there's a mismatch in expected values
|
||||
hostnames, err := c.checkClusterNodes()
|
||||
c.manager.NodeCheckCompatibility(c.debugDisableFFmpegCheck)
|
||||
|
||||
hostnames, _ := c.manager.GetHostnames(true)
|
||||
|
||||
c.stateLock.Lock()
|
||||
if err != nil {
|
||||
c.isDegraded = true
|
||||
c.isDegradedErr = err
|
||||
c.hostnames = []string{}
|
||||
} else {
|
||||
c.isDegraded = false
|
||||
c.isDegradedErr = nil
|
||||
c.hostnames = hostnames
|
||||
}
|
||||
c.stateLock.Unlock()
|
||||
|
||||
// Put the cluster in "coreDegraded" mode in case there's a mismatch in expected values
|
||||
err = c.checkClusterCoreNodes()
|
||||
|
||||
c.stateLock.Lock()
|
||||
if err != nil {
|
||||
c.isCoreDegraded = true
|
||||
c.isCoreDegradedErr = err
|
||||
} else {
|
||||
c.isCoreDegraded = false
|
||||
c.isCoreDegradedErr = nil
|
||||
}
|
||||
c.hostnames = hostnames
|
||||
c.stateLock.Unlock()
|
||||
case <-c.shutdownCh:
|
||||
return
|
||||
@ -1073,240 +966,15 @@ func (c *cluster) trackNodeChanges() {
|
||||
}
|
||||
}
|
||||
|
||||
// checkClusterNodes returns a list of hostnames that are configured on all nodes. The
|
||||
// returned list will not contain any duplicates. An error is returned in case the
|
||||
// node is not compatible.
|
||||
func (c *cluster) checkClusterNodes() ([]string, error) {
|
||||
hostnames := map[string]int{}
|
||||
|
||||
c.nodesLock.RLock()
|
||||
defer c.nodesLock.RUnlock()
|
||||
|
||||
for id, node := range c.nodes {
|
||||
if status, err := node.Status(); status == "offline" {
|
||||
return nil, fmt.Errorf("node %s is offline: %w", id, err)
|
||||
}
|
||||
|
||||
version := node.Version()
|
||||
if err := verifyClusterVersion(version); err != nil {
|
||||
return nil, fmt.Errorf("node %s has a different cluster version: %s: %w", id, version, err)
|
||||
}
|
||||
|
||||
config, err := node.CoreConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("node %s has no configuration available: %w", id, err)
|
||||
}
|
||||
if err := verifyClusterConfig(c.config, config); err != nil {
|
||||
return nil, fmt.Errorf("node %s has a different configuration: %w", id, err)
|
||||
}
|
||||
|
||||
if !c.debugDisableFFmpegCheck {
|
||||
skills, err := node.CoreSkills()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("node %s has no FFmpeg skills available: %w", id, err)
|
||||
}
|
||||
if !c.skills.Equal(skills) {
|
||||
return nil, fmt.Errorf("node %s has mismatching FFmpeg skills", id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range config.Host.Name {
|
||||
hostnames[name]++
|
||||
}
|
||||
}
|
||||
|
||||
names := []string{}
|
||||
|
||||
for key, value := range hostnames {
|
||||
if value != len(c.nodes) {
|
||||
continue
|
||||
}
|
||||
|
||||
names = append(names, key)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (c *cluster) checkClusterCoreNodes() error {
|
||||
c.nodesLock.RLock()
|
||||
defer c.nodesLock.RUnlock()
|
||||
|
||||
for id, node := range c.nodes {
|
||||
if status, err := node.CoreStatus(); status == "offline" {
|
||||
return fmt.Errorf("node %s core is offline: %w", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getClusterHostnames return a list of all hostnames configured on all nodes. The
|
||||
// returned list will not contain any duplicates.
|
||||
func (c *cluster) getClusterHostnames() ([]string, error) {
|
||||
hostnames := map[string]struct{}{}
|
||||
|
||||
c.nodesLock.RLock()
|
||||
defer c.nodesLock.RUnlock()
|
||||
|
||||
for id, node := range c.nodes {
|
||||
config, err := node.CoreConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("node %s has no configuration available: %w", id, err)
|
||||
}
|
||||
|
||||
for _, name := range config.Host.Name {
|
||||
hostnames[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
names := []string{}
|
||||
|
||||
for key := range hostnames {
|
||||
names = append(names, key)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
return names, nil
|
||||
return c.manager.GetHostnames(false)
|
||||
}
|
||||
|
||||
// getClusterBarrier returns whether all nodes are currently on the same barrier.
|
||||
func (c *cluster) getClusterBarrier(name string) (bool, error) {
|
||||
c.nodesLock.RLock()
|
||||
defer c.nodesLock.RUnlock()
|
||||
|
||||
for _, node := range c.nodes {
|
||||
ok, err := node.Barrier(name)
|
||||
if !ok {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func verifyClusterVersion(v string) error {
|
||||
version, err := ParseClusterVersion(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing version %s: %w", v, err)
|
||||
}
|
||||
|
||||
if !Version.Equal(version) {
|
||||
return fmt.Errorf("version %s not equal to my version %s", version.String(), Version.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyClusterConfig(local, remote *config.Config) error {
|
||||
if local == nil || remote == nil {
|
||||
return fmt.Errorf("config is not available")
|
||||
}
|
||||
|
||||
if local.Cluster.Enable != remote.Cluster.Enable {
|
||||
return fmt.Errorf("cluster.enable is different")
|
||||
}
|
||||
|
||||
if local.Cluster.ID != remote.Cluster.ID {
|
||||
return fmt.Errorf("cluster.id is different")
|
||||
}
|
||||
|
||||
if local.Cluster.SyncInterval != remote.Cluster.SyncInterval {
|
||||
return fmt.Errorf("cluster.sync_interval_sec is different")
|
||||
}
|
||||
|
||||
if local.Cluster.NodeRecoverTimeout != remote.Cluster.NodeRecoverTimeout {
|
||||
return fmt.Errorf("cluster.node_recover_timeout_sec is different")
|
||||
}
|
||||
|
||||
if local.Cluster.EmergencyLeaderTimeout != remote.Cluster.EmergencyLeaderTimeout {
|
||||
return fmt.Errorf("cluster.emergency_leader_timeout_sec is different")
|
||||
}
|
||||
|
||||
if local.Cluster.Debug.DisableFFmpegCheck != remote.Cluster.Debug.DisableFFmpegCheck {
|
||||
return fmt.Errorf("cluster.debug.disable_ffmpeg_check is different")
|
||||
}
|
||||
|
||||
if !local.API.Auth.Enable {
|
||||
return fmt.Errorf("api.auth.enable must be true")
|
||||
}
|
||||
|
||||
if local.API.Auth.Enable != remote.API.Auth.Enable {
|
||||
return fmt.Errorf("api.auth.enable is different")
|
||||
}
|
||||
|
||||
if local.API.Auth.Username != remote.API.Auth.Username {
|
||||
return fmt.Errorf("api.auth.username is different")
|
||||
}
|
||||
|
||||
if local.API.Auth.Password != remote.API.Auth.Password {
|
||||
return fmt.Errorf("api.auth.password is different")
|
||||
}
|
||||
|
||||
if local.API.Auth.JWT.Secret != remote.API.Auth.JWT.Secret {
|
||||
return fmt.Errorf("api.auth.jwt.secret is different")
|
||||
}
|
||||
|
||||
if local.RTMP.Enable != remote.RTMP.Enable {
|
||||
return fmt.Errorf("rtmp.enable is different")
|
||||
}
|
||||
|
||||
if local.RTMP.Enable {
|
||||
if local.RTMP.App != remote.RTMP.App {
|
||||
return fmt.Errorf("rtmp.app is different")
|
||||
}
|
||||
}
|
||||
|
||||
if local.SRT.Enable != remote.SRT.Enable {
|
||||
return fmt.Errorf("srt.enable is different")
|
||||
}
|
||||
|
||||
if local.SRT.Enable {
|
||||
if local.SRT.Passphrase != remote.SRT.Passphrase {
|
||||
return fmt.Errorf("srt.passphrase is different")
|
||||
}
|
||||
}
|
||||
|
||||
if local.Resources.MaxCPUUsage == 0 || remote.Resources.MaxCPUUsage == 0 {
|
||||
return fmt.Errorf("resources.max_cpu_usage must be defined")
|
||||
}
|
||||
|
||||
if local.Resources.MaxMemoryUsage == 0 || remote.Resources.MaxMemoryUsage == 0 {
|
||||
return fmt.Errorf("resources.max_memory_usage must be defined")
|
||||
}
|
||||
|
||||
if local.TLS.Enable != remote.TLS.Enable {
|
||||
return fmt.Errorf("tls.enable is different")
|
||||
}
|
||||
|
||||
if local.TLS.Enable {
|
||||
if local.TLS.Auto != remote.TLS.Auto {
|
||||
return fmt.Errorf("tls.auto is different")
|
||||
}
|
||||
|
||||
if len(local.Host.Name) == 0 || len(remote.Host.Name) == 0 {
|
||||
return fmt.Errorf("host.name must be set")
|
||||
}
|
||||
|
||||
if local.TLS.Auto {
|
||||
if local.TLS.Email != remote.TLS.Email {
|
||||
return fmt.Errorf("tls.email is different")
|
||||
}
|
||||
|
||||
if local.TLS.Staging != remote.TLS.Staging {
|
||||
return fmt.Errorf("tls.staging is different")
|
||||
}
|
||||
|
||||
if local.TLS.Secret != remote.TLS.Secret {
|
||||
return fmt.Errorf("tls.secret is different")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return c.manager.Barrier(name)
|
||||
}
|
||||
|
||||
// trackLeaderChanges registers an Observer with raft in order to receive updates
|
||||
@ -1370,169 +1038,6 @@ func (c *cluster) applyCommand(cmd *store.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
Status 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
|
||||
Status 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
|
||||
Status string
|
||||
Raft ClusterRaft
|
||||
Nodes []ClusterNode
|
||||
Version ClusterVersion
|
||||
Degraded bool
|
||||
DegradedErr error
|
||||
}
|
||||
|
||||
func (c *cluster) About() (ClusterAbout, error) {
|
||||
degraded, degradedErr := c.IsDegraded()
|
||||
|
||||
about := ClusterAbout{
|
||||
ID: c.id,
|
||||
Leader: ClusterAboutLeader{},
|
||||
Status: "online",
|
||||
Version: Version,
|
||||
Degraded: degraded,
|
||||
DegradedErr: degradedErr,
|
||||
}
|
||||
|
||||
if about.Degraded {
|
||||
about.Status = "offline"
|
||||
}
|
||||
|
||||
c.stateLock.RLock()
|
||||
about.Domains = slices.Copy(c.hostnames)
|
||||
c.stateLock.RUnlock()
|
||||
|
||||
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()
|
||||
|
||||
c.nodesLock.RLock()
|
||||
for id, node := range c.nodes {
|
||||
nodeAbout := node.About()
|
||||
|
||||
node := ClusterNode{
|
||||
ID: id,
|
||||
Name: nodeAbout.Name,
|
||||
Version: nodeAbout.Version,
|
||||
Status: nodeAbout.Status,
|
||||
Error: nodeAbout.Error,
|
||||
Address: nodeAbout.Address,
|
||||
LastContact: nodeAbout.LastContact,
|
||||
Latency: nodeAbout.Latency,
|
||||
CreatedAt: nodeAbout.Core.CreatedAt,
|
||||
Uptime: nodeAbout.Core.Uptime,
|
||||
Core: ClusterNodeCore{
|
||||
Address: nodeAbout.Core.Address,
|
||||
Status: nodeAbout.Core.Status,
|
||||
Error: nodeAbout.Core.Error,
|
||||
LastContact: nodeAbout.Core.LastContact,
|
||||
Latency: nodeAbout.Core.Latency,
|
||||
Version: nodeAbout.Core.Version,
|
||||
},
|
||||
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[id]; ok {
|
||||
node.Voter = s.Voter
|
||||
node.Leader = s.Leader
|
||||
}
|
||||
|
||||
if storeNode, hasStoreNode := storeNodes[id]; hasStoreNode {
|
||||
if storeNode.State == "maintenance" {
|
||||
node.Status = storeNode.State
|
||||
}
|
||||
}
|
||||
|
||||
about.Nodes = append(about.Nodes, node)
|
||||
}
|
||||
c.nodesLock.RUnlock()
|
||||
|
||||
return about, nil
|
||||
}
|
||||
|
||||
func (c *cluster) sentinel() {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
@ -1570,6 +1075,26 @@ func (c *cluster) sentinel() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cluster) ProxyReader() proxy.ProxyReader {
|
||||
return c.proxy.Reader()
|
||||
func (c *cluster) Resources() (resources.Info, error) {
|
||||
if c.resources == nil {
|
||||
return resources.Info{}, fmt.Errorf("resource information is not available")
|
||||
}
|
||||
|
||||
return c.resources.Info(), nil
|
||||
}
|
||||
|
||||
func (c *cluster) Manager() *clusternode.Manager {
|
||||
if c.manager == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.manager
|
||||
}
|
||||
|
||||
func (c *cluster) Store() store.Store {
|
||||
if c.store == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return c.store
|
||||
}
|
||||
|
||||
21
cluster/forwarder/errors.go
Normal file
21
cluster/forwarder/errors.go
Normal 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
|
||||
}
|
||||
@ -7,66 +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
|
||||
RelocateProcesses(origin string, relocations map[app.ProcessID]string) 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)
|
||||
|
||||
SetNodeState(origin, nodeid, state string) 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()
|
||||
@ -85,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()
|
||||
|
||||
@ -93,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{
|
||||
@ -112,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
|
||||
@ -128,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
|
||||
@ -142,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
|
||||
@ -156,248 +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) RelocateProcesses(origin string, relocations map[app.ProcessID]string) error {
|
||||
if origin == "" {
|
||||
origin = f.id
|
||||
}
|
||||
|
||||
r := apiclient.RelocateProcessesRequest{
|
||||
Map: relocations,
|
||||
}
|
||||
|
||||
f.lock.RLock()
|
||||
client := f.client
|
||||
f.lock.RUnlock()
|
||||
|
||||
return client.RelocateProcesses(origin, r)
|
||||
}
|
||||
|
||||
func (f *forwarder) AddIdentity(origin string, identity iamidentity.User) error {
|
||||
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)
|
||||
}
|
||||
|
||||
func (f *forwarder) SetNodeState(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 client.SetNodeState(origin, nodeid, r)
|
||||
}
|
||||
|
||||
67
cluster/forwarder/iam.go
Normal file
67
cluster/forwarder/iam.go
Normal 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
50
cluster/forwarder/kvs.go
Normal 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
36
cluster/forwarder/lock.go
Normal 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
21
cluster/forwarder/node.go
Normal 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))
|
||||
}
|
||||
98
cluster/forwarder/process.go
Normal file
98
cluster/forwarder/process.go
Normal 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))
|
||||
}
|
||||
@ -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{
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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{}{}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
1064
cluster/leader.go
1064
cluster/leader.go
File diff suppressed because it is too large
Load Diff
185
cluster/leader_rebalance.go
Normal file
185
cluster/leader_rebalance.go
Normal 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
206
cluster/leader_relocate.go
Normal 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
|
||||
}
|
||||
377
cluster/leader_synchronize.go
Normal file
377
cluster/leader_synchronize.go
Normal 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
@ -7,12 +7,12 @@ import (
|
||||
)
|
||||
|
||||
func (c *cluster) ListNodes() map[string]store.Node {
|
||||
return c.store.ListNodes()
|
||||
return c.store.NodeList()
|
||||
}
|
||||
|
||||
var ErrUnsupportedNodeState = errors.New("unsupported node state")
|
||||
|
||||
func (c *cluster) SetNodeState(origin, id, state string) error {
|
||||
func (c *cluster) NodeSetState(origin, id, state string) error {
|
||||
switch state {
|
||||
case "online":
|
||||
case "maintenance":
|
||||
@ -22,7 +22,7 @@ func (c *cluster) SetNodeState(origin, id, state string) error {
|
||||
}
|
||||
|
||||
if !c.IsRaftLeader() {
|
||||
return c.forwarder.SetNodeState(origin, id, state)
|
||||
return c.forwarder.NodeSetState(origin, id, state)
|
||||
}
|
||||
|
||||
cmd := &store.Command{
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package proxy
|
||||
package node
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -1,4 +1,4 @@
|
||||
package proxy
|
||||
package node
|
||||
|
||||
import (
|
||||
"testing"
|
||||
806
cluster/node/core.go
Normal file
806
cluster/node/core.go
Normal file
@ -0,0 +1,806 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/datarhei/core/v16/config"
|
||||
"github.com/datarhei/core/v16/http/api"
|
||||
"github.com/datarhei/core/v16/http/client"
|
||||
"github.com/datarhei/core/v16/log"
|
||||
"github.com/datarhei/core/v16/restream/app"
|
||||
)
|
||||
|
||||
type Core struct {
|
||||
id string
|
||||
|
||||
client client.RestClient
|
||||
clientErr error
|
||||
|
||||
lock sync.RWMutex
|
||||
|
||||
cancel context.CancelFunc
|
||||
|
||||
address string
|
||||
config *config.Config
|
||||
|
||||
secure bool
|
||||
httpAddress *url.URL
|
||||
hasRTMP bool
|
||||
rtmpAddress *url.URL
|
||||
hasSRT bool
|
||||
srtAddress *url.URL
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
var ErrNoPeer = errors.New("not connected to the core API: client not available")
|
||||
|
||||
func NewCore(id string, logger log.Logger) *Core {
|
||||
core := &Core{
|
||||
id: id,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
if core.logger == nil {
|
||||
core.logger = log.New("")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
core.cancel = cancel
|
||||
|
||||
go core.reconnect(ctx, time.Second)
|
||||
|
||||
return core
|
||||
}
|
||||
|
||||
func (n *Core) SetEssentials(address string, config *config.Config) {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if address != n.address {
|
||||
n.address = address
|
||||
n.client = nil // force reconnet
|
||||
}
|
||||
|
||||
if n.config == nil && config != nil {
|
||||
n.config = config
|
||||
n.client = nil // force reconnect
|
||||
}
|
||||
|
||||
if n.config.UpdatedAt != config.UpdatedAt {
|
||||
n.config = config
|
||||
n.client = nil // force reconnect
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Core) reconnect(ctx context.Context, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
err := n.connect()
|
||||
|
||||
n.lock.Lock()
|
||||
n.clientErr = err
|
||||
n.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Core) Stop() {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if n.cancel == nil {
|
||||
return
|
||||
}
|
||||
|
||||
n.cancel()
|
||||
n.cancel = nil
|
||||
}
|
||||
|
||||
func (n *Core) Reconnect() {
|
||||
n.lock.Lock()
|
||||
defer n.lock.Unlock()
|
||||
|
||||
n.client = nil
|
||||
}
|
||||
|
||||
func (n *Core) connect() error {
|
||||
n.lock.Lock()
|
||||
|
||||
if n.client != nil {
|
||||
n.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(n.address) == 0 {
|
||||
n.lock.Unlock()
|
||||
return fmt.Errorf("no address provided")
|
||||
}
|
||||
|
||||
if n.config == nil {
|
||||
n.lock.Unlock()
|
||||
return fmt.Errorf("config not available")
|
||||
}
|
||||
|
||||
address := n.address
|
||||
config := n.config.Clone()
|
||||
|
||||
n.lock.Unlock()
|
||||
|
||||
u, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid address (%s): %w", address, err)
|
||||
}
|
||||
|
||||
secure := strings.HasPrefix(config.Address, "https://")
|
||||
|
||||
nodehost, _, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid address (%s): %w", u.Host, err)
|
||||
}
|
||||
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.MaxIdleConns = 10
|
||||
tr.IdleConnTimeout = 30 * time.Second
|
||||
|
||||
client, err := client.New(client.Config{
|
||||
Address: u.String(),
|
||||
Client: &http.Client{
|
||||
Transport: tr,
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating client failed (%s): %w", address, err)
|
||||
}
|
||||
|
||||
httpAddress := u
|
||||
hasRTMP := false
|
||||
rtmpAddress := &url.URL{}
|
||||
|
||||
if config.RTMP.Enable {
|
||||
hasRTMP = true
|
||||
rtmpAddress.Scheme = "rtmp"
|
||||
|
||||
isHostIP := net.ParseIP(nodehost) != nil
|
||||
|
||||
address := config.RTMP.Address
|
||||
if secure && config.RTMP.EnableTLS && !isHostIP {
|
||||
address = config.RTMP.AddressTLS
|
||||
rtmpAddress.Scheme = "rtmps"
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid rtmp address '%s': %w", address, err)
|
||||
}
|
||||
|
||||
if len(host) == 0 {
|
||||
rtmpAddress.Host = net.JoinHostPort(nodehost, port)
|
||||
} else {
|
||||
rtmpAddress.Host = net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
rtmpAddress = rtmpAddress.JoinPath(n.config.RTMP.App)
|
||||
}
|
||||
|
||||
hasSRT := false
|
||||
srtAddress := &url.URL{}
|
||||
|
||||
if config.SRT.Enable {
|
||||
hasSRT = true
|
||||
srtAddress.Scheme = "srt"
|
||||
|
||||
host, port, err := net.SplitHostPort(config.SRT.Address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid srt address '%s': %w", config.SRT.Address, err)
|
||||
}
|
||||
|
||||
if len(host) == 0 {
|
||||
srtAddress.Host = net.JoinHostPort(nodehost, port)
|
||||
} else {
|
||||
srtAddress.Host = net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
v := url.Values{}
|
||||
|
||||
v.Set("mode", "caller")
|
||||
if len(config.SRT.Passphrase) != 0 {
|
||||
v.Set("passphrase", config.SRT.Passphrase)
|
||||
}
|
||||
|
||||
srtAddress.RawQuery = v.Encode()
|
||||
}
|
||||
|
||||
n.lock.Lock()
|
||||
|
||||
n.secure = secure
|
||||
n.httpAddress = httpAddress
|
||||
n.hasRTMP = hasRTMP
|
||||
n.rtmpAddress = rtmpAddress
|
||||
n.hasSRT = hasSRT
|
||||
n.srtAddress = srtAddress
|
||||
n.client = client
|
||||
|
||||
n.lock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CoreAbout struct {
|
||||
ID string
|
||||
Name string
|
||||
Address string
|
||||
State string
|
||||
Error error
|
||||
CreatedAt time.Time
|
||||
Uptime time.Duration
|
||||
LastContact time.Time
|
||||
Latency time.Duration
|
||||
Version CoreVersion
|
||||
}
|
||||
|
||||
type CoreVersion struct {
|
||||
Number string
|
||||
Commit string
|
||||
Branch string
|
||||
Build time.Time
|
||||
Arch string
|
||||
Compiler string
|
||||
}
|
||||
|
||||
func (n *Core) About() (CoreAbout, error) {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return CoreAbout{}, ErrNoPeer
|
||||
}
|
||||
|
||||
about, err := client.About(false)
|
||||
if err != nil {
|
||||
return CoreAbout{}, err
|
||||
}
|
||||
|
||||
cabout := CoreAbout{
|
||||
ID: about.ID,
|
||||
Name: about.Name,
|
||||
Address: n.address,
|
||||
Error: n.clientErr,
|
||||
Version: CoreVersion{
|
||||
Number: about.Version.Number,
|
||||
Commit: about.Version.Commit,
|
||||
Branch: about.Version.Branch,
|
||||
Arch: about.Version.Arch,
|
||||
Compiler: about.Version.Compiler,
|
||||
},
|
||||
}
|
||||
|
||||
createdAt, err := time.Parse(time.RFC3339, about.CreatedAt)
|
||||
if err != nil {
|
||||
createdAt = time.Now()
|
||||
}
|
||||
|
||||
cabout.CreatedAt = createdAt
|
||||
cabout.Uptime = time.Since(createdAt)
|
||||
|
||||
build, err := time.Parse(time.RFC3339, about.Version.Build)
|
||||
if err != nil {
|
||||
build = time.Time{}
|
||||
}
|
||||
|
||||
cabout.Version.Build = build
|
||||
|
||||
return cabout, nil
|
||||
}
|
||||
|
||||
func (n *Core) ProcessAdd(config *app.Config, metadata map[string]interface{}) error {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return ErrNoPeer
|
||||
}
|
||||
|
||||
return client.ProcessAdd(config, metadata)
|
||||
}
|
||||
|
||||
func (n *Core) ProcessCommand(id app.ProcessID, command string) error {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return ErrNoPeer
|
||||
}
|
||||
|
||||
return client.ProcessCommand(id, command)
|
||||
}
|
||||
|
||||
func (n *Core) ProcessDelete(id app.ProcessID) error {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return ErrNoPeer
|
||||
}
|
||||
|
||||
return client.ProcessDelete(id)
|
||||
}
|
||||
|
||||
func (n *Core) ProcessUpdate(id app.ProcessID, config *app.Config, metadata map[string]interface{}) error {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return ErrNoPeer
|
||||
}
|
||||
|
||||
return client.ProcessUpdate(id, config, metadata)
|
||||
}
|
||||
|
||||
func (n *Core) ProcessProbe(id app.ProcessID) (api.Probe, error) {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
probe := api.Probe{
|
||||
Log: []string{fmt.Sprintf("the node %s where the process %s resides, is not connected", n.id, id.String())},
|
||||
}
|
||||
return probe, ErrNoPeer
|
||||
}
|
||||
|
||||
probe, err := client.ProcessProbe(id)
|
||||
|
||||
probe.Log = append([]string{fmt.Sprintf("probed on node: %s", n.id)}, probe.Log...)
|
||||
|
||||
return probe, err
|
||||
}
|
||||
|
||||
func (n *Core) ProcessProbeConfig(config *app.Config) (api.Probe, error) {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
probe := api.Probe{
|
||||
Log: []string{fmt.Sprintf("the node %s where the process config should be probed, is not connected", n.id)},
|
||||
}
|
||||
return probe, ErrNoPeer
|
||||
}
|
||||
|
||||
probe, err := client.ProcessProbeConfig(config)
|
||||
|
||||
probe.Log = append([]string{fmt.Sprintf("probed on node: %s", n.id)}, probe.Log...)
|
||||
|
||||
return probe, err
|
||||
}
|
||||
|
||||
func (n *Core) ProcessList(options client.ProcessListOptions) ([]api.Process, error) {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return nil, ErrNoPeer
|
||||
}
|
||||
|
||||
return client.ProcessList(options)
|
||||
}
|
||||
|
||||
func (n *Core) FilesystemList(storage, pattern string) ([]api.FileInfo, error) {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return nil, ErrNoPeer
|
||||
}
|
||||
|
||||
files, err := client.FilesystemList(storage, pattern, "", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range files {
|
||||
files[i].CoreID = n.id
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (n *Core) FilesystemDeleteFile(storage, path string) error {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return ErrNoPeer
|
||||
}
|
||||
|
||||
return client.FilesystemDeleteFile(storage, path)
|
||||
}
|
||||
|
||||
func (n *Core) FilesystemPutFile(storage, path string, data io.Reader) error {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return ErrNoPeer
|
||||
}
|
||||
|
||||
return client.FilesystemAddFile(storage, path, data)
|
||||
}
|
||||
|
||||
func (n *Core) FilesystemGetFileInfo(storage, path string) (int64, time.Time, error) {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return 0, time.Time{}, ErrNoPeer
|
||||
}
|
||||
|
||||
info, err := client.FilesystemList(storage, path, "", "")
|
||||
if err != nil {
|
||||
return 0, time.Time{}, fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
if len(info) != 1 {
|
||||
return 0, time.Time{}, fmt.Errorf("ambigous result")
|
||||
}
|
||||
|
||||
return info[0].Size, time.Unix(info[0].LastMod, 0), nil
|
||||
}
|
||||
|
||||
func (n *Core) FilesystemGetFile(storage, path string, offset int64) (io.ReadCloser, error) {
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return nil, ErrNoPeer
|
||||
}
|
||||
|
||||
return client.FilesystemGetFileOffset(storage, path, offset)
|
||||
}
|
||||
|
||||
type NodeFiles struct {
|
||||
ID string
|
||||
Files []string
|
||||
LastUpdate time.Time
|
||||
}
|
||||
|
||||
func (n *Core) MediaList() NodeFiles {
|
||||
files := NodeFiles{
|
||||
ID: n.id,
|
||||
Files: []string{},
|
||||
LastUpdate: time.Now(),
|
||||
}
|
||||
|
||||
errorsChan := make(chan error, 8)
|
||||
filesChan := make(chan string, 1024)
|
||||
errorList := []error{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for file := range filesChan {
|
||||
files.Files = append(files.Files, file)
|
||||
}
|
||||
|
||||
for err := range errorsChan {
|
||||
errorList = append(errorList, err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
go func(f chan<- string, e chan<- error) {
|
||||
defer wg.Done()
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
e <- ErrNoPeer
|
||||
return
|
||||
}
|
||||
|
||||
files, err := client.FilesystemList("mem", "/*", "name", "asc")
|
||||
if err != nil {
|
||||
e <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
f <- "mem:" + file.Name
|
||||
}
|
||||
}(filesChan, errorsChan)
|
||||
|
||||
go func(f chan<- string, e chan<- error) {
|
||||
defer wg.Done()
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
e <- ErrNoPeer
|
||||
return
|
||||
}
|
||||
|
||||
files, err := client.FilesystemList("disk", "/*", "name", "asc")
|
||||
if err != nil {
|
||||
e <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
f <- "disk:" + file.Name
|
||||
}
|
||||
}(filesChan, errorsChan)
|
||||
|
||||
if n.hasRTMP {
|
||||
wg.Add(1)
|
||||
|
||||
go func(f chan<- string, e chan<- error) {
|
||||
defer wg.Done()
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
e <- ErrNoPeer
|
||||
return
|
||||
}
|
||||
|
||||
files, err := client.RTMPChannels()
|
||||
if err != nil {
|
||||
e <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
f <- "rtmp:" + file.Name
|
||||
}
|
||||
}(filesChan, errorsChan)
|
||||
}
|
||||
|
||||
if n.hasSRT {
|
||||
wg.Add(1)
|
||||
|
||||
go func(f chan<- string, e chan<- error) {
|
||||
defer wg.Done()
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
e <- ErrNoPeer
|
||||
return
|
||||
}
|
||||
|
||||
files, err := client.SRTChannels()
|
||||
if err != nil {
|
||||
e <- err
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
f <- "srt:" + file.Name
|
||||
}
|
||||
}(filesChan, errorsChan)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(filesChan)
|
||||
close(errorsChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func cloneURL(src *url.URL) *url.URL {
|
||||
dst := &url.URL{
|
||||
Scheme: src.Scheme,
|
||||
Opaque: src.Opaque,
|
||||
User: nil,
|
||||
Host: src.Host,
|
||||
Path: src.Path,
|
||||
RawPath: src.RawPath,
|
||||
OmitHost: src.OmitHost,
|
||||
ForceQuery: src.ForceQuery,
|
||||
RawQuery: src.RawQuery,
|
||||
Fragment: src.Fragment,
|
||||
RawFragment: src.RawFragment,
|
||||
}
|
||||
|
||||
if src.User != nil {
|
||||
username := src.User.Username()
|
||||
password, ok := src.User.Password()
|
||||
|
||||
if ok {
|
||||
dst.User = url.UserPassword(username, password)
|
||||
} else {
|
||||
dst.User = url.User(username)
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func (n *Core) MediaGetURL(prefix, path string) (*url.URL, error) {
|
||||
var u *url.URL
|
||||
|
||||
if prefix == "mem" {
|
||||
u = cloneURL(n.httpAddress)
|
||||
u = u.JoinPath("memfs", path)
|
||||
} else if prefix == "disk" {
|
||||
u = cloneURL(n.httpAddress)
|
||||
u = u.JoinPath(path)
|
||||
} else if prefix == "rtmp" {
|
||||
u = cloneURL(n.rtmpAddress)
|
||||
u = u.JoinPath(path)
|
||||
} else if prefix == "srt" {
|
||||
u = cloneURL(n.srtAddress)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown prefix")
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (n *Core) MediaGetInfo(prefix, path string) (int64, time.Time, error) {
|
||||
if prefix == "disk" || prefix == "mem" {
|
||||
return n.FilesystemGetFileInfo(prefix, path)
|
||||
}
|
||||
|
||||
if prefix != "rtmp" && prefix != "srt" {
|
||||
return 0, time.Time{}, fmt.Errorf("unknown prefix: %s", prefix)
|
||||
}
|
||||
|
||||
n.lock.RLock()
|
||||
client := n.client
|
||||
n.lock.RUnlock()
|
||||
|
||||
if client == nil {
|
||||
return 0, time.Time{}, ErrNoPeer
|
||||
}
|
||||
|
||||
if prefix == "rtmp" {
|
||||
files, err := n.client.RTMPChannels()
|
||||
if err != nil {
|
||||
return 0, time.Time{}, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if path == file.Name {
|
||||
return 0, time.Now(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, time.Time{}, fmt.Errorf("media not found")
|
||||
}
|
||||
|
||||
if prefix == "srt" {
|
||||
files, err := n.client.SRTChannels()
|
||||
if err != nil {
|
||||
return 0, time.Time{}, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if path == file.Name {
|
||||
return 0, time.Now(), nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, time.Time{}, fmt.Errorf("media not found")
|
||||
}
|
||||
|
||||
return 0, time.Time{}, fmt.Errorf("unknown prefix: %s", prefix)
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
NodeID string
|
||||
Order string
|
||||
State string
|
||||
CPU float64 // Current CPU load of this process, 0-100*ncpu
|
||||
Mem uint64 // Currently consumed memory of this process in bytes
|
||||
Throttling bool
|
||||
Runtime time.Duration
|
||||
UpdatedAt time.Time
|
||||
Config *app.Config
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
func (n *Core) ClusterProcessList() ([]Process, error) {
|
||||
list, err := n.ProcessList(client.ProcessListOptions{
|
||||
Filter: []string{"config", "state", "metadata"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeid := n.id
|
||||
|
||||
processes := []Process{}
|
||||
|
||||
for _, p := range list {
|
||||
if p.State == nil {
|
||||
p.State = &api.ProcessState{}
|
||||
}
|
||||
|
||||
if p.Config == nil {
|
||||
p.Config = &api.ProcessConfig{}
|
||||
}
|
||||
|
||||
cpu, err := p.State.Resources.CPU.Current.Float64()
|
||||
if err != nil {
|
||||
cpu = 0
|
||||
}
|
||||
|
||||
process := Process{
|
||||
NodeID: nodeid,
|
||||
Order: p.State.Order,
|
||||
State: p.State.State,
|
||||
Mem: p.State.Resources.Memory.Current,
|
||||
CPU: cpu,
|
||||
Throttling: p.State.Resources.CPU.IsThrottling,
|
||||
Runtime: time.Duration(p.State.Runtime) * time.Second,
|
||||
UpdatedAt: time.Unix(p.UpdatedAt, 0),
|
||||
}
|
||||
|
||||
config, metadata := p.Config.Marshal()
|
||||
|
||||
process.Config = config
|
||||
process.Metadata = metadata
|
||||
|
||||
processes = append(processes, process)
|
||||
}
|
||||
|
||||
return processes, nil
|
||||
}
|
||||
601
cluster/node/manager.go
Normal file
601
cluster/node/manager.go
Normal file
@ -0,0 +1,601 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/datarhei/core/v16/http/api"
|
||||
"github.com/datarhei/core/v16/http/client"
|
||||
"github.com/datarhei/core/v16/log"
|
||||
"github.com/datarhei/core/v16/restream/app"
|
||||
)
|
||||
|
||||
type ProcessListOptions = client.ProcessListOptions
|
||||
|
||||
type ManagerConfig struct {
|
||||
ID string // ID of the node
|
||||
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
id string
|
||||
|
||||
nodes map[string]*Node // List of known nodes
|
||||
lock sync.RWMutex
|
||||
|
||||
cache *Cache[string]
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
var ErrNodeNotFound = errors.New("node not found")
|
||||
|
||||
func NewManager(config ManagerConfig) (*Manager, error) {
|
||||
p := &Manager{
|
||||
id: config.ID,
|
||||
nodes: map[string]*Node{},
|
||||
cache: NewCache[string](nil),
|
||||
logger: config.Logger,
|
||||
}
|
||||
|
||||
if p.logger == nil {
|
||||
p.logger = log.New("")
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Manager) NodeAdd(id string, node *Node) (string, error) {
|
||||
about := node.About()
|
||||
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if n, ok := p.nodes[id]; ok {
|
||||
n.Stop()
|
||||
delete(p.nodes, id)
|
||||
}
|
||||
|
||||
p.nodes[id] = node
|
||||
|
||||
p.logger.Info().WithFields(log.Fields{
|
||||
"address": about.Address,
|
||||
"name": about.Name,
|
||||
"id": id,
|
||||
}).Log("Added node")
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (p *Manager) NodeRemove(id string) (*Node, error) {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
node, ok := p.nodes[id]
|
||||
if !ok {
|
||||
return nil, ErrNodeNotFound
|
||||
}
|
||||
|
||||
node.Stop()
|
||||
|
||||
delete(p.nodes, id)
|
||||
|
||||
p.logger.Info().WithFields(log.Fields{
|
||||
"id": id,
|
||||
}).Log("Removed node")
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (p *Manager) NodesClear() {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
for _, node := range p.nodes {
|
||||
node.Stop()
|
||||
}
|
||||
|
||||
p.nodes = map[string]*Node{}
|
||||
|
||||
p.logger.Info().Log("Removed all nodes")
|
||||
}
|
||||
|
||||
func (p *Manager) NodeHasNode(id string) bool {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
_, hasNode := p.nodes[id]
|
||||
|
||||
return hasNode
|
||||
}
|
||||
|
||||
func (p *Manager) NodeIDs() []string {
|
||||
list := []string{}
|
||||
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
for id := range p.nodes {
|
||||
list = append(list, id)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (p *Manager) NodeCount() int {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
return len(p.nodes)
|
||||
}
|
||||
|
||||
func (p *Manager) NodeList() []*Node {
|
||||
list := []*Node{}
|
||||
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
for _, node := range p.nodes {
|
||||
list = append(list, node)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (p *Manager) NodeGet(id string) (*Node, error) {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
node, ok := p.nodes[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("node not found")
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (p *Manager) NodeCheckCompatibility(skipSkillsCheck bool) {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
local, hasLocal := p.nodes[p.id]
|
||||
if !hasLocal {
|
||||
local = nil
|
||||
}
|
||||
|
||||
for id, node := range p.nodes {
|
||||
if id == p.id {
|
||||
continue
|
||||
}
|
||||
|
||||
node.CheckCompatibility(local, skipSkillsCheck)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Manager) Barrier(name string) (bool, error) {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
for _, node := range p.nodes {
|
||||
ok, err := node.Barrier(name)
|
||||
if !ok {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getClusterHostnames return a list of all hostnames configured on all nodes. The
|
||||
// returned list will not contain any duplicates.
|
||||
func (p *Manager) GetHostnames(common bool) ([]string, error) {
|
||||
hostnames := map[string]int{}
|
||||
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
for id, node := range p.nodes {
|
||||
config, err := node.CoreConfig(true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("node %s has no configuration available: %w", id, err)
|
||||
}
|
||||
|
||||
for _, name := range config.Host.Name {
|
||||
hostnames[name]++
|
||||
}
|
||||
}
|
||||
|
||||
names := []string{}
|
||||
|
||||
for key, value := range hostnames {
|
||||
if common && value != len(p.nodes) {
|
||||
continue
|
||||
}
|
||||
|
||||
names = append(names, key)
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (p *Manager) MediaGetURL(prefix, path string) (*url.URL, error) {
|
||||
logger := p.logger.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"prefix": prefix,
|
||||
})
|
||||
|
||||
node, err := p.getNodeForMedia(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().WithError(err).Log("Unknown node")
|
||||
return nil, fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
url, err := node.Core().MediaGetURL(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().Log("Invalid path")
|
||||
return nil, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
logger.Debug().WithField("url", url).Log("File cluster url")
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (p *Manager) FilesystemGetFile(prefix, path string, offset int64) (io.ReadCloser, error) {
|
||||
logger := p.logger.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"prefix": prefix,
|
||||
})
|
||||
|
||||
node, err := p.getNodeForMedia(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().WithError(err).Log("File not available")
|
||||
return nil, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
data, err := node.Core().FilesystemGetFile(prefix, path, offset)
|
||||
if err != nil {
|
||||
logger.Debug().Log("Invalid path")
|
||||
return nil, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
logger.Debug().Log("File cluster path")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (p *Manager) FilesystemGetFileInfo(prefix, path string) (int64, time.Time, error) {
|
||||
logger := p.logger.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"prefix": prefix,
|
||||
})
|
||||
|
||||
node, err := p.getNodeForMedia(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().WithError(err).Log("File not available")
|
||||
return 0, time.Time{}, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
size, lastModified, err := node.Core().FilesystemGetFileInfo(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().Log("Invalid path")
|
||||
return 0, time.Time{}, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
logger.Debug().Log("File cluster path")
|
||||
|
||||
return size, lastModified, nil
|
||||
}
|
||||
|
||||
func (p *Manager) getNodeIDForMedia(prefix, path string) (string, error) {
|
||||
// this is only for mem and disk prefixes
|
||||
nodesChan := make(chan string, 16)
|
||||
nodeids := []string{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for nodeid := range nodesChan {
|
||||
if len(nodeid) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
nodeids = append(nodeids, nodeid)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
p.lock.RLock()
|
||||
for id, n := range p.nodes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(nodeid string, node *Node, p chan<- string) {
|
||||
defer wg.Done()
|
||||
|
||||
_, _, err := node.Core().MediaGetInfo(prefix, path)
|
||||
if err != nil {
|
||||
nodeid = ""
|
||||
}
|
||||
|
||||
p <- nodeid
|
||||
}(id, n, nodesChan)
|
||||
}
|
||||
p.lock.RUnlock()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(nodesChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
if len(nodeids) == 0 {
|
||||
return "", fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
return nodeids[0], nil
|
||||
}
|
||||
|
||||
func (p *Manager) getNodeForMedia(prefix, path string) (*Node, error) {
|
||||
id, err := p.cache.Get(prefix + ":" + path)
|
||||
if err == nil {
|
||||
node, err := p.NodeGet(id)
|
||||
if err == nil {
|
||||
return node, nil
|
||||
}
|
||||
}
|
||||
|
||||
id, err = p.getNodeIDForMedia(prefix, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.cache.Put(prefix+":"+path, id, 5*time.Second)
|
||||
|
||||
return p.NodeGet(id)
|
||||
}
|
||||
|
||||
func (p *Manager) FilesystemList(storage, pattern string) []api.FileInfo {
|
||||
filesChan := make(chan []api.FileInfo, 64)
|
||||
filesList := []api.FileInfo{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for list := range filesChan {
|
||||
filesList = append(filesList, list...)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
p.lock.RLock()
|
||||
for _, n := range p.nodes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(node *Node, p chan<- []api.FileInfo) {
|
||||
defer wg.Done()
|
||||
|
||||
files, err := node.Core().FilesystemList(storage, pattern)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p <- files
|
||||
}(n, filesChan)
|
||||
}
|
||||
p.lock.RUnlock()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(filesChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
return filesList
|
||||
}
|
||||
|
||||
func (p *Manager) ClusterProcessList() []Process {
|
||||
processChan := make(chan []Process, 64)
|
||||
processList := []Process{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for list := range processChan {
|
||||
processList = append(processList, list...)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
p.lock.RLock()
|
||||
for _, n := range p.nodes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(node *Node, p chan<- []Process) {
|
||||
defer wg.Done()
|
||||
|
||||
processes, err := node.Core().ClusterProcessList()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p <- processes
|
||||
}(n, processChan)
|
||||
}
|
||||
p.lock.RUnlock()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(processChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
return processList
|
||||
}
|
||||
|
||||
func (p *Manager) ProcessFindNodeID(id app.ProcessID) (string, error) {
|
||||
procs := p.ClusterProcessList()
|
||||
nodeid := ""
|
||||
|
||||
for _, p := range procs {
|
||||
if p.Config.ProcessID() != id {
|
||||
continue
|
||||
}
|
||||
|
||||
nodeid = p.NodeID
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if len(nodeid) == 0 {
|
||||
return "", fmt.Errorf("the process '%s' is not registered with any node", id.String())
|
||||
}
|
||||
|
||||
return nodeid, nil
|
||||
}
|
||||
|
||||
func (p *Manager) FindNodeForResources(nodeid string, cpu float64, memory uint64) string {
|
||||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
if len(nodeid) != 0 {
|
||||
node, ok := p.nodes[nodeid]
|
||||
if ok {
|
||||
r := node.About().Resources
|
||||
if r.Error == nil && r.CPU+cpu <= r.CPULimit && r.Mem+memory <= r.MemLimit && !r.IsThrottling {
|
||||
return nodeid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for nodeid, node := range p.nodes {
|
||||
r := node.About().Resources
|
||||
if r.Error == nil && r.CPU+cpu <= r.CPULimit && r.Mem+memory <= r.MemLimit && !r.IsThrottling {
|
||||
return nodeid
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *Manager) ProcessList(options client.ProcessListOptions) []api.Process {
|
||||
processChan := make(chan []api.Process, 64)
|
||||
processList := []api.Process{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for list := range processChan {
|
||||
processList = append(processList, list...)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
p.lock.RLock()
|
||||
for _, n := range p.nodes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(node *Node, p chan<- []api.Process) {
|
||||
defer wg.Done()
|
||||
|
||||
processes, err := node.Core().ProcessList(options)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p <- processes
|
||||
}(n, processChan)
|
||||
}
|
||||
p.lock.RUnlock()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(processChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
return processList
|
||||
}
|
||||
|
||||
func (p *Manager) ProcessAdd(nodeid string, config *app.Config, metadata map[string]interface{}) error {
|
||||
node, err := p.NodeGet(nodeid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.Core().ProcessAdd(config, metadata)
|
||||
}
|
||||
|
||||
func (p *Manager) ProcessDelete(nodeid string, id app.ProcessID) error {
|
||||
node, err := p.NodeGet(nodeid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.Core().ProcessDelete(id)
|
||||
}
|
||||
|
||||
func (p *Manager) ProcessUpdate(nodeid string, id app.ProcessID, config *app.Config, metadata map[string]interface{}) error {
|
||||
node, err := p.NodeGet(nodeid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.Core().ProcessUpdate(id, config, metadata)
|
||||
}
|
||||
|
||||
func (p *Manager) ProcessCommand(nodeid string, id app.ProcessID, command string) error {
|
||||
node, err := p.NodeGet(nodeid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.Core().ProcessCommand(id, command)
|
||||
}
|
||||
|
||||
func (p *Manager) ProcessProbe(nodeid string, id app.ProcessID) (api.Probe, error) {
|
||||
node, err := p.NodeGet(nodeid)
|
||||
if err != nil {
|
||||
probe := api.Probe{
|
||||
Log: []string{fmt.Sprintf("the node %s where the process %s should reside on, doesn't exist", nodeid, id.String())},
|
||||
}
|
||||
return probe, fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.Core().ProcessProbe(id)
|
||||
}
|
||||
|
||||
func (p *Manager) ProcessProbeConfig(nodeid string, config *app.Config) (api.Probe, error) {
|
||||
node, err := p.NodeGet(nodeid)
|
||||
if err != nil {
|
||||
probe := api.Probe{
|
||||
Log: []string{fmt.Sprintf("the node %s where the process config should be probed on, doesn't exist", nodeid)},
|
||||
}
|
||||
return probe, fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.Core().ProcessProbeConfig(config)
|
||||
}
|
||||
@ -2,6 +2,7 @@ package node
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -9,48 +10,36 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/datarhei/core/v16/cluster/client"
|
||||
"github.com/datarhei/core/v16/cluster/proxy"
|
||||
"github.com/datarhei/core/v16/config"
|
||||
"github.com/datarhei/core/v16/ffmpeg/skills"
|
||||
"github.com/datarhei/core/v16/log"
|
||||
)
|
||||
|
||||
type Node interface {
|
||||
Stop() error
|
||||
About() About
|
||||
Version() string
|
||||
IPs() []string
|
||||
Status() (string, error)
|
||||
LastContact() time.Time
|
||||
Barrier(name string) (bool, error)
|
||||
type Node struct {
|
||||
id string
|
||||
address string
|
||||
ips []string
|
||||
version string
|
||||
|
||||
CoreStatus() (string, error)
|
||||
CoreEssentials() (string, *config.Config, error)
|
||||
CoreConfig() (*config.Config, error)
|
||||
CoreSkills() (skills.Skills, error)
|
||||
CoreAPIAddress() (string, error)
|
||||
node client.APIClient
|
||||
nodeAbout About
|
||||
nodeLastContact time.Time
|
||||
nodeLastErr error
|
||||
nodeLatency float64
|
||||
|
||||
Proxy() proxy.Node
|
||||
}
|
||||
core *Core
|
||||
coreAbout CoreAbout
|
||||
coreLastContact time.Time
|
||||
coreLastErr error
|
||||
coreLatency float64
|
||||
|
||||
type node struct {
|
||||
client client.APIClient
|
||||
compatibilityErr error
|
||||
|
||||
id string
|
||||
address string
|
||||
ips []string
|
||||
version string
|
||||
lastContact time.Time
|
||||
lastContactErr error
|
||||
lastCoreContact time.Time
|
||||
lastCoreContactErr error
|
||||
latency float64
|
||||
pingLock sync.RWMutex
|
||||
config *config.Config
|
||||
skills *skills.Skills
|
||||
|
||||
runLock sync.Mutex
|
||||
cancelPing context.CancelFunc
|
||||
|
||||
proxyNode proxy.Node
|
||||
lock sync.RWMutex
|
||||
cancel context.CancelFunc
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
@ -62,15 +51,20 @@ type Config struct {
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func New(config Config) Node {
|
||||
n := &node{
|
||||
func New(config Config) *Node {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.MaxIdleConns = 10
|
||||
tr.IdleConnTimeout = 30 * time.Second
|
||||
|
||||
n := &Node{
|
||||
id: config.ID,
|
||||
address: config.Address,
|
||||
version: "0.0.0",
|
||||
client: client.APIClient{
|
||||
node: client.APIClient{
|
||||
Address: config.Address,
|
||||
Client: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: tr,
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
},
|
||||
logger: config.Logger,
|
||||
@ -86,286 +80,499 @@ func New(config Config) Node {
|
||||
}
|
||||
}
|
||||
|
||||
if version, err := n.client.Version(); err == nil {
|
||||
if version, err := n.node.Version(); err == nil {
|
||||
n.version = version
|
||||
}
|
||||
|
||||
n.start(n.id)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
n.cancel = cancel
|
||||
|
||||
n.nodeLastErr = fmt.Errorf("not started yet")
|
||||
n.coreLastErr = fmt.Errorf("not started yet")
|
||||
|
||||
address, coreConfig, coreSkills, err := n.CoreEssentials()
|
||||
n.config = coreConfig
|
||||
n.skills = coreSkills
|
||||
|
||||
n.core = NewCore(n.id, n.logger.WithComponent("ClusterCore").WithField("address", address))
|
||||
n.core.SetEssentials(address, coreConfig)
|
||||
|
||||
n.coreLastErr = err
|
||||
|
||||
go n.updateCore(ctx, 5*time.Second)
|
||||
go n.ping(ctx, time.Second)
|
||||
go n.pingCore(ctx, time.Second)
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *node) start(id string) error {
|
||||
n.runLock.Lock()
|
||||
defer n.runLock.Unlock()
|
||||
func (n *Node) Stop() error {
|
||||
|
||||
if n.cancelPing != nil {
|
||||
n.lock.Lock()
|
||||
|
||||
defer n.lock.Unlock()
|
||||
|
||||
if n.cancel == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
n.cancelPing = cancel
|
||||
n.cancel()
|
||||
n.cancel = nil
|
||||
|
||||
n.lastCoreContactErr = fmt.Errorf("not started yet")
|
||||
n.lastContactErr = fmt.Errorf("not started yet")
|
||||
|
||||
address, config, err := n.CoreEssentials()
|
||||
n.proxyNode = proxy.NewNode(proxy.NodeConfig{
|
||||
ID: id,
|
||||
Address: address,
|
||||
Config: config,
|
||||
Logger: n.logger.WithComponent("ClusterProxyNode").WithField("address", address),
|
||||
})
|
||||
|
||||
n.lastCoreContactErr = err
|
||||
|
||||
if err != nil {
|
||||
go func(ctx context.Context) {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
address, config, err := n.CoreEssentials()
|
||||
n.pingLock.Lock()
|
||||
if err == nil {
|
||||
n.proxyNode.SetEssentials(address, config)
|
||||
n.lastCoreContactErr = nil
|
||||
} else {
|
||||
n.lastCoreContactErr = err
|
||||
n.logger.Error().WithError(err).Log("Failed to retrieve core essentials")
|
||||
}
|
||||
n.pingLock.Unlock()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
}
|
||||
|
||||
go n.ping(ctx)
|
||||
go n.pingCore(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *node) Stop() error {
|
||||
n.runLock.Lock()
|
||||
defer n.runLock.Unlock()
|
||||
|
||||
if n.cancelPing == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
n.proxyNode.Disconnect()
|
||||
|
||||
n.cancelPing()
|
||||
n.cancelPing = nil
|
||||
n.core.Stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var maxLastContact time.Duration = 5 * time.Second
|
||||
|
||||
type AboutCore struct {
|
||||
Address string
|
||||
State string
|
||||
StateError error
|
||||
Status string
|
||||
Error error
|
||||
CreatedAt time.Time
|
||||
Uptime time.Duration
|
||||
LastContact time.Duration
|
||||
Latency time.Duration
|
||||
Version string
|
||||
}
|
||||
|
||||
type About struct {
|
||||
ID string
|
||||
Name string
|
||||
Version string
|
||||
Address string
|
||||
Status string
|
||||
LastContact time.Duration
|
||||
State string
|
||||
Uptime time.Duration
|
||||
LastContact time.Time
|
||||
Latency time.Duration
|
||||
Error error
|
||||
Core AboutCore
|
||||
Resources proxy.NodeResources
|
||||
Core CoreAbout
|
||||
Resources Resources
|
||||
}
|
||||
|
||||
func (n *node) About() About {
|
||||
type Resources 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 // Last error
|
||||
}
|
||||
|
||||
func (n *Node) About() About {
|
||||
n.lock.RLock()
|
||||
defer n.lock.RUnlock()
|
||||
|
||||
a := About{
|
||||
ID: n.id,
|
||||
Version: n.Version(),
|
||||
Version: n.version,
|
||||
Address: n.address,
|
||||
}
|
||||
|
||||
n.pingLock.RLock()
|
||||
a.LastContact = time.Since(n.lastContact)
|
||||
if a.LastContact > maxLastContact {
|
||||
a.Status = "offline"
|
||||
a.Name = n.coreAbout.Name
|
||||
a.Error = n.nodeLastErr
|
||||
a.LastContact = n.nodeLastContact
|
||||
if time.Since(a.LastContact) > maxLastContact {
|
||||
a.State = "offline"
|
||||
} else if n.nodeLastErr != nil {
|
||||
a.State = "degraded"
|
||||
} else if n.compatibilityErr != nil {
|
||||
a.State = "degraded"
|
||||
a.Error = n.compatibilityErr
|
||||
} else {
|
||||
a.Status = "online"
|
||||
a.State = "online"
|
||||
}
|
||||
a.Latency = time.Duration(n.latency * float64(time.Second))
|
||||
a.Error = n.lastContactErr
|
||||
a.Latency = time.Duration(n.nodeLatency * float64(time.Second))
|
||||
|
||||
coreError := n.lastCoreContactErr
|
||||
n.pingLock.RUnlock()
|
||||
|
||||
about := n.CoreAbout()
|
||||
|
||||
a.Name = about.Name
|
||||
a.Core.Address = about.Address
|
||||
a.Core.State = about.State
|
||||
a.Core.StateError = about.Error
|
||||
a.Core.CreatedAt = about.CreatedAt
|
||||
a.Core.Uptime = about.Uptime
|
||||
a.Core.LastContact = time.Since(about.LastContact)
|
||||
if a.Core.LastContact > maxLastContact {
|
||||
a.Core.Status = "offline"
|
||||
} else {
|
||||
a.Core.Status = "online"
|
||||
a.Resources = n.nodeAbout.Resources
|
||||
if a.Resources.Error != nil {
|
||||
a.Resources.CPU = a.Resources.CPULimit
|
||||
a.Resources.Mem = a.Resources.MemLimit
|
||||
a.Resources.IsThrottling = true
|
||||
}
|
||||
|
||||
a.Core = n.coreAbout
|
||||
a.Core.Error = n.coreLastErr
|
||||
a.Core.LastContact = n.coreLastContact
|
||||
a.Core.Latency = time.Duration(n.coreLatency * float64(time.Second))
|
||||
|
||||
if a.State == "online" {
|
||||
if a.Resources.Error != nil {
|
||||
a.State = "degraded"
|
||||
a.Error = a.Resources.Error
|
||||
}
|
||||
}
|
||||
|
||||
if a.State == "online" {
|
||||
if time.Since(a.Core.LastContact) > maxLastContact {
|
||||
a.Core.State = "offline"
|
||||
} else if n.coreLastErr != nil {
|
||||
a.Core.State = "degraded"
|
||||
a.Error = n.coreLastErr
|
||||
} else {
|
||||
a.Core.State = "online"
|
||||
}
|
||||
|
||||
a.State = a.Core.State
|
||||
}
|
||||
a.Core.Error = coreError
|
||||
a.Core.Latency = about.Latency
|
||||
a.Core.Version = about.Version
|
||||
a.Resources = about.Resources
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (n *node) Version() string {
|
||||
n.pingLock.RLock()
|
||||
defer n.pingLock.RUnlock()
|
||||
func (n *Node) Version() string {
|
||||
n.lock.RLock()
|
||||
defer n.lock.RUnlock()
|
||||
|
||||
return n.version
|
||||
}
|
||||
|
||||
func (n *node) IPs() []string {
|
||||
func (n *Node) IPs() []string {
|
||||
return n.ips
|
||||
}
|
||||
|
||||
func (n *node) Status() (string, error) {
|
||||
n.pingLock.RLock()
|
||||
defer n.pingLock.RUnlock()
|
||||
func (n *Node) Status() (string, error) {
|
||||
n.lock.RLock()
|
||||
defer n.lock.RUnlock()
|
||||
|
||||
since := time.Since(n.lastContact)
|
||||
since := time.Since(n.nodeLastContact)
|
||||
if since > maxLastContact {
|
||||
return "offline", fmt.Errorf("the cluster API didn't respond for %s because: %w", since, n.lastContactErr)
|
||||
return "offline", fmt.Errorf("the cluster API didn't respond for %s because: %w", since, n.nodeLastErr)
|
||||
}
|
||||
|
||||
return "online", nil
|
||||
}
|
||||
|
||||
func (n *node) CoreStatus() (string, error) {
|
||||
n.pingLock.RLock()
|
||||
defer n.pingLock.RUnlock()
|
||||
func (n *Node) CoreStatus() (string, error) {
|
||||
n.lock.RLock()
|
||||
defer n.lock.RUnlock()
|
||||
|
||||
since := time.Since(n.lastCoreContact)
|
||||
since := time.Since(n.coreLastContact)
|
||||
if since > maxLastContact {
|
||||
return "offline", fmt.Errorf("the core API didn't respond for %s because: %w", since, n.lastCoreContactErr)
|
||||
return "offline", fmt.Errorf("the core API didn't respond for %s because: %w", since, n.coreLastErr)
|
||||
}
|
||||
|
||||
return "online", nil
|
||||
}
|
||||
|
||||
func (n *node) LastContact() time.Time {
|
||||
n.pingLock.RLock()
|
||||
defer n.pingLock.RUnlock()
|
||||
|
||||
return n.lastContact
|
||||
}
|
||||
|
||||
func (n *node) CoreEssentials() (string, *config.Config, error) {
|
||||
func (n *Node) CoreEssentials() (string, *config.Config, *skills.Skills, error) {
|
||||
address, err := n.CoreAPIAddress()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
config, err := n.CoreConfig()
|
||||
config, err := n.CoreConfig(false)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
return address, config, nil
|
||||
skills, err := n.CoreSkills(false)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
return address, config, skills, nil
|
||||
}
|
||||
|
||||
func (n *node) CoreConfig() (*config.Config, error) {
|
||||
return n.client.CoreConfig()
|
||||
func (n *Node) CoreConfig(cached bool) (*config.Config, error) {
|
||||
if cached {
|
||||
n.lock.RLock()
|
||||
config := n.config
|
||||
n.lock.RUnlock()
|
||||
|
||||
if config != nil {
|
||||
return config, nil
|
||||
}
|
||||
}
|
||||
|
||||
return n.node.CoreConfig()
|
||||
}
|
||||
|
||||
func (n *node) CoreSkills() (skills.Skills, error) {
|
||||
return n.client.CoreSkills()
|
||||
func (n *Node) CoreSkills(cached bool) (*skills.Skills, error) {
|
||||
if cached {
|
||||
n.lock.RLock()
|
||||
skills := n.skills
|
||||
n.lock.RUnlock()
|
||||
|
||||
if skills != nil {
|
||||
return skills, nil
|
||||
}
|
||||
}
|
||||
|
||||
skills, err := n.node.CoreSkills()
|
||||
|
||||
return &skills, err
|
||||
}
|
||||
|
||||
func (n *node) CoreAPIAddress() (string, error) {
|
||||
return n.client.CoreAPIAddress()
|
||||
func (n *Node) CoreAPIAddress() (string, error) {
|
||||
return n.node.CoreAPIAddress()
|
||||
}
|
||||
|
||||
func (n *node) CoreAbout() proxy.NodeAbout {
|
||||
return n.proxyNode.About()
|
||||
func (n *Node) Barrier(name string) (bool, error) {
|
||||
return n.node.Barrier(name)
|
||||
}
|
||||
|
||||
func (n *node) Barrier(name string) (bool, error) {
|
||||
return n.client.Barrier(name)
|
||||
func (n *Node) CoreAbout() CoreAbout {
|
||||
return n.About().Core
|
||||
}
|
||||
|
||||
func (n *node) Proxy() proxy.Node {
|
||||
return n.proxyNode
|
||||
func (n *Node) Core() *Core {
|
||||
return n.core
|
||||
}
|
||||
|
||||
func (n *node) ping(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
func (n *Node) CheckCompatibility(other *Node, skipSkillsCheck bool) {
|
||||
err := n.checkCompatibility(other, skipSkillsCheck)
|
||||
|
||||
n.lock.Lock()
|
||||
n.compatibilityErr = err
|
||||
n.lock.Unlock()
|
||||
}
|
||||
|
||||
func (n *Node) checkCompatibility(other *Node, skipSkillsCheck bool) error {
|
||||
if other == nil {
|
||||
return fmt.Errorf("no other node available to compare to")
|
||||
}
|
||||
|
||||
n.lock.RLock()
|
||||
version := n.version
|
||||
config := n.config
|
||||
skills := n.skills
|
||||
n.lock.RUnlock()
|
||||
|
||||
otherVersion := other.Version()
|
||||
otherConfig, _ := other.CoreConfig(true)
|
||||
otherSkills, _ := other.CoreSkills(true)
|
||||
|
||||
err := verifyVersion(version, otherVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("version: %w", err)
|
||||
}
|
||||
|
||||
err = verifyConfig(config, otherConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
|
||||
if !skipSkillsCheck {
|
||||
err := verifySkills(skills, otherSkills)
|
||||
if err != nil {
|
||||
return fmt.Errorf("skills: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyVersion(local, other string) error {
|
||||
if local != other {
|
||||
return fmt.Errorf("actual: %s, expected %s", local, other)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyConfig(local, other *config.Config) error {
|
||||
if local == nil || other == nil {
|
||||
return fmt.Errorf("config is not available")
|
||||
}
|
||||
|
||||
if local.Cluster.Enable != other.Cluster.Enable {
|
||||
return fmt.Errorf("cluster.enable: actual: %v, expected: %v", local.Cluster.Enable, other.Cluster.Enable)
|
||||
}
|
||||
|
||||
if local.Cluster.ID != other.Cluster.ID {
|
||||
return fmt.Errorf("cluster.id: actual: %v, expected: %v", local.Cluster.ID, other.Cluster.ID)
|
||||
}
|
||||
|
||||
if local.Cluster.SyncInterval != other.Cluster.SyncInterval {
|
||||
return fmt.Errorf("cluster.sync_interval_sec: actual: %v, expected: %v", local.Cluster.SyncInterval, other.Cluster.SyncInterval)
|
||||
}
|
||||
|
||||
if local.Cluster.NodeRecoverTimeout != other.Cluster.NodeRecoverTimeout {
|
||||
return fmt.Errorf("cluster.node_recover_timeout_sec: actual: %v, expected: %v", local.Cluster.NodeRecoverTimeout, other.Cluster.NodeRecoverTimeout)
|
||||
}
|
||||
|
||||
if local.Cluster.EmergencyLeaderTimeout != other.Cluster.EmergencyLeaderTimeout {
|
||||
return fmt.Errorf("cluster.emergency_leader_timeout_sec: actual: %v, expected: %v", local.Cluster.EmergencyLeaderTimeout, other.Cluster.EmergencyLeaderTimeout)
|
||||
}
|
||||
|
||||
if local.Cluster.Debug.DisableFFmpegCheck != other.Cluster.Debug.DisableFFmpegCheck {
|
||||
return fmt.Errorf("cluster.debug.disable_ffmpeg_check: actual: %v, expected: %v", local.Cluster.Debug.DisableFFmpegCheck, other.Cluster.Debug.DisableFFmpegCheck)
|
||||
}
|
||||
|
||||
if !local.API.Auth.Enable {
|
||||
return fmt.Errorf("api.auth.enable must be enabled")
|
||||
}
|
||||
|
||||
if local.API.Auth.Enable != other.API.Auth.Enable {
|
||||
return fmt.Errorf("api.auth.enable: actual: %v, expected: %v", local.API.Auth.Enable, other.API.Auth.Enable)
|
||||
}
|
||||
|
||||
if local.API.Auth.Username != other.API.Auth.Username {
|
||||
return fmt.Errorf("api.auth.username: actual: %v, expected: %v", local.API.Auth.Username, other.API.Auth.Username)
|
||||
}
|
||||
|
||||
if local.API.Auth.Password != other.API.Auth.Password {
|
||||
return fmt.Errorf("api.auth.password: actual: %v, expected: %v", local.API.Auth.Password, other.API.Auth.Password)
|
||||
}
|
||||
|
||||
if local.API.Auth.JWT.Secret != other.API.Auth.JWT.Secret {
|
||||
return fmt.Errorf("api.auth.jwt.secret: actual: %v, expected: %v", local.API.Auth.JWT.Secret, other.API.Auth.JWT.Secret)
|
||||
}
|
||||
|
||||
if local.RTMP.Enable != other.RTMP.Enable {
|
||||
return fmt.Errorf("rtmp.enable: actual: %v, expected: %v", local.RTMP.Enable, other.RTMP.Enable)
|
||||
}
|
||||
|
||||
if local.RTMP.Enable {
|
||||
if local.RTMP.App != other.RTMP.App {
|
||||
return fmt.Errorf("rtmp.app: actual: %v, expected: %v", local.RTMP.App, other.RTMP.App)
|
||||
}
|
||||
}
|
||||
|
||||
if local.SRT.Enable != other.SRT.Enable {
|
||||
return fmt.Errorf("srt.enable: actual: %v, expected: %v", local.SRT.Enable, other.SRT.Enable)
|
||||
}
|
||||
|
||||
if local.SRT.Enable {
|
||||
if local.SRT.Passphrase != other.SRT.Passphrase {
|
||||
return fmt.Errorf("srt.passphrase: actual: %v, expected: %v", local.SRT.Passphrase, other.SRT.Passphrase)
|
||||
}
|
||||
}
|
||||
|
||||
if local.Resources.MaxCPUUsage == 0 || other.Resources.MaxCPUUsage == 0 {
|
||||
return fmt.Errorf("resources.max_cpu_usage")
|
||||
}
|
||||
|
||||
if local.Resources.MaxMemoryUsage == 0 || other.Resources.MaxMemoryUsage == 0 {
|
||||
return fmt.Errorf("resources.max_memory_usage")
|
||||
}
|
||||
|
||||
if local.TLS.Enable != other.TLS.Enable {
|
||||
return fmt.Errorf("tls.enable: actual: %v, expected: %v", local.TLS.Enable, other.TLS.Enable)
|
||||
}
|
||||
|
||||
if local.TLS.Enable {
|
||||
if local.TLS.Auto != other.TLS.Auto {
|
||||
return fmt.Errorf("tls.auto: actual: %v, expected: %v", local.TLS.Auto, other.TLS.Auto)
|
||||
}
|
||||
|
||||
if len(local.Host.Name) == 0 || len(other.Host.Name) == 0 {
|
||||
return fmt.Errorf("host.name must be set")
|
||||
}
|
||||
|
||||
if local.TLS.Auto {
|
||||
if local.TLS.Email != other.TLS.Email {
|
||||
return fmt.Errorf("tls.email: actual: %v, expected: %v", local.TLS.Email, other.TLS.Email)
|
||||
}
|
||||
|
||||
if local.TLS.Staging != other.TLS.Staging {
|
||||
return fmt.Errorf("tls.staging: actual: %v, expected: %v", local.TLS.Staging, other.TLS.Staging)
|
||||
}
|
||||
|
||||
if local.TLS.Secret != other.TLS.Secret {
|
||||
return fmt.Errorf("tls.secret: actual: %v, expected: %v", local.TLS.Secret, other.TLS.Secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifySkills(local, other *skills.Skills) error {
|
||||
if local == nil || other == nil {
|
||||
return fmt.Errorf("skills are not available")
|
||||
}
|
||||
|
||||
if err := local.Equal(*other); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) ping(ctx context.Context, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
start := time.Now()
|
||||
version, err := n.client.Version()
|
||||
if err == nil {
|
||||
n.pingLock.Lock()
|
||||
n.version = version
|
||||
n.lastContact = time.Now()
|
||||
n.lastContactErr = nil
|
||||
n.latency = n.latency*0.2 + time.Since(start).Seconds()*0.8
|
||||
n.pingLock.Unlock()
|
||||
} else {
|
||||
n.pingLock.Lock()
|
||||
n.lastContactErr = err
|
||||
n.pingLock.Unlock()
|
||||
about, err := n.node.About()
|
||||
|
||||
n.lock.Lock()
|
||||
if err == nil {
|
||||
n.version = about.Version
|
||||
n.nodeAbout = About{
|
||||
ID: about.ID,
|
||||
Version: about.Version,
|
||||
Address: about.Address,
|
||||
Uptime: time.Since(about.StartedAt),
|
||||
Error: err,
|
||||
Resources: Resources{
|
||||
IsThrottling: about.Resources.IsThrottling,
|
||||
NCPU: about.Resources.NCPU,
|
||||
CPU: about.Resources.CPU,
|
||||
CPULimit: about.Resources.CPULimit,
|
||||
Mem: about.Resources.Mem,
|
||||
MemLimit: about.Resources.MemLimit,
|
||||
Error: nil,
|
||||
},
|
||||
}
|
||||
if len(about.Resources.Error) != 0 {
|
||||
n.nodeAbout.Resources.Error = errors.New(about.Resources.Error)
|
||||
}
|
||||
n.nodeLastContact = time.Now()
|
||||
n.nodeLastErr = nil
|
||||
n.nodeLatency = n.nodeLatency*0.2 + time.Since(start).Seconds()*0.8
|
||||
} else {
|
||||
n.nodeLastErr = err
|
||||
n.logger.Warn().WithError(err).Log("Failed to ping cluster API")
|
||||
}
|
||||
n.lock.Unlock()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) pingCore(ctx context.Context) {
|
||||
ticker := time.NewTicker(time.Second)
|
||||
func (n *Node) updateCore(ctx context.Context, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
_, err := n.proxyNode.IsConnected()
|
||||
address, config, skills, err := n.CoreEssentials()
|
||||
|
||||
n.lock.Lock()
|
||||
if err == nil {
|
||||
n.pingLock.Lock()
|
||||
n.lastCoreContact = time.Now()
|
||||
n.lastCoreContactErr = nil
|
||||
n.pingLock.Unlock()
|
||||
n.config = config
|
||||
n.skills = skills
|
||||
n.core.SetEssentials(address, config)
|
||||
n.coreLastErr = nil
|
||||
} else {
|
||||
n.pingLock.Lock()
|
||||
n.lastCoreContactErr = fmt.Errorf("not connected to core api: %w", err)
|
||||
n.pingLock.Unlock()
|
||||
|
||||
n.logger.Warn().WithError(err).Log("not connected to core API")
|
||||
n.coreLastErr = err
|
||||
n.logger.Error().WithError(err).Log("Failed to retrieve core essentials")
|
||||
}
|
||||
n.lock.Unlock()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Node) pingCore(ctx context.Context, interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
start := time.Now()
|
||||
about, err := n.core.About()
|
||||
|
||||
n.lock.Lock()
|
||||
if err == nil {
|
||||
n.coreLastContact = time.Now()
|
||||
n.coreLastErr = nil
|
||||
n.coreAbout = about
|
||||
n.coreLatency = n.coreLatency*0.2 + time.Since(start).Seconds()*0.8
|
||||
} else {
|
||||
n.coreLastErr = fmt.Errorf("not connected to core api: %w", err)
|
||||
}
|
||||
n.lock.Unlock()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
|
||||
@ -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,17 @@ 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) RelocateProcesses(origin string, relocations map[app.ProcessID]string) 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.RelocateProcesses(origin, relocations)
|
||||
return c.forwarder.ProcessesRelocate(origin, relocations)
|
||||
}
|
||||
|
||||
cmd := &store.Command{
|
||||
@ -121,13 +93,9 @@ func (c *cluster) RelocateProcesses(origin string, relocations map[app.ProcessID
|
||||
return c.applyCommand(cmd)
|
||||
}
|
||||
|
||||
func (c *cluster) SetProcessMetadata(origin string, id app.ProcessID, key string, data interface{}) error {
|
||||
if ok, _ := c.IsDegraded(); ok {
|
||||
return ErrDegraded
|
||||
}
|
||||
|
||||
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{
|
||||
@ -142,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
|
||||
}
|
||||
@ -163,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
@ -1,620 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/datarhei/core/v16/log"
|
||||
"github.com/datarhei/core/v16/restream/app"
|
||||
|
||||
clientapi "github.com/datarhei/core-client-go/v16/api"
|
||||
)
|
||||
|
||||
type Proxy interface {
|
||||
Start()
|
||||
Stop()
|
||||
|
||||
AddNode(id string, node Node) (string, error)
|
||||
RemoveNode(id string) error
|
||||
|
||||
ProxyReader
|
||||
Reader() ProxyReader
|
||||
|
||||
AddProcess(nodeid string, config *app.Config, metadata map[string]interface{}) error
|
||||
DeleteProcess(nodeid string, id app.ProcessID) error
|
||||
UpdateProcess(nodeid string, id app.ProcessID, config *app.Config, metadata map[string]interface{}) error
|
||||
CommandProcess(nodeid string, id app.ProcessID, command string) error
|
||||
}
|
||||
|
||||
type ProxyReader interface {
|
||||
ListNodes() []NodeReader
|
||||
GetNodeReader(id string) (NodeReader, error)
|
||||
|
||||
FindNodeFromProcess(id app.ProcessID) (string, error)
|
||||
FindNodeFromResources(nodeid string, cpu float64, memory uint64) string
|
||||
|
||||
Resources() map[string]NodeResources
|
||||
|
||||
ListProcesses(ProcessListOptions) []clientapi.Process
|
||||
ListProxyProcesses() []Process
|
||||
ProbeProcess(nodeid string, id app.ProcessID) (clientapi.Probe, error)
|
||||
ProbeProcessConfig(nodeid string, config *app.Config) (clientapi.Probe, error)
|
||||
|
||||
ListFiles(storage, pattern string) []clientapi.FileInfo
|
||||
|
||||
GetURL(prefix, path string) (*url.URL, error)
|
||||
GetFile(prefix, path string, offset int64) (io.ReadCloser, error)
|
||||
GetFileInfo(prefix, path string) (int64, time.Time, error)
|
||||
}
|
||||
|
||||
type ProxyConfig struct {
|
||||
ID string // ID of the node
|
||||
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
type proxy struct {
|
||||
id string
|
||||
|
||||
nodes map[string]Node // List of known nodes
|
||||
nodesLock sync.RWMutex
|
||||
|
||||
lock sync.RWMutex
|
||||
running bool
|
||||
|
||||
cache *Cache[string]
|
||||
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
var ErrNodeNotFound = errors.New("node not found")
|
||||
|
||||
func NewProxy(config ProxyConfig) (Proxy, error) {
|
||||
p := &proxy{
|
||||
id: config.ID,
|
||||
nodes: map[string]Node{},
|
||||
cache: NewCache[string](nil),
|
||||
logger: config.Logger,
|
||||
}
|
||||
|
||||
if p.logger == nil {
|
||||
p.logger = log.New("")
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *proxy) Start() {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if p.running {
|
||||
return
|
||||
}
|
||||
|
||||
p.running = true
|
||||
|
||||
p.logger.Debug().Log("Starting proxy")
|
||||
}
|
||||
|
||||
func (p *proxy) Stop() {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if !p.running {
|
||||
return
|
||||
}
|
||||
|
||||
p.running = false
|
||||
|
||||
p.logger.Debug().Log("Stopping proxy")
|
||||
|
||||
p.nodes = map[string]Node{}
|
||||
}
|
||||
|
||||
func (p *proxy) Reader() ProxyReader {
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *proxy) Resources() map[string]NodeResources {
|
||||
resources := map[string]NodeResources{}
|
||||
|
||||
p.nodesLock.RLock()
|
||||
defer p.nodesLock.RUnlock()
|
||||
|
||||
for id, node := range p.nodes {
|
||||
resources[id] = node.Resources()
|
||||
}
|
||||
|
||||
return resources
|
||||
}
|
||||
|
||||
func (p *proxy) AddNode(id string, node Node) (string, error) {
|
||||
about := node.About()
|
||||
|
||||
//if id != about.ID {
|
||||
// return "", fmt.Errorf("the provided (%s) and retrieved (%s) ID's don't match", id, about.ID)
|
||||
//}
|
||||
|
||||
p.nodesLock.Lock()
|
||||
defer p.nodesLock.Unlock()
|
||||
|
||||
if n, ok := p.nodes[id]; ok {
|
||||
n.Disconnect()
|
||||
delete(p.nodes, id)
|
||||
}
|
||||
|
||||
p.nodes[id] = node
|
||||
|
||||
p.logger.Info().WithFields(log.Fields{
|
||||
"address": about.Address,
|
||||
"name": about.Name,
|
||||
"id": id,
|
||||
}).Log("Added node")
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (p *proxy) RemoveNode(id string) error {
|
||||
p.nodesLock.Lock()
|
||||
defer p.nodesLock.Unlock()
|
||||
|
||||
node, ok := p.nodes[id]
|
||||
if !ok {
|
||||
return ErrNodeNotFound
|
||||
}
|
||||
|
||||
node.Disconnect()
|
||||
|
||||
delete(p.nodes, id)
|
||||
|
||||
p.logger.Info().WithFields(log.Fields{
|
||||
"id": id,
|
||||
}).Log("Removed node")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxy) ListNodes() []NodeReader {
|
||||
list := []NodeReader{}
|
||||
|
||||
p.nodesLock.RLock()
|
||||
defer p.nodesLock.RUnlock()
|
||||
|
||||
for _, node := range p.nodes {
|
||||
list = append(list, node)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (p *proxy) GetNode(id string) (Node, error) {
|
||||
p.nodesLock.RLock()
|
||||
defer p.nodesLock.RUnlock()
|
||||
|
||||
node, ok := p.nodes[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("node not found")
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func (p *proxy) GetNodeReader(id string) (NodeReader, error) {
|
||||
return p.GetNode(id)
|
||||
}
|
||||
|
||||
func (p *proxy) GetURL(prefix, path string) (*url.URL, error) {
|
||||
logger := p.logger.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"prefix": prefix,
|
||||
})
|
||||
|
||||
node, err := p.getNodeForFile(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().WithError(err).Log("Unknown node")
|
||||
return nil, fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
url, err := node.GetURL(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().Log("Invalid path")
|
||||
return nil, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
logger.Debug().WithField("url", url).Log("File cluster url")
|
||||
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func (p *proxy) GetFile(prefix, path string, offset int64) (io.ReadCloser, error) {
|
||||
logger := p.logger.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"prefix": prefix,
|
||||
})
|
||||
|
||||
node, err := p.getNodeForFile(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().WithError(err).Log("File not available")
|
||||
return nil, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
data, err := node.GetFile(prefix, path, offset)
|
||||
if err != nil {
|
||||
logger.Debug().Log("Invalid path")
|
||||
return nil, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
logger.Debug().Log("File cluster path")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (p *proxy) GetFileInfo(prefix, path string) (int64, time.Time, error) {
|
||||
logger := p.logger.WithFields(log.Fields{
|
||||
"path": path,
|
||||
"prefix": prefix,
|
||||
})
|
||||
|
||||
node, err := p.getNodeForFile(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().WithError(err).Log("File not available")
|
||||
return 0, time.Time{}, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
size, lastModified, err := node.GetFileInfo(prefix, path)
|
||||
if err != nil {
|
||||
logger.Debug().Log("Invalid path")
|
||||
return 0, time.Time{}, fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
logger.Debug().Log("File cluster path")
|
||||
|
||||
return size, lastModified, nil
|
||||
}
|
||||
|
||||
func (p *proxy) getNodeIDForFile(prefix, path string) (string, error) {
|
||||
// this is only for mem and disk prefixes
|
||||
nodesChan := make(chan string, 16)
|
||||
nodeids := []string{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for nodeid := range nodesChan {
|
||||
if len(nodeid) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
nodeids = append(nodeids, nodeid)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
p.nodesLock.RLock()
|
||||
for id, node := range p.nodes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(nodeid string, node Node, p chan<- string) {
|
||||
defer wg.Done()
|
||||
|
||||
_, _, err := node.GetResourceInfo(prefix, path)
|
||||
if err != nil {
|
||||
nodeid = ""
|
||||
}
|
||||
|
||||
p <- nodeid
|
||||
}(id, node, nodesChan)
|
||||
}
|
||||
p.nodesLock.RUnlock()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(nodesChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
if len(nodeids) == 0 {
|
||||
return "", fmt.Errorf("file not found")
|
||||
}
|
||||
|
||||
return nodeids[0], nil
|
||||
}
|
||||
|
||||
func (p *proxy) getNodeForFile(prefix, path string) (Node, error) {
|
||||
id, err := p.cache.Get(prefix + ":" + path)
|
||||
if err == nil {
|
||||
node, err := p.GetNode(id)
|
||||
if err == nil {
|
||||
return node, nil
|
||||
}
|
||||
}
|
||||
|
||||
id, err = p.getNodeIDForFile(prefix, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.cache.Put(prefix+":"+path, id, 5*time.Second)
|
||||
|
||||
return p.GetNode(id)
|
||||
}
|
||||
|
||||
func (p *proxy) ListFiles(storage, pattern string) []clientapi.FileInfo {
|
||||
filesChan := make(chan []clientapi.FileInfo, 64)
|
||||
filesList := []clientapi.FileInfo{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for list := range filesChan {
|
||||
filesList = append(filesList, list...)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
p.nodesLock.RLock()
|
||||
for _, node := range p.nodes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(node Node, p chan<- []clientapi.FileInfo) {
|
||||
defer wg.Done()
|
||||
|
||||
files, err := node.ListFiles(storage, pattern)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p <- files
|
||||
}(node, filesChan)
|
||||
}
|
||||
p.nodesLock.RUnlock()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(filesChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
return filesList
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
NodeID string
|
||||
Order string
|
||||
State string
|
||||
CPU float64 // Current CPU load of this process, 0-100*ncpu
|
||||
Mem uint64 // Currently consumed memory of this process in bytes
|
||||
Throttling bool
|
||||
Runtime time.Duration
|
||||
UpdatedAt time.Time
|
||||
Config *app.Config
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
type ProcessListOptions struct {
|
||||
ID []string
|
||||
Filter []string
|
||||
Domain string
|
||||
Reference string
|
||||
IDPattern string
|
||||
RefPattern string
|
||||
OwnerPattern string
|
||||
DomainPattern string
|
||||
}
|
||||
|
||||
func (p *proxy) ListProxyProcesses() []Process {
|
||||
processChan := make(chan []Process, 64)
|
||||
processList := []Process{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for list := range processChan {
|
||||
processList = append(processList, list...)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
p.nodesLock.RLock()
|
||||
for _, node := range p.nodes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(node Node, p chan<- []Process) {
|
||||
defer wg.Done()
|
||||
|
||||
processes, err := node.ProxyProcessList()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p <- processes
|
||||
}(node, processChan)
|
||||
}
|
||||
p.nodesLock.RUnlock()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(processChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
return processList
|
||||
}
|
||||
|
||||
func (p *proxy) FindNodeFromProcess(id app.ProcessID) (string, error) {
|
||||
procs := p.ListProxyProcesses()
|
||||
nodeid := ""
|
||||
|
||||
for _, p := range procs {
|
||||
if p.Config.ProcessID() != id {
|
||||
continue
|
||||
}
|
||||
|
||||
nodeid = p.NodeID
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if len(nodeid) == 0 {
|
||||
return "", fmt.Errorf("the process '%s' is not registered with any node", id.String())
|
||||
}
|
||||
|
||||
return nodeid, nil
|
||||
}
|
||||
|
||||
func (p *proxy) FindNodeFromResources(nodeid string, cpu float64, memory uint64) string {
|
||||
p.nodesLock.RLock()
|
||||
defer p.nodesLock.RUnlock()
|
||||
|
||||
if len(nodeid) != 0 {
|
||||
node, ok := p.nodes[nodeid]
|
||||
if ok {
|
||||
r := node.Resources()
|
||||
if r.CPU+cpu <= r.CPULimit && r.Mem+memory <= r.MemLimit && !r.IsThrottling {
|
||||
return nodeid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for nodeid, node := range p.nodes {
|
||||
r := node.Resources()
|
||||
if r.CPU+cpu <= r.CPULimit && r.Mem+memory <= r.MemLimit && !r.IsThrottling {
|
||||
return nodeid
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *proxy) ListProcesses(options ProcessListOptions) []clientapi.Process {
|
||||
processChan := make(chan []clientapi.Process, 64)
|
||||
processList := []clientapi.Process{}
|
||||
|
||||
wgList := sync.WaitGroup{}
|
||||
wgList.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wgList.Done()
|
||||
|
||||
for list := range processChan {
|
||||
processList = append(processList, list...)
|
||||
}
|
||||
}()
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
p.nodesLock.RLock()
|
||||
for _, node := range p.nodes {
|
||||
wg.Add(1)
|
||||
|
||||
go func(node Node, p chan<- []clientapi.Process) {
|
||||
defer wg.Done()
|
||||
|
||||
processes, err := node.ProcessList(options)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p <- processes
|
||||
}(node, processChan)
|
||||
}
|
||||
p.nodesLock.RUnlock()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
close(processChan)
|
||||
|
||||
wgList.Wait()
|
||||
|
||||
return processList
|
||||
}
|
||||
|
||||
func (p *proxy) AddProcess(nodeid string, config *app.Config, metadata map[string]interface{}) error {
|
||||
node, err := p.GetNode(nodeid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.AddProcess(config, metadata)
|
||||
}
|
||||
|
||||
func (p *proxy) DeleteProcess(nodeid string, id app.ProcessID) error {
|
||||
node, err := p.GetNode(nodeid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.DeleteProcess(id)
|
||||
}
|
||||
|
||||
func (p *proxy) UpdateProcess(nodeid string, id app.ProcessID, config *app.Config, metadata map[string]interface{}) error {
|
||||
node, err := p.GetNode(nodeid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.UpdateProcess(id, config, metadata)
|
||||
}
|
||||
|
||||
func (p *proxy) CommandProcess(nodeid string, id app.ProcessID, command string) error {
|
||||
node, err := p.GetNode(nodeid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "start":
|
||||
err = node.StartProcess(id)
|
||||
case "stop":
|
||||
err = node.StopProcess(id)
|
||||
case "restart":
|
||||
err = node.RestartProcess(id)
|
||||
case "reload":
|
||||
err = node.ReloadProcess(id)
|
||||
default:
|
||||
err = fmt.Errorf("unknown command: %s", command)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *proxy) ProbeProcess(nodeid string, id app.ProcessID) (clientapi.Probe, error) {
|
||||
node, err := p.GetNode(nodeid)
|
||||
if err != nil {
|
||||
probe := clientapi.Probe{
|
||||
Log: []string{fmt.Sprintf("the node %s where the process %s should reside on, doesn't exist", nodeid, id.String())},
|
||||
}
|
||||
return probe, fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.ProbeProcess(id)
|
||||
}
|
||||
|
||||
func (p *proxy) ProbeProcessConfig(nodeid string, config *app.Config) (clientapi.Probe, error) {
|
||||
node, err := p.GetNode(nodeid)
|
||||
if err != nil {
|
||||
probe := clientapi.Probe{
|
||||
Log: []string{fmt.Sprintf("the node %s where the process config should be probed on, doesn't exist", nodeid)},
|
||||
}
|
||||
return probe, fmt.Errorf("node not found: %w", err)
|
||||
}
|
||||
|
||||
return node.ProbeProcessConfig(config)
|
||||
}
|
||||
123
cluster/resources.go
Normal file
123
cluster/resources.go
Normal 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
6
cluster/store/errors.go
Normal file
@ -0,0 +1,6 @@
|
||||
package store
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrNotFound = errors.New("")
|
||||
var ErrBadRequest = errors.New("")
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ func (s *store) clearLocks(_ 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()
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ func (s *store) setNodeState(cmd CommandSetNodeState) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *store) ListNodes() map[string]Node {
|
||||
func (s *store) NodeList() map[string]Node {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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()
|
||||
@ -134,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
|
||||
@ -153,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 {
|
||||
@ -180,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
|
||||
@ -199,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()
|
||||
|
||||
@ -219,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{
|
||||
@ -238,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()
|
||||
|
||||
@ -251,7 +251,7 @@ func (s *store) GetProcessNodeMap() map[string]string {
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *store) GetProcessRelocateMap() map[string]string {
|
||||
func (s *store) ProcessGetRelocateMap() map[string]string {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -20,23 +20,23 @@ type Store interface {
|
||||
|
||||
OnApply(func(op Operation))
|
||||
|
||||
ListProcesses() []Process
|
||||
GetProcess(id app.ProcessID) (Process, error)
|
||||
GetProcessNodeMap() map[string]string
|
||||
GetProcessRelocateMap() 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)
|
||||
|
||||
ListNodes() map[string]Node
|
||||
NodeList() map[string]Node
|
||||
}
|
||||
|
||||
type Process struct {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
3
go.mod
@ -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
2
go.sum
@ -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=
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
@ -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) {
|
||||
@ -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
256
http/client/process.go
Normal 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
|
||||
}
|
||||
@ -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) {
|
||||
@ -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) {
|
||||
@ -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) {
|
||||
@ -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,
|
||||
|
||||
@ -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{}
|
||||
|
||||
@ -8,7 +8,7 @@ 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"
|
||||
@ -21,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
|
||||
}
|
||||
|
||||
@ -29,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,
|
||||
}
|
||||
|
||||
@ -68,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,
|
||||
@ -77,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 {
|
||||
@ -98,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,
|
||||
@ -108,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,
|
||||
@ -148,9 +148,9 @@ 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)
|
||||
}
|
||||
|
||||
// TransferLeadership transfers the leadership to another node
|
||||
@ -266,7 +266,7 @@ func (h *ClusterHandler) Reallocation(c echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
err := h.cluster.RelocateProcesses("", relocations)
|
||||
err := h.cluster.ProcessesRelocate("", relocations)
|
||||
if err != nil {
|
||||
return api.Err(http.StatusInternalServerError, "", "%s", err.Error())
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
|
||||
@ -7,15 +7,14 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
clientapi "github.com/datarhei/core-client-go/v16/api"
|
||||
"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/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.?.?
|
||||
@ -25,17 +24,17 @@ 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.ListNodes()
|
||||
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.Status = dbnode.State
|
||||
node.State = dbnode.State
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +44,7 @@ func (h *ClusterHandler) GetNodes(c echo.Context) error {
|
||||
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.?.?
|
||||
@ -56,12 +55,12 @@ 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.ListNodes()
|
||||
nodes := h.cluster.Store().NodeList()
|
||||
|
||||
for _, node := range about.Nodes {
|
||||
if node.ID != id {
|
||||
@ -70,7 +69,7 @@ func (h *ClusterHandler) GetNode(c echo.Context) error {
|
||||
|
||||
if dbnode, hasNode := nodes[node.ID]; hasNode {
|
||||
if dbnode.State == "maintenance" {
|
||||
node.Status = dbnode.State
|
||||
node.State = dbnode.State
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +79,7 @@ func (h *ClusterHandler) GetNode(c echo.Context) error {
|
||||
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.?.?
|
||||
@ -91,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.?.?
|
||||
@ -124,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())
|
||||
}
|
||||
@ -136,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()
|
||||
|
||||
@ -176,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())
|
||||
}
|
||||
@ -234,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())
|
||||
}
|
||||
@ -270,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())
|
||||
}
|
||||
@ -303,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())
|
||||
}
|
||||
@ -316,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.?.?
|
||||
@ -336,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 {
|
||||
@ -352,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,
|
||||
@ -371,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") {
|
||||
@ -384,7 +383,7 @@ func (h *ClusterHandler) ListNodeProcesses(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, processes)
|
||||
}
|
||||
|
||||
// GetNodeState returns the state of a node with the given ID
|
||||
// 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.?.?
|
||||
@ -395,7 +394,7 @@ func (h *ClusterHandler) ListNodeProcesses(c echo.Context) error {
|
||||
// @Failure 404 {object} api.Error
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v3/cluster/node/{id}/state [get]
|
||||
func (h *ClusterHandler) GetNodeState(c echo.Context) error {
|
||||
func (h *ClusterHandler) NodeGetState(c echo.Context) error {
|
||||
id := util.PathParam(c, "id")
|
||||
|
||||
about, _ := h.cluster.About()
|
||||
@ -406,7 +405,7 @@ func (h *ClusterHandler) GetNodeState(c echo.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
state = node.Status
|
||||
state = node.State
|
||||
break
|
||||
}
|
||||
|
||||
@ -414,7 +413,7 @@ func (h *ClusterHandler) GetNodeState(c echo.Context) error {
|
||||
return api.Err(http.StatusNotFound, "", "node not found")
|
||||
}
|
||||
|
||||
nodes := h.cluster.ListNodes()
|
||||
nodes := h.cluster.Store().NodeList()
|
||||
if node, hasNode := nodes[id]; hasNode {
|
||||
if node.State == "maintenance" {
|
||||
state = node.State
|
||||
@ -426,7 +425,7 @@ func (h *ClusterHandler) GetNodeState(c echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// SetNodeState sets the state of a node with the given ID
|
||||
// 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.?.?
|
||||
@ -440,7 +439,7 @@ func (h *ClusterHandler) GetNodeState(c echo.Context) error {
|
||||
// @Failure 500 {object} api.Error
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v3/cluster/node/{id}/state [put]
|
||||
func (h *ClusterHandler) SetNodeState(c echo.Context) error {
|
||||
func (h *ClusterHandler) NodeSetState(c echo.Context) error {
|
||||
id := util.PathParam(c, "id")
|
||||
|
||||
about, _ := h.cluster.About()
|
||||
@ -478,7 +477,7 @@ func (h *ClusterHandler) SetNodeState(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, "OK")
|
||||
}
|
||||
|
||||
err := h.cluster.SetNodeState("", id, state.State)
|
||||
err := h.cluster.NodeSetState("", id, state.State)
|
||||
if err != nil {
|
||||
if errors.Is(err, cluster.ErrUnsupportedNodeState) {
|
||||
return api.Err(http.StatusBadRequest, "", "%s", err.Error())
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
|
||||
@ -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{}
|
||||
|
||||
@ -260,7 +262,7 @@ func (h *ClusterHandler) ListStoreKV(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, kvs)
|
||||
}
|
||||
|
||||
// ListStoreNodes returns the list of stored node metadata
|
||||
// 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.?.?
|
||||
@ -269,8 +271,8 @@ func (h *ClusterHandler) ListStoreKV(c echo.Context) error {
|
||||
// @Success 200 {array} api.ClusterStoreNode
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /api/v3/cluster/db/node [get]
|
||||
func (h *ClusterHandler) ListStoreNodes(c echo.Context) error {
|
||||
clusternodes := h.cluster.ListNodes()
|
||||
func (h *ClusterHandler) StoreListNodes(c echo.Context) error {
|
||||
clusternodes := h.cluster.Store().NodeList()
|
||||
|
||||
nodes := []api.ClusterStoreNode{}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,59 +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/node", s.v3handler.cluster.ListStoreNodes)
|
||||
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/state", s.v3handler.cluster.GetNodeState)
|
||||
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.SetNodeState)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
3
restream/app/metadata.go
Normal file
@ -0,0 +1,3 @@
|
||||
package app
|
||||
|
||||
type Metadata interface{}
|
||||
@ -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{}
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
74
rtmp/rtmp.go
74
rtmp/rtmp.go
@ -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"
|
||||
}
|
||||
|
||||
@ -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
61
rtmp/url/url.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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, ","))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
21
vendor/github.com/datarhei/core-client-go/v16/LICENSE
generated
vendored
21
vendor/github.com/datarhei/core-client-go/v16/LICENSE
generated
vendored
@ -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.
|
||||
312
vendor/github.com/datarhei/core-client-go/v16/README.md
generated
vendored
312
vendor/github.com/datarhei/core-client-go/v16/README.md
generated
vendored
@ -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)
|
||||
33
vendor/github.com/datarhei/core-client-go/v16/api/about.go
generated
vendored
33
vendor/github.com/datarhei/core-client-go/v16/api/about.go
generated
vendored
@ -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"`
|
||||
}
|
||||
23
vendor/github.com/datarhei/core-client-go/v16/api/avstream.go
generated
vendored
23
vendor/github.com/datarhei/core-client-go/v16/api/avstream.go
generated
vendored
@ -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"`
|
||||
}
|
||||
99
vendor/github.com/datarhei/core-client-go/v16/api/cluster.go
generated
vendored
99
vendor/github.com/datarhei/core-client-go/v16/api/cluster.go
generated
vendored
@ -1,99 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ClusterNode struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
Voter bool `json:"voter"`
|
||||
Leader bool `json:"leader"`
|
||||
Address string `json:"address"`
|
||||
CreatedAt string `json:"created_at"` // RFC 3339
|
||||
Uptime int64 `json:"uptime_seconds"` // seconds
|
||||
LastContact float64 `json:"last_contact_ms"` // milliseconds
|
||||
Latency float64 `json:"latency_ms"` // milliseconds
|
||||
Core ClusterNodeCore `json:"core"`
|
||||
Resources ClusterNodeResources `json:"resources"`
|
||||
}
|
||||
|
||||
type ClusterNodeCore struct {
|
||||
Address string `json:"address"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error"`
|
||||
LastContact float64 `json:"last_contact_ms"` // milliseconds
|
||||
Latency float64 `json:"latency_ms"` // milliseconds
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type ClusterNodeResources struct {
|
||||
IsThrottling bool `json:"is_throttling"`
|
||||
NCPU float64 `json:"ncpu"`
|
||||
CPU float64 `json:"cpu_used"` // percent 0-100*npcu
|
||||
CPULimit float64 `json:"cpu_limit"` // percent 0-100*npcu
|
||||
Mem uint64 `json:"memory_used_bytes"` // bytes
|
||||
MemLimit uint64 `json:"memory_limit_bytes"` // bytes
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type ClusterRaft struct {
|
||||
Address string `json:"address"`
|
||||
State string `json:"state"`
|
||||
LastContact float64 `json:"last_contact_ms"` // milliseconds
|
||||
NumPeers uint64 `json:"num_peers"`
|
||||
LogTerm uint64 `json:"log_term"`
|
||||
LogIndex uint64 `json:"log_index"`
|
||||
}
|
||||
|
||||
type ClusterAboutLeader struct {
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
ElectedSince uint64 `json:"elected_seconds"`
|
||||
}
|
||||
|
||||
type ClusterAbout struct {
|
||||
Raft ClusterRaft `json:"raft"`
|
||||
Nodes []ClusterNode `json:"nodes"`
|
||||
Version string `json:"version"`
|
||||
Degraded bool `json:"degraded"`
|
||||
DegradedErr string `json:"degraded_error"`
|
||||
}
|
||||
|
||||
type ClusterAboutV1 struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Leader bool `json:"leader"`
|
||||
Address string `json:"address"`
|
||||
ClusterAbout
|
||||
}
|
||||
|
||||
type ClusterAboutV2 struct {
|
||||
ID string `json:"id"`
|
||||
Domains []string `json:"public_domains"`
|
||||
Leader ClusterAboutLeader `json:"leader"`
|
||||
Status string `json:"status"`
|
||||
ClusterAbout
|
||||
}
|
||||
|
||||
type ClusterNodeFiles struct {
|
||||
LastUpdate int64 `json:"last_update"` // unix timestamp
|
||||
Files map[string][]string `json:"files"`
|
||||
}
|
||||
|
||||
type ClusterLock struct {
|
||||
Name string `json:"name"`
|
||||
ValidUntil time.Time `json:"valid_until"`
|
||||
}
|
||||
|
||||
type ClusterKVSValue struct {
|
||||
Value string `json:"value"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ClusterKVS map[string]ClusterKVSValue
|
||||
|
||||
type ClusterProcessMap map[string]string
|
||||
6
vendor/github.com/datarhei/core-client-go/v16/api/command.go
generated
vendored
6
vendor/github.com/datarhei/core-client-go/v16/api/command.go
generated
vendored
@ -1,6 +0,0 @@
|
||||
package api
|
||||
|
||||
// Command is a command to send to a process
|
||||
type Command struct {
|
||||
Command string `json:"command" validate:"required" enums:"start,stop,restart,reload" jsonschema:"enum=start,enum=stop,enum=restart,enum=reload"`
|
||||
}
|
||||
524
vendor/github.com/datarhei/core-client-go/v16/api/config.go
generated
vendored
524
vendor/github.com/datarhei/core-client-go/v16/api/config.go
generated
vendored
@ -1,524 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigV1 struct {
|
||||
Version int64 `json:"version" jsonschema:"minimum=1,maximum=1"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
CheckForUpdates bool `json:"update_check"`
|
||||
Log struct {
|
||||
Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"`
|
||||
Topics []string `json:"topics"`
|
||||
MaxLines int `json:"max_lines"`
|
||||
} `json:"log"`
|
||||
DB struct {
|
||||
Dir string `json:"dir"`
|
||||
} `json:"db"`
|
||||
Host struct {
|
||||
Name []string `json:"name"`
|
||||
Auto bool `json:"auto"`
|
||||
} `json:"host"`
|
||||
API struct {
|
||||
ReadOnly bool `json:"read_only"`
|
||||
Access struct {
|
||||
HTTP struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"http"`
|
||||
HTTPS struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"https"`
|
||||
} `json:"access"`
|
||||
Auth struct {
|
||||
Enable bool `json:"enable"`
|
||||
DisableLocalhost bool `json:"disable_localhost"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
JWT struct {
|
||||
Secret string `json:"secret"`
|
||||
} `json:"jwt"`
|
||||
Auth0 struct {
|
||||
Enable bool `json:"enable"`
|
||||
Tenants []Auth0Tenant `json:"tenants"`
|
||||
} `json:"auth0"`
|
||||
} `json:"auth"`
|
||||
} `json:"api"`
|
||||
TLS struct {
|
||||
Address string `json:"address"`
|
||||
Enable bool `json:"enable"`
|
||||
Auto bool `json:"auto"`
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
} `json:"tls"`
|
||||
Storage struct {
|
||||
Disk struct {
|
||||
Dir string `json:"dir"`
|
||||
Size int64 `json:"max_size_mbytes"`
|
||||
Cache struct {
|
||||
Enable bool `json:"enable"`
|
||||
Size uint64 `json:"max_size_mbytes"`
|
||||
TTL int64 `json:"ttl_seconds"`
|
||||
FileSize uint64 `json:"max_file_size_mbytes"`
|
||||
Types []string `json:"types"`
|
||||
} `json:"cache"`
|
||||
} `json:"disk"`
|
||||
Memory struct {
|
||||
Auth struct {
|
||||
Enable bool `json:"enable"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
} `json:"auth"`
|
||||
Size int64 `json:"max_size_mbytes"`
|
||||
Purge bool `json:"purge"`
|
||||
} `json:"memory"`
|
||||
CORS struct {
|
||||
Origins []string `json:"origins"`
|
||||
} `json:"cors"`
|
||||
MimeTypes string `json:"mimetypes_file"`
|
||||
} `json:"storage"`
|
||||
RTMP struct {
|
||||
Enable bool `json:"enable"`
|
||||
EnableTLS bool `json:"enable_tls"`
|
||||
Address string `json:"address"`
|
||||
App string `json:"app"`
|
||||
Token string `json:"token"`
|
||||
} `json:"rtmp"`
|
||||
FFmpeg struct {
|
||||
Binary string `json:"binary"`
|
||||
MaxProcesses int64 `json:"max_processes"`
|
||||
Access struct {
|
||||
Input struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"input"`
|
||||
Output struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"output"`
|
||||
} `json:"access"`
|
||||
Log struct {
|
||||
MaxLines int `json:"max_lines"`
|
||||
MaxHistory int `json:"max_history"`
|
||||
} `json:"log"`
|
||||
} `json:"ffmpeg"`
|
||||
Playout struct {
|
||||
Enable bool `json:"enable"`
|
||||
MinPort int `json:"min_port"`
|
||||
MaxPort int `json:"max_port"`
|
||||
} `json:"playout"`
|
||||
Debug struct {
|
||||
Profiling bool `json:"profiling"`
|
||||
ForceGC int `json:"force_gc"`
|
||||
} `json:"debug"`
|
||||
Metrics struct {
|
||||
Enable bool `json:"enable"`
|
||||
EnablePrometheus bool `json:"enable_prometheus"`
|
||||
Range int64 `json:"range_sec"` // seconds
|
||||
Interval int64 `json:"interval_sec"` // seconds
|
||||
} `json:"metrics"`
|
||||
Sessions struct {
|
||||
Enable bool `json:"enable"`
|
||||
IPIgnoreList []string `json:"ip_ignorelist"`
|
||||
SessionTimeout int `json:"session_timeout_sec"`
|
||||
Persist bool `json:"persist"`
|
||||
PersistInterval int `json:"persist_interval_sec"`
|
||||
MaxBitrate uint64 `json:"max_bitrate_mbit"`
|
||||
MaxSessions uint64 `json:"max_sessions"`
|
||||
} `json:"sessions"`
|
||||
Service struct {
|
||||
Enable bool `json:"enable"`
|
||||
Token string `json:"token"`
|
||||
URL string `json:"url"`
|
||||
} `json:"service"`
|
||||
Router struct {
|
||||
BlockedPrefixes []string `json:"blocked_prefixes"`
|
||||
Routes map[string]string `json:"routes"`
|
||||
UIPath string `json:"ui_path"`
|
||||
} `json:"router"`
|
||||
}
|
||||
|
||||
type ConfigV2 struct {
|
||||
Version int64 `json:"version" jsonschema:"minimum=2,maximum=2"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
CheckForUpdates bool `json:"update_check"`
|
||||
Log struct {
|
||||
Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"`
|
||||
Topics []string `json:"topics"`
|
||||
MaxLines int `json:"max_lines"`
|
||||
} `json:"log"`
|
||||
DB struct {
|
||||
Dir string `json:"dir"`
|
||||
} `json:"db"`
|
||||
Host struct {
|
||||
Name []string `json:"name"`
|
||||
Auto bool `json:"auto"`
|
||||
} `json:"host"`
|
||||
API struct {
|
||||
ReadOnly bool `json:"read_only"`
|
||||
Access struct {
|
||||
HTTP struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"http"`
|
||||
HTTPS struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"https"`
|
||||
} `json:"access"`
|
||||
Auth struct {
|
||||
Enable bool `json:"enable"`
|
||||
DisableLocalhost bool `json:"disable_localhost"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
JWT struct {
|
||||
Secret string `json:"secret"`
|
||||
} `json:"jwt"`
|
||||
Auth0 struct {
|
||||
Enable bool `json:"enable"`
|
||||
Tenants []Auth0Tenant `json:"tenants"`
|
||||
} `json:"auth0"`
|
||||
} `json:"auth"`
|
||||
} `json:"api"`
|
||||
TLS struct {
|
||||
Address string `json:"address"`
|
||||
Enable bool `json:"enable"`
|
||||
Auto bool `json:"auto"`
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
} `json:"tls"`
|
||||
Storage struct {
|
||||
Disk struct {
|
||||
Dir string `json:"dir"`
|
||||
Size int64 `json:"max_size_mbytes"`
|
||||
Cache struct {
|
||||
Enable bool `json:"enable"`
|
||||
Size uint64 `json:"max_size_mbytes"`
|
||||
TTL int64 `json:"ttl_seconds"`
|
||||
FileSize uint64 `json:"max_file_size_mbytes"`
|
||||
Types []string `json:"types"`
|
||||
} `json:"cache"`
|
||||
} `json:"disk"`
|
||||
Memory struct {
|
||||
Auth struct {
|
||||
Enable bool `json:"enable"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
} `json:"auth"`
|
||||
Size int64 `json:"max_size_mbytes"`
|
||||
Purge bool `json:"purge"`
|
||||
} `json:"memory"`
|
||||
CORS struct {
|
||||
Origins []string `json:"origins"`
|
||||
} `json:"cors"`
|
||||
MimeTypes string `json:"mimetypes_file"`
|
||||
} `json:"storage"`
|
||||
RTMP struct {
|
||||
Enable bool `json:"enable"`
|
||||
EnableTLS bool `json:"enable_tls"`
|
||||
Address string `json:"address"`
|
||||
AddressTLS string `json:"address_tls"`
|
||||
App string `json:"app"`
|
||||
Token string `json:"token"`
|
||||
} `json:"rtmp"`
|
||||
SRT struct {
|
||||
Enable bool `json:"enable"`
|
||||
Address string `json:"address"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Token string `json:"token"`
|
||||
Log struct {
|
||||
Enable bool `json:"enable"`
|
||||
Topics []string `json:"topics"`
|
||||
} `json:"log"`
|
||||
} `json:"srt"`
|
||||
FFmpeg struct {
|
||||
Binary string `json:"binary"`
|
||||
MaxProcesses int64 `json:"max_processes"`
|
||||
Access struct {
|
||||
Input struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"input"`
|
||||
Output struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"output"`
|
||||
} `json:"access"`
|
||||
Log struct {
|
||||
MaxLines int `json:"max_lines"`
|
||||
MaxHistory int `json:"max_history"`
|
||||
} `json:"log"`
|
||||
} `json:"ffmpeg"`
|
||||
Playout struct {
|
||||
Enable bool `json:"enable"`
|
||||
MinPort int `json:"min_port"`
|
||||
MaxPort int `json:"max_port"`
|
||||
} `json:"playout"`
|
||||
Debug struct {
|
||||
Profiling bool `json:"profiling"`
|
||||
ForceGC int `json:"force_gc"`
|
||||
} `json:"debug"`
|
||||
Metrics struct {
|
||||
Enable bool `json:"enable"`
|
||||
EnablePrometheus bool `json:"enable_prometheus"`
|
||||
Range int64 `json:"range_sec"` // seconds
|
||||
Interval int64 `json:"interval_sec"` // seconds
|
||||
} `json:"metrics"`
|
||||
Sessions struct {
|
||||
Enable bool `json:"enable"`
|
||||
IPIgnoreList []string `json:"ip_ignorelist"`
|
||||
SessionTimeout int `json:"session_timeout_sec"`
|
||||
Persist bool `json:"persist"`
|
||||
PersistInterval int `json:"persist_interval_sec"`
|
||||
MaxBitrate uint64 `json:"max_bitrate_mbit"`
|
||||
MaxSessions uint64 `json:"max_sessions"`
|
||||
} `json:"sessions"`
|
||||
Service struct {
|
||||
Enable bool `json:"enable"`
|
||||
Token string `json:"token"`
|
||||
URL string `json:"url"`
|
||||
} `json:"service"`
|
||||
Router struct {
|
||||
BlockedPrefixes []string `json:"blocked_prefixes"`
|
||||
Routes map[string]string `json:"routes"`
|
||||
UIPath string `json:"ui_path"`
|
||||
} `json:"router"`
|
||||
}
|
||||
|
||||
type ConfigV3 struct {
|
||||
Version int64 `json:"version" jsonschema:"minimum=3,maximum=3" format:"int64"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
CheckForUpdates bool `json:"update_check"`
|
||||
Log struct {
|
||||
Level string `json:"level" enums:"debug,info,warn,error,silent" jsonschema:"enum=debug,enum=info,enum=warn,enum=error,enum=silent"`
|
||||
Topics []string `json:"topics"`
|
||||
MaxLines int `json:"max_lines" format:"int"`
|
||||
} `json:"log"`
|
||||
DB struct {
|
||||
Dir string `json:"dir"`
|
||||
} `json:"db"`
|
||||
Host struct {
|
||||
Name []string `json:"name"`
|
||||
Auto bool `json:"auto"`
|
||||
} `json:"host"`
|
||||
API struct {
|
||||
ReadOnly bool `json:"read_only"`
|
||||
Access struct {
|
||||
HTTP struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"http"`
|
||||
HTTPS struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"https"`
|
||||
} `json:"access"`
|
||||
Auth struct {
|
||||
Enable bool `json:"enable"`
|
||||
DisableLocalhost bool `json:"disable_localhost"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
JWT struct {
|
||||
Secret string `json:"secret"`
|
||||
} `json:"jwt"`
|
||||
Auth0 struct {
|
||||
Enable bool `json:"enable"`
|
||||
Tenants []Auth0Tenant `json:"tenants"`
|
||||
} `json:"auth0"`
|
||||
} `json:"auth"`
|
||||
} `json:"api"`
|
||||
TLS struct {
|
||||
Address string `json:"address"`
|
||||
Enable bool `json:"enable"`
|
||||
Auto bool `json:"auto"`
|
||||
Email string `json:"email"`
|
||||
Staging bool `json:"staging"`
|
||||
Secret string `json:"secret"`
|
||||
CertFile string `json:"cert_file"`
|
||||
KeyFile string `json:"key_file"`
|
||||
} `json:"tls"`
|
||||
Storage struct {
|
||||
Disk struct {
|
||||
Dir string `json:"dir"`
|
||||
Size int64 `json:"max_size_mbytes" format:"int64"`
|
||||
Cache struct {
|
||||
Enable bool `json:"enable"`
|
||||
Size uint64 `json:"max_size_mbytes" format:"uint64"`
|
||||
TTL int64 `json:"ttl_seconds" format:"int64"`
|
||||
FileSize uint64 `json:"max_file_size_mbytes" format:"uint64"`
|
||||
Types struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"types"`
|
||||
} `json:"cache"`
|
||||
} `json:"disk"`
|
||||
Memory struct {
|
||||
Auth struct {
|
||||
Enable bool `json:"enable"` // Deprecated, use IAM
|
||||
Username string `json:"username"` // Deprecated, use IAM
|
||||
Password string `json:"password"` // Deprecated, use IAM
|
||||
} `json:"auth"` // Deprecated, use IAM
|
||||
Size int64 `json:"max_size_mbytes" format:"int64"`
|
||||
Purge bool `json:"purge"`
|
||||
Backup struct {
|
||||
Dir string `json:"dir"`
|
||||
Patterns []string `json:"patterns"`
|
||||
} `json:"backup"`
|
||||
} `json:"memory"`
|
||||
S3 []S3Storage `json:"s3"`
|
||||
CORS struct {
|
||||
Origins []string `json:"origins"`
|
||||
} `json:"cors"`
|
||||
MimeTypes string `json:"mimetypes_file"`
|
||||
} `json:"storage"`
|
||||
RTMP struct {
|
||||
Enable bool `json:"enable"`
|
||||
EnableTLS bool `json:"enable_tls"`
|
||||
Address string `json:"address"`
|
||||
AddressTLS string `json:"address_tls"`
|
||||
App string `json:"app"`
|
||||
Token string `json:"token"` // Deprecated, use IAM
|
||||
} `json:"rtmp"`
|
||||
SRT struct {
|
||||
Enable bool `json:"enable"`
|
||||
Address string `json:"address"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Token string `json:"token"` // Deprecated, use IAM
|
||||
Log struct {
|
||||
Enable bool `json:"enable"`
|
||||
Topics []string `json:"topics"`
|
||||
} `json:"log"`
|
||||
} `json:"srt"`
|
||||
FFmpeg struct {
|
||||
Binary string `json:"binary"`
|
||||
MaxProcesses int64 `json:"max_processes" format:"int64"`
|
||||
Access struct {
|
||||
Input struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"input"`
|
||||
Output struct {
|
||||
Allow []string `json:"allow"`
|
||||
Block []string `json:"block"`
|
||||
} `json:"output"`
|
||||
} `json:"access"`
|
||||
Log struct {
|
||||
MaxLines int `json:"max_lines" format:"int"`
|
||||
MaxHistory int `json:"max_history" format:"int"`
|
||||
MaxMinimalHistory int `json:"max_minimal_history" format:"int"`
|
||||
} `json:"log"`
|
||||
} `json:"ffmpeg"`
|
||||
Playout struct {
|
||||
Enable bool `json:"enable"`
|
||||
MinPort int `json:"min_port" format:"int"`
|
||||
MaxPort int `json:"max_port" format:"int"`
|
||||
} `json:"playout"`
|
||||
Debug struct {
|
||||
Profiling bool `json:"profiling"`
|
||||
ForceGC int `json:"force_gc" format:"int"` // deprecated, use MemoryLimit instead
|
||||
MemoryLimit int64 `json:"memory_limit_mbytes" format:"int64"`
|
||||
AutoMaxProcs bool `json:"auto_max_procs"`
|
||||
AgentAddress string `json:"agent_address"`
|
||||
} `json:"debug"`
|
||||
Metrics struct {
|
||||
Enable bool `json:"enable"`
|
||||
EnablePrometheus bool `json:"enable_prometheus"`
|
||||
Range int64 `json:"range_sec" format:"int64"` // seconds
|
||||
Interval int64 `json:"interval_sec" format:"int64"` // seconds
|
||||
} `json:"metrics"`
|
||||
Sessions struct {
|
||||
Enable bool `json:"enable"`
|
||||
IPIgnoreList []string `json:"ip_ignorelist"`
|
||||
Persist bool `json:"persist"`
|
||||
PersistInterval int `json:"persist_interval_sec" format:"int"`
|
||||
SessionTimeout int `json:"session_timeout_sec" format:"int"`
|
||||
SessionLogPathPattern string `json:"session_log_path_pattern"`
|
||||
SessionLogBuffer int `json:"session_log_buffer_sec" format:"int"`
|
||||
MaxBitrate uint64 `json:"max_bitrate_mbit" format:"uint64"`
|
||||
MaxSessions uint64 `json:"max_sessions" format:"uint64"`
|
||||
} `json:"sessions"`
|
||||
Service struct {
|
||||
Enable bool `json:"enable"`
|
||||
Token string `json:"token"`
|
||||
URL string `json:"url"`
|
||||
} `json:"service"`
|
||||
Router struct {
|
||||
BlockedPrefixes []string `json:"blocked_prefixes"`
|
||||
Routes map[string]string `json:"routes"`
|
||||
UIPath string `json:"ui_path"`
|
||||
} `json:"router"`
|
||||
Resources struct {
|
||||
MaxCPUUsage float64 `json:"max_cpu_usage"` // percent 0-100
|
||||
MaxMemoryUsage float64 `json:"max_memory_usage"` // percent 0-100
|
||||
} `json:"resources"`
|
||||
Cluster struct {
|
||||
Enable bool `json:"enable"`
|
||||
Address string `json:"address"` // ip:port
|
||||
Peers []string `json:"peers"`
|
||||
StartupTimeout int64 `json:"startup_timeout_sec" format:"int64"` // seconds
|
||||
SyncInterval int64 `json:"sync_interval_sec" format:"int64"` // seconds
|
||||
NodeRecoverTimeout int64 `json:"node_recover_timeout_sec" format:"int64"` // seconds
|
||||
EmergencyLeaderTimeout int64 `json:"emergency_leader_timeout_sec" format:"int64"` // seconds
|
||||
Debug struct {
|
||||
DisableFFmpegCheck bool `json:"disable_ffmpeg_check"`
|
||||
} `json:"debug"`
|
||||
} `json:"cluster"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LoadedAt time.Time `json:"loaded_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Config interface{} `json:"config"`
|
||||
|
||||
Overrides []string `json:"overrides"`
|
||||
}
|
||||
|
||||
type Auth0Tenant struct {
|
||||
Domain string `json:"domain"`
|
||||
Audience string `json:"audience"`
|
||||
ClientID string `json:"clientid"`
|
||||
Users []string `json:"users"`
|
||||
}
|
||||
|
||||
type S3Storage struct {
|
||||
Name string `json:"name"`
|
||||
Mountpoint string `json:"mountpoint"`
|
||||
Auth S3StorageAuth `json:"auth"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
AccessKeyID string `json:"access_key_id"`
|
||||
SecretAccessKey string `json:"secret_access_key"`
|
||||
Bucket string `json:"bucket"`
|
||||
Region string `json:"region"`
|
||||
UseSSL bool `json:"use_ssl"`
|
||||
}
|
||||
|
||||
type S3StorageAuth struct {
|
||||
Enable bool `json:"enable"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// ConfigError is used to return error messages when uploading a new config
|
||||
type ConfigError map[string][]string
|
||||
|
||||
func (c ConfigError) Error() string {
|
||||
s := strings.Builder{}
|
||||
|
||||
for key, messages := range map[string][]string(c) {
|
||||
s.WriteString(fmt.Sprintf("%s: %s", key, strings.Join(messages, ",")))
|
||||
}
|
||||
|
||||
return s.String()
|
||||
}
|
||||
2
vendor/github.com/datarhei/core-client-go/v16/api/doc.go
generated
vendored
2
vendor/github.com/datarhei/core-client-go/v16/api/doc.go
generated
vendored
@ -1,2 +0,0 @@
|
||||
// Package api provides types for communicating with the REST API
|
||||
package api
|
||||
19
vendor/github.com/datarhei/core-client-go/v16/api/error.go
generated
vendored
19
vendor/github.com/datarhei/core-client-go/v16/api/error.go
generated
vendored
@ -1,19 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Error represents an error response of the API
|
||||
type Error struct {
|
||||
Code int `json:"code" jsonschema:"required"`
|
||||
Message string `json:"message" jsonschema:""`
|
||||
Details []string `json:"details" jsonschema:""`
|
||||
Body []byte `json:"-"`
|
||||
}
|
||||
|
||||
// Error returns the string representation of the error
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("code=%d, message=%s, details=%s", e.Code, e.Message, strings.Join(e.Details, " "))
|
||||
}
|
||||
22
vendor/github.com/datarhei/core-client-go/v16/api/event.go
generated
vendored
22
vendor/github.com/datarhei/core-client-go/v16/api/event.go
generated
vendored
@ -1,22 +0,0 @@
|
||||
package api
|
||||
|
||||
type Event struct {
|
||||
Timestamp int64 `json:"ts" format:"int64"`
|
||||
Level int `json:"level"`
|
||||
Component string `json:"event"`
|
||||
Message string `json:"message"`
|
||||
Caller string `json:"caller"`
|
||||
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
type EventFilter struct {
|
||||
Component string `json:"event"`
|
||||
Message string `json:"message"`
|
||||
Level string `json:"level"`
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
type EventFilters struct {
|
||||
Filters []EventFilter `json:"filters"`
|
||||
}
|
||||
23
vendor/github.com/datarhei/core-client-go/v16/api/fs.go
generated
vendored
23
vendor/github.com/datarhei/core-client-go/v16/api/fs.go
generated
vendored
@ -1,23 +0,0 @@
|
||||
package api
|
||||
|
||||
// FileInfo represents informatiion about a file on a filesystem
|
||||
type FileInfo struct {
|
||||
Name string `json:"name" jsonschema:"minLength=1"`
|
||||
Size int64 `json:"size_bytes" jsonschema:"minimum=0"`
|
||||
LastMod int64 `json:"last_modified" jsonschema:"minimum=0"`
|
||||
CoreID string `json:"core_id,omitempty"`
|
||||
}
|
||||
|
||||
type FilesystemInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Mount string `json:"mount"`
|
||||
}
|
||||
|
||||
// FilesystemOperation represents a file operation on one or more filesystems
|
||||
type FilesystemOperation struct {
|
||||
Operation string `json:"operation" validate:"required" enums:"copy,move" jsonschema:"enum=copy,enum=move"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
RateLimit uint64 `json:"bandwidth_limit_kbit"` // kbit/s
|
||||
}
|
||||
11
vendor/github.com/datarhei/core-client-go/v16/api/graph.go
generated
vendored
11
vendor/github.com/datarhei/core-client-go/v16/api/graph.go
generated
vendored
@ -1,11 +0,0 @@
|
||||
package api
|
||||
|
||||
type GraphQuery struct {
|
||||
Query string `json:"query"`
|
||||
Variables interface{} `json:"variables"`
|
||||
}
|
||||
|
||||
type GraphResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
Errors []interface{} `json:"errors"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user