chore(): small pkg refactoring

This commit is contained in:
Nicolas Carlier 2020-02-26 21:19:11 +00:00
parent 43204677d3
commit c21443c10e
16 changed files with 65 additions and 48 deletions

View File

@ -75,6 +75,9 @@ You can override the default using the `WHD_SCRIPTS` environment variable or `-s
Note that Webhookd is able to run any type of file in this directory as long as the file is executable. Note that Webhookd is able to run any type of file in this directory as long as the file is executable.
For example, you can execute a Node.js file if you give execution rights to the file and add the appropriate `#!` header (in this case: `#!/usr/bin/env node`). For example, you can execute a Node.js file if you give execution rights to the file and add the appropriate `#!` header (in this case: `#!/usr/bin/env node`).
You can find sample scripts in the [example folder](./scripts/examples).
In particular, examples of integration with Gitlab and Github.
### Webhook URL ### Webhook URL
The directory structure define the webhook URL. The directory structure define the webhook URL.
@ -308,7 +311,7 @@ $ curl -X POST \
-H 'Date: <req-date>' \ -H 'Date: <req-date>' \
-H 'Signature: keyId=<key-id>,algorithm="rsa-sha256",headers="(request-target) date",signature=<signature-string>' \ -H 'Signature: keyId=<key-id>,algorithm="rsa-sha256",headers="(request-target) date",signature=<signature-string>' \
-H 'Accept: application/json' \ -H 'Accept: application/json' \
"http://loclahost:8080/echo?msg=hello" "http://localhost:8080/echo?msg=hello"
``` ```
You can find a small HTTP client in the ["tooling" directory](./tooling/httpsig/README.md) that is capable of forging HTTP signatures. You can find a small HTTP client in the ["tooling" directory](./tooling/httpsig/README.md) that is capable of forging HTTP signatures.
@ -345,4 +348,10 @@ On *nix, if you want to listen on ports 80 and 443, don't forget to use `setcap`
sudo setcap CAP_NET_BIND_SERVICE+ep webhookd sudo setcap CAP_NET_BIND_SERVICE+ep webhookd
``` ```
## License
The MIT License (MIT)
See [LICENSE](./LICENSE) to see the full text.
--- ---

View File

@ -1,4 +1,4 @@
package tools package api
import ( import (
"bytes" "bytes"

View File

@ -13,7 +13,6 @@ import (
"github.com/ncarlier/webhookd/pkg/config" "github.com/ncarlier/webhookd/pkg/config"
"github.com/ncarlier/webhookd/pkg/logger" "github.com/ncarlier/webhookd/pkg/logger"
"github.com/ncarlier/webhookd/pkg/model" "github.com/ncarlier/webhookd/pkg/model"
"github.com/ncarlier/webhookd/pkg/tools"
"github.com/ncarlier/webhookd/pkg/worker" "github.com/ncarlier/webhookd/pkg/worker"
) )
@ -58,7 +57,7 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
// Get script location // Get script location
p := strings.TrimPrefix(r.URL.Path, "/") p := strings.TrimPrefix(r.URL.Path, "/")
script, err := tools.ResolveScript(scriptDir, p) script, err := worker.ResolveScript(scriptDir, p)
if err != nil { if err != nil {
logger.Error.Println(err.Error()) logger.Error.Println(err.Error())
http.Error(w, err.Error(), http.StatusNotFound) http.Error(w, err.Error(), http.StatusNotFound)
@ -72,8 +71,8 @@ func triggerWebhook(w http.ResponseWriter, r *http.Request) {
return return
} }
params := tools.QueryParamsToShellVars(r.URL.Query()) params := QueryParamsToShellVars(r.URL.Query())
params = append(params, tools.HTTPHeadersToShellVars(r.Header)...) params = append(params, HTTPHeadersToShellVars(r.Header)...)
// logger.Debug.Printf("API REQUEST: \"%s\" with params %s...\n", p, params) // logger.Debug.Printf("API REQUEST: \"%s\" with params %s...\n", p, params)
@ -119,7 +118,7 @@ func getWebhookLog(w http.ResponseWriter, r *http.Request) {
// Get script location // Get script location
name := path.Dir(strings.TrimPrefix(r.URL.Path, "/")) name := path.Dir(strings.TrimPrefix(r.URL.Path, "/"))
_, err := tools.ResolveScript(scriptDir, name) _, err := worker.ResolveScript(scriptDir, name)
if err != nil { if err != nil {
logger.Error.Println(err.Error()) logger.Error.Println(err.Error())
http.Error(w, err.Error(), http.StatusNotFound) http.Error(w, err.Error(), http.StatusNotFound)

View File

@ -1,12 +1,12 @@
package tools_test package test
import ( import (
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
"github.com/ncarlier/webhookd/pkg/api"
"github.com/ncarlier/webhookd/pkg/assert" "github.com/ncarlier/webhookd/pkg/assert"
"github.com/ncarlier/webhookd/pkg/tools"
) )
func TestQueryParamsToShellVars(t *testing.T) { func TestQueryParamsToShellVars(t *testing.T) {
@ -14,7 +14,7 @@ func TestQueryParamsToShellVars(t *testing.T) {
"string": []string{"foo"}, "string": []string{"foo"},
"list": []string{"foo", "bar"}, "list": []string{"foo", "bar"},
} }
values := tools.QueryParamsToShellVars(tc) values := api.QueryParamsToShellVars(tc)
assert.ContainsStr(t, "string=foo", values, "") assert.ContainsStr(t, "string=foo", values, "")
assert.ContainsStr(t, "list=foo,bar", values, "") assert.ContainsStr(t, "list=foo,bar", values, "")
} }
@ -24,7 +24,7 @@ func TestHTTPHeadersToShellVars(t *testing.T) {
"Content-Type": []string{"text/plain"}, "Content-Type": []string{"text/plain"},
"X-Foo-Bar": []string{"foo", "bar"}, "X-Foo-Bar": []string{"foo", "bar"},
} }
values := tools.HTTPHeadersToShellVars(tc) values := api.HTTPHeadersToShellVars(tc)
assert.ContainsStr(t, "content_type=text/plain", values, "") assert.ContainsStr(t, "content_type=text/plain", values, "")
assert.ContainsStr(t, "x_foo_bar=foo,bar", values, "") assert.ContainsStr(t, "x_foo_bar=foo,bar", values, "")
} }

View File

@ -1,15 +0,0 @@
package auth
import (
"testing"
"github.com/ncarlier/webhookd/pkg/assert"
)
func TestValidateCredentials(t *testing.T) {
htpasswdFile, err := NewHtpasswdFromFile("test.htpasswd")
assert.Nil(t, err, ".htpasswd file should be loaded")
assert.NotNil(t, htpasswdFile, ".htpasswd file should be loaded")
assert.Equal(t, true, htpasswdFile.validateCredentials("foo", "bar"), "credentials should be valid")
assert.Equal(t, false, htpasswdFile.validateCredentials("foo", "bir"), "credentials should not be valid")
}

View File

@ -0,0 +1,23 @@
package test
import (
"net/http"
"testing"
"github.com/ncarlier/webhookd/pkg/assert"
"github.com/ncarlier/webhookd/pkg/auth"
)
func TestValidateCredentials(t *testing.T) {
htpasswdFile, err := auth.NewHtpasswdFromFile("test.htpasswd")
assert.Nil(t, err, ".htpasswd file should be loaded")
assert.NotNil(t, htpasswdFile, ".htpasswd file should be loaded")
req, err := http.NewRequest("POST", "http://localhost:8080", nil)
assert.Nil(t, err, "")
req.SetBasicAuth("foo", "bar")
assert.Equal(t, true, htpasswdFile.Validate(req), "credentials should be valid")
req.SetBasicAuth("foo", "bad")
assert.Equal(t, false, htpasswdFile.Validate(req), "credentials should not be valid")
}

View File

@ -1,4 +1,4 @@
package tools package worker
import ( import (
"errors" "errors"

View File

@ -1,32 +1,32 @@
package tools_test package test
import ( import (
"testing" "testing"
"github.com/ncarlier/webhookd/pkg/assert" "github.com/ncarlier/webhookd/pkg/assert"
"github.com/ncarlier/webhookd/pkg/tools" "github.com/ncarlier/webhookd/pkg/worker"
) )
func TestResolveScript(t *testing.T) { func TestResolveScript(t *testing.T) {
script, err := tools.ResolveScript("../../scripts", "../scripts/echo") script, err := worker.ResolveScript("../../../scripts", "../scripts/echo")
assert.Nil(t, err, "") assert.Nil(t, err, "")
assert.Equal(t, "../../scripts/echo.sh", script, "") assert.Equal(t, "../../../scripts/echo.sh", script, "")
} }
func TestNotResolveScript(t *testing.T) { func TestNotResolveScript(t *testing.T) {
_, err := tools.ResolveScript("../../scripts", "foo") _, err := worker.ResolveScript("../../scripts", "foo")
assert.NotNil(t, err, "") assert.NotNil(t, err, "")
assert.Equal(t, "Script not found: ../../scripts/foo.sh", err.Error(), "") assert.Equal(t, "Script not found: ../../scripts/foo.sh", err.Error(), "")
} }
func TestResolveBadScript(t *testing.T) { func TestResolveBadScript(t *testing.T) {
_, err := tools.ResolveScript("../../scripts", "../tests/test_simple") _, err := worker.ResolveScript("../../scripts", "../tests/test_simple")
assert.NotNil(t, err, "") assert.NotNil(t, err, "")
assert.Equal(t, "Invalid script path: ../tests/test_simple.sh", err.Error(), "") assert.Equal(t, "Invalid script path: ../tests/test_simple.sh", err.Error(), "")
} }
func TestResolveScriptWithExtension(t *testing.T) { func TestResolveScriptWithExtension(t *testing.T) {
_, err := tools.ResolveScript("../../scripts", "node.js") _, err := worker.ResolveScript("../../scripts", "node.js")
assert.NotNil(t, err, "") assert.NotNil(t, err, "")
assert.Equal(t, "Script not found: ../../scripts/node.js", err.Error(), "") assert.Equal(t, "Script not found: ../../scripts/node.js", err.Error(), "")
} }

View File

@ -1,4 +1,4 @@
package worker package test
import ( import (
"os" "os"
@ -8,6 +8,7 @@ import (
"github.com/ncarlier/webhookd/pkg/assert" "github.com/ncarlier/webhookd/pkg/assert"
"github.com/ncarlier/webhookd/pkg/logger" "github.com/ncarlier/webhookd/pkg/logger"
"github.com/ncarlier/webhookd/pkg/model" "github.com/ncarlier/webhookd/pkg/model"
"github.com/ncarlier/webhookd/pkg/worker"
) )
func printWorkMessages(work *model.WorkRequest) { func printWorkMessages(work *model.WorkRequest) {
@ -24,7 +25,7 @@ func printWorkMessages(work *model.WorkRequest) {
func TestWorkRunner(t *testing.T) { func TestWorkRunner(t *testing.T) {
logger.Init("debug") logger.Init("debug")
script := "../../tests/test_simple.sh" script := "./test_simple.sh"
args := []string{ args := []string{
"name=foo", "name=foo",
"user_agent=test", "user_agent=test",
@ -33,14 +34,14 @@ func TestWorkRunner(t *testing.T) {
work := model.NewWorkRequest("test", script, payload, os.TempDir(), args, 5) work := model.NewWorkRequest("test", script, payload, os.TempDir(), args, 5)
assert.NotNil(t, work, "") assert.NotNil(t, work, "")
printWorkMessages(work) printWorkMessages(work)
err := run(work) err := worker.Run(work)
assert.Nil(t, err, "") assert.Nil(t, err, "")
assert.Equal(t, work.Status, model.Success, "") assert.Equal(t, work.Status, model.Success, "")
assert.Equal(t, work.GetLogContent("notify:"), "OK\n", "") assert.Equal(t, work.GetLogContent("notify:"), "OK\n", "")
// Test that we can retrieve log file afterward // Test that we can retrieve log file afterward
id := strconv.FormatUint(work.ID, 10) id := strconv.FormatUint(work.ID, 10)
logFile, err := RetrieveLogFile(id, "test", os.TempDir()) logFile, err := worker.RetrieveLogFile(id, "test", os.TempDir())
defer logFile.Close() defer logFile.Close()
assert.Nil(t, err, "Log file should exists") assert.Nil(t, err, "Log file should exists")
assert.NotNil(t, logFile, "Log file should be retrieve") assert.NotNil(t, logFile, "Log file should be retrieve")
@ -48,11 +49,11 @@ func TestWorkRunner(t *testing.T) {
func TestWorkRunnerWithError(t *testing.T) { func TestWorkRunnerWithError(t *testing.T) {
logger.Init("debug") logger.Init("debug")
script := "../../tests/test_error.sh" script := "./test_error.sh"
work := model.NewWorkRequest("test", script, "", os.TempDir(), []string{}, 5) work := model.NewWorkRequest("test", script, "", os.TempDir(), []string{}, 5)
assert.NotNil(t, work, "") assert.NotNil(t, work, "")
printWorkMessages(work) printWorkMessages(work)
err := run(work) err := worker.Run(work)
assert.NotNil(t, err, "") assert.NotNil(t, err, "")
assert.Equal(t, work.Status, model.Error, "") assert.Equal(t, work.Status, model.Error, "")
assert.Equal(t, "exit status 1", err.Error(), "") assert.Equal(t, "exit status 1", err.Error(), "")
@ -60,11 +61,11 @@ func TestWorkRunnerWithError(t *testing.T) {
func TestWorkRunnerWithTimeout(t *testing.T) { func TestWorkRunnerWithTimeout(t *testing.T) {
logger.Init("debug") logger.Init("debug")
script := "../../tests/test_timeout.sh" script := "./test_timeout.sh"
work := model.NewWorkRequest("test", script, "", os.TempDir(), []string{}, 1) work := model.NewWorkRequest("test", script, "", os.TempDir(), []string{}, 1)
assert.NotNil(t, work, "") assert.NotNil(t, work, "")
printWorkMessages(work) printWorkMessages(work)
err := run(work) err := worker.Run(work)
assert.NotNil(t, err, "") assert.NotNil(t, err, "")
assert.Equal(t, work.Status, model.Error, "") assert.Equal(t, work.Status, model.Error, "")
assert.Equal(t, "signal: killed", err.Error(), "") assert.Equal(t, "signal: killed", err.Error(), "")

View File

@ -23,7 +23,8 @@ func (c *ChanWriter) Write(p []byte) (int, error) {
return len(p), nil return len(p), nil
} }
func run(work *model.WorkRequest) error { // Run work request
func Run(work *model.WorkRequest) error {
work.Status = model.Running work.Status = model.Running
logger.Info.Printf("job %s#%d started...\n", work.Name, work.ID) logger.Info.Printf("job %s#%d started...\n", work.Name, work.ID)
logger.Debug.Printf("job %s#%d script: %s\n", work.Name, work.ID, work.Script) logger.Debug.Printf("job %s#%d script: %s\n", work.Name, work.ID, work.Script)

View File

@ -10,16 +10,15 @@ import (
"github.com/ncarlier/webhookd/pkg/notification" "github.com/ncarlier/webhookd/pkg/notification"
) )
// NewWorker creates, and returns a new Worker object. Its only argument // NewWorker creates, and returns a new Worker object.
// is a channel that the worker can add itself to whenever it is done its
// work.
func NewWorker(id int, workerQueue chan chan model.WorkRequest) Worker { func NewWorker(id int, workerQueue chan chan model.WorkRequest) Worker {
// Create, and return the worker. // Create, and return the worker.
worker := Worker{ worker := Worker{
ID: id, ID: id,
Work: make(chan model.WorkRequest), Work: make(chan model.WorkRequest),
WorkerQueue: workerQueue, WorkerQueue: workerQueue,
QuitChan: make(chan bool)} QuitChan: make(chan bool),
}
return worker return worker
} }
@ -45,7 +44,7 @@ func (w Worker) Start() {
// Receive a work request. // Receive a work request.
logger.Debug.Printf("worker #%d received job request: %s#%d\n", w.ID, work.Name, work.ID) logger.Debug.Printf("worker #%d received job request: %s#%d\n", w.ID, work.Name, work.ID)
metric.Requests.Add(1) metric.Requests.Add(1)
err := run(&work) err := Run(&work)
if err != nil { if err != nil {
metric.RequestsFailed.Add(1) metric.RequestsFailed.Add(1)
work.MessageChan <- []byte(fmt.Sprintf("error: %s", err.Error())) work.MessageChan <- []byte(fmt.Sprintf("error: %s", err.Error()))