From: Abhishek Desai Date: Mon, 9 Feb 2026 19:43:44 +0000 (+0530) Subject: mgr/dashboard : Update create/edit service modal to support certmgr X-Git-Tag: v21.0.0~189^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=f16ef7a95ee9bc508ab68ffd08ccd5abfc724d00;p=ceph.git mgr/dashboard : Update create/edit service modal to support certmgr fixes: https://tracker.ceph.com/issues/74636 Signed-off-by: Abhishek Desai --- diff --git a/src/pybind/mgr/dashboard/controllers/certificate.py b/src/pybind/mgr/dashboard/controllers/certificate.py index ebcdb9e68d35..579f227d97de 100644 --- a/src/pybind/mgr/dashboard/controllers/certificate.py +++ b/src/pybind/mgr/dashboard/controllers/certificate.py @@ -130,7 +130,7 @@ class Certificate(RESTController): cert_scope = CertificateScope(cert_config.get('scope', CertificateScope.SERVICE.value)) cert_ls_data = CertificateService.fetch_certificates_for_service( - orch, service_type, user_cert_name, cephadm_cert_name + orch, service_type, user_cert_name, cephadm_cert_name, service_name_full ) daemon_hostnames, _ = CertificateService.get_daemon_hostnames(orch, service_name_full) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index 2285de4d3d1b..fadb8312a7c9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -22,7 +22,8 @@ import { LayoutModule, NumberModule, FileUploaderModule, - TabsModule + TabsModule, + RadioModule } from 'carbon-components-angular'; import Analytics from '@carbon/icons/es/analytics/16'; import CloseFilled from '@carbon/icons/es/close--filled/16'; @@ -131,7 +132,8 @@ import { TextLabelListComponent } from '~/app/shared/components/text-label-list/ SelectModule, LayoutModule, NumberModule, - FileUploaderModule + FileUploaderModule, + RadioModule ], declarations: [ MonitorComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.html index b04f2ed1ad33..d57e0322277a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.html @@ -41,7 +41,7 @@ - + @if (SERVICES_SUPPORTING_CERT_EDIT.includes(serviceType)) { + }
{ beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ServiceCertificateDetailsComponent, IconComponent], + imports: [ComponentsModule], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -59,15 +61,17 @@ describe('ServiceCertificateDetailsComponent', () => { it('should emit editService with service identifiers', () => { component.serviceName = 'svc-name'; - component.serviceType = 'svc-type'; + component.serviceType = 'rgw'; + component.certificate = baseCert; fixture.detectChanges(); const emitSpy = jest.spyOn(component.editService, 'emit'); const button = fixture.debugElement.query(By.css('cds-icon-button')); + jestExpect(button).not.toBeNull(); button.triggerEventHandler('click', {}); - jestExpect(emitSpy).toHaveBeenCalledWith({ serviceName: 'svc-name', serviceType: 'svc-type' }); + jestExpect(emitSpy).toHaveBeenCalledWith({ serviceName: 'svc-name', serviceType: 'rgw' }); }); it('should show success icon and text for valid status', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.ts index 05605a738534..1c48dde7dff1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.ts @@ -1,9 +1,9 @@ import { CephCertificateStatus, - CephServiceCertificate + CephServiceCertificate, + CERTIFICATE_STATUS_ICON_MAP } from '~/app/shared/models/service.interface'; import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { ICON_TYPE } from '~/app/shared/enum/icons.enum'; import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe'; @Component({ @@ -20,13 +20,16 @@ export class ServiceCertificateDetailsComponent { @Output() editService = new EventEmitter<{ serviceName?: string; serviceType?: string }>(); - readonly statusIconMap: Record = { - valid: 'success', - expiring: 'warning', - expiring_soon: 'warning', - expired: 'danger', - default: 'warning' - }; + readonly SERVICES_SUPPORTING_CERT_EDIT = [ + 'rgw', + 'ingress', + 'iscsi', + 'oauth2-proxy', + 'mgmt-gateway', + 'nvmeof', + 'nfs' + ]; + statusIconMap = CERTIFICATE_STATUS_ICON_MAP; constructor(private cdDatePipe: CdDatePipe) {} @@ -34,11 +37,11 @@ export class ServiceCertificateDetailsComponent { if (!cert || !cert.requires_certificate || !cert.status) { return '-'; } - const formattedDate = this.formatDate(cert.expiry_date); switch (cert.status) { case CephCertificateStatus.valid: return formattedDate ? `Valid - ${formattedDate}` : 'Valid'; + case CephCertificateStatus.expiring: case CephCertificateStatus.expiringSoon: return formattedDate ? `Expiring soon - ${formattedDate}` : 'Expiring soon'; case CephCertificateStatus.expired: diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html index 8a158c229f44..648b0282202d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html @@ -1118,9 +1118,9 @@
} - + @if (!serviceForm.controls.unmanaged.value && ['rgw', 'iscsi', 'ingress', 'oauth2-proxy', - 'mgmt-gateway'].includes(serviceForm.controls.service_type.value)) { + 'mgmt-gateway', 'nfs'].includes(serviceForm.controls.service_type.value)) { @if (!['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) {
@@ -1131,31 +1131,108 @@
} - - @if (serviceForm.controls.ssl.value || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) { -
- Certificate -
- - + + @if (serviceForm.controls.ssl.value && ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'mgmt-gateway', 'nvmeof', 'nfs'].includes(serviceForm.controls.service_type.value)) { +
+ + @if (editing && currentCertificate?.has_certificate) { +
+ +
+
+ +
{{ currentCertificate.cert_name }}
+
+
+ +
{{ currentCertificate.expiry_date | cdDate }} • {{ currentCertificate.days_to_expiration }} days left
+
+
+
+
+ +
+ + @switch (currentCertificate.status) { + @case ('valid') { Valid } + @case ('expiring') { Expiring soon } + @case ('expired') { Expired } + @case ('not_configured') { Not configured } + @case ('invalid') { Invalid } + @default { {{ currentCertificate.status }} } + } +
+
+
+ +
+ @if (currentCertificate.signed_by === 'cephadm') { + Internal (Cephadm CA) + } @else { + {{ currentCertificate.issuer || 'External' }} + } +
+
- +
+ } + + +
+ + + + Internal + + + External + + +
+ + @if (showCertSourceChangeWarning) { + + Changing the certificate source will redeploy the service daemons to apply the new certificate configuration. + + } + + @if (serviceForm.controls.certificateType.value === 'internal') { + + Certificate will be generated automatically by Cephadm CA for internal certificate type. + +
+ + +
+ } +
+ } + + + @if (showExternalSslCert) { +
+ @if (serviceForm.showError('ssl_cert', frm, 'required')) {
} - - @if ((serviceForm.controls.ssl.value && !(['rgw', 'ingress'].includes(serviceForm.controls.service_type.value))) - || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) { + @if (showExternalSslKey) {
- Private key -
- - - -
-
+ @if (serviceForm.showError('ssl_key', frm, 'required')) {
+ + @if (serviceForm.controls.ssl.value && serviceForm.controls.certificateType.value === 'external' && serviceForm.controls.service_type.value === 'nfs') { +
+ + + @if (serviceForm.showError('ssl_ca_cert', frm, 'required')) { + This field is required. + } + @if (serviceForm.showError('ssl_ca_cert', frm, 'pattern')) { + Invalid CA certificate. + } + +
+ } } @@ -1296,25 +1366,7 @@ @if (serviceForm.controls.enable_mtls.value) {
- Root CA certificate - - - - + @if (serviceForm.showError('root_ca_cert', frm, 'required')) {
- Client CA certificate - - - + @if (serviceForm.showError('client_cert', frm, 'required')) {
- Client key - - - - + @if (serviceForm.showError('client_key', frm, 'required')) {
- Gateway server certificate - - - - + @if (serviceForm.showError('server_cert', frm, 'required')) {
- Gateway server key - - - - + @if (serviceForm.showError('server_key', frm, 'required')) { https://ssl-config.mozilla.org/#server=nginx + + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts index 03628af82262..2490f993d497 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts @@ -16,12 +16,14 @@ import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed, FormHelper, Mocks } from '~/testing/unit-test-helper'; import { ServiceFormComponent } from './service-form.component'; import { PoolService } from '~/app/shared/api/pool.service'; +import { TextLabelListComponent } from '~/app/shared/components/text-label-list/text-label-list.component'; import { USER } from '~/app/shared/constants/app.constants'; import { CheckboxModule, InputModule, ModalModule, NumberModule, + RadioModule, SelectModule } from 'carbon-components-angular'; @@ -58,7 +60,9 @@ describe('ServiceFormComponent', () => { SelectModule, NumberModule, ModalModule, - CheckboxModule + CheckboxModule, + RadioModule, + TextLabelListComponent ] }); @@ -252,8 +256,8 @@ describe('ServiceFormComponent', () => { placement: {}, unmanaged: false, rgw_frontend_port: 1234, - rgw_frontend_ssl_certificate: '', - ssl: true + ssl: true, + certificate_source: 'cephadm-signed' }); }); @@ -376,8 +380,7 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA== api_user: USER, api_password: 'password', api_secure: true, - ssl_cert: '', - ssl_key: '', + certificate_source: 'cephadm-signed', trusted_ip_list: ['172.16.0.5', '192.1.1.10'] }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts index 4943ab6651cf..6ecb3ba29158 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts @@ -1,6 +1,6 @@ import { Location } from '@angular/common'; import { HttpParams } from '@angular/common/http'; -import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { AbstractControl, UntypedFormControl, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; @@ -34,7 +34,13 @@ import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { CdValidators } from '~/app/shared/forms/cd-validators'; import { FinishedTask } from '~/app/shared/models/finished-task'; import { Host } from '~/app/shared/models/host.interface'; -import { CephServiceSpec, QatOptions, QatSepcs } from '~/app/shared/models/service.interface'; +import { + CephServiceCertificate, + CephServiceSpec, + QatOptions, + QatSepcs, + CERTIFICATE_STATUS_ICON_MAP +} from '~/app/shared/models/service.interface'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { TimerService } from '~/app/shared/services/timer.service'; @@ -67,6 +73,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { @Input() serviceType: string; + @Output() serviceUpdated = new EventEmitter(); + serviceForm: CdFormGroup; action: string; resource: string; @@ -107,6 +115,7 @@ export class ServiceFormComponent extends CdForm implements OnInit { selected: false })); showMgmtGatewayMessage: boolean = false; + showCertSourceChangeWarning: boolean = false; qatCompressionOptions = [ { value: QatOptions.hw, label: 'Hardware' }, { value: QatOptions.sw, label: 'Software' }, @@ -114,6 +123,9 @@ export class ServiceFormComponent extends CdForm implements OnInit { ]; open: boolean = false; hostsAndLabels$: Observable<{ hosts: { content: string }[]; labels: { content: string }[] }>; + currentCertificate: CephServiceCertificate = null; + currentSpecCertificateSource: string = null; + statusIconMap = CERTIFICATE_STATUS_ICON_MAP; constructor( public actionLabels: ActionLabelsI18n, @@ -410,7 +422,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'rgw', unmanaged: false, - ssl: true + ssl: true, + certificateType: 'external' }, [Validators.required, CdValidators.pemCert()] ), @@ -418,7 +431,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'iscsi', unmanaged: false, - ssl: true + ssl: true, + certificateType: 'external' }, [Validators.required, CdValidators.sslCert()] ), @@ -426,7 +440,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'ingress', unmanaged: false, - ssl: true + ssl: true, + certificateType: 'external' }, [Validators.required, CdValidators.pemCert()] ), @@ -434,7 +449,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'oauth2-proxy', unmanaged: false, - ssl: true + ssl: true, + certificateType: 'external' }, [Validators.required, CdValidators.sslCert()] ), @@ -442,9 +458,19 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'mgmt-gateway', unmanaged: false, - ssl: false + ssl: true, + certificateType: 'external' + }, + [Validators.required, CdValidators.sslCert()] + ), + CdValidators.composeIf( + { + service_type: 'nfs', + unmanaged: false, + ssl: true, + certificateType: 'external' }, - [CdValidators.sslCert()] + [Validators.required, CdValidators.pemCert()] ) ] ], @@ -455,7 +481,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'iscsi', unmanaged: false, - ssl: true + ssl: true, + certificateType: 'external' }, [Validators.required, CdValidators.sslPrivKey()] ), @@ -463,7 +490,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'oauth2-proxy', unmanaged: false, - ssl: true + ssl: true, + certificateType: 'external' }, [Validators.required, CdValidators.sslPrivKey()] ), @@ -471,9 +499,35 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'mgmt-gateway', unmanaged: false, - ssl: false + ssl: true, + certificateType: 'external' + }, + [Validators.required, CdValidators.sslPrivKey()] + ), + CdValidators.composeIf( + { + service_type: 'nfs', + unmanaged: false, + ssl: true, + certificateType: 'external' }, - [CdValidators.sslPrivKey()] + [Validators.required, CdValidators.sslPrivKey()] + ) + ] + ], + certificateType: ['internal'], + custom_sans: [null], + ssl_ca_cert: [ + '', + [ + CdValidators.composeIf( + { + service_type: 'nfs', + unmanaged: false, + ssl: true, + certificateType: 'external' + }, + [Validators.required, CdValidators.pemCert()] ) ] ], @@ -676,6 +730,12 @@ export class ServiceFormComponent extends CdForm implements OnInit { formKeys.forEach((keys) => { this.serviceForm.get(keys).setValue(response[0][keys]); }); + if (response[0].certificate) { + this.currentCertificate = response[0].certificate; + } + if (response[0].spec?.certificate_source) { + this.currentSpecCertificateSource = response[0].spec.certificate_source; + } if (!response[0]['unmanaged']) { const placementKey = Object.keys(response[0]['placement'])[0]; let placementValue: string; @@ -722,9 +782,18 @@ export class ServiceFormComponent extends CdForm implements OnInit { ); this.serviceForm.get('ssl').setValue(response[0].spec?.ssl); if (response[0].spec?.ssl) { - this.serviceForm - .get('ssl_cert') - .setValue(response[0].spec?.rgw_frontend_ssl_certificate); + // 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'); + } + let certValue = response[0].spec?.rgw_frontend_ssl_certificate || ''; + if (response[0].spec?.ssl_cert) { + certValue = response[0].spec.ssl_cert; + if (response[0].spec?.ssl_key) { + certValue = response[0].spec.ssl_cert + '\n' + response[0].spec.ssl_key; + } + } + this.serviceForm.get('ssl_cert').setValue(certValue); } break; case 'ingress': @@ -748,7 +817,10 @@ export class ServiceFormComponent extends CdForm implements OnInit { let hrefSplitted = window.location.href.split(':'); this.currentURL = hrefSplitted[0] + hrefSplitted[1]; 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'); + } if (response[0].spec?.ssl_protocols) { let selectedValues: Array = []; for (const value of response[0].spec.ssl_protocols) { @@ -1093,6 +1165,8 @@ export class ServiceFormComponent extends CdForm implements OnInit { if (selectedServiceType === 'mgmt-gateway') { let hrefSplitted = window.location.href.split(':'); this.currentURL = hrefSplitted[0] + hrefSplitted[1]; + // mgmt-gateway is always SSL + this.serviceForm.get('ssl').setValue(true); // mgmt-gateway lacks HA for now this.serviceForm.get('count').disable(); } else { @@ -1141,6 +1215,15 @@ export class ServiceFormComponent extends CdForm implements OnInit { } } + onCertificateTypeChange(type: string) { + this.serviceForm.get('certificateType').setValue(type); + if (this.editing && this.currentCertificate?.has_certificate) { + const originalSource = + this.currentSpecCertificateSource === 'cephadm-signed' ? 'internal' : 'external'; + this.showCertSourceChangeWarning = type !== originalSource; + } + } + prePopulateId() { const control: AbstractControl = this.serviceForm.get('service_id'); const backendService = this.serviceForm.getValue('backend_service'); @@ -1287,7 +1370,10 @@ export class ServiceFormComponent extends CdForm implements OnInit { } serviceSpec['ssl'] = values['ssl']; if (values['ssl']) { - serviceSpec['rgw_frontend_ssl_certificate'] = values['ssl_cert']?.trim(); + this.applySslCertificateConfig(serviceSpec, values, { + sslCertField: 'rgw_frontend_ssl_certificate', + includeSslKey: false + }); } break; case 'iscsi': @@ -1303,15 +1389,13 @@ export class ServiceFormComponent extends CdForm implements OnInit { serviceSpec['api_password'] = values['api_password']; serviceSpec['api_secure'] = values['ssl']; if (values['ssl']) { - serviceSpec['ssl_cert'] = values['ssl_cert']?.trim(); - serviceSpec['ssl_key'] = values['ssl_key']?.trim(); + this.applySslCertificateConfig(serviceSpec, values); } break; case 'ingress': serviceSpec['ssl'] = values['ssl']; if (values['ssl']) { - serviceSpec['ssl_cert'] = values['ssl_cert']?.trim(); - serviceSpec['ssl_key'] = values['ssl_key']?.trim(); + this.applySslCertificateConfig(serviceSpec, values); } if (values['virtual_interface_networks']) { serviceSpec['virtual_interface_networks'] = values['virtual_interface_networks'] @@ -1357,26 +1441,34 @@ export class ServiceFormComponent extends CdForm implements OnInit { ); } if (values['ssl']) { - serviceSpec['ssl_cert'] = values['ssl_cert']?.trim(); - serviceSpec['ssl_key'] = values['ssl_key']?.trim(); + this.applySslCertificateConfig(serviceSpec, values); + } + break; + case 'nfs': + if (values['ssl']) { + serviceSpec['ssl'] = values['ssl']; + this.applySslCertificateConfig(serviceSpec, values, { includeCaCert: true }); } break; } } + const apiCall$ = this.editing + ? this.cephServiceService.update(serviceSpec) + : this.cephServiceService.create(serviceSpec); + this.taskWrapperService .wrapTaskAroundCall({ task: new FinishedTask(taskUrl, { service_name: serviceName }), - call: this.editing - ? this.cephServiceService.update(serviceSpec) - : this.cephServiceService.create(serviceSpec) + call: apiCall$ }) .subscribe({ error() { self.serviceForm.setErrors({ cdSubmitButton: true }); }, complete: () => { + this.serviceUpdated.emit(); this.closeModal(); } }); @@ -1421,6 +1513,28 @@ 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 isSslEnabled = this.serviceForm.controls.ssl?.value; + + if (serviceType === 'mgmt-gateway') { + return isExternalCert; + } + + const sslCertServices = ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'nvmeof', 'nfs']; + return isSslEnabled && isExternalCert && sslCertServices.includes(serviceType); + } + + get showExternalSslKey(): boolean { + const serviceType = this.serviceForm.controls.service_type?.value; + const isExternalCert = this.serviceForm.controls.certificateType?.value === 'external'; + const isSslEnabled = this.serviceForm.controls.ssl?.value; + + const sslKeyServices = ['iscsi', 'grafana', 'oauth2-proxy', 'nvmeof', 'nfs', 'mgmt-gateway']; + return isSslEnabled && isExternalCert && sslKeyServices.includes(serviceType); + } + closeModal(): void { if (this.pageURL === 'services') { this.location.back(); @@ -1429,4 +1543,39 @@ export class ServiceFormComponent extends CdForm implements OnInit { this.modalService.dismissAll(); } } + + applySslCertificateConfig( + serviceSpec: object, + values: object, + options: { + sslCertField?: string; + sslKeyField?: string; + includeSslKey?: boolean; + includeCaCert?: boolean; + } = {} + ): void { + const { + sslCertField = 'ssl_cert', + sslKeyField = 'ssl_key', + includeSslKey = true, + includeCaCert = false + } = options; + + serviceSpec['certificate_source'] = + values['certificateType'] === 'internal' ? 'cephadm-signed' : 'inline'; + + if (values['certificateType'] === 'internal' && values['custom_sans']?.length > 0) { + serviceSpec['custom_sans'] = values['custom_sans']; + } + + if (values['certificateType'] === 'external') { + serviceSpec[sslCertField] = values['ssl_cert']?.trim(); + if (includeSslKey) { + serviceSpec[sslKeyField] = values['ssl_key']?.trim(); + } + if (includeCaCert) { + serviceSpec['ssl_ca_cert'] = values['ssl_ca_cert']?.trim(); + } + } + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html index b864fb163ebe..a979e34e5d65 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html @@ -11,6 +11,7 @@ [hasDetails]="hasDetails" [serverSide]="true" [count]="count" + updateExpandedOnRefresh="always" (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)"> - @if (row.certificate) { +@if (row.certificate?.requires_certificate && row.certificate?.status && row.certificate?.status !== 'not_configured') { + + {{ formatCertificateStatus(row.certificate) }} - } @else { - - - } + +} @else { + - +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts index 0aa51e340abc..db665911aab7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts @@ -1,6 +1,5 @@ import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { Router } from '@angular/router'; - import { delay } from 'rxjs/operators'; import { CephServiceService } from '~/app/shared/api/ceph-service.service'; @@ -21,7 +20,8 @@ import { Permissions } from '~/app/shared/models/permissions'; import { CephCertificateStatus, CephServiceCertificate, - CephServiceSpec + CephServiceSpec, + CERTIFICATE_STATUS_ICON_MAP } from '~/app/shared/models/service.interface'; import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe'; import { RelativeDatePipe } from '~/app/shared/pipes/relative-date.pipe'; @@ -86,6 +86,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI icons = Icons; serviceUrls = { grafana: '', prometheus: '', alertmanager: '' }; isMgmtGateway: boolean = false; + statusIconMap = CERTIFICATE_STATUS_ICON_MAP; constructor( private actionLabels: ActionLabelsI18n, @@ -160,6 +161,9 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI }); let modalRef = this.cdsModalService.show(ServiceFormComponent); Object.assign(modalRef, initialState); + modalRef.serviceUpdated.subscribe(() => { + this.table?.reloadData(); + }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts index 5af1ff726544..78f0d682954d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts @@ -1,4 +1,11 @@ -import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; import { ICON_TYPE, Icons, IconSize } from '../../enum/icons.enum'; @Component({ @@ -8,13 +15,23 @@ import { ICON_TYPE, Icons, IconSize } from '../../enum/icons.enum'; standalone: false, encapsulation: ViewEncapsulation.None }) -export class IconComponent implements OnInit { +export class IconComponent implements OnInit, OnChanges { @Input() type!: keyof typeof ICON_TYPE; @Input() size: IconSize = IconSize.size16; icon: string; ngOnInit() { + this.updateIcon(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['type']) { + this.updateIcon(); + } + } + + private updateIcon() { this.icon = Icons[this.type]; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts index 303590a814a7..b9decea14fb0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts @@ -16,6 +16,16 @@ export enum CephCertificateStatus { invalid = 'invalid' } +export const CERTIFICATE_STATUS_ICON_MAP: Record = { + valid: 'success', + expiring: 'warning', + expiring_soon: 'warning', + expired: 'danger', + not_configured: 'warning', + invalid: 'danger', + default: 'warning' +}; + export interface CephServiceCertificate { cert_name: string; scope: string; @@ -68,6 +78,7 @@ export interface CephServiceAdditionalSpec { ssl_certificate_key: string; ssl_protocols: string[]; ssl_ciphers: string[]; + certificate_source: string; port: number; initial_admin_password: string; rgw_realm: string; diff --git a/src/pybind/mgr/dashboard/services/certificate.py b/src/pybind/mgr/dashboard/services/certificate.py index a880d3c39ad2..53f17d23c6ea 100644 --- a/src/pybind/mgr/dashboard/services/certificate.py +++ b/src/pybind/mgr/dashboard/services/certificate.py @@ -301,7 +301,8 @@ class CertificateService: :return: Tuple of (cert_details, target_key, cert_name, actual_scope) """ user_cert_name = f"{service_type.replace('-', '_')}_ssl_cert" - cephadm_cert_name = f"cephadm-signed_{service_type}_cert" + cephadm_cert_name_by_service = f"cephadm-signed_{service_name}_cert" + cephadm_cert_name_by_type = f"cephadm-signed_{service_type}_cert" cert_details = None target_key = None cert_name = user_cert_name @@ -322,19 +323,30 @@ class CertificateService: cert_name = user_cert_name # If user-provided cert not found, try cephadm-signed certificate - if not cert_details and cert_ls_data and cephadm_cert_name in cert_ls_data: + # First try by service_name (e.g., cephadm-signed_rgw.test-rgw_cert) + if not cert_details and cert_ls_data and cephadm_cert_name_by_service in cert_ls_data: cert_details, target_key = _find_certificate_in_data( - cert_ls_data, cephadm_cert_name, CertificateScope.HOST, + cert_ls_data, cephadm_cert_name_by_service, CertificateScope.HOST, service_name, daemon_hostnames) if cert_details: - cert_name = cephadm_cert_name + cert_name = cephadm_cert_name_by_service + actual_scope = CertificateScope.HOST.value.upper() + + # Then try by service_type (e.g., cephadm-signed_grafana_cert) + if not cert_details and cert_ls_data and cephadm_cert_name_by_type in cert_ls_data: + cert_details, target_key = _find_certificate_in_data( + cert_ls_data, cephadm_cert_name_by_type, CertificateScope.HOST, + service_name, daemon_hostnames) + if cert_details: + cert_name = cephadm_cert_name_by_type actual_scope = CertificateScope.HOST.value.upper() return (cert_details, target_key, cert_name, actual_scope) @staticmethod def fetch_certificates_for_service(orch: OrchClient, service_type: str, - user_cert_name: str, cephadm_cert_name: str + user_cert_name: str, cephadm_cert_name: str, + service_name: Optional[str] = None ) -> Dict[str, Any]: """ Fetch certificates for a specific service, including missing ones. @@ -342,7 +354,8 @@ class CertificateService: :param orch: Orchestrator client instance :param service_type: Service type for filter pattern :param user_cert_name: User-provided certificate name - :param cephadm_cert_name: Cephadm-signed certificate name + :param cephadm_cert_name: Cephadm-signed certificate name (by service_type) + :param service_name: Optional service name for cephadm-signed cert by service_name :return: Dictionary of certificate data """ service_type_for_filter = service_type.replace('-', '_') @@ -355,11 +368,18 @@ class CertificateService: ) cert_ls_data = cert_ls_result or {} + # Build list of cert names to check + # Cephadm-signed certs may use service_name (e.g., rgw.test-rgw) or service_type + cephadm_cert_name_by_service = \ + f"cephadm-signed_{service_name}_cert" if service_name else None + missing_certs: List[str] = [] if user_cert_name not in cert_ls_data: missing_certs.append(user_cert_name) if cephadm_cert_name not in cert_ls_data: missing_certs.append(cephadm_cert_name) + if cephadm_cert_name_by_service and cephadm_cert_name_by_service not in cert_ls_data: + missing_certs.append(cephadm_cert_name_by_service) # Fetch any missing certificates individually for cert_name in missing_certs: @@ -435,7 +455,8 @@ class CertificateService: response.days_to_expiration = remaining_days response.signed_by = signed_by response.has_certificate = True - response.certificate_source = 'reference' + is_cephadm_signed = CEPHADM_SIGNED_CERT in cert_name + response.certificate_source = 'cephadm-signed' if is_cephadm_signed else 'inline' response.expiry_date = expiry_date response.issuer = issuer_str response.common_name = common_name