Skip to content

Commit 13d547b

Browse files
authored
feat(notification): add new notification type: task_validation (#233)
* feat(notification): add new notification type: task_validation New notification type has been introduced: task_validation. It will be fired every time a task is created and the task will require to be validated by a human operator, aka, task will not be auto-runned. This will concerns tasks that require resolver_inputs, task not declared as auto_runnable, or task created by a requester which is not an allowed_resolver. * feat(notification): add task_step_update notification type task_step_update notification will be fired every time a step is changing its state. This commit also modifies the behaviour of task_state_update to prevent notifying when the state is not changing, but only the steps count is evolving. Signed-off-by: Romain Beuque <[email protected]>
1 parent 8cb07d7 commit 13d547b

File tree

14 files changed

+449
-41
lines changed

14 files changed

+449
-41
lines changed

Diff for: .golangci.yml

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# This file contains all available configuration options
2+
# with their default values.
3+
4+
# options for analysis running
5+
run:
6+
# include test files or not, default is true
7+
tests: false
8+
# timeout for running linters, default is 1m
9+
timeout: 4m
10+
11+
linters:
12+
enable:
13+
- govet
14+
- megacheck
15+
- staticcheck
16+
- deadcode
17+
- structcheck
18+
- typecheck
19+
- gosimple
20+
- varcheck
21+
- ineffassign
22+
- bodyclose
23+
- asciicheck
24+
- exportloopref
25+
- goconst
26+
- misspell
27+
- gofmt
28+
- goimports
29+
- predeclared
30+
- gosec
31+
- golint
32+
- nolintlint
33+
- unconvert
34+
- errcheck
35+
- scopelint
36+
- unparam
37+
- unused
38+
39+
disable:
40+
- maligned
41+
42+
# cosmetic
43+
- lll
44+
- gochecknoglobals
45+
- gochecknoinits
46+
47+
linters-settings:
48+
maligned:
49+
# print struct with more effective memory layout or not, false by default
50+
suggest-new: true
51+
52+
issues:
53+
# only reports issues that have been introduced in commits ahead of origin/master
54+
new-from-rev: origin/master
55+
# exclude some irrelevant issues for us
56+
exclude:
57+
- '^G401: Use of weak cryptographic primitive'
58+
- '^G402: TLS InsecureSkipVerify'
59+
- '^G404: Use of weak random number generator'
60+
- '^G501: Blocklisted import crypto/md5: weak cryptographic primitive'
61+
- '^G505: Blocklisted import crypto/sha1: weak cryptographic primitive'
62+
- '^G601: Implicit memory aliasing in for loop'

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ We will list in this document any breaking changes between versions, that requir
66

77
## Breaking changes
88

9+
### v1.13.0
10+
#### Notifications
11+
- Added a new `notification_type` : `task_validation` that fires every time a new task need a human validation. To integrate different `notification_strategy`, all `notification_strategy` are scopped to the `notification_type`. Then, `default_notification_strategy` is now an object containing the `notification_type` as key, and the `strategy` as value ; and `template_notification_strategies` is now an object containing the `notification_type` as key, and the strategies array as value.
12+
- Changed the configuration key `task_state_action` (in the `notify_action` section) to the new value `task_state_update` (to match the `notification_type` value).
13+
914
### v1.10.0
1015
#### SQL
1116
- `005_resolution_creation_timestamp.sql` migration file should be applied while upgrading. It adds a column `created` in the `resolution` table to keep track of the resolution creation timestamp.

Diff for: README.md

+39-2
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,13 @@ Extending this basic authentication mechanism is possible by developing an "init
156156
Every task state change can be notified to a notification backend.
157157
µTask implements three differents notification backends: Slack, [TaT](https://github.com/ovh/tat), and generic webhooks.
158158

159-
Default payload that will be sent for generic webhooks is :
159+
Default payload that will be sent for generic webhooks are:
160+
161+
__task_state_update notifications:__
160162
```json
161163
{
162164
"message": "string",
165+
"notification_type": "task_state_update",
163166
"task_id": "public_task_uuid",
164167
"title": "task title string",
165168
"state": "current task state",
@@ -173,6 +176,40 @@ Default payload that will be sent for generic webhooks is :
173176
}
174177
```
175178

179+
__task_step_update notifications:__
180+
```json
181+
{
182+
"message": "string",
183+
"notification_type": "task_step_update",
184+
"task_id": "public_task_uuid",
185+
"title": "task title string",
186+
"state": "current task state",
187+
"template": "template_name",
188+
"step_name": "string",
189+
"step_state": "string",
190+
"requester": "string",
191+
"resolver": "string",
192+
"steps": "14/20",
193+
"resolution_id": "public_resolution_uuid",
194+
"tags": "{\"tag1\":\"value1\"}"
195+
}
196+
```
197+
198+
__task_validation notifications:__
199+
```json
200+
{
201+
"message": "string",
202+
"notification_type": "task_validation",
203+
"task_id": "public_task_uuid",
204+
"title": "task title string",
205+
"state": "TODO",
206+
"template": "template_name",
207+
"requester": "optional",
208+
"potential_resolvers": "user1,user2,admin",
209+
"tags": "{\"tag1\":\"value1\"}"
210+
}
211+
```
212+
176213
Notification backends can be configured in the global µTask configuration, as described [here](./config/README.md#utask-cfg).
177214

178215
## Authoring Task Templates <a name="templates"></a>
@@ -211,7 +248,7 @@ The following templating functions are available:
211248
| **`Golang`** | Builtin functions from Golang text template | [Doc](https://golang.org/pkg/text/template/#hdr-Actions) |
212249
| **`Sprig`** | Extended set of functions from the Sprig project | [Doc](https://masterminds.github.io/sprig/) |
213250
| **`field`** | Equivalent to the dot notation, for entries with forbidden characters | ``{{field `config` `foo.bar`}}`` |
214-
| **`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 | fieldFrom `foo.foo` }}``` | ```{{expr | fieldFrom `config` `foo.bar`}}``` |
251+
| **`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 | fieldFrom `foo.foo` }}``` | ```{{expr | fieldFrom `config` `foo.bar`}}``` |
215252
| **`eval`** | Evaluates the value of a template variable | ``{{eval `var1`}}`` |
216253
| **`evalCache`** | Evaluates the value of a template variable, and cache for future usage (to avoid further computation) | ``{{evalCache `var1`}}`` |
217254
| **`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"}`}}`` |

Diff for: config/README.md

+26-8
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,27 @@ postgres://user:pass@db/utask?sslmode=disable
6161
"url": "http://localhost:9999/tat",
6262
"topic": "utask.notifications"
6363
},
64-
"default_notification_strategy":"silent",
65-
"template_notification_strategies": [{
66-
"templates": ["hello-world", "hello-world-2"],
67-
"notification_strategy": "always"
68-
}]
64+
"default_notification_strategy": {
65+
"task_state_update": "silent",
66+
"task_validation": "always"
67+
},
68+
"template_notification_strategies": {
69+
"task_state_update": [
70+
{
71+
"templates": ["hello-world", "hello-world-2"],
72+
"notification_strategy": "always"
73+
}
74+
]
75+
}
6976
},
7077
"slack-webhook": {
7178
"type": "slack",
7279
"config": {
7380
"webhook_url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
7481
},
75-
"default_notification_strategy":"failure_only"
82+
"default_notification_strategy": {
83+
"task_state_update": "failure_only"
84+
},
7685
},
7786
"webhook-example.org": {
7887
"type": "webhook",
@@ -88,11 +97,20 @@ postgres://user:pass@db/utask?sslmode=disable
8897
},
8998
// notify_actions specifies a notification config for existing events in µTask
9099
// existing events are:
91-
// - task_state_action (fired every time a task's state changes)
100+
// - task_state_update: fired every time a task's state changes
101+
// - task_validation: fired every time a new task is created and requires a human validation
102+
// - task_step_update: fired every time a step's state changes
92103
"notify_actions": {
93-
"task_state_action": {
104+
"task_state_update": {
94105
"disabled": false, // set to true to avoid sending out notification
95106
"notify_backends": ["tat-internal", "slack-webhook"] // choose among the named configs in notify_config, leave empty to broadcast on any notification backend
107+
},
108+
"task_validation": {
109+
"disabled": false, // set to true to avoid sending out notification
110+
"notify_backends": ["slack-webhook"] // choose among the named configs in notify_config, leave empty to broadcast on any notification backend
111+
},
112+
"task_step_update": {
113+
"disabled": true // set to true to avoid sending out notification
96114
}
97115
},
98116
// database_config holds configuration to fine-tune DB connection

Diff for: engine/engine.go

+9
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,11 @@ func resolve(dbp zesty.DBProvider, res *resolution.Resolution, t *task.Task, sm
406406
}
407407
}
408408

409+
var oldState string
410+
if oldStep, ok := res.Steps[s.Name]; ok {
411+
oldState = oldStep.State
412+
}
413+
409414
// "commit" step back into resolution
410415
res.SetStep(s.Name, s)
411416
// consolidate its result into live values
@@ -429,6 +434,10 @@ func resolve(dbp zesty.DBProvider, res *resolution.Resolution, t *task.Task, sm
429434

430435
debugLogger.Debugf("Engine: resolve() %s loop, step %s (#%d) result: %s", res.PublicID, s.Name, s.TryCount, s.State)
431436

437+
if newStep, ok := res.Steps[s.Name]; ok && newStep.State != oldState {
438+
t.NotifyStepState(s.Name, newStep.State)
439+
}
440+
432441
// update done step count
433442
// ignore foreach iterations for global done count
434443
if s.IsFinal() && !s.IsChild() {

Diff for: engine/step/executor/executor.go

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package executor
22

33
import (
44
"encoding/json"
5+
56
"github.com/ghodss/yaml"
67
)
78

Diff for: models/resolution/resolution.go

+11
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,18 @@ func (r *Resolution) setSteps(st map[string]*step.Step) {
489489
}
490490

491491
func (r *Resolution) SetStepState(stepName, state string) {
492+
oldState := r.Steps[stepName].State
492493
r.Steps[stepName].State = state
494+
495+
// notify about step state modification
496+
if oldState != state {
497+
if dbp, err := zesty.NewDBProvider(utask.DBName); err == nil {
498+
if t, err := task.LoadFromID(dbp, r.TaskID); err == nil {
499+
t.NotifyStepState(stepName, state)
500+
}
501+
}
502+
}
503+
493504
if r.Values == nil {
494505
return
495506
}

Diff for: models/task/task.go

+62-2
Original file line numberDiff line numberDiff line change
@@ -533,8 +533,16 @@ func applyTemplateToMap(m map[string]interface{}, values *values.Values) error {
533533

534534
// SetState updates the task's state
535535
func (t *Task) SetState(s string) {
536+
var notify bool
537+
if t.State != s {
538+
notify = true
539+
}
540+
536541
t.State = s
537-
t.notifyState(nil)
542+
543+
if notify {
544+
t.notifyState(nil)
545+
}
538546
}
539547

540548
func (t *Task) SetTags(tags map[string]string, values *values.Values) error {
@@ -620,6 +628,58 @@ func (t *Task) notifyState(potentialResolvers []string) {
620628

621629
notify.Send(
622630
notify.WrapTaskStateUpdate(tsu),
623-
notify.ListActions().TaskStateAction,
631+
notify.ListActions().TaskStateUpdateAction,
632+
)
633+
}
634+
635+
func (t *Task) NotifyValidationRequired(tt *tasktemplate.TaskTemplate) {
636+
notificationAllowedResolverUsernames := []string{}
637+
if tt != nil {
638+
notificationAllowedResolverUsernames = append(notificationAllowedResolverUsernames, tt.AllowedResolverUsernames...)
639+
}
640+
if tt.AllowAllResolverUsernames {
641+
notificationAllowedResolverUsernames = append(notificationAllowedResolverUsernames, t.RequesterUsername)
642+
}
643+
644+
tv := &notify.TaskValidation{
645+
Title: t.Title,
646+
PublicID: t.PublicID,
647+
State: t.State,
648+
TemplateName: t.TemplateName,
649+
PotentialResolvers: notificationAllowedResolverUsernames,
650+
RequesterUsername: t.RequesterUsername,
651+
Tags: t.Tags,
652+
}
653+
654+
notify.Send(
655+
notify.WrapTaskValidation(tv),
656+
notify.ListActions().TaskValidationAction,
657+
)
658+
}
659+
660+
func (t *Task) NotifyStepState(stepName, stepState string) {
661+
if t.Resolution == nil || t.ResolverUsername == nil {
662+
// matches mainly the period where the task is getting created and all steps states are assigned to TODO
663+
return
664+
}
665+
666+
tsu := &notify.TaskStepUpdate{
667+
Title: t.Title,
668+
PublicID: t.PublicID,
669+
State: t.State,
670+
TemplateName: t.TemplateName,
671+
RequesterUsername: t.RequesterUsername,
672+
ResolverUsername: *t.ResolverUsername,
673+
StepsDone: t.StepsDone,
674+
StepsTotal: t.StepsTotal,
675+
Tags: t.Tags,
676+
StepName: stepName,
677+
StepState: stepState,
678+
ResolutionPublicID: *t.Resolution,
679+
}
680+
681+
notify.Send(
682+
notify.WrapTaskStepUpdate(tsu),
683+
notify.ListActions().TaskStepUpdateAction,
624684
)
625685
}

0 commit comments

Comments
 (0)