Skip to content

Commit 4dd7f48

Browse files
authored
Merge pull request ceph#60843 from rhcs-dashboard/fix-69055-main
mgr/dashboard: When configuring the RGW Multisite endpoints from the UI allow FQDN(Not only IP) Reviewed-by: Nizamudeen A <[email protected]>
2 parents f835ec0 + 9f3619a commit 4dd7f48

File tree

10 files changed

+115
-118
lines changed

10 files changed

+115
-118
lines changed

src/pybind/mgr/dashboard/frontend/package-lock.json

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/pybind/mgr/dashboard/frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"@types/lodash": "4.14.161",
106106
"@types/node": "18.17.12",
107107
"@types/swagger-ui": "3.52.0",
108+
"@types/validator": "13.12.2",
108109
"@types/xml2js": "0.4.14",
109110
"@typescript-eslint/eslint-plugin": "8.14.0",
110111
"@typescript-eslint/parser": "8.14.0",
@@ -137,7 +138,8 @@
137138
"table": "6.8.0",
138139
"transifex-i18ntool": "1.1.0",
139140
"ts-node": "9.0.0",
140-
"typescript": "5.4.5"
141+
"typescript": "5.4.5",
142+
"validator": "13.12.0"
141143
},
142144
"cypress-cucumber-preprocessor": {
143145
"stepDefinitions": "cypress/e2e/common"

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@
6666
*ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'required')"
6767
i18n>This field is required.</span>
6868
<span class="invalid-feedback"
69-
*ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
70-
i18n>Please enter a valid IP address.</span>
69+
*ngIf="multisiteMigrateForm.showError('zonegroup_endpoints', formDir, 'invalidURL')"
70+
i18n>Please enter a valid URL.</span>
7171
</div>
7272
</div>
7373
<div class="form-group row">
@@ -105,8 +105,8 @@
105105
*ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'required')"
106106
i18n>This field is required.</span>
107107
<span class="invalid-feedback"
108-
*ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'endpoint')"
109-
i18n>Please enter a valid IP address.</span>
108+
*ngIf="multisiteMigrateForm.showError('zone_endpoints', formDir, 'invalidURL')"
109+
i18n>Please enter a valid URL.</span>
110110
</div>
111111
</div>
112112
<div class="form-group row">

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
2121
styleUrls: ['./rgw-multisite-migrate.component.scss']
2222
})
2323
export class RgwMultisiteMigrateComponent implements OnInit {
24-
readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
25-
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;
26-
readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
27-
2824
@Output()
2925
submitAction = new EventEmitter();
3026

@@ -84,57 +80,12 @@ export class RgwMultisiteMigrateComponent implements OnInit {
8480
})
8581
]
8682
}),
87-
zone_endpoints: new UntypedFormControl([], {
88-
validators: [
89-
CdValidators.custom('endpoint', (value: string) => {
90-
if (_.isEmpty(value)) {
91-
return false;
92-
} else {
93-
if (value.includes(',')) {
94-
value.split(',').forEach((url: string) => {
95-
return (
96-
!this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
97-
);
98-
});
99-
} else {
100-
return (
101-
!this.endpoints.test(value) &&
102-
!this.ipv4Rgx.test(value) &&
103-
!this.ipv6Rgx.test(value)
104-
);
105-
}
106-
return false;
107-
}
108-
}),
109-
Validators.required
110-
]
83+
zone_endpoints: new UntypedFormControl(null, {
84+
validators: [CdValidators.url, Validators.required]
85+
}),
86+
zonegroup_endpoints: new UntypedFormControl(null, {
87+
validators: [CdValidators.url, Validators.required]
11188
}),
112-
zonegroup_endpoints: new UntypedFormControl(
113-
[],
114-
[
115-
CdValidators.custom('endpoint', (value: string) => {
116-
if (_.isEmpty(value)) {
117-
return false;
118-
} else {
119-
if (value.includes(',')) {
120-
value.split(',').forEach((url: string) => {
121-
return (
122-
!this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
123-
);
124-
});
125-
} else {
126-
return (
127-
!this.endpoints.test(value) &&
128-
!this.ipv4Rgx.test(value) &&
129-
!this.ipv6Rgx.test(value)
130-
);
131-
}
132-
return false;
133-
}
134-
}),
135-
Validators.required
136-
]
137-
),
13889
username: new UntypedFormControl(null, {
13990
validators: [Validators.required]
14091
})

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@
105105
*ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'required')"
106106
i18n>This field is required.</span>
107107
<span class="invalid-feedback"
108-
*ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'endpoint')"
109-
i18n>Please enter a valid IP address.</span>
108+
*ngIf="multisiteZoneForm.showError('zone_endpoints', formDir, 'invalidURL')"
109+
i18n>Please enter a valid URL.</span>
110110
</div>
111111
</div>
112112
<div class="form-group row">

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ import { ModalService } from '~/app/shared/services/modal.service';
2020
styleUrls: ['./rgw-multisite-zone-form.component.scss']
2121
})
2222
export class RgwMultisiteZoneFormComponent implements OnInit {
23-
readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
24-
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;
25-
readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
2623
action: string;
2724
info: any;
2825
multisiteZoneForm: CdFormGroup;
@@ -88,29 +85,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit {
8885
master_zone: new UntypedFormControl(false),
8986
selectedZonegroup: new UntypedFormControl(null),
9087
zone_endpoints: new UntypedFormControl(null, {
91-
validators: [
92-
CdValidators.custom('endpoint', (value: string) => {
93-
if (_.isEmpty(value)) {
94-
return false;
95-
} else {
96-
if (value.includes(',')) {
97-
value.split(',').forEach((url: string) => {
98-
return (
99-
!this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
100-
);
101-
});
102-
} else {
103-
return (
104-
!this.endpoints.test(value) &&
105-
!this.ipv4Rgx.test(value) &&
106-
!this.ipv6Rgx.test(value)
107-
);
108-
}
109-
return false;
110-
}
111-
}),
112-
Validators.required
113-
]
88+
validators: [CdValidators.url, Validators.required]
11489
}),
11590
access_key: new UntypedFormControl('', {}),
11691
secret_key: new UntypedFormControl('', {}),

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@
9999
*ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'required')"
100100
i18n>This field is required.</span>
101101
<span class="invalid-feedback"
102-
*ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'endpoint')"
103-
i18n>Please enter a valid IP address.</span>
102+
*ngIf="multisiteZonegroupForm.showError('zonegroup_endpoints', formDir, 'invalidURL')"
103+
i18n>Please enter a valid URL.</span>
104104
</div>
105105
</div>
106106
<div class="form-group row"

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ import { SelectOption } from '~/app/shared/components/select/select-option.model
2424
styleUrls: ['./rgw-multisite-zonegroup-form.component.scss']
2525
})
2626
export class RgwMultisiteZonegroupFormComponent implements OnInit {
27-
readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
28-
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;
29-
readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
3027
action: string;
3128
icons = Icons;
3229
multisiteZonegroupForm: CdFormGroup;
@@ -85,29 +82,9 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
8582
}),
8683
master_zonegroup: new UntypedFormControl(false),
8784
selectedRealm: new UntypedFormControl(null),
88-
zonegroup_endpoints: new UntypedFormControl(null, [
89-
CdValidators.custom('endpoint', (value: string) => {
90-
if (_.isEmpty(value)) {
91-
return false;
92-
} else {
93-
if (value.includes(',')) {
94-
value.split(',').forEach((url: string) => {
95-
return (
96-
!this.endpoints.test(url) && !this.ipv4Rgx.test(url) && !this.ipv6Rgx.test(url)
97-
);
98-
});
99-
} else {
100-
return (
101-
!this.endpoints.test(value) &&
102-
!this.ipv4Rgx.test(value) &&
103-
!this.ipv6Rgx.test(value)
104-
);
105-
}
106-
return false;
107-
}
108-
}),
109-
Validators.required
110-
]),
85+
zonegroup_endpoints: new UntypedFormControl(null, {
86+
validators: [CdValidators.url, Validators.required]
87+
}),
11188
placementTargets: this.formBuilder.array([])
11289
});
11390
}
@@ -170,7 +147,9 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit {
170147
this.multisiteZonegroupForm.get('selectedRealm').setValue(this.info.data.parent);
171148
this.multisiteZonegroupForm.get('default_zonegroup').setValue(this.info.data.is_default);
172149
this.multisiteZonegroupForm.get('master_zonegroup').setValue(this.info.data.is_master);
173-
this.multisiteZonegroupForm.get('zonegroup_endpoints').setValue(this.info.data.endpoints);
150+
this.multisiteZonegroupForm
151+
.get('zonegroup_endpoints')
152+
.setValue(this.info.data.endpoints.toString());
174153

175154
if (this.info.data.is_default) {
176155
this.multisiteZonegroupForm.get('default_zonegroup').disable();

src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.spec.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { fakeAsync, tick } from '@angular/core/testing';
2-
import { FormControl, Validators } from '@angular/forms';
2+
import { FormControl, UntypedFormControl, Validators } from '@angular/forms';
33

44
import _ from 'lodash';
55
import { of as observableOf } from 'rxjs';
@@ -902,5 +902,52 @@ describe('CdValidators', () => {
902902
testValidator('testName', true);
903903
}));
904904
});
905+
906+
describe('url', () => {
907+
it('should return null for a valid URL with port', () => {
908+
const control = new UntypedFormControl('https://example.com:8080');
909+
expect(CdValidators.url(control)).toBeNull();
910+
});
911+
912+
it('should return null for multiple valid URLs with ports', () => {
913+
const control = new UntypedFormControl('https://example.com:8080,http://localhost:3000');
914+
expect(CdValidators.url(control)).toBeNull();
915+
});
916+
917+
it('should return null for a URL without a port', () => {
918+
const control = new UntypedFormControl('https://example.com');
919+
expect(CdValidators.url(control)).toBeNull();
920+
});
921+
922+
it('should return an error object for multiple invalid URLs', () => {
923+
const control = new UntypedFormControl('https://example.com,http://192.1666.33.00:099999');
924+
expect(CdValidators.url(control)).toEqual({ invalidURL: true });
925+
});
926+
927+
it('should return an error object for a non-URL string', () => {
928+
const control = new UntypedFormControl('randomstring');
929+
expect(CdValidators.url(control)).toEqual({ invalidURL: true });
930+
});
931+
932+
it('should return null for a valid IP address with port', () => {
933+
const control = new UntypedFormControl('https://192.168.1.1:9090');
934+
expect(CdValidators.url(control)).toBeNull();
935+
});
936+
937+
it('should return null for an IP address without a port', () => {
938+
const control = new UntypedFormControl('https://192.168.1.1');
939+
expect(CdValidators.url(control)).toBeNull();
940+
});
941+
942+
it('should return null for an empty value', () => {
943+
const control = new UntypedFormControl(null);
944+
expect(CdValidators.url(control)).toBeNull();
945+
});
946+
947+
it('should return null for an empty string', () => {
948+
const control = new UntypedFormControl('');
949+
expect(CdValidators.url(control)).toBeNull();
950+
});
951+
});
905952
});
906953
});

src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { map, switchMapTo, take } from 'rxjs/operators';
1313
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
1414
import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
1515
import { FormatterService } from '~/app/shared/services/formatter.service';
16+
import validator from 'validator';
1617

1718
export function isEmptyInputValue(value: any): boolean {
1819
return value == null || value.length === 0;
@@ -683,4 +684,29 @@ export class CdValidators {
683684
return { invalidAddress: !(addressTest && portTest) };
684685
};
685686
}
687+
688+
/**
689+
* Validator function to validate endpoints, allowing FQDN, IPv4, and IPv6 addresses with ports.
690+
* Accepts multiple endpoints separated by commas.
691+
*/
692+
static url(control: AbstractControl): ValidationErrors | null {
693+
const value = control.value;
694+
695+
if (_.isEmpty(value)) {
696+
return null;
697+
}
698+
699+
const urls = value.includes(',') ? value.split(',') : [value];
700+
701+
const invalidUrls = urls.filter(
702+
(url: string) =>
703+
!validator.isURL(url, {
704+
require_protocol: true,
705+
allow_underscores: true,
706+
require_tld: false
707+
}) && !validator.isIP(url)
708+
);
709+
710+
return invalidUrls.length > 0 ? { invalidURL: true } : null;
711+
}
686712
}

0 commit comments

Comments
 (0)