Giter Site home page Giter Site logo

ovh / utask Goto Github PK

View Code? Open in Web Editor NEW
1.1K 38.0 75.0 15.85 MB

µTask is an automation engine that models and executes business processes declared in yaml. ✏️📋

License: BSD 3-Clause "New" or "Revised" License

Dockerfile 0.11% Makefile 0.50% Go 68.10% Shell 1.04% PLpgSQL 0.60% JavaScript 1.10% TypeScript 20.55% HTML 6.26% Sass 1.56% SCSS 0.17% Python 0.02%
workflow-engine workflow-automation golang-application devops-tools golang angular8 automation workflow devops go hacktoberfest

utask's Introduction

µTask, the Lightweight Automation Engine

Build Status Go Report Card Coverage Status GoDoc GitHub stars GitHub last commit GitHub license

µTask is an automation engine built for the cloud. It is:

  • simple to operate: only a postgres DB is required
  • secure: all data is encrypted, only visible to authorized users
  • extensible: you can develop custom actions in golang

µTask allows you to model business processes in a declarative yaml format. Describe a set of inputs and a graph of actions and their inter-dependencies: µTask will asynchronously handle the execution of each action, working its way around transient errors and keeping an encrypted, auditable trace of all intermediary states until completion.

Table of contents

Real-world examples

Here are a few real-world examples that can be implemented with µTask:

Kubernetes ingress TLS certificate provisioning

A new ingress is created on the production kubernetes cluster. A hook triggers a µTask template that:

  • generates a private key
  • requests a new certificate
  • meets the certificate issuer's challenges
  • commits the resulting certificate back to the cluster

New team member bootstrap

A new member joins the team. The team leader starts a task specifying the new member's name, that:

  • asks the new team member to generate an SSH key pair and copy the public key in a µTask-generated form
  • registers the public SSH key centrally
  • creates accounts on internal services (code repository, CI/CD, internal PaaS, ...) for the new team member
  • triggers another task to spawn a development VM
  • sends a welcome email full of GIFs

Payments API asynchronous processing

The payments API receives a request that requires an asynchronous antifraud check. It spawns a task on its companion µTask instance that:

  • calls a first risk-assessing API which returns a number
  • if the risk is low, the task succeeds immediately
  • otherwise it calls a SaaS antifraud solution API which returns a score
  • if the score is good, the task succeeds
  • if the score is very bad, the task fails
  • if it is in between, it triggers a human investigation step where an operator can enter a score in a µTask-generated form
  • when it is done, the task sends an event to the payments API to notify of the result

The payments API keeps a reference to the running workflow via its task ID. Operators of the payments API can follow the state of current tasks by requesting the µTask instance directly. Depending on the payments API implementation, it may allow its callers to follow a task's state.

Quick start

Running with docker-compose

Download our latest install script, setup your environment and launch your own local instance of µTask.

mkdir utask && cd utask
wget https://github.com/ovh/utask/releases/latest/download/install-utask.sh
sh install-utask.sh
docker-compose up

All the configuration for the application is found in the environment variables in docker-compose.yaml. You'll see that basic auth is setup for user admin with password 1234. Try logging in with this user on the graphical dashboard: http://localhost:8081/ui/dashboard.

You can also explore the API schema: http://localhost:8081/unsecured/spec.json.

Request a new task:

Get an overview of all tasks:

Get a detailed view of a running task:

Browse available task templates:

Running with your own postgres service

Alternatively, you can clone this repository and build the µTask binary:

make all

Operating in production

The folder you created in the previous step is meant to become a git repo where you version your own task templates and plugins. Re-download and run the latest install script to bump your version of µTask.

You'll deploy your version of µTask by building a docker image based on the official µTask image, which will include your extensions. See the Dockerfile generated during installation.

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.

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.

Maintenance procedures

Key rotation

  1. Generate a new key with symmecrypt, with the 'storage' label.
  2. Add it to your configuration items. The library will take all keys into account and use the latest possible key, falling back to older keys when finding older data.
  3. Set your API in maintenance mode (env var or command line arg, see config below): all write actions will be refused when you reboot the API.
  4. Reboot API.
  5. Make a POST request on the /key-rotate endpoint of the API.
  6. All data will be encrypted with the latest key, you can delete older keys.
  7. De-activate maintenance mode.
  8. Reboot API.

Dependencies

The only dependency for µTask is a Postgres database server. The minimum version for the Postgres database is 9.5

Configuration 🔨

Command line args

The µTask binary accepts the following arguments as binary args or env var. All are optional and have a default value:

  • init-path: the directory from where initialization plugins (see "Developing plugins") are loaded in *.so form (default: ./init)
  • plugins-path: the directory from where action plugins (see "Developing plugins") are loaded in *.so form (default: ./plugins)
  • templates-path: the directories where yaml-formatted task templates are loaded from, can be a colon separated list (default: ./templates)
  • functions-path: the directory where yaml-formatted functions templates are loaded from (default: ./functions)
  • region: an arbitrary identifier, to aggregate a running group of µTask instances (commonly containers), and differentiate them from another group, in a separate region (default: default)
  • http-port: the port on which the HTTP API listents (default: 8081)
  • debug: a boolean flag to activate verbose logs (default: false)
  • maintenance-mode: a boolean to switch API to maintenance mode (default: false)

Config keys and files

Checkout the µTask config keys and files README.

Authentication

The vanilla version of µTask doesn't handle authentication by itself, it is meant to be placed behind a reverse proxy that provides a username through the "x-remote-user" http header. A username found there will be trusted as is, and used for authorization purposes (admin actions, task resolution, etc...).

For development purposes, an optional basic-auth configstore item can be provided to define a mapping of usernames and passwords. This is not meant for use in production.

Extending this basic authentication mechanism is possible by developing an "init" plugin, as described below.

Notification

Every task state change can be notified to a notification backend. µTask implements three differents notification backends: Slack, Opsgenie, and generic webhooks.

Default payload that will be sent for generic webhooks are:

task_state_update notifications:

{
    "message": "string",
    "notification_type": "task_state_update",
    "task_id": "public_task_uuid",
    "title": "task title string",
    "state": "current task state",
    "template": "template_name",
    "requester": "optional",
    "resolver": "optional",
    "steps": "14/20",
    "potential_resolvers": "user1,user2,admin",
    "resolution_id": "optional,public_resolution_uuid",
    "tags": "{\"tag1\":\"value1\"}"
}

task_step_update notifications:

{
    "message": "string",
    "notification_type": "task_step_update",
    "task_id": "public_task_uuid",
    "title": "task title string",
    "state": "current task state",
    "template": "template_name",
    "step_name": "string",
    "step_state": "string",
    "requester": "string",
    "resolver": "string",
    "steps": "14/20",
    "resolution_id": "public_resolution_uuid",
    "tags": "{\"tag1\":\"value1\"}"
}

task_validation notifications:

{
    "message": "string",
    "notification_type": "task_validation",
    "task_id": "public_task_uuid",
    "title": "task title string",
    "state": "TODO",
    "template": "template_name",
    "requester": "optional",
    "potential_resolvers": "user1,user2,admin",
    "tags": "{\"tag1\":\"value1\"}"
}

Notification backends can be configured in the global µTask configuration, as described here.

Authoring Task Templates

Checkout the µTask examples directory.

A process that can be executed by µTask is modelled as a task template: it is written in yaml format and describes a sequence of steps, their interdepencies, and additional conditions and constraints to control the flow of execution.

The user that creates a task is called requester, and the user that executes it is called resolver. Both can be the same user in some scenarios.

A user can be allowed to resolve a task in four ways:

  • the user is included in the global configuration's list of admin_usernames
  • the user is included in the task's template list of allowed_resolver_usernames
  • the user is in a group that is included in the task's template list of allowed_resolver_groups
  • the user is included in the task resolver_usernames list

Value Templating

µTask uses the go templating engine in order to introduce dynamic values during a task's execution. As you'll see in the example template below, template handles can be used to access values from different sources. Here's a summary of how you can access values through template handles:

  • .input.[INPUT_NAME]: the value of an input provided by the task's requester
  • .resolver_input.[INPUT_NAME]: the value of an input provided by the task's resolver
  • .step.[STEP_NAME].output.foo: field foo from the output of a named step
  • .step.[STEP_NAME].metadata.HTTPStatus: field HTTPStatus from the metadata of a named step
  • .step.[STEP_NAME].children: the collection of results from a 'foreach' step
  • .step.[STEP_NAME].error: error message from a failed step
  • .step.[STEP_NAME].state: current state of the given step
  • .step.[STEP_NAME].max_retries: max retries of the given step
  • .step.[STEP_NAME].try_count: try count of the given step
  • .config.[CONFIG_ITEM].bar: field bar from a config item (configstore, see above)
  • .iterator.foo: field foo from the iterator in a loop (see foreach steps below)
  • .pre_hook.output.foo: field foo from the output of the step's pre-hook (see pre-hooks)
  • .pre_hook.metadata.HTTPStatus: field HTTPStatus from the metadata of the step's pre-hook (see pre-hooks)
  • .function_args.[ARG_NAME]: argument that needs to be given in the conifguration section to the function (see functions below)

The following templating functions are available:

Name Description Reference
Golang Builtin functions from Golang text template Doc
Sprig Extended set of functions from the Sprig project Doc
field Equivalent to the dot notation, for entries with forbidden characters {{field `config` `foo.bar`}}
fieldFrom Equivalent to the dot notation, for entries with forbidden characters. It takes the previous template expression as source for the templating values. Example: ```{{ {"foo.foo":"bar"} fromJson
eval Evaluates the value of a template variable {{eval `var1`}}
evalCache Evaluates the value of a template variable, and cache for future usage (to avoid further computation) {{evalCache `var1`}}
fromJson Decodes a JSON document into a structure. If the input cannot be decoded as JSON, the function will return an empty string {{fromJson `{"a":"b"}`}}
mustFromJson Similar to fromJson, but will return an error in case the JSON is invalid. A common usecase consists of returning a JSON stringified data structure from a JavaScript expression (object, array), and use one of its members in the template. Example: {{(eval `myExpression` | fromJson).myArr}} or {{(eval `myExpression` | fromJson).myObj}} {{mustFromJson `{"a":"b"}`}}

Basic properties

  • name: a short unique human-readable identifier
  • description: sentence-long description of intent
  • long_description: paragraph-long basic documentation
  • doc_link: URL for external documentation about the task
  • title_format: templateable text, generates a title for a task based on this template
  • result_format: templateable map, used to generate a final result object from data collected during execution

Advanced properties

  • allowed_resolver_groups: a list of groups with the right to resolve a task based on this template
  • allowed_resolver_usernames: a list of usernames with the right to resolve a task based on this template
  • allow_all_resolver_usernames: boolean (default: false): when true, any user can execute a task based on this template
  • 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
  • tags: templatable map, used to filter tasks (see tags)

Inputs

When creating a new task, a requester needs to provide parameters described as a list of objects under the inputs property of a template. Additional parameters can be requested from a task's resolver user: those are represented under the resolver_inputs property of a template.

An input's definition allows to define validation constraints on the values provided for that input. See example template above.

Input properties

  • name: unique name, used to access the value provided by the task's requester
  • description: human readable description of the input, meant to give context to the task's requester
  • regex: (optional) a regular expression that the provided value must match
  • legal_values: (optional) a list of possible values accepted for this input
  • collection: boolean (default: false) a list of values is accepted, instead of a single value
  • type: (string|number|bool) (default: string) the type of data accepted
  • optional: boolean (default: false) the input can be left empty
  • default: (optional) a value assigned to the input if left empty

Variables

A template variable is a named holder of either:

  • a fixed value
  • a JavaScript 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.

The JavaScript evaluation is done using otto.

Tags

Tags are a map of strings property of a task. They will be used in the task listing to search for some tasks using filters. With tags, uTask can be used as a task backend by others APIs.

Tags values are expected to be a string: it support all uTask templating on values. To remove a tag from a task, use the empty value "".

  tags:
      customer: "{{.input.customer_id}}"
      type: "billing"

In this example, tag customer will be templated from the task inputs, and allow others APIs to search all the tasks for a given customer.

Tags can be added to a task:

  • from the template definition of the task
  • while creating a task, requester can input custom tags
  • during the execution, using the tag builtin plugin

Steps

A step is the smallest unit of work that can be performed within a task. At is's heart, a step defines an action: several types of actions are available, and each type requires a different configuration, provided as part of the step definition. The state of a step will change during a task's resolution process, and determine which steps become eligible for execution. Custom states can be defined for a step, to fine-tune execution flow (see below).

A sequence of ordered steps constitutes the entire workload of a task. Steps are ordered by declaring dependencies between each other. A step declares its dependencies as a list of step names on which it waits, meaning that a step's execution will be on hold until its dependencies have been resolved. More details about dependencies.

The flow of this sequence can further be controlled with conditions on the steps: a condition is a clause that can be run before or after the step's action. A condition can either be used:

  • to skip a step altogether
  • to analyze its outcome and override the engine's default behaviour

Several conditions can be specified. Unless final is set to true, they are all evaluated in order. If multiple conditions evaluate to true, they will be applied sequentially. Once a condition is applied, the next condition is evaluated using the new context (i.e. using the new state value of steps that got updated). If multiple conditions are evaluated to true and are changing the same step state value, then the last condition to evaluate as true will be the one that will change the state step for real.

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 then object to impact the state of steps (this refers to the current step)
  • a final boolean, defaulting to false. When set to true, it prevents the evaluation of the next conditions if this one is evaluated to true
  • an optional message to convey the intention of the condition, making it easier to inspect tasks

Here's an example of a skip condition. The value of an input is evaluated to determine the result: if the value of runType is dry, the createUser step will not be executed, its state will be set directly to DONE.

inputs:
- name: runType
  description: Run this task with/without side effects
  legal_values: [dry, wet]
steps:
  createUser:
    description: Create new user
    action:
      ... etc...
    conditions:
    - type: skip
      if:
      - value: '{{.input.runType}}'
        operator: EQ
        expected: dry
      then:
        this: DONE
      message: Dry run, skip user creation

Here's an example of a check condition. Here the return of an http call is inspected: a 404 status will put the step in a custom NOT_FOUND state. The default behavior would be to consider any 4xx status as a client error, which blocks execution of the task. The check condition allows you to consider this situation as normal, and proceed with other steps that take the NOT_FOUND state into account (creating the missing resource, for instance).

steps:
  getUser:
    description: Get user
    custom_states: [NOT_FOUND]
    action:
      type: http
      configuration:
        url: http://example.org/user/{{.input.id}}
        method: GET
    conditions:
    - type: check
      if:
      - value: '{{.step.getUser.metadata.HTTPStatus}}'
        operator: EQ
        expected: '404'
      then:
        this: NOT_FOUND
      message: User {{.input.id}} not found
  createUser:
    description: Create the user
    dependencies: ["getUser:NOT_FOUND"]
    action:
      type: http
      configuration:
        url: http://example.org/user
        method: POST
        body: |-
          {"user_id":"{{.input.id}}"}

Condition Operators

A condition can use one of the following operators:

  • EQ: equal
  • NE: not equal
  • GT: greater than
  • LT: less than
  • GE: greater or equal
  • LE: less than or equal
  • REGEXP: match a regexp
  • NOTREGEXP: doesn't match a regexp
  • IN: found in a list of values
  • NOTIN: not found in a list of values

Note that the operators IN and NOTIN expect a list of acceptable values in the field value, instead of a single one. You can specify the separator character to use to split the values of the list using the field list_separator (default: ,). Each value of the list will be trimmed of its leading and trailing white spaces before comparison.

Basic Step Properties

  • name: a unique identifier
  • description: a human readable sentence to convey the step's intent
  • action: the actual task the step executes, see Action
  • foreach: see Loops
  • pre_hook: an action that can be executed before the actual action of the step
  • dependencies: a list of step names on which this step waits before running
  • idempotent: a boolean indicating if this step is safe to be replayed in case of uTask instance crash
  • json_schema: a JSON-Schema object to validate the step output
  • resources: a list of resources that will be used by this step to apply some rate-limiting (see resources)
  • custom_states: a list of personnalised allowed state for this step (can be assigned to the state's step using conditions)
  • retry_pattern: (seconds, minutes, hours) define on what temporal order of magnitude the re-runs of this step should be spread (default = seconds)
  • resources: a list of resources that will be used during the step execution, to control and limit the concurrent execution of the step (more information in the resources section).

Action

The action field of a step defines the actual workload to be performed. It consists of at least a type chosen among the registered action plugins, and a configuration fitting that plugin. See below for a detailed description of builtin plugins. For information on how to develop your own action plugins, refer to this section.

When an action's configuration is repeated across several steps, it can be factored by defining base_configurations at the root of the template. For example:

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:

steps:
  sayHello:
    description: Say hello on the message board
    action:
      type: http
      base_configuration: postMessage
      configuration:
        body: Hello
  sayGoodbye:
    description: Say goodbye on the message board
    dependencies: [sayHello]
    action:
      type: http
      base_configuration: postMessage
      configuration:
        body: Goodbye

These two step definitions are the equivalent of:

steps:
  sayHello:
    description: Say hello on the message board
    action:
      type: http
      configuration:
        body: Hello
        method: POST
        url: http://message.board/new
  sayGoodbye:
    description: Say goodbye on the message board
    dependencies: [sayHello]
    action:
      type: http
      configuration:
        body: Goodbye
        method: POST
        url: http://message.board/new

The output of an action can be enriched by means of an output. For example, in a template with an input field named id, value 1234 and a call to a service which returns the following payload:

{
  "name": "username"
}

The following action definition:

steps:
  getUser:
    description: Prefix an ID received as input, return both
    action:
      type: http
      output:
        strategy: merge
        format:
          id: "{{.input.id}}"
      configuration:
        method: GET
        url: http://directory/user/{{.input.id}}

Will render the following output, a combination of the action's raw output and the output:

{
  "id": "1234",
  "name": "username"
}

All the strategies available are:

  • merge: data in format must be a dict and will be merged with the output of the action (e.g. ahead)
  • template: the action will return exactly the data in format that can be templated (see Value Templating)

Builtin actions

Browse builtin actions

Plugin name Description Documentation
echo Print out a pre-determined result Access plugin doc
http Make an http request Access plugin doc
subtask Spawn a new task on µTask Access plugin doc
notify Dispatch a notification over a registered channel Access plugin doc
apiovh Make a signed call on OVH's public API (requires credentials retrieved from configstore, containing the fields endpoint, appKey, appSecret, consumerKey, more info here) Access plugin doc
ssh Connect to a remote system and run commands on it Access plugin doc
email Send an email Access plugin doc
ping Send a ping to an hostname Warn: This plugin will keep running until the count is done Access plugin doc
script Execute a script under scripts folder Access plugin doc
tag Add tags to the current running task Access plugin doc
callback Use callbacks to manage your tasks life-cycle Access plugin doc

Pre-hooks

The pre_hook field of a step can be set to define an action that is executed before the step's action. This field supports all the same fields as the action. It aims to fetch data for the execution of the action that can change over time and needs to be fetched at every retry, such as OTPs. All the result values of the pre-hook are available under the templating variable .pre_hook.

doSomeAuthPost:
  pre_hook:
    type: http
    configuration:
      method: "GET"
      url: "https://example.org/otp"
  action:
    type: http
    configuration:
      method: "POST"
      url: "https://example.org/doSomePost"
      headers:
        X-Otp: "{{ .pre_hook.output }}"

Functions

Functions are abstraction of the actions to define a behavior that can be re-used in templates. They act like a plugin but are pre-declared in dedicated directory functions. They can have arguments that need to be given in the configuration section of the action and can be used in the declaration of the function by accessing the templating variables under .function_args.

name: ovh::request
description: Execute a call to the ovh API
pre_hook:
  type: http
  configuration:
    method: "GET"
    url: https://api.ovh.com/1.0/auth/time
action:
  type: http
  configuration:
    headers:
    - name: X-Ovh-Signature
      value: '{{ printf "%s+%s+%s+%s%s+%s+%v" .config.apiovh.applicationSecret .config.apiovh.consumerKey .function_args.method .config.apiovh.basePath .function_args.path .function_args.body .pre_hook.output | sha1sum | printf "$1$%s"}}'
    - name: X-Ovh-Timestamp
      value: "{{ .pre_hook.output }}"
    - name: X-Ovh-Consumer
      value: "{{ .config.apiovh.consumerKey }}"
    - name: X-Ovh-Application
      value: "{{ .config.apiovh.applicationKey }}"
    method: "{{ .function_args.method }}"
    url: "{{.config.apiovh.basePath}}{{ .function_args.path }}"
    body: "{{ .function_args.body }}"

This function can be used in a template like this:

steps:
  getService:
    description: Get Service
    action:
      type: ovh::request
      configuration:
        path: "{{.input.path}}"
        method: GET
        body: ""

Dependencies

Dependencies can be declared on a step, to indicate what requirements should be met before the step can actually run. A step can have multiple dependencies, which will all have to be met before the step can start running.

A dependency can be qualified with a step's state (stepX:stateY, it depends on stepX, finishing in stateY). If omitted, then DONE is assumed.

There are two different kinds of states: builtin and custom. Builtin states are provided by uTask and include: TODO, RUNNING, DONE, CLIENT_ERROR, SERVER_ERROR, FATAL_ERROR, CRASHED, PRUNE, TO_RETRY, AFTERRUN_ERROR. Additionally, a step can define custom states via its custom_states field. These custom states provide a way for the step to express that it ran successfully, but the result may be different from the normal expected case (e.g. a custom state NOT_FOUND would let the rest of the workflow proceed, but may trigger additional provisioning steps).

A dependency (stepX:stateY) can be on any of stepX's custom states, along with DONE (builtin). These are all considered final (uTask will not touch that step anymore, it has been run to completion). Conversely, other builtin states (CLIENT_ERROR, ...) may not be used in a dependency, since those imply a transient state and the uTask engine still has work to do on these.

If you wish to declare a dependency on something normally considered as a CLIENT_ERROR (e.g. GET HTTP returns a 404), you can write a check condition to inspect your step result, and change it to a custom state instead (meaning an alternative termination, see the NOT_FOUND example)

It is possible that a dependency will never match the expected state. For example, step1 is in DONE state, and step2 has a dependency declared as step1:NOT_FOUND: it means that step2 requires that step1 finishes its execution with state NOT_FOUND. In that case, step2 will never be allowed to run, as step1 finished with state DONE. To remedy this, uTask will remove step2 from the workflow by setting its state to the special state PRUNE. Any further step depending on step2 will also be pruned, removing entire alternative execution branches. This allows crossroads patterns, where a step may be followed by two mutually exclusive branches (one for DONE, one for ALTERNATE_STATE_XXX). (Note: PRUNE may also be used in conditions to manually eliminate entire branches of execution)

A special qualifier that can be used as a dependency state is ANY (stepX:ANY). ANY matches all custom states and DONE, and it also does not get PRUNE'd recursively if stepX is set to PRUNE. This is used mostly for sequencing, either when the actual result of the step does not matter, but its timing does; or to reconcile mutually exclusive branches in a diamond pattern (using e.g. the coalesce templating function to mix optional step results).

For example, step2 can declare a dependency on step1 in the following ways:

  • step1: wait for step1 to be in state DONE (could also be written as step1:DONE)
  • step1:DONE,ALREADY_EXISTS: wait for step1 to be either in state DONE or ALREADY_EXISTS
  • step1:ANY: wait for step1 to be in any "final" state, ie. it cannot keep running

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.

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"}]'
    action:
      type: echo
      configuration:
        output:
          prefixed: pre-{{.iterator.id}}

The following output can be expected to be accessible at {{.step.prefixStrings.children}}

[
    {
        "output": {
            "prefixed": "pre-a"
        },
        "metadata": {},
        "state": "DONE"
    },
    {
        "output": {
            "prefixed": "pre-b"
        },
        "metadata": {},
        "state": "DONE"
    },
    {
        "output": {
            "prefixed": "pre-c"
        },
        "metadata": {},
        "state": "DONE"
    }
]

It contains all the output, metadata and state of the different iterations, coming from the foreach loop.

This output can be then passed to another step in json format:

foreach: '{{.step.prefixStrings.children | toJson}}'

It's possible to configure the strategy used to run each elements:

  • parallel (default): each elements will be run in parallel to maximize throughput
  • sequence: will run each element when the previous one is done, to ensure the sequence between elements.

It can be declared in the template like this:

foreach_strategy: "sequence"

When writing skip conditions on loops, an additional property foreach can be added. It can have two values:

  • children: default value. If no value is set, this value is used. The condition will be run on every iteration of the foreach loop;
  • parent: the condition will be run on the step itself before creating its children.

For example:

foreach: '{{.step.aPreviousStep.output.ids | toJson}}'
action:
  type: echo
  configuration:
    output:
      url: '{{ .iterator }}'
conditions:
  - type: skip
    foreach: children  # <- this line can be omitted
    if:
      - value: '{{ .iterator }}'
        operator: EQ
        expected: '{{ .step.something.output.dontTouchId }}'
    then:
      this: PRUNE

will be run on every children and skip the child by pruning it the condition is true. And

foreach: '{{.step.aPreviousStep.output.ids | toJson}}'
action:
  type: echo
  configuration:
    output:
      url: '{{ .iterator }}'
conditions:
  - type: skip
    foreach: parent
    if:
      - value: '{{ step.previousCheck.output.result }}'
        operator: EQ
        expected: 'already_done'
    then:
      this: PRUNE

will be run before creating any children, by pruning the parent.

Resources

Resources are a way to restrict the concurrency factor of operations, to control the throughput and avoid dangerous behavior (e.g. flooding the targets).

High level view:

  • For each action to execute, a list of target resources is determined. (see later)
  • In the µTask configuration, numerical limits can be set to each resource label. This acts as a semaphore, allowing a certain number of concurrent slots for the given resource label. If no limit is set for a resource label, the previously mentionned target resources have no effect. Limits are declared in the resource_limits property.

The target resources for a step can be defined in its YAML definition, using the resources property.

steps:
  foobar:
    description: A dummy step, that should not execute in parallel
    resources: ["myLimitedResource"]
    action:
      type: echo
      configuration:
        output:
          foobar: fuzz

Alternatively, some target resources are determined automatically by µTask Engine:

  • When a task is run, the resource template:my-template-name is used automatically.
  • When a step is run, the plugin in charge of the execution automatically generates a list of resources. This includes generic resources such as socket, url:www.example.org, fork... allowing the µTask administrator to set-up generic limits such as "socket": 48 or "url:www.example.org": 1.

Each builtin plugins declares resources which can be discovered using the README of the plugin (example for http plugin).

Declared resource_limits must be positive integers. When a step is executed, if the number of concurrent executions is reached, the µTask Engine will wait for a slot to be released. If the resource is limited to the 0 value, then the step will not be executed and is set to TO_RETRY state, it will be run once the instance allows the execution of its resources. The default time that µTask Engine will wait for a resource to become available is 1 minute, but it can be configured using the resource_acquire_timeout property.

Task templates validation

A JSON-schema file is available to validate the syntax of task templates and functions, it's available in files hack/template-schema.json and hack/function-schema.json.

Validation can be performed at writing time if you are using a modern IDE or editor.

Working with Visual Studio Code

  • Install official µTask extension.
    • Ctrl+P, then type ext install ovh.vscode-utask

Task template snippets with Visual Studio Code

Code snippets are available in this repository to be used for task template editing: 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

Extending µTask with plugins

µTask is extensible with golang plugins compiled in *.so format. Two kinds of plugins exist:

  • action plugins, that you can re-use in your task templates to implement steps
  • init plugins, a way to customize the authentication mechanism of the API, and to draw data from different providers of the configstore library

The installation script for utask creates a folder structure that will automatically package and build your code in a docker image, with your plugins ready to be loaded by the main binary at boot time. Create a separate folder for each of your plugins, within either the plugins or the init folders.

Action Plugins

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:

type TaskPlugin interface {
	ValidConfig(baseConfig json.RawMessage, config json.RawMessage) error
	Exec(stepName string, baseConfig json.RawMessage, config json.RawMessage, ctx interface{}) (interface{}, interface{}, error)
	Context(stepName string) interface{}
	PluginName() string
	PluginVersion() string
	MetadataSchema() json.RawMessage
}

The taskplugin package provides helper functions to build a Plugin:

package main

import (
	"github.com/ovh/utask/pkg/plugins/taskplugin"
)

var (
	Plugin = taskplugin.New("my-plugin", "v0.1", exec,
		taskplugin.WithConfig(validConfig, Config{}))
)

type Config struct { ... }

func validConfig(config interface{}) (err error) {
  cfg := config.(*Config)
  ...
  return
}

func exec(stepName string, config interface{}, ctx interface{}) (output interface{}, metadata interface{}, err error) {
  cfg := config.(*Config)
  ...
  return
}

Exec function returns 3 values:

  • output: an object representing the output of the plugin, that will be usable as {{.step.xxx.output}} in the templating engine.
  • metadata: an object representing the metadata of the plugin, that will be usable as {{.step.xxx.metadata}} in the templating engine.
  • err: an error if the execution of the plugin failed. uTask is based on github.com/juju/errors package to determine if the returned error is a CLIENT_ERROR or a SERVER_ERROR.

Warning: output and metadata should not be named structures but plain map. Otherwise, you might encounter some inconsistencies in templating as keys could be different before and after marshalling in the database.

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.

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:

type Service struct {
	Store  *configstore.Store
	Server *api.Server
}

type InitializerPlugin interface {
	Init(service *Service) error // access configstore and server to customize µTask
	Description() string         // describe what the initialization plugin does
}

As of version v1.0.0, this is meant to give you access to two features:

  • service.Store exposes the RegisterProvider(name string, f configstore.Provider) method that allow you to plug different data sources for you configuration, which are not available by default in the main runtime
  • service.Server exposes the WithAuth(authProvider func(*http.Request) (string, error)) and WithGroupAuth(groupAuthProvider func(*http.Request) (string, []string, error)) methods, where you can provide a custom source of authentication and authorization based on the incoming http requests

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.

In a separate terminal, rebuild (make re) each time you want to iterate on a code patch, then reboot the server in the terminal where it is running.

To visualize API routes, a swagger-ui interface is available with the docker image, accessible through your browser at http://hostname.example/ui/swagger/.

Frontend

µTask serves a graphical interface for general use of the tool (dashboard). It is found in the ui folder and has its own Makefile for development purposes.

Run make dev to launch a live-reloading on your machine. The dashboard needs a backing µTask api (see above to run a server).

Run the tests

Run all test suites against an ephemeral postgres DB:

$ 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

License

The uTask logo is an original artwork under Creative Commons 3.0 license based on a design by Renee French under Creative Commons 3.0 Attributions.

Swagger UI is an open-source software, under Apache 2 license.

For the rest of the code, see LICENSE.

Related links

utask's People

Contributors

alecerf avatar alkorin avatar amstuta avatar ccamel avatar deathiop avatar dependabot[bot] avatar drrebus avatar floriancazals avatar fly2f avatar fsamin avatar galiley avatar guilhem avatar loopfz avatar orandin avatar pablito-perez avatar qexk avatar rbeuque74 avatar rclsilver avatar reenjii avatar rgbj avatar richardlt avatar sguiheux avatar simonmartinez avatar testwill avatar w3st3ry avatar wi2l avatar ybriffa avatar yesnault avatar zhouzhuojie avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

utask's Issues

[script] Allow to define environment variables in scripts

Is your feature request related to a problem? Please describe.
I would like to provides to my scripts utask task id and resolution id. Currently, i've to give through argv but it could be fine to provide them through environment variables.

Describe the solution you'd like
Currently:

action:
  type: script
  configuration:
    file_path: my_wrapper.sh
    argv:
    - '{{ .task.task_id }}'
    - '{{ .task.resolution_id }}'

Expected:

action:
  type: script
  configuration:
    file_path: my_wrapper.sh
    environment:
       utask_task_id: '{{ .task.task_id }}'
       utask_resolution_id:  '{{ .task.resolution_id }}'

Allow to retrieve current resolution watchers

Is your feature request related to a problem? Please describe.
In a template, i'm creating a subtask. I would like to define watcher_usernames with the main template value.

Describe the solution you'd like

...
inputs:
...
steps:
...
  install-servers:
    foreach: '{{ field `step` `wait-scale` `output` `result` `result` | toJson }}'
    description: Follow server installation and do post-installation steps
    action:
      type: subtask
      configuration:
        template: install-server
        input:
          ...
        watcher_usernames: '{{ .watcher_usernames | toJson }}'
...

Describe alternatives you've considered
Currently, when i create a new task, i've an input with a list of usernames.

Async action pattern for HTTP webhook/callback

Is your feature request related to a problem? Please describe.
For example:
image

It's common that some remote services are designed in an async communication pattern:

  • Webhook workflow:
    • Send HTTP
    • Wait for HTTP callback (usually listen on POST request from remote servers)
    • continue

Describe the solution you'd like
For example, define a generic callback endpoint that's capable of finding the task by deriving public id from the request. Upon getting the webhook, look for the task/resolution, pause it and modify the state or the step or the input of the task and reevaluate the task.

Describe alternatives you've considered
Maybe some workaround is to build something outside utask, e.g. a simple webserver that talks to utask APIs.

Additional context

Can't make the condition operator "IN / NOTIN" working

Describe the bug

In a template step, I want to use the condition operator IN / NOTIN. But it doesn't work: the assertion to check if a given value is IN / NOTIN a list of items is never true so I never match this condition.

Acccording to the doc:

Note that the operators IN and NOTIN expect a list of acceptable values in the field value, instead of a single one. You can specify the separator character to use to split the values of the list using the field list_separator (default: ,). Each value of the list will be trimmed of its leading and trailing white spaces before comparison.

Here is my step:

checkPlaybookStatus:
  description: Check the previously launched Ansible playbook's status is "successful"
  dependencies: [launchPlaybook]
  action:
    type: http
    configuration:
      url: http://some-url/api/v2/jobs/{{.step.launchPlaybook.output.id}}
      method: GET
  conditions:
    - type: check
      if:
        - value: '{{.step.checkPlaybookStatus.output.status}}'
          operator: IN
          expected: pending,waiting,running
      then:
        this: TO_RETRY
      message: Ansile playbook is still running

Here the value of {{.step.checkPlaybookStatus.output.status}} is "pending", I can see it in µTask GUI.
Screenshot_2020-06-30_17-54-02

In µTask logs I can see that my condition is not matched:

AfterRun: Step [checkPlaybookStatus] condition eval: Condition not met: expected pending to be found in list of acceptable values"

I tried to play with the format of the expected: entry in my template (using a yaml list; using another delimiter than , and specify it in list_separator; etc.) without success. The only solution I found is to divide this condition in 3 different if conditions trying to match the value with the EQ operator. It works, but using the IN operator would be much better.

Am I doing it wrong, or am I missing something in my template config ? Or is it a bug with the IN / NOTIN condition operators ?

To Reproduce
Steps to reproduce the behavior:

  1. Have a template step that generate an output
  2. Add a conditions block of type check with an IN operator; having at least one of the items in the expected: list that match your value
  3. See the log Condition not met

Expected behavior
The condition should be matched.

utask version impacted by the bug
v1.5.0

Context which you are currently running utask
Running inside a Docker container, built with this repo Quick Start. Not using any specific plugin.

template with uppercase can't be updated from dir

  • I'm submitting a ...

    • bug report
    • feature request
  • What is the current behavior?
    When using a template with some uppercase inside the name, template is being normalized before inserted inside database.
    When the template already exists in database and needs to be updated from the template directory, template is not normalized and leads to this error:
    FATA[2020-01-15T11:29:24+01:00] pq: duplicate key value violates unique constraint "task_template_name_key"

  • What is the expected behavior?
    No errors.

  • Which version of uTask are you using?
    v1.2.1

foreach with auto-dependency

Is your feature request related to a problem? Please describe.
When I'm using foreach, all steps are created without dependency and not sequentially.

Describe the solution you'd like
I would like to be able to create my steps sequentially, with a dependency created on the previous step.

...
variables:
  - name: my-groups
    value: '["group1", "group2"]'

steps:
  deploy-groups:
    (new)foreach: '{{ eval `my-groups` }}'
    description: Dummy
    action:
      type: echo
      configuration:
        output: '{{ .iterator }}'

This will create deploy-groups-0 and deploy-groups-1. I want that deploy-groups-0 is executed before deploy-groups-1 and that deploy-groups-1 is dependent of deploy-groups-0 (if deploy-groups-0 fails, deploy-groups-1 is not executed).

Describe alternatives you've considered
No alternative found.

Additional context
N/A

Periodic task run (cron)

Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

I'd like to be able to configure periodic execs for a given template + set of input.

Describe the solution you'd like
A clear and concise description of what you want to happen.

Periodic execs definition:

  • every X minute/hour/day
  • or everyday at 3pm
  • or every 3rd of each month at 2pm
  • or ...
    with template: create-new-thing-XXX
    with inputs:
    foo: yyy
    baz: xxxx
    bar: zzzz

Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.

Additional context
Add any other context or screenshots about the feature request here.

Step grouping in blocks

In task templates, some sequences of steps tend to form natural clusters, ie units of work that have a meaning as a group
The ability to "collapse" those steps into a single block would allow for better rendering of an entire task, a clearer visualization of the whole

Multi-file templates

As a task template author, expressing a process in a single yaml file limits its overall complexity
Having the possibility of spreading and organizing many actions across several files would allow for more structured code
Design TBD (import modules from a "root" template? unmarshal several yaml files onto the same object? stitch their text content naively? etc...)

Allow requester/watchers to see step outputs

Is your feature request related to a problem? Please describe.
We use uTask in our deployment software. We store some technical informations in step results but watchers are not allowed to see data.

Describe the solution you'd like
For a specific template (or maybe specific step), I would like a flag which allows watcher to see output.

Describe alternatives you've considered
The only alternative is to set all watchers as resolver...

InitializerPlugin documentation

Hi,

I am trying to implement an InitializerPlugin for http header token authentication.
I find documentation not clear enough and really struggling with this one.
The documentation of the plugins us much more clearer and lots of examples exist.
Would really appreciate simple example or additional information for the documentation.

ssh: add timeout configuration

Describe the solution you'd like
Allow timeout configuration on ssh builtin plugin to control the maximum time that can be spent waiting for the end of the execution of the ssh command.

subtask: fail to import multiples templates at first start

  • I'm submitting a ...

    • bug report
    • feature request
  • What is the current behavior?
    When importing templates at startup, if templates contains some subtasks, the subtask template name needs to already exists in database to be added.
    In case of a first deployment, if we have two templates like this:

name: hello-world
steps:
    say-hello-world:
        action:
            type: subtask
            configuration:
                template: say-something
                input:
                    who: 'rbeuque'
                    what: 'Hello, world!'
name: say-something
inputs:
  - name: who
    description: Who
  - name: what
    description: What
steps:
    say-it:
        action:
            type: echo
            configuration:
              output:
                who: '{{.input.who}}'
                what: '{{.input.what}}'

hello-world.yaml will be imported first, but will fail with errors, as template contain a subtask, calling template say-something, which haven't been imported yet.

  • What is the expected behavior?
    Check all template name that will be imported during the batch before performing validation.

  • Which version of uTask are you using?
    v1.2.2

notification over a webhook URL

Describe the solution you'd like
To integrate easily different notification system, we could add a "webhook" notify package, where utask admins could declare the URL, method and input body, using templating to replace value and format the message

Can't update a task with a _utask_parent_task_id tag

Describe the bug
When a subtask is spawned, a _utask_parent_task_id tag is added on the subtask.
This tag is read-only, because we don't want users to change it, as it could also to re-run arbitrary task.
This security prevents any modification of the task, meaning we can't change the inputs or everything else.

Expected behavior
Allow edition of the task, but make sure we can't change _utask_parent_task_id.

utask version impacted by the bug
v1.8.2+
Introduced by 70cbec9

Allow golang template in template field of subtask action

Is your feature request related to a problem? Please describe.
In a template, i want to exexcute a subtask with a name provided as input of the main template.

Describe the solution you'd like

...
inputs:
...
- name: installation_template
  description: Installation template
- name: installation_watchers
  description: Installation task watchers
  collection: true
...
steps:
...
  install-servers:
    foreach: '{{ field `step` `wait-scale` `output` `result` `result` | toJson }}'
    description: Follow server installation and do post-installation steps
    action:
      type: subtask
      configuration:
        template: '{{ .input.installation_template }}'
        input:
          ...
        watcher_usernames: '{{ .input.installation_watchers | toJson }}'
...

Need better error messages

As a µTask user debugging is hard on two counts:

  • validity check on templates doesn't give enough context (could be fixed by #5 run within docker build)
  • error messages on blocked tasks lack aren't clear enough

Group support

We should have a group support to handle permissions access from a LDAP, SSO, ...

allowed_resolvers_groups on templates for example.
admin_groups on configuration for example.

Flatten variables inside a resolution

Variables are linked to a tasktemplate and not to a resolution: when there is some modification needed in the variable, it requires either to re-prod a new µTask version, or to search/replace all occurrences of the variable by the correct value, which is not possible everywhere.

Action plugin: script

Implement a plugin "script" that forks and executes a script within the utask container.

Suggestion:

    type: script
    configuration:
        file: check-invoice.py
        argv:
            - {{.step.getInvoice.output.invoiceID}}
            - ...
        stdin: foobar
        timeout: 5s

This should execute for the step the script check-invoice.py, which needs to be present in a configured script folder within the utask container.
The script should be executable (+x), and use any interpreter as long as it is present in the utask container. The base utask image should maybe provide the most common interpreters?

There should be a way to pass data to the following steps in a structured way, it may adopt a similar strategy as the ssh plugin: the last line of the script's stdout will be json parsed, and if it succeeds will be used as the result payload for the step.

templating: field operator pipe support

Describe the solution you'd like
field templating function only looks inside the templating global context when called.
In case we want to use it in a pipe context, we should take as the first parameter the context on which to apply.

Example: (context)

"step":
  "first":
    "output": 
       "result": 
           "my-payload": "{\"common-name\":\"utask.example.org\",\"id\":32}"

To retrieve the common_name key, we could use templating like this:

{{ field `step` `first` `output` `result` `my-payload` | fromJson | fieldFrom `common-name` }}

The fieldFrom operator would take the context from the first argument, instead of the global task/resolution context; hence enabling the pipe operator to work as expected.

InitializerPlugin type assertion failure

Describe the bug
Failed to assert type of plugin 'myinit.so': expected InitializerPlugin got *plugins.InitializerPlugin

To Reproduce
Steps to reproduce the behavior:

wget https://github.com/ovh/utask/releases/latest/download/install-utask.sh
sh install-utask.sh
mkdir init/myplugin
// init/myplugin/main.go

package main                                                                  
                                                                              
import "github.com/ovh/utask/pkg/plugins"                                     
                                                                              
var Plugin = NewMyPlugin()                                                    
                                                                              
type MyPlugin struct{}                                                        
                                                                              
func NewMyPlugin() plugins.InitializerPlugin            { return &MyPlugin{} }
func (p *MyPlugin) Init(service *plugins.Service) error { return nil }        
func (p *MyPlugin) Description() string                 { return "myplugin" } 
docker-compose build
docker-compose up
Starting utask-test_db_1 ... done
Recreating utask-test_utask_1 ... done
Attaching to utask-test_db_1, utask-test_utask_1
db_1     | LOG:  database system was shut down at 2020-05-23 15:57:11 UTC
db_1     | LOG:  MultiXact member wraparound protections are now enabled
utask_1  | wait-for-it.sh: waiting 15 seconds for db:5432
utask_1  | wait-for-it.sh: db:5432 is available after 0 seconds
utask_1  | time="2020-05-23T15:59:18Z" level=fatal msg="failed to assert type of plugin 'myinit.so': expected InitializerPlugin got *plugins.InitializerPlugin"
db_1     | LOG:  database system is ready to accept connections
db_1     | LOG:  autovacuum launcher started
db_1     | LOG:  incomplete startup packet
utask-test_utask_1 exited with code 1

Expected behavior
It should pass the interface type assertion.

utask version impacted by the bug
v1.6.0

Context which you are currently running utask
Fresh installation from install-utask.sh

Additional context
Got the same error on Mac and Ubuntu.

base_output: useless with array results

  • I'm submitting a ...

    • bug report
    • feature request
  • What is the current behavior?
    if I configure

base_output:
    foo: bar

for a step that returns an array, my base output is ignored.

  • What is the expected behavior?
    I'd like to be able to access my base output (important to keep local context in foreach step chains).
    This could mean that the base_output should be injected in each item of the result array? Then how does this work if the result array is a list of scalars (ie, list of IDs) ?

  • Which version of uTask are you using?
    1.1.0

  • Please tell us about your environment

  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)

Task archival

As a µTask operator, I'd like to limit the amount of finished tasks that are left in my "live" table: this could hit the performance of the engine when collecting unfinished tasks.

The task expiration parameter in µTask's configuration allows me to set a period after which they get deleted

However, I'd still like to keep a trace of those tasks, for auditing purposes, and it is practical for me that they remain within µTask, to leverage its security guarantees

Hence, an option to "archive" finished tasks instead of outright deleting them would be valuable

Design TBD (a copy of the task and resolution tables? a new set of API endpoints to retrieve this data?)

Automate the release process

Currently we need to use some commands to release µTask.
We want to automate the release process directly through the CI.

Remove travis-ci build

travis-ci.org is closing, we need to remove build from travis and migrate all the build/test pipeline on our CDS instance.

OSS must have

  • Transfer examples in a specific folder
  • Init Travis CI
  • Move.shfiles to hack folder, Kubernetes style
  • Init code coverage badge
  • Complete README draft
  • Add SC

Make tasks searchable

Finding one particular task on a long list is difficult. Some kind of search/filter primitive at the API level would be useful not to burden the graphical client.

As execution payloads are encrypted, some tag/label system could be devised to add custom metadata on top of current generated metadata (creation date, requester, state etc...)

foreach + subtask breaks the templating engine

  • I'm submitting a ...

    • bug report
    • feature request
  • What is the current behavior?
    https://github.com/ovh/utask/blob/master/pkg/plugins/builtin/subtask/subtask.go#L43
    The subtask plugin uses an internal context object to exchange data between the main controller goroutine and itself. The templating instructions are generated in a Sprintf here, and results in e.g. {{.step.foobar.output}}.
    In the case of a foreach child step, the generated template instruction becomes {{.step.foobar-0.output}}, which makes the templating engine very unhappy because of the presence of a hyphen: failed to template context: Templating error: template: tmpl:1: bad character U+002D '-'.
    This is a common issue with texttemplate, see moby/moby#35886 helm/helm#2192 gohugoio/hugo#1474 etc.

  • What is the expected behavior?
    The templating generation should be more cautious to avoid generating invalid templating instructions in that case.

  • Which version of uTask are you using?
    1.1

  • Please tell us about your environment

  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)

Slack notifications

Implement slack integration: let µTask send notifications over slack chans.
Current available integrations: TaT (github.com/ovh/tat)

Add resolver inputs into GET /task/<id>

When a task requires resolver input to be resolved, resolver inputs should be fetched from the template.
We should provide a quick way to see what resolver inputs are needed to simplify creating the resolution.

Kafka-triggered tasks

As a µtask operator, I would like for task templates to listen to a kafka topic and derive inputs from the contents of messages on that topic, in order to trigger a new process execution.

subtask plugin doesn't follow TO_RETRY directives

Describe the bug
When a subtask step is set to TO_RETRY by a child step, the subtask is not restarted and step pass directly to DONE.
It should spawn a new subtask, to retry the subtask execution, and not re-use the old spawned task.

Expected behavior
New subtask is started.

utask version impacted by the bug
All

install-utask.sh is not correctly updated

  • I'm submitting a ...

    • [ x] bug report
  • Do you want to request a feature or report a bug?
    Bug report
    install-utask.sh provided is not correctly updated when we download it :
    Steps to reproduce :

wget https://github.com/ovh/utask/releases/latest/download/install-utask.sh
docker-compose up
  • What is the current behavior?
docker-compose up
Starting utask_db_1    ... done
Starting utask_utask_1 ... done
Attaching to utask_utask_1, utask_db_1
utask_1  | time="2019-11-21T08:24:07Z" level=info msg="[DatabaseConfig] Using 50 max open connections, 30 max idle connections, 60 seconds timeout"
utask_1  | time="2019-11-21T08:24:08Z" level=fatal msg="dial tcp 172.18.0.3:5432: connect: connection refused"
db_1     | LOG:  database system was shut down at 2019-11-21 08:24:00 UTC
  • What is the expected behavior?
    Application is starting

  • Which version of uTask are you using?
    Latest version - 1.0.1 (see wget command)

  • Please tell us about your environment

lsb_release -d
Description:	Ubuntu 18.04.2 LTS
  • Other information
    This commit is not correctly applied to install-task.sh provided
    8ecc5dd

In docker-compose.yaml :

    depends_on:
      - db

is missing in utask section

After adding this part, application is starting correctly

--
I am not familiar on how install-script.sh is generated for the release, but if you have few guidelines, I will be glad to open a PR

releasing: go-releaser should push install-utask.sh

When releasing a new version of utask, goreleaser is not pushing install-utask.sh, which breaks our installation procedure which is:

mkdir utask && cd utask
wget https://github.com/ovh/utask/releases/latest/download/install-utask.sh
sh install-utask.sh
docker-compose up

We should have a way to push this file automatically to the release, in order to avoid breaking while releasing.

More flexible subtask plugin

Is your feature request related to a problem? Please describe.
When uTask invokes an other template as a subtask, the step is not very relevant. Its state is SERVER_ERROR while the template is running and if watchers list is not defined, it is a sort of black-box for them.

Describe the solution you'd like
We could be execute a subtask with two modes:

  1. Mode 1: With the current behavior, but with a better status (#181) and a easy way to view subtask details (a link or something like that)

  2. Mode 2: Subtask steps could be included in the main template (recursive behavior) and with this mode, subtask mecanism is hidden for the users.

Describe alternatives you've considered
In all cases, an option which define watchers list with parent watchers list could be fine, particullary when we have subtasks of subtasks.

foreach + subtask breaks the templating engine

Note: for support questions, erase this form. Otherwise use approriate areas..

  • I'm submitting a ...

    • bug report
    • feature request
  • What is the current behavior?
    https://github.com/ovh/utask/blob/master/pkg/plugins/builtin/subtask/subtask.go#L43
    The subtask plugin uses an internal context object to exchange data between the main controller goroutine and itself.
    In #54, we fixed a problem regarding the usage of hiphens inside the template (for the stepName). Now that we fixed it, a new bug has been introduced as the context is being json.Marshal, then, the double-quotes get escaped, which breaks the templating with error:
    failed to template context: Templating error: template: tmpl:1: unexpected "\\" in operand
    Faulty context example:
    {"task_id":"{{ if (index .step \"subTask-1\" \"output\") }}{{ index .step \"subTask-1\" \"output\" \"id\" }}{{ end }}","requester_username":"{{.task.requester_username}}"}

  • Which version of uTask are you using?
    v1.2.0

  • Internal reference
    CORDEV-138

http module: handle cursor pagination

Is your feature request related to a problem? Please describe.
When using an API that do cursor pagination, we can't use the http action to range over multiple cursors and save all datas

Describe the solution you'd like
We should have a special package to range over all pages, or some settings in the current http package to configure the pagination behaviour

feat: make the error value of a step available

Note: for support questions, erase this form. Otherwise use approriate areas..

  • I'm submitting a ...

    • bug report
    • feature request
  • Do you want to request a feature or report a bug?

Feature request/enhancement.

  • What is the current behavior?

The error value of a step action is not accessible, and cannot be used in a check.

  • What is the expected behavior?

Given the following step with conditions, I would like to be able to access to the error value so that it can be used with the REGEXP operator in a check.

    myStep:
        dependencies: [myDep]
        description : Loreum Ipsum
        action:
            type: toto
        conditions:
            - type: check
              if:
              - value: '{{.step.anotherStep.error}}'
                operator: regexp
                expected: '.*error message.*'
              then:
                this: DONE
              message: foobar
  • Which version of uTask are you using?

Latest.

HTTP plugin: missing params

Enrich the http action plugin with the following options:

  • timeout of the request
  • auth enable basic auth
  • deny redirects
  • http parameters

How to ...

Hi,

Copuld you please provide an example for the following flow:

  1. Execute request that starts some procees --> this one i know how to do
  2. Loop and check above process until it is done.

Pseudo code:
execute_process
while [ true ]
do
check_if_process_is_done
if [ done ] {
return success
} else {
sleep 20 seconds
check_if_process_is_done
}

Make UI proxypass aware

  • I'm submitting a ...

    • bug report
    • feature request
  • What is the current behavior?
    When utask is being deployed behind a ProxyPass with a path prefix, UI doesn't behave correctly because it doesn't have the correct base URL for it's own files, and to call API.

  • What is the expected behavior?
    To be able to configure a path prefix while working behind a proxypass

  • Which version of uTask are you using?
    v1.2.0

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.