Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4bf5d50

Browse files
fsaminbnjjj
authored andcommitted
feat(api, sdk, ui): notifications as code (#3546)
close #3534 #3519
1 parent 6287d01 commit 4bf5d50

File tree

17 files changed

+774
-149
lines changed

17 files changed

+774
-149
lines changed

engine/api/notification.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,23 @@ import (
1010

1111
func (api *API) getUserNotificationTypeHandler() service.Handler {
1212
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
13-
return service.WriteJSON(w, []string{
14-
sdk.EmailUserNotification,
15-
sdk.JabberUserNotification,
13+
return service.WriteJSON(w, map[string]sdk.UserNotificationSettings{
14+
sdk.EmailUserNotification: sdk.UserNotificationSettings{
15+
OnSuccess: sdk.UserNotificationChange,
16+
OnFailure: sdk.UserNotificationAlways,
17+
OnStart: &sdk.False,
18+
SendToAuthor: &sdk.True,
19+
SendToGroups: &sdk.False,
20+
Template: &sdk.UserNotificationTemplateEmail,
21+
},
22+
sdk.JabberUserNotification: sdk.UserNotificationSettings{
23+
OnSuccess: sdk.UserNotificationChange,
24+
OnFailure: sdk.UserNotificationAlways,
25+
OnStart: &sdk.False,
26+
SendToAuthor: &sdk.True,
27+
SendToGroups: &sdk.False,
28+
Template: &sdk.UserNotificationTemplateJabber,
29+
},
1630
}, http.StatusOK)
1731
}
1832
}

engine/api/notification/permission.go

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -11,73 +11,6 @@ import (
1111
"github.com/ovh/cds/sdk/log"
1212
)
1313

14-
// applicationPipelineEnvironmentUsers returns users list with expected access to application/pipeline/environment
15-
func applicationPipelineEnvironmentUsers(db gorp.SqlExecutor, appID, pipID, envID int64, access int) ([]sdk.User, error) {
16-
var query string
17-
var args []interface{}
18-
users := []sdk.User{}
19-
if envID == sdk.DefaultEnv.ID {
20-
query = `
21-
SELECT DISTINCT "user".id, "user".username, "user".data
22-
FROM "group"
23-
JOIN application_group ON "group".id = application_group.group_id
24-
JOIN pipeline_group ON "group".id = pipeline_group.group_id
25-
JOIN group_user ON "group".id = group_user.group_id
26-
JOIN "user" ON group_user.user_id = "user".id
27-
WHERE application_group.application_id = $1
28-
AND pipeline_group.pipeline_id = $2
29-
AND application_group.role >= $3
30-
AND pipeline_group.role >= $3
31-
AND "group".id <> $4
32-
`
33-
args = []interface{}{appID, pipID, access, permission.DefaultGroupID}
34-
} else {
35-
query = `
36-
SELECT DISTINCT "user".id, "user".username, "user".data
37-
FROM "group"
38-
JOIN application_group ON "group".id = application_group.group_id
39-
JOIN pipeline_group ON "group".id = pipeline_group.group_id
40-
JOIN environment_group ON "group".id = environment_group.group_id
41-
JOIN group_user ON "group".id = group_user.group_id
42-
JOIN "user" ON group_user.user_id = "user".id
43-
WHERE application_group.application_id = $1
44-
AND pipeline_group.pipeline_id = $2
45-
AND environment_group.environment_id = $3
46-
AND application_group.role >= $4
47-
AND pipeline_group.role >= $4
48-
AND environment_group.role >= $4
49-
AND "group".id <> $5
50-
`
51-
args = []interface{}{appID, pipID, envID, access, permission.DefaultGroupID}
52-
}
53-
54-
rows, err := db.Query(query, args...)
55-
if err != nil {
56-
if err == sql.ErrNoRows {
57-
return users, nil
58-
}
59-
return users, err
60-
}
61-
defer rows.Close()
62-
63-
for rows.Next() {
64-
u := sdk.User{}
65-
var data string
66-
if err := rows.Scan(&u.ID, &u.Username, &data); err != nil {
67-
log.Warning("permission.ApplicationPipelineEnvironmentGroups> error while scanning user : %s", err)
68-
continue
69-
}
70-
71-
uTemp := &sdk.User{}
72-
if err := json.Unmarshal([]byte(data), uTemp); err != nil {
73-
log.Warning("permission.ApplicationPipelineEnvironmentGroups> error while parsing user : %s", err)
74-
continue
75-
}
76-
users = append(users, *uTemp)
77-
}
78-
return users, nil
79-
}
80-
8114
// projectPermissionUsers Get users that access to given project, without default group
8215
func projectPermissionUsers(db gorp.SqlExecutor, projectID int64, access int) ([]sdk.User, error) {
8316
var query string
@@ -102,6 +35,7 @@ func projectPermissionUsers(db gorp.SqlExecutor, projectID int64, access int) ([
10235
}
10336
defer rows.Close()
10437

38+
//TODO: refactor this ugly scan
10539
for rows.Next() {
10640
u := sdk.User{}
10741
var data string

engine/api/notification/user.go

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package notification
22

33
import (
44
"fmt"
5-
"strings"
65

76
"github.com/go-gorp/gorp"
87

98
"github.com/ovh/cds/engine/api/permission"
109
"github.com/ovh/cds/engine/api/user"
1110
"github.com/ovh/cds/sdk"
11+
"github.com/ovh/cds/sdk/interpolate"
1212
"github.com/ovh/cds/sdk/log"
1313
)
1414

@@ -50,7 +50,7 @@ func GetUserWorkflowEvents(db gorp.SqlExecutor, w sdk.Workflow, previousWR *sdk.
5050
case sdk.JabberUserNotification:
5151
jn := &notif.Settings
5252
//Get recipents from groups
53-
if jn.SendToGroups {
53+
if jn.SendToGroups != nil && *jn.SendToGroups {
5454
u, errPerm := projectPermissionUsers(db, w.ProjectID, permission.PermissionRead)
5555
if errPerm != nil {
5656
log.Error("notification[Jabber]. error while loading permission:%s", errPerm.Error())
@@ -59,43 +59,52 @@ func GetUserWorkflowEvents(db gorp.SqlExecutor, w sdk.Workflow, previousWR *sdk.
5959
jn.Recipients = append(jn.Recipients, u[i].Username)
6060
}
6161
}
62-
if jn.SendToAuthor {
62+
if jn.SendToAuthor == nil || *jn.SendToAuthor {
6363
if author, ok := params["cds.author"]; ok {
6464
jn.Recipients = append(jn.Recipients, author)
6565
}
6666
}
6767

6868
//Finally deduplicate everyone
6969
removeDuplicates(&jn.Recipients)
70-
events = append(events, getWorkflowEvent(jn, params))
70+
notif, err := getWorkflowEvent(jn, params)
71+
if err != nil {
72+
log.Error("notification.GetUserWorkflowEvents> unable to handle event %+v: %v", jn, err)
73+
}
74+
events = append(events, notif)
75+
7176
case sdk.EmailUserNotification:
7277
jn := &notif.Settings
7378
//Get recipents from groups
74-
if jn.SendToGroups {
79+
if jn.SendToGroups != nil && *jn.SendToGroups {
7580
u, errPerm := projectPermissionUsers(db, w.ProjectID, permission.PermissionRead)
7681
if errPerm != nil {
77-
log.Error("notification[Email].SendPipelineBuild> error while loading permission:%s", errPerm.Error())
82+
log.Error("notification[Email].GetUserWorkflowEvents> error while loading permission:%s", errPerm.Error())
7883
return nil
7984
}
8085
for i := range u {
8186
jn.Recipients = append(jn.Recipients, u[i].Email)
8287
}
8388
}
84-
if jn.SendToAuthor {
89+
if jn.SendToAuthor == nil || *jn.SendToAuthor {
8590
if email, ok := params["cds.author.email"]; ok {
8691
jn.Recipients = append(jn.Recipients, email)
8792
} else if author, okA := params["cds.author"]; okA && author != "" {
8893
u, err := user.LoadUserWithoutAuth(db, author)
8994
if err != nil {
90-
log.Warning("notification[Email].SendPipelineBuild> Cannot load author %s: %s", author, err)
95+
log.Warning("notification[Email].GetUserWorkflowEvents> Cannot load author %s: %s", author, err)
9196
continue
9297
}
9398
jn.Recipients = append(jn.Recipients, u.Email)
9499
}
95100
}
96101
//Finally deduplicate everyone
97102
removeDuplicates(&jn.Recipients)
98-
go SendMailNotif(getWorkflowEvent(jn, params))
103+
notif, err := getWorkflowEvent(jn, params)
104+
if err != nil {
105+
log.Error("notification.GetUserWorkflowEvents> unable to handle event %+v: %v", jn, err)
106+
}
107+
go SendMailNotif(notif)
99108
}
100109
}
101110
}
@@ -140,19 +149,21 @@ func ShouldSendUserWorkflowNotification(notif sdk.WorkflowNotification, nodeRun
140149
return true
141150
}
142151
case sdk.StatusWaiting.String():
143-
return notif.Settings.OnStart
152+
return notif.Settings.OnStart != nil && *notif.Settings.OnStart
144153
}
145154

146155
return false
147156
}
148157

149-
func getWorkflowEvent(notif *sdk.UserNotificationSettings, params map[string]string) sdk.EventNotif {
150-
subject := notif.Template.Subject
151-
body := notif.Template.Body
152-
for k, value := range params {
153-
key := "{{." + k + "}}"
154-
subject = strings.Replace(subject, key, value, -1)
155-
body = strings.Replace(body, key, value, -1)
158+
func getWorkflowEvent(notif *sdk.UserNotificationSettings, params map[string]string) (sdk.EventNotif, error) {
159+
subject, err := interpolate.Do(notif.Template.Subject, params)
160+
if err != nil {
161+
return sdk.EventNotif{}, err
162+
}
163+
164+
body, err := interpolate.Do(notif.Template.Body, params)
165+
if err != nil {
166+
return sdk.EventNotif{}, err
156167
}
157168

158169
e := sdk.EventNotif{
@@ -163,7 +174,7 @@ func getWorkflowEvent(notif *sdk.UserNotificationSettings, params map[string]str
163174
e.Recipients = append(e.Recipients, r)
164175
}
165176

166-
return e
177+
return e, nil
167178
}
168179

169180
func removeDuplicates(xs *[]string) {

engine/api/workflow/dao_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,11 +1091,11 @@ func TestInsertComplexeWorkflowWithComplexeJoins(t *testing.T) {
10911091
SourceNodeRefs: []string{"pip6", "pip5"},
10921092
Settings: sdk.UserNotificationSettings{
10931093
OnFailure: sdk.UserNotificationAlways,
1094-
OnStart: true,
1094+
OnStart: &sdk.True,
10951095
OnSuccess: sdk.UserNotificationAlways,
1096-
SendToAuthor: true,
1097-
SendToGroups: true,
1098-
Template: sdk.UserNotificationTemplate{
1096+
SendToAuthor: &sdk.True,
1097+
SendToGroups: &sdk.True,
1098+
Template: &sdk.UserNotificationTemplate{
10991099
Body: "body",
11001100
Subject: "title",
11011101
},

engine/api/workflow/workflow_exporter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ func exportWorkflow(wf sdk.Workflow, f exportentities.Format, w io.Writer, opts
4444
}
4545

4646
// Useful to not display history_length in yaml or json if it's his default value
47-
if e.HistoryLength == sdk.DefaultHistoryLength {
48-
e.HistoryLength = 0
47+
if e.HistoryLength != nil && *e.HistoryLength == sdk.DefaultHistoryLength {
48+
e.HistoryLength = nil
4949
}
5050

5151
// Marshal to the desired format

sdk/common.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ const MaxIconSize = 120000
2727
// IconFormat is the format prefix accepted for icon
2828
const IconFormat = "data:image/"
2929

30+
// True of false
31+
var (
32+
True = true
33+
False = false
34+
)
35+
3036
// EncryptFunc is a common type
3137
type EncryptFunc func(gorp.SqlExecutor, int64, string, string) (string, error)
3238

sdk/error.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,18 @@ func (e *MultiError) Join(j MultiError) {
782782
}
783783

784784
// Append appends an error to a MultiError
785-
func (e *MultiError) Append(err error) { *e = append(*e, err) }
785+
func (e *MultiError) Append(err error) {
786+
if err == nil {
787+
return
788+
}
789+
if mError, ok := err.(*MultiError); ok {
790+
for i := range *mError {
791+
e.Append((*mError)[i])
792+
}
793+
} else {
794+
*e = append(*e, err)
795+
}
796+
}
786797

787798
// IsEmpty return true if MultiError is empty, false otherwise
788799
func (e *MultiError) IsEmpty() bool { return len(*e) == 0 }

sdk/exportentities/workflow.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,28 +19,30 @@ type Workflow struct {
1919
Workflow map[string]NodeEntry `json:"workflow,omitempty" yaml:"workflow,omitempty"`
2020
Hooks map[string][]HookEntry `json:"hooks,omitempty" yaml:"hooks,omitempty"`
2121
// This will be filled for simple workflows
22-
DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
23-
Conditions *sdk.WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty"`
24-
When []string `json:"when,omitempty" yaml:"when,omitempty"` //This is use only for manual and success condition
25-
PipelineName string `json:"pipeline,omitempty" yaml:"pipeline,omitempty"`
26-
Payload map[string]interface{} `json:"payload,omitempty" yaml:"payload,omitempty"`
27-
Parameters map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty"`
28-
ApplicationName string `json:"application,omitempty" yaml:"application,omitempty"`
29-
EnvironmentName string `json:"environment,omitempty" yaml:"environment,omitempty"`
30-
ProjectPlatformName string `json:"platform,omitempty" yaml:"platform,omitempty"`
31-
PipelineHooks []HookEntry `json:"pipeline_hooks,omitempty" yaml:"pipeline_hooks,omitempty"`
32-
Permissions map[string]int `json:"permissions,omitempty" yaml:"permissions,omitempty"`
33-
Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty" db:"-"`
34-
PurgeTags []string `json:"purge_tags,omitempty" yaml:"purge_tags,omitempty" db:"-"`
35-
HistoryLength int64 `json:"history_length,omitempty" yaml:"history_length,omitempty" db:"-"`
22+
DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
23+
Conditions *sdk.WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty"`
24+
When []string `json:"when,omitempty" yaml:"when,omitempty"` //This is used only for manual and success condition
25+
PipelineName string `json:"pipeline,omitempty" yaml:"pipeline,omitempty"`
26+
Payload map[string]interface{} `json:"payload,omitempty" yaml:"payload,omitempty"`
27+
Parameters map[string]string `json:"parameters,omitempty" yaml:"parameters,omitempty"`
28+
ApplicationName string `json:"application,omitempty" yaml:"application,omitempty"`
29+
EnvironmentName string `json:"environment,omitempty" yaml:"environment,omitempty"`
30+
ProjectPlatformName string `json:"platform,omitempty" yaml:"platform,omitempty"`
31+
PipelineHooks []HookEntry `json:"pipeline_hooks,omitempty" yaml:"pipeline_hooks,omitempty"`
32+
Permissions map[string]int `json:"permissions,omitempty" yaml:"permissions,omitempty"`
33+
Metadata map[string]string `json:"metadata,omitempty" yaml:"metadata,omitempty"`
34+
PurgeTags []string `json:"purge_tags,omitempty" yaml:"purge_tags,omitempty"`
35+
HistoryLength *int64 `json:"history_length,omitempty" yaml:"history_length,omitempty"`
36+
Notifications []NotificationEntry `json:"notify,omitempty" yaml:"notify,omitempty"` // This is used when the workflow have only one pipeline
37+
MapNotifications map[string][]NotificationEntry `json:"notifications,omitempty" yaml:"notifications,omitempty"` // This is used when the workflow have more than one pipeline
3638
}
3739

3840
// NodeEntry represents a node as code
3941
type NodeEntry struct {
4042
ID int64 `json:"-" yaml:"-"`
4143
DependsOn []string `json:"depends_on,omitempty" yaml:"depends_on,omitempty"`
4244
Conditions *sdk.WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty"`
43-
When []string `json:"when,omitempty" yaml:"when,omitempty"` //This is use only for manual and success condition
45+
When []string `json:"when,omitempty" yaml:"when,omitempty"` //This is used only for manual and success condition
4446
PipelineName string `json:"pipeline,omitempty" yaml:"pipeline,omitempty"`
4547
ApplicationName string `json:"application,omitempty" yaml:"application,omitempty"`
4648
EnvironmentName string `json:"environment,omitempty" yaml:"environment,omitempty"`
@@ -222,10 +224,8 @@ func NewWorkflow(w sdk.Workflow, opts ...WorkflowOptions) (Workflow, error) {
222224
}
223225
}
224226

225-
if w.HistoryLength > 0 {
226-
exportedWorkflow.HistoryLength = w.HistoryLength
227-
} else {
228-
exportedWorkflow.HistoryLength = 20
227+
if w.HistoryLength > 0 && w.HistoryLength != sdk.DefaultHistoryLength {
228+
exportedWorkflow.HistoryLength = &w.HistoryLength
229229
}
230230

231231
exportedWorkflow.PurgeTags = w.PurgeTags
@@ -296,6 +296,11 @@ func NewWorkflow(w sdk.Workflow, opts ...WorkflowOptions) (Workflow, error) {
296296
}
297297
}
298298

299+
//Notifications
300+
if err := craftNotifications(w, &exportedWorkflow); err != nil {
301+
return exportedWorkflow, err
302+
}
303+
299304
for _, f := range opts {
300305
if err := f(w, &exportedWorkflow); err != nil {
301306
return exportedWorkflow, sdk.WrapError(err, "Unable to run function")
@@ -367,6 +372,9 @@ func (w Workflow) checkValidity() error {
367372
}
368373
}
369374

375+
//Checks map notifications validity
376+
mError.Append(checkWorkflowNotificationsValidity(w))
377+
370378
if mError.IsEmpty() {
371379
return nil
372380
}
@@ -429,8 +437,10 @@ func (w Workflow) GetWorkflow() (*sdk.Workflow, error) {
429437
wf.Metadata[k] = v
430438
}
431439
}
432-
if w.HistoryLength > 0 && w.HistoryLength != sdk.DefaultHistoryLength {
433-
wf.HistoryLength = w.HistoryLength
440+
if w.HistoryLength != nil && *w.HistoryLength > 0 {
441+
wf.HistoryLength = *w.HistoryLength
442+
} else {
443+
wf.HistoryLength = sdk.DefaultHistoryLength
434444
}
435445

436446
rand.Seed(time.Now().Unix())
@@ -466,6 +476,11 @@ func (w Workflow) GetWorkflow() (*sdk.Workflow, error) {
466476
wf.Groups = append(wf.Groups, perm)
467477
}
468478

479+
//Compute notifications
480+
if err := w.processNotifications(wf); err != nil {
481+
return nil, err
482+
}
483+
469484
wf.SortNode()
470485

471486
return wf, nil

0 commit comments

Comments
 (0)