mirror of
https://github.com/datarhei/core.git
synced 2025-09-30 22:02:28 +08:00
Fix cluster process update on metadata change
This commit is contained in:
@@ -1289,6 +1289,11 @@ const docTemplateClusterAPI = `{
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"startup_timeout_sec": {
|
||||||
|
"description": "seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"sync_interval_sec": {
|
"sync_interval_sec": {
|
||||||
"description": "seconds",
|
"description": "seconds",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -1745,6 +1750,9 @@ const docTemplateClusterAPI = `{
|
|||||||
},
|
},
|
||||||
"key_file": {
|
"key_file": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1281,6 +1281,11 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"startup_timeout_sec": {
|
||||||
|
"description": "seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"sync_interval_sec": {
|
"sync_interval_sec": {
|
||||||
"description": "seconds",
|
"description": "seconds",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -1737,6 +1742,9 @@
|
|||||||
},
|
},
|
||||||
"key_file": {
|
"key_file": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -243,6 +243,10 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
startup_timeout_sec:
|
||||||
|
description: seconds
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
sync_interval_sec:
|
sync_interval_sec:
|
||||||
description: seconds
|
description: seconds
|
||||||
format: int64
|
format: int64
|
||||||
@@ -552,6 +556,8 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
key_file:
|
key_file:
|
||||||
type: string
|
type: string
|
||||||
|
staging:
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
update_check:
|
update_check:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
package cluster
|
package cluster
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -571,7 +573,7 @@ func (c *cluster) doSynchronize(emergency bool) {
|
|||||||
|
|
||||||
opStack, _, reality := synchronize(wish, want, have, nodesMap, c.nodeRecoverTimeout)
|
opStack, _, reality := synchronize(wish, want, have, nodesMap, c.nodeRecoverTimeout)
|
||||||
|
|
||||||
if !emergency {
|
if !emergency && len(opStack) != 0 {
|
||||||
cmd := &store.Command{
|
cmd := &store.Command{
|
||||||
Operation: store.OpSetProcessNodeMap,
|
Operation: store.OpSetProcessNodeMap,
|
||||||
Data: store.CommandSetProcessNodeMap{
|
Data: store.CommandSetProcessNodeMap{
|
||||||
@@ -606,6 +608,55 @@ func (c *cluster) doRebalance(emergency bool) {
|
|||||||
c.applyOpStack(opStack)
|
c.applyOpStack(opStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isMetadataUpdateRequired compares two metadata. It relies on the documents 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
|
// 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.
|
// with taking the available resources on each node into account.
|
||||||
func synchronize(wish map[string]string, want []store.Process, have []proxy.Process, nodes map[string]proxy.NodeAbout, nodeRecoverTimeout time.Duration) ([]interface{}, map[string]proxy.NodeResources, map[string]string) {
|
func synchronize(wish map[string]string, want []store.Process, have []proxy.Process, nodes map[string]proxy.NodeAbout, nodeRecoverTimeout time.Duration) ([]interface{}, map[string]proxy.NodeResources, map[string]string) {
|
||||||
@@ -654,13 +705,15 @@ func synchronize(wish map[string]string, want []store.Process, have []proxy.Proc
|
|||||||
|
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
// The process is on the wantMap. Update the process if the configuration differ.
|
// The process is on the wantMap. Update the process if the configuration and/or metadata differ.
|
||||||
if !wantP.Config.Equal(haveP.Config) {
|
hasConfigChanges := !wantP.Config.Equal(haveP.Config)
|
||||||
|
hasMetadataChanges, metadata := isMetadataUpdateRequired(wantP.Metadata, haveP.Metadata)
|
||||||
|
if hasConfigChanges || hasMetadataChanges {
|
||||||
opStack = append(opStack, processOpUpdate{
|
opStack = append(opStack, processOpUpdate{
|
||||||
nodeid: haveP.NodeID,
|
nodeid: haveP.NodeID,
|
||||||
processid: haveP.Config.ProcessID(),
|
processid: haveP.Config.ProcessID(),
|
||||||
config: wantP.Config,
|
config: wantP.Config,
|
||||||
metadata: wantP.Metadata,
|
metadata: metadata,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -566,6 +566,236 @@ func TestSynchronizeAddRemove(t *testing.T) {
|
|||||||
}, reality)
|
}, reality)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSynchronizeNoUpdate(t *testing.T) {
|
||||||
|
wish := map[string]string{
|
||||||
|
"foobar@": "node1",
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []store.Process{
|
||||||
|
{
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
Config: &app.Config{
|
||||||
|
ID: "foobar",
|
||||||
|
LimitCPU: 10,
|
||||||
|
LimitMemory: 5,
|
||||||
|
Reference: "baz",
|
||||||
|
},
|
||||||
|
Order: "start",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
have := []proxy.Process{
|
||||||
|
{
|
||||||
|
NodeID: "node1",
|
||||||
|
Order: "start",
|
||||||
|
State: "running",
|
||||||
|
CPU: 12,
|
||||||
|
Mem: 5,
|
||||||
|
Runtime: 42,
|
||||||
|
Config: &app.Config{
|
||||||
|
ID: "foobar",
|
||||||
|
LimitCPU: 10,
|
||||||
|
LimitMemory: 5,
|
||||||
|
Reference: "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := map[string]proxy.NodeAbout{
|
||||||
|
"node1": {
|
||||||
|
LastContact: time.Now(),
|
||||||
|
Resources: proxy.NodeResources{
|
||||||
|
NCPU: 1,
|
||||||
|
CPU: 7,
|
||||||
|
Mem: 35,
|
||||||
|
CPULimit: 90,
|
||||||
|
MemLimit: 90,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"node2": {
|
||||||
|
LastContact: time.Now(),
|
||||||
|
Resources: proxy.NodeResources{
|
||||||
|
NCPU: 1,
|
||||||
|
CPU: 85,
|
||||||
|
Mem: 65,
|
||||||
|
CPULimit: 90,
|
||||||
|
MemLimit: 90,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
stack, _, reality := synchronize(wish, want, have, nodes, 2*time.Minute)
|
||||||
|
|
||||||
|
require.Empty(t, stack)
|
||||||
|
|
||||||
|
require.Equal(t, map[string]string{
|
||||||
|
"foobar@": "node1",
|
||||||
|
}, reality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSynchronizeUpdate(t *testing.T) {
|
||||||
|
wish := map[string]string{
|
||||||
|
"foobar@": "node1",
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []store.Process{
|
||||||
|
{
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
Config: &app.Config{
|
||||||
|
ID: "foobar",
|
||||||
|
LimitCPU: 10,
|
||||||
|
LimitMemory: 5,
|
||||||
|
Reference: "baz",
|
||||||
|
},
|
||||||
|
Order: "start",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
have := []proxy.Process{
|
||||||
|
{
|
||||||
|
NodeID: "node1",
|
||||||
|
Order: "start",
|
||||||
|
State: "running",
|
||||||
|
CPU: 12,
|
||||||
|
Mem: 5,
|
||||||
|
Runtime: 42,
|
||||||
|
Config: &app.Config{
|
||||||
|
ID: "foobar",
|
||||||
|
LimitCPU: 10,
|
||||||
|
LimitMemory: 5,
|
||||||
|
Reference: "boz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := map[string]proxy.NodeAbout{
|
||||||
|
"node1": {
|
||||||
|
LastContact: time.Now(),
|
||||||
|
Resources: proxy.NodeResources{
|
||||||
|
NCPU: 1,
|
||||||
|
CPU: 7,
|
||||||
|
Mem: 35,
|
||||||
|
CPULimit: 90,
|
||||||
|
MemLimit: 90,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"node2": {
|
||||||
|
LastContact: time.Now(),
|
||||||
|
Resources: proxy.NodeResources{
|
||||||
|
NCPU: 1,
|
||||||
|
CPU: 85,
|
||||||
|
Mem: 65,
|
||||||
|
CPULimit: 90,
|
||||||
|
MemLimit: 90,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
stack, _, reality := synchronize(wish, want, have, nodes, 2*time.Minute)
|
||||||
|
|
||||||
|
require.Equal(t, []interface{}{
|
||||||
|
processOpUpdate{
|
||||||
|
nodeid: "node1",
|
||||||
|
processid: app.ProcessID{ID: "foobar"},
|
||||||
|
config: &app.Config{
|
||||||
|
ID: "foobar",
|
||||||
|
LimitCPU: 10,
|
||||||
|
LimitMemory: 5,
|
||||||
|
Reference: "baz",
|
||||||
|
},
|
||||||
|
metadata: nil,
|
||||||
|
},
|
||||||
|
}, stack)
|
||||||
|
|
||||||
|
require.Equal(t, map[string]string{
|
||||||
|
"foobar@": "node1",
|
||||||
|
}, reality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSynchronizeUpdateMetadata(t *testing.T) {
|
||||||
|
wish := map[string]string{
|
||||||
|
"foobar@": "node1",
|
||||||
|
}
|
||||||
|
|
||||||
|
want := []store.Process{
|
||||||
|
{
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
Config: &app.Config{
|
||||||
|
ID: "foobar",
|
||||||
|
LimitCPU: 10,
|
||||||
|
LimitMemory: 5,
|
||||||
|
Reference: "boz",
|
||||||
|
},
|
||||||
|
Order: "start",
|
||||||
|
Metadata: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
have := []proxy.Process{
|
||||||
|
{
|
||||||
|
NodeID: "node1",
|
||||||
|
Order: "start",
|
||||||
|
State: "running",
|
||||||
|
CPU: 12,
|
||||||
|
Mem: 5,
|
||||||
|
Runtime: 42,
|
||||||
|
Config: &app.Config{
|
||||||
|
ID: "foobar",
|
||||||
|
LimitCPU: 10,
|
||||||
|
LimitMemory: 5,
|
||||||
|
Reference: "boz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := map[string]proxy.NodeAbout{
|
||||||
|
"node1": {
|
||||||
|
LastContact: time.Now(),
|
||||||
|
Resources: proxy.NodeResources{
|
||||||
|
NCPU: 1,
|
||||||
|
CPU: 7,
|
||||||
|
Mem: 35,
|
||||||
|
CPULimit: 90,
|
||||||
|
MemLimit: 90,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"node2": {
|
||||||
|
LastContact: time.Now(),
|
||||||
|
Resources: proxy.NodeResources{
|
||||||
|
NCPU: 1,
|
||||||
|
CPU: 85,
|
||||||
|
Mem: 65,
|
||||||
|
CPULimit: 90,
|
||||||
|
MemLimit: 90,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
stack, _, reality := synchronize(wish, want, have, nodes, 2*time.Minute)
|
||||||
|
|
||||||
|
require.Equal(t, []interface{}{
|
||||||
|
processOpUpdate{
|
||||||
|
nodeid: "node1",
|
||||||
|
processid: app.ProcessID{ID: "foobar"},
|
||||||
|
config: &app.Config{
|
||||||
|
ID: "foobar",
|
||||||
|
LimitCPU: 10,
|
||||||
|
LimitMemory: 5,
|
||||||
|
Reference: "boz",
|
||||||
|
},
|
||||||
|
metadata: map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, stack)
|
||||||
|
|
||||||
|
require.Equal(t, map[string]string{
|
||||||
|
"foobar@": "node1",
|
||||||
|
}, reality)
|
||||||
|
}
|
||||||
|
|
||||||
func TestSynchronizeWaitDisconnectedNode(t *testing.T) {
|
func TestSynchronizeWaitDisconnectedNode(t *testing.T) {
|
||||||
wish := map[string]string{
|
wish := map[string]string{
|
||||||
"foobar1@": "node1",
|
"foobar1@": "node1",
|
||||||
@@ -1569,3 +1799,57 @@ func TestCreateReferenceAffinityNodeMap(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, affinityMap)
|
}, affinityMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsMetadataUpdateRequired(t *testing.T) {
|
||||||
|
want1 := map[string]interface{}{
|
||||||
|
"foo": "boz",
|
||||||
|
"sum": "sum",
|
||||||
|
"sim": []string{"id", "sam"},
|
||||||
|
}
|
||||||
|
|
||||||
|
have := map[string]interface{}{
|
||||||
|
"sim": []string{"id", "sam"},
|
||||||
|
"foo": "boz",
|
||||||
|
"sum": "sum",
|
||||||
|
}
|
||||||
|
|
||||||
|
changes, _ := isMetadataUpdateRequired(want1, have)
|
||||||
|
require.False(t, changes)
|
||||||
|
|
||||||
|
want2 := map[string]interface{}{
|
||||||
|
"sim": []string{"id", "sam"},
|
||||||
|
"foo": "boz",
|
||||||
|
}
|
||||||
|
|
||||||
|
changes, metadata := isMetadataUpdateRequired(want2, have)
|
||||||
|
require.True(t, changes)
|
||||||
|
require.Equal(t, map[string]interface{}{
|
||||||
|
"sim": []string{"id", "sam"},
|
||||||
|
"foo": "boz",
|
||||||
|
"sum": nil,
|
||||||
|
}, metadata)
|
||||||
|
|
||||||
|
want3 := map[string]interface{}{
|
||||||
|
"sim": []string{"id", "sim"},
|
||||||
|
"foo": "boz",
|
||||||
|
"sum": "sum",
|
||||||
|
}
|
||||||
|
|
||||||
|
changes, metadata = isMetadataUpdateRequired(want3, have)
|
||||||
|
require.True(t, changes)
|
||||||
|
require.Equal(t, map[string]interface{}{
|
||||||
|
"sim": []string{"id", "sim"},
|
||||||
|
"foo": "boz",
|
||||||
|
"sum": "sum",
|
||||||
|
}, metadata)
|
||||||
|
|
||||||
|
want4 := map[string]interface{}{}
|
||||||
|
|
||||||
|
changes, metadata = isMetadataUpdateRequired(want4, have)
|
||||||
|
require.True(t, changes)
|
||||||
|
require.Equal(t, map[string]interface{}{
|
||||||
|
"sim": nil,
|
||||||
|
"foo": nil,
|
||||||
|
"sum": nil,
|
||||||
|
}, metadata)
|
||||||
|
}
|
||||||
|
@@ -891,7 +891,9 @@ func (n *node) ProcessList(options ProcessListOptions) ([]clientapi.Process, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *node) ProxyProcessList() ([]Process, error) {
|
func (n *node) ProxyProcessList() ([]Process, error) {
|
||||||
list, err := n.ProcessList(ProcessListOptions{})
|
list, err := n.ProcessList(ProcessListOptions{
|
||||||
|
Filter: []string{"config", "state", "metadata"},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@@ -801,6 +801,7 @@ func (s *store) GetProcess(id app.ProcessID) (Process, error) {
|
|||||||
CreatedAt: process.CreatedAt,
|
CreatedAt: process.CreatedAt,
|
||||||
UpdatedAt: process.UpdatedAt,
|
UpdatedAt: process.UpdatedAt,
|
||||||
Config: process.Config.Clone(),
|
Config: process.Config.Clone(),
|
||||||
|
Order: process.Order,
|
||||||
Metadata: process.Metadata,
|
Metadata: process.Metadata,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
67
docs/docs.go
67
docs/docs.go
@@ -245,14 +245,14 @@ const docTemplate = `{
|
|||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "List of processes in the cluster",
|
"description": "List of processes in the cluster DB",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"v16.?.?"
|
"v16.?.?"
|
||||||
],
|
],
|
||||||
"summary": "List of processes in the cluster",
|
"summary": "List of processes in the cluster DB",
|
||||||
"operationId": "cluster-3-db-list-processes",
|
"operationId": "cluster-3-db-list-processes",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -267,6 +267,47 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v3/cluster/db/process/:id": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get a process in the cluster DB",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"v16.?.?"
|
||||||
|
],
|
||||||
|
"summary": "Get a process in the cluster DB",
|
||||||
|
"operationId": "cluster-3-db-get-process",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Process ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Domain to act on",
|
||||||
|
"name": "domain",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api.Process"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v3/cluster/db/user": {
|
"/api/v3/cluster/db/user": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1216,6 +1257,12 @@ const docTemplate = `{
|
|||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Domain to act on",
|
||||||
|
"name": "domain",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Process config",
|
"description": "Process config",
|
||||||
"name": "config",
|
"name": "config",
|
||||||
@@ -4517,6 +4564,11 @@ const docTemplate = `{
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"startup_timeout_sec": {
|
||||||
|
"description": "seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"sync_interval_sec": {
|
"sync_interval_sec": {
|
||||||
"description": "seconds",
|
"description": "seconds",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -4973,6 +5025,9 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"key_file": {
|
"key_file": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6736,6 +6791,11 @@ const docTemplate = `{
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"startup_timeout_sec": {
|
||||||
|
"description": "seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"sync_interval_sec": {
|
"sync_interval_sec": {
|
||||||
"description": "seconds",
|
"description": "seconds",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -7192,6 +7252,9 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"key_file": {
|
"key_file": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -237,14 +237,14 @@
|
|||||||
"ApiKeyAuth": []
|
"ApiKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "List of processes in the cluster",
|
"description": "List of processes in the cluster DB",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"v16.?.?"
|
"v16.?.?"
|
||||||
],
|
],
|
||||||
"summary": "List of processes in the cluster",
|
"summary": "List of processes in the cluster DB",
|
||||||
"operationId": "cluster-3-db-list-processes",
|
"operationId": "cluster-3-db-list-processes",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -259,6 +259,47 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v3/cluster/db/process/:id": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Get a process in the cluster DB",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"v16.?.?"
|
||||||
|
],
|
||||||
|
"summary": "Get a process in the cluster DB",
|
||||||
|
"operationId": "cluster-3-db-get-process",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Process ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Domain to act on",
|
||||||
|
"name": "domain",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api.Process"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v3/cluster/db/user": {
|
"/api/v3/cluster/db/user": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
@@ -1208,6 +1249,12 @@
|
|||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Domain to act on",
|
||||||
|
"name": "domain",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Process config",
|
"description": "Process config",
|
||||||
"name": "config",
|
"name": "config",
|
||||||
@@ -4509,6 +4556,11 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"startup_timeout_sec": {
|
||||||
|
"description": "seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"sync_interval_sec": {
|
"sync_interval_sec": {
|
||||||
"description": "seconds",
|
"description": "seconds",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -4965,6 +5017,9 @@
|
|||||||
},
|
},
|
||||||
"key_file": {
|
"key_file": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -6728,6 +6783,11 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"startup_timeout_sec": {
|
||||||
|
"description": "seconds",
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
"sync_interval_sec": {
|
"sync_interval_sec": {
|
||||||
"description": "seconds",
|
"description": "seconds",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
@@ -7184,6 +7244,9 @@
|
|||||||
},
|
},
|
||||||
"key_file": {
|
"key_file": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"staging": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -282,6 +282,10 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
startup_timeout_sec:
|
||||||
|
description: seconds
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
sync_interval_sec:
|
sync_interval_sec:
|
||||||
description: seconds
|
description: seconds
|
||||||
format: int64
|
format: int64
|
||||||
@@ -591,6 +595,8 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
key_file:
|
key_file:
|
||||||
type: string
|
type: string
|
||||||
|
staging:
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
update_check:
|
update_check:
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -1848,6 +1854,10 @@ definitions:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
startup_timeout_sec:
|
||||||
|
description: seconds
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
sync_interval_sec:
|
sync_interval_sec:
|
||||||
description: seconds
|
description: seconds
|
||||||
format: int64
|
format: int64
|
||||||
@@ -2157,6 +2167,8 @@ definitions:
|
|||||||
type: boolean
|
type: boolean
|
||||||
key_file:
|
key_file:
|
||||||
type: string
|
type: string
|
||||||
|
staging:
|
||||||
|
type: boolean
|
||||||
type: object
|
type: object
|
||||||
update_check:
|
update_check:
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -2532,7 +2544,7 @@ paths:
|
|||||||
- v16.?.?
|
- v16.?.?
|
||||||
/api/v3/cluster/db/process:
|
/api/v3/cluster/db/process:
|
||||||
get:
|
get:
|
||||||
description: List of processes in the cluster
|
description: List of processes in the cluster DB
|
||||||
operationId: cluster-3-db-list-processes
|
operationId: cluster-3-db-list-processes
|
||||||
produces:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
@@ -2545,7 +2557,33 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
security:
|
security:
|
||||||
- ApiKeyAuth: []
|
- ApiKeyAuth: []
|
||||||
summary: List of processes in the cluster
|
summary: List of processes in the cluster DB
|
||||||
|
tags:
|
||||||
|
- v16.?.?
|
||||||
|
/api/v3/cluster/db/process/:id:
|
||||||
|
get:
|
||||||
|
description: Get a process in the cluster DB
|
||||||
|
operationId: cluster-3-db-get-process
|
||||||
|
parameters:
|
||||||
|
- description: Process ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
- description: Domain to act on
|
||||||
|
in: query
|
||||||
|
name: domain
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/api.Process'
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
|
summary: Get a process in the cluster DB
|
||||||
tags:
|
tags:
|
||||||
- v16.?.?
|
- v16.?.?
|
||||||
/api/v3/cluster/db/user:
|
/api/v3/cluster/db/user:
|
||||||
@@ -3197,6 +3235,10 @@ paths:
|
|||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- description: Domain to act on
|
||||||
|
in: query
|
||||||
|
name: domain
|
||||||
|
type: string
|
||||||
- description: Process config
|
- description: Process config
|
||||||
in: body
|
in: body
|
||||||
name: config
|
name: config
|
||||||
|
@@ -65,7 +65,7 @@ type ProcessConfig struct {
|
|||||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal converts a process config in API representation to a restreamer process config
|
// Marshal converts a process config in API representation to a restreamer process config and metadata
|
||||||
func (cfg *ProcessConfig) Marshal() (*app.Config, map[string]interface{}) {
|
func (cfg *ProcessConfig) Marshal() (*app.Config, map[string]interface{}) {
|
||||||
p := &app.Config{
|
p := &app.Config{
|
||||||
ID: cfg.ID,
|
ID: cfg.ID,
|
||||||
|
@@ -209,6 +209,10 @@ func (h *ClusterHandler) GetAllNodesProcess(c echo.Context) error {
|
|||||||
return api.Err(http.StatusNotFound, "", "Unknown process ID: %s", id)
|
return api.Err(http.StatusNotFound, "", "Unknown process ID: %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if procs[0].Domain != domain {
|
||||||
|
return api.Err(http.StatusNotFound, "", "Unknown process ID: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, procs[0])
|
return c.JSON(http.StatusOK, procs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,8 +421,8 @@ func (h *ClusterHandler) ListNodeProcesses(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListStoreProcesses returns the list of processes stored in the DB of the cluster
|
// ListStoreProcesses returns the list of processes stored in the DB of the cluster
|
||||||
// @Summary List of processes in the cluster
|
// @Summary List of processes in the cluster DB
|
||||||
// @Description List of processes in the cluster
|
// @Description List of processes in the cluster DB
|
||||||
// @Tags v16.?.?
|
// @Tags v16.?.?
|
||||||
// @ID cluster-3-db-list-processes
|
// @ID cluster-3-db-list-processes
|
||||||
// @Produce json
|
// @Produce json
|
||||||
@@ -427,14 +431,13 @@ func (h *ClusterHandler) ListNodeProcesses(c echo.Context) error {
|
|||||||
// @Router /api/v3/cluster/db/process [get]
|
// @Router /api/v3/cluster/db/process [get]
|
||||||
func (h *ClusterHandler) ListStoreProcesses(c echo.Context) error {
|
func (h *ClusterHandler) ListStoreProcesses(c echo.Context) error {
|
||||||
ctxuser := util.DefaultContext(c, "user", "")
|
ctxuser := util.DefaultContext(c, "user", "")
|
||||||
domain := util.DefaultQuery(c, "domain", "")
|
|
||||||
|
|
||||||
procs := h.cluster.ListProcesses()
|
procs := h.cluster.ListProcesses()
|
||||||
|
|
||||||
processes := []api.Process{}
|
processes := []api.Process{}
|
||||||
|
|
||||||
for _, p := range procs {
|
for _, p := range procs {
|
||||||
if !h.iam.Enforce(ctxuser, domain, "process:"+p.Config.ID, "read") {
|
if !h.iam.Enforce(ctxuser, p.Config.Domain, "process:"+p.Config.ID, "read") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,6 +467,59 @@ func (h *ClusterHandler) ListStoreProcesses(c echo.Context) error {
|
|||||||
return c.JSON(http.StatusOK, processes)
|
return c.JSON(http.StatusOK, processes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GerStoreProcess returns a process stored in the DB of the cluster
|
||||||
|
// @Summary Get a process in the cluster DB
|
||||||
|
// @Description Get a process in the cluster DB
|
||||||
|
// @Tags v16.?.?
|
||||||
|
// @ID cluster-3-db-get-process
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Process ID"
|
||||||
|
// @Param domain query string false "Domain to act on"
|
||||||
|
// @Success 200 {object} api.Process
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /api/v3/cluster/db/process/:id [get]
|
||||||
|
func (h *ClusterHandler) GetStoreProcess(c echo.Context) error {
|
||||||
|
ctxuser := util.DefaultContext(c, "user", "")
|
||||||
|
domain := util.DefaultQuery(c, "domain", "")
|
||||||
|
id := util.PathParam(c, "id")
|
||||||
|
|
||||||
|
pid := app.ProcessID{
|
||||||
|
ID: id,
|
||||||
|
Domain: domain,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.iam.Enforce(ctxuser, domain, "process:"+id, "read") {
|
||||||
|
return api.Err(http.StatusForbidden, "", "API user %s is not allowed to read this process", ctxuser)
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := h.cluster.GetProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return api.Err(http.StatusNotFound, "", "process not found: %s in domain '%s'", pid.ID, pid.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
process := api.Process{
|
||||||
|
ID: p.Config.ID,
|
||||||
|
Owner: p.Config.Owner,
|
||||||
|
Domain: p.Config.Domain,
|
||||||
|
Type: "ffmpeg",
|
||||||
|
Reference: p.Config.Reference,
|
||||||
|
CreatedAt: p.CreatedAt.Unix(),
|
||||||
|
UpdatedAt: p.UpdatedAt.Unix(),
|
||||||
|
Metadata: p.Metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &api.ProcessConfig{}
|
||||||
|
config.Unmarshal(p.Config)
|
||||||
|
|
||||||
|
process.Config = config
|
||||||
|
|
||||||
|
process.State = &api.ProcessState{
|
||||||
|
Order: p.Order,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, process)
|
||||||
|
}
|
||||||
|
|
||||||
// Add adds a new process to the cluster
|
// Add adds a new process to the cluster
|
||||||
// @Summary Add a new process
|
// @Summary Add a new process
|
||||||
// @Description Add a new FFmpeg process
|
// @Description Add a new FFmpeg process
|
||||||
@@ -531,6 +587,7 @@ func (h *ClusterHandler) AddProcess(c echo.Context) error {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param id path string true "Process ID"
|
// @Param id path string true "Process ID"
|
||||||
|
// @Param domain query string false "Domain to act on"
|
||||||
// @Param config body api.ProcessConfig true "Process config"
|
// @Param config body api.ProcessConfig true "Process config"
|
||||||
// @Success 200 {object} api.ProcessConfig
|
// @Success 200 {object} api.ProcessConfig
|
||||||
// @Failure 400 {object} api.Error
|
// @Failure 400 {object} api.Error
|
||||||
@@ -560,7 +617,7 @@ func (h *ClusterHandler) UpdateProcess(c echo.Context) error {
|
|||||||
|
|
||||||
current, err := h.cluster.GetProcess(pid)
|
current, err := h.cluster.GetProcess(pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return api.Err(http.StatusNotFound, "", "process not found: %s", id)
|
return api.Err(http.StatusNotFound, "", "process not found: %s in domain '%s'", pid.ID, pid.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefill the config with the current values
|
// Prefill the config with the current values
|
||||||
@@ -584,7 +641,7 @@ func (h *ClusterHandler) UpdateProcess(c echo.Context) error {
|
|||||||
|
|
||||||
if err := h.cluster.UpdateProcess("", pid, config); err != nil {
|
if err := h.cluster.UpdateProcess("", pid, config); err != nil {
|
||||||
if err == restream.ErrUnknownProcess {
|
if err == restream.ErrUnknownProcess {
|
||||||
return api.Err(http.StatusNotFound, "", "process not found: %s", id)
|
return api.Err(http.StatusNotFound, "", "process not found: %s in domain '%s'", pid.ID, pid.Domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.Err(http.StatusBadRequest, "", "process can't be updated: %s", err.Error())
|
return api.Err(http.StatusBadRequest, "", "process can't be updated: %s", err.Error())
|
||||||
|
@@ -693,6 +693,7 @@ func (s *server) setRoutesV3(v3 *echo.Group) {
|
|||||||
v3.GET("/cluster/snapshot", s.v3handler.cluster.GetSnapshot)
|
v3.GET("/cluster/snapshot", s.v3handler.cluster.GetSnapshot)
|
||||||
|
|
||||||
v3.GET("/cluster/db/process", s.v3handler.cluster.ListStoreProcesses)
|
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", s.v3handler.cluster.ListStoreIdentities)
|
||||||
v3.GET("/cluster/db/user/:name", s.v3handler.cluster.ListStoreIdentity)
|
v3.GET("/cluster/db/user/:name", s.v3handler.cluster.ListStoreIdentity)
|
||||||
v3.GET("/cluster/db/policies", s.v3handler.cluster.ListStorePolicies)
|
v3.GET("/cluster/db/policies", s.v3handler.cluster.ListStorePolicies)
|
||||||
|
@@ -150,7 +150,6 @@ func (config *Config) Hash() []byte {
|
|||||||
b.WriteString(config.Reference)
|
b.WriteString(config.Reference)
|
||||||
b.WriteString(config.Owner)
|
b.WriteString(config.Owner)
|
||||||
b.WriteString(config.Domain)
|
b.WriteString(config.Domain)
|
||||||
b.WriteString(config.FFVersion)
|
|
||||||
b.WriteString(config.Scheduler)
|
b.WriteString(config.Scheduler)
|
||||||
b.WriteString(strings.Join(config.Options, ","))
|
b.WriteString(strings.Join(config.Options, ","))
|
||||||
b.WriteString(strings.Join(config.LogPatterns, ","))
|
b.WriteString(strings.Join(config.LogPatterns, ","))
|
||||||
|
@@ -50,7 +50,7 @@ func TestConfigHash(t *testing.T) {
|
|||||||
|
|
||||||
hash1 := config.Hash()
|
hash1 := config.Hash()
|
||||||
|
|
||||||
require.Equal(t, []byte{0x23, 0x5d, 0xcc, 0x36, 0x77, 0xa1, 0x49, 0x7c, 0xcd, 0x8a, 0x72, 0x6a, 0x6c, 0xa2, 0xc3, 0x24}, hash1)
|
require.Equal(t, []byte{0x7e, 0xae, 0x5b, 0xc3, 0xad, 0xe3, 0x9a, 0xfc, 0xd3, 0x49, 0x15, 0x28, 0x93, 0x17, 0xc5, 0xbf}, hash1)
|
||||||
|
|
||||||
config.Reconnect = false
|
config.Reconnect = false
|
||||||
|
|
||||||
|
@@ -1175,16 +1175,16 @@ func (r *restream) UpdateProcess(id app.ProcessID, config *app.Config) error {
|
|||||||
return ErrUnknownProcess
|
return ErrUnknownProcess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the new config has the same hash as the current config, do nothing.
|
||||||
|
if task.process.Config.Equal(config) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
t, err := r.createTask(config)
|
t, err := r.createTask(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the new config has the same hash as the current config, do nothing.
|
|
||||||
if task.config.Equal(t.config) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tid := t.ID()
|
tid := t.ID()
|
||||||
|
|
||||||
if !tid.Equal(id) {
|
if !tid.Equal(id) {
|
||||||
|
Reference in New Issue
Block a user