mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
mgr/dashboard : Support wildcard sans and zonegroup hostnames
fixes : https://tracker.ceph.com/issues/76795 Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
This commit is contained in:
parent
4fdb541ea6
commit
eee6a15dcd
@ -403,6 +403,37 @@
|
||||
}
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
@if (!rgwModuleEnabled) {
|
||||
<cd-alert-panel type="warning"
|
||||
spacingClass="mb-3"
|
||||
actionName="Enable"
|
||||
(action)="enableRgwModule()"
|
||||
i18n-actionName
|
||||
i18n>
|
||||
The RGW mgr module must be enabled to configure S3 hostnames.
|
||||
Enabling the module will cause temporary manager downtime while it loads.
|
||||
</cd-alert-panel>
|
||||
}
|
||||
<div class="form-item">
|
||||
<cds-checkbox i18n-label
|
||||
formControlName="virtual_host_enabled">
|
||||
Enable virtual-host style bucket access
|
||||
<cd-help-text i18n>
|
||||
Allows bucket access via hostnames such as mybucket.s3.example.com.
|
||||
</cd-help-text>
|
||||
</cds-checkbox>
|
||||
</div>
|
||||
@if (serviceForm.controls.virtual_host_enabled.value) {
|
||||
<div class="form-item">
|
||||
<cd-text-label-list formControlName="zonegroup_hostnames"
|
||||
label="S3 hostname"
|
||||
i18n-label
|
||||
helperText="Domain names for S3 virtual-host bucket access (e.g., s3.example.com). Enables bucket URLs like bucket.s3.example.com instead of s3.example.com/bucket."
|
||||
i18n-helperText>
|
||||
</cd-text-label-list>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- iSCSI -->
|
||||
@ -1218,13 +1249,13 @@
|
||||
<cds-radio-group formControlName="certificateType"
|
||||
orientation="horizontal"
|
||||
helperText="Select how certificates will be signed for this service. Choose internal to use the cluster’s CA, or external to upload certificates signed by your organization.">
|
||||
<cds-radio value="internal"
|
||||
(change)="onCertificateTypeChange('internal')"
|
||||
<cds-radio [value]="CertificateType.internal"
|
||||
(change)="onCertificateTypeChange(CertificateType.internal)"
|
||||
i18n>
|
||||
Internal
|
||||
</cds-radio>
|
||||
<cds-radio value="external"
|
||||
(change)="onCertificateTypeChange('external')"
|
||||
<cds-radio [value]="CertificateType.external"
|
||||
(change)="onCertificateTypeChange(CertificateType.external)"
|
||||
i18n>
|
||||
External
|
||||
</cds-radio>
|
||||
@ -1239,7 +1270,7 @@
|
||||
</cd-alert-panel>
|
||||
}
|
||||
|
||||
@if (serviceForm.controls.certificateType.value === 'internal') {
|
||||
@if (serviceForm.controls.certificateType.value === CertificateType.internal) {
|
||||
<cd-alert-panel type="info"
|
||||
spacingClass="mb-3"
|
||||
i18n>
|
||||
@ -1253,6 +1284,18 @@
|
||||
i18n-helperText>
|
||||
</cd-text-label-list>
|
||||
</div>
|
||||
@if (serviceForm.controls.service_type.value === 'rgw'
|
||||
&& serviceForm.controls.virtual_host_enabled.value) {
|
||||
<div class="form-item">
|
||||
<cds-checkbox i18n-label
|
||||
formControlName="wildcard_enabled">
|
||||
Include wildcard certificate for bucket subdomains
|
||||
<cd-help-text i18n>
|
||||
Add wildcard certificates (*.domain) to allow SSL for all bucket subdomains. Required for virtual-host style with SSL.
|
||||
</cd-help-text>
|
||||
</cds-checkbox>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@ -1288,7 +1331,7 @@
|
||||
</ng-template>
|
||||
</div>
|
||||
<!-- ssl_ca_cert - Only show for NFS when SSL is enabled AND certificate type is external -->
|
||||
@if (serviceForm.controls.ssl.value && serviceForm.controls.certificateType.value === 'external' && serviceForm.controls.service_type.value === 'nfs') {
|
||||
@if (serviceForm.controls.ssl.value && serviceForm.controls.certificateType.value === CertificateType.external && serviceForm.controls.service_type.value === 'nfs') {
|
||||
<div class="form-item">
|
||||
<ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'ssl_ca_cert', title: 'CA Certificate Input', helperText: 'Uploaded files will populate the CA certificate details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a CA certificate file, or paste the CA certificate PEM content directly.', invalidTemplate: invalidSslCaCertError, isRequired: false }"></ng-container>
|
||||
<ng-template #invalidSslCaCertError>
|
||||
|
||||
@ -263,6 +263,64 @@ describe('ServiceFormComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit rgw with virtual-host style bucket access and SSL', () => {
|
||||
formHelper.setValue('virtual_host_enabled', true);
|
||||
formHelper.setValue('ssl', true);
|
||||
formHelper.setValue('zonegroup_hostnames', ['s3.cephlab.com']);
|
||||
formHelper.setValue('wildcard_enabled', true);
|
||||
component.onSubmit();
|
||||
expect(cephServiceService.create).toHaveBeenCalledWith({
|
||||
service_type: 'rgw',
|
||||
service_id: 'svc',
|
||||
rgw_realm: null,
|
||||
rgw_zone: null,
|
||||
rgw_zonegroup: null,
|
||||
placement: {},
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificate_source: 'cephadm-signed',
|
||||
zonegroup_hostnames: ['s3.cephlab.com'],
|
||||
wildcard_enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit rgw with SSL and without virtual-host style bucket access', () => {
|
||||
formHelper.setValue('ssl', true);
|
||||
component.onSubmit();
|
||||
expect(cephServiceService.create).toHaveBeenCalledWith({
|
||||
service_type: 'rgw',
|
||||
service_id: 'svc',
|
||||
rgw_realm: null,
|
||||
rgw_zone: null,
|
||||
rgw_zonegroup: null,
|
||||
placement: {},
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificate_source: 'cephadm-signed'
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit rgw with virtual-host style bucket access and SSL without wildcard certificate', () => {
|
||||
formHelper.setValue('virtual_host_enabled', true);
|
||||
formHelper.setValue('ssl', true);
|
||||
formHelper.setValue('zonegroup_hostnames', ['s3.cephlab.com']);
|
||||
formHelper.setValue('wildcard_enabled', false);
|
||||
component.onSubmit();
|
||||
expect(cephServiceService.create).toHaveBeenCalledWith({
|
||||
service_type: 'rgw',
|
||||
service_id: 'svc',
|
||||
rgw_realm: null,
|
||||
rgw_zone: null,
|
||||
rgw_zonegroup: null,
|
||||
placement: {},
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificate_source: 'cephadm-signed',
|
||||
zonegroup_hostnames: ['s3.cephlab.com'],
|
||||
wildcard_enabled: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should submit valid rgw port (1)', () => {
|
||||
formHelper.setValue('rgw_frontend_port', 1);
|
||||
component.onSubmit();
|
||||
|
||||
@ -18,6 +18,7 @@ import { HostService } from '~/app/shared/api/host.service';
|
||||
import { PoolService } from '~/app/shared/api/pool.service';
|
||||
import { RbdService } from '~/app/shared/api/rbd.service';
|
||||
import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
|
||||
import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
|
||||
import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
|
||||
import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
|
||||
import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
|
||||
@ -37,6 +38,7 @@ import { Host } from '~/app/shared/models/host.interface';
|
||||
import {
|
||||
CephServiceCertificate,
|
||||
CephServiceSpec,
|
||||
CertificateType,
|
||||
QatOptions,
|
||||
QatSepcs,
|
||||
CERTIFICATE_STATUS_ICON_MAP
|
||||
@ -54,6 +56,7 @@ import { TimerService } from '~/app/shared/services/timer.service';
|
||||
export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
public sub = new Subscription();
|
||||
|
||||
readonly CertificateType = CertificateType;
|
||||
readonly MDS_SVC_ID_PATTERN = /^[a-zA-Z_.-][a-zA-Z0-9_.-]*$/;
|
||||
readonly SNMP_DESTINATION_PATTERN = /^[^\:]+:[0-9]/;
|
||||
readonly SNMP_ENGINE_ID_PATTERN = /^[0-9A-Fa-f]{10,64}/g;
|
||||
@ -115,6 +118,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
}));
|
||||
showMgmtGatewayMessage: boolean = false;
|
||||
showCertSourceChangeWarning: boolean = false;
|
||||
rgwModuleEnabled = false;
|
||||
qatCompressionOptions = [
|
||||
{ value: QatOptions.hw, label: 'Hardware' },
|
||||
{ value: QatOptions.sw, label: 'Software' },
|
||||
@ -141,6 +145,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
public rgwZonegroupService: RgwZonegroupService,
|
||||
public rgwZoneService: RgwZoneService,
|
||||
public rgwMultisiteService: RgwMultisiteService,
|
||||
private mgrModuleService: MgrModuleService,
|
||||
private route: ActivatedRoute,
|
||||
public modalService: ModalCdsService,
|
||||
private location: Location
|
||||
@ -422,7 +427,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'rgw',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.pemCert()]
|
||||
),
|
||||
@ -431,7 +436,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'iscsi',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.sslCert()]
|
||||
),
|
||||
@ -440,7 +445,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'ingress',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.pemCert()]
|
||||
),
|
||||
@ -449,7 +454,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'oauth2-proxy',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.sslCert()]
|
||||
),
|
||||
@ -458,7 +463,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'mgmt-gateway',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.sslCert()]
|
||||
),
|
||||
@ -467,7 +472,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'nfs',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.pemCert()]
|
||||
)
|
||||
@ -481,7 +486,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'iscsi',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.sslPrivKey()]
|
||||
),
|
||||
@ -490,7 +495,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'oauth2-proxy',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.sslPrivKey()]
|
||||
),
|
||||
@ -499,7 +504,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'mgmt-gateway',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.sslPrivKey()]
|
||||
),
|
||||
@ -508,14 +513,17 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'nfs',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.sslPrivKey()]
|
||||
)
|
||||
]
|
||||
],
|
||||
certificateType: ['internal'],
|
||||
certificateType: [CertificateType.internal],
|
||||
custom_sans: [null],
|
||||
virtual_host_enabled: [false],
|
||||
zonegroup_hostnames: [null],
|
||||
wildcard_enabled: [true],
|
||||
ssl_ca_cert: [
|
||||
'',
|
||||
[
|
||||
@ -524,7 +532,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
service_type: 'nfs',
|
||||
unmanaged: false,
|
||||
ssl: true,
|
||||
certificateType: 'external'
|
||||
certificateType: CertificateType.external
|
||||
},
|
||||
[Validators.required, CdValidators.pemCert()]
|
||||
)
|
||||
@ -686,6 +694,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
this.open = true;
|
||||
this.action = this.actionLabels.CREATE;
|
||||
this.resolveRoute();
|
||||
this.getRgwModuleStatus();
|
||||
this.mgrModuleService.updateCompleted$.subscribe(() => this.getRgwModuleStatus());
|
||||
|
||||
this.cephServiceService
|
||||
.list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }))
|
||||
@ -792,10 +802,21 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
response[0].spec?.qat
|
||||
);
|
||||
this.serviceForm.get('ssl').setValue(response[0].spec?.ssl);
|
||||
if (response[0].spec?.zonegroup_hostnames?.length) {
|
||||
this.serviceForm
|
||||
.get('zonegroup_hostnames')
|
||||
.setValue(response[0].spec.zonegroup_hostnames);
|
||||
if (this.rgwModuleEnabled) {
|
||||
this.serviceForm.get('virtual_host_enabled').setValue(true);
|
||||
}
|
||||
}
|
||||
this.serviceForm
|
||||
.get('wildcard_enabled')
|
||||
.setValue(response[0].spec?.wildcard_enabled ?? true);
|
||||
if (response[0].spec?.ssl) {
|
||||
// Special case for rgw: if certificate_source is not cephadm-signed, set certificateType to external
|
||||
if (response[0].spec?.certificate_source != 'cephadm-signed') {
|
||||
this.serviceForm.get('certificateType').setValue('external');
|
||||
this.serviceForm.get('certificateType').setValue(CertificateType.external);
|
||||
}
|
||||
let certValue = response[0].spec?.rgw_frontend_ssl_certificate || '';
|
||||
if (response[0].spec?.ssl_cert) {
|
||||
@ -805,6 +826,9 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
}
|
||||
this.serviceForm.get('ssl_cert').setValue(certValue);
|
||||
if (response[0].spec?.custom_sans) {
|
||||
this.serviceForm.get('custom_sans').setValue(response[0].spec.custom_sans);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'ingress':
|
||||
@ -830,7 +854,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
this.port = response[0].spec?.port;
|
||||
this.serviceForm.get('ssl').setValue(true);
|
||||
if (response[0].spec?.certificate_source !== 'cephadm-signed') {
|
||||
this.serviceForm.get('certificateType').setValue('external');
|
||||
this.serviceForm.get('certificateType').setValue(CertificateType.external);
|
||||
}
|
||||
if (response[0].spec?.ssl_protocols) {
|
||||
let selectedValues: Array<ListItem> = [];
|
||||
@ -1232,15 +1256,37 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
onCertificateTypeChange(type: string) {
|
||||
onCertificateTypeChange(type: CertificateType) {
|
||||
this.serviceForm.get('certificateType').setValue(type);
|
||||
if (this.editing && this.currentCertificate?.has_certificate) {
|
||||
const originalSource =
|
||||
this.currentSpecCertificateSource === 'cephadm-signed' ? 'internal' : 'external';
|
||||
this.currentSpecCertificateSource === 'cephadm-signed'
|
||||
? CertificateType.internal
|
||||
: CertificateType.external;
|
||||
this.showCertSourceChangeWarning = type !== originalSource;
|
||||
}
|
||||
}
|
||||
|
||||
private getRgwModuleStatus(): void {
|
||||
this.rgwMultisiteService.getRgwModuleStatus().subscribe((enabled: boolean) => {
|
||||
this.rgwModuleEnabled = enabled;
|
||||
const virtualHostControl = this.serviceForm.get('virtual_host_enabled');
|
||||
if (enabled) {
|
||||
virtualHostControl.enable({ emitEvent: false });
|
||||
if (this.serviceForm.get('zonegroup_hostnames').value?.length) {
|
||||
virtualHostControl.setValue(true, { emitEvent: false });
|
||||
}
|
||||
} else {
|
||||
virtualHostControl.setValue(false, { emitEvent: false });
|
||||
virtualHostControl.disable({ emitEvent: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enableRgwModule(): void {
|
||||
this.mgrModuleService.updateModuleState('rgw', false, null, '', $localize`Enabled RGW Module`);
|
||||
}
|
||||
|
||||
prePopulateId() {
|
||||
const control: AbstractControl = this.serviceForm.get('service_id');
|
||||
const backendService = this.serviceForm.getValue('backend_service');
|
||||
@ -1386,11 +1432,21 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
serviceSpec['rgw_frontend_port'] = values['rgw_frontend_port'];
|
||||
}
|
||||
serviceSpec['ssl'] = values['ssl'];
|
||||
if (values['virtual_host_enabled'] && values['zonegroup_hostnames']?.length > 0) {
|
||||
serviceSpec['zonegroup_hostnames'] = values['zonegroup_hostnames'];
|
||||
}
|
||||
if (values['ssl']) {
|
||||
this.applySslCertificateConfig(serviceSpec, values, {
|
||||
sslCertField: 'rgw_frontend_ssl_certificate',
|
||||
includeSslKey: false
|
||||
});
|
||||
if (
|
||||
values['certificateType'] === CertificateType.internal &&
|
||||
values['virtual_host_enabled'] &&
|
||||
values['zonegroup_hostnames']?.length > 0
|
||||
) {
|
||||
serviceSpec['wildcard_enabled'] = values['wildcard_enabled'];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'iscsi':
|
||||
@ -1538,7 +1594,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
|
||||
get showExternalSslCert(): boolean {
|
||||
const serviceType = this.serviceForm.controls.service_type?.value;
|
||||
const isExternalCert = this.serviceForm.controls.certificateType?.value === 'external';
|
||||
const isExternalCert =
|
||||
this.serviceForm.controls.certificateType?.value === CertificateType.external;
|
||||
const isSslEnabled = this.serviceForm.controls.ssl?.value;
|
||||
|
||||
if (serviceType === 'mgmt-gateway') {
|
||||
@ -1551,7 +1608,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
|
||||
get showExternalSslKey(): boolean {
|
||||
const serviceType = this.serviceForm.controls.service_type?.value;
|
||||
const isExternalCert = this.serviceForm.controls.certificateType?.value === 'external';
|
||||
const isExternalCert =
|
||||
this.serviceForm.controls.certificateType?.value === CertificateType.external;
|
||||
const isSslEnabled = this.serviceForm.controls.ssl?.value;
|
||||
|
||||
const sslKeyServices = ['iscsi', 'grafana', 'oauth2-proxy', 'nvmeof', 'nfs', 'mgmt-gateway'];
|
||||
@ -1585,13 +1643,16 @@ export class ServiceFormComponent extends CdForm implements OnInit {
|
||||
} = options;
|
||||
|
||||
serviceSpec['certificate_source'] =
|
||||
values['certificateType'] === 'internal' ? 'cephadm-signed' : 'inline';
|
||||
values['certificateType'] === CertificateType.internal ? 'cephadm-signed' : 'inline';
|
||||
|
||||
if (values['certificateType'] === 'internal' && values['custom_sans']?.length > 0) {
|
||||
if (
|
||||
values['certificateType'] === CertificateType.internal &&
|
||||
values['custom_sans']?.length > 0
|
||||
) {
|
||||
serviceSpec['custom_sans'] = values['custom_sans'];
|
||||
}
|
||||
|
||||
if (values['certificateType'] === 'external') {
|
||||
if (values['certificateType'] === CertificateType.external) {
|
||||
serviceSpec[sslCertField] = values['ssl_cert']?.trim();
|
||||
if (includeSslKey) {
|
||||
serviceSpec[sslKeyField] = values['ssl_key']?.trim();
|
||||
|
||||
@ -91,6 +91,9 @@ export interface CephServiceAdditionalSpec {
|
||||
ssl_protocols: string[];
|
||||
ssl_ciphers: string[];
|
||||
certificate_source: string;
|
||||
custom_sans?: string[];
|
||||
zonegroup_hostnames?: string[];
|
||||
wildcard_enabled?: boolean;
|
||||
port: number;
|
||||
initial_admin_password: string;
|
||||
rgw_realm: string;
|
||||
@ -123,6 +126,11 @@ export interface QatSepcs {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export enum CertificateType {
|
||||
internal = 'internal',
|
||||
external = 'external'
|
||||
}
|
||||
|
||||
export enum QatOptions {
|
||||
hw = 'hw',
|
||||
sw = 'sw',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user