Extreact & refactor automation scripts & triggets
Automation (scripts and triggers) is now a standalone package and can be used in other services.
This commit is contained in:
@@ -763,6 +763,28 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "runScript",
|
||||
"method": "POST",
|
||||
"title": "Trigger a specific script on record",
|
||||
"path": "/run-script",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "recordID",
|
||||
"required": false,
|
||||
"title": "Record ID"
|
||||
},
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "Script ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "upload",
|
||||
"path": "/attachment",
|
||||
@@ -937,192 +959,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Triggers",
|
||||
"description": "Compose Triggers",
|
||||
"entrypoint": "trigger",
|
||||
"path": "/namespace/{namespaceID}/trigger",
|
||||
"authentication": [],
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "namespaceID",
|
||||
"required": true,
|
||||
"title": "Namespace ID"
|
||||
}
|
||||
]
|
||||
},
|
||||
"struct": [
|
||||
{
|
||||
"imports": [
|
||||
"time"
|
||||
]
|
||||
}
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"name": "list",
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"title": "List available triggers",
|
||||
"parameters": {
|
||||
"get": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "moduleID",
|
||||
"required": false,
|
||||
"title": "Filter triggers by module"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "query",
|
||||
"required": false,
|
||||
"title": "Search query"
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"type": "uint",
|
||||
"required": false,
|
||||
"title": "Page number (0 based)"
|
||||
},
|
||||
{
|
||||
"name": "perPage",
|
||||
"type": "uint",
|
||||
"required": false,
|
||||
"title": "Returned items per page (default 50)"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create",
|
||||
"method": "POST",
|
||||
"title": "Create trigger",
|
||||
"path": "/",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "moduleID",
|
||||
"required": false,
|
||||
"title": "Module ID"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "Name"
|
||||
},
|
||||
{
|
||||
"type": "[]string",
|
||||
"name": "actions",
|
||||
"required": false,
|
||||
"title": "Actions that trigger this trigger"
|
||||
},
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "enabled",
|
||||
"required": false,
|
||||
"title": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "source",
|
||||
"required": false,
|
||||
"title": "Trigger source code"
|
||||
},
|
||||
{
|
||||
"type": "*time.Time",
|
||||
"name": "updatedAt",
|
||||
"required": false,
|
||||
"title": "Last update (or creation) date"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read",
|
||||
"path": "/{triggerID}",
|
||||
"method": "GET",
|
||||
"title": "Get trigger details",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "Trigger ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "update",
|
||||
"method": "POST",
|
||||
"title": "Update trigger",
|
||||
"path": "/{triggerID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "Trigger ID"
|
||||
}
|
||||
],
|
||||
"post": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "moduleID",
|
||||
"required": false,
|
||||
"title": "Module ID"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "Name"
|
||||
},
|
||||
{
|
||||
"type": "[]string",
|
||||
"name": "actions",
|
||||
"required": false,
|
||||
"title": "Actions that trigger this trigger"
|
||||
},
|
||||
{
|
||||
"type": "bool",
|
||||
"name": "enabled",
|
||||
"required": false,
|
||||
"title": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "source",
|
||||
"required": false,
|
||||
"title": "Trigger source code"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete",
|
||||
"path": "/{triggerID}",
|
||||
"method": "Delete",
|
||||
"title": "Delete trigger",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "Trigger ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Notifications",
|
||||
"description": "Compose Notifications",
|
||||
@@ -1447,5 +1283,385 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Automation scripts",
|
||||
"entrypoint": "automation_script",
|
||||
"path": "/namespace/{namespaceID}/automation/script",
|
||||
"authentication": [
|
||||
"Client ID",
|
||||
"Session ID"
|
||||
],
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "namespaceID",
|
||||
"required": true,
|
||||
"title": "Namespace ID"
|
||||
}
|
||||
]
|
||||
},
|
||||
"apis": [
|
||||
{
|
||||
"name": "list",
|
||||
"method": "GET",
|
||||
"title": "List/read automation script",
|
||||
"path": "/",
|
||||
"parameters": {
|
||||
"get": [
|
||||
{
|
||||
"name": "query",
|
||||
"required": false,
|
||||
"title": "Search query to match against automation script",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "resource",
|
||||
"required": false,
|
||||
"title": "Limit by resource (via trigger)",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "incDeleted",
|
||||
"required": false,
|
||||
"title": "Include deleted scripts",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"type": "uint",
|
||||
"required": false,
|
||||
"title": "Page number (0 based)"
|
||||
},
|
||||
{
|
||||
"name": "perPage",
|
||||
"type": "uint",
|
||||
"required": false,
|
||||
"title": "Returned items per page (default 50)"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create",
|
||||
"method": "POST",
|
||||
"title": "Add new automation script ",
|
||||
"path": "/",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "name",
|
||||
"title": "automation name",
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "sourceRef",
|
||||
"title": "Source URL",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "Source code",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "runAs",
|
||||
"title": "Run as specific user",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "runInUA",
|
||||
"title": "Run script in user-agent (browser)",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"title": "Script timeout (in milliseconds)",
|
||||
"type": "uint"
|
||||
},
|
||||
{
|
||||
"name": "critical",
|
||||
"title": "Is it critical to run this script successfully",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "async",
|
||||
"title": "Will this script be ran asynchronously",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read",
|
||||
"method": "GET",
|
||||
"title": "Read automation script by ID",
|
||||
"path": "/{scriptID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "automation script ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "update",
|
||||
"method": "POST",
|
||||
"title": "Update automation script",
|
||||
"path": "/{scriptID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "Automation script ID"
|
||||
}
|
||||
],
|
||||
"post": [
|
||||
{
|
||||
"name": "name",
|
||||
"title": "Script name",
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "sourceRef",
|
||||
"title": "Source URL",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "Source code",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "runAs",
|
||||
"title": "Run script as specific user",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "runInUA",
|
||||
"title": "Run script in user-agent (browser)",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"title": "Run script in user-agent (browser)",
|
||||
"type": "uint"
|
||||
},
|
||||
{
|
||||
"name": "critical",
|
||||
"title": "Is it critical to run this script successfully",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "async",
|
||||
"title": "Will this script be ran asynchronously",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete",
|
||||
"method": "DELETE",
|
||||
"title": "Delete script",
|
||||
"path": "/{scriptID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "automation ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Automation script triggers",
|
||||
"entrypoint": "automation_trigger",
|
||||
"path": "/namespace/{namespaceID}/automation/script/{scriptID}/trigger",
|
||||
"authentication": [
|
||||
"Client ID",
|
||||
"Session ID"
|
||||
],
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "namespaceID",
|
||||
"required": true,
|
||||
"title": "Namespace ID"
|
||||
},
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "Script ID"
|
||||
}
|
||||
]
|
||||
},
|
||||
"apis": [
|
||||
{
|
||||
"name": "list",
|
||||
"method": "GET",
|
||||
"title": "List/read automation script triggers",
|
||||
"path": "/",
|
||||
"parameters": {
|
||||
"get": [
|
||||
{
|
||||
"name": "resource",
|
||||
"required": false,
|
||||
"title": "Only triggers of a specific resource",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"required": false,
|
||||
"title": "Only triggers of a specific event",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "incDeleted",
|
||||
"required": false,
|
||||
"title": "Include deleted scripts",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"type": "uint",
|
||||
"required": false,
|
||||
"title": "Page number (0 based)"
|
||||
},
|
||||
{
|
||||
"name": "perPage",
|
||||
"type": "uint",
|
||||
"required": false,
|
||||
"title": "Returned items per page (default 50)"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create",
|
||||
"method": "POST",
|
||||
"title": "Add new automation script trigger",
|
||||
"path": "/",
|
||||
"parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "resource",
|
||||
"title": "Resource",
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"title": "Event",
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"title": "Event",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read",
|
||||
"method": "GET",
|
||||
"title": "Read automation script trigger by ID",
|
||||
"path": "/{triggerID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "Automation script trigger ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "update",
|
||||
"method": "POST",
|
||||
"title": "Update automation script trigger",
|
||||
"path": "/{triggerID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "Automation script trigger ID"
|
||||
}
|
||||
],
|
||||
"post": [
|
||||
{
|
||||
"name": "resource",
|
||||
"title": "Resource",
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"title": "Event",
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"title": "Event",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "delete",
|
||||
"method": "DELETE",
|
||||
"title": "Delete script",
|
||||
"path": "/{triggerID}",
|
||||
"parameters": {
|
||||
"path": [
|
||||
{
|
||||
"type": "uint64",
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "automation script trigger ID"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
213
api/compose/spec/automation_script.json
Normal file
213
api/compose/spec/automation_script.json
Normal file
@@ -0,0 +1,213 @@
|
||||
{
|
||||
"Title": "Automation scripts",
|
||||
"Interface": "Automation_script",
|
||||
"Struct": null,
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "namespaceID",
|
||||
"required": true,
|
||||
"title": "Namespace ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Protocol": "",
|
||||
"Authentication": [
|
||||
"Client ID",
|
||||
"Session ID"
|
||||
],
|
||||
"Path": "/namespace/{namespaceID}/automation/script",
|
||||
"APIs": [
|
||||
{
|
||||
"Name": "list",
|
||||
"Method": "GET",
|
||||
"Title": "List/read automation script",
|
||||
"Path": "/",
|
||||
"Parameters": {
|
||||
"get": [
|
||||
{
|
||||
"name": "query",
|
||||
"required": false,
|
||||
"title": "Search query to match against automation script",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "resource",
|
||||
"required": false,
|
||||
"title": "Limit by resource (via trigger)",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "incDeleted",
|
||||
"required": false,
|
||||
"title": "Include deleted scripts",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"required": false,
|
||||
"title": "Page number (0 based)",
|
||||
"type": "uint"
|
||||
},
|
||||
{
|
||||
"name": "perPage",
|
||||
"required": false,
|
||||
"title": "Returned items per page (default 50)",
|
||||
"type": "uint"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "create",
|
||||
"Method": "POST",
|
||||
"Title": "Add new automation script ",
|
||||
"Path": "/",
|
||||
"Parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "automation name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "sourceRef",
|
||||
"title": "Source URL",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "Source code",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "runAs",
|
||||
"title": "Run as specific user",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "runInUA",
|
||||
"title": "Run script in user-agent (browser)",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"title": "Script timeout (in milliseconds)",
|
||||
"type": "uint"
|
||||
},
|
||||
{
|
||||
"name": "critical",
|
||||
"title": "Is it critical to run this script successfully",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "async",
|
||||
"title": "Will this script be ran asynchronously",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "read",
|
||||
"Method": "GET",
|
||||
"Title": "Read automation script by ID",
|
||||
"Path": "/{scriptID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "automation script ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "update",
|
||||
"Method": "POST",
|
||||
"Title": "Update automation script",
|
||||
"Path": "/{scriptID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "Automation script ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"post": [
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "Script name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "sourceRef",
|
||||
"title": "Source URL",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "Source code",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "runAs",
|
||||
"title": "Run script as specific user",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "runInUA",
|
||||
"title": "Run script in user-agent (browser)",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"title": "Run script in user-agent (browser)",
|
||||
"type": "uint"
|
||||
},
|
||||
{
|
||||
"name": "critical",
|
||||
"title": "Is it critical to run this script successfully",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "async",
|
||||
"title": "Will this script be ran asynchronously",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"type": "bool"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "delete",
|
||||
"Method": "DELETE",
|
||||
"Title": "Delete script",
|
||||
"Path": "/{scriptID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "automation ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,14 +1,7 @@
|
||||
{
|
||||
"Title": "Triggers",
|
||||
"Description": "Compose Triggers",
|
||||
"Interface": "Trigger",
|
||||
"Struct": [
|
||||
{
|
||||
"imports": [
|
||||
"time"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Title": "Automation script triggers",
|
||||
"Interface": "Automation_trigger",
|
||||
"Struct": null,
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
@@ -16,32 +9,47 @@
|
||||
"required": true,
|
||||
"title": "Namespace ID",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "Script ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Protocol": "",
|
||||
"Authentication": [],
|
||||
"Path": "/namespace/{namespaceID}/trigger",
|
||||
"Authentication": [
|
||||
"Client ID",
|
||||
"Session ID"
|
||||
],
|
||||
"Path": "/namespace/{namespaceID}/automation/script/{scriptID}/trigger",
|
||||
"APIs": [
|
||||
{
|
||||
"Name": "list",
|
||||
"Method": "GET",
|
||||
"Title": "List available triggers",
|
||||
"Title": "List/read automation script triggers",
|
||||
"Path": "/",
|
||||
"Parameters": {
|
||||
"get": [
|
||||
{
|
||||
"name": "moduleID",
|
||||
"name": "resource",
|
||||
"required": false,
|
||||
"title": "Filter triggers by module",
|
||||
"type": "uint64"
|
||||
"title": "Only triggers of a specific resource",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "query",
|
||||
"name": "event",
|
||||
"required": false,
|
||||
"title": "Search query",
|
||||
"title": "Only triggers of a specific event",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "incDeleted",
|
||||
"required": false,
|
||||
"title": "Include deleted scripts",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"required": false,
|
||||
@@ -60,45 +68,34 @@
|
||||
{
|
||||
"Name": "create",
|
||||
"Method": "POST",
|
||||
"Title": "Create trigger",
|
||||
"Title": "Add new automation script trigger",
|
||||
"Path": "/",
|
||||
"Parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "moduleID",
|
||||
"required": false,
|
||||
"title": "Module ID",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "Name",
|
||||
"title": "automation name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "actions",
|
||||
"required": false,
|
||||
"title": "Actions that trigger this trigger",
|
||||
"type": "[]string"
|
||||
"name": "resource",
|
||||
"title": "Resource",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"title": "Event",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"title": "Event",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"required": false,
|
||||
"title": "Enabled",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"required": false,
|
||||
"title": "Trigger source code",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "updatedAt",
|
||||
"required": false,
|
||||
"title": "Last update (or creation) date",
|
||||
"type": "*time.Time"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -106,14 +103,14 @@
|
||||
{
|
||||
"Name": "read",
|
||||
"Method": "GET",
|
||||
"Title": "Get trigger details",
|
||||
"Title": "Read automation script trigger by ID",
|
||||
"Path": "/{triggerID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "Trigger ID",
|
||||
"title": "Automation script trigger ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
@@ -122,62 +119,57 @@
|
||||
{
|
||||
"Name": "update",
|
||||
"Method": "POST",
|
||||
"Title": "Update trigger",
|
||||
"Title": "Update automation script trigger",
|
||||
"Path": "/{triggerID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "Trigger ID",
|
||||
"title": "Automation script trigger ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
],
|
||||
"post": [
|
||||
{
|
||||
"name": "moduleID",
|
||||
"required": false,
|
||||
"title": "Module ID",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"title": "Name",
|
||||
"title": "automation name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "actions",
|
||||
"required": false,
|
||||
"title": "Actions that trigger this trigger",
|
||||
"type": "[]string"
|
||||
"name": "resource",
|
||||
"title": "Resource",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"title": "Event",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"title": "Event",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"required": false,
|
||||
"title": "Enabled",
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"required": false,
|
||||
"title": "Trigger source code",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "delete",
|
||||
"Method": "Delete",
|
||||
"Title": "Delete trigger",
|
||||
"Method": "DELETE",
|
||||
"Title": "Delete script",
|
||||
"Path": "/{triggerID}",
|
||||
"Parameters": {
|
||||
"path": [
|
||||
{
|
||||
"name": "triggerID",
|
||||
"required": true,
|
||||
"title": "Trigger ID",
|
||||
"title": "automation script trigger ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
@@ -199,6 +199,28 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "runScript",
|
||||
"Method": "POST",
|
||||
"Title": "Trigger a specific script on record",
|
||||
"Path": "/run-script",
|
||||
"Parameters": {
|
||||
"post": [
|
||||
{
|
||||
"name": "recordID",
|
||||
"required": false,
|
||||
"title": "Record ID",
|
||||
"type": "uint64"
|
||||
},
|
||||
{
|
||||
"name": "scriptID",
|
||||
"required": true,
|
||||
"title": "Script ID",
|
||||
"type": "uint64"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "upload",
|
||||
"Method": "POST",
|
||||
|
||||
@@ -38,7 +38,6 @@ function types {
|
||||
./build/gen-type-set --types Module --output compose/types/module.gen.go
|
||||
./build/gen-type-set --types Page --output compose/types/page.gen.go
|
||||
./build/gen-type-set --types Chart --output compose/types/chart.gen.go
|
||||
./build/gen-type-set --types Trigger --output compose/types/trigger.gen.go
|
||||
./build/gen-type-set --types Record --output compose/types/record.gen.go
|
||||
./build/gen-type-set --types ModuleField --output compose/types/module_field.gen.go
|
||||
|
||||
@@ -47,7 +46,6 @@ function types {
|
||||
./build/gen-type-set-test --types Module --output compose/types/module.gen_test.go
|
||||
./build/gen-type-set-test --types Page --output compose/types/page.gen_test.go
|
||||
./build/gen-type-set-test --types Chart --output compose/types/chart.gen_test.go
|
||||
./build/gen-type-set-test --types Trigger --output compose/types/trigger.gen_test.go
|
||||
./build/gen-type-set-test --types Record --output compose/types/record.gen_test.go
|
||||
./build/gen-type-set-test --types ModuleField --output compose/types/module_field.gen_test.go
|
||||
|
||||
@@ -99,6 +97,12 @@ function types {
|
||||
./build/gen-type-set-test --types Rule --output internal/permissions/rule.gen_test.go --with-primary-key=false --package permissions
|
||||
./build/gen-type-set-test --types Resource --output internal/permissions/resource.gen_test.go --with-primary-key=false --package permissions
|
||||
|
||||
./build/gen-type-set --types Script --output pkg/automation/script.gen.go --package automation
|
||||
./build/gen-type-set-test --types Script --output pkg/automation/script.gen_test.go --package automation
|
||||
./build/gen-type-set --types Trigger --output pkg/automation/trigger.gen.go --package automation
|
||||
./build/gen-type-set-test --types Trigger --output pkg/automation/trigger.gen_test.go --package automation
|
||||
|
||||
|
||||
green "OK"
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
68
compose/db/schema/mysql/20190701090000.automation.up.sql
Normal file
68
compose/db/schema/mysql/20190701090000.automation.up.sql
Normal file
@@ -0,0 +1,68 @@
|
||||
DROP TABLE IF EXISTS compose_automation_trigger;
|
||||
DROP TABLE IF EXISTS compose_automation_script;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS compose_automation_script (
|
||||
`id` BIGINT(20) UNSIGNED NOT NULL,
|
||||
`name` VARCHAR(64) NOT NULL DEFAULT 'unnamed' COMMENT 'The name of the script',
|
||||
`source` TEXT NOT NULL COMMENT 'Source code for the script',
|
||||
`source_ref` VARCHAR(200) NOT NULL COMMENT 'Where is the script located (if remote)',
|
||||
`async` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Do we run this script asynchronously?',
|
||||
`rel_runner` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Who is running the script? 0 for invoker',
|
||||
`run_in_ua` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Run this script inside user-agent environment',
|
||||
`timeout` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Any explicit timeout set for this script (milliseconds)?',
|
||||
`critical` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is it critical that this script is executed successfully',
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Is this script enabled?',
|
||||
|
||||
`created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`updated_at` DATETIME NULL DEFAULT NULL,
|
||||
`deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`deleted_at` DATETIME NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS compose_automation_trigger (
|
||||
`id` BIGINT(20) UNSIGNED NOT NULL,
|
||||
`rel_script` BIGINT(20) UNSIGNED NOT NULL COMMENT 'Script that is trigger',
|
||||
|
||||
`resource` VARCHAR(128) NOT NULL COMMENT 'Resource triggering the event',
|
||||
`event` VARCHAR(128) NOT NULL COMMENT 'Event triggered',
|
||||
`condition` TEXT NOT NULL COMMENT 'Trigger condition',
|
||||
|
||||
`enabled` BOOLEAN NOT NULL DEFAULT TRUE COMMENT 'Trigger enabled?',
|
||||
|
||||
`created_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`updated_at` DATETIME NULL DEFAULT NULL,
|
||||
`deleted_by` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
|
||||
`deleted_at` DATETIME NULL DEFAULT NULL,
|
||||
|
||||
CONSTRAINT `fk_script` FOREIGN KEY (`rel_script`) REFERENCES `compose_automation_script` (`id`),
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
|
||||
|
||||
# Insert all into
|
||||
INSERT INTO compose_automation_script (id, name, source, source_ref, run_in_ua, critical, enabled, created_at, updated_at, deleted_at)
|
||||
SELECT id, name, source, '', true, false, enabled, created_at, updated_at, deleted_at from compose_trigger;
|
||||
|
||||
INSERT INTO compose_automation_trigger (id, event, resource, `condition`, rel_script, enabled, created_at, updated_at, deleted_at)
|
||||
SELECT id+seq, events.event, 'compose:record', rel_module, id, enabled, created_at, updated_at, deleted_at from compose_trigger AS t INNER JOIN
|
||||
( SELECT 0 as seq, '' AS event
|
||||
UNION SELECT 1 as seq, 'manual' AS event
|
||||
UNION SELECT 2 as seq, 'beforeCreate' AS event
|
||||
UNION SELECT 3 as seq, 'afterCreate' AS event
|
||||
UNION SELECT 4 as seq, 'beforeUpdate' AS event
|
||||
UNION SELECT 5 as seq, 'afterUpdate' AS event
|
||||
UNION SELECT 6 as seq, 'beforeDelete' AS event
|
||||
UNION SELECT 7 as seq, 'afterDelete' AS event) AS events ON ((event = '' AND t.actions = '')
|
||||
OR (event <> '' AND t.actions LIKE concat('%',event,'%') ));
|
||||
|
||||
-- DROP TABLE IF EXISTS compose_trigger;
|
||||
214
compose/internal/service/automation_runner.go
Normal file
214
compose/internal/service/automation_runner.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/proto"
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/internal/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
"github.com/cortezaproject/corteza-server/pkg/sentry"
|
||||
)
|
||||
|
||||
type (
|
||||
automationRunner struct {
|
||||
logger *zap.Logger
|
||||
runner proto.ScriptRunnerClient
|
||||
scriptFinder automationScriptsFinder
|
||||
jwtEncoder auth.TokenEncoder
|
||||
}
|
||||
|
||||
automationScriptsFinder interface {
|
||||
Watch(ctx context.Context)
|
||||
FindRunnableScripts(event, resource string, cc ...automation.TriggerConditionChecker) automation.ScriptSet
|
||||
}
|
||||
)
|
||||
|
||||
func AutomationRunner(f automationScriptsFinder, r proto.ScriptRunnerClient) automationRunner {
|
||||
var svc = automationRunner{
|
||||
scriptFinder: f,
|
||||
runner: r,
|
||||
|
||||
logger: DefaultLogger.Named("automationRunner"),
|
||||
jwtEncoder: auth.DefaultJwtHandler,
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (svc automationRunner) findRecordScripts(event string, moduleID uint64) (ss automation.ScriptSet) {
|
||||
const resource = "compose:record"
|
||||
|
||||
// We'll be comparing strings, not uint64!
|
||||
var moduleIDs = strconv.FormatUint(moduleID, 10)
|
||||
|
||||
return svc.scriptFinder.FindRunnableScripts(event, resource,
|
||||
// ModuleID MUST match
|
||||
func(cModuleID string) bool {
|
||||
return moduleIDs == cModuleID
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (svc automationRunner) Watch(ctx context.Context) {
|
||||
svc.scriptFinder.Watch(ctx)
|
||||
}
|
||||
|
||||
// ManualRecordRun - Manual trigger run
|
||||
//
|
||||
// This is explicitly called, extra security check is needed
|
||||
func (svc automationRunner) ManualRecordRun(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
// @todo security check (can user run this script (scriptID) manually)
|
||||
|
||||
runner := svc.makeRecordScriptRunner(ctx, ns, m, r, true)
|
||||
|
||||
return svc.findRecordScripts("manual", m.ID).Walk(func(script *automation.Script) error {
|
||||
// Interested in a specific script, so skip everything else
|
||||
if script.ID != scriptID {
|
||||
return nil
|
||||
}
|
||||
|
||||
return runner(script)
|
||||
})
|
||||
}
|
||||
|
||||
// BeforeRecordCreate - run scripts before record is created
|
||||
//
|
||||
// This is implicitly called, no extra security check is needed
|
||||
func (svc automationRunner) BeforeRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
return svc.findRecordScripts("beforeCreate", m.ID).Walk(
|
||||
svc.makeRecordScriptRunner(ctx, ns, m, r, true),
|
||||
)
|
||||
}
|
||||
|
||||
// AfterRecordCreate - run scripts before record is created
|
||||
//
|
||||
// This is implicitly called, no extra security check is needed
|
||||
func (svc automationRunner) AfterRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
return svc.findRecordScripts("afterCreate", m.ID).Walk(
|
||||
svc.makeRecordScriptRunner(ctx, ns, m, r, true),
|
||||
)
|
||||
}
|
||||
|
||||
// BeforeRecordUpdate - run scripts before record is updated
|
||||
//
|
||||
// This is implicitly called, no extra security check is needed
|
||||
func (svc automationRunner) BeforeRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
return svc.findRecordScripts("beforeUpdate", m.ID).Walk(
|
||||
svc.makeRecordScriptRunner(ctx, ns, m, r, true),
|
||||
)
|
||||
}
|
||||
|
||||
// AfterRecordUpdate - run scripts before record is updated
|
||||
//
|
||||
// This is implicitly called, no extra security check is needed
|
||||
func (svc automationRunner) AfterRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
return svc.findRecordScripts("afterUpdate", m.ID).Walk(
|
||||
svc.makeRecordScriptRunner(ctx, ns, m, r, true),
|
||||
)
|
||||
}
|
||||
|
||||
// BeforeRecordDelete - run scripts before record is deleted
|
||||
//
|
||||
// This is implicitly called, no extra security check is needed
|
||||
func (svc automationRunner) BeforeRecordDelete(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
return svc.findRecordScripts("beforeDelete", m.ID).Walk(
|
||||
svc.makeRecordScriptRunner(ctx, ns, m, r, true),
|
||||
)
|
||||
}
|
||||
|
||||
// AfterRecordDelete - run scripts after record is deleted
|
||||
//
|
||||
// This is implicitly called, no extra security check is needed
|
||||
func (svc automationRunner) AfterRecordDelete(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error) {
|
||||
return svc.findRecordScripts("afterDelete", m.ID).Walk(
|
||||
svc.makeRecordScriptRunner(ctx, ns, m, r, true),
|
||||
)
|
||||
}
|
||||
|
||||
// Runs record script
|
||||
//
|
||||
// We set-up script-running environment: security (definer / invoker), async, critical
|
||||
// and copying values from the run to the given Record
|
||||
//
|
||||
func (svc automationRunner) makeRecordScriptRunner(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record, discard bool) func(script *automation.Script) error {
|
||||
// Static request params (record gets updated
|
||||
var req = &proto.RunRecordRequest{
|
||||
Namespace: proto.FromNamespace(ns),
|
||||
Module: proto.FromModule(m),
|
||||
Record: proto.FromRecord(r),
|
||||
}
|
||||
|
||||
svc.logger.Debug("executing script", zap.Any("record", r))
|
||||
|
||||
return func(script *automation.Script) error {
|
||||
// This could be executed in a goroutine (by *after triggers,
|
||||
// so we need ot rewire the sentry panic recoverty
|
||||
defer sentry.Recover()
|
||||
|
||||
ctx, cancelFn := context.WithTimeout(ctx, time.Second*5)
|
||||
defer cancelFn()
|
||||
|
||||
// Add invoker's or defined credentials/jwt
|
||||
req.JWT = svc.getJWT(ctx, script.RunAs)
|
||||
|
||||
// Add script info
|
||||
req.Script = proto.FromAutomationScript(script)
|
||||
|
||||
rsp, err := svc.runner.Record(ctx, req, grpc.WaitForReady(script.Critical))
|
||||
|
||||
svc.logger.Debug("call sent")
|
||||
|
||||
if err != nil {
|
||||
// @todo aborted?
|
||||
svc.logger.Debug("script executed, did not return record", zap.Error(err))
|
||||
if !script.Critical {
|
||||
// This was not a critical call and we do not care about
|
||||
// errors from script running service.
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if script.Async || discard {
|
||||
// Discard returned values (in case of async call or when forced)
|
||||
//
|
||||
// Backend is still returning, so we do not
|
||||
// need to handle multiple gRPC endpoints
|
||||
svc.logger.Debug("script executed / async")
|
||||
return nil
|
||||
}
|
||||
|
||||
if rsp.Record == nil {
|
||||
// Script did not return any results
|
||||
// This means we should stop with the execution
|
||||
// @todo aborted
|
||||
return nil
|
||||
}
|
||||
|
||||
svc.logger.Debug("script executed", zap.Any("record", rsp.Record))
|
||||
|
||||
// Convert from proto and copy record owner & values from the result
|
||||
result := proto.ToRecord(rsp.Record)
|
||||
r.OwnedBy, r.Values = result.OwnedBy, result.Values
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a new JWT for
|
||||
func (svc automationRunner) getJWT(ctx context.Context, userID uint64) string {
|
||||
if userID > 0 {
|
||||
// @todo implement this
|
||||
// at the moment we do not he the ability fetch user info from non-system service
|
||||
// extend/implement this feature when our services will know how to communicate with each-other
|
||||
}
|
||||
|
||||
return svc.jwtEncoder.Encode(auth.GetIdentityFromContext(ctx))
|
||||
}
|
||||
60
compose/internal/service/automation_script.go
Normal file
60
compose/internal/service/automation_script.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
)
|
||||
|
||||
type (
|
||||
automationScript struct {
|
||||
logger *zap.Logger
|
||||
scriptManager automationScriptManager
|
||||
}
|
||||
|
||||
automationScriptManager interface {
|
||||
FindScriptByID(context.Context, uint64) (*automation.Script, error)
|
||||
FindScripts(context.Context, automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error)
|
||||
CreateScript(context.Context, *automation.Script) error
|
||||
UpdateScript(context.Context, *automation.Script) error
|
||||
DeleteScript(context.Context, *automation.Script) error
|
||||
}
|
||||
)
|
||||
|
||||
func AutomationScript(sm automationScriptManager) automationScript {
|
||||
var svc = automationScript{
|
||||
scriptManager: sm,
|
||||
logger: DefaultLogger.Named("automation-script"),
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (svc automationScript) FindByID(ctx context.Context, scriptID uint64) (*automation.Script, error) {
|
||||
// @todo security check - can user read this script?
|
||||
return svc.scriptManager.FindScriptByID(ctx, scriptID)
|
||||
}
|
||||
|
||||
func (svc automationScript) Find(ctx context.Context, f automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error) {
|
||||
// @todo security check - can user read these scripts?
|
||||
return svc.scriptManager.FindScripts(ctx, f)
|
||||
}
|
||||
|
||||
func (svc automationScript) Create(ctx context.Context, s *automation.Script) (err error) {
|
||||
// @todo security check - can user create scripts?
|
||||
// @todo security check - can make scripts with security-definer?
|
||||
return svc.scriptManager.CreateScript(ctx, s)
|
||||
}
|
||||
|
||||
func (svc automationScript) Update(ctx context.Context, s *automation.Script) (err error) {
|
||||
// @todo security check - can user update this script?
|
||||
// @todo security check - can make scripts with security-definer?
|
||||
return svc.scriptManager.UpdateScript(ctx, s)
|
||||
}
|
||||
|
||||
func (svc automationScript) Delete(ctx context.Context, s *automation.Script) (err error) {
|
||||
// @todo security check - can user delete this script?
|
||||
return svc.scriptManager.DeleteScript(ctx, s)
|
||||
}
|
||||
59
compose/internal/service/automation_trigger.go
Normal file
59
compose/internal/service/automation_trigger.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
)
|
||||
|
||||
type (
|
||||
// Handles automation triggers storing and loading
|
||||
automationTrigger struct {
|
||||
logger *zap.Logger
|
||||
triggerManager automationTriggerManager
|
||||
}
|
||||
|
||||
automationTriggerManager interface {
|
||||
FindTriggerByID(context.Context, uint64) (*automation.Trigger, error)
|
||||
FindTriggers(context.Context, automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error)
|
||||
CreateTrigger(context.Context, *automation.Script, *automation.Trigger) error
|
||||
UpdateTrigger(context.Context, *automation.Script, *automation.Trigger) error
|
||||
DeleteTrigger(context.Context, *automation.Trigger) error
|
||||
}
|
||||
)
|
||||
|
||||
func AutomationTrigger(tm automationTriggerManager) automationTrigger {
|
||||
var svc = automationTrigger{
|
||||
triggerManager: tm,
|
||||
logger: DefaultLogger.Named("automation-trigger"),
|
||||
}
|
||||
|
||||
return svc
|
||||
}
|
||||
|
||||
func (svc automationTrigger) FindByID(ctx context.Context, triggerID uint64) (*automation.Trigger, error) {
|
||||
// @todo security check - can user read this trigger?
|
||||
return svc.triggerManager.FindTriggerByID(ctx, triggerID)
|
||||
}
|
||||
|
||||
func (svc automationTrigger) Find(ctx context.Context, f automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error) {
|
||||
// @todo security check - can user read these triggers?
|
||||
return svc.triggerManager.FindTriggers(ctx, f)
|
||||
}
|
||||
|
||||
func (svc automationTrigger) Create(ctx context.Context, s *automation.Script, t *automation.Trigger) (err error) {
|
||||
// @todo security check - can user create trigger on this specific resource
|
||||
return svc.triggerManager.CreateTrigger(ctx, s, t)
|
||||
}
|
||||
|
||||
func (svc automationTrigger) Update(ctx context.Context, s *automation.Script, t *automation.Trigger) (err error) {
|
||||
// @todo security check - can user create update triggers on this specific resource
|
||||
return svc.triggerManager.UpdateTrigger(ctx, s, t)
|
||||
}
|
||||
|
||||
func (svc automationTrigger) Delete(ctx context.Context, t *automation.Trigger) (err error) {
|
||||
// @todo security check - can user create delete triggers on this specific resource
|
||||
return svc.triggerManager.DeleteTrigger(ctx, t)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ type (
|
||||
logger *zap.Logger
|
||||
|
||||
ac recordAccessController
|
||||
sr RecordScriptRunner
|
||||
sr RecordScriptsRunner
|
||||
|
||||
recordRepo repository.RecordRepository
|
||||
moduleRepo repository.ModuleRepository
|
||||
@@ -41,8 +41,14 @@ type (
|
||||
CanUpdateRecordValue(context.Context, *types.ModuleField) bool
|
||||
}
|
||||
|
||||
RecordScriptRunner interface {
|
||||
Record(context.Context, Runnable, *types.Namespace, *types.Module, *types.Record) (*types.Record, error)
|
||||
RecordScriptsRunner interface {
|
||||
ManualRecordRun(ctx context.Context, scriptID uint64, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
BeforeRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
AfterRecordCreate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
BeforeRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
AfterRecordUpdate(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
BeforeRecordDelete(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
AfterRecordDelete(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) (err error)
|
||||
}
|
||||
|
||||
RecordService interface {
|
||||
@@ -59,7 +65,7 @@ type (
|
||||
|
||||
DeleteByID(namespaceID, recordID uint64) error
|
||||
|
||||
// Fields(module *types.Module, record *types.Record) ([]*types.RecordValue, error)
|
||||
RunScript(namespaceID, moduleID, recordID, scriptID uint64) error
|
||||
}
|
||||
|
||||
Encoder interface {
|
||||
@@ -71,7 +77,7 @@ func Record() RecordService {
|
||||
return (&record{
|
||||
logger: DefaultLogger.Named("record"),
|
||||
ac: DefaultAccessControl,
|
||||
sr: DefaultScriptRunner,
|
||||
sr: DefaultAutomationRunner,
|
||||
}).With(context.Background())
|
||||
}
|
||||
|
||||
@@ -188,6 +194,7 @@ func (svc record) Find(filter types.RecordFilter) (set types.RecordSet, f types.
|
||||
// @todo better value handling
|
||||
func (svc record) Export(filter types.RecordFilter, enc Encoder) error {
|
||||
m, err := svc.loadModule(filter.NamespaceID, filter.ModuleID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -205,7 +212,7 @@ func (svc record) Export(filter types.RecordFilter, enc Encoder) error {
|
||||
}
|
||||
|
||||
func (svc record) Create(mod *types.Record) (r *types.Record, err error) {
|
||||
ns, m, r, tt, err := svc.loadCombo(mod.NamespaceID, mod.ModuleID, 0)
|
||||
ns, m, r, err := svc.loadCombo(mod.NamespaceID, mod.ModuleID, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -231,12 +238,14 @@ func (svc record) Create(mod *types.Record) (r *types.Record, err error) {
|
||||
|
||||
mod = nil // make sure we do not use it anymore
|
||||
|
||||
if err = tt.WalkByAction("beforeCreate", svc.runTrigger(svc.ctx, ns, m, r)); err != nil {
|
||||
if err = svc.sr.BeforeRecordCreate(svc.ctx, ns, m, r); err != nil {
|
||||
// Calling
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tt.WalkByAction("afterCreate", svc.runTrigger(svc.ctx, ns, m, r))
|
||||
// Run this at the end and discard the error
|
||||
_ = svc.sr.AfterRecordCreate(svc.ctx, ns, m, r)
|
||||
}()
|
||||
|
||||
return r, svc.db.Transaction(func() (err error) {
|
||||
@@ -257,7 +266,7 @@ func (svc record) Update(mod *types.Record) (r *types.Record, err error) {
|
||||
return nil, ErrInvalidID.withStack()
|
||||
}
|
||||
|
||||
ns, m, r, tt, err := svc.loadCombo(mod.NamespaceID, mod.ModuleID, mod.ID)
|
||||
ns, m, r, err := svc.loadCombo(mod.NamespaceID, mod.ModuleID, mod.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -281,12 +290,14 @@ func (svc record) Update(mod *types.Record) (r *types.Record, err error) {
|
||||
|
||||
mod = nil // make sure we do not use it anymore
|
||||
|
||||
if err = tt.WalkByAction("beforeUpdate", svc.runTrigger(svc.ctx, ns, m, r)); err != nil {
|
||||
if err = svc.sr.BeforeRecordUpdate(svc.ctx, ns, m, r); err != nil {
|
||||
// Calling
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tt.WalkByAction("afterUpdate", svc.runTrigger(svc.ctx, ns, m, r))
|
||||
// Run this at the end and discard the error
|
||||
_ = svc.sr.AfterRecordUpdate(svc.ctx, ns, m, r)
|
||||
}()
|
||||
|
||||
return r, svc.db.Transaction(func() (err error) {
|
||||
@@ -307,17 +318,19 @@ func (svc record) DeleteByID(namespaceID, recordID uint64) (err error) {
|
||||
return ErrInvalidID.withStack()
|
||||
}
|
||||
|
||||
ns, m, r, tt, err := svc.loadCombo(namespaceID, 0, recordID)
|
||||
ns, m, r, err := svc.loadCombo(namespaceID, 0, recordID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = tt.WalkByAction("beforeDelete", svc.runTrigger(svc.ctx, ns, m, r)); err != nil {
|
||||
if err = svc.sr.BeforeRecordCreate(svc.ctx, ns, m, r); err != nil {
|
||||
// Calling
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = tt.WalkByAction("afterDelete", svc.runTrigger(svc.ctx, ns, m, r))
|
||||
// Run this at the end and discard the error
|
||||
_ = svc.sr.AfterRecordDelete(svc.ctx, ns, m, r)
|
||||
}()
|
||||
|
||||
err = svc.db.Transaction(func() (err error) {
|
||||
@@ -342,7 +355,7 @@ func (svc record) DeleteByID(namespaceID, recordID uint64) (err error) {
|
||||
// loadCombo Loads everything we need for record manipulation
|
||||
//
|
||||
// Loads namespace, module, record and set of triggers.
|
||||
func (svc record) loadCombo(namespaceID, moduleID, recordID uint64) (ns *types.Namespace, m *types.Module, r *types.Record, tt types.TriggerSet, err error) {
|
||||
func (svc record) loadCombo(namespaceID, moduleID, recordID uint64) (ns *types.Namespace, m *types.Module, r *types.Record, err error) {
|
||||
if namespaceID == 0 {
|
||||
err = ErrNamespaceRequired
|
||||
return
|
||||
@@ -363,54 +376,12 @@ func (svc record) loadCombo(namespaceID, moduleID, recordID uint64) (ns *types.N
|
||||
return
|
||||
}
|
||||
|
||||
tt, _, err = svc.tRepo.Find(types.TriggerFilter{
|
||||
// Make sure we stay in the same namespace
|
||||
NamespaceID: ns.ID,
|
||||
|
||||
// Triggered scripts are always module-bound
|
||||
ModuleID: m.ID,
|
||||
|
||||
// We are only interested in enabled scripts
|
||||
EnabledOnly: true,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc record) runTrigger(ctx context.Context, ns *types.Namespace, m *types.Module, r *types.Record) func(t *types.Trigger) error {
|
||||
svc.logger.Debug("initializing trigger runner")
|
||||
return func(t *types.Trigger) error {
|
||||
svc.logger.Debug("running trigger", zap.Uint64("triggerID", t.ID))
|
||||
if svc.sr == nil {
|
||||
// No script runner set
|
||||
svc.logger.Debug("script runner not set")
|
||||
return nil
|
||||
}
|
||||
|
||||
// pr == processed record
|
||||
pr, err := svc.sr.Record(svc.ctx, t, ns, m, r)
|
||||
if err != nil {
|
||||
svc.logger.Debug("failed to run record script", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if pr == nil {
|
||||
// Did not get any processed record,
|
||||
// consider canceled
|
||||
return errors.New("aborted by automation")
|
||||
}
|
||||
|
||||
return svc.copyChanges(m, pr, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Copies changes from mod to r(ecord)
|
||||
func (svc record) copyChanges(m *types.Module, mod, r *types.Record) (err error) {
|
||||
// Automation scripts are allowed to modify record owner & values.
|
||||
if mod.OwnedBy > 0 {
|
||||
r.OwnedBy = mod.OwnedBy
|
||||
}
|
||||
|
||||
r.OwnedBy = mod.OwnedBy
|
||||
r.Values, err = svc.sanitizeValues(m, mod.Values)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/proto"
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/internal/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/cli/options"
|
||||
)
|
||||
|
||||
// Script runner provides an interface to corteza-corredor (Spanish for runner) service
|
||||
// that helps us with execution of JavaScript code -- compose's triggers & automation code
|
||||
//
|
||||
// corteza-server communicates with corteza-corredor via gRPC protocol.
|
||||
//
|
||||
// This service accepts ns/trigger/module/record (combinations), makes a call via gRPC protocol and
|
||||
// returns record/module/ns or just tests trigger's script
|
||||
|
||||
type (
|
||||
scriptRunner struct {
|
||||
c options.ScriptRunnerOpt
|
||||
logger *zap.Logger
|
||||
conn *grpc.ClientConn
|
||||
client proto.ScriptRunnerClient
|
||||
jwtEncoder auth.TokenEncoder
|
||||
}
|
||||
|
||||
Runnable interface {
|
||||
proto.Runnable
|
||||
|
||||
IsCritical() bool
|
||||
GetRunnerID() uint64
|
||||
}
|
||||
|
||||
ScriptRunnerService interface {
|
||||
Close() error
|
||||
Namespace(context.Context, Runnable, *types.Namespace) (*types.Namespace, error)
|
||||
Module(context.Context, Runnable, *types.Namespace, *types.Module) (*types.Module, error)
|
||||
Record(context.Context, Runnable, *types.Namespace, *types.Module, *types.Record) (*types.Record, error)
|
||||
}
|
||||
)
|
||||
|
||||
// @todo move to opt so all services can use it
|
||||
func ScriptRunner(c options.ScriptRunnerOpt) (svc *scriptRunner, err error) {
|
||||
svc = &scriptRunner{
|
||||
c: c,
|
||||
logger: DefaultLogger.Named("script-runner"),
|
||||
jwtEncoder: auth.DefaultJwtHandler,
|
||||
}
|
||||
|
||||
if !c.Enabled {
|
||||
// Do not connect when script runner is not enabled
|
||||
return
|
||||
}
|
||||
|
||||
return svc, svc.connect()
|
||||
}
|
||||
|
||||
func (svc *scriptRunner) connect() (err error) {
|
||||
if svc.c.Log {
|
||||
// Send logs to zap
|
||||
//
|
||||
// waiting for https://github.com/uber-go/zap/pull/538
|
||||
grpclog.SetLogger(zapgrpc.NewLogger(svc.logger.Named("grpc")))
|
||||
}
|
||||
|
||||
var dopts = []grpc.DialOption{
|
||||
// @todo insecure?
|
||||
grpc.WithInsecure(),
|
||||
}
|
||||
|
||||
if svc.c.MaxBackoffDelay > 0 {
|
||||
dopts = append(dopts, grpc.WithBackoffMaxDelay(svc.c.MaxBackoffDelay))
|
||||
}
|
||||
|
||||
svc.conn, err = grpc.Dial(svc.c.Addr, dopts...)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
svc.client = proto.NewScriptRunnerClient(svc.conn)
|
||||
return
|
||||
}
|
||||
|
||||
func (svc scriptRunner) Close() error {
|
||||
return svc.conn.Close()
|
||||
}
|
||||
|
||||
func (svc scriptRunner) callOptions() []grpc.CallOption {
|
||||
return []grpc.CallOption{
|
||||
grpc.WaitForReady(true),
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a new JWT for
|
||||
func (svc scriptRunner) getJWT(ctx context.Context, r Runnable) string {
|
||||
if r.GetRunnerID() > 0 {
|
||||
// @todo implement this
|
||||
// at the moment we do not he the ability fetch user info from non-system service
|
||||
// extend/implement this feature when our services will know how to communicate with each-other
|
||||
}
|
||||
|
||||
return svc.jwtEncoder.Encode(auth.GetIdentityFromContext(ctx))
|
||||
}
|
||||
|
||||
func (svc scriptRunner) Namespace(ctx context.Context, s Runnable, ns *types.Namespace) (*types.Namespace, error) {
|
||||
panic("scriptRunner.Namespace() not implemented")
|
||||
}
|
||||
|
||||
func (svc scriptRunner) Module(ctx context.Context, s Runnable, ns *types.Namespace, m *types.Module) (*types.Module, error) {
|
||||
panic("scriptRunner.Module() not implemented")
|
||||
}
|
||||
|
||||
func (svc scriptRunner) Record(ctx context.Context, s Runnable, ns *types.Namespace, m *types.Module, r *types.Record) (*types.Record, error) {
|
||||
if s == nil {
|
||||
return nil, errors.New("script not provided")
|
||||
}
|
||||
|
||||
if ns == nil {
|
||||
return nil, errors.New("namespace not provided")
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return nil, errors.New("module not provided")
|
||||
}
|
||||
|
||||
if !svc.c.Enabled {
|
||||
if s.IsCritical() {
|
||||
// Oh dear, we are in quite a pickle:
|
||||
// Script runner is disabled but we have critical script to run
|
||||
return nil, errors.New("script runner disabled")
|
||||
}
|
||||
|
||||
// Log this
|
||||
svc.logger.Debug("executing script", zap.Any("record", r))
|
||||
|
||||
// and pretend like nothing happened
|
||||
return r, nil
|
||||
}
|
||||
|
||||
svc.logger.Debug("executing script", zap.Any("record", r))
|
||||
|
||||
ctx, cancelFn := context.WithTimeout(ctx, time.Second*5)
|
||||
defer cancelFn()
|
||||
|
||||
rsp, err := svc.client.Record(
|
||||
ctx,
|
||||
&proto.RunRecordRequest{
|
||||
JWT: svc.getJWT(ctx, s),
|
||||
Script: proto.ScriptFromRunnable(s),
|
||||
Namespace: proto.FromNamespace(ns),
|
||||
Module: proto.FromModule(m),
|
||||
Record: proto.FromRecord(r),
|
||||
},
|
||||
svc.callOptions()...,
|
||||
)
|
||||
|
||||
svc.logger.Debug("call sent")
|
||||
|
||||
if err != nil {
|
||||
svc.logger.Debug("script executed, did not return record", zap.Error(err))
|
||||
if !s.IsCritical() {
|
||||
// This was not a critical call and we do not care about
|
||||
// errors from script running service.
|
||||
return r, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.IsAsync() {
|
||||
svc.logger.Debug("script executed / async")
|
||||
// Async call, we do not care about what we get back
|
||||
return r, nil
|
||||
}
|
||||
|
||||
svc.logger.Debug("script executed", zap.Any("record", rsp.Record))
|
||||
|
||||
// Result from the automation script
|
||||
return proto.ToRecord(rsp.Record), nil
|
||||
}
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/internal/repository"
|
||||
"github.com/cortezaproject/corteza-server/compose/proto"
|
||||
"github.com/cortezaproject/corteza-server/internal/permissions"
|
||||
"github.com/cortezaproject/corteza-server/internal/store"
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
"github.com/cortezaproject/corteza-server/pkg/cli/options"
|
||||
)
|
||||
|
||||
@@ -18,6 +20,13 @@ type (
|
||||
Watch(ctx context.Context)
|
||||
}
|
||||
|
||||
// automationManager interface {
|
||||
// automationScriptsFinder
|
||||
// automationScriptManager
|
||||
// automationTriggerManager
|
||||
// Watch(ctx context.Context)
|
||||
// }
|
||||
|
||||
Config struct {
|
||||
Storage options.StorageOpt
|
||||
ScriptRunner options.ScriptRunnerOpt
|
||||
@@ -25,25 +34,36 @@ type (
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultPermissions permissionServicer
|
||||
|
||||
DefaultLogger *zap.Logger
|
||||
|
||||
// DefaultPermissions Retrives & stores permissions
|
||||
DefaultPermissions permissionServicer
|
||||
|
||||
// DefaultAccessControl Access control checking
|
||||
DefaultAccessControl *accessControl
|
||||
|
||||
DefaultRecord RecordService
|
||||
DefaultModule ModuleService
|
||||
DefaultTrigger TriggerService
|
||||
DefaultChart ChartService
|
||||
DefaultPage PageService
|
||||
DefaultNotification NotificationService
|
||||
DefaultAttachment AttachmentService
|
||||
DefaultNamespace NamespaceService
|
||||
// DefaultAutomationScriptManager manages scripts
|
||||
DefaultAutomationScriptManager automationScript
|
||||
|
||||
DefaultScriptRunner ScriptRunnerService
|
||||
// DefaultAutomationTriggerManager manages triggerManager
|
||||
DefaultAutomationTriggerManager automationTrigger
|
||||
|
||||
// DefaultAutomationRunner runs automation scripts by listening to triggerManager and invoking Corredor service
|
||||
DefaultAutomationRunner automationRunner
|
||||
|
||||
DefaultNamespace NamespaceService
|
||||
DefaultRecord RecordService
|
||||
DefaultModule ModuleService
|
||||
DefaultChart ChartService
|
||||
DefaultPage PageService
|
||||
|
||||
DefaultAttachment AttachmentService
|
||||
DefaultNotification NotificationService
|
||||
)
|
||||
|
||||
func Init(ctx context.Context, log *zap.Logger, c Config) (err error) {
|
||||
var db = repository.DB(ctx)
|
||||
|
||||
DefaultLogger = log.Named("service")
|
||||
|
||||
fs, err := store.New(c.Storage.Path)
|
||||
@@ -52,31 +72,47 @@ func Init(ctx context.Context, log *zap.Logger, c Config) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Permissions, access control
|
||||
DefaultPermissions = permissions.Service(
|
||||
ctx,
|
||||
DefaultLogger,
|
||||
permissions.Repository(repository.DB(ctx), "compose_permission_rules"))
|
||||
permissions.Repository(db, "compose_permission_rules"))
|
||||
|
||||
DefaultAccessControl = AccessControl(DefaultPermissions)
|
||||
|
||||
DefaultScriptRunner, err = ScriptRunner(c.ScriptRunner)
|
||||
// ias Internal Automatinon Service
|
||||
// handles script & trigger management & keeping runnables cripts in internal cache
|
||||
ias := automation.Service(ctx, DefaultLogger, automation.AutomationServiceConfig{DB: db, DbTablePrefix: "compose"})
|
||||
|
||||
// Pass automation manager to
|
||||
DefaultAutomationScriptManager = AutomationScript(ias)
|
||||
DefaultAutomationTriggerManager = AutomationTrigger(ias)
|
||||
|
||||
corredor, err := automation.Corredor(ctx, c.ScriptRunner, DefaultLogger)
|
||||
log.Info("initializing corredor connection", zap.String("addr", c.ScriptRunner.Addr), zap.Error(err))
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
DefaultAutomationRunner = AutomationRunner(ias, proto.NewScriptRunnerClient(corredor))
|
||||
|
||||
// Compose internals:
|
||||
DefaultNamespace = Namespace()
|
||||
DefaultRecord = Record()
|
||||
DefaultModule = Module()
|
||||
DefaultTrigger = Trigger()
|
||||
DefaultPage = Page()
|
||||
DefaultChart = Chart()
|
||||
DefaultNotification = Notification()
|
||||
DefaultAttachment = Attachment(fs)
|
||||
DefaultNamespace = Namespace()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Watchers(ctx context.Context) {
|
||||
// Reloading automation scripts on change
|
||||
DefaultAutomationRunner.Watch(ctx)
|
||||
|
||||
// Reloading permissions on change
|
||||
DefaultPermissions.Watch(ctx)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/titpetric/factory"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/internal/repository"
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
trigger struct {
|
||||
db *factory.DB
|
||||
ctx context.Context
|
||||
logger *zap.Logger
|
||||
|
||||
ac triggerAccessController
|
||||
|
||||
triggerRepo repository.TriggerRepository
|
||||
nsRepo repository.NamespaceRepository
|
||||
}
|
||||
|
||||
triggerAccessController interface {
|
||||
CanReadNamespace(context.Context, *types.Namespace) bool
|
||||
CanCreateTrigger(context.Context, *types.Namespace) bool
|
||||
CanReadTrigger(context.Context, *types.Trigger) bool
|
||||
CanUpdateTrigger(context.Context, *types.Trigger) bool
|
||||
CanDeleteTrigger(context.Context, *types.Trigger) bool
|
||||
}
|
||||
|
||||
TriggerService interface {
|
||||
With(ctx context.Context) TriggerService
|
||||
|
||||
FindByID(namespaceID, triggerID uint64) (*types.Trigger, error)
|
||||
Find(filter types.TriggerFilter) (set types.TriggerSet, f types.TriggerFilter, err error)
|
||||
|
||||
Create(trigger *types.Trigger) (*types.Trigger, error)
|
||||
Update(trigger *types.Trigger) (*types.Trigger, error)
|
||||
DeleteByID(namespaceID, triggerID uint64) error
|
||||
}
|
||||
)
|
||||
|
||||
func Trigger() TriggerService {
|
||||
return (&trigger{
|
||||
logger: DefaultLogger.Named("trigger"),
|
||||
ac: DefaultAccessControl,
|
||||
}).With(context.Background())
|
||||
}
|
||||
|
||||
func (svc trigger) With(ctx context.Context) TriggerService {
|
||||
db := repository.DB(ctx)
|
||||
return &trigger{
|
||||
db: db,
|
||||
ctx: ctx,
|
||||
logger: svc.logger,
|
||||
|
||||
ac: svc.ac,
|
||||
|
||||
triggerRepo: repository.Trigger(ctx, db),
|
||||
nsRepo: repository.Namespace(ctx, db),
|
||||
}
|
||||
}
|
||||
|
||||
// log() returns zap's logger with requestID from current context and fields.
|
||||
// func (svc trigger) log(fields ...zapcore.Field) *zap.Logger {
|
||||
// return logger.AddRequestID(svc.ctx, svc.logger).With(fields...)
|
||||
// }
|
||||
|
||||
func (svc trigger) FindByID(namespaceID, triggerID uint64) (t *types.Trigger, err error) {
|
||||
if namespaceID == 0 {
|
||||
return nil, ErrNamespaceRequired
|
||||
}
|
||||
|
||||
if t, err = svc.triggerRepo.FindByID(namespaceID, triggerID); err != nil {
|
||||
return
|
||||
} else if !svc.ac.CanReadTrigger(svc.ctx, t) {
|
||||
return nil, ErrNoReadPermissions.withStack()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc trigger) Find(filter types.TriggerFilter) (set types.TriggerSet, f types.TriggerFilter, err error) {
|
||||
set, f, err = svc.triggerRepo.Find(filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
set, _ = set.Filter(func(m *types.Trigger) (bool, error) {
|
||||
return svc.ac.CanReadTrigger(svc.ctx, m), nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc trigger) Create(mod *types.Trigger) (t *types.Trigger, err error) {
|
||||
if mod.NamespaceID == 0 {
|
||||
return nil, ErrNamespaceRequired.withStack()
|
||||
}
|
||||
|
||||
if ns, err := svc.loadNamespace(mod.NamespaceID); err != nil {
|
||||
return nil, err
|
||||
} else if !svc.ac.CanCreateTrigger(svc.ctx, ns) {
|
||||
return nil, ErrNoCreatePermissions.withStack()
|
||||
}
|
||||
|
||||
return svc.triggerRepo.Create(mod)
|
||||
}
|
||||
|
||||
func (svc trigger) Update(mod *types.Trigger) (c *types.Trigger, err error) {
|
||||
if mod.ID == 0 {
|
||||
return nil, ErrInvalidID.withStack()
|
||||
}
|
||||
|
||||
if c, err = svc.triggerRepo.FindByID(mod.NamespaceID, mod.ID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isStale(mod.UpdatedAt, c.UpdatedAt, c.CreatedAt) {
|
||||
return nil, ErrStaleData.withStack()
|
||||
}
|
||||
|
||||
if !svc.ac.CanUpdateTrigger(svc.ctx, c) {
|
||||
return nil, ErrNoUpdatePermissions.withStack()
|
||||
}
|
||||
|
||||
c.Name = mod.Name
|
||||
c.ModuleID = mod.ModuleID
|
||||
c.Source = mod.Source
|
||||
c.Actions = mod.Actions
|
||||
c.Enabled = mod.Enabled
|
||||
|
||||
return svc.triggerRepo.Update(c)
|
||||
}
|
||||
|
||||
func (svc trigger) DeleteByID(namespaceID, triggerID uint64) error {
|
||||
if triggerID == 0 {
|
||||
return ErrInvalidID.withStack()
|
||||
}
|
||||
|
||||
if namespaceID == 0 {
|
||||
return ErrNamespaceRequired.withStack()
|
||||
}
|
||||
|
||||
if c, err := svc.triggerRepo.FindByID(namespaceID, triggerID); err != nil {
|
||||
return err
|
||||
} else if !svc.ac.CanDeleteTrigger(svc.ctx, c) {
|
||||
return ErrNoDeletePermissions.withStack()
|
||||
}
|
||||
|
||||
return svc.triggerRepo.DeleteByID(namespaceID, triggerID)
|
||||
}
|
||||
|
||||
func (svc trigger) loadNamespace(namespaceID uint64) (ns *types.Namespace, err error) {
|
||||
if namespaceID == 0 {
|
||||
return nil, ErrNamespaceRequired.withStack()
|
||||
}
|
||||
|
||||
if ns, err = svc.nsRepo.FindByID(namespaceID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !svc.ac.CanReadNamespace(svc.ctx, ns) {
|
||||
return nil, ErrNoReadPermissions.withStack()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// +build integration
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/internal/auth"
|
||||
"github.com/cortezaproject/corteza-server/internal/test"
|
||||
)
|
||||
|
||||
func TestTrigger(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), "testing", true)
|
||||
|
||||
// Set Identity (required for permission checks).
|
||||
ctx = auth.SetIdentityToContext(ctx, auth.NewIdentity(1337))
|
||||
|
||||
ns1, _ := createTestNamespaces(ctx, t)
|
||||
|
||||
svc := Trigger().With(ctx)
|
||||
|
||||
// the trigger object we're working with
|
||||
trigger := &types.Trigger{
|
||||
NamespaceID: ns1.ID,
|
||||
Name: "Test",
|
||||
ModuleID: 123,
|
||||
}
|
||||
|
||||
{
|
||||
{
|
||||
m, err := svc.Update(trigger)
|
||||
test.Assert(t, m == nil, "Expected empty return for invalid update, got %#v", m)
|
||||
test.Assert(t, err != nil, "Expected error when updating invalid content")
|
||||
}
|
||||
|
||||
// create trigger
|
||||
m, err := svc.Create(trigger)
|
||||
test.Assert(t, err == nil, "Error when creating trigger: %+v", err)
|
||||
test.Assert(t, m.ID > 0, "Expected auto generated ID")
|
||||
|
||||
{
|
||||
_, err := svc.Create(trigger)
|
||||
test.Assert(t, err == nil, "Unexpected error when creating trigger, %+v", err)
|
||||
}
|
||||
|
||||
// fetch created trigger
|
||||
{
|
||||
ms, err := svc.FindByID(m.NamespaceID, m.ID)
|
||||
test.Assert(t, err == nil, "Error when retrieving trigger by id: %+v", err)
|
||||
test.Assert(t, ms.ID == m.ID, "Expected ID from database to match, %+v", errors.Errorf("%d != %d", m.ID, ms.ID))
|
||||
test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %+v", errors.Errorf("%s != %s", m.Name, ms.Name))
|
||||
}
|
||||
|
||||
// update created trigger
|
||||
{
|
||||
m.UpdatedAt = nil
|
||||
m.Name = "Updated test"
|
||||
_, err := svc.Update(m)
|
||||
test.Assert(t, err == nil, "Error when updating trigger, %+v", err)
|
||||
}
|
||||
|
||||
// re-fetch trigger
|
||||
{
|
||||
ms, err := svc.FindByID(m.NamespaceID, m.ID)
|
||||
test.Assert(t, err == nil, "Error when retrieving trigger by id: %+v", err)
|
||||
test.Assert(t, ms.ID == m.ID, "re-fetch: Expected ID from database to match, %d != %d", m.ID, ms.ID)
|
||||
test.Assert(t, ms.Name == m.Name, "Expected Name from database to match, %s != %s", m.Name, ms.Name)
|
||||
}
|
||||
|
||||
// delete trigger
|
||||
{
|
||||
err := svc.DeleteByID(m.NamespaceID, m.ID)
|
||||
test.Assert(t, err == nil, "Error when deleting trigger by id: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/golang/protobuf/ptypes/timestamp"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -90,16 +91,12 @@ func FromNamespace(i *types.Namespace) *Namespace {
|
||||
return p
|
||||
}
|
||||
|
||||
func ScriptFromRunnable(s Runnable) *Script {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
func FromAutomationScript(s *automation.Script) *Script {
|
||||
return &Script{
|
||||
Source: s.GetSource(),
|
||||
Name: s.GetName(),
|
||||
Timeout: s.GetTimeout(),
|
||||
Async: s.IsAsync(),
|
||||
Source: s.Source,
|
||||
Name: s.Name,
|
||||
Timeout: uint32(s.Timeout),
|
||||
Async: s.Async,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
142
compose/rest/automation_script.go
Normal file
142
compose/rest/automation_script.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/internal/service"
|
||||
"github.com/cortezaproject/corteza-server/compose/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
)
|
||||
|
||||
var _ = errors.Wrap
|
||||
|
||||
type (
|
||||
automationScriptPayload struct {
|
||||
*automation.Script
|
||||
}
|
||||
|
||||
automationScriptSetPayload struct {
|
||||
Filter automation.ScriptFilter `json:"filter"`
|
||||
Set []*automationScriptPayload `json:"set"`
|
||||
}
|
||||
|
||||
AutomationScript struct {
|
||||
scripts automationScriptService
|
||||
}
|
||||
|
||||
automationScriptService interface {
|
||||
FindByID(context.Context, uint64) (*automation.Script, error)
|
||||
Find(context.Context, automation.ScriptFilter) (automation.ScriptSet, automation.ScriptFilter, error)
|
||||
Create(context.Context, *automation.Script) error
|
||||
Update(context.Context, *automation.Script) error
|
||||
Delete(context.Context, *automation.Script) error
|
||||
}
|
||||
)
|
||||
|
||||
func (AutomationScript) New() *AutomationScript {
|
||||
return &AutomationScript{
|
||||
scripts: service.DefaultAutomationScriptManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) List(ctx context.Context, r *request.AutomationScriptList) (interface{}, error) {
|
||||
set, filter, err := ctrl.scripts.Find(ctx, automation.ScriptFilter{
|
||||
// @todo namespace filtering
|
||||
// Might be a bit tricky as scripts themselves not know about namespaces
|
||||
// Namespace: r.NamespaceID
|
||||
|
||||
Query: r.Query,
|
||||
Resource: r.Resource,
|
||||
|
||||
IncDeleted: false,
|
||||
PageFilter: rh.Paging(r.Page, r.PerPage),
|
||||
})
|
||||
|
||||
return ctrl.makeFilterPayload(ctx, set, filter, err)
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) Create(ctx context.Context, r *request.AutomationScriptCreate) (interface{}, error) {
|
||||
var (
|
||||
script = &automation.Script{
|
||||
Name: r.Name,
|
||||
SourceRef: r.SourceRef,
|
||||
Source: r.Source,
|
||||
Async: r.Async,
|
||||
RunAs: r.RunAs,
|
||||
RunInUA: r.RunInUA,
|
||||
Timeout: r.Timeout,
|
||||
Critical: r.Critical,
|
||||
Enabled: r.Enabled,
|
||||
}
|
||||
)
|
||||
|
||||
return ctrl.makePayload(ctx, script, ctrl.scripts.Create(ctx, script))
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) Read(ctx context.Context, r *request.AutomationScriptRead) (interface{}, error) {
|
||||
script, err := ctrl.scripts.FindByID(ctx, r.ScriptID)
|
||||
return ctrl.makePayload(ctx, script, err)
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) Update(ctx context.Context, r *request.AutomationScriptUpdate) (interface{}, error) {
|
||||
script, err := ctrl.scripts.FindByID(ctx, r.ScriptID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can not update script")
|
||||
}
|
||||
|
||||
script.Name = r.Name
|
||||
script.SourceRef = r.SourceRef
|
||||
script.Source = r.Source
|
||||
script.Async = r.Async
|
||||
script.RunAs = r.RunAs
|
||||
script.RunInUA = r.RunInUA
|
||||
script.Timeout = r.Timeout
|
||||
script.Critical = r.Critical
|
||||
script.Enabled = r.Enabled
|
||||
|
||||
return ctrl.makePayload(ctx, script, ctrl.scripts.Update(ctx, script))
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) Delete(ctx context.Context, r *request.AutomationScriptDelete) (interface{}, error) {
|
||||
script, err := ctrl.scripts.FindByID(ctx, r.ScriptID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can not delete script")
|
||||
}
|
||||
|
||||
return resputil.OK(), ctrl.scripts.Delete(ctx, script)
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) makePayload(ctx context.Context, s *automation.Script, err error) (*automationScriptPayload, error) {
|
||||
if err != nil || s == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &automationScriptPayload{
|
||||
Script: s,
|
||||
|
||||
// CanUpdateModule: ctrl.ac.CanUpdateModule(ctx, s),
|
||||
// CanDeleteModule: ctrl.ac.CanDeleteModule(ctx, s),
|
||||
// CanCreateRecord: ctrl.ac.CanCreateRecord(ctx, s),
|
||||
// CanReadRecord: ctrl.ac.CanReadRecord(ctx, s),
|
||||
// CanUpdateRecord: ctrl.ac.CanUpdateRecord(ctx, s),
|
||||
// CanDeleteRecord: ctrl.ac.CanDeleteRecord(ctx, s),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctrl AutomationScript) makeFilterPayload(ctx context.Context, nn automation.ScriptSet, f automation.ScriptFilter, err error) (*automationScriptSetPayload, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modp := &automationScriptSetPayload{Filter: f, Set: make([]*automationScriptPayload, len(nn))}
|
||||
|
||||
for i := range nn {
|
||||
modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
|
||||
}
|
||||
|
||||
return modp, nil
|
||||
}
|
||||
164
compose/rest/automation_trigger.go
Normal file
164
compose/rest/automation_trigger.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/internal/service"
|
||||
"github.com/cortezaproject/corteza-server/compose/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/pkg/automation"
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
)
|
||||
|
||||
var _ = errors.Wrap
|
||||
|
||||
type (
|
||||
automationTriggerPayload struct {
|
||||
*automation.Trigger
|
||||
}
|
||||
|
||||
automationTriggerSetPayload struct {
|
||||
Filter automation.TriggerFilter `json:"filter"`
|
||||
Set []*automationTriggerPayload `json:"set"`
|
||||
}
|
||||
|
||||
AutomationTrigger struct {
|
||||
triggers automationTriggerService
|
||||
scripts automationScriptFinderService
|
||||
}
|
||||
|
||||
automationTriggerService interface {
|
||||
FindByID(context.Context, uint64) (*automation.Trigger, error)
|
||||
Find(context.Context, automation.TriggerFilter) (automation.TriggerSet, automation.TriggerFilter, error)
|
||||
Create(context.Context, *automation.Script, *automation.Trigger) error
|
||||
Update(context.Context, *automation.Script, *automation.Trigger) error
|
||||
Delete(context.Context, *automation.Trigger) error
|
||||
}
|
||||
|
||||
automationScriptFinderService interface {
|
||||
FindByID(context.Context, uint64) (*automation.Script, error)
|
||||
}
|
||||
)
|
||||
|
||||
func (AutomationTrigger) New() *AutomationTrigger {
|
||||
return &AutomationTrigger{
|
||||
scripts: service.DefaultAutomationScriptManager,
|
||||
triggers: service.DefaultAutomationTriggerManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl AutomationTrigger) List(ctx context.Context, r *request.AutomationTriggerList) (interface{}, error) {
|
||||
set, filter, err := ctrl.triggers.Find(ctx, automation.TriggerFilter{
|
||||
// @todo namespace filtering
|
||||
// Might be a bit tricky as triggers themselves not know about namespaces
|
||||
// Namespace: r.NamespaceID
|
||||
|
||||
Resource: r.Resource,
|
||||
Event: r.Event,
|
||||
ScriptID: r.ScriptID,
|
||||
|
||||
IncDeleted: false,
|
||||
PageFilter: rh.Paging(r.Page, r.PerPage),
|
||||
})
|
||||
|
||||
return ctrl.makeFilterPayload(ctx, set, filter, err)
|
||||
}
|
||||
|
||||
func (ctrl AutomationTrigger) Create(ctx context.Context, r *request.AutomationTriggerCreate) (interface{}, error) {
|
||||
s, _, err := ctrl.loadCombo(ctx, r.ScriptID, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can not create trigger")
|
||||
}
|
||||
|
||||
var (
|
||||
t = &automation.Trigger{
|
||||
Event: r.Event,
|
||||
Resource: r.Resource,
|
||||
Condition: r.Condition,
|
||||
ScriptID: s.ID,
|
||||
Enabled: r.Enabled,
|
||||
}
|
||||
)
|
||||
|
||||
return ctrl.makePayload(ctx, t, ctrl.triggers.Create(ctx, s, t))
|
||||
}
|
||||
|
||||
func (ctrl AutomationTrigger) Read(ctx context.Context, r *request.AutomationTriggerRead) (interface{}, error) {
|
||||
_, t, err := ctrl.loadCombo(ctx, r.ScriptID, 0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can not read trigger")
|
||||
}
|
||||
|
||||
return ctrl.makePayload(ctx, t, err)
|
||||
}
|
||||
|
||||
func (ctrl AutomationTrigger) Update(ctx context.Context, r *request.AutomationTriggerUpdate) (interface{}, error) {
|
||||
s, t, err := ctrl.loadCombo(ctx, r.ScriptID, r.TriggerID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can not update trigger")
|
||||
}
|
||||
|
||||
t.Event = r.Event
|
||||
t.Resource = r.Resource
|
||||
t.Condition = r.Condition
|
||||
t.ScriptID = r.ScriptID
|
||||
t.Enabled = r.Enabled
|
||||
|
||||
return ctrl.makePayload(ctx, t, ctrl.triggers.Update(ctx, s, t))
|
||||
}
|
||||
|
||||
func (ctrl AutomationTrigger) Delete(ctx context.Context, r *request.AutomationTriggerDelete) (interface{}, error) {
|
||||
trigger, err := ctrl.triggers.FindByID(ctx, r.TriggerID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "can not delete trigger")
|
||||
}
|
||||
|
||||
return resputil.OK(), ctrl.triggers.Delete(ctx, trigger)
|
||||
}
|
||||
|
||||
func (ctrl AutomationTrigger) loadCombo(ctx context.Context, scriptID, triggerID uint64) (s *automation.Script, t *automation.Trigger, err error) {
|
||||
if triggerID > 0 {
|
||||
t, err = ctrl.triggers.FindByID(ctx, triggerID)
|
||||
return
|
||||
}
|
||||
|
||||
if scriptID > 0 {
|
||||
s, err = ctrl.scripts.FindByID(ctx, scriptID)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ctrl AutomationTrigger) makePayload(ctx context.Context, s *automation.Trigger, err error) (*automationTriggerPayload, error) {
|
||||
if err != nil || s == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &automationTriggerPayload{
|
||||
Trigger: s,
|
||||
|
||||
// CanUpdateModule: ctrl.ac.CanUpdateModule(ctx, s),
|
||||
// CanDeleteModule: ctrl.ac.CanDeleteModule(ctx, s),
|
||||
// CanCreateRecord: ctrl.ac.CanCreateRecord(ctx, s),
|
||||
// CanReadRecord: ctrl.ac.CanReadRecord(ctx, s),
|
||||
// CanUpdateRecord: ctrl.ac.CanUpdateRecord(ctx, s),
|
||||
// CanDeleteRecord: ctrl.ac.CanDeleteRecord(ctx, s),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctrl AutomationTrigger) makeFilterPayload(ctx context.Context, nn automation.TriggerSet, f automation.TriggerFilter, err error) (*automationTriggerSetPayload, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modp := &automationTriggerSetPayload{Filter: f, Set: make([]*automationTriggerPayload, len(nn))}
|
||||
|
||||
for i := range nn {
|
||||
modp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
|
||||
}
|
||||
|
||||
return modp, nil
|
||||
}
|
||||
@@ -10,8 +10,8 @@ package handlers
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `trigger.go`, `trigger.util.go` or `trigger_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `trigger.go`
|
||||
You may edit `automation_script.go`, `automation_script.util.go` or `automation_script_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `automation_script.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
@@ -28,16 +28,16 @@ import (
|
||||
)
|
||||
|
||||
// Internal API interface
|
||||
type TriggerAPI interface {
|
||||
List(context.Context, *request.TriggerList) (interface{}, error)
|
||||
Create(context.Context, *request.TriggerCreate) (interface{}, error)
|
||||
Read(context.Context, *request.TriggerRead) (interface{}, error)
|
||||
Update(context.Context, *request.TriggerUpdate) (interface{}, error)
|
||||
Delete(context.Context, *request.TriggerDelete) (interface{}, error)
|
||||
type AutomationScriptAPI interface {
|
||||
List(context.Context, *request.AutomationScriptList) (interface{}, error)
|
||||
Create(context.Context, *request.AutomationScriptCreate) (interface{}, error)
|
||||
Read(context.Context, *request.AutomationScriptRead) (interface{}, error)
|
||||
Update(context.Context, *request.AutomationScriptUpdate) (interface{}, error)
|
||||
Delete(context.Context, *request.AutomationScriptDelete) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type Trigger struct {
|
||||
type AutomationScript struct {
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
@@ -45,104 +45,104 @@ type Trigger struct {
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewTrigger(h TriggerAPI) *Trigger {
|
||||
return &Trigger{
|
||||
func NewAutomationScript(h AutomationScriptAPI) *AutomationScript {
|
||||
return &AutomationScript{
|
||||
List: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewTriggerList()
|
||||
params := request.NewAutomationScriptList()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("Trigger.List", r, err)
|
||||
logger.LogParamError("AutomationScript.List", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.List(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("Trigger.List", r, err, params.Auditable())
|
||||
logger.LogControllerError("AutomationScript.List", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("Trigger.List", r, params.Auditable())
|
||||
logger.LogControllerCall("AutomationScript.List", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Create: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewTriggerCreate()
|
||||
params := request.NewAutomationScriptCreate()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("Trigger.Create", r, err)
|
||||
logger.LogParamError("AutomationScript.Create", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Create(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("Trigger.Create", r, err, params.Auditable())
|
||||
logger.LogControllerError("AutomationScript.Create", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("Trigger.Create", r, params.Auditable())
|
||||
logger.LogControllerCall("AutomationScript.Create", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Read: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewTriggerRead()
|
||||
params := request.NewAutomationScriptRead()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("Trigger.Read", r, err)
|
||||
logger.LogParamError("AutomationScript.Read", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Read(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("Trigger.Read", r, err, params.Auditable())
|
||||
logger.LogControllerError("AutomationScript.Read", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("Trigger.Read", r, params.Auditable())
|
||||
logger.LogControllerCall("AutomationScript.Read", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Update: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewTriggerUpdate()
|
||||
params := request.NewAutomationScriptUpdate()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("Trigger.Update", r, err)
|
||||
logger.LogParamError("AutomationScript.Update", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Update(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("Trigger.Update", r, err, params.Auditable())
|
||||
logger.LogControllerError("AutomationScript.Update", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("Trigger.Update", r, params.Auditable())
|
||||
logger.LogControllerCall("AutomationScript.Update", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Delete: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewTriggerDelete()
|
||||
params := request.NewAutomationScriptDelete()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("Trigger.Delete", r, err)
|
||||
logger.LogParamError("AutomationScript.Delete", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Delete(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("Trigger.Delete", r, err, params.Auditable())
|
||||
logger.LogControllerError("AutomationScript.Delete", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("Trigger.Delete", r, params.Auditable())
|
||||
logger.LogControllerCall("AutomationScript.Delete", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
@@ -150,13 +150,13 @@ func NewTrigger(h TriggerAPI) *Trigger {
|
||||
}
|
||||
}
|
||||
|
||||
func (h Trigger) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
func (h AutomationScript) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Get("/namespace/{namespaceID}/trigger/", h.List)
|
||||
r.Post("/namespace/{namespaceID}/trigger/", h.Create)
|
||||
r.Get("/namespace/{namespaceID}/trigger/{triggerID}", h.Read)
|
||||
r.Post("/namespace/{namespaceID}/trigger/{triggerID}", h.Update)
|
||||
r.Delete("/namespace/{namespaceID}/trigger/{triggerID}", h.Delete)
|
||||
r.Get("/namespace/{namespaceID}/automation/script/", h.List)
|
||||
r.Post("/namespace/{namespaceID}/automation/script/", h.Create)
|
||||
r.Get("/namespace/{namespaceID}/automation/script/{scriptID}", h.Read)
|
||||
r.Post("/namespace/{namespaceID}/automation/script/{scriptID}", h.Update)
|
||||
r.Delete("/namespace/{namespaceID}/automation/script/{scriptID}", h.Delete)
|
||||
})
|
||||
}
|
||||
162
compose/rest/handlers/automation_trigger.go
Normal file
162
compose/rest/handlers/automation_trigger.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package handlers
|
||||
|
||||
/*
|
||||
Hello! This file is auto-generated from `docs/src/spec.json`.
|
||||
|
||||
For development:
|
||||
In order to update the generated files, edit this file under the location,
|
||||
add your struct fields, imports, API definitions and whatever you want, and:
|
||||
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `automation_trigger.go`, `automation_trigger.util.go` or `automation_trigger_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `automation_trigger.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
)
|
||||
|
||||
// Internal API interface
|
||||
type AutomationTriggerAPI interface {
|
||||
List(context.Context, *request.AutomationTriggerList) (interface{}, error)
|
||||
Create(context.Context, *request.AutomationTriggerCreate) (interface{}, error)
|
||||
Read(context.Context, *request.AutomationTriggerRead) (interface{}, error)
|
||||
Update(context.Context, *request.AutomationTriggerUpdate) (interface{}, error)
|
||||
Delete(context.Context, *request.AutomationTriggerDelete) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type AutomationTrigger struct {
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewAutomationTrigger(h AutomationTriggerAPI) *AutomationTrigger {
|
||||
return &AutomationTrigger{
|
||||
List: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAutomationTriggerList()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("AutomationTrigger.List", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.List(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("AutomationTrigger.List", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("AutomationTrigger.List", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Create: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAutomationTriggerCreate()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("AutomationTrigger.Create", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Create(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("AutomationTrigger.Create", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("AutomationTrigger.Create", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Read: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAutomationTriggerRead()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("AutomationTrigger.Read", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Read(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("AutomationTrigger.Read", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("AutomationTrigger.Read", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Update: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAutomationTriggerUpdate()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("AutomationTrigger.Update", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Update(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("AutomationTrigger.Update", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("AutomationTrigger.Update", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Delete: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewAutomationTriggerDelete()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("AutomationTrigger.Delete", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.Delete(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("AutomationTrigger.Delete", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("AutomationTrigger.Delete", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h AutomationTrigger) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middlewares...)
|
||||
r.Get("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/", h.List)
|
||||
r.Post("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/", h.Create)
|
||||
r.Get("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}", h.Read)
|
||||
r.Post("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}", h.Update)
|
||||
r.Delete("/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}", h.Delete)
|
||||
})
|
||||
}
|
||||
@@ -36,19 +36,21 @@ type RecordAPI interface {
|
||||
Read(context.Context, *request.RecordRead) (interface{}, error)
|
||||
Update(context.Context, *request.RecordUpdate) (interface{}, error)
|
||||
Delete(context.Context, *request.RecordDelete) (interface{}, error)
|
||||
RunScript(context.Context, *request.RecordRunScript) (interface{}, error)
|
||||
Upload(context.Context, *request.RecordUpload) (interface{}, error)
|
||||
}
|
||||
|
||||
// HTTP API interface
|
||||
type Record struct {
|
||||
Report func(http.ResponseWriter, *http.Request)
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Export func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
Upload func(http.ResponseWriter, *http.Request)
|
||||
Report func(http.ResponseWriter, *http.Request)
|
||||
List func(http.ResponseWriter, *http.Request)
|
||||
Export func(http.ResponseWriter, *http.Request)
|
||||
Create func(http.ResponseWriter, *http.Request)
|
||||
Read func(http.ResponseWriter, *http.Request)
|
||||
Update func(http.ResponseWriter, *http.Request)
|
||||
Delete func(http.ResponseWriter, *http.Request)
|
||||
RunScript func(http.ResponseWriter, *http.Request)
|
||||
Upload func(http.ResponseWriter, *http.Request)
|
||||
}
|
||||
|
||||
func NewRecord(h RecordAPI) *Record {
|
||||
@@ -193,6 +195,26 @@ func NewRecord(h RecordAPI) *Record {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
RunScript: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewRecordRunScript()
|
||||
if err := params.Fill(r); err != nil {
|
||||
logger.LogParamError("Record.RunScript", r, err)
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
value, err := h.RunScript(r.Context(), params)
|
||||
if err != nil {
|
||||
logger.LogControllerError("Record.RunScript", r, err, params.Auditable())
|
||||
resputil.JSON(w, err)
|
||||
return
|
||||
}
|
||||
logger.LogControllerCall("Record.RunScript", r, params.Auditable())
|
||||
if !serveHTTP(value, w, r) {
|
||||
resputil.JSON(w, value)
|
||||
}
|
||||
},
|
||||
Upload: func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
params := request.NewRecordUpload()
|
||||
@@ -226,6 +248,7 @@ func (h Record) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http
|
||||
r.Get("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Read)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Update)
|
||||
r.Delete("/namespace/{namespaceID}/module/{moduleID}/record/{recordID}", h.Delete)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/run-script", h.RunScript)
|
||||
r.Post("/namespace/{namespaceID}/module/{moduleID}/record/attachment", h.Upload)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -229,6 +229,15 @@ func (ctrl *Record) Export(ctx context.Context, r *request.RecordExport) (interf
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctrl *Record) RunScript(ctx context.Context, r *request.RecordRunScript) (interface{}, error) {
|
||||
return resputil.OK(), ctrl.record.RunScript(
|
||||
r.NamespaceID,
|
||||
r.ModuleID,
|
||||
r.RecordID,
|
||||
r.ScriptID,
|
||||
)
|
||||
}
|
||||
|
||||
func (ctrl Record) makePayload(ctx context.Context, m *types.Module, r *types.Record, err error) (*recordPayload, error) {
|
||||
if err != nil || r == nil {
|
||||
return nil, err
|
||||
|
||||
410
compose/rest/request/automation_script.go
Normal file
410
compose/rest/request/automation_script.go
Normal file
@@ -0,0 +1,410 @@
|
||||
package request
|
||||
|
||||
/*
|
||||
Hello! This file is auto-generated from `docs/src/spec.json`.
|
||||
|
||||
For development:
|
||||
In order to update the generated files, edit this file under the location,
|
||||
add your struct fields, imports, API definitions and whatever you want, and:
|
||||
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `automation_script.go`, `automation_script.util.go` or `automation_script_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `automation_script.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"encoding/json"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// AutomationScript list request parameters
|
||||
type AutomationScriptList struct {
|
||||
Query string
|
||||
Resource string
|
||||
IncDeleted bool
|
||||
Page uint
|
||||
PerPage uint
|
||||
NamespaceID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewAutomationScriptList() *AutomationScriptList {
|
||||
return &AutomationScriptList{}
|
||||
}
|
||||
|
||||
func (r AutomationScriptList) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["query"] = r.Query
|
||||
out["resource"] = r.Resource
|
||||
out["incDeleted"] = r.IncDeleted
|
||||
out["page"] = r.Page
|
||||
out["perPage"] = r.PerPage
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *AutomationScriptList) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := get["query"]; ok {
|
||||
r.Query = val
|
||||
}
|
||||
if val, ok := get["resource"]; ok {
|
||||
r.Resource = val
|
||||
}
|
||||
if val, ok := get["incDeleted"]; ok {
|
||||
r.IncDeleted = parseBool(val)
|
||||
}
|
||||
if val, ok := get["page"]; ok {
|
||||
r.Page = parseUint(val)
|
||||
}
|
||||
if val, ok := get["perPage"]; ok {
|
||||
r.PerPage = parseUint(val)
|
||||
}
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAutomationScriptList()
|
||||
|
||||
// AutomationScript create request parameters
|
||||
type AutomationScriptCreate struct {
|
||||
Name string
|
||||
SourceRef string
|
||||
Source string
|
||||
RunAs uint64 `json:",string"`
|
||||
RunInUA bool
|
||||
Timeout uint
|
||||
Critical bool
|
||||
Async bool
|
||||
Enabled bool
|
||||
NamespaceID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewAutomationScriptCreate() *AutomationScriptCreate {
|
||||
return &AutomationScriptCreate{}
|
||||
}
|
||||
|
||||
func (r AutomationScriptCreate) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["name"] = r.Name
|
||||
out["sourceRef"] = r.SourceRef
|
||||
out["source"] = r.Source
|
||||
out["runAs"] = r.RunAs
|
||||
out["runInUA"] = r.RunInUA
|
||||
out["timeout"] = r.Timeout
|
||||
out["critical"] = r.Critical
|
||||
out["async"] = r.Async
|
||||
out["enabled"] = r.Enabled
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *AutomationScriptCreate) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["name"]; ok {
|
||||
r.Name = val
|
||||
}
|
||||
if val, ok := post["sourceRef"]; ok {
|
||||
r.SourceRef = val
|
||||
}
|
||||
if val, ok := post["source"]; ok {
|
||||
r.Source = val
|
||||
}
|
||||
if val, ok := post["runAs"]; ok {
|
||||
r.RunAs = parseUInt64(val)
|
||||
}
|
||||
if val, ok := post["runInUA"]; ok {
|
||||
r.RunInUA = parseBool(val)
|
||||
}
|
||||
if val, ok := post["timeout"]; ok {
|
||||
r.Timeout = parseUint(val)
|
||||
}
|
||||
if val, ok := post["critical"]; ok {
|
||||
r.Critical = parseBool(val)
|
||||
}
|
||||
if val, ok := post["async"]; ok {
|
||||
r.Async = parseBool(val)
|
||||
}
|
||||
if val, ok := post["enabled"]; ok {
|
||||
r.Enabled = parseBool(val)
|
||||
}
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAutomationScriptCreate()
|
||||
|
||||
// AutomationScript read request parameters
|
||||
type AutomationScriptRead struct {
|
||||
ScriptID uint64 `json:",string"`
|
||||
NamespaceID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewAutomationScriptRead() *AutomationScriptRead {
|
||||
return &AutomationScriptRead{}
|
||||
}
|
||||
|
||||
func (r AutomationScriptRead) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["scriptID"] = r.ScriptID
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *AutomationScriptRead) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID"))
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAutomationScriptRead()
|
||||
|
||||
// AutomationScript update request parameters
|
||||
type AutomationScriptUpdate struct {
|
||||
ScriptID uint64 `json:",string"`
|
||||
NamespaceID uint64 `json:",string"`
|
||||
Name string
|
||||
SourceRef string
|
||||
Source string
|
||||
RunAs uint64 `json:",string"`
|
||||
RunInUA bool
|
||||
Timeout uint
|
||||
Critical bool
|
||||
Async bool
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
func NewAutomationScriptUpdate() *AutomationScriptUpdate {
|
||||
return &AutomationScriptUpdate{}
|
||||
}
|
||||
|
||||
func (r AutomationScriptUpdate) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["scriptID"] = r.ScriptID
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
out["name"] = r.Name
|
||||
out["sourceRef"] = r.SourceRef
|
||||
out["source"] = r.Source
|
||||
out["runAs"] = r.RunAs
|
||||
out["runInUA"] = r.RunInUA
|
||||
out["timeout"] = r.Timeout
|
||||
out["critical"] = r.Critical
|
||||
out["async"] = r.Async
|
||||
out["enabled"] = r.Enabled
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *AutomationScriptUpdate) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID"))
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
if val, ok := post["name"]; ok {
|
||||
r.Name = val
|
||||
}
|
||||
if val, ok := post["sourceRef"]; ok {
|
||||
r.SourceRef = val
|
||||
}
|
||||
if val, ok := post["source"]; ok {
|
||||
r.Source = val
|
||||
}
|
||||
if val, ok := post["runAs"]; ok {
|
||||
r.RunAs = parseUInt64(val)
|
||||
}
|
||||
if val, ok := post["runInUA"]; ok {
|
||||
r.RunInUA = parseBool(val)
|
||||
}
|
||||
if val, ok := post["timeout"]; ok {
|
||||
r.Timeout = parseUint(val)
|
||||
}
|
||||
if val, ok := post["critical"]; ok {
|
||||
r.Critical = parseBool(val)
|
||||
}
|
||||
if val, ok := post["async"]; ok {
|
||||
r.Async = parseBool(val)
|
||||
}
|
||||
if val, ok := post["enabled"]; ok {
|
||||
r.Enabled = parseBool(val)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAutomationScriptUpdate()
|
||||
|
||||
// AutomationScript delete request parameters
|
||||
type AutomationScriptDelete struct {
|
||||
ScriptID uint64 `json:",string"`
|
||||
NamespaceID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewAutomationScriptDelete() *AutomationScriptDelete {
|
||||
return &AutomationScriptDelete{}
|
||||
}
|
||||
|
||||
func (r AutomationScriptDelete) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["scriptID"] = r.ScriptID
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *AutomationScriptDelete) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID"))
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewAutomationScriptDelete()
|
||||
@@ -10,8 +10,8 @@ package request
|
||||
1. run [spec](https://github.com/titpetric/spec) in the same folder,
|
||||
2. run `./_gen.php` in this folder.
|
||||
|
||||
You may edit `trigger.go`, `trigger.util.go` or `trigger_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `trigger.go`
|
||||
You may edit `automation_trigger.go`, `automation_trigger.util.go` or `automation_trigger_test.go` to
|
||||
implement your API calls, helper functions and tests. The file `automation_trigger.go`
|
||||
is only generated the first time, and will not be overwritten if it exists.
|
||||
*/
|
||||
|
||||
@@ -25,39 +25,41 @@ import (
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ = chi.URLParam
|
||||
var _ = multipart.FileHeader{}
|
||||
|
||||
// Trigger list request parameters
|
||||
type TriggerList struct {
|
||||
ModuleID uint64 `json:",string"`
|
||||
Query string
|
||||
// AutomationTrigger list request parameters
|
||||
type AutomationTriggerList struct {
|
||||
Resource string
|
||||
Event string
|
||||
IncDeleted bool
|
||||
Page uint
|
||||
PerPage uint
|
||||
NamespaceID uint64 `json:",string"`
|
||||
ScriptID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewTriggerList() *TriggerList {
|
||||
return &TriggerList{}
|
||||
func NewAutomationTriggerList() *AutomationTriggerList {
|
||||
return &AutomationTriggerList{}
|
||||
}
|
||||
|
||||
func (r TriggerList) Auditable() map[string]interface{} {
|
||||
func (r AutomationTriggerList) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["moduleID"] = r.ModuleID
|
||||
out["query"] = r.Query
|
||||
out["resource"] = r.Resource
|
||||
out["event"] = r.Event
|
||||
out["incDeleted"] = r.IncDeleted
|
||||
out["page"] = r.Page
|
||||
out["perPage"] = r.PerPage
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
out["scriptID"] = r.ScriptID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *TriggerList) Fill(req *http.Request) (err error) {
|
||||
func (r *AutomationTriggerList) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
@@ -84,11 +86,14 @@ func (r *TriggerList) Fill(req *http.Request) (err error) {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := get["moduleID"]; ok {
|
||||
r.ModuleID = parseUInt64(val)
|
||||
if val, ok := get["resource"]; ok {
|
||||
r.Resource = val
|
||||
}
|
||||
if val, ok := get["query"]; ok {
|
||||
r.Query = val
|
||||
if val, ok := get["event"]; ok {
|
||||
r.Event = val
|
||||
}
|
||||
if val, ok := get["incDeleted"]; ok {
|
||||
r.IncDeleted = parseBool(val)
|
||||
}
|
||||
if val, ok := get["page"]; ok {
|
||||
r.Page = parseUint(val)
|
||||
@@ -97,42 +102,43 @@ func (r *TriggerList) Fill(req *http.Request) (err error) {
|
||||
r.PerPage = parseUint(val)
|
||||
}
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewTriggerList()
|
||||
var _ RequestFiller = NewAutomationTriggerList()
|
||||
|
||||
// Trigger create request parameters
|
||||
type TriggerCreate struct {
|
||||
ModuleID uint64 `json:",string"`
|
||||
// AutomationTrigger create request parameters
|
||||
type AutomationTriggerCreate struct {
|
||||
Name string
|
||||
Actions []string
|
||||
Resource string
|
||||
Event string
|
||||
Condition string
|
||||
Enabled bool
|
||||
Source string
|
||||
UpdatedAt *time.Time
|
||||
NamespaceID uint64 `json:",string"`
|
||||
ScriptID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewTriggerCreate() *TriggerCreate {
|
||||
return &TriggerCreate{}
|
||||
func NewAutomationTriggerCreate() *AutomationTriggerCreate {
|
||||
return &AutomationTriggerCreate{}
|
||||
}
|
||||
|
||||
func (r TriggerCreate) Auditable() map[string]interface{} {
|
||||
func (r AutomationTriggerCreate) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["moduleID"] = r.ModuleID
|
||||
out["name"] = r.Name
|
||||
out["actions"] = r.Actions
|
||||
out["resource"] = r.Resource
|
||||
out["event"] = r.Event
|
||||
out["condition"] = r.Condition
|
||||
out["enabled"] = r.Enabled
|
||||
out["source"] = r.Source
|
||||
out["updatedAt"] = r.UpdatedAt
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
out["scriptID"] = r.ScriptID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *TriggerCreate) Fill(req *http.Request) (err error) {
|
||||
func (r *AutomationTriggerCreate) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
@@ -159,56 +165,51 @@ func (r *TriggerCreate) Fill(req *http.Request) (err error) {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["moduleID"]; ok {
|
||||
r.ModuleID = parseUInt64(val)
|
||||
}
|
||||
if val, ok := post["name"]; ok {
|
||||
r.Name = val
|
||||
}
|
||||
|
||||
if val, ok := req.Form["actions"]; ok {
|
||||
r.Actions = parseStrings(val)
|
||||
if val, ok := post["resource"]; ok {
|
||||
r.Resource = val
|
||||
}
|
||||
if val, ok := post["event"]; ok {
|
||||
r.Event = val
|
||||
}
|
||||
if val, ok := post["condition"]; ok {
|
||||
r.Condition = val
|
||||
}
|
||||
|
||||
if val, ok := post["enabled"]; ok {
|
||||
r.Enabled = parseBool(val)
|
||||
}
|
||||
if val, ok := post["source"]; ok {
|
||||
r.Source = val
|
||||
}
|
||||
if val, ok := post["updatedAt"]; ok {
|
||||
|
||||
if r.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewTriggerCreate()
|
||||
var _ RequestFiller = NewAutomationTriggerCreate()
|
||||
|
||||
// Trigger read request parameters
|
||||
type TriggerRead struct {
|
||||
// AutomationTrigger read request parameters
|
||||
type AutomationTriggerRead struct {
|
||||
TriggerID uint64 `json:",string"`
|
||||
NamespaceID uint64 `json:",string"`
|
||||
ScriptID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewTriggerRead() *TriggerRead {
|
||||
return &TriggerRead{}
|
||||
func NewAutomationTriggerRead() *AutomationTriggerRead {
|
||||
return &AutomationTriggerRead{}
|
||||
}
|
||||
|
||||
func (r TriggerRead) Auditable() map[string]interface{} {
|
||||
func (r AutomationTriggerRead) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["triggerID"] = r.TriggerID
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
out["scriptID"] = r.ScriptID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *TriggerRead) Fill(req *http.Request) (err error) {
|
||||
func (r *AutomationTriggerRead) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
@@ -237,42 +238,45 @@ func (r *TriggerRead) Fill(req *http.Request) (err error) {
|
||||
|
||||
r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID"))
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewTriggerRead()
|
||||
var _ RequestFiller = NewAutomationTriggerRead()
|
||||
|
||||
// Trigger update request parameters
|
||||
type TriggerUpdate struct {
|
||||
// AutomationTrigger update request parameters
|
||||
type AutomationTriggerUpdate struct {
|
||||
TriggerID uint64 `json:",string"`
|
||||
NamespaceID uint64 `json:",string"`
|
||||
ModuleID uint64 `json:",string"`
|
||||
ScriptID uint64 `json:",string"`
|
||||
Name string
|
||||
Actions []string
|
||||
Resource string
|
||||
Event string
|
||||
Condition string
|
||||
Enabled bool
|
||||
Source string
|
||||
}
|
||||
|
||||
func NewTriggerUpdate() *TriggerUpdate {
|
||||
return &TriggerUpdate{}
|
||||
func NewAutomationTriggerUpdate() *AutomationTriggerUpdate {
|
||||
return &AutomationTriggerUpdate{}
|
||||
}
|
||||
|
||||
func (r TriggerUpdate) Auditable() map[string]interface{} {
|
||||
func (r AutomationTriggerUpdate) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["triggerID"] = r.TriggerID
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
out["moduleID"] = r.ModuleID
|
||||
out["scriptID"] = r.ScriptID
|
||||
out["name"] = r.Name
|
||||
out["actions"] = r.Actions
|
||||
out["resource"] = r.Resource
|
||||
out["event"] = r.Event
|
||||
out["condition"] = r.Condition
|
||||
out["enabled"] = r.Enabled
|
||||
out["source"] = r.Source
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *TriggerUpdate) Fill(req *http.Request) (err error) {
|
||||
func (r *AutomationTriggerUpdate) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
@@ -301,49 +305,50 @@ func (r *TriggerUpdate) Fill(req *http.Request) (err error) {
|
||||
|
||||
r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID"))
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
if val, ok := post["moduleID"]; ok {
|
||||
r.ModuleID = parseUInt64(val)
|
||||
}
|
||||
r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID"))
|
||||
if val, ok := post["name"]; ok {
|
||||
r.Name = val
|
||||
}
|
||||
|
||||
if val, ok := req.Form["actions"]; ok {
|
||||
r.Actions = parseStrings(val)
|
||||
if val, ok := post["resource"]; ok {
|
||||
r.Resource = val
|
||||
}
|
||||
if val, ok := post["event"]; ok {
|
||||
r.Event = val
|
||||
}
|
||||
if val, ok := post["condition"]; ok {
|
||||
r.Condition = val
|
||||
}
|
||||
|
||||
if val, ok := post["enabled"]; ok {
|
||||
r.Enabled = parseBool(val)
|
||||
}
|
||||
if val, ok := post["source"]; ok {
|
||||
r.Source = val
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewTriggerUpdate()
|
||||
var _ RequestFiller = NewAutomationTriggerUpdate()
|
||||
|
||||
// Trigger delete request parameters
|
||||
type TriggerDelete struct {
|
||||
// AutomationTrigger delete request parameters
|
||||
type AutomationTriggerDelete struct {
|
||||
TriggerID uint64 `json:",string"`
|
||||
NamespaceID uint64 `json:",string"`
|
||||
ScriptID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewTriggerDelete() *TriggerDelete {
|
||||
return &TriggerDelete{}
|
||||
func NewAutomationTriggerDelete() *AutomationTriggerDelete {
|
||||
return &AutomationTriggerDelete{}
|
||||
}
|
||||
|
||||
func (r TriggerDelete) Auditable() map[string]interface{} {
|
||||
func (r AutomationTriggerDelete) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["triggerID"] = r.TriggerID
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
out["scriptID"] = r.ScriptID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *TriggerDelete) Fill(req *http.Request) (err error) {
|
||||
func (r *AutomationTriggerDelete) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
@@ -372,8 +377,9 @@ func (r *TriggerDelete) Fill(req *http.Request) (err error) {
|
||||
|
||||
r.TriggerID = parseUInt64(chi.URLParam(req, "triggerID"))
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
r.ScriptID = parseUInt64(chi.URLParam(req, "scriptID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewTriggerDelete()
|
||||
var _ RequestFiller = NewAutomationTriggerDelete()
|
||||
@@ -478,6 +478,70 @@ func (r *RecordDelete) Fill(req *http.Request) (err error) {
|
||||
|
||||
var _ RequestFiller = NewRecordDelete()
|
||||
|
||||
// Record runScript request parameters
|
||||
type RecordRunScript struct {
|
||||
RecordID uint64 `json:",string"`
|
||||
ScriptID uint64 `json:",string"`
|
||||
NamespaceID uint64 `json:",string"`
|
||||
ModuleID uint64 `json:",string"`
|
||||
}
|
||||
|
||||
func NewRecordRunScript() *RecordRunScript {
|
||||
return &RecordRunScript{}
|
||||
}
|
||||
|
||||
func (r RecordRunScript) Auditable() map[string]interface{} {
|
||||
var out = map[string]interface{}{}
|
||||
|
||||
out["recordID"] = r.RecordID
|
||||
out["scriptID"] = r.ScriptID
|
||||
out["namespaceID"] = r.NamespaceID
|
||||
out["moduleID"] = r.ModuleID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r *RecordRunScript) Fill(req *http.Request) (err error) {
|
||||
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
|
||||
err = json.NewDecoder(req.Body).Decode(r)
|
||||
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
err = nil
|
||||
case err != nil:
|
||||
return errors.Wrap(err, "error parsing http request body")
|
||||
}
|
||||
}
|
||||
|
||||
if err = req.ParseForm(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
get := map[string]string{}
|
||||
post := map[string]string{}
|
||||
urlQuery := req.URL.Query()
|
||||
for name, param := range urlQuery {
|
||||
get[name] = string(param[0])
|
||||
}
|
||||
postVars := req.Form
|
||||
for name, param := range postVars {
|
||||
post[name] = string(param[0])
|
||||
}
|
||||
|
||||
if val, ok := post["recordID"]; ok {
|
||||
r.RecordID = parseUInt64(val)
|
||||
}
|
||||
if val, ok := post["scriptID"]; ok {
|
||||
r.ScriptID = parseUInt64(val)
|
||||
}
|
||||
r.NamespaceID = parseUInt64(chi.URLParam(req, "namespaceID"))
|
||||
r.ModuleID = parseUInt64(chi.URLParam(req, "moduleID"))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var _ RequestFiller = NewRecordRunScript()
|
||||
|
||||
// Record upload request parameters
|
||||
type RecordUpload struct {
|
||||
RecordID uint64 `json:",string"`
|
||||
|
||||
@@ -14,9 +14,11 @@ func MountRoutes(r chi.Router) {
|
||||
record = Record{}.New()
|
||||
page = Page{}.New()
|
||||
chart = Chart{}.New()
|
||||
trigger = Trigger{}.New()
|
||||
notification = Notification{}.New()
|
||||
attachment = Attachment{}.New()
|
||||
|
||||
automationScript = AutomationScript{}.New()
|
||||
automationTrgger = AutomationTrigger{}.New()
|
||||
)
|
||||
|
||||
// Initialize handlers & controllers.
|
||||
@@ -34,8 +36,10 @@ func MountRoutes(r chi.Router) {
|
||||
handlers.NewModule(module).MountRoutes(r)
|
||||
handlers.NewRecord(record).MountRoutes(r)
|
||||
handlers.NewChart(chart).MountRoutes(r)
|
||||
handlers.NewTrigger(trigger).MountRoutes(r)
|
||||
handlers.NewNotification(notification).MountRoutes(r)
|
||||
|
||||
handlers.NewAutomationScript(automationScript).MountRoutes(r)
|
||||
handlers.NewAutomationTrigger(automationTrgger).MountRoutes(r)
|
||||
})
|
||||
|
||||
// Use alternative handlers that support file serving
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/titpetric/factory/resputil"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/compose/internal/service"
|
||||
"github.com/cortezaproject/corteza-server/compose/rest/request"
|
||||
"github.com/cortezaproject/corteza-server/compose/types"
|
||||
)
|
||||
|
||||
type (
|
||||
triggerPayload struct {
|
||||
*types.Trigger
|
||||
|
||||
CanGrant bool `json:"canGrant"`
|
||||
CanUpdateTrigger bool `json:"canUpdateTrigger"`
|
||||
CanDeleteTrigger bool `json:"canDeleteTrigger"`
|
||||
}
|
||||
|
||||
triggerSetPayload struct {
|
||||
Filter types.TriggerFilter `json:"filter"`
|
||||
Set []*triggerPayload `json:"set"`
|
||||
}
|
||||
|
||||
Trigger struct {
|
||||
trigger service.TriggerService
|
||||
ac triggerAccessController
|
||||
}
|
||||
|
||||
triggerAccessController interface {
|
||||
CanGrant(context.Context) bool
|
||||
|
||||
CanUpdateTrigger(context.Context, *types.Trigger) bool
|
||||
CanDeleteTrigger(context.Context, *types.Trigger) bool
|
||||
}
|
||||
)
|
||||
|
||||
func (Trigger) New() *Trigger {
|
||||
return &Trigger{
|
||||
trigger: service.DefaultTrigger,
|
||||
ac: service.DefaultAccessControl,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl Trigger) List(ctx context.Context, r *request.TriggerList) (interface{}, error) {
|
||||
f := types.TriggerFilter{
|
||||
NamespaceID: r.NamespaceID,
|
||||
Query: r.Query,
|
||||
PerPage: r.PerPage,
|
||||
Page: r.Page,
|
||||
}
|
||||
|
||||
set, filter, err := ctrl.trigger.With(ctx).Find(f)
|
||||
return ctrl.makeFilterPayload(ctx, set, filter, err)
|
||||
}
|
||||
|
||||
func (ctrl Trigger) Create(ctx context.Context, r *request.TriggerCreate) (interface{}, error) {
|
||||
var (
|
||||
err error
|
||||
ns = &types.Trigger{
|
||||
NamespaceID: r.NamespaceID,
|
||||
ModuleID: r.ModuleID,
|
||||
Name: r.Name,
|
||||
Actions: r.Actions,
|
||||
Enabled: r.Enabled,
|
||||
Source: r.Source,
|
||||
}
|
||||
)
|
||||
|
||||
ns, err = ctrl.trigger.With(ctx).Create(ns)
|
||||
return ctrl.makePayload(ctx, ns, err)
|
||||
}
|
||||
|
||||
func (ctrl Trigger) Read(ctx context.Context, r *request.TriggerRead) (interface{}, error) {
|
||||
mod, err := ctrl.trigger.With(ctx).FindByID(r.NamespaceID, r.TriggerID)
|
||||
return ctrl.makePayload(ctx, mod, err)
|
||||
}
|
||||
|
||||
func (ctrl Trigger) Update(ctx context.Context, r *request.TriggerUpdate) (interface{}, error) {
|
||||
var (
|
||||
mod = &types.Trigger{}
|
||||
err error
|
||||
)
|
||||
|
||||
mod.ID = r.TriggerID
|
||||
mod.NamespaceID = r.NamespaceID
|
||||
mod.ModuleID = r.ModuleID
|
||||
mod.Name = r.Name
|
||||
mod.Actions = r.Actions
|
||||
mod.Enabled = r.Enabled
|
||||
mod.Source = r.Source
|
||||
|
||||
mod, err = ctrl.trigger.With(ctx).Update(mod)
|
||||
return ctrl.makePayload(ctx, mod, err)
|
||||
}
|
||||
|
||||
func (ctrl Trigger) Delete(ctx context.Context, r *request.TriggerDelete) (interface{}, error) {
|
||||
_, err := ctrl.trigger.With(ctx).FindByID(r.NamespaceID, r.TriggerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resputil.OK(), ctrl.trigger.With(ctx).DeleteByID(r.NamespaceID, r.TriggerID)
|
||||
}
|
||||
|
||||
func (ctrl Trigger) makePayload(ctx context.Context, t *types.Trigger, err error) (*triggerPayload, error) {
|
||||
if err != nil || t == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &triggerPayload{
|
||||
Trigger: t,
|
||||
|
||||
CanGrant: ctrl.ac.CanGrant(ctx),
|
||||
|
||||
CanUpdateTrigger: ctrl.ac.CanUpdateTrigger(ctx, t),
|
||||
CanDeleteTrigger: ctrl.ac.CanDeleteTrigger(ctx, t),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ctrl Trigger) makeFilterPayload(ctx context.Context, nn types.TriggerSet, f types.TriggerFilter, err error) (*triggerSetPayload, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nsp := &triggerSetPayload{Filter: f, Set: make([]*triggerPayload, len(nn))}
|
||||
|
||||
for i := range nn {
|
||||
nsp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
|
||||
}
|
||||
|
||||
return nsp, nil
|
||||
}
|
||||
@@ -11,30 +11,43 @@ import (
|
||||
|
||||
type (
|
||||
ActionSet []string
|
||||
Trigger struct {
|
||||
|
||||
Trigger struct {
|
||||
ID uint64 `json:"triggerID,string" db:"id"`
|
||||
NamespaceID uint64 `json:"namespaceID,string" db:"rel_namespace"`
|
||||
ModuleID uint64 `json:"moduleID,string,omitempty" db:"rel_module"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Actions ActionSet `json:"actions" db:"actions"`
|
||||
Enabled bool `json:"enabled" db:"enabled"`
|
||||
Source string `json:"source" db:"source"`
|
||||
// Weight int `json:"weight" db:"weight"`
|
||||
|
||||
Enabled bool `json:"enabled" db:"enabled"`
|
||||
|
||||
// What is running this? browser? corredor?
|
||||
Engine string `json:"engine" db:"engine"`
|
||||
|
||||
Source string `json:"source" db:"source"`
|
||||
|
||||
// Is execution of this script critical?
|
||||
Critical bool `json:"critical" db:"critical"`
|
||||
|
||||
// No need to wait for script to return the value
|
||||
Async bool `json:"async" db:"async"`
|
||||
|
||||
// Order in which script(s) will be executed
|
||||
Weight int `json:"weight" db:"weight"`
|
||||
|
||||
// Who is running this script?
|
||||
// Leave it at 0 for the current user
|
||||
RunAs uint64 `json:"runAs", db:"rel_runner"`
|
||||
|
||||
// Are you doing something that can take more time?
|
||||
// specify timeout (in secods)
|
||||
Timeout uint32 `json:"timeout" db:"timeout"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at" json:"createdAt,omitempty"`
|
||||
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
|
||||
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
|
||||
}
|
||||
|
||||
Script struct {
|
||||
Source string `json:"source"`
|
||||
Language string `json:"language"`
|
||||
Critical bool `json:"critical"`
|
||||
Async bool `json:"async"`
|
||||
Timeout uint32 `json:"timeout"`
|
||||
RunAs uint64 `json:"runAs,string"`
|
||||
}
|
||||
|
||||
TriggerFilter struct {
|
||||
NamespaceID uint64 `json:"namespaceID,string"`
|
||||
Query string `json:"query"`
|
||||
@@ -48,19 +61,19 @@ type (
|
||||
)
|
||||
|
||||
func (t Trigger) IsCritical() bool {
|
||||
return true
|
||||
return t.Critical
|
||||
}
|
||||
|
||||
func (t Trigger) IsAsync() bool {
|
||||
return false
|
||||
return t.Async
|
||||
}
|
||||
|
||||
func (t Trigger) GetRunnerID() uint64 {
|
||||
return 0
|
||||
return t.RunAs
|
||||
}
|
||||
|
||||
func (t Trigger) GetTimeout() uint32 {
|
||||
return 0
|
||||
return t.Timeout
|
||||
}
|
||||
|
||||
func (t Trigger) GetName() string {
|
||||
|
||||
@@ -111,6 +111,225 @@
|
||||
|
||||
|
||||
|
||||
# Automation scripts
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
| ------ | -------- | ------- |
|
||||
| `GET` | `/namespace/{namespaceID}/automation/script/` | List/read automation script |
|
||||
| `POST` | `/namespace/{namespaceID}/automation/script/` | Add new automation script |
|
||||
| `GET` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Read automation script by ID |
|
||||
| `POST` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Update automation script |
|
||||
| `DELETE` | `/namespace/{namespaceID}/automation/script/{scriptID}` | Delete script |
|
||||
|
||||
## List/read automation script
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/` | HTTP/S | GET | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| query | string | GET | Search query to match against automation script | N/A | NO |
|
||||
| resource | string | GET | Limit by resource (via trigger) | N/A | NO |
|
||||
| incDeleted | bool | GET | Include deleted scripts | N/A | NO |
|
||||
| page | uint | GET | Page number (0 based) | N/A | NO |
|
||||
| perPage | uint | GET | Returned items per page (default 50) | N/A | NO |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
|
||||
## Add new automation script
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/` | HTTP/S | POST | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| name | string | POST | automation name | N/A | YES |
|
||||
| sourceRef | string | POST | Source URL | N/A | NO |
|
||||
| source | string | POST | Source code | N/A | NO |
|
||||
| runAs | uint64 | POST | Run as specific user | N/A | NO |
|
||||
| runInUA | bool | POST | Run script in user-agent (browser) | N/A | NO |
|
||||
| timeout | uint | POST | Script timeout (in milliseconds) | N/A | NO |
|
||||
| critical | bool | POST | Is it critical to run this script successfully | N/A | NO |
|
||||
| async | bool | POST | Will this script be ran asynchronously | N/A | NO |
|
||||
| enabled | bool | POST | | N/A | NO |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
|
||||
## Read automation script by ID
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/{scriptID}` | HTTP/S | GET | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| scriptID | uint64 | PATH | automation script ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
|
||||
## Update automation script
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/{scriptID}` | HTTP/S | POST | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| scriptID | uint64 | PATH | Automation script ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| name | string | POST | Script name | N/A | YES |
|
||||
| sourceRef | string | POST | Source URL | N/A | NO |
|
||||
| source | string | POST | Source code | N/A | NO |
|
||||
| runAs | uint64 | POST | Run script as specific user | N/A | NO |
|
||||
| runInUA | bool | POST | Run script in user-agent (browser) | N/A | NO |
|
||||
| timeout | uint | POST | Run script in user-agent (browser) | N/A | NO |
|
||||
| critical | bool | POST | Is it critical to run this script successfully | N/A | NO |
|
||||
| async | bool | POST | Will this script be ran asynchronously | N/A | NO |
|
||||
| enabled | bool | POST | | N/A | NO |
|
||||
|
||||
## Delete script
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/{scriptID}` | HTTP/S | DELETE | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| scriptID | uint64 | PATH | automation ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
# Automation script triggers
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
| ------ | -------- | ------- |
|
||||
| `GET` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/` | List/read automation script triggers |
|
||||
| `POST` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/` | Add new automation script trigger |
|
||||
| `GET` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | Read automation script trigger by ID |
|
||||
| `POST` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | Update automation script trigger |
|
||||
| `DELETE` | `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | Delete script |
|
||||
|
||||
## List/read automation script triggers
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/` | HTTP/S | GET | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| resource | string | GET | Only triggers of a specific resource | N/A | NO |
|
||||
| event | string | GET | Only triggers of a specific event | N/A | NO |
|
||||
| incDeleted | bool | GET | Include deleted scripts | N/A | NO |
|
||||
| page | uint | GET | Page number (0 based) | N/A | NO |
|
||||
| perPage | uint | GET | Returned items per page (default 50) | N/A | NO |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| scriptID | uint64 | PATH | Script ID | N/A | YES |
|
||||
|
||||
## Add new automation script trigger
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/` | HTTP/S | POST | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| name | string | POST | automation name | N/A | YES |
|
||||
| resource | string | POST | Resource | N/A | NO |
|
||||
| event | string | POST | Event | N/A | NO |
|
||||
| condition | string | POST | Event | N/A | NO |
|
||||
| enabled | bool | POST | | N/A | NO |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| scriptID | uint64 | PATH | Script ID | N/A | YES |
|
||||
|
||||
## Read automation script trigger by ID
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | GET | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| triggerID | uint64 | PATH | Automation script trigger ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| scriptID | uint64 | PATH | Script ID | N/A | YES |
|
||||
|
||||
## Update automation script trigger
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | POST | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| triggerID | uint64 | PATH | Automation script trigger ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| scriptID | uint64 | PATH | Script ID | N/A | YES |
|
||||
| name | string | POST | automation name | N/A | YES |
|
||||
| resource | string | POST | Resource | N/A | NO |
|
||||
| event | string | POST | Event | N/A | NO |
|
||||
| condition | string | POST | Event | N/A | NO |
|
||||
| enabled | bool | POST | | N/A | NO |
|
||||
|
||||
## Delete script
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/automation/script/{scriptID}/trigger/{triggerID}` | HTTP/S | DELETE | Client ID, Session ID |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| triggerID | uint64 | PATH | automation script trigger ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| scriptID | uint64 | PATH | Script ID | N/A | YES |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
# Charts
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
@@ -686,6 +905,7 @@ Compose records
|
||||
| `GET` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Read records by ID from module section |
|
||||
| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Update records in module section |
|
||||
| `DELETE` | `/namespace/{namespaceID}/module/{moduleID}/record/{recordID}` | Delete record row from module section |
|
||||
| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/run-script` | Trigger a specific script on record |
|
||||
| `POST` | `/namespace/{namespaceID}/module/{moduleID}/record/attachment` | Uploads attachment and validates it against record field requirements |
|
||||
|
||||
## Generates report from module records
|
||||
@@ -809,6 +1029,23 @@ Compose records
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| moduleID | uint64 | PATH | Module ID | N/A | YES |
|
||||
|
||||
## Trigger a specific script on record
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/module/{moduleID}/record/run-script` | HTTP/S | POST | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| recordID | uint64 | POST | Record ID | N/A | NO |
|
||||
| scriptID | uint64 | POST | Script ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| moduleID | uint64 | PATH | Module ID | N/A | YES |
|
||||
|
||||
## Uploads attachment and validates it against record field requirements
|
||||
|
||||
#### Method
|
||||
@@ -827,109 +1064,4 @@ Compose records
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| moduleID | uint64 | PATH | Module ID | N/A | YES |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
# Triggers
|
||||
|
||||
Compose Triggers
|
||||
|
||||
| Method | Endpoint | Purpose |
|
||||
| ------ | -------- | ------- |
|
||||
| `GET` | `/namespace/{namespaceID}/trigger/` | List available triggers |
|
||||
| `POST` | `/namespace/{namespaceID}/trigger/` | Create trigger |
|
||||
| `GET` | `/namespace/{namespaceID}/trigger/{triggerID}` | Get trigger details |
|
||||
| `POST` | `/namespace/{namespaceID}/trigger/{triggerID}` | Update trigger |
|
||||
| `Delete` | `/namespace/{namespaceID}/trigger/{triggerID}` | Delete trigger |
|
||||
|
||||
## List available triggers
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/trigger/` | HTTP/S | GET | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| moduleID | uint64 | GET | Filter triggers by module | N/A | NO |
|
||||
| query | string | GET | Search query | N/A | NO |
|
||||
| page | uint | GET | Page number (0 based) | N/A | NO |
|
||||
| perPage | uint | GET | Returned items per page (default 50) | N/A | NO |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
|
||||
## Create trigger
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/trigger/` | HTTP/S | POST | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| moduleID | uint64 | POST | Module ID | N/A | NO |
|
||||
| name | string | POST | Name | N/A | YES |
|
||||
| actions | []string | POST | Actions that trigger this trigger | N/A | NO |
|
||||
| enabled | bool | POST | Enabled | N/A | NO |
|
||||
| source | string | POST | Trigger source code | N/A | NO |
|
||||
| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
|
||||
## Get trigger details
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | GET | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| triggerID | uint64 | PATH | Trigger ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
|
||||
## Update trigger
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | POST | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| triggerID | uint64 | PATH | Trigger ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
| moduleID | uint64 | POST | Module ID | N/A | NO |
|
||||
| name | string | POST | Name | N/A | YES |
|
||||
| actions | []string | POST | Actions that trigger this trigger | N/A | NO |
|
||||
| enabled | bool | POST | Enabled | N/A | NO |
|
||||
| source | string | POST | Trigger source code | N/A | NO |
|
||||
|
||||
## Delete trigger
|
||||
|
||||
#### Method
|
||||
|
||||
| URI | Protocol | Method | Authentication |
|
||||
| --- | -------- | ------ | -------------- |
|
||||
| `/namespace/{namespaceID}/trigger/{triggerID}` | HTTP/S | Delete | |
|
||||
|
||||
#### Request parameters
|
||||
|
||||
| Parameter | Type | Method | Description | Default | Required? |
|
||||
| --------- | ---- | ------ | ----------- | ------- | --------- |
|
||||
| triggerID | uint64 | PATH | Trigger ID | N/A | YES |
|
||||
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
|
||||
|
||||
---
|
||||
38
pkg/automation/corredor.go
Normal file
38
pkg/automation/corredor.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapgrpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/grpclog"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/cli/options"
|
||||
)
|
||||
|
||||
// Corredor standard connector to Corredor service via gRPC
|
||||
func Corredor(ctx context.Context, opt options.ScriptRunnerOpt, logger *zap.Logger) (c *grpc.ClientConn, err error) {
|
||||
if !opt.Enabled {
|
||||
// Do not connect when script runner is not enabled
|
||||
return
|
||||
}
|
||||
|
||||
if opt.Log {
|
||||
// Send logs to zap
|
||||
//
|
||||
// waiting for https://github.com/uber-go/zap/pull/538
|
||||
grpclog.SetLogger(zapgrpc.NewLogger(logger.Named("grpc")))
|
||||
}
|
||||
|
||||
var dopts = []grpc.DialOption{
|
||||
// @todo insecure?
|
||||
grpc.WithInsecure(),
|
||||
}
|
||||
|
||||
if opt.MaxBackoffDelay > 0 {
|
||||
dopts = append(dopts, grpc.WithBackoffMaxDelay(opt.MaxBackoffDelay))
|
||||
}
|
||||
|
||||
return grpc.DialContext(ctx, opt.Addr, dopts...)
|
||||
}
|
||||
67
pkg/automation/script.gen.go
Normal file
67
pkg/automation/script.gen.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package automation
|
||||
|
||||
// Hello! This file is auto-generated.
|
||||
|
||||
type (
|
||||
|
||||
// ScriptSet slice of Script
|
||||
//
|
||||
// This type is auto-generated.
|
||||
ScriptSet []*Script
|
||||
)
|
||||
|
||||
// Walk iterates through every slice item and calls w(Script) err
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ScriptSet) Walk(w func(*Script) error) (err error) {
|
||||
for i := range set {
|
||||
if err = w(set[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Filter iterates through every slice item, calls f(Script) (bool, err) and return filtered slice
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ScriptSet) Filter(f func(*Script) (bool, error)) (out ScriptSet, err error) {
|
||||
var ok bool
|
||||
out = ScriptSet{}
|
||||
for i := range set {
|
||||
if ok, err = f(set[i]); err != nil {
|
||||
return
|
||||
} else if ok {
|
||||
out = append(out, set[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FindByID finds items from slice by its ID property
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ScriptSet) FindByID(ID uint64) *Script {
|
||||
for i := range set {
|
||||
if set[i].ID == ID {
|
||||
return set[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDs returns a slice of uint64s from all items in the set
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set ScriptSet) IDs() (IDs []uint64) {
|
||||
IDs = make([]uint64, len(set))
|
||||
|
||||
for i := range set {
|
||||
IDs[i] = set[i].ID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
91
pkg/automation/script.gen_test.go
Normal file
91
pkg/automation/script.gen_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/internal/test"
|
||||
)
|
||||
|
||||
// Hello! This file is auto-generated.
|
||||
|
||||
func TestScriptSetWalk(t *testing.T) {
|
||||
value := make(ScriptSet, 3)
|
||||
|
||||
// check walk with no errors
|
||||
{
|
||||
err := value.Walk(func(*Script) error {
|
||||
return nil
|
||||
})
|
||||
test.NoError(t, err, "Expected no returned error from Walk, got %+v", err)
|
||||
}
|
||||
|
||||
// check walk with error
|
||||
test.Error(t, value.Walk(func(*Script) error { return errors.New("Walk error") }), "Expected error from walk, got nil")
|
||||
}
|
||||
|
||||
func TestScriptSetFilter(t *testing.T) {
|
||||
value := make(ScriptSet, 3)
|
||||
|
||||
// filter nothing
|
||||
{
|
||||
set, err := value.Filter(func(*Script) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
test.NoError(t, err, "Didn't expect error when filtering set: %+v", err)
|
||||
test.Assert(t, len(set) == len(value), "Expected equal length filter: %d != %d", len(value), len(set))
|
||||
}
|
||||
|
||||
// filter one item
|
||||
{
|
||||
found := false
|
||||
set, err := value.Filter(func(*Script) (bool, error) {
|
||||
if !found {
|
||||
found = true
|
||||
return found, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
test.NoError(t, err, "Didn't expect error when filtering set: %+v", err)
|
||||
test.Assert(t, len(set) == 1, "Expected single item, got %d", len(value))
|
||||
}
|
||||
|
||||
// filter error
|
||||
{
|
||||
_, err := value.Filter(func(*Script) (bool, error) {
|
||||
return false, errors.New("Filter error")
|
||||
})
|
||||
test.Error(t, err, "Expected error, got %#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptSetIDs(t *testing.T) {
|
||||
value := make(ScriptSet, 3)
|
||||
// construct objects
|
||||
value[0] = new(Script)
|
||||
value[1] = new(Script)
|
||||
value[2] = new(Script)
|
||||
// set ids
|
||||
value[0].ID = 1
|
||||
value[1].ID = 2
|
||||
value[2].ID = 3
|
||||
|
||||
// Find existing
|
||||
{
|
||||
val := value.FindByID(2)
|
||||
test.Assert(t, val.ID == 2, "Expected ID 2, got %d", val.ID)
|
||||
}
|
||||
|
||||
// Find non-existing
|
||||
{
|
||||
val := value.FindByID(4)
|
||||
test.Assert(t, val == nil, "Expected no value, got %#v", val)
|
||||
}
|
||||
|
||||
// List IDs from set
|
||||
{
|
||||
val := value.IDs()
|
||||
test.Assert(t, len(val) == len(value), "Expected ID count mismatch, %d != %d", len(val), len(value))
|
||||
}
|
||||
}
|
||||
122
pkg/automation/script.go
Normal file
122
pkg/automation/script.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
)
|
||||
|
||||
type (
|
||||
Script struct {
|
||||
ID uint64 `json:"scriptID,string" db:"id"`
|
||||
|
||||
Name string `json:"name" db:"name"`
|
||||
|
||||
// (URL) Where did we get the source from?
|
||||
SourceRef string `json:"sourceRef" db:"source_ref"`
|
||||
|
||||
// Code
|
||||
Source string `json:"source" db:"source"`
|
||||
|
||||
// No need to wait for script to return the value
|
||||
Async bool `json:"async" db:"async"`
|
||||
|
||||
// Who is running this script?
|
||||
// Leave it at 0 for the current user (security invoker) or
|
||||
// set ID of specific user (security definer)
|
||||
RunAs uint64 `json:"runAs,string" db:"rel_runner"`
|
||||
|
||||
// Where can we run this script? user-agent? corredor service?
|
||||
RunInUA bool `json:"runInUA" db:"run_in_ua"`
|
||||
|
||||
// Are you doing something that can take more time?
|
||||
// specify timeout (in milliseconds)
|
||||
Timeout uint `json:"timeout" db:"timeout"`
|
||||
|
||||
// Is it critical to run this script successfully?
|
||||
Critical bool `json:"critical" db:"critical"`
|
||||
|
||||
Enabled bool `json:"enabled" db:"enabled"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at" json:"createdAt"`
|
||||
CreatedBy uint64 `db:"created_by" json:"createdBy,string" `
|
||||
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
|
||||
UpdatedBy uint64 `db:"updated_by" json:"updatedBy,string,omitempty" `
|
||||
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
|
||||
DeletedBy uint64 `db:"deleted_by" json:"deletedBy,string,omitempty" `
|
||||
|
||||
triggers TriggerSet
|
||||
}
|
||||
|
||||
ScriptFilter struct {
|
||||
Query string
|
||||
Resource string
|
||||
IncDeleted bool `json:"incDeleted"`
|
||||
|
||||
// Standard paging fields & helpers
|
||||
rh.PageFilter
|
||||
}
|
||||
)
|
||||
|
||||
// IsValid - enabled, deleted?
|
||||
func (s *Script) IsValid() bool {
|
||||
return s != nil && s.Enabled && s.DeletedAt == nil
|
||||
}
|
||||
|
||||
// Verify - sanity check of script's properties
|
||||
func (s Script) Verify() error {
|
||||
if s.RunAsDefined() && s.RunInUA {
|
||||
return errors.New("user-agent engine does not support run-as-defined scripts")
|
||||
}
|
||||
|
||||
if s.Critical && s.RunInUA {
|
||||
return errors.New("user-agent engine scripts can not be critical")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsCompatible verifies if trigger can be added to a script
|
||||
func (s *Script) CheckCompatibility(t *Trigger) error {
|
||||
if s == nil {
|
||||
return errors.New("not compatible with nil script")
|
||||
}
|
||||
if s == nil || t == nil {
|
||||
return errors.New("not compatible with nil trigger")
|
||||
}
|
||||
|
||||
if t.IsDeferred() {
|
||||
if s.RunInUA {
|
||||
return errors.New("deferred triggers are not compatible with user-agent scripts")
|
||||
}
|
||||
|
||||
if s.RunAsInvoker() {
|
||||
return errors.New("deferred triggers are not compatible with run-as-invoker scripts")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterByEvent
|
||||
//
|
||||
// we will use the Trigger struct as a holder for conditions
|
||||
func (set ScriptSet) FilterByEvent(event, resource string, cc ...TriggerConditionChecker) (out ScriptSet) {
|
||||
out, _ = set.Filter(func(s *Script) (bool, error) {
|
||||
return s.triggers.HasMatch(Trigger{Event: event, Resource: resource}, cc...), nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RunAsDefined - script should be run with pre-defined privileges (user)
|
||||
func (s Script) RunAsDefined() bool {
|
||||
return s.RunAs > 0
|
||||
}
|
||||
|
||||
// RunAsInvoker - this script should run with invoker's privileges (user)
|
||||
func (s Script) RunAsInvoker() bool {
|
||||
return s.RunAs == 0
|
||||
}
|
||||
140
pkg/automation/script_repository.go
Normal file
140
pkg/automation/script_repository.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory"
|
||||
"gopkg.in/Masterminds/squirrel.v1"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
)
|
||||
|
||||
type (
|
||||
// repository servs as a db storage layer for permission rules
|
||||
scriptRepository struct {
|
||||
dbh *factory.DB
|
||||
|
||||
// sql table reference
|
||||
dbTablePrefix string
|
||||
}
|
||||
)
|
||||
|
||||
func ScriptRepository(db *factory.DB, dbTablePrefix string) *scriptRepository {
|
||||
return &scriptRepository{
|
||||
dbTablePrefix: dbTablePrefix,
|
||||
dbh: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *scriptRepository) With(ctx context.Context) *scriptRepository {
|
||||
return &scriptRepository{
|
||||
dbTablePrefix: r.dbTablePrefix,
|
||||
dbh: r.db().With(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *scriptRepository) db() *factory.DB {
|
||||
return r.dbh
|
||||
}
|
||||
|
||||
func (r scriptRepository) table() string {
|
||||
return r.dbTablePrefix + "_automation_script"
|
||||
}
|
||||
|
||||
func (r scriptRepository) columns() []string {
|
||||
return []string{
|
||||
"id",
|
||||
"name",
|
||||
"source_ref",
|
||||
"source",
|
||||
"async",
|
||||
"rel_runner",
|
||||
"run_in_ua",
|
||||
"timeout",
|
||||
"critical",
|
||||
"enabled",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"deleted_at",
|
||||
"deleted_by",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *scriptRepository) query() squirrel.SelectBuilder {
|
||||
return squirrel.
|
||||
Select(r.columns()...).
|
||||
From(r.table())
|
||||
}
|
||||
|
||||
// FindByID finds specific script
|
||||
func (r *scriptRepository) FindByID(ctx context.Context, scriptID uint64) (*Script, error) {
|
||||
var (
|
||||
rval = &Script{}
|
||||
|
||||
query = r.query().
|
||||
Columns(r.columns()...).
|
||||
Where("id = ?", scriptID)
|
||||
)
|
||||
|
||||
return rval, rh.IsFound(rh.FetchOne(r.db(), query, rval), rval.ID > 0, errors.New("script not found"))
|
||||
}
|
||||
|
||||
// Find - finds scripts using given filter
|
||||
func (r *scriptRepository) Find(ctx context.Context, filter ScriptFilter) (set ScriptSet, f ScriptFilter, err error) {
|
||||
f = filter
|
||||
|
||||
query := r.query()
|
||||
|
||||
if !filter.IncDeleted {
|
||||
query = query.Where("deleted_at IS NULL")
|
||||
}
|
||||
|
||||
if f.Query != "" {
|
||||
q := "%" + f.Query + "%"
|
||||
query = query.Where("name like ?", q)
|
||||
}
|
||||
|
||||
if f.Resource != "" {
|
||||
// Making partial trigger repo struct on the fly to help us calculate the name of the triggers table
|
||||
ttable := (triggerRepository{dbTablePrefix: r.dbTablePrefix}).table()
|
||||
query = query.Where(
|
||||
fmt.Sprintf("id IN (SELECT rel_script FROM `%s` WHERE resource = ?", ttable),
|
||||
f.Resource,
|
||||
)
|
||||
}
|
||||
|
||||
if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
query = query.OrderBy("id ASC")
|
||||
|
||||
return set, f, rh.FetchPaged(r.db(), query, f.Page, f.PerPage, &set)
|
||||
}
|
||||
|
||||
// FindAllRunnable - loads and returns all runnable scripts
|
||||
func (r *scriptRepository) FindAllRunnable() (ScriptSet, error) {
|
||||
rr := make([]*Script, 0)
|
||||
|
||||
return rr, errors.Wrap(rh.FetchAll(
|
||||
r.db(),
|
||||
r.query().Where("enabled AND deleted_at IS NULL"),
|
||||
&rr,
|
||||
), "could not load runnable scripts")
|
||||
}
|
||||
|
||||
func (r *scriptRepository) Create(s *Script) (err error) {
|
||||
return r.dbh.Transaction(func() error {
|
||||
return r.dbh.Insert(r.table(), s)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *scriptRepository) Update(s *Script) (err error) {
|
||||
return r.dbh.Transaction(func() error {
|
||||
return r.dbh.Update(r.table(), s, "id")
|
||||
})
|
||||
}
|
||||
48
pkg/automation/script_test.go
Normal file
48
pkg/automation/script_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScript_CheckCompatibility(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
s *Script
|
||||
t *Trigger
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "both nil",
|
||||
s: nil,
|
||||
t: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "both vanilla",
|
||||
s: &Script{},
|
||||
t: &Trigger{},
|
||||
wantErr: false,
|
||||
},
|
||||
{name: "deferred trigger with UA script",
|
||||
s: &Script{RunInUA: true},
|
||||
t: &Trigger{Event: EVENT_TYPE_INTERVAL},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "deferred trigger with invoker security",
|
||||
s: &Script{RunAs: 0},
|
||||
t: &Trigger{Event: EVENT_TYPE_INTERVAL},
|
||||
wantErr: true,
|
||||
},
|
||||
{name: "deferred trigger with invoker security",
|
||||
s: &Script{RunAs: 1, RunInUA: false},
|
||||
t: &Trigger{Event: EVENT_TYPE_INTERVAL},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.s.CheckCompatibility(tt.t); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckCompatibility() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
322
pkg/automation/service.go
Normal file
322
pkg/automation/service.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/internal/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/sentry"
|
||||
)
|
||||
|
||||
type (
|
||||
service struct {
|
||||
l sync.Mutex
|
||||
logger *zap.Logger
|
||||
|
||||
c AutomationServiceConfig
|
||||
|
||||
// service will flush values on TRUE or just reload on FALSE
|
||||
f chan bool
|
||||
|
||||
// internal list of runnable scripts (and their accompanying triggers)
|
||||
runnables ScriptSet
|
||||
|
||||
srepo *scriptRepository
|
||||
trepo *triggerRepository
|
||||
}
|
||||
|
||||
ScriptsProvider interface {
|
||||
FilterByEvent(event, resource string, cc ...TriggerConditionChecker) ScriptSet
|
||||
}
|
||||
|
||||
WatcherService interface {
|
||||
Watch(ctx context.Context)
|
||||
}
|
||||
|
||||
AutomationServiceConfig struct {
|
||||
DB *factory.DB
|
||||
DbTablePrefix string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
watchInterval = time.Hour
|
||||
)
|
||||
|
||||
// Service initializes service{} struct
|
||||
//
|
||||
// service{} struct handles scripts & triggers. It acts as a caching layer and
|
||||
// proxy to repository where it verifies and enriches payloads
|
||||
//
|
||||
func Service(ctx context.Context, logger *zap.Logger, c AutomationServiceConfig) (svc *service) {
|
||||
svc = &service{
|
||||
logger: logger.Named("automation"),
|
||||
|
||||
c: c,
|
||||
|
||||
f: make(chan bool),
|
||||
}
|
||||
|
||||
if c.DB != nil {
|
||||
svc.srepo = ScriptRepository(c.DB, c.DbTablePrefix)
|
||||
svc.trepo = TriggerRepository(c.DB, c.DbTablePrefix)
|
||||
}
|
||||
|
||||
svc.Reload(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// Watch() Watches for changes
|
||||
func (svc service) Watch(ctx context.Context) {
|
||||
go func() {
|
||||
defer sentry.Recover()
|
||||
|
||||
var ticker = time.NewTicker(watchInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
svc.Reload(ctx)
|
||||
case <-svc.f:
|
||||
svc.Reload(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
svc.logger.Debug("watcher initialized")
|
||||
}
|
||||
|
||||
func (svc *service) Reload(ctx context.Context) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
if svc.c.DB == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
ss ScriptSet
|
||||
tt TriggerSet
|
||||
)
|
||||
|
||||
ss, err = svc.srepo.With(ctx).FindAllRunnable()
|
||||
svc.logger.Info("scripts loaded", zap.Error(err), zap.Int("count", len(tt)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Only interested in valid scritps
|
||||
ss, _ = ss.Filter(func(s *Script) (b bool, e error) {
|
||||
return s.IsValid(), nil
|
||||
})
|
||||
|
||||
tt, err = svc.trepo.With(ctx).FindAllRunnable()
|
||||
svc.logger.Info("triggers loaded", zap.Error(err), zap.Int("count", len(tt)))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_ = tt.Walk(func(t *Trigger) error {
|
||||
s := ss.FindByID(t.ScriptID)
|
||||
if t.IsValid() && s.CheckCompatibility(t) != nil {
|
||||
// Add only compatible triggers
|
||||
s.triggers = append(s.triggers, t)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// FindRunnableScripts scans internal list of runnable scripts and filters them by (trigger's) event and origin
|
||||
func (svc service) FindRunnableScripts(event, origin string, cc ...TriggerConditionChecker) ScriptSet {
|
||||
return svc.runnables.FilterByEvent(event, origin, cc...)
|
||||
}
|
||||
|
||||
// updateRunnableScripts - updates script set (internal runnable scripts list)
|
||||
func (svc service) updateRunnableScripts(n *Script) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
ss := svc.runnables
|
||||
|
||||
for i := range svc.runnables {
|
||||
if ss[i].ID != n.ID {
|
||||
continue
|
||||
}
|
||||
|
||||
if n.IsValid() {
|
||||
// Valid, replace
|
||||
ss[i] = n
|
||||
}
|
||||
|
||||
// Invalid, remove
|
||||
ss = append(ss[:i], ss[i+1:]...)
|
||||
return
|
||||
}
|
||||
|
||||
if n.IsValid() {
|
||||
ss = append(ss, n)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// updateScriptsWithTrigger - finds the referenced script and updates its trigger set
|
||||
func (svc service) updateScriptWithTrigger(n *Trigger) {
|
||||
svc.l.Lock()
|
||||
defer svc.l.Unlock()
|
||||
|
||||
ss := svc.runnables
|
||||
|
||||
for i := range ss {
|
||||
if ss[i].ID != n.ScriptID {
|
||||
continue
|
||||
}
|
||||
|
||||
tt := ss[i].triggers
|
||||
|
||||
for i = range tt {
|
||||
if n.IsValid() {
|
||||
// Valid, replace
|
||||
tt[i] = n
|
||||
}
|
||||
|
||||
// Invalid, remove
|
||||
tt = append(tt[:i], tt[i+i:]...)
|
||||
return
|
||||
}
|
||||
|
||||
if n.IsValid() {
|
||||
tt = append(tt, n)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (svc service) FindScriptByID(ctx context.Context, scriptID uint64) (*Script, error) {
|
||||
return svc.srepo.FindByID(ctx, scriptID)
|
||||
}
|
||||
|
||||
func (svc service) FindScripts(ctx context.Context, f ScriptFilter) (ScriptSet, ScriptFilter, error) {
|
||||
return svc.srepo.Find(ctx, f)
|
||||
}
|
||||
|
||||
// CreateScript - modifies script's props, pushes to repo & updates scripts cache
|
||||
func (svc service) CreateScript(ctx context.Context, s *Script) (err error) {
|
||||
s.ID = factory.Sonyflake.NextID()
|
||||
s.CreatedAt = time.Now()
|
||||
s.CreatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
if err = svc.srepo.Create(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.updateRunnableScripts(s)
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateScript - modifies script's props, pushes to repo & updates scripts cache
|
||||
func (svc service) UpdateScript(ctx context.Context, s *Script) (err error) {
|
||||
s.UpdatedAt = &time.Time{}
|
||||
*s.UpdatedAt = time.Now()
|
||||
s.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// Ensure sanity
|
||||
s.UpdatedAt, s.UpdatedBy = nil, 0
|
||||
s.DeletedAt, s.DeletedBy = nil, 0
|
||||
|
||||
if err = svc.srepo.Update(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.updateRunnableScripts(s)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteScript - modifies script's props, pushes to repo & updates scripts cache
|
||||
func (svc service) DeleteScript(ctx context.Context, s *Script) (err error) {
|
||||
s.DeletedAt = &time.Time{}
|
||||
*s.DeletedAt = time.Now()
|
||||
s.DeletedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// We're doing soft delete in the repo
|
||||
if err = svc.srepo.Update(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.updateRunnableScripts(s)
|
||||
return
|
||||
}
|
||||
|
||||
func (svc service) FindTriggerByID(ctx context.Context, scriptID uint64) (*Trigger, error) {
|
||||
return svc.trepo.FindByID(ctx, scriptID)
|
||||
}
|
||||
|
||||
func (svc service) FindTriggers(ctx context.Context, f TriggerFilter) (TriggerSet, TriggerFilter, error) {
|
||||
return svc.trepo.Find(ctx, f)
|
||||
}
|
||||
|
||||
// CreateScript - modifies script's props, pushes to repo & updates scripts cache
|
||||
func (svc service) CreateTrigger(ctx context.Context, s *Script, t *Trigger) (err error) {
|
||||
if err = s.CheckCompatibility(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.ID = factory.Sonyflake.NextID()
|
||||
t.CreatedAt = time.Now()
|
||||
t.CreatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
t.ScriptID = s.ID
|
||||
|
||||
if err = svc.trepo.Create(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.updateScriptWithTrigger(t)
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateTrigger - modifies script's props, pushes to repo & updates scripts cache
|
||||
func (svc service) UpdateTrigger(ctx context.Context, s *Script, t *Trigger) (err error) {
|
||||
if s.ID != t.ScriptID {
|
||||
return errors.New("invalid script-trigger reference")
|
||||
}
|
||||
|
||||
if err = s.CheckCompatibility(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.UpdatedAt = &time.Time{}
|
||||
*t.UpdatedAt = time.Now()
|
||||
t.UpdatedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
if err = svc.trepo.Update(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.updateScriptWithTrigger(t)
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteTrigger - modifies script's props, pushes to repo & updates scripts cache
|
||||
func (svc service) DeleteTrigger(ctx context.Context, t *Trigger) (err error) {
|
||||
t.DeletedAt = &time.Time{}
|
||||
*t.DeletedAt = time.Now()
|
||||
t.DeletedBy = auth.GetIdentityFromContext(ctx).Identity()
|
||||
|
||||
// We're doing soft delete in the repo
|
||||
if err = svc.trepo.Update(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
svc.updateScriptWithTrigger(t)
|
||||
return
|
||||
}
|
||||
67
pkg/automation/trigger.gen.go
Normal file
67
pkg/automation/trigger.gen.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package automation
|
||||
|
||||
// Hello! This file is auto-generated.
|
||||
|
||||
type (
|
||||
|
||||
// TriggerSet slice of Trigger
|
||||
//
|
||||
// This type is auto-generated.
|
||||
TriggerSet []*Trigger
|
||||
)
|
||||
|
||||
// Walk iterates through every slice item and calls w(Trigger) err
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set TriggerSet) Walk(w func(*Trigger) error) (err error) {
|
||||
for i := range set {
|
||||
if err = w(set[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Filter iterates through every slice item, calls f(Trigger) (bool, err) and return filtered slice
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set TriggerSet) Filter(f func(*Trigger) (bool, error)) (out TriggerSet, err error) {
|
||||
var ok bool
|
||||
out = TriggerSet{}
|
||||
for i := range set {
|
||||
if ok, err = f(set[i]); err != nil {
|
||||
return
|
||||
} else if ok {
|
||||
out = append(out, set[i])
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// FindByID finds items from slice by its ID property
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set TriggerSet) FindByID(ID uint64) *Trigger {
|
||||
for i := range set {
|
||||
if set[i].ID == ID {
|
||||
return set[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IDs returns a slice of uint64s from all items in the set
|
||||
//
|
||||
// This function is auto-generated.
|
||||
func (set TriggerSet) IDs() (IDs []uint64) {
|
||||
IDs = make([]uint64, len(set))
|
||||
|
||||
for i := range set {
|
||||
IDs[i] = set[i].ID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
91
pkg/automation/trigger.gen_test.go
Normal file
91
pkg/automation/trigger.gen_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/internal/test"
|
||||
)
|
||||
|
||||
// Hello! This file is auto-generated.
|
||||
|
||||
func TestTriggerSetWalk(t *testing.T) {
|
||||
value := make(TriggerSet, 3)
|
||||
|
||||
// check walk with no errors
|
||||
{
|
||||
err := value.Walk(func(*Trigger) error {
|
||||
return nil
|
||||
})
|
||||
test.NoError(t, err, "Expected no returned error from Walk, got %+v", err)
|
||||
}
|
||||
|
||||
// check walk with error
|
||||
test.Error(t, value.Walk(func(*Trigger) error { return errors.New("Walk error") }), "Expected error from walk, got nil")
|
||||
}
|
||||
|
||||
func TestTriggerSetFilter(t *testing.T) {
|
||||
value := make(TriggerSet, 3)
|
||||
|
||||
// filter nothing
|
||||
{
|
||||
set, err := value.Filter(func(*Trigger) (bool, error) {
|
||||
return true, nil
|
||||
})
|
||||
test.NoError(t, err, "Didn't expect error when filtering set: %+v", err)
|
||||
test.Assert(t, len(set) == len(value), "Expected equal length filter: %d != %d", len(value), len(set))
|
||||
}
|
||||
|
||||
// filter one item
|
||||
{
|
||||
found := false
|
||||
set, err := value.Filter(func(*Trigger) (bool, error) {
|
||||
if !found {
|
||||
found = true
|
||||
return found, nil
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
test.NoError(t, err, "Didn't expect error when filtering set: %+v", err)
|
||||
test.Assert(t, len(set) == 1, "Expected single item, got %d", len(value))
|
||||
}
|
||||
|
||||
// filter error
|
||||
{
|
||||
_, err := value.Filter(func(*Trigger) (bool, error) {
|
||||
return false, errors.New("Filter error")
|
||||
})
|
||||
test.Error(t, err, "Expected error, got %#v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriggerSetIDs(t *testing.T) {
|
||||
value := make(TriggerSet, 3)
|
||||
// construct objects
|
||||
value[0] = new(Trigger)
|
||||
value[1] = new(Trigger)
|
||||
value[2] = new(Trigger)
|
||||
// set ids
|
||||
value[0].ID = 1
|
||||
value[1].ID = 2
|
||||
value[2].ID = 3
|
||||
|
||||
// Find existing
|
||||
{
|
||||
val := value.FindByID(2)
|
||||
test.Assert(t, val.ID == 2, "Expected ID 2, got %d", val.ID)
|
||||
}
|
||||
|
||||
// Find non-existing
|
||||
{
|
||||
val := value.FindByID(4)
|
||||
test.Assert(t, val == nil, "Expected no value, got %#v", val)
|
||||
}
|
||||
|
||||
// List IDs from set
|
||||
{
|
||||
val := value.IDs()
|
||||
test.Assert(t, len(val) == len(value), "Expected ID count mismatch, %d != %d", len(val), len(value))
|
||||
}
|
||||
}
|
||||
111
pkg/automation/trigger.go
Normal file
111
pkg/automation/trigger.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
)
|
||||
|
||||
type (
|
||||
Event string
|
||||
|
||||
Trigger struct {
|
||||
ID uint64 `json:"triggerID,string" db:"id"`
|
||||
|
||||
// Resource that triggered the event
|
||||
// - "compose:" (unspec, general)
|
||||
// - "compose:record"
|
||||
// - "compose:namespace"
|
||||
Resource string `json:"resource" db:"resource"`
|
||||
|
||||
// Event name, arbitrary string
|
||||
// - "before"
|
||||
// - "after"
|
||||
// - "on"
|
||||
// - "at"
|
||||
Event string `json:"event" db:"event"`
|
||||
|
||||
// Arbitrary data for trigger condition
|
||||
//
|
||||
// It is caller's responsibility to encode, decode and verify conditions
|
||||
Condition string `json:"condition" db:"condition"`
|
||||
|
||||
ScriptID uint64 `json:"scriptID,string" db:"rel_script"`
|
||||
|
||||
// Is trigger enabled or disabled?
|
||||
Enabled bool `json:"enabled" db:"enabled"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at" json:"createdAt"`
|
||||
CreatedBy uint64 `db:"created_by" json:"createdBy,string" `
|
||||
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
|
||||
UpdatedBy uint64 `db:"updated_by" json:"updatedBy,string,omitempty" `
|
||||
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
|
||||
DeletedBy uint64 `db:"deleted_by" json:"deletedBy,string,omitempty" `
|
||||
}
|
||||
|
||||
TriggerFilter struct {
|
||||
Resource string
|
||||
Event string
|
||||
ScriptID uint64
|
||||
|
||||
IncDeleted bool
|
||||
|
||||
// Standard paging fields & helpers
|
||||
rh.PageFilter
|
||||
}
|
||||
|
||||
TriggerConditionChecker func(string) bool
|
||||
)
|
||||
|
||||
const (
|
||||
EVENT_TYPE_INTERVAL = "interval"
|
||||
EVENT_TYPE_TIMESTAMP = "at"
|
||||
)
|
||||
|
||||
// IsValid checks if trigger is enabled and not deleted
|
||||
func (t *Trigger) IsValid() bool {
|
||||
return t != nil && t.Enabled && t.DeletedAt == nil
|
||||
}
|
||||
|
||||
// IsDeferred - not called as consequence of a user's action (create, delete, update)
|
||||
func (t Trigger) IsDeferred() bool {
|
||||
return t.Event == EVENT_TYPE_INTERVAL || t.Event == EVENT_TYPE_TIMESTAMP
|
||||
}
|
||||
|
||||
// HasMatch checks if any og the triggers in a set matches the given parameters
|
||||
func (set TriggerSet) HasMatch(m Trigger, ff ...TriggerConditionChecker) bool {
|
||||
withTriggers:
|
||||
for _, t := range set {
|
||||
if !t.IsValid() {
|
||||
// only valid can match
|
||||
continue withTriggers
|
||||
}
|
||||
|
||||
if m.ID > 0 && m.ID != t.ID {
|
||||
// Are we looking for a particular trigger?
|
||||
continue withTriggers
|
||||
}
|
||||
|
||||
if m.Resource != t.Resource {
|
||||
// event should match
|
||||
continue withTriggers
|
||||
}
|
||||
|
||||
if m.Event != t.Event {
|
||||
// event should match
|
||||
continue withTriggers
|
||||
}
|
||||
|
||||
// Go through all condition checking functions
|
||||
// All of them should return true for trigger to match
|
||||
for _, fn := range ff {
|
||||
if !fn(t.Condition) {
|
||||
continue withTriggers
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
149
pkg/automation/trigger_repository.go
Normal file
149
pkg/automation/trigger_repository.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/titpetric/factory"
|
||||
"gopkg.in/Masterminds/squirrel.v1"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/rh"
|
||||
)
|
||||
|
||||
type (
|
||||
// repository servs as a db storage layer for permission rules
|
||||
triggerRepository struct {
|
||||
dbh *factory.DB
|
||||
|
||||
// sql table reference
|
||||
dbTablePrefix string
|
||||
}
|
||||
)
|
||||
|
||||
func TriggerRepository(db *factory.DB, dbTablePrefix string) *triggerRepository {
|
||||
return &triggerRepository{
|
||||
dbTablePrefix: dbTablePrefix,
|
||||
dbh: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *triggerRepository) With(ctx context.Context) *triggerRepository {
|
||||
return &triggerRepository{
|
||||
dbTablePrefix: r.dbTablePrefix,
|
||||
dbh: r.db().With(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *triggerRepository) db() *factory.DB {
|
||||
return r.dbh
|
||||
}
|
||||
|
||||
func (r triggerRepository) table() string {
|
||||
return r.dbTablePrefix + "_automation_trigger"
|
||||
}
|
||||
|
||||
func (r triggerRepository) columns() []string {
|
||||
return []string{
|
||||
"id",
|
||||
"event",
|
||||
"resource",
|
||||
"`condition`",
|
||||
"rel_script",
|
||||
"enabled",
|
||||
"created_at",
|
||||
"created_by",
|
||||
"updated_at",
|
||||
"updated_by",
|
||||
"deleted_at",
|
||||
"deleted_by",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *triggerRepository) query() squirrel.SelectBuilder {
|
||||
return squirrel.
|
||||
Select(r.columns()...).
|
||||
From(r.table())
|
||||
}
|
||||
|
||||
// FindByID finds specific trigger
|
||||
func (r *triggerRepository) FindByID(ctx context.Context, triggerID uint64) (*Trigger, error) {
|
||||
var (
|
||||
rval = &Trigger{}
|
||||
|
||||
query = r.query().
|
||||
Columns(r.columns()...).
|
||||
Where("id = ?", triggerID)
|
||||
)
|
||||
|
||||
return rval, rh.IsFound(rh.FetchOne(r.db(), query, rval), rval.ID > 0, errors.New("trigger not found"))
|
||||
}
|
||||
|
||||
// Find - finds triggers using given filter
|
||||
func (r *triggerRepository) Find(ctx context.Context, filter TriggerFilter) (set TriggerSet, f TriggerFilter, err error) {
|
||||
f = filter
|
||||
|
||||
query := r.query()
|
||||
|
||||
if f.ScriptID > 0 {
|
||||
query = query.Where("rel_script = ?", f.ScriptID)
|
||||
}
|
||||
|
||||
if f.Event != "" {
|
||||
query = query.Where("resource = ?", f.Event)
|
||||
}
|
||||
|
||||
if f.Resource != "" {
|
||||
query = query.Where("resource = ?", f.Resource)
|
||||
}
|
||||
|
||||
if !filter.IncDeleted {
|
||||
query = query.Where("deleted_at IS NULL")
|
||||
}
|
||||
|
||||
if f.Count, err = rh.Count(r.db(), query); err != nil || f.Count == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
query = query.OrderBy("id ASC")
|
||||
|
||||
return set, f, rh.FetchPaged(r.db(), query, f.Page, f.PerPage, &set)
|
||||
}
|
||||
|
||||
// FindAllRunnable - loads and returns all runnable triggers
|
||||
func (r *triggerRepository) FindAllRunnable() (TriggerSet, error) {
|
||||
rr := make([]*Trigger, 0)
|
||||
|
||||
return rr, errors.Wrap(rh.FetchAll(
|
||||
r.db(),
|
||||
r.query().Where("enabled AND deleted_at IS NULL"),
|
||||
&rr,
|
||||
), "could not load runnable triggers")
|
||||
}
|
||||
|
||||
func (r *triggerRepository) Create(s *Trigger) (err error) {
|
||||
return r.dbh.Transaction(func() error {
|
||||
// Generate ID
|
||||
s.ID = factory.Sonyflake.NextID()
|
||||
|
||||
if s.CreatedAt.IsZero() {
|
||||
// Make sure time of creation is set
|
||||
s.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
// Ensure sanity
|
||||
s.UpdatedAt, s.UpdatedBy = nil, 0
|
||||
s.DeletedAt, s.DeletedBy = nil, 0
|
||||
|
||||
return r.dbh.Insert(r.table(), s)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *triggerRepository) Update(s *Trigger) (err error) {
|
||||
return r.dbh.Transaction(func() error {
|
||||
s.UpdatedAt = &time.Time{}
|
||||
*s.UpdatedAt = time.Now()
|
||||
|
||||
return r.dbh.Update(r.table(), s, "id")
|
||||
})
|
||||
}
|
||||
46
pkg/automation/trigger_test.go
Normal file
46
pkg/automation/trigger_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package automation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTriggerSet_HasMatch(t *testing.T) {
|
||||
type args struct {
|
||||
m Trigger
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
set TriggerSet
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "simple match",
|
||||
set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: true}, nil, &Trigger{}},
|
||||
args: args{m: Trigger{Event: "e"}},
|
||||
want: true,
|
||||
}, {
|
||||
name: "simple miss",
|
||||
set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: true}, nil, &Trigger{}},
|
||||
args: args{m: Trigger{}},
|
||||
want: false,
|
||||
}, {
|
||||
name: "specific",
|
||||
set: TriggerSet{nil, &Trigger{}, &Trigger{ID: 2, Enabled: true}, nil, &Trigger{}},
|
||||
args: args{m: Trigger{ID: 2}},
|
||||
want: true,
|
||||
}, {
|
||||
name: "invalid",
|
||||
set: TriggerSet{nil, &Trigger{}, &Trigger{Event: "e", Enabled: false}, nil, &Trigger{}},
|
||||
args: args{m: Trigger{Event: "e"}},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.set.HasMatch(tt.args.m); got != tt.want {
|
||||
t.Errorf("HasMatch() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -32,3 +32,7 @@ func (pf *PageFilter) NormalizePerPage(min, max, def uint) {
|
||||
func (pf *PageFilter) NormalizePerPageWithDefaults() {
|
||||
pf.PerPage = NormalizePerPage(pf.PerPage, PER_PAGE_MIN, PER_PAGE_MAX, PER_PAGE_DEFAULT)
|
||||
}
|
||||
|
||||
func (pf *PageFilter) NormalizePerPageNoMax() {
|
||||
pf.PerPage = NormalizePerPage(pf.PerPage, PER_PAGE_MIN, 0, PER_PAGE_DEFAULT)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user