Skip to content

Commit

Permalink
Merge pull request ajvb#226 from tooolbox/feature/template
Browse files Browse the repository at this point in the history
Templating Commands/Urls/Bodies
  • Loading branch information
tooolbox authored May 29, 2020
2 parents 6048be6 + 9cf4074 commit 75ac2a8
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 6 deletions.
11 changes: 11 additions & 0 deletions job/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ type Job struct {
jobTimer clock.Timer
NextRunAt time.Time `json:"next_run_at"`

// Templating delimiters, the left & right separated by space,
// for example `{{ }}` or `${ }`.
//
// If this field is non-empty, then each time this
// job is executed, Kala will template its main
// content as a Go Template with the job itself as data.
//
// The Command is templated for local jobs,
// and Url and Body in RemoteProperties.
TemplateDelimiters string

// The clock for this job; used to mock time during tests.
clk Clock

Expand Down
62 changes: 56 additions & 6 deletions job/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"os/exec"
"strings"
"text/template"
"time"

"github.com/mattn/go-shellwords"
Expand All @@ -25,9 +26,10 @@ type JobRunner struct {
}

var (
ErrJobDisabled = errors.New("Job cannot run, as it is disabled")
ErrCmdIsEmpty = errors.New("Job Command is empty.")
ErrJobTypeInvalid = errors.New("Job Type is not valid.")
ErrJobDisabled = errors.New("Job cannot run, as it is disabled")
ErrCmdIsEmpty = errors.New("Job Command is empty.")
ErrJobTypeInvalid = errors.New("Job Type is not valid.")
ErrInvalidDelimiters = errors.New("Job has invalid templating delimiters.")
)

// Run calls the appropriate run function, collects metadata around the success
Expand Down Expand Up @@ -124,10 +126,21 @@ func (j *JobRunner) RemoteRun() (string, error) {
defer cncl()
}

// Get the actual url and body we're going to be using,
// including any necessary templating.
url, err := j.tryTemplatize(j.job.RemoteProperties.Url)
if err != nil {
return "", fmt.Errorf("Error templatizing url: %v", err)
}
body, err := j.tryTemplatize(j.job.RemoteProperties.Body)
if err != nil {
return "", fmt.Errorf("Error templatizing body: %v", err)
}

// Normalize the method passed by the user
method := strings.ToUpper(j.job.RemoteProperties.Method)
bodyBuffer := bytes.NewBufferString(j.job.RemoteProperties.Body)
req, err := http.NewRequest(method, j.job.RemoteProperties.Url, bodyBuffer)
bodyBuffer := bytes.NewBufferString(body)
req, err := http.NewRequest(method, url, bodyBuffer)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -164,9 +177,16 @@ func initShParser() *shellwords.Parser {
func (j *JobRunner) runCmd() (string, error) {
j.numberOfAttempts++

// Get the actual command we're going to be running,
// including any necessary templating.
cmdText, err := j.tryTemplatize(j.job.Command)
if err != nil {
return "", fmt.Errorf("Error templatizing command: %v", err)
}

// Execute command
shParser := initShParser()
args, err := shParser.Parse(j.job.Command)
args, err := shParser.Parse(cmdText)
if err != nil {
return "", err
}
Expand All @@ -182,6 +202,36 @@ func (j *JobRunner) runCmd() (string, error) {
return strings.TrimSpace(string(out)), nil
}

func (j *JobRunner) tryTemplatize(content string) (string, error) {
delims := j.job.TemplateDelimiters

if delims == "" {
return content, nil
}

split := strings.Split(delims, " ")
if len(split) != 2 {
return "", ErrInvalidDelimiters
}

left, right := split[0], split[1]
if left == "" || right == "" {
return "", ErrInvalidDelimiters
}

t, err := template.New("tmpl").Delims(left, right).Parse(content)
if err != nil {
return "", fmt.Errorf("Error parsing template: %v", err)
}

b := bytes.NewBuffer(nil)
if err := t.Execute(b, j.job); err != nil {
return "", fmt.Errorf("Error executing template: %v", err)
}

return b.String(), nil
}

func (j *JobRunner) shouldRetry() bool {
// Check number of retries left
if j.currentRetries == 0 {
Expand Down
139 changes: 139 additions & 0 deletions job/runner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package job

import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestTemplatize(t *testing.T) {

t.Run("command", func(t *testing.T) {

t.Run("raw", func(t *testing.T) {
j := &Job{
Name: "mock_job",
Command: "echo mr.{{$.Owner}}",
Owner: "[email protected]",
}
r := JobRunner{
job: j,
}
out, err := r.LocalRun()
assert.NoError(t, err)
assert.Equal(t, "mr.{{$.Owner}}", out)
})

t.Run("templated", func(t *testing.T) {
j := &Job{
Name: "mock_job",
Command: "echo mr.{{$.Owner}}",
Owner: "[email protected]",
TemplateDelimiters: "{{ }}",
}
r := JobRunner{
job: j,
}
out, err := r.LocalRun()
assert.NoError(t, err)
assert.Equal(t, "[email protected]", out)
})

})

t.Run("url", func(t *testing.T) {

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
val := r.URL.Query().Get("val")
_, err := w.Write([]byte(val))
if err != nil {
http.Error(w, err.Error(), 400)
return
}
w.WriteHeader(200)
}))

t.Run("raw", func(t *testing.T) {
j := &Job{
Name: "mock_job",
Owner: "[email protected]",
RemoteProperties: RemoteProperties{
Url: "http://" + srv.Listener.Addr().String() + "/path?val=a_{{$.Name}}",
},
}
r := JobRunner{
job: j,
}
out, err := r.RemoteRun()
assert.NoError(t, err)
assert.Equal(t, "a_{{$.Name}}", out)
})

t.Run("templated", func(t *testing.T) {
j := &Job{
Name: "mock_job",
Owner: "[email protected]",
RemoteProperties: RemoteProperties{
Url: "http://" + srv.Listener.Addr().String() + "/path?val=a_{{$.Name}}",
},
TemplateDelimiters: "{{ }}",
}
r := JobRunner{
job: j,
}
out, err := r.RemoteRun()
assert.NoError(t, err)
assert.Equal(t, "a_mock_job", out)
})

})

t.Run("body", func(t *testing.T) {

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := ioutil.ReadAll(r.Body)
w.Write(b)
w.WriteHeader(200)
}))

t.Run("raw", func(t *testing.T) {
j := &Job{
Name: "mock_job",
Owner: "[email protected]",
RemoteProperties: RemoteProperties{
Url: "http://" + srv.Listener.Addr().String() + "/path",
Body: `{"hello": "world", "foo": "young-${$.Owner}"}`,
},
}
r := JobRunner{
job: j,
}
out, err := r.RemoteRun()
assert.NoError(t, err)
assert.Equal(t, `{"hello": "world", "foo": "young-${$.Owner}"}`, out)
})

t.Run("templated", func(t *testing.T) {
j := &Job{
Name: "mock_job",
Owner: "[email protected]",
RemoteProperties: RemoteProperties{
Url: "http://" + srv.Listener.Addr().String() + "/path",
Body: `{"hello": "world", "foo": "young-${$.Owner}"}`,
},
TemplateDelimiters: "${ }",
}
r := JobRunner{
job: j,
}
out, err := r.RemoteRun()
assert.NoError(t, err)
assert.Equal(t, `{"hello": "world", "foo": "[email protected]"}`, out)
})

})

}

0 comments on commit 75ac2a8

Please sign in to comment.