3
0

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:
Denis Arh
2019-08-04 11:20:25 +02:00
parent 82e2c58d0a
commit 38fe06998c
42 changed files with 3875 additions and 1174 deletions

View File

@@ -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"
}
]
}
}
]
}
]

View 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"
}
]
}
}
]
}

View File

@@ -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"
}
]

View File

@@ -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",

View File

@@ -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

View 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;

View 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))
}

View 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)
}

View 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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}
}

View File

@@ -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,
}
}

View 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
}

View 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
}

View File

@@ -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)
})
}

View 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)
})
}

View File

@@ -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)
})
}

View File

@@ -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

View 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()

View File

@@ -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()

View File

@@ -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"`

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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 |
---

View 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...)
}

View 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
}

View 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
View 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
}

View 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")
})
}

View 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
View 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
}

View 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
}

View 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
View 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
}

View 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")
})
}

View 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)
}
})
}
}

View File

@@ -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)
}