]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: simplify object locking fields in 'Bucket Creation' form 41777/head
authorAlfonso Martínez <almartin@redhat.com>
Thu, 3 Jun 2021 10:25:45 +0000 (12:25 +0200)
committerAlfonso Martínez <almartin@redhat.com>
Wed, 9 Jun 2021 07:10:42 +0000 (09:10 +0200)
- UI: The object locking retention period must be expressed in days.
  Years value from API (as API keeps param parity with RGW API) are converted to days.
- API: add more validation and increase test coverage.

Fixes: https://tracker.ceph.com/issues/49885
Signed-off-by: Alfonso Martínez <almartin@redhat.com>
(cherry picked from commit 09394a19913b8159a7608a594e29886eaae7570e)

src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
src/pybind/mgr/dashboard/services/rgw_client.py
src/pybind/mgr/dashboard/tests/test_rgw_client.py

index bd395f47315ae1b1f7083d14e2843b5a5a44109a..12a65613dd164d7cceca42e521deb10a3691c4f2 100644 (file)
@@ -201,8 +201,8 @@ class RgwBucket(RgwRESTController):
                      retention_period_days, retention_period_years):
         rgw_client = RgwClient.instance(owner, daemon_name)
         return rgw_client.set_bucket_locking(bucket_name, mode,
-                                             int(retention_period_days),
-                                             int(retention_period_years))
+                                             retention_period_days,
+                                             retention_period_years)
 
     @staticmethod
     def strip_tenant_from_bucket_name(bucket_name):
index 6950227aee97bb77a91aa3717122fd1b9be287d5..a17e73e80d0e9f46d994a35bc844ba35116ea8c2 100644 (file)
@@ -42,7 +42,6 @@ export class BucketsPageHelper extends PageHelper {
       this.selectLockMode('Compliance');
       cy.get('#lock_mode').should('have.class', 'ng-valid');
       cy.get('#lock_retention_period_days').type('3');
-      cy.get('#lock_retention_period_years').type('0');
     }
 
     // Click the create button and wait for bucket to be made
index be54ee84efb51ef048c8bb19a2c6a787dd0cc2c4..be46e01524ea7cf23cffab6fb68d5d9e282753a8 100644 (file)
                     *ngIf="bucketForm.showError('lock_retention_period_days', frm, 'pattern')"
                     i18n>The entered value must be a positive integer.</span>
               <span class="invalid-feedback"
-                    *ngIf="bucketForm.showError('lock_retention_period_days', frm, 'eitherDaysOrYears')"
-                    i18n>Retention period requires either Days or Years.</span>
-            </div>
-          </div>
-
-          <!-- Retention period (years) -->
-          <div *ngIf="bucketForm.getValue('lock_enabled')"
-               class="form-group row">
-            <label class="cd-col-form-label"
-                   for="lock_retention_period_years">
-              <ng-container i18n>Years</ng-container>
-              <cd-helper i18n>The number of years that you want to specify for the default retention period that will be applied to new objects placed in this bucket.</cd-helper>
-            </label>
-            <div class="cd-col-form-input">
-              <input class="form-control"
-                     type="number"
-                     id="lock_retention_period_years"
-                     formControlName="lock_retention_period_years"
-                     min="0">
-              <span class="invalid-feedback"
-                    *ngIf="bucketForm.showError('lock_retention_period_days', frm, 'pattern')"
-                    i18n>The entered value must be a positive integer.</span>
-              <span class="invalid-feedback"
-                    *ngIf="bucketForm.showError('lock_retention_period_years', frm, 'eitherDaysOrYears')"
-                    i18n>Retention period requires either Days or Years.</span>
+                    *ngIf="bucketForm.showError('lock_retention_period_days', frm, 'lockDays')"
+                    i18n>Retention Days must be a positive integer.</span>
             </div>
           </div>
         </fieldset>
index aa02c4cc0304cab166d08f41b2b37eefe3c3f0e2..574d807215f2f15d1bfce5c162c509ac4e6f4ea5 100644 (file)
@@ -318,29 +318,17 @@ describe('RgwBucketFormComponent', () => {
   });
 
   describe('object locking', () => {
-    const setDaysAndYears = (fn: (name: string) => void) => {
-      ['lock_retention_period_days', 'lock_retention_period_years'].forEach(fn);
-    };
-
     const expectPatternLockError = (value: string) => {
       formHelper.setValue('lock_enabled', true, true);
-      setDaysAndYears((name: string) => {
-        formHelper.setValue(name, value);
-        formHelper.expectError(name, 'pattern');
-      });
+      formHelper.setValue('lock_retention_period_days', value);
+      formHelper.expectError('lock_retention_period_days', 'pattern');
     };
 
-    const expectValidLockInputs = (enabled: boolean, mode: string, days: string, years: string) => {
+    const expectValidLockInputs = (enabled: boolean, mode: string, days: string) => {
       formHelper.setValue('lock_enabled', enabled);
       formHelper.setValue('lock_mode', mode);
       formHelper.setValue('lock_retention_period_days', days);
-      formHelper.setValue('lock_retention_period_years', years);
-      [
-        'lock_enabled',
-        'lock_mode',
-        'lock_retention_period_days',
-        'lock_retention_period_years'
-      ].forEach((name) => {
+      ['lock_enabled', 'lock_mode', 'lock_retention_period_days'].forEach((name) => {
         const control = component.bucketForm.get(name);
         expect(control.valid).toBeTruthy();
         expect(control.errors).toBeNull();
@@ -360,15 +348,13 @@ describe('RgwBucketFormComponent', () => {
       expect(control.disabled).toBeTruthy();
     });
 
-    it('should have the "eitherDaysOrYears" error', () => {
+    it('should have the "lockDays" error', () => {
       formHelper.setValue('lock_enabled', true);
-      setDaysAndYears((name: string) => {
-        const control = component.bucketForm.get(name);
-        control.updateValueAndValidity();
-        expect(control.value).toBe(0);
-        expect(control.invalid).toBeTruthy();
-        formHelper.expectError(control, 'eitherDaysOrYears');
-      });
+      const control = component.bucketForm.get('lock_retention_period_days');
+      control.updateValueAndValidity();
+      expect(control.value).toBe(0);
+      expect(control.invalid).toBeTruthy();
+      formHelper.expectError(control, 'lockDays');
     });
 
     it('should have the "pattern" error [1]', () => {
@@ -380,11 +366,16 @@ describe('RgwBucketFormComponent', () => {
     });
 
     it('should have valid values [1]', () => {
-      expectValidLockInputs(true, 'Governance', '0', '1');
+      expectValidLockInputs(true, 'Governance', '1');
     });
 
     it('should have valid values [2]', () => {
-      expectValidLockInputs(false, 'Compliance', '100', '0');
+      expectValidLockInputs(false, 'Compliance', '2');
+    });
+
+    it('should convert retention years to days', () => {
+      expect(component['getLockDays']({ lock_retention_period_years: 1000 })).toBe(365242);
+      expect(component['getLockDays']({ lock_retention_period_days: 5 })).toBe(5);
     });
   });
 });
index da3fb9f3e0b128a1279ab6e290b3c0e6916ad438..b1e3a4400a704caa4dc6dda5bd249f53b6210275 100644 (file)
@@ -63,15 +63,13 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
 
   createForm() {
     const self = this;
-    const eitherDaysOrYears = CdValidators.custom('eitherDaysOrYears', () => {
+    const lockDaysValidator = CdValidators.custom('lockDays', () => {
       if (!self.bucketForm || !_.get(self.bucketForm.getRawValue(), 'lock_enabled')) {
         return false;
       }
-      const years = self.bucketForm.getValue('lock_retention_period_years');
-      const days = self.bucketForm.getValue('lock_retention_period_days');
-      return (days > 0 && years > 0) || (days === 0 && years === 0);
+      const lockDays = Number(self.bucketForm.getValue('lock_retention_period_days'));
+      return !Number.isInteger(lockDays) || lockDays === 0;
     });
-    const lockPeriodDefinition = [0, [CdValidators.number(false), eitherDaysOrYears]];
     this.bucketForm = this.formBuilder.group({
       id: [null],
       bid: [null, [Validators.required], this.editing ? [] : [this.bucketNameValidator()]],
@@ -83,8 +81,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
       'mfa-token-pin': [''],
       lock_enabled: [{ value: false, disabled: this.editing }],
       lock_mode: ['COMPLIANCE'],
-      lock_retention_period_days: lockPeriodDefinition,
-      lock_retention_period_years: lockPeriodDefinition
+      lock_retention_period_days: [0, [CdValidators.number(false), lockDaysValidator]]
     });
   }
 
@@ -135,6 +132,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
           // the Angular react framework will throw an error if there is no
           // field for a given key.
           let value: object = _.pick(bidResp, _.keys(defaults));
+          value['lock_retention_period_days'] = this.getLockDays(bidResp);
           value['placement-target'] = bidResp['placement_rule'];
           value['versioning'] = bidResp['versioning'] === RgwBucketVersioning.ENABLED;
           value['mfa-delete'] = bidResp['mfa_delete'] === RgwBucketMfaDelete.ENABLED;
@@ -184,8 +182,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
           values['mfa-token-serial'],
           values['mfa-token-pin'],
           values['lock_mode'],
-          values['lock_retention_period_days'],
-          values['lock_retention_period_years']
+          values['lock_retention_period_days']
         )
         .subscribe(
           () => {
@@ -210,8 +207,7 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
           values['placement-target'],
           values['lock_enabled'],
           values['lock_mode'],
-          values['lock_retention_period_days'],
-          values['lock_retention_period_years']
+          values['lock_retention_period_days']
         )
         .subscribe(
           () => {
@@ -362,4 +358,12 @@ export class RgwBucketFormComponent extends CdForm implements OnInit {
   getMfaDeleteStatus() {
     return this.isMfaDeleteEnabled ? RgwBucketMfaDelete.ENABLED : RgwBucketMfaDelete.DISABLED;
   }
+
+  private getLockDays(bucketData: object): number {
+    if (bucketData['lock_retention_period_years'] > 0) {
+      return Math.floor(bucketData['lock_retention_period_years'] * 365.242);
+    }
+
+    return bucketData['lock_retention_period_days'];
+  }
 }
index 32c88b8c947511724732b7203461c49fa5ba94d7..e457c2ab12277dc71e09780b7f7f4713d23b5cf9 100644 (file)
@@ -41,20 +41,20 @@ describe('RgwBucketService', () => {
 
   it('should call create', () => {
     service
-      .create('foo', 'bar', 'default', 'default-placement', false, 'COMPLIANCE', '10', '0')
+      .create('foo', 'bar', 'default', 'default-placement', false, 'COMPLIANCE', '5')
       .subscribe();
     const req = httpTesting.expectOne(
-      `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=10&lock_retention_period_years=0&${RgwHelper.DAEMON_QUERY_PARAM}`
+      `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&placement_target=default-placement&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&${RgwHelper.DAEMON_QUERY_PARAM}`
     );
     expect(req.request.method).toBe('POST');
   });
 
   it('should call update', () => {
     service
-      .update('foo', 'bar', 'baz', 'Enabled', 'Enabled', '1', '223344', 'GOVERNANCE', '0', '1')
+      .update('foo', 'bar', 'baz', 'Enabled', 'Enabled', '1', '223344', 'GOVERNANCE', '10')
       .subscribe();
     const req = httpTesting.expectOne(
-      `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=0&lock_retention_period_years=1`
+      `api/rgw/bucket/foo?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_id=bar&uid=baz&versioning_state=Enabled&mfa_delete=Enabled&mfa_token_serial=1&mfa_token_pin=223344&lock_mode=GOVERNANCE&lock_retention_period_days=10`
     );
     expect(req.request.method).toBe('PUT');
   });
index 03fe5ca017dcec39ac6f046587d4999645cf604d..c1c02629cdd5fe8bee1b11499f8e42981f6098a5 100644 (file)
@@ -41,8 +41,7 @@ export class RgwBucketService {
     placementTarget: string,
     lockEnabled: boolean,
     lock_mode: 'GOVERNANCE' | 'COMPLIANCE',
-    lock_retention_period_days: string,
-    lock_retention_period_years: string
+    lock_retention_period_days: string
   ) {
     return this.rgwDaemonService.request((params: HttpParams) => {
       return this.http.post(this.url, null, {
@@ -55,7 +54,6 @@ export class RgwBucketService {
             lock_enabled: String(lockEnabled),
             lock_mode,
             lock_retention_period_days,
-            lock_retention_period_years,
             daemon_name: params.get('daemon_name')
           }
         })
@@ -72,8 +70,7 @@ export class RgwBucketService {
     mfaTokenSerial: string,
     mfaTokenPin: string,
     lockMode: 'GOVERNANCE' | 'COMPLIANCE',
-    lockRetentionPeriodDays: string,
-    lockRetentionPeriodYears: string
+    lockRetentionPeriodDays: string
   ) {
     return this.rgwDaemonService.request((params: HttpParams) => {
       params = params.append('bucket_id', bucketId);
@@ -84,7 +81,6 @@ export class RgwBucketService {
       params = params.append('mfa_token_pin', mfaTokenPin);
       params = params.append('lock_mode', lockMode);
       params = params.append('lock_retention_period_days', lockRetentionPeriodDays);
-      params = params.append('lock_retention_period_years', lockRetentionPeriodYears);
       return this.http.put(`${this.url}/${bucket}`, null, { params: params });
     });
   }
index 598d05ee39df9cfe5abda89d2eb58a71b50f7f18..25d1ed5ddac102d982b8b7b2a456248400e2477a 100644 (file)
@@ -15,7 +15,7 @@ from ..settings import Settings
 from ..tools import build_url, dict_contains_path, dict_get, json_str_to_object
 
 try:
-    from typing import Any, Dict, List, Optional, Tuple
+    from typing import Any, Dict, List, Optional, Tuple, Union
 except ImportError:
     pass  # For typing only
 
@@ -608,12 +608,11 @@ class RgwClient(RestClient):
 
     @RestClient.api_put('/{bucket_name}?object-lock')
     def set_bucket_locking(self,
-                           bucket_name,
-                           mode,
-                           retention_period_days,
-                           retention_period_years,
-                           request=None):
-        # type: (str, str, int, int, Optional[object]) -> None
+                           bucket_name: str,
+                           mode: str,
+                           retention_period_days: Optional[Union[int, str]] = None,
+                           retention_period_years: Optional[Union[int, str]] = None,
+                           request: Optional[object] = None) -> None:
         """
         Places the locking configuration on the specified bucket. The
         locking configuration will be applied by default to every new
@@ -631,6 +630,14 @@ class RgwClient(RestClient):
         # pylint: disable=unused-argument
 
         # Do some validations.
+        try:
+            retention_period_days = int(retention_period_days) if retention_period_days else 0
+            retention_period_years = int(retention_period_years) if retention_period_years else 0
+            if retention_period_days < 0 or retention_period_years < 0:
+                raise ValueError
+        except (TypeError, ValueError):
+            msg = "Retention period must be a positive integer."
+            raise DashboardException(msg=msg, component='rgw')
         if retention_period_days and retention_period_years:
             # https://docs.aws.amazon.com/AmazonS3/latest/API/archive-RESTBucketPUTObjectLockConfiguration.html
             msg = "Retention period requires either Days or Years. "\
@@ -640,6 +647,9 @@ class RgwClient(RestClient):
             msg = "Retention period requires either Days or Years. "\
                 "You must specify at least one."
             raise DashboardException(msg=msg, component='rgw')
+        if not isinstance(mode, str) or mode.upper() not in ['COMPLIANCE', 'GOVERNANCE']:
+            msg = "Retention mode must be either COMPLIANCE or GOVERNANCE."
+            raise DashboardException(msg=msg, component='rgw')
 
         # Generate the XML data like this:
         # <ObjectLockConfiguration>
index edc10071f35217918ba8238a60dcb9425de3c60b..b01076c8b19efe0ec06dbeb8a606ef48366e4427 100644 (file)
@@ -103,6 +103,49 @@ class RgwClientTest(TestCase, KVStoreMockMixin):
         self.assertEqual(['realm1', 'realm2'], instance.get_realms())
         self.assertEqual([], instance.get_realms())
 
+    def test_set_bucket_locking_error(self):
+        instance = RgwClient.admin_instance()
+        test_params = [
+            ('COMPLIANCE', 'null', None, 'must be a positive integer'),
+            ('COMPLIANCE', None, 'null', 'must be a positive integer'),
+            ('COMPLIANCE', -1, None, 'must be a positive integer'),
+            ('COMPLIANCE', None, -1, 'must be a positive integer'),
+            ('COMPLIANCE', 1, 1, 'You can\'t specify both at the same time'),
+            ('COMPLIANCE', None, None, 'You must specify at least one'),
+            ('COMPLIANCE', 0, 0, 'You must specify at least one'),
+            (None, 1, 0, 'must be either COMPLIANCE or GOVERNANCE'),
+            ('', 1, 0, 'must be either COMPLIANCE or GOVERNANCE'),
+            ('FAKE_MODE', 1, 0, 'must be either COMPLIANCE or GOVERNANCE')
+        ]
+        for params in test_params:
+            mode, days, years, error_msg = params
+            with self.assertRaises(DashboardException) as cm:
+                instance.set_bucket_locking(
+                    bucket_name='test',
+                    mode=mode,
+                    retention_period_days=days,
+                    retention_period_years=years
+                )
+            self.assertIn(error_msg, str(cm.exception))
+
+    @patch('dashboard.rest_client._Request', Mock())
+    def test_set_bucket_locking_success(self):
+        instance = RgwClient.admin_instance()
+        test_params = [
+            ('Compliance', '1', None),
+            ('Governance', 1, None),
+            ('COMPLIANCE', None, '1'),
+            ('GOVERNANCE', None, 1),
+        ]
+        for params in test_params:
+            mode, days, years = params
+            self.assertIsNone(instance.set_bucket_locking(
+                bucket_name='test',
+                mode=mode,
+                retention_period_days=days,
+                retention_period_years=years
+            ))
+
 
 class RgwClientHelperTest(TestCase):
     def test_parse_frontend_config_1(self):