Skip to content

Commit

Permalink
Merge 7d7248c into 706a13f
Browse files Browse the repository at this point in the history
  • Loading branch information
rbeuque74 committed Dec 16, 2020
2 parents 706a13f + 7d7248c commit ba9d041
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 3 deletions.
24 changes: 24 additions & 0 deletions README.md
Expand Up @@ -151,6 +151,30 @@ For development purposes, an optional `basic-auth` configstore item can be provi

Extending this basic authentication mechanism is possible by developing an "init" plugin, as described [below](#plugins).

### Notification

Every task state change can be notified to a notification backend.
µTask implements three differents notification backends: Slack, [TaT](https://github.com/ovh/tat), and generic webhooks.

Default payload that will be sent for generic webhooks is :
```json
{
"message": "string",
"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\"}"
}
```

Notification backends can be configured in the global µTask configuration, as described [here](./config/README.md#utask-cfg).

## Authoring Task Templates <a name="templates"></a>

Checkout the [µTask examples directory](./examples).
Expand Down
12 changes: 12 additions & 0 deletions config/README.md
Expand Up @@ -47,6 +47,7 @@ postgres://user:pass@db/utask?sslmode=disable
// implemented notifiers include:
// - tat (github.com/ovh/tat)
// - slack webhook (https://api.slack.com/messaging/webhooks)
// - generic webhook (custom URL, with HTTP POST method)
"notify_config": {
"tat-internal": {
"type": "tat",
Expand All @@ -62,6 +63,17 @@ postgres://user:pass@db/utask?sslmode=disable
"config": {
"webhook_url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
}
},
"webhook-example.org": {
"type": "webhook",
"config": {
"webhook_url": "https://example.org/webhook/XXXXXXXXXXXXXXXXXXXX",
"username": "foo",
"password": "very-secret",
"headers": {
"X-Specific-Header": "foobar"
}
}
}
},
// notify_actions specifies a notification config for existing events in µTask
Expand Down
11 changes: 10 additions & 1 deletion models/task/task.go
Expand Up @@ -163,7 +163,12 @@ func Create(dbp zesty.DBProvider, tt *tasktemplate.TaskTemplate, reqUsername str
return nil, pgjuju.Interpret(err)
}

t.notifyState(tt.AllowedResolverUsernames)
notificationAllowedResolverUsernames := []string{}
notificationAllowedResolverUsernames = append(notificationAllowedResolverUsernames, tt.AllowedResolverUsernames...)
if tt.AllowAllResolverUsernames {
notificationAllowedResolverUsernames = append(notificationAllowedResolverUsernames, t.RequesterUsername)
}
t.notifyState(notificationAllowedResolverUsernames)

return t, nil
}
Expand Down Expand Up @@ -602,6 +607,10 @@ func (t *Task) notifyState(potentialResolvers []string) {
ResolverUsername: t.ResolverUsername,
StepsDone: t.StepsDone,
StepsTotal: t.StepsTotal,
Tags: t.Tags,
}
if t.Resolution != nil {
tsu.ResolutionPublicID = *t.Resolution
}

notify.Send(
Expand Down
13 changes: 11 additions & 2 deletions pkg/notify/init/init.go
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/ovh/utask/pkg/notify"
"github.com/ovh/utask/pkg/notify/slack"
"github.com/ovh/utask/pkg/notify/tat"
"github.com/ovh/utask/pkg/notify/webhook"
)

const (
Expand All @@ -27,7 +28,7 @@ func Init(store *configstore.Store) error {
case tat.Type:
f := utask.NotifyBackendTat{}
if err := json.Unmarshal(ncfg.Config, &f); err != nil {
return fmt.Errorf("%s: %s, %s", errRetrieveCfg, ncfg.Type, name)
return fmt.Errorf("%s: %s, %s: %s", errRetrieveCfg, ncfg.Type, name, err)
}
tn, err := tat.NewTatNotificationSender(
f.URL,
Expand All @@ -43,11 +44,19 @@ func Init(store *configstore.Store) error {
case slack.Type:
f := utask.NotifyBackendSlack{}
if err := json.Unmarshal(ncfg.Config, &f); err != nil {
return fmt.Errorf("%s: %s, %s", errRetrieveCfg, ncfg.Type, name)
return fmt.Errorf("%s: %s, %s: %s", errRetrieveCfg, ncfg.Type, name, err)
}
sn := slack.NewSlackNotificationSender(f.WebhookURL)
notify.RegisterSender(sn, name)

case webhook.Type:
f := utask.NotifyBackendWebhook{}
if err := json.Unmarshal(ncfg.Config, &f); err != nil {
return fmt.Errorf("%s: %s, %s: %s", errRetrieveCfg, ncfg.Type, name, err)
}
sn := webhook.NewWebhookNotificationSender(f.WebhookURL, f.Username, f.Password, f.Headers)
notify.RegisterSender(sn, name)

default:
return fmt.Errorf("Failed to identify backend type: %s", ncfg.Type)
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/notify/messages.go
@@ -1,7 +1,9 @@
package notify

import (
"encoding/json"
"fmt"
"log"
"strings"
)

Expand All @@ -15,13 +17,15 @@ type Message struct {
type TaskStateUpdate struct {
Title string
PublicID string
ResolutionPublicID string
State string
TemplateName string
RequesterUsername string
ResolverUsername *string
PotentialResolvers []string
StepsDone int
StepsTotal int
Tags map[string]string
}

// WrapTaskStateUpdate returns a Message struct formatted for a task state change
Expand All @@ -32,6 +36,8 @@ func WrapTaskStateUpdate(tsu *TaskStateUpdate) *Message {

m.Fields = make(map[string]string)

m.Fields["task_id"] = tsu.PublicID
m.Fields["title"] = tsu.Title
m.Fields["state"] = tsu.State
m.Fields["template"] = tsu.TemplateName
if tsu.RequesterUsername != "" {
Expand All @@ -44,6 +50,18 @@ func WrapTaskStateUpdate(tsu *TaskStateUpdate) *Message {
if tsu.PotentialResolvers != nil && len(tsu.PotentialResolvers) > 0 {
m.Fields["potential_resolvers"] = strings.Join(tsu.PotentialResolvers, " ")
}
if tsu.ResolutionPublicID != "" {
m.Fields["resolution_id"] = tsu.ResolutionPublicID
}

if tsu.Tags != nil {
tags, err := json.Marshal(tsu.Tags)
if err == nil {
m.Fields["tags"] = string(tags)
} else {
log.Printf("notify error: failed to marshal tags for task #%s: %s", tsu.PublicID, err)
}
}

return &m
}
87 changes: 87 additions & 0 deletions pkg/notify/webhook/webhook.go
@@ -0,0 +1,87 @@
package webhook

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"

"github.com/ovh/utask/pkg/notify"
)

const (
// Type represents Webhook as notify backend
Type string = "webhook"
)

// NotificationSender is a notify.NotificationSender implementation
// capable of sending notifications to a webhook
type NotificationSender struct {
webhookURL string
username string
password string
headers map[string]string
httpClient *http.Client
}

// NewWebhookNotificationSender instantiates a NotificationSender
func NewWebhookNotificationSender(webhookURL, username, password string, headers map[string]string) *NotificationSender {
return &NotificationSender{
webhookURL: webhookURL,
username: username,
password: password,
headers: headers,
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}

// Send is the implementation for triggering a webhook to send the notification
func (w *NotificationSender) Send(m *notify.Message, name string) {
msg := map[string]string{
"message": m.MainMessage,
}

for k, v := range m.Fields {
msg[k] = v
}

b, err := json.Marshal(msg)
if err != nil {
fmt.Println(err)
return
}

req, err := http.NewRequest("POST", w.webhookURL, bytes.NewBuffer(b))
if err != nil {
fmt.Println(err)
return
}

for k, v := range w.headers {
req.Header.Set(k, v)
}

if w.username != "" && w.password != "" {
req.SetBasicAuth(w.username, w.password)
}

res, err := w.httpClient.Do(req)
if err != nil {
log.Println(err)
return
}

defer res.Body.Close()

if res.StatusCode >= 400 {
resBody, err := ioutil.ReadAll(res.Body)
log.Printf("failed to send notification using %q: backend returned with status code %d\n", name, res.StatusCode)
if err == nil {
log.Printf("webhook %q response body: %s\n", name, string(resBody))
}
return
}
}
8 changes: 8 additions & 0 deletions utask.go
Expand Up @@ -134,6 +134,14 @@ type NotifyBackendSlack struct {
WebhookURL string `json:"webhook_url"`
}

// NotifyBackendWebhook holds configuration for instantiating a Webhook notify client
type NotifyBackendWebhook struct {
WebhookURL string `json:"webhook_url"`
Username string `json:"username"`
Password string `json:"password"`
Headers map[string]string `json:"headers"`
}

// NotifyActions holds configuration of each actions
// By default all the actions are enabled /w any config name registered
type NotifyActions struct {
Expand Down

0 comments on commit ba9d041

Please sign in to comment.