1 import { Component, OnInit } from '@angular/core';
2 import { AbstractControl, AsyncValidatorFn, ValidationErrors, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
5 import _ from 'lodash';
6 import { forkJoin, Observable, of as observableOf, timer as observableTimer } from 'rxjs';
7 import { map, switchMapTo } from 'rxjs/operators';
9 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
10 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
11 import { RgwUserService } from '~/app/shared/api/rgw-user.service';
12 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
13 import { Icons } from '~/app/shared/enum/icons.enum';
14 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
15 import { CdForm } from '~/app/shared/forms/cd-form';
16 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
17 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
18 import { CdValidators } from '~/app/shared/forms/cd-validators';
19 import { NotificationService } from '~/app/shared/services/notification.service';
20 import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
21 import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
24 selector: 'cd-rgw-bucket-form',
25 templateUrl: './rgw-bucket-form.component.html',
26 styleUrls: ['./rgw-bucket-form.component.scss']
28 export class RgwBucketFormComponent extends CdForm implements OnInit {
29 bucketForm: CdFormGroup;
31 owners: string[] = null;
35 placementTargets: object[] = [];
36 isVersioningAlreadyEnabled = false;
37 isMfaDeleteAlreadyEnabled = false;
40 get isVersioningEnabled(): boolean {
41 return this.bucketForm.getValue('versioning');
43 get isMfaDeleteEnabled(): boolean {
44 return this.bucketForm.getValue('mfa-delete');
48 private route: ActivatedRoute,
49 private router: Router,
50 private formBuilder: CdFormBuilder,
51 private rgwBucketService: RgwBucketService,
52 private rgwSiteService: RgwSiteService,
53 private rgwUserService: RgwUserService,
54 private notificationService: NotificationService,
55 public actionLabels: ActionLabelsI18n
58 this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
59 this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
60 this.resource = $localize`bucket`;
66 const lockDaysValidator = CdValidators.custom('lockDays', () => {
67 if (!self.bucketForm || !_.get(self.bucketForm.getRawValue(), 'lock_enabled')) {
70 const lockDays = Number(self.bucketForm.getValue('lock_retention_period_days'));
71 return !Number.isInteger(lockDays) || lockDays === 0;
73 this.bucketForm = this.formBuilder.group({
75 bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]],
76 owner: [null, [Validators.required]],
77 'placement-target': [null, this.editing ? [] : [Validators.required]],
80 'mfa-token-serial': [''],
81 'mfa-token-pin': [''],
82 lock_enabled: [{ value: false, disabled: this.editing }],
83 lock_mode: ['COMPLIANCE'],
84 lock_retention_period_days: [0, [CdValidators.number(false), lockDaysValidator]]
90 owners: this.rgwUserService.enumerate()
94 promises['getPlacementTargets'] = this.rgwSiteService.get('placement-targets');
97 // Process route parameters.
98 this.route.params.subscribe((params: { bid: string }) => {
99 if (params.hasOwnProperty('bid')) {
100 const bid = decodeURIComponent(params.bid);
101 promises['getBid'] = this.rgwBucketService.get(bid);
104 forkJoin(promises).subscribe((data: any) => {
105 // Get the list of possible owners.
106 this.owners = (<string[]>data.owners).sort();
108 // Get placement targets:
109 if (data['getPlacementTargets']) {
110 const placementTargets = data['getPlacementTargets'];
111 this.zonegroup = placementTargets['zonegroup'];
112 _.forEach(placementTargets['placement_targets'], (placementTarget) => {
113 placementTarget['description'] = `${placementTarget['name']} (${$localize`pool`}: ${
114 placementTarget['data_pool']
116 this.placementTargets.push(placementTarget);
119 // If there is only 1 placement target, select it by default:
120 if (this.placementTargets.length === 1) {
121 this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']);
125 if (data['getBid']) {
126 const bidResp = data['getBid'];
127 // Get the default values (incl. the values from disabled fields).
128 const defaults = _.clone(this.bucketForm.getRawValue());
130 // Get the values displayed in the form. We need to do that to
131 // extract those key/value pairs from the response data, otherwise
132 // the Angular react framework will throw an error if there is no
133 // field for a given key.
134 let value: object = _.pick(bidResp, _.keys(defaults));
135 value['lock_retention_period_days'] = this.rgwBucketService.getLockDays(bidResp);
136 value['placement-target'] = bidResp['placement_rule'];
137 value['versioning'] = bidResp['versioning'] === RgwBucketVersioning.ENABLED;
138 value['mfa-delete'] = bidResp['mfa_delete'] === RgwBucketMfaDelete.ENABLED;
140 // Append default values.
141 value = _.merge(defaults, value);
144 this.bucketForm.setValue(value);
146 this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
147 this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
148 this.setMfaDeleteValidators();
149 if (value['lock_enabled']) {
150 this.bucketForm.controls['versioning'].disable();
161 this.router.navigate(['/rgw/bucket']);
165 // Exit immediately if the form isn't dirty.
166 if (this.bucketForm.pristine) {
170 const values = this.bucketForm.value;
173 const versioning = this.getVersioningStatus();
174 const mfaDelete = this.getMfaDeleteStatus();
175 this.rgwBucketService
182 values['mfa-token-serial'],
183 values['mfa-token-pin'],
185 values['lock_retention_period_days']
189 this.notificationService.show(
190 NotificationType.success,
191 $localize`Updated Object Gateway bucket '${values.bid}'.`
196 // Reset the 'Submit' button.
197 this.bucketForm.setErrors({ cdSubmitButton: true });
202 this.rgwBucketService
207 values['placement-target'],
208 values['lock_enabled'],
210 values['lock_retention_period_days']
214 this.notificationService.show(
215 NotificationType.success,
216 $localize`Created Object Gateway bucket '${values.bid}'`
221 // Reset the 'Submit' button.
222 this.bucketForm.setErrors({ cdSubmitButton: true });
229 * Validate the bucket name. In general, bucket names should follow domain
231 * - Bucket names must be unique.
232 * - Bucket names cannot be formatted as IP address.
233 * - Bucket names can be between 3 and 63 characters long.
234 * - Bucket names must not contain uppercase characters or underscores.
235 * - Bucket names must start with a lowercase letter or number.
236 * - Bucket names must be a series of one or more labels. Adjacent
237 * labels are separated by a single period (.). Bucket names can
238 * contain lowercase letters, numbers, and hyphens. Each label must
239 * start and end with a lowercase letter or a number.
241 bucketNameValidator(): AsyncValidatorFn {
242 return (control: AbstractControl): Observable<ValidationErrors | null> => {
243 // Exit immediately if user has not interacted with the control yet
244 // or the control value is empty.
245 if (control.pristine || control.value === '') {
246 return observableOf(null);
248 const constraints = [];
249 let errorName: string;
250 // - Bucket names cannot be formatted as IP address.
251 constraints.push(() => {
252 const ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
253 const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
254 const name = this.bucketForm.get('bid').value;
256 if (ipv4Rgx.test(name) || ipv6Rgx.test(name)) {
257 errorName = 'ipAddress';
262 // - Bucket names can be between 3 and 63 characters long.
263 constraints.push((name: string) => {
264 if (!_.inRange(name.length, 3, 64)) {
265 errorName = 'shouldBeInRange';
270 // - Bucket names must not contain uppercase characters or underscores.
271 // - Bucket names must start with a lowercase letter or number.
272 // - Bucket names must be a series of one or more labels. Adjacent
273 // labels are separated by a single period (.). Bucket names can
274 // contain lowercase letters, numbers, and hyphens. Each label must
275 // start and end with a lowercase letter or a number.
276 constraints.push((name: string) => {
277 const labels = _.split(name, '.');
278 return _.every(labels, (label) => {
279 // Bucket names must not contain uppercase characters or underscores.
280 if (label !== _.toLower(label) || label.includes('_')) {
281 errorName = 'containsUpperCase';
284 // Bucket names can contain lowercase letters, numbers, and hyphens.
285 if (!/^\S*$/.test(name) || !/[0-9a-z-]/.test(label)) {
286 errorName = 'onlyLowerCaseAndNumbers';
289 // Each label must start and end with a lowercase letter or a number.
290 return _.every([0, label.length - 1], (index) => {
291 errorName = 'lowerCaseOrNumber';
292 return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
296 if (!_.every(constraints, (func: Function) => func(control.value))) {
297 return observableTimer().pipe(
300 case 'onlyLowerCaseAndNumbers':
301 return { onlyLowerCaseAndNumbers: true };
302 case 'shouldBeInRange':
303 return { shouldBeInRange: true };
305 return { ipAddress: true };
306 case 'containsUpperCase':
307 return { containsUpperCase: true };
308 case 'lowerCaseOrNumber':
309 return { lowerCaseOrNumber: true };
311 return { bucketNameInvalid: true };
316 // - Bucket names must be unique.
317 return observableTimer().pipe(
318 switchMapTo(this.rgwBucketService.exists.call(this.rgwBucketService, control.value)),
319 map((resp: boolean) => {
323 return { bucketNameExists: true };
330 areMfaCredentialsRequired() {
332 this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled ||
333 (this.isMfaDeleteAlreadyEnabled &&
334 this.isVersioningEnabled !== this.isVersioningAlreadyEnabled)
338 setMfaDeleteValidators() {
339 const mfaTokenSerialControl = this.bucketForm.get('mfa-token-serial');
340 const mfaTokenPinControl = this.bucketForm.get('mfa-token-pin');
342 if (this.areMfaCredentialsRequired()) {
343 mfaTokenSerialControl.setValidators(Validators.required);
344 mfaTokenPinControl.setValidators(Validators.required);
346 mfaTokenSerialControl.setValidators(null);
347 mfaTokenPinControl.setValidators(null);
350 mfaTokenSerialControl.updateValueAndValidity();
351 mfaTokenPinControl.updateValueAndValidity();
354 getVersioningStatus() {
355 return this.isVersioningEnabled ? RgwBucketVersioning.ENABLED : RgwBucketVersioning.SUSPENDED;
358 getMfaDeleteStatus() {
359 return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;