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