Skip to content

Commit

Permalink
feat: Implement script plugin (#46)
Browse files Browse the repository at this point in the history
Signed-off-by: Valentin Pichard <[email protected]>
  • Loading branch information
w3st3ry authored and pablito-perez committed Dec 17, 2019
1 parent 3515d5b commit 4aaae74
Show file tree
Hide file tree
Showing 16 changed files with 327 additions and 4 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@ ui/editor/node_modules
config/*
!config/README.md

# Related to scripts
scripts/*
!scripts/.gitkeep

# Related to Goreleaser
dist/
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ RUN BASEHREF=/ui/dashboard/ PREFIX_API_BASE_URL=/ make build-prod
WORKDIR /home/node/ui/editor
RUN BASEHREF=/ui/editor/ make build-prod

FROM golang:1.12
FROM golang:1.13-buster

COPY . /go/src/github.com/ovh/utask
WORKDIR /go/src/github.com/ovh/utask
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ BINARY = utask
MAIN_LOCATION = ./cmd

TEST_LOCATION = ./...
TEST_CMD = go test -v -mod=vendor -cover ${TEST_LOCATION}
TEST_CMD = go test -count=1 -v -mod=vendor -cover ${TEST_LOCATION}
TEST_CMD_COV = ${TEST_CMD} -covermode=count -coverprofile=coverage.out

VERSION := $(shell git describe --exact-match --abbrev=0 --tags $(git rev-list --tags --max-count=1) 2> /dev/null)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ Browse [builtin actions](./pkg/plugins/builtin)
|**`ssh`** | Connect to a remote system and run commands on it | [Access plugin doc](./pkg/plugins/builtin/ssh/README.md)
|**`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

Expand Down
2 changes: 2 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/ovh/utask/pkg/auth"
"github.com/ovh/utask/pkg/now"
"github.com/ovh/utask/pkg/plugins/builtin/echo"
"github.com/ovh/utask/pkg/plugins/builtin/script"
"github.com/ovh/utask/pkg/utils"
)

Expand All @@ -49,6 +50,7 @@ func TestMain(m *testing.M) {
logrus.SetLevel(logrus.ErrorLevel)

step.RegisterRunner(echo.Plugin.PluginName(), echo.Plugin)
step.RegisterRunner(script.Plugin.PluginName(), script.Plugin)

db.Init(store)

Expand Down
6 changes: 6 additions & 0 deletions cmd/utask/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ const (
defaultInitializersFolder = "./init"
defaultPluginFolder = "./plugins"
defaultTemplatesFolder = "./templates"
defaultScriptsFolder = "./scripts"
defaultRegion = "default"
defaultPort = 8081

envInit = "INIT"
envPlugins = "PLUGINS"
envTemplates = "TEMPLATES"
envScripts = "SCRIPTS"
envRegion = "REGION"
envHTTPPort = "SERVER_PORT"
envDebug = "DEBUG"
Expand All @@ -54,6 +56,7 @@ func init() {
viper.BindEnv(envInit)
viper.BindEnv(envPlugins)
viper.BindEnv(envTemplates)
viper.BindEnv(envScripts)
viper.BindEnv(envRegion)
viper.BindEnv(envHTTPPort)
viper.BindEnv(envDebug)
Expand All @@ -71,6 +74,7 @@ func init() {
flags.StringVar(&utask.FInitializersFolder, "init-path", defaultInitializersFolder, "Initializer folder absolute path")
flags.StringVar(&utask.FPluginFolder, "plugins-path", defaultPluginFolder, "Plugins folder absolute path")
flags.StringVar(&utask.FTemplatesFolder, "templates-path", defaultTemplatesFolder, "Templates folder absolute path")
flags.StringVar(&utask.FScriptsFolder, "scripts-path", defaultScriptsFolder, "Scripts folder absolute path")
flags.StringVar(&utask.FRegion, "region", defaultRegion, "Region in which instance is located")
flags.UintVar(&utask.FPort, "http-port", defaultPort, "HTTP port to expose")
flags.BoolVar(&utask.FDebug, "debug", false, "Run engine in debug mode")
Expand All @@ -79,6 +83,7 @@ func init() {
viper.BindPFlag(envInit, rootCmd.Flags().Lookup("init-path"))
viper.BindPFlag(envPlugins, rootCmd.Flags().Lookup("plugins-path"))
viper.BindPFlag(envTemplates, rootCmd.Flags().Lookup("templates-path"))
viper.BindPFlag(envScripts, rootCmd.Flags().Lookup("scripts-path"))
viper.BindPFlag(envRegion, rootCmd.Flags().Lookup("region"))
viper.BindPFlag(envHTTPPort, rootCmd.Flags().Lookup("http-port"))
viper.BindPFlag(envDebug, rootCmd.Flags().Lookup("debug"))
Expand All @@ -96,6 +101,7 @@ var rootCmd = &cobra.Command{
utask.FInitializersFolder = viper.GetString(envInit)
utask.FPluginFolder = viper.GetString(envPlugins)
utask.FTemplatesFolder = viper.GetString(envTemplates)
utask.FScriptsFolder = viper.GetString(envScripts)
utask.FRegion = viper.GetString(envRegion)
utask.FPort = viper.GetUint(envHTTPPort)
utask.FDebug = viper.GetBool(envDebug)
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ services:
- 8081:8081
volumes:
- "./templates:/app/templates:ro"
- "./scripts:/app/scripts:ro"
depends_on:
- db
db:
Expand Down
33 changes: 33 additions & 0 deletions engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -27,6 +28,7 @@ import (
"github.com/ovh/utask/models/tasktemplate"
"github.com/ovh/utask/pkg/now"
"github.com/ovh/utask/pkg/plugins/builtin/echo"
"github.com/ovh/utask/pkg/plugins/builtin/script"
)

const (
Expand All @@ -50,6 +52,7 @@ func TestMain(m *testing.M) {
}

step.RegisterRunner(echo.Plugin.PluginName(), echo.Plugin)
step.RegisterRunner(script.Plugin.PluginName(), script.Plugin)

os.Exit(m.Run())
}
Expand Down Expand Up @@ -622,6 +625,36 @@ func TestBaseOutput(t *testing.T) {
assert.Equal(t, "bar", output["foo"])
}

func TestScriptPlugin(t *testing.T) {
argv := "world"
res, err := createResolution("execScript.yaml", map[string]interface{}{"argv": argv}, nil)
assert.NotNil(t, res)
assert.Nil(t, err)

res, err = runResolution(res)
assert.NotNil(t, res)
assert.Nil(t, err)

payload := make(map[string]interface{})
payload["dumb_string"] = fmt.Sprintf("Hello %s!", argv)
payload["random_object"] = map[string]interface{}{"foo": "bar"}

metadata := script.Metadata{
ExitCode: "0",
ProcessState: "exit status 0",
Output: "Hello world script\n{\"dumb_string\":\"Hello world!\",\"random_object\":{\"foo\":\"bar\"}}\n",
ExecutionTime: "",
Error: "",
}

// because time can't be consistant through tests
metadataOutput := res.Steps["stepOne"].Metadata.(script.Metadata)
metadataOutput.ExecutionTime = ""

assert.Equal(t, payload, res.Steps["stepOne"].Payload)
assert.Equal(t, metadata, metadataOutput)
}

func TestBaseBaseConfiguration(t *testing.T) {
res, err := createResolution("base_configuration.yaml", nil, nil)
assert.NotNil(t, res)
Expand Down
6 changes: 6 additions & 0 deletions engine/scripts_tests/hello-world.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh

cat <<EOF
Hello world script
{"dumb_string":"Hello $1!","random_object":{"foo":"bar"}}
EOF
38 changes: 38 additions & 0 deletions engine/templates_tests/execScript.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: exec-script
description: Executing a shell script
title_format: "[test] a simple task for script-plugin"

inputs:
- name: argv
description: ARGV script input

steps:
stepOne:
description: first step
action:
type: script
configuration:
# ____________________
# / \
# | This is only for |
# | testing purpose |
# \____________________/
# ! !
# ! !
# L_ !
# / _)!
# / /__L
# _____/ (____)
# (____)
# _____ (____)
# \_(____)
# ! !
# ! !
# \__/
# This file param is valid only in a testing context
# In production, `file` will be prefixed by the utask.FScriptsFolder variable ("./scripts" by default)
# You can specify your file's path relative to that location
file: "./scripts_tests/hello-world.sh"
argv:
- "{{.input.argv}}"
timeout_seconds: "25"
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
Expand Down Expand Up @@ -357,4 +358,4 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
2 changes: 2 additions & 0 deletions pkg/plugins/builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
pluginhttp "github.com/ovh/utask/pkg/plugins/builtin/http"
pluginnotify "github.com/ovh/utask/pkg/plugins/builtin/notify"
pluginping "github.com/ovh/utask/pkg/plugins/builtin/ping"
pluginscript "github.com/ovh/utask/pkg/plugins/builtin/script"
pluginssh "github.com/ovh/utask/pkg/plugins/builtin/ssh"
pluginsubtask "github.com/ovh/utask/pkg/plugins/builtin/subtask"
"github.com/ovh/utask/pkg/plugins/taskplugin"
Expand All @@ -24,6 +25,7 @@ func Register() error {
pluginecho.Plugin,
pluginemail.Plugin,
pluginping.Plugin,
pluginscript.Plugin,
} {
if err := step.RegisterRunner(p.PluginName(), p); err != nil {
return err
Expand Down
70 changes: 70 additions & 0 deletions pkg/plugins/builtin/script/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# `script` plugin

This plugin execute a script.

*Warn: This plugin will keep running until the execution is done*

*Runtime(s) must be accessible on the host you deploy µTask if you want to execute interpreted scripts: [verify shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)) and available packages*

Files must be located under scripts folder, you should set exec permissions (+x). Otherwise the script plugin will try to set the exec permissions.

## Configuration

|Fields|Description
|---|---
| `file` | file name under scripts folder
| `argv` | a collection of script argv
| `timeout` | timeout of the script execution
| `stdin` | inject stdin in your script
| `last_line_not_json` | skip or not unmarshaling of last JSON line
| `allow_exit_non_zero` | allow or not non zero exit status code

## Example

An action of type `script` requires the following kind of configuration:

```yaml
action:
type: script
configuration:
# mandatory, string
# file field must be related to you scripts path (./scripts)
# and could modified /w `scripts-path` flag when you run binary
file: hello-world.sh
# optional, a collection of string
argv:
- world
# optional, string as Duration
# Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
# default is 2m
timeout: "25s"
# optional, boolean
# default is false, can't be templated
last_line_not_json: false
# optional, boolean
# default is false, can't be templated
allow_exit_non_zero: false
```
## Note
The plugin returns two objects, the `Payload` who is the last returned line of your script as json:

```json
{"dumb_string":"Hello world!","random_object":{"foo":"bar"}}
```

*Your JSON must be printed on last line*

The `Metadata` to fetch informations about plugin execution:

```js
{
"exit_code":"0",
"process_state":"exit status 0",
// Output combine Stdout and Stderr streams without any distinction
"output":"Hello world script\n{\"dumb_string\":\"Hello world!\",\"random_object\":{\"foo\":\"bar\"}}\n",
"execution_time":"846.889µs",
"error":""
}
```
Loading

0 comments on commit 4aaae74

Please sign in to comment.