]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
aa241505c85ccdb1c74ab0fc4a8647b4d6d0a5cf
[ceph-ci.git] /
1 import { Component } from '@angular/core';
2 import { Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import { I18n } from '@ngx-translate/i18n-polyfill';
6 import * as _ from 'lodash';
7 import * as moment from 'moment';
8
9 import { PrometheusService } from '../../../../shared/api/prometheus.service';
10 import {
11   ActionLabelsI18n,
12   SucceededActionLabelsI18n
13 } from '../../../../shared/constants/app.constants';
14 import { Icons } from '../../../../shared/enum/icons.enum';
15 import { NotificationType } from '../../../../shared/enum/notification-type.enum';
16 import { CdFormBuilder } from '../../../../shared/forms/cd-form-builder';
17 import { CdFormGroup } from '../../../../shared/forms/cd-form-group';
18 import { CdValidators } from '../../../../shared/forms/cd-validators';
19 import {
20   AlertmanagerSilence,
21   AlertmanagerSilenceMatcher,
22   AlertmanagerSilenceMatcherMatch
23 } from '../../../../shared/models/alertmanager-silence';
24 import { Permission } from '../../../../shared/models/permissions';
25 import { AlertmanagerAlert, PrometheusRule } from '../../../../shared/models/prometheus-alerts';
26 import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
27 import { ModalService } from '../../../../shared/services/modal.service';
28 import { NotificationService } from '../../../../shared/services/notification.service';
29 import { PrometheusSilenceMatcherService } from '../../../../shared/services/prometheus-silence-matcher.service';
30 import { TimeDiffService } from '../../../../shared/services/time-diff.service';
31 import { SilenceMatcherModalComponent } from '../silence-matcher-modal/silence-matcher-modal.component';
32
33 @Component({
34   selector: 'cd-prometheus-form',
35   templateUrl: './silence-form.component.html',
36   styleUrls: ['./silence-form.component.scss']
37 })
38 export class SilenceFormComponent {
39   icons = Icons;
40   permission: Permission;
41   form: CdFormGroup;
42   rules: PrometheusRule[];
43
44   recreate = false;
45   edit = false;
46   id: string;
47
48   action: string;
49   resource = this.i18n('silence');
50
51   matchers: AlertmanagerSilenceMatcher[] = [];
52   matcherMatch: AlertmanagerSilenceMatcherMatch = undefined;
53   matcherConfig = [
54     {
55       tooltip: this.i18n('Attribute name'),
56       icon: this.icons.paragraph,
57       attribute: 'name'
58     },
59     {
60       tooltip: this.i18n('Value'),
61       icon: this.icons.terminal,
62       attribute: 'value'
63     },
64     {
65       tooltip: this.i18n('Regular expression'),
66       icon: this.icons.magic,
67       attribute: 'isRegex'
68     }
69   ];
70
71   datetimeFormat = 'YYYY-MM-DD HH:mm';
72
73   constructor(
74     private i18n: I18n,
75     private router: Router,
76     private authStorageService: AuthStorageService,
77     private formBuilder: CdFormBuilder,
78     private prometheusService: PrometheusService,
79     private notificationService: NotificationService,
80     private route: ActivatedRoute,
81     private timeDiff: TimeDiffService,
82     private modalService: ModalService,
83     private silenceMatcher: PrometheusSilenceMatcherService,
84     private actionLabels: ActionLabelsI18n,
85     private succeededLabels: SucceededActionLabelsI18n
86   ) {
87     this.init();
88   }
89
90   private init() {
91     this.chooseMode();
92     this.authenticate();
93     this.createForm();
94     this.setupDates();
95     this.getData();
96   }
97
98   private chooseMode() {
99     this.edit = this.router.url.startsWith('/monitoring/silences/edit');
100     this.recreate = this.router.url.startsWith('/monitoring/silences/recreate');
101     if (this.edit) {
102       this.action = this.actionLabels.EDIT;
103     } else if (this.recreate) {
104       this.action = this.actionLabels.RECREATE;
105     } else {
106       this.action = this.actionLabels.CREATE;
107     }
108   }
109
110   private authenticate() {
111     this.permission = this.authStorageService.getPermissions().prometheus;
112     const allowed =
113       this.permission.read && (this.edit ? this.permission.update : this.permission.create);
114     if (!allowed) {
115       this.router.navigate(['/404']);
116     }
117   }
118
119   private createForm() {
120     const formatValidator = CdValidators.custom('format', (expiresAt: string) => {
121       const result = expiresAt === '' || moment(expiresAt, this.datetimeFormat).isValid();
122       return !result;
123     });
124     this.form = this.formBuilder.group(
125       {
126         startsAt: ['', [Validators.required, formatValidator]],
127         duration: ['2h', [Validators.min(1)]],
128         endsAt: ['', [Validators.required, formatValidator]],
129         createdBy: [this.authStorageService.getUsername(), [Validators.required]],
130         comment: [null, [Validators.required]]
131       },
132       {
133         validators: CdValidators.custom('matcherRequired', () => this.matchers.length === 0)
134       }
135     );
136   }
137
138   private setupDates() {
139     const now = moment().format(this.datetimeFormat);
140     this.form.silentSet('startsAt', now);
141     this.updateDate();
142     this.subscribeDateChanges();
143   }
144
145   private updateDate(updateStartDate?: boolean) {
146     const date = moment(
147       this.form.getValue(updateStartDate ? 'endsAt' : 'startsAt'),
148       this.datetimeFormat
149     ).toDate();
150     const next = this.timeDiff.calculateDate(date, this.form.getValue('duration'), updateStartDate);
151     if (next) {
152       const nextDate = moment(next).format(this.datetimeFormat);
153       this.form.silentSet(updateStartDate ? 'startsAt' : 'endsAt', nextDate);
154     }
155   }
156
157   private subscribeDateChanges() {
158     this.form.get('startsAt').valueChanges.subscribe(() => {
159       this.onDateChange();
160     });
161     this.form.get('duration').valueChanges.subscribe(() => {
162       this.updateDate();
163     });
164     this.form.get('endsAt').valueChanges.subscribe(() => {
165       this.onDateChange(true);
166     });
167   }
168
169   private onDateChange(updateStartDate?: boolean) {
170     const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat);
171     const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat);
172     if (startsAt.isBefore(endsAt)) {
173       this.updateDuration();
174     } else {
175       this.updateDate(updateStartDate);
176     }
177   }
178
179   private updateDuration() {
180     const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat).toDate();
181     const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat).toDate();
182     this.form.silentSet('duration', this.timeDiff.calculateDuration(startsAt, endsAt));
183   }
184
185   private getData() {
186     this.getRules();
187     this.getModeSpecificData();
188   }
189
190   private getRules() {
191     this.prometheusService.ifPrometheusConfigured(
192       () =>
193         this.prometheusService.getRules().subscribe(
194           (groups) => {
195             this.rules = groups['groups'].reduce(
196               (acc, group) => _.concat<PrometheusRule>(acc, group.rules),
197               []
198             );
199           },
200           () => {
201             this.prometheusService.disablePrometheusConfig();
202             this.rules = [];
203           }
204         ),
205       () => {
206         this.rules = [];
207         this.notificationService.show(
208           NotificationType.info,
209           this.i18n(
210             'Please add your Prometheus host to the dashboard configuration and refresh the page'
211           ),
212           undefined,
213           undefined,
214           'Prometheus'
215         );
216       }
217     );
218   }
219
220   private getModeSpecificData() {
221     this.route.params.subscribe((params: { id: string }) => {
222       if (!params.id) {
223         return;
224       }
225       if (this.edit || this.recreate) {
226         this.prometheusService.getSilences(params).subscribe((silences) => {
227           this.fillFormWithSilence(silences[0]);
228         });
229       } else {
230         this.prometheusService.getAlerts(params).subscribe((alerts) => {
231           this.fillFormByAlert(alerts[0]);
232         });
233       }
234     });
235   }
236
237   private fillFormWithSilence(silence: AlertmanagerSilence) {
238     this.id = silence.id;
239     if (this.edit) {
240       ['startsAt', 'endsAt'].forEach((attr) =>
241         this.form.silentSet(attr, moment(silence[attr]).format(this.datetimeFormat))
242       );
243       this.updateDuration();
244     }
245     ['createdBy', 'comment'].forEach((attr) => this.form.silentSet(attr, silence[attr]));
246     this.matchers = silence.matchers;
247     this.validateMatchers();
248   }
249
250   private validateMatchers() {
251     if (!this.rules) {
252       window.setTimeout(() => this.validateMatchers(), 100);
253       return;
254     }
255     this.matcherMatch = this.silenceMatcher.multiMatch(this.matchers, this.rules);
256     this.form.markAsDirty();
257     this.form.updateValueAndValidity();
258   }
259
260   private fillFormByAlert(alert: AlertmanagerAlert) {
261     const labels = alert.labels;
262     Object.keys(labels).forEach((key) =>
263       this.setMatcher({
264         name: key,
265         value: labels[key],
266         isRegex: false
267       })
268     );
269   }
270
271   private setMatcher(matcher: AlertmanagerSilenceMatcher, index?: number) {
272     if (_.isNumber(index)) {
273       this.matchers[index] = matcher;
274     } else {
275       this.matchers.push(matcher);
276     }
277     this.validateMatchers();
278   }
279
280   showMatcherModal(index?: number) {
281     const modalRef = this.modalService.show(SilenceMatcherModalComponent);
282     const modalComponent = modalRef.componentInstance as SilenceMatcherModalComponent;
283     modalComponent.rules = this.rules;
284     if (_.isNumber(index)) {
285       modalComponent.editMode = true;
286       modalComponent.preFillControls(this.matchers[index]);
287     }
288     modalComponent.submitAction.subscribe((matcher: AlertmanagerSilenceMatcher) => {
289       this.setMatcher(matcher, index);
290     });
291   }
292
293   deleteMatcher(index: number) {
294     this.matchers.splice(index, 1);
295     this.validateMatchers();
296   }
297
298   submit() {
299     if (this.form.invalid) {
300       return;
301     }
302     this.prometheusService.setSilence(this.getSubmitData()).subscribe(
303       (resp) => {
304         this.router.navigate(['/monitoring/silences']);
305         this.notificationService.show(
306           NotificationType.success,
307           this.getNotificationTile(resp.body['silenceId']),
308           undefined,
309           undefined,
310           'Prometheus'
311         );
312       },
313       () => this.form.setErrors({ cdSubmitButton: true })
314     );
315   }
316
317   private getSubmitData(): AlertmanagerSilence {
318     const payload = this.form.value;
319     delete payload.duration;
320     payload.startsAt = moment(payload.startsAt, this.datetimeFormat).toISOString();
321     payload.endsAt = moment(payload.endsAt, this.datetimeFormat).toISOString();
322     payload.matchers = this.matchers;
323     if (this.edit) {
324       payload.id = this.id;
325     }
326     return payload;
327   }
328
329   private getNotificationTile(id: string) {
330     let action;
331     if (this.edit) {
332       action = this.succeededLabels.EDITED;
333     } else if (this.recreate) {
334       action = this.succeededLabels.RECREATED;
335     } else {
336       action = this.succeededLabels.CREATED;
337     }
338     return `${action} ${this.resource} ${id}`;
339   }
340 }