]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: When configuring the RGW Multisite endpoints from the UI allow FQDN...
authorAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Tue, 26 Nov 2024 09:56:38 +0000 (15:26 +0530)
committerAashish Sharma <Aashish.Sharma1@ibm.com>
Tue, 11 Mar 2025 12:08:50 +0000 (17:38 +0530)
When configuring the RGW Multisite endpoints from the UI allow FQDN, at the moment when using a FQDN it's not allowed

Fixes: https://tracker.ceph.com/issues/69055
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts

index 975c3f74c69ecb567946a1ea7b8a1a4579702117..282424164a225d39901ad2dc71395d81a283615c 100644 (file)
@@ -72,6 +72,7 @@
         "@types/lodash": "4.14.161",
         "@types/node": "18.17.12",
         "@types/swagger-ui": "3.52.0",
+        "@types/validator": "13.12.2",
         "@types/xml2js": "0.4.14",
         "@typescript-eslint/eslint-plugin": "8.14.0",
         "@typescript-eslint/parser": "8.14.0",
         "table": "6.8.0",
         "transifex-i18ntool": "1.1.0",
         "ts-node": "9.0.0",
-        "typescript": "5.4.5"
+        "typescript": "5.4.5",
+        "validator": "13.12.0"
       }
     },
     "node_modules/@aashutoshrathi/word-wrap": {
       "integrity": "sha512-pAeZeUbLE4Z9Vi9wsWV2bYPTweEHeJJy0G4pEjOA/FSvy1Ad5U5Km8iDV6TKre1mjBiVNfAdVHKruP8bAh4Q5A==",
       "dev": true
     },
+    "node_modules/@types/validator": {
+      "version": "13.12.2",
+      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz",
+      "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==",
+      "dev": true
+    },
     "node_modules/@types/wrap-ansi": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz",
         "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
       }
     },
+    "node_modules/validator": {
+      "version": "13.12.0",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
+      "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
     "node_modules/vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
index aa950f4cf60a6aab49b2db4c9f39a718d90f0c50..4dead7eca5043866b90c49142f7899d98250b868 100644 (file)
     "@types/lodash": "4.14.161",
     "@types/node": "18.17.12",
     "@types/swagger-ui": "3.52.0",
+    "@types/validator": "13.12.2",
     "@types/xml2js": "0.4.14",
     "@typescript-eslint/eslint-plugin": "8.14.0",
     "@typescript-eslint/parser": "8.14.0",
     "table": "6.8.0",
     "transifex-i18ntool": "1.1.0",
     "ts-node": "9.0.0",
-    "typescript": "5.4.5"
+    "typescript": "5.4.5",
+    "validator": "13.12.0"
   },
   "cypress-cucumber-preprocessor": {
     "stepDefinitions": "cypress/e2e/common"
index 9117e71c34b71d727d62d91d04a29cdacfad0efd..1277af79362acd42c94fce74facc1580c62cafec 100644 (file)
@@ -66,8 +66,8 @@
                 *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'required')"
                 i18n>This field is required.</span>
           <span class="invalid-feedback"
-                *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
-                i18n>Please enter a valid IP address.</span>
+                *ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'invalidURL')"
+                i18n>Please enter a valid URL.</span>
         </div>
       </div>
       <div class="form-group row">
                 *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'required')"
                 i18n>This field is required.</span>
           <span class="invalid-feedback"
-                *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'endpoint')"
-                i18n>Please enter a valid IP address.</span>
+                *ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'invalidURL')"
+                i18n>Please enter a valid URL.</span>
         </div>
       </div>
       <div class="form-group row">
index d9ad56a5bf4da977eb538cf3ce96b71ddbf539ff..84fd3eb1ae44780419d628d78af45f7906eb0c46 100644 (file)
@@ -21,10 +21,6 @@ import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
   styleUrls: ['./rgw-multisite-migrate.component.scss']
 })
 export class RgwMultisiteMigrateComponent implements OnInit {
-  readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
-  readonly 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;
-  readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
-
   @Output()
   submitAction = new EventEmitter();
 
@@ -84,57 +80,12 @@ export class RgwMultisiteMigrateComponent implements OnInit {
           })
         ]
       }),
-      zone_endpoints: new UntypedFormControl([], {
-        validators: [
-          CdValidators.custom('endpoint', (value: string) => {
-            if (_.isEmpty(value)) {
-              return false;
-            } else {
-              if (value.includes(',')) {
-                value.split(',').forEach((url: string) => {
-                  return (
-                    !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
-                  );
-                });
-              } else {
-                return (
-                  !this.endpoints.test(value) &&
-                  !this.ipv4Rgx.test(value) &&
-                  !this.ipv6Rgx.test(value)
-                );
-              }
-              return false;
-            }
-          }),
-          Validators.required
-        ]
+      zone_endpoints: new UntypedFormControl(null, {
+        validators: [CdValidators.url, Validators.required]
+      }),
+      zonegroup_endpoints: new UntypedFormControl(null, {
+        validators: [CdValidators.url, Validators.required]
       }),
-      zonegroup_endpoints: new UntypedFormControl(
-        [],
-        [
-          CdValidators.custom('endpoint', (value: string) => {
-            if (_.isEmpty(value)) {
-              return false;
-            } else {
-              if (value.includes(',')) {
-                value.split(',').forEach((url: string) => {
-                  return (
-                    !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
-                  );
-                });
-              } else {
-                return (
-                  !this.endpoints.test(value) &&
-                  !this.ipv4Rgx.test(value) &&
-                  !this.ipv6Rgx.test(value)
-                );
-              }
-              return false;
-            }
-          }),
-          Validators.required
-        ]
-      ),
       username: new UntypedFormControl(null, {
         validators: [Validators.required]
       })
index e6ad0603f17fa0f97aca725be225aa55a9ffbaaf..c6a001be3fb004301093d2eb3d390db37469713b 100644 (file)
                 *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'required')"
                 i18n>This field is required.</span>
           <span class="invalid-feedback"
-                *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'endpoint')"
-                i18n>Please enter a valid IP address.</span>
+                *ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'invalidURL')"
+                i18n>Please enter a valid URL.</span>
         </div>
       </div>
       <div class="form-group row">
index 03c14c43c752ee047658ae42f03ff7e8e140d15d..01b5abda40ed5adde04180e9eaa4c922138aa46e 100644 (file)
@@ -20,9 +20,6 @@ import { ModalService } from '~/app/shared/services/modal.service';
   styleUrls: ['./rgw-multisite-zone-form.component.scss']
 })
 export class RgwMultisiteZoneFormComponent implements OnInit {
-  readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
-  readonly 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;
-  readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
   action: string;
   info: any;
   multisiteZoneForm: CdFormGroup;
@@ -88,29 +85,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
       master_zone: new UntypedFormControl(false),
       selectedZonegroup: new UntypedFormControl(null),
       zone_endpoints: new UntypedFormControl(null, {
-        validators: [
-          CdValidators.custom('endpoint', (value: string) => {
-            if (_.isEmpty(value)) {
-              return false;
-            } else {
-              if (value.includes(',')) {
-                value.split(',').forEach((url: string) => {
-                  return (
-                    !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
-                  );
-                });
-              } else {
-                return (
-                  !this.endpoints.test(value) &&
-                  !this.ipv4Rgx.test(value) &&
-                  !this.ipv6Rgx.test(value)
-                );
-              }
-              return false;
-            }
-          }),
-          Validators.required
-        ]
+        validators: [CdValidators.url, Validators.required]
       }),
       access_key: new UntypedFormControl('', {}),
       secret_key: new UntypedFormControl('', {}),
index fe32f082cbc45646ac7eedfe59743f2bcf6df559..196426bc5b60464a438c971f5a2e3d26c66b63f7 100644 (file)
@@ -99,8 +99,8 @@
               *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'required')"
               i18n>This field is required.</span>
         <span class="invalid-feedback"
-              *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
-              i18n>Please enter a valid IP address.</span>
+              *ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'invalidURL')"
+              i18n>Please enter a valid URL.</span>
         </div>
       </div>
       <div class="form-group row"
index bf1054eb5b2f555e0c6b6831bc71bbfb83cd4dff..95791ffd22e1016b595f1818eb0532f180e825de 100644 (file)
@@ -24,9 +24,6 @@ import { SelectOption } from '~/app/shared/components/select/select-option.model
   styleUrls: ['./rgw-multisite-zonegroup-form.component.scss']
 })
 export class RgwMultisiteZonegroupFormComponent implements OnInit {
-  readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
-  readonly 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;
-  readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
   action: string;
   icons = Icons;
   multisiteZonegroupForm: CdFormGroup;
@@ -85,29 +82,9 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
       }),
       master_zonegroup: new UntypedFormControl(false),
       selectedRealm: new UntypedFormControl(null),
-      zonegroup_endpoints: new UntypedFormControl(null, [
-        CdValidators.custom('endpoint', (value: string) => {
-          if (_.isEmpty(value)) {
-            return false;
-          } else {
-            if (value.includes(',')) {
-              value.split(',').forEach((url: string) => {
-                return (
-                  !this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
-                );
-              });
-            } else {
-              return (
-                !this.endpoints.test(value) &&
-                !this.ipv4Rgx.test(value) &&
-                !this.ipv6Rgx.test(value)
-              );
-            }
-            return false;
-          }
-        }),
-        Validators.required
-      ]),
+      zonegroup_endpoints: new UntypedFormControl(null, {
+        validators: [CdValidators.url, Validators.required]
+      }),
       placementTargets: this.formBuilder.array([])
     });
   }
@@ -170,7 +147,9 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
       this.multisiteZonegroupForm.get('selectedRealm').setValue(this.info.data.parent);
       this.multisiteZonegroupForm.get('default_zonegroup').setValue(this.info.data.is_default);
       this.multisiteZonegroupForm.get('master_zonegroup').setValue(this.info.data.is_master);
-      this.multisiteZonegroupForm.get('zonegroup_endpoints').setValue(this.info.data.endpoints);
+      this.multisiteZonegroupForm
+        .get('zonegroup_endpoints')
+        .setValue(this.info.data.endpoints.toString());
 
       if (this.info.data.is_default) {
         this.multisiteZonegroupForm.get('default_zonegroup').disable();
index 011d7011fa45a44508a144f61d0cb2dcaca41607..0b065f464dddf5d5f18b31e0c06107b72ad5ee14 100644 (file)
@@ -1,5 +1,5 @@
 import { fakeAsync, tick } from '@angular/core/testing';
-import { FormControl, Validators } from '@angular/forms';
+import { FormControl, UntypedFormControl, Validators } from '@angular/forms';
 
 import _ from 'lodash';
 import { of as observableOf } from 'rxjs';
@@ -902,5 +902,52 @@ describe('CdValidators', () => {
         testValidator('testName', true);
       }));
     });
+
+    describe('url', () => {
+      it('should return null for a valid URL with port', () => {
+        const control = new UntypedFormControl('https://example.com:8080');
+        expect(CdValidators.url(control)).toBeNull();
+      });
+
+      it('should return null for multiple valid URLs with ports', () => {
+        const control = new UntypedFormControl('https://example.com:8080,http://localhost:3000');
+        expect(CdValidators.url(control)).toBeNull();
+      });
+
+      it('should return null for a URL without a port', () => {
+        const control = new UntypedFormControl('https://example.com');
+        expect(CdValidators.url(control)).toBeNull();
+      });
+
+      it('should return an error object for multiple invalid URLs', () => {
+        const control = new UntypedFormControl('https://example.com,http://192.1666.33.00:099999');
+        expect(CdValidators.url(control)).toEqual({ invalidURL: true });
+      });
+
+      it('should return an error object for a non-URL string', () => {
+        const control = new UntypedFormControl('randomstring');
+        expect(CdValidators.url(control)).toEqual({ invalidURL: true });
+      });
+
+      it('should return null for a valid IP address with port', () => {
+        const control = new UntypedFormControl('https://192.168.1.1:9090');
+        expect(CdValidators.url(control)).toBeNull();
+      });
+
+      it('should return null for an IP address without a port', () => {
+        const control = new UntypedFormControl('https://192.168.1.1');
+        expect(CdValidators.url(control)).toBeNull();
+      });
+
+      it('should return null for an empty value', () => {
+        const control = new UntypedFormControl(null);
+        expect(CdValidators.url(control)).toBeNull();
+      });
+
+      it('should return null for an empty string', () => {
+        const control = new UntypedFormControl('');
+        expect(CdValidators.url(control)).toBeNull();
+      });
+    });
   });
 });
index 15f166f4a2506576397eeabb1792fc8960e0ce58..5c236730d467c5c88a6489f648bdcbe7e85bd268 100644 (file)
@@ -13,6 +13,7 @@ import { map, switchMapTo, take } from 'rxjs/operators';
 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
 import { FormatterService } from '~/app/shared/services/formatter.service';
+import validator from 'validator';
 
 export function isEmptyInputValue(value: any): boolean {
   return value == null || value.length === 0;
@@ -683,4 +684,29 @@ export class CdValidators {
       return { invalidAddress: !(addressTest && portTest) };
     };
   }
+
+  /**
+   * Validator function to validate endpoints, allowing FQDN, IPv4, and IPv6 addresses with ports.
+   * Accepts multiple endpoints separated by commas.
+   */
+  static url(control: AbstractControl): ValidationErrors | null {
+    const value = control.value;
+
+    if (_.isEmpty(value)) {
+      return null;
+    }
+
+    const urls = value.includes(',') ? value.split(',') : [value];
+
+    const invalidUrls = urls.filter(
+      (url: string) =>
+        !validator.isURL(url, {
+          require_protocol: true,
+          allow_underscores: true,
+          require_tld: false
+        }) && !validator.isIP(url)
+    );
+
+    return invalidUrls.length > 0 ? { invalidURL: true } : null;
+  }
 }