]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
import 15.2.4
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / rgw / rgw-bucket-form / rgw-bucket-form.component.ts
CommitLineData
11fdf7f2
TL
1import { Component, OnInit } from '@angular/core';
2import { AbstractControl, AsyncValidatorFn, ValidationErrors, Validators } from '@angular/forms';
3import { ActivatedRoute, Router } from '@angular/router';
4
5import { I18n } from '@ngx-translate/i18n-polyfill';
6import * as _ from 'lodash';
7
8import { RgwBucketService } from '../../../shared/api/rgw-bucket.service';
9f95a23c 9import { RgwSiteService } from '../../../shared/api/rgw-site.service';
11fdf7f2
TL
10import { RgwUserService } from '../../../shared/api/rgw-user.service';
11import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
9f95a23c 12import { Icons } from '../../../shared/enum/icons.enum';
11fdf7f2
TL
13import { NotificationType } from '../../../shared/enum/notification-type.enum';
14import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
15import { CdFormGroup } from '../../../shared/forms/cd-form-group';
9f95a23c 16import { CdValidators } from '../../../shared/forms/cd-validators';
11fdf7f2 17import { NotificationService } from '../../../shared/services/notification.service';
9f95a23c
TL
18import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
19import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
11fdf7f2
TL
20
21@Component({
22 selector: 'cd-rgw-bucket-form',
23 templateUrl: './rgw-bucket-form.component.html',
24 styleUrls: ['./rgw-bucket-form.component.scss']
25})
26export class RgwBucketFormComponent implements OnInit {
27 bucketForm: CdFormGroup;
28 editing = false;
29 error = false;
30 loading = false;
9f95a23c 31 owners: string[] = null;
11fdf7f2
TL
32 action: string;
33 resource: string;
9f95a23c
TL
34 zonegroup: string;
35 placementTargets: object[] = [];
36 isVersioningEnabled = false;
37 isVersioningAlreadyEnabled = false;
38 isMfaDeleteEnabled = false;
39 isMfaDeleteAlreadyEnabled = false;
40 icons = Icons;
11fdf7f2
TL
41
42 constructor(
43 private route: ActivatedRoute,
44 private router: Router,
45 private formBuilder: CdFormBuilder,
46 private rgwBucketService: RgwBucketService,
9f95a23c 47 private rgwSiteService: RgwSiteService,
11fdf7f2
TL
48 private rgwUserService: RgwUserService,
49 private notificationService: NotificationService,
50 private i18n: I18n,
51 public actionLabels: ActionLabelsI18n
52 ) {
53 this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
54 this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
55 this.resource = this.i18n('bucket');
56 this.createForm();
57 }
58
59 createForm() {
9f95a23c
TL
60 const self = this;
61 const eitherDaysOrYears = CdValidators.custom('eitherDaysOrYears', () => {
62 if (!self.bucketForm || !_.get(self.bucketForm.getRawValue(), 'lock_enabled')) {
63 return false;
64 }
65 const years = self.bucketForm.getValue('lock_retention_period_years');
66 const days = self.bucketForm.getValue('lock_retention_period_days');
67 return (days > 0 && years > 0) || (days === 0 && years === 0);
68 });
69 const lockPeriodDefinition = [0, [CdValidators.number(false), eitherDaysOrYears]];
11fdf7f2
TL
70 this.bucketForm = this.formBuilder.group({
71 id: [null],
9f95a23c
TL
72 bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]],
73 owner: [null, [Validators.required]],
74 'placement-target': [null, this.editing ? [] : [Validators.required]],
75 versioning: [null],
76 'mfa-delete': [null],
77 'mfa-token-serial': [''],
78 'mfa-token-pin': [''],
79 lock_enabled: [{ value: false, disabled: this.editing }],
80 lock_mode: ['COMPLIANCE'],
81 lock_retention_period_days: lockPeriodDefinition,
82 lock_retention_period_years: lockPeriodDefinition
11fdf7f2
TL
83 });
84 }
85
86 ngOnInit() {
87 // Get the list of possible owners.
88 this.rgwUserService.enumerate().subscribe((resp: string[]) => {
89 this.owners = resp.sort();
90 });
91
9f95a23c
TL
92 if (!this.editing) {
93 // Get placement targets:
e306af50 94 this.rgwSiteService.get('placement-targets').subscribe((placementTargets: any) => {
9f95a23c
TL
95 this.zonegroup = placementTargets['zonegroup'];
96 _.forEach(placementTargets['placement_targets'], (placementTarget) => {
97 placementTarget['description'] = `${placementTarget['name']} (${this.i18n('pool')}: ${
98 placementTarget['data_pool']
99 })`;
100 this.placementTargets.push(placementTarget);
11fdf7f2 101 });
9f95a23c
TL
102
103 // If there is only 1 placement target, select it by default:
104 if (this.placementTargets.length === 1) {
105 this.bucketForm.get('placement-target').setValue(this.placementTargets[0]['name']);
106 }
107 });
108 }
109
110 // Process route parameters.
111 this.route.params.subscribe((params: { bid: string }) => {
112 if (!params.hasOwnProperty('bid')) {
113 return;
11fdf7f2 114 }
9f95a23c
TL
115 const bid = decodeURIComponent(params.bid);
116 this.loading = true;
117
118 this.rgwBucketService.get(bid).subscribe((resp: object) => {
119 this.loading = false;
120 // Get the default values (incl. the values from disabled fields).
121 const defaults = _.clone(this.bucketForm.getRawValue());
122 // Get the values displayed in the form. We need to do that to
123 // extract those key/value pairs from the response data, otherwise
124 // the Angular react framework will throw an error if there is no
125 // field for a given key.
126 let value: object = _.pick(resp, _.keys(defaults));
127 value['placement-target'] = resp['placement_rule'];
128 // Append default values.
129 value = _.merge(defaults, value);
130 // Update the form.
131 this.bucketForm.setValue(value);
132 if (this.editing) {
133 this.setVersioningStatus(resp['versioning']);
134 this.isVersioningAlreadyEnabled = this.isVersioningEnabled;
135 this.setMfaDeleteStatus(resp['mfa_delete']);
136 this.isMfaDeleteAlreadyEnabled = this.isMfaDeleteEnabled;
137 this.setMfaDeleteValidators();
138 }
139 });
140 });
11fdf7f2
TL
141 }
142
143 goToListView() {
144 this.router.navigate(['/rgw/bucket']);
145 }
146
147 submit() {
148 // Exit immediately if the form isn't dirty.
149 if (this.bucketForm.pristine) {
150 this.goToListView();
151 return;
152 }
9f95a23c 153 const values = this.bucketForm.value;
11fdf7f2
TL
154 if (this.editing) {
155 // Edit
9f95a23c
TL
156 const versioning = this.getVersioningStatus();
157 const mfaDelete = this.getMfaDeleteStatus();
158 this.rgwBucketService
159 .update(
160 values['bid'],
161 values['id'],
162 values['owner'],
163 versioning,
164 mfaDelete,
165 values['mfa-token-serial'],
166 values['mfa-token-pin'],
167 values['lock_mode'],
168 values['lock_retention_period_days'],
169 values['lock_retention_period_years']
170 )
171 .subscribe(
172 () => {
173 this.notificationService.show(
174 NotificationType.success,
175 this.i18n('Updated Object Gateway bucket "{{bid}}".', values)
176 );
177 this.goToListView();
178 },
179 () => {
180 // Reset the 'Submit' button.
181 this.bucketForm.setErrors({ cdSubmitButton: true });
182 }
183 );
11fdf7f2
TL
184 } else {
185 // Add
9f95a23c
TL
186 this.rgwBucketService
187 .create(
188 values['bid'],
189 values['owner'],
190 this.zonegroup,
191 values['placement-target'],
192 values['lock_enabled'],
193 values['lock_mode'],
194 values['lock_retention_period_days'],
195 values['lock_retention_period_years']
196 )
197 .subscribe(
198 () => {
199 this.notificationService.show(
200 NotificationType.success,
201 this.i18n('Created Object Gateway bucket "{{bid}}"', values)
202 );
203 this.goToListView();
204 },
205 () => {
206 // Reset the 'Submit' button.
207 this.bucketForm.setErrors({ cdSubmitButton: true });
208 }
209 );
11fdf7f2
TL
210 }
211 }
212
9f95a23c
TL
213 /**
214 * Validate the bucket name. In general, bucket names should follow domain
215 * name constraints:
216 * - Bucket names must be unique.
217 * - Bucket names cannot be formatted as IP address.
218 * - Bucket names can be between 3 and 63 characters long.
219 * - Bucket names must not contain uppercase characters or underscores.
220 * - Bucket names must start with a lowercase letter or number.
221 * - Bucket names must be a series of one or more labels. Adjacent
222 * labels are separated by a single period (.). Bucket names can
223 * contain lowercase letters, numbers, and hyphens. Each label must
224 * start and end with a lowercase letter or a number.
225 */
11fdf7f2
TL
226 bucketNameValidator(): AsyncValidatorFn {
227 const rgwBucketService = this.rgwBucketService;
228 return (control: AbstractControl): Promise<ValidationErrors | null> => {
229 return new Promise((resolve) => {
230 // Exit immediately if user has not interacted with the control yet
231 // or the control value is empty.
232 if (control.pristine || control.value === '') {
233 resolve(null);
234 return;
235 }
9f95a23c
TL
236 const constraints = [];
237 // - Bucket names cannot be formatted as IP address.
238 constraints.push((name: AbstractControl) => {
239 const validatorFn = CdValidators.ip();
240 return !validatorFn(name);
241 });
242 // - Bucket names can be between 3 and 63 characters long.
243 constraints.push((name: string) => _.inRange(name.length, 3, 64));
244 // - Bucket names must not contain uppercase characters or underscores.
245 // - Bucket names must start with a lowercase letter or number.
246 // - Bucket names must be a series of one or more labels. Adjacent
247 // labels are separated by a single period (.). Bucket names can
248 // contain lowercase letters, numbers, and hyphens. Each label must
249 // start and end with a lowercase letter or a number.
250 constraints.push((name: string) => {
251 const labels = _.split(name, '.');
252 return _.every(labels, (label) => {
253 // Bucket names must not contain uppercase characters or underscores.
254 if (label !== _.toLower(label) || label.includes('_')) {
255 return false;
256 }
257 // Bucket names can contain lowercase letters, numbers, and hyphens.
258 if (!/[0-9a-z-]/.test(label)) {
259 return false;
260 }
261 // Each label must start and end with a lowercase letter or a number.
262 return _.every([0, label.length], (index) => {
263 return /[a-z]/.test(label[index]) || _.isInteger(_.parseInt(label[index]));
264 });
265 });
266 });
267 if (!_.every(constraints, (func: Function) => func(control.value))) {
11fdf7f2
TL
268 resolve({ bucketNameInvalid: true });
269 return;
270 }
9f95a23c 271 // - Bucket names must be unique.
11fdf7f2
TL
272 rgwBucketService.exists(control.value).subscribe((resp: boolean) => {
273 if (!resp) {
274 resolve(null);
275 } else {
276 resolve({ bucketNameExists: true });
277 }
278 });
279 });
280 };
281 }
9f95a23c
TL
282
283 areMfaCredentialsRequired() {
284 return (
285 this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled ||
286 (this.isMfaDeleteAlreadyEnabled &&
287 this.isVersioningEnabled !== this.isVersioningAlreadyEnabled)
288 );
289 }
290
291 setMfaDeleteValidators() {
292 const mfaTokenSerialControl = this.bucketForm.get('mfa-token-serial');
293 const mfaTokenPinControl = this.bucketForm.get('mfa-token-pin');
294
295 if (this.areMfaCredentialsRequired()) {
296 mfaTokenSerialControl.setValidators(Validators.required);
297 mfaTokenPinControl.setValidators(Validators.required);
298 } else {
299 mfaTokenSerialControl.setValidators(null);
300 mfaTokenPinControl.setValidators(null);
301 }
302
303 mfaTokenSerialControl.updateValueAndValidity();
304 mfaTokenPinControl.updateValueAndValidity();
305 }
306
307 getVersioningStatus() {
308 return this.isVersioningEnabled ? RgwBucketVersioning.ENABLED : RgwBucketVersioning.SUSPENDED;
309 }
310
311 setVersioningStatus(status: RgwBucketVersioning) {
312 this.isVersioningEnabled = status === RgwBucketVersioning.ENABLED;
313 }
314
315 updateVersioning() {
316 this.isVersioningEnabled = !this.isVersioningEnabled;
317 this.setMfaDeleteValidators();
318 }
319
320 getMfaDeleteStatus() {
321 return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;
322 }
323
324 setMfaDeleteStatus(status: RgwBucketMfaDelete) {
325 this.isMfaDeleteEnabled = status === RgwBucketMfaDelete.ENABLED;
326 }
327
328 updateMfaDelete() {
329 this.isMfaDeleteEnabled = !this.isMfaDeleteEnabled;
330 this.setMfaDeleteValidators();
331 }
11fdf7f2 332}