]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Update create/edit service modal to support certmgr 67276/head
authorAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 9 Feb 2026 19:43:44 +0000 (01:13 +0530)
committerAbhishek Desai <abhishek.desai1@ibm.com>
Wed, 25 Feb 2026 18:08:38 +0000 (23:38 +0530)
fixes: https://tracker.ceph.com/issues/74636
Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
13 files changed:
src/pybind/mgr/dashboard/controllers/certificate.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts
src/pybind/mgr/dashboard/services/certificate.py

index ebcdb9e68d350beb91ac39f4d8709b2759752bdd..579f227d97dee8b7ae7672fa74c271f8c1fd7cb5 100644 (file)
@@ -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)
index 2285de4d3d1b29f7c7221cc84be105170c1e02a8..fadb8312a7c9f6d472af63f4858fb2054dde383c 100644 (file)
@@ -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,
index b04f2ed1ad335de60902553b8734122e7240016e..d57e0322277a933dd9cd232591b0be772229b822 100644 (file)
@@ -41,7 +41,7 @@
             </span>
             <ng-container *ngTemplateOutlet="statusTemplate; context: { status: certificate?.status }"></ng-container>
           </div>
-          <!-- icon -->
+          @if (SERVICES_SUPPORTING_CERT_EDIT.includes(serviceType)) {
           <cds-icon-button kind="ghost"
                            size="sm"
                            (click)="onEdit()"
@@ -49,6 +49,7 @@
             <cd-icon type="edit">
             </cd-icon>
           </cds-icon-button>
+          }
 
         </div>
         <div cdsStack="vertical"
index 6aea2e317a3ae7feb2aae24d0e74d4f5d779d4d5..b60d008b357ee4597d0cf249abe01c49ce022d48 100644 (file)
@@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { By } from '@angular/platform-browser';
 import { expect as jestExpect } from '@jest/globals';
 
+import { ComponentsModule } from '~/app/shared/components/components.module';
 import { IconComponent } from '~/app/shared/components/icon/icon.component';
 import {
   CephCertificateStatus,
@@ -34,6 +35,7 @@ describe('ServiceCertificateDetailsComponent', () => {
   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', () => {
index 05605a7385342ddb6e17f19b6f21be27329aae92..1c48dde7dff13da7252f9f7b72a7cfb21e4c8d70 100644 (file)
@@ -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<string, keyof typeof ICON_TYPE> = {
-    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:
index 8a158c229f441bfb06f4efe917f4c96c62f56330..648b0282202d39737a63d88c0dd34755904096d7 100644 (file)
         </cds-text-label>
       </div>
       }
-      <!-- RGW, Ingress, iSCSI, Oauth2-proxy & mgmt-gateway -->
+      <!-- RGW, Ingress, iSCSI, Oauth2-proxy, NFS & mgmt-gateway -->
       @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)) {
       <!-- ssl -->
       @if (!['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) {
       <div class="form-item">
         </cds-checkbox>
       </div>
       }
-      <!-- ssl_cert -->
-      @if (serviceForm.controls.ssl.value || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) {
-      <div class="form-item">
-        <cds-textarea-label helperText="The SSL certificate in PEM format."
-                            i18n
-                            [invalid]="serviceForm.controls.ssl_cert.invalid && serviceForm.controls.ssl_cert.dirty"
-                            [invalidText]="invalidSslCertError">Certificate
-          <div class="cd-cl-form-input">
-            <textarea cdsTextArea
-                      id="ssl_cert"
-                      formControlName="ssl_cert"
-                      cols="100"
-                      rows="4"
-                      [invalid]="serviceForm.controls.ssl_cert.invalid && serviceForm.controls.ssl_cert.dirty">
-            </textarea>
 
-            <cds-file-uploader buttonText="Choose file"
-                               i18n-buttonText
-                               buttonType="secondary"
-                               [multiple]="false"
-                               size="sm"
-                               (filesChange)="fileUpload($event, 'ssl_cert')"
-                               (removeFile)="clearText()"></cds-file-uploader>
+      <!-- Certificate Management UI -->
+      @if (serviceForm.controls.ssl.value && ['rgw', 'ingress', 'iscsi', 'grafana', 'oauth2-proxy', 'mgmt-gateway', 'nvmeof', 'nfs'].includes(serviceForm.controls.service_type.value)) {
+      <div>
+        <!-- Current Certificate Section - Only shown in Edit mode when certificate exists -->
+        @if (editing && currentCertificate?.has_certificate) {
+        <div class="form-item">
+          <label class="cds--label fw-bold"
+                 i18n>Current Certificate</label>
+          <div class="row">
+            <div class="col-6">
+              <label class="cds--label"
+                     i18n>Certificate</label>
+              <div>{{ currentCertificate.cert_name }}</div>
+            </div>
+            <div class="col-6">
+              <label class="cds--label"
+                     i18n>Valid Until</label>
+              <div>{{ currentCertificate.expiry_date | cdDate }} • {{ currentCertificate.days_to_expiration }} <span i18n>days left</span></div>
+            </div>
+          </div>
+          <div class="row mt-3">
+            <div class="col-6">
+              <label class="cds--label"
+                     i18n>Status</label>
+              <div class="align-items-center">
+                <cd-icon [type]="statusIconMap[currentCertificate.status] || statusIconMap['default']"></cd-icon>
+                @switch (currentCertificate.status) {
+                  @case ('valid') { <span i18n>Valid</span> }
+                  @case ('expiring') { <span i18n>Expiring soon</span> }
+                  @case ('expired') { <span i18n>Expired</span> }
+                  @case ('not_configured') { <span i18n>Not configured</span> }
+                  @case ('invalid') { <span i18n>Invalid</span> }
+                  @default { {{ currentCertificate.status }} }
+                }
+              </div>
+            </div>
+            <div class="col-6">
+              <label class="cds--label"
+                     i18n>Issuer</label>
+              <div>
+                @if (currentCertificate.signed_by === 'cephadm') {
+                <span i18n>Internal (Cephadm CA)</span>
+                } @else {
+                {{ currentCertificate.issuer || 'External' }}
+                }
+              </div>
+            </div>
           </div>
-        </cds-textarea-label>
+        </div>
+        }
+
+        <!-- Certificate Authority Selection -->
+        <div class="form-item">
+          <label class="cds--label fw-bold"
+                 i18n>Choose Certificate Authority</label>
+          <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')"
+                       i18n>
+              Internal
+            </cds-radio>
+            <cds-radio value="external"
+                       (change)="onCertificateTypeChange('external')"
+                       i18n>
+              External
+            </cds-radio>
+          </cds-radio-group>
+        </div>
+
+        @if (showCertSourceChangeWarning) {
+          <cd-alert-panel type="warning"
+                          spacingClass="mb-3"
+                          i18n>
+            Changing the certificate source will redeploy the service daemons to apply the new certificate configuration.
+          </cd-alert-panel>
+        }
+
+        @if (serviceForm.controls.certificateType.value === 'internal') {
+        <cd-alert-panel type="info"
+                        spacingClass="mb-3"
+                        i18n>
+          Certificate will be generated automatically by Cephadm CA for internal certificate type.
+        </cd-alert-panel>
+        <div class="form-item">
+          <cd-text-label-list formControlName="custom_sans"
+                              label="Custom SAN Entries"
+                              i18n-label
+                              helperText="Optional list of Subject Alternative Names (hostnames, IPs, or DNS names) to include in the auto-generated certificate."
+                              i18n-helperText>
+          </cd-text-label-list>
+        </div>
+        }
+      </div>
+      }
+
+      <!-- ssl_cert - Only show when SSL is enabled AND certificate type is external -->
+      @if (showExternalSslCert) {
+      <div class="form-item">
+        <ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'ssl_cert', title: 'Certificate Input', helperText: 'Uploaded files will populate the certificate details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a .crt file, or paste the certificate PEM content directly.', invalidTemplate: invalidSslCertError, isRequired: false }"></ng-container>
         <ng-template #invalidSslCertError>
           @if (serviceForm.showError('ssl_cert', frm, 'required')) {
           <span class="invalid-feedback"
         </ng-template>
       </div>
       }
-      <!-- ssl_key -->
-      @if ((serviceForm.controls.ssl.value && !(['rgw', 'ingress'].includes(serviceForm.controls.service_type.value)))
-      || ['mgmt-gateway'].includes(serviceForm.controls.service_type.value)) {
+      @if (showExternalSslKey) {
       <div class="form-item">
-        <cds-textarea-label helperText="The SSL certificate in PEM format."
-                            i18n
-                            [invalid]="serviceForm.controls.ssl_key.invalid && serviceForm.controls.ssl_key.dirty"
-                            [invalidText]="invalidSslKeyError">Private key
-          <div class="cd-col-form-input">
-            <textarea cdsTextArea
-                      id="ssl_key"
-                      formControlName="ssl_key"
-                      cols="100"
-                      rows="4"
-                      [invalid]="serviceForm.controls.ssl_key.invalid && serviceForm.controls.ssl_key.dirty">
-            </textarea>
-
-            <cds-file-uploader buttonText="Choose file"
-                               i18n-buttonText
-                               buttonType="secondary"
-                               [multiple]="false"
-                               size="sm"
-                               (filesChange)="fileUpload($event, 'ssl_key')"
-                               (removeFile)="clearText()"></cds-file-uploader>
-          </div>
-        </cds-textarea-label>
+        <ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'ssl_key', title: 'Private Key Input', helperText: 'Uploaded files will populate the private key details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a .key file, or paste the private key PEM content directly.', invalidTemplate: invalidSslKeyError, isRequired: false }"></ng-container>
         <ng-template #invalidSslKeyError>
           @if (serviceForm.showError('ssl_key', frm, 'required')) {
           <span class="invalid-feedback"
           }
         </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') {
+      <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>
+          @if (serviceForm.showError('ssl_ca_cert', frm, 'required')) {
+          <span class="invalid-feedback"
+                i18n>This field is required.</span>
+          }
+          @if (serviceForm.showError('ssl_ca_cert', frm, 'pattern')) {
+          <span class="invalid-feedback"
+                i18n>Invalid CA certificate.</span>
+          }
+        </ng-template>
+      </div>
+      }
       }
 
       <!-- RGW QAT Compression -->
       @if (serviceForm.controls.enable_mtls.value) {
       <!-- root_ca_cert -->
       <div class="form-item">
-        <cds-textarea-label cdRequiredField="Root CA certificate"
-                            [invalid]="serviceForm.controls.root_ca_cert.invalid && serviceForm.controls.root_ca_cert.dirty"
-                            [invalidText]="invalidRootCaCertError">Root CA certificate
-          <textarea cdsTextArea
-                    id="root_ca_cert"
-                    formControlName="root_ca_cert"
-                    cols="100"
-                    rows="4"
-                    [invalid]="serviceForm.controls.root_ca_cert.invalid && serviceForm.controls.root_ca_cert.dirty">
-          </textarea>
-
-          <cds-file-uploader buttonText="Choose file"
-                             i18n-buttonText
-                             buttonType="secondary"
-                             [multiple]="false"
-                             size="sm"
-                             (filesChange)="fileUpload($event, 'root_ca_cert')"
-                             (removeFile)="clearText()"></cds-file-uploader>
-        </cds-textarea-label>
+        <ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'root_ca_cert', title: 'Root CA Certificate Input', helperText: 'Uploaded files will populate the Root CA certificate details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a Root CA certificate file, or paste the Root CA certificate PEM content directly.', invalidTemplate: invalidRootCaCertError, isRequired: true }"></ng-container>
         <ng-template #invalidRootCaCertError>
           @if (serviceForm.showError('root_ca_cert', frm, 'required')) {
           <span class="invalid-feedback"
 
       <!-- client_cert -->
       <div class="form-item">
-        <cds-textarea-label cdRequiredField="Client CA certificate"
-                            [invalid]="serviceForm.controls.client_cert.invalid && serviceForm.controls.client_cert.dirty"
-                            [invalidText]="invalidClientCertError">Client CA certificate
-          <textarea cdsTextArea
-                    id="client_cert"
-                    formControlName="client_cert"
-                    cols="100"
-                    rows="4"
-                    [invalid]="serviceForm.controls.client_cert.invalid && serviceForm.controls.client_cert.dirty">
-          </textarea>
-          <cds-file-uploader buttonText="Choose file"
-                             i18n-buttonText
-                             buttonType="secondary"
-                             [multiple]="false"
-                             size="sm"
-                             (filesChange)="fileUpload($event, 'client_cert')"
-                             (removeFile)="clearText()"></cds-file-uploader>
-        </cds-textarea-label>
+        <ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'client_cert', title: 'Client Certificate Input', helperText: 'Uploaded files will populate the client certificate details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a client certificate file, or paste the client certificate PEM content directly.', invalidTemplate: invalidClientCertError, isRequired: true }"></ng-container>
         <ng-template #invalidClientCertError>
           @if (serviceForm.showError('client_cert', frm, 'required')) {
           <span class="invalid-feedback"
 
       <!-- client_key -->
       <div class="form-item">
-        <cds-textarea-label cdRequiredField="Client key"
-                            [invalid]="serviceForm.controls.client_key.invalid && serviceForm.controls.client_key.dirty"
-                            [invalidText]="invalidClientKeyError">Client key
-          <textarea cdsTextArea
-                    id="client_key"
-                    formControlName="client_key"
-                    cols="100"
-                    rows="4"
-                    [invalid]="serviceForm.controls.client_key.invalid && serviceForm.controls.client_key.dirty">
-          </textarea>
-
-          <cds-file-uploader buttonText="Choose file"
-                             i18n-buttonText
-                             buttonType="secondary"
-                             [multiple]="false"
-                             size="sm"
-                             (filesChange)="fileUpload($event, 'client_key')"
-                             (removeFile)="clearText()"></cds-file-uploader>
-        </cds-textarea-label>
+        <ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'client_key', title: 'Client Key Input', helperText: 'Uploaded files will populate the client key details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a client key file, or paste the client key PEM content directly.', invalidTemplate: invalidClientKeyError, isRequired: true }"></ng-container>
         <ng-template #invalidClientKeyError>
           @if (serviceForm.showError('client_key', frm, 'required')) {
           <span class="invalid-feedback"
 
       <!-- server_cert -->
       <div class="form-item">
-        <cds-textarea-label cdRequiredField="Gateway server certificate"
-                            [invalid]="serviceForm.controls.server_cert.invalid && serviceForm.controls.server_cert.dirty"
-                            [invalidText]="invalidServerCertError">Gateway server certificate
-          <textarea cdsTextArea
-                    id="server_cert"
-                    formControlName="server_cert"
-                    cols="100"
-                    rows="4"
-                    [invalid]="serviceForm.controls.server_cert.invalid && serviceForm.controls.server_cert.dirty">
-          </textarea>
-
-          <cds-file-uploader buttonText="Choose file"
-                             i18n-buttonText
-                             buttonType="secondary"
-                             [multiple]="false"
-                             size="sm"
-                             (filesChange)="fileUpload($event, 'server_cert')"
-                             (removeFile)="clearText()"></cds-file-uploader>
-        </cds-textarea-label>
+        <ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'server_cert', title: 'Gateway Server Certificate Input', helperText: 'Uploaded files will populate the gateway server certificate details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a gateway server certificate file, or paste the certificate PEM content directly.', invalidTemplate: invalidServerCertError, isRequired: true }"></ng-container>
         <ng-template #invalidServerCertError>
           @if (serviceForm.showError('server_cert', frm, 'required')) {
           <span class="invalid-feedback"
 
       <!-- server_key -->
       <div class="form-item">
-        <cds-textarea-label cdRequiredField="Gateway server key"
-                            [invalid]="serviceForm.controls.server_key.invalid && serviceForm.controls.server_key.dirty"
-                            [invalidText]="invalidServerKeyError">Gateway server key
-          <textarea cdsTextArea
-                    id="server_key"
-                    formControlName="server_key"
-                    cols="100"
-                    rows="4"
-                    [invalid]="serviceForm.controls.server_key.invalid && serviceForm.controls.server_key.dirty">
-          </textarea>
-
-          <cds-file-uploader buttonText="Choose file"
-                             i18n-buttonText
-                             buttonType="secondary"
-                             [multiple]="false"
-                             size="sm"
-                             (filesChange)="fileUpload($event, 'server_key')"
-                             (removeFile)="clearText()"></cds-file-uploader>
-        </cds-textarea-label>
+        <ng-container *ngTemplateOutlet="fileUploaderTextarea; context: { controlName: 'server_key', title: 'Gateway Server Key Input', helperText: 'Uploaded files will populate the gateway server key details automatically. Or paste the PEM content directly in the text area.', description: 'Upload a gateway server key file, or paste the key PEM content directly.', invalidTemplate: invalidServerKeyError, isRequired: true }"></ng-container>
         <ng-template #invalidServerKeyError>
           @if (serviceForm.showError('server_key', frm, 'required')) {
           <span class="invalid-feedback"
                                  target="_blank">https://ssl-config.mozilla.org/#server=nginx</a>
   </span>
 </ng-template>
+
+<ng-template #fileUploaderTextarea
+             let-controlName="controlName"
+             let-title="title"
+             let-helperText="helperText"
+             let-description="description"
+             let-invalidTemplate="invalidTemplate"
+             let-isRequired="isRequired">
+  <cds-textarea-label [attr.cdRequiredField]="isRequired ? title : null"
+                      [helperText]="helperText"
+                      [invalid]="serviceForm.controls[controlName].invalid && serviceForm.controls[controlName].dirty"
+                      [invalidText]="invalidTemplate">
+    <cds-file-uploader [title]="title"
+                       buttonText="Upload File"
+                       i18n-buttonText
+                       buttonType="tertiary"
+                       [multiple]="false"
+                       size="md"
+                       [description]="description"
+                       (filesChange)="fileUpload($event, controlName)"
+                       (removeFile)="clearText()"></cds-file-uploader>
+    <textarea cdsTextArea
+              [id]="controlName"
+              [theme]="'dark'"
+              placeholder="Paste certificate or private key PEM content"
+              [formControl]="serviceForm.controls[controlName]"
+              cols="50"
+              rows="5"
+              [invalid]="serviceForm.controls[controlName].invalid && serviceForm.controls[controlName].dirty">
+    </textarea>
+  </cds-textarea-label>
+</ng-template>
index 03628af822622f9444c67629dc8e631a8b8f74cc..2490f993d497ab534ecc679d97399477bc47c4e3 100644 (file)
@@ -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']
         });
       });
index 4943ab6651cf0b6f51bb7d4a25dbc87300920afc..6ecb3ba29158b58515db427de13e09358297986d 100644 (file)
@@ -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<void>();
+
   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<ListItem> = [];
                 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();
+      }
+    }
+  }
 }
index b864fb163ebe8da9562642fa048163983476c655..a979e34e5d654d406d8b2da9ab8d794213d31bbc 100644 (file)
@@ -11,6 +11,7 @@
             [hasDetails]="hasDetails"
             [serverSide]="true"
             [count]="count"
+            updateExpandedOnRefresh="always"
             (setExpandedRow)="setExpandedRow($event)"
             (updateSelection)="updateSelection($event)">
     <cd-table-actions class="table-actions"
 
 <ng-template #certificateStatusTpl
              let-row="data.row">
-  @if (row.certificate) {
+@if (row.certificate?.requires_certificate && row.certificate?.status && row.certificate?.status !== 'not_configured') {
+  <span class="d-flex align-items-center gap-1">
+    <cd-icon [type]="statusIconMap[row.certificate.status] || statusIconMap['default']"></cd-icon>
     {{ formatCertificateStatus(row.certificate) }}
-  } @else {
-    -
-  }
+  </span>
+} @else {
+  -
+}
 </ng-template>
index 0aa51e340abc14d4d5bbe49d1f5bad222e1e144e..db665911aab78e324efd392b48b75c14d7bb8853 100644 (file)
@@ -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();
+      });
     }
   }
 
index 5af1ff726544850b155a0231ff5a2d380f7cc9f7..78f0d682954dec6f60acd576d52435dd9128c378 100644 (file)
@@ -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];
   }
 }
index 303590a814a742c675f3ec42002e00dd114a11e9..b9decea14fb024ac9c133fe30b68264fb4a242fe 100644 (file)
@@ -16,6 +16,16 @@ export enum CephCertificateStatus {
   invalid = 'invalid'
 }
 
+export const CERTIFICATE_STATUS_ICON_MAP: Record<string, string> = {
+  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;
index a880d3c39ad22f89767701a00622f58dbe090cf8..53f17d23c6ea68f1010cbd90a9cff108a2e2f35c 100644 (file)
@@ -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