diff --git a/README.md b/README.md
index cd8ba693..30ad78b1 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ The payments API keeps a reference to the running workflow via its task ID. Oper
### Running with docker-compose
-Download our latest install script, setup your environment and launch your own local instance of µTask.
+Download our latest install script, setup your environment and launch your own local instance of µTask.
```bash
mkdir utask && cd utask
@@ -106,7 +106,7 @@ You'll deploy your version of µTask by building a docker image based on the off
### Architecture
-µTask is designed to run a task scheduler and perform the task workloads within a single runtime: work is not delegated to external agents. Multiple instances of the application will coordinate around a single postgres database: each will be able to determine independently which tasks are available. When an instance of µTask decides to execute a task, it will take hold of that task to avoid collisions, then release it at the end of an execution cycle.
+µTask is designed to run a task scheduler and perform the task workloads within a single runtime: work is not delegated to external agents. Multiple instances of the application will coordinate around a single postgres database: each will be able to determine independently which tasks are available. When an instance of µTask decides to execute a task, it will take hold of that task to avoid collisions, then release it at the end of an execution cycle.
A task will keep running as long as its steps are successfully executed. If a task's execution is interrupted before completion, it will become available to be re-collected by one of the active instances of µTask. That means that execution might start in one instance and resume on a different one.
@@ -125,7 +125,7 @@ A task will keep running as long as its steps are successfully executed. If a ta
## Configuration 🔨
-### Command line args
+### Command line args
The µTask binary accepts the following arguments as binary args or env var.
All are optional and have a default value:
@@ -190,7 +190,7 @@ A user can be allowed to resolve a task in three ways:
- `auto_runnable`; boolean (default: false): when true, the task will be executed directly after being created, IF the requester is an accepted resolver or `allow_all_resolver_usernames` is true
- `blocked`: boolean (default: false): no tasks can be created from this template
- `hidden`: boolean (default: false): the template is not listed on the API, it is concealed to regular users
-- `retry_max`: int (default: 100): maximum amount of consecutive executions of a task based on this template, before being blocked for manual review
+- `retry_max`: int (default: 100): maximum amount of consecutive executions of a task based on this template, before being blocked for manual review
### Inputs
@@ -213,7 +213,7 @@ An input's definition allows to define validation constraints on the values prov
A template variable is a named holder of either:
- a fixed value
-- a JS expression evaluated on the fly.
+- a JS expression evaluated on the fly.
See the example template above to see variables in action. The expression in a variable can contain template handles to introduce values dynamically (from executed steps, for instance), like a step's configuration.
@@ -231,8 +231,8 @@ The flow of this sequence can further be controlled with **conditions** on the s
- to analyze its outcome and override the engine's default behaviour
Several conditions can be specified, the first one to evaluate as `true` is applied. A condition is composed of:
-- a `type` (skip or check)
-- a list of `if` assertions (`value`, `operator`, `expected`) which all have to be true (AND on the collection),
+- a `type` (skip or check)
+- a list of `if` assertions (`value`, `operator`, `expected`) which all have to be true (AND on the collection),
- a `then` object to impact the state of steps (`this` refers to the current step)
- an optional `message` to convey the intention of the condition, making it easier to inspect tasks
@@ -265,7 +265,7 @@ steps:
getUser:
description: Get user
custom_states: [NOT_FOUND]
- action:
+ action:
type: http
configuration:
url: http://example.org/user/{{.input.id}}
@@ -281,12 +281,12 @@ steps:
message: User {{.input.id}} not found
```
-#### Basic Step Properties
+#### Basic Step Properties
- `name`: a unique identifier
- `description`: a human readable sentence to convey the step's intent
- `dependencies`: a list of step names on which this step waits before running
-- `retry_pattern`: (seconds|minutes|hours) define on what temporal order of magnitude the re-runs of this step should be spread
+- `retry_pattern`: (seconds|minutes|hours) define on what temporal order of magnitude the re-runs of this step should be spread
#### Action
@@ -299,7 +299,7 @@ base_configurations:
postMessage:
method: POST
url: http://message.board/new
-```
+```
This base configuration can then be leveraged by any step wanting to post a message, with different bodies:
@@ -322,7 +322,7 @@ steps:
body: Goodbye
```
-These two step definitions are the equivalent of:
+These two step definitions are the equivalent of:
```yaml
steps:
@@ -381,7 +381,7 @@ Will render the following output, a combination of the action's raw output and t
Browse [builtin actions](./pkg/plugins/builtin)
-|Plugin name|Description|Documentation
+|Plugin name|Description|Documentation
|---|---|---
|**`echo`** | Print out a pre-determined result | [Access plugin doc](./pkg/plugins/builtin/echo/README.md)
|**`http`** | Make an http request | [Access plugin doc](./pkg/plugins/builtin/http/README.md)
@@ -392,7 +392,7 @@ Browse [builtin actions](./pkg/plugins/builtin)
|**`email`** | Send an email | [Access plugin doc](./pkg/plugins/builtin/email/README.md)
|**`ping`** | Send a ping to an hostname *Warn: This plugin will keep running until the count is done* | [Access plugin doc](./pkg/plugins/builtin/ping/README.md)
|**`script`** | Execute a script under `scripts` folder | [Access plugin doc](./pkg/plugins/builtin/script/README.md)
-
+
#### Loops
A step can be configured to take a json-formatted collection as input, in its `foreach` property. It will be executed once for each element in the collection, and its result will be a collection of each iteration. This scheme makes it possible to chain several steps with the `foreach` property.
@@ -402,7 +402,7 @@ For the following step definition (note json-format of `foreach`):
steps:
prefixStrings:
description: Process a collection of strings, adding a prefix
- foreach: '[{"id":"a"},{"id":"b"},{"id":"c"}]'
+ foreach: '[{"id":"a"},{"id":"b"},{"id":"c"}]'
action:
type: echo
configuration:
@@ -426,6 +426,42 @@ This output can be then passed to another step in json format:
foreach: '{{.step.prefixStrings.children | jsonmarshal}}'
```
+### Task templates validation
+
+A JSON-schema file is available to validate the syntax of task templates, it's available in `hack/template-schema.json`.
+
+Validation can be performed at writing time if you are using a modern IDE or editor.
+
+#### Validation with Visual Studio Code
+
+- Install YAML extension from RedHat.
+ - Ctrl+P, then type `ext install redhat.vscode-yaml`
+- Edit your workspace configuration (`settings.json` file) to add:
+```json
+{
+ "yaml.schemas": {
+ "./hack/template-schema.json": ["/*.yaml"]
+ }
+}
+```
+- Every templates will get real-time validation while writing.
+
+![](./assets/img/vscode_template_validation.png)
+
+#### Task templates snippets with Visual Studio Code
+
+Code snippets are available in this repository to be used for task template writing: `hack/templates.code-snippets`
+
+To use them inside your repository, copy the `templates.code-snippets` file into your `.vscode` workspace folder.
+
+Available snippets:
+- template
+- variable
+- input
+- step
+
+![](./assets/img/vscode_code_snippets_templates.gif)
+
## Extending µTask with plugins
µTask is extensible with [golang plugins](https://golang.org/pkg/plugin/) compiled in *.so format. Two kinds of plugins exist:
@@ -438,7 +474,7 @@ The installation script for utask creates a folder structure that will automatic
Action plugins allow you to extend the kind of work that can be performed during a task. An action plugin has a name, that will be referred to as the action `type` in a template. It declares a configuration structure, a validation function for the data received from the template as configuration, and an execution function which performs an action based on valid configuration.
-Create a new folder within the `plugins` folder of your utask repo. There, develop a `main` package that exposes a `Plugin` variable that implements the `TaskPlugin` defined in the `plugins` package:
+Create a new folder within the `plugins` folder of your utask repo. There, develop a `main` package that exposes a `Plugin` variable that implements the `TaskPlugin` defined in the `plugins` package:
```golang
type TaskPlugin interface {
@@ -451,7 +487,7 @@ type TaskPlugin interface {
}
```
-The `taskplugin` [package](./pkg/plugins/taskplugin/taskplugin.go) provides helper functions to build a Plugin:
+The `taskplugin` [package](./pkg/plugins/taskplugin/taskplugin.go) provides helper functions to build a Plugin:
```golang
package main
@@ -482,9 +518,9 @@ func exec(stepName string, config interface{}, ctx interface{}) (output interfac
### Init Plugins
-Init plugins allow you to customize your instance of µtask by giving you access to its underlying configuration store and its API server.
+Init plugins allow you to customize your instance of µtask by giving you access to its underlying configuration store and its API server.
-Create a new folder within the `init` folder of your utask repo. There, develop a `main` package that exposes a `Plugin` variable that implements the `InitializerPlugin` defined in the `plugins` package:
+Create a new folder within the `init` folder of your utask repo. There, develop a `main` package that exposes a `Plugin` variable that implements the `InitializerPlugin` defined in the `plugins` package:
```golang
type Service struct {
@@ -505,7 +541,7 @@ As of version `v1.0.0`, this is meant to give you access to two features:
If you develop more than one initialization plugin, they will all be loaded in alphabetical order. You might want to provide a default initialization, plus more specific behaviour under certain scenarios.
## Contributing
-
+
### Backend
In order to iterate on feature development, run the utask server plus a backing postgres DB by invoking `make run-test-stack-docker` in a terminal. Use SIGINT (`Ctrl+C`) to reboot the server, and SIGQUIT (`Ctrl+4`) to teardown the server and its DB.
@@ -514,7 +550,7 @@ In a separate terminal, rebuild (`make re`) each time you want to iterate on a c
### Frontend
-µTask serves two graphical interfaces: one for general use of the tool (`dashboard`), the other one for task template authoring (`editor`). They're found in the `ui` folder and each have their own Makefile for development purposes.
+µTask serves two graphical interfaces: one for general use of the tool (`dashboard`), the other one for task template authoring (`editor`). They're found in the `ui` folder and each have their own Makefile for development purposes.
Run `make dev` to launch a live-reloading on your machine. The editor is a standalone GUI, while the dashboard needs a backing µTask api (see above to run a server).
@@ -525,15 +561,15 @@ Run all test suites against an ephemeral postgres DB:
```bash
$ make test-docker
```
-
+
### Get in touch
You've developed a new cool feature ? Fixed an annoying bug ? We'll be happy
to hear from you! Take a look at [CONTRIBUTING.md](https://github.com/ovh/utask/blob/master/CONTRIBUTING.md)
-
+
## Related links
-
+
* Contribute: [CONTRIBUTING.md](CONTRIBUTING.md)
* Report bugs: [https://github.com/ovh/utask/issues](https://github.com/ovh/utask/issues)
* Get latest version: [https://github.com/ovh/utask/releases](https://github.com/ovh/utask/releases)
diff --git a/assets/img/vscode_code_snippets_templates.gif b/assets/img/vscode_code_snippets_templates.gif
new file mode 100644
index 00000000..e7006b04
Binary files /dev/null and b/assets/img/vscode_code_snippets_templates.gif differ
diff --git a/assets/img/vscode_template_validation.png b/assets/img/vscode_template_validation.png
new file mode 100644
index 00000000..5e0b9747
Binary files /dev/null and b/assets/img/vscode_template_validation.png differ
diff --git a/hack/template-schema.json b/hack/template-schema.json
new file mode 100644
index 00000000..51a7744e
--- /dev/null
+++ b/hack/template-schema.json
@@ -0,0 +1,905 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "type": "object",
+ "title": "µTask template schema",
+ "description": "This validates a µTask template",
+ "additionalProperties": false,
+ "definitions": {
+ "Step": {
+ "type": "object",
+ "title": "A µTask template step",
+ "default": {},
+ "additionalProperties": false,
+ "examples": [
+ {
+ "description": "Get UTC time",
+ "action": {
+ "type": "http",
+ "configuration": {
+ "method": "GET",
+ "url": "http://worldclockapi.com/api/json/utc/now"
+ }
+ }
+ }
+ ],
+ "required": [
+ "description",
+ "action"
+ ],
+ "properties": {
+ "description": {
+ "type": "string",
+ "title": "Step description",
+ "description": "Will be displayed to customers in UI.",
+ "default": "",
+ "examples": [
+ "Get UTC time"
+ ]
+ },
+ "retry_pattern": {
+ "type": "string",
+ "enum": [
+ "seconds",
+ "minutes",
+ "hours"
+ ],
+ "title": "Retry pattern of the step",
+ "description": "Define on what temporal order of magnitude the re-runs of this step should be spread."
+ },
+ "dependencies": {
+ "type": "array",
+ "description": "List of step names on which this step waits before running",
+ "items": {
+ "type": "string"
+ }
+ },
+ "action": {
+ "$ref": "#/definitions/Action"
+ },
+ "conditions": {
+ "type": "array",
+ "description": "Conditions before/after step executed",
+ "items": {
+ "$ref": "#/definitions/Condition"
+ }
+ },
+ "custom_states": {
+ "type": "array",
+ "description": "Declares some custom state to be used within the step",
+ "items": {
+ "type": "string"
+ }
+ },
+ "foreach": {
+ "type": "string",
+ "description": "Elements on which the step will loop"
+ }
+ }
+ },
+ "Action": {
+ "oneOf": [
+ {
+ "$ref": "#/definitions/ActionHTTP"
+ },
+ {
+ "$ref": "#/definitions/ActionEcho"
+ },
+ {
+ "$ref": "#/definitions/ActionScript"
+ },
+ {
+ "$ref": "#/definitions/ActionSSH"
+ },
+ {
+ "$ref": "#/definitions/ActionSubtask"
+ },
+ {
+ "$ref": "#/definitions/ActionPing"
+ },
+ {
+ "$ref": "#/definitions/ActionNotify"
+ },
+ {
+ "$ref": "#/definitions/ActionEmail"
+ },
+ {
+ "$ref": "#/definitions/ActionAPIOVH"
+ },
+ {
+ "type": "object",
+ "title": "Generic action",
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "not": {
+ "type": "string",
+ "enum": [
+ "http",
+ "echo",
+ "script",
+ "ssh",
+ "subtask",
+ "ping",
+ "notify",
+ "email",
+ "apiovh"
+ ]
+ }
+ },
+ "configuration": {
+ "type": "object",
+ "description": "Action configuration"
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ }
+ }
+ ]
+ },
+ "ActionHTTP": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "http"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "url",
+ "method"
+ ],
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "method": {
+ "type": "string",
+ "enum": [
+ "GET",
+ "POST",
+ "PUT",
+ "DELETE"
+ ]
+ },
+ "body": {
+ "type": "string"
+ },
+ "headers": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "timeout_seconds": {
+ "type": "string",
+ "pattern": "^\\d+$"
+ },
+ "auth": {
+ "type": "object",
+ "properties": {
+ "basic": {
+ "type": "object",
+ "properties": {
+ "user": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ }
+ }
+ },
+ "bearer": {
+ "type": "string"
+ }
+ }
+ },
+ "deny_redirects": {
+ "type": "string",
+ "pattern": "^(true|false)$"
+ },
+ "parameters": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "trim_prefix": {
+ "type": "string"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "HTTP Action",
+ "description": "HTTP action performs a HTTP call to a remote server",
+ "additionalProperties": false
+ },
+ "ActionEcho": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "echo"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "error_type": {
+ "type": "string",
+ "enum": [
+ "server",
+ "client"
+ ]
+ },
+ "error_message": {
+ "type": "string"
+ },
+ "output": {
+ "type": "object"
+ },
+ "metadata": {
+ "type": "object"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "Echo Action",
+ "additionalProperties": false,
+ "description": "Echo action just prints some result as output"
+ },
+ "ActionScript": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "script"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "file_path": {
+ "type": "string"
+ },
+ "argv": {
+ "type": "array"
+ },
+ "stdin": {
+ "type": "string"
+ },
+ "timeout": {
+ "type": "string"
+ },
+ "last_line_not_json": {
+ "type": "boolean"
+ },
+ "allow_exit_non_zero": {
+ "type": "boolean"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "Script Action",
+ "additionalProperties": false,
+ "description": "Script action executes a script locally in the µTask instance"
+ },
+ "ActionSSH": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ssh"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "user": {
+ "type": "string"
+ },
+ "hops": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "target": {
+ "type": "string"
+ },
+ "script": {
+ "type": "string"
+ },
+ "result": {
+ "type": "object"
+ },
+ "ssh_key": {
+ "type": "string"
+ },
+ "ssh_key_passphrase": {
+ "type": "string"
+ },
+ "allow_exit_non_zero": {
+ "type": "boolean"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "SSH Action",
+ "additionalProperties": false,
+ "description": "SSH action will performs a SSH connection to a remote host, and executes a command"
+ },
+ "ActionSubtask": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "subtask"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "template": {
+ "type": "string"
+ },
+ "input": {
+ "type": "object"
+ },
+ "resolver_usernames": {
+ "type": "string"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "Subtask Action",
+ "additionalProperties": false,
+ "description": "Subtask action will spawn a subtask of the specified template, and wait for resolution"
+ },
+ "ActionPing": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ping"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "hostname": {
+ "type": "string"
+ },
+ "count": {
+ "type": "string",
+ "pattern": "^\\d+$"
+ },
+ "interval_second": {
+ "type": "string",
+ "pattern": "^\\d+$"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "Ping Action",
+ "additionalProperties": false,
+ "description": "Ping action will perform a ping to a remote server to verify connectivity"
+ },
+ "ActionNotify": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "notify"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "message": {
+ "type": "string"
+ },
+ "backends": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "fields": {
+ "type": "object"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "Notify Action",
+ "additionalProperties": false,
+ "description": "Notify action will send a notification when executed"
+ },
+ "ActionEmail": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "email"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "smtp_username",
+ "smtp_password",
+ "smtp_port",
+ "smtp_hostname",
+ "from_address",
+ "to",
+ "subject",
+ "body"
+ ],
+ "properties": {
+ "smtp_username": {
+ "type": "string"
+ },
+ "smtp_password": {
+ "type": "string"
+ },
+ "smtp_port": {
+ "type": "string",
+ "pattern": "^\\d+$"
+ },
+ "smtp_skip_tls_verify": {
+ "type": "string",
+ "pattern": "^(true|false)$"
+ },
+ "from_address": {
+ "type": "string"
+ },
+ "from_name": {
+ "type": "string"
+ },
+ "subject": {
+ "type": "string"
+ },
+ "to": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "body": {
+ "type": "object"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "Email Action",
+ "additionalProperties": false,
+ "description": "Email action will send an email when executed"
+ },
+ "ActionAPIOVH": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "apiovh"
+ },
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "method",
+ "path"
+ ],
+ "properties": {
+ "method": {
+ "type": "string",
+ "enum": [
+ "GET",
+ "POST",
+ "PUT",
+ "DELETE"
+ ]
+ },
+ "path": {
+ "type": "string"
+ },
+ "credentials": {
+ "type": "string"
+ },
+ "body": {
+ "type": "object"
+ }
+ }
+ },
+ "base_configuration": {
+ "type": "object"
+ },
+ "base_output": {
+ "type": "object"
+ }
+ },
+ "title": "APIOVH Action",
+ "additionalProperties": false,
+ "description": "APIOVH action will perform an API call to OVH systems"
+ },
+ "Condition": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "Indicates if condition applies before (skip) or after (check) step is running",
+ "enum": [
+ "skip",
+ "check"
+ ]
+ },
+ "if": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string",
+ "description": "Value on which the condition applies"
+ },
+ "operator": {
+ "type": "string",
+ "description": "Operator used by the condition",
+ "enum": [
+ "EQ",
+ "NE",
+ "GT",
+ "LT",
+ "GE",
+ "LE",
+ "REGEXP"
+ ]
+ },
+ "expected": {
+ "type": "string",
+ "description": "Expected value for the condition to be true"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "description": "Describes state changement if all conditions match"
+ },
+ "message": {
+ "type": "string",
+ "description": "Message to display in the UI if condition match"
+ }
+ },
+ "description": "Conditions to apply on the step"
+ },
+ "Input": {
+ "type": "object",
+ "additionalProperties": false,
+ "examples": [
+ {
+ "name": "language",
+ "default": "english",
+ "optional": true,
+ "description": "The language in which you wish to greet the world",
+ "legal_values": [
+ "english",
+ "spanish"
+ ]
+ }
+ ],
+ "required": [
+ "name",
+ "description",
+ "legal_values",
+ "optional",
+ "default"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Input name - to be used inside template as .input.name",
+ "default": "",
+ "examples": [
+ "language"
+ ]
+ },
+ "description": {
+ "type": "string",
+ "description": "Description displayed in the UI",
+ "default": "",
+ "examples": [
+ "The language in which you wish to greet the world"
+ ]
+ },
+ "regex": {
+ "type": "string",
+ "description": "Regex to validate the input value",
+ "default": "",
+ "examples": [
+ "^[a-z\\d]+$"
+ ]
+ },
+ "type": {
+ "type": "string",
+ "description": "Input value type",
+ "enum": [
+ "string",
+ "number",
+ "bool"
+ ],
+ "examples": [
+ "bool"
+ ]
+ },
+ "legal_values": {
+ "type": "array",
+ "description": "Restrict input value to some values",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "optional": {
+ "type": "boolean",
+ "description": "Indicates if input is optional or not",
+ "default": false
+ },
+ "default": {
+ "type": "string",
+ "description": "Default value of the input if not fulfilled",
+ "examples": [
+ "english"
+ ]
+ },
+ "collection": {
+ "type": "boolean",
+ "description": "Indicates if multiple values can be specified for this input",
+ "default": false
+ }
+ }
+ },
+ "Variable": {
+ "type": "object",
+ "additionalProperties": false,
+ "examples": [
+ {
+ "name": "english-message",
+ "value": "Hello World!"
+ },
+ {
+ "expression": "// a short javascript snippet\nvar h = 'Hola';\nvar m = 'mundo';\nh + ' ' + m + '!';",
+ "name": "spanish-message"
+ }
+ ],
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Variable name",
+ "default": "",
+ "examples": [
+ "english-message"
+ ]
+ },
+ "value": {
+ "type": "string",
+ "description": "Fixed value for the variable (cannot be templated)",
+ "default": "",
+ "examples": [
+ "Hello World!"
+ ]
+ },
+ "expression": {
+ "type": "string",
+ "description": "Javascript expression that will be evaluated (can be templated)"
+ }
+ }
+ }
+ },
+ "required": [
+ "name",
+ "description",
+ "title_format",
+ "steps"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Template name",
+ "default": "",
+ "examples": [
+ "hello-world-now"
+ ]
+ },
+ "description": {
+ "type": "string",
+ "description": "Template short description",
+ "default": "",
+ "examples": [
+ "Say hello to the world, now!"
+ ]
+ },
+ "long_description": {
+ "type": "string",
+ "description": "Template long description (will be displayed to customer when selecting templates)",
+ "default": "",
+ "examples": [
+ "This task prints out a greeting to the entire world, after retrieving the current UTC time from an external API"
+ ]
+ },
+ "doc_link": {
+ "type": "string",
+ "description": "Template documentation URL",
+ "default": "",
+ "examples": [
+ "https://en.wikipedia.org/wiki/%22Hello,_World!%22_program"
+ ]
+ },
+ "title_format": {
+ "type": "string",
+ "description": "Template title (will be used when task are created using this template)",
+ "default": "",
+ "examples": [
+ "Say hello in {{.input.language}}"
+ ]
+ },
+ "result_format": {
+ "type": "object",
+ "description": "Describes the object format that will be saved as task result",
+ "default": {},
+ "examples": [
+ {
+ "echo_message": "{{.step.sayHello.output.message}}",
+ "echo_when": "{{.step.sayHello.output.when}}"
+ }
+ ]
+ },
+ "allowed_resolver_usernames": {
+ "type": "array",
+ "description": "Usernames of people allowed to resolve a task based on this template",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "allow_all_resolver_usernames": {
+ "type": "boolean",
+ "description": "Indicates if any user can resolve a task based on this template",
+ "default": false,
+ "examples": [
+ true
+ ]
+ },
+ "auto_runnable": {
+ "type": "boolean",
+ "description": "Indicates if task will run automatically with validation if requester is a valid resolver",
+ "default": false,
+ "examples": [
+ true
+ ]
+ },
+ "blocked": {
+ "type": "boolean",
+ "description": "Indicates if tasks can be created from this template",
+ "default": false,
+ "examples": [
+ false
+ ]
+ },
+ "hidden": {
+ "type": "boolean",
+ "description": "Indicates if template will be listed for regular users",
+ "default": false,
+ "examples": [
+ false
+ ]
+ },
+ "retry_max": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Maximum amount of consecutive executions of a task based on this template",
+ "default": 100,
+ "examples": [
+ 100
+ ]
+ },
+ "variables": {
+ "type": "array",
+ "description": "Variables that can be used inside this templates",
+ "default": [],
+ "items": {
+ "$ref": "#/definitions/Variable"
+ }
+ },
+ "inputs": {
+ "type": "array",
+ "description": "Inputs that should be provided when creating a task based on this template",
+ "default": [],
+ "items": {
+ "$ref": "#/definitions/Input"
+ }
+ },
+ "steps": {
+ "type": "object",
+ "description": "Steps that will be executed when a task based on this template is created",
+ "default": {},
+ "patternProperties": {
+ ".*": {
+ "$ref": "#/definitions/Step"
+ }
+ }
+ },
+ "base_configurations": {
+ "type": "object",
+ "description": "Global configurations that will be used as base for steps inside this template"
+ }
+ }
+}
\ No newline at end of file
diff --git a/hack/templates.code-snippets b/hack/templates.code-snippets
new file mode 100644
index 00000000..0636af4f
--- /dev/null
+++ b/hack/templates.code-snippets
@@ -0,0 +1,81 @@
+{
+ "utask-template": {
+ "scope": "yaml",
+ "prefix": "template",
+ "body": [
+ "name: ${1:my-new-template}",
+ "description: ${2:New template}",
+ "long_description: ${2:New template to do stuff}",
+ "doc_link: ",
+ "",
+ "title_format: ${3:New task for my-new-template}",
+ "result_format:",
+ " ${4:foo: bar}",
+ "",
+ "allowed_resolver_usernames: [${5:admin}]",
+ "allow_all_resolver_usernames: ${6|true,false|}",
+ "auto_runnable: ${7|true,false|}",
+ "blocked: ${8|true,false|}",
+ "hidden: ${9|true,false|}",
+ "",
+ "",
+ "inputs:",
+ "",
+ "",
+ "variables:",
+ "",
+ "",
+ "steps:"
+ ],
+ "description": "Generate a basic template skeleton"
+ },
+ "utask-input": {
+ "scope": "yaml",
+ "prefix": "input",
+ "body": [
+ "- name: ${1:param1}",
+ " description: ${2:Choose your first param}",
+ " type: ${3|string,number,bool|}",
+ " legal_values: [${4}]",
+ " regex: \"${5:^\\d+$}\"",
+ " optional: ${6|false,true|}",
+ " default: \"${7}\"",
+ " collection: ${8|false,true|}"
+ ],
+ "description": "Generate an task template input"
+ },
+ "utask-variable": {
+ "scope": "yaml",
+ "prefix": "variable",
+ "body": [
+ "- name: ${1:var1}",
+ " value: ${2:foobar}",
+ " expression: >-",
+ " ${3:var s = 'stuff';}"
+ ],
+ "description": "Generate a variable"
+ },
+ "utask-step": {
+ "scope": "yaml",
+ "prefix": "step",
+ "body": [
+ " ${1:createStuff}:",
+ " description: ${2:Create stuff}",
+ " dependencies: [${3}]",
+ " action:",
+ " type: ${4|echo,notify,script,ssh,http,subtask,apiovh,email,ping|}",
+ " configuration:",
+ " ${5:foobar: buzz}",
+ " conditions:",
+ " - type: ${6|check,skip|}",
+ " if:",
+ " - value: '${7}'",
+ " operator: ${8|EQ,NE,GT,LT,GE,LE,REGEXP|}",
+ " expected: '${9}'",
+ " then:",
+ " ${10:this: NOT_FOUND}",
+ " message: ${11:User not found}"
+ ],
+ "description": "Generate a basic step for task template"
+ }
+}
\ No newline at end of file