Skip to content

Commit

Permalink
feat(api,ui): allow to send notification conditionally based on workf…
Browse files Browse the repository at this point in the history
…low variables #4494

Signed-off-by: Benjamin Coenen <benjamin.coenen@corp.ovh.com>
  • Loading branch information
Benjamin Coenen authored and bnjjj committed Aug 2, 2019
1 parent 10e10a5 commit 4eb79dc
Show file tree
Hide file tree
Showing 16 changed files with 235 additions and 132 deletions.
1 change: 1 addition & 0 deletions engine/api/api_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ func (api *API) InitRouter() {
r.Handle("/project/{key}/workflows/{permWorkflowName}/label", r.POST(api.postWorkflowLabelHandler))
r.Handle("/project/{key}/workflows/{permWorkflowName}/label/{labelID}", r.DELETE(api.deleteWorkflowLabelHandler))
r.Handle("/project/{key}/workflows/{permWorkflowName}/rollback/{auditID}", r.POST(api.postWorkflowRollbackHandler))
r.Handle("/project/{key}/workflows/{permWorkflowName}/notifications/conditions", r.GET(api.getWorkflowNotificationsConditionsHandler))
r.Handle("/project/{key}/workflows/{permWorkflowName}/groups", r.POST(api.postWorkflowGroupHandler))
r.Handle("/project/{key}/workflows/{permWorkflowName}/groups/{groupName}", r.PUT(api.putWorkflowGroupHandler), r.DELETE(api.deleteWorkflowGroupHandler))
r.Handle("/project/{key}/workflows/{permWorkflowName}/hooks/{uuid}", r.GET(api.getWorkflowHookHandler))
Expand Down
29 changes: 26 additions & 3 deletions engine/api/notification/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/ovh/cds/sdk"
"github.com/ovh/cds/sdk/interpolate"
"github.com/ovh/cds/sdk/log"
"github.com/ovh/cds/sdk/luascript"
)

var (
Expand Down Expand Up @@ -141,20 +142,42 @@ func ShouldSendUserWorkflowNotification(notif sdk.WorkflowNotification, nodeRun

switch nodeRun.Status {
case sdk.StatusSuccess.String():
if check(notif.Settings.OnSuccess) {
if check(notif.Settings.OnSuccess) && checkConditions(notif.Settings.Conditions, nodeRun.BuildParameters) {
return true
}
case sdk.StatusFail.String():
if check(notif.Settings.OnFailure) {
if check(notif.Settings.OnFailure) && checkConditions(notif.Settings.Conditions, nodeRun.BuildParameters) {
return true
}
case sdk.StatusWaiting.String():
return notif.Settings.OnStart != nil && *notif.Settings.OnStart
return notif.Settings.OnStart != nil && *notif.Settings.OnStart && checkConditions(notif.Settings.Conditions, nodeRun.BuildParameters)
}

return false
}

func checkConditions(conditions sdk.WorkflowNodeConditions, params []sdk.Parameter) bool {
var conditionsOK bool
var errc error
if conditions.LuaScript == "" {
conditionsOK, errc = sdk.WorkflowCheckConditions(conditions.PlainConditions, params)
} else {
luacheck, err := luascript.NewCheck()
if err != nil {
log.Error("notification check condition error: %s", err)
return false
}
luacheck.SetVariables(sdk.ParametersToMap(params))
errc = luacheck.Perform(conditions.LuaScript)
conditionsOK = luacheck.Result
}
if errc != nil {
log.Error("notification check condition error on execution: %s", errc)
return false
}
return conditionsOK
}

func getWorkflowEvent(notif *sdk.UserNotificationSettings, params map[string]string) (sdk.EventNotif, error) {
subject, err := interpolate.Do(notif.Template.Subject, params)
if err != nil {
Expand Down
78 changes: 78 additions & 0 deletions engine/api/workflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"sort"
"strconv"
"strings"

Expand Down Expand Up @@ -618,3 +619,80 @@ func (api *API) getWorkflowHookHandler() service.Handler {
return service.WriteJSON(w, task, http.StatusOK)
}
}

func (api *API) getWorkflowNotificationsConditionsHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
key := vars["key"]
name := vars["permWorkflowName"]

data := struct {
Operators map[string]string `json:"operators"`
ConditionNames []string `json:"names"`
}{
Operators: sdk.WorkflowConditionsOperators,
}

wr, errr := workflow.LoadLastRun(api.mustDB(), key, name, workflow.LoadRunOptions{})
if errr != nil {
if !sdk.ErrorIs(errr, sdk.ErrWorkflowNotFound) {
return sdk.WrapError(errr, "getWorkflowTriggerConditionHandler> Unable to load last run workflow")
}
}

params := []sdk.Parameter{}
var refNode *sdk.Node
if wr != nil {
refNode = &wr.Workflow.WorkflowData.Node
var errp error
params, errp = workflow.NodeBuildParametersFromRun(*wr, refNode.ID)
if errp != nil {
return sdk.WrapError(errp, "getWorkflowTriggerConditionHandler> Unable to load build parameters from workflow run")
}
if len(params) == 0 {
refNode = nil
}
} else {
data.ConditionNames = append(data.ConditionNames,
"cds.version",
"cds.application",
"cds.environment",
"cds.job",
"cds.manual",
"cds.pipeline",
"cds.project",
"cds.run",
"cds.run.number",
"cds.run.subnumber",
"cds.stage",
"cds.triggered_by.email",
"cds.triggered_by.fullname",
"cds.triggered_by.username",
"cds.ui.pipeline.run",
"cds.worker",
"cds.workflow",
"cds.workspace",
"payload",
)
}

if sdk.ParameterFind(&params, "git.repository") == nil {
data.ConditionNames = append(data.ConditionNames, "git.repository")
data.ConditionNames = append(data.ConditionNames, "git.branch")
data.ConditionNames = append(data.ConditionNames, "git.message")
data.ConditionNames = append(data.ConditionNames, "git.author")
data.ConditionNames = append(data.ConditionNames, "git.hash")
data.ConditionNames = append(data.ConditionNames, "git.hash.short")
}
if sdk.ParameterFind(&params, "git.tag") == nil {
data.ConditionNames = append(data.ConditionNames, "git.tag")
}

for _, p := range params {
data.ConditionNames = append(data.ConditionNames, p.Name)
}

sort.Strings(data.ConditionNames)
return service.WriteJSON(w, data, http.StatusOK)
}
}
1 change: 1 addition & 0 deletions sdk/notif.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type UserNotificationSettings struct {
SendToAuthor *bool `json:"send_to_author,omitempty" yaml:"send_to_author,omitempty"` // default is true, nil is true
Recipients []string `json:"recipients,omitempty" yaml:"recipients,omitempty"`
Template *UserNotificationTemplate `json:"template,omitempty" yaml:"template,omitempty"`
Conditions WorkflowNodeConditions `json:"conditions,omitempty" yaml:"conditions,omitempty"`
}

// UserNotificationTemplate is the notification content
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/model/application.model.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Key } from './keys.model';
import { Metric } from './metric.model';
import { Notification } from './notification.model';
import { Usage } from './usage.model';
import { Variable } from './variable.model';
import { VCSStrategy } from './vcs.model';
import { Notification } from './workflow.model';
import { WorkflowRun } from './workflow.run.model';

export const applicationNamePattern: RegExp = new RegExp('^[a-zA-Z0-9._-]+$');
Expand Down
45 changes: 0 additions & 45 deletions ui/src/app/model/notification.model.ts

This file was deleted.

46 changes: 45 additions & 1 deletion ui/src/app/model/workflow.model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { notificationTypes, UserNotificationSettings } from 'app/model/notification.model';
import { Application } from './application.model';
import { AuditWorkflow } from './audit.model';
import { Environment } from './environment.model';
Expand Down Expand Up @@ -697,3 +696,48 @@ export class WorkflowPullItem {
name: string;
value: string;
}

export const notificationTypes = ['jabber', 'email'];
export const notificationOnSuccess = ['always', 'change', 'never'];
export const notificationOnFailure = ['always', 'change', 'never'];

export class Notification {
application_pipeline_id: number;
pipeline: Pipeline;
environment: Environment;
notifications: any;

// UI attribute
updating = false;

constructor() {
this.notifications = {};
}
}

export class UserNotificationSettings {
on_success: string;
on_failure: string;
on_start: boolean;
send_to_groups: boolean;
send_to_author: boolean;
recipients: Array<string>;
template: UserNotificationTemplate;
notifications: WorkflowNodeConditions;

constructor() {
this.on_success = notificationOnSuccess[1];
this.on_failure = notificationOnFailure[0];
this.on_start = false;
this.send_to_author = true;
this.send_to_groups = false;
this.recipients = [];
this.template = new UserNotificationTemplate();
this.notifications = new WorkflowNodeConditions();
}
}

export class UserNotificationTemplate {
subject: string;
body: string;
}
25 changes: 15 additions & 10 deletions ui/src/app/service/notification/notification.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { HttpClient } from '@angular/common/http';
import {Injectable} from '@angular/core';
import { UserNotificationSettings } from 'app/model/notification.model';
import {Observable} from 'rxjs';
import {NotificationOpts, Permission} from './notification.type';
import { Injectable } from '@angular/core';
import { UserNotificationSettings, WorkflowTriggerConditionCache } from 'app/model/workflow.model';
import { Observable } from 'rxjs';
import { NotificationOpts, Permission } from './notification.type';

declare const Notification: any;

Expand All @@ -12,7 +12,7 @@ export class NotificationService {
permission: Permission;

constructor(private _http: HttpClient) {
this.permission = this.isSupported() ? Notification.permission : 'denied';
this.permission = this.isSupported() ? Notification.permission : 'denied';
}

requestPermission() {
Expand Down Expand Up @@ -47,7 +47,7 @@ export class NotificationService {
if (options.onshow && typeof options.onshow === 'function') {
options.onshow(e);
}
obs.next({notification: notif, event: e});
obs.next({ notification: notif, event: e });
setTimeout(() => {
if (notif && notif.close) {
notif.close();
Expand All @@ -62,14 +62,14 @@ export class NotificationService {
window.focus();
notif.close();
}
obs.next({notification: notif, event: e});
obs.next({ notification: notif, event: e });
};

notif.onerror = (e: any) => {
if (options.onerror && typeof options.onerror === 'function') {
options.onerror(e);
}
obs.error({notification: notif, event: e});
obs.error({ notification: notif, event: e });
};

notif.onclose = () => {
Expand All @@ -81,7 +81,12 @@ export class NotificationService {
});
}

getNotificationTypes(): Observable<{[key: string]: UserNotificationSettings }> {
return this._http.get<{[key: string]: UserNotificationSettings}>('/notification/type');
getNotificationTypes(): Observable<{ [key: string]: UserNotificationSettings }> {
return this._http.get<{ [key: string]: UserNotificationSettings }>('/notification/type');
}

getConditions(key: string, workflowName: string): Observable<WorkflowTriggerConditionCache> {
return this._http
.get<WorkflowTriggerConditionCache>(`/project/${key}/workflows/${workflowName}/notifications/conditions`);
}
}
3 changes: 1 addition & 2 deletions ui/src/app/shared/conditions/conditions.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,13 @@ export class ConditionsComponent extends Table<WorkflowNodeCondition> implements
}

@Input() project: Project;
@Input() pipelineId: number;

_conditions: WorkflowNodeConditions;
@Input() readonly = true;

@Output() conditionsChange = new EventEmitter<WorkflowNodeConditions>();

@ViewChild('textareaCodeMirror', {static: false}) codemirror: CodemirrorComponent;
@ViewChild('textareaCodeMirror', { static: false }) codemirror: CodemirrorComponent;
codeMirrorConfig: any;
isAdvanced = false;
suggest: Array<string> = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Store } from '@ngxs/store';
import { PermissionValue } from 'app/model/permission.model';
Expand Down Expand Up @@ -41,7 +32,7 @@ import { Subscription } from 'rxjs/Subscription';
})
@AutoUnsubscribe()
export class WorkflowWizardNodeConditionComponent extends Table<WorkflowNodeCondition> implements OnInit {
@ViewChild('textareaCodeMirror', {static: false}) codemirror: any;
@ViewChild('textareaCodeMirror', { static: false }) codemirror: any;

@Input() project: Project;
@Input() workflow: Workflow;
Expand Down Expand Up @@ -76,8 +67,6 @@ export class WorkflowWizardNodeConditionComponent extends Table<WorkflowNodeCond
codeMirrorConfig: any;
suggest: Array<string> = [];
loadingConditions = false;
operators: Array<any>;
conditionNames: Array<string>;
permission = PermissionValue;
statuses = [PipelineStatus.SUCCESS, PipelineStatus.FAIL, PipelineStatus.SKIPPED];
loading = false;
Expand Down Expand Up @@ -131,13 +120,7 @@ export class WorkflowWizardNodeConditionComponent extends Table<WorkflowNodeCond
this._cd.markForCheck();
})
)
.subscribe(wtc => {
this.triggerConditions = wtc;
this.operators = Object.keys(wtc.operators).map(k => {
return { key: k, value: wtc.operators[k] };
});
this.conditionNames = wtc.names;
});
.subscribe(wtc => this.triggerConditions = wtc);
}

updateWorkflow(): void {
Expand Down
Loading

0 comments on commit 4eb79dc

Please sign in to comment.