]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Support wildcard sans and zonegroup hostnames 69090/head
authorAbhishek Desai <abhishek.desai1@ibm.com>
Tue, 26 May 2026 07:48:40 +0000 (13:18 +0530)
committerAbhishek Desai <abhishek.desai1@ibm.com>
Thu, 18 Jun 2026 15:59:39 +0000 (21:29 +0530)
fixes : https://tracker.ceph.com/issues/76795
Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
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/shared/models/service.interface.ts

index 9cdf6e025bc0fb0f231d4fd5206c12b89afe889b..c77c36abc14801cbcff5f33678c684b950e629ef 100644 (file)
           }
         </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 -->
           <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>
           </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>
                               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>
       }
         </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>
index 6ffd5be14c652d825709575515c3076ff54859a0..915c320baccf75a0c53ef1718b36921c8c1243cb 100644 (file)
@@ -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();
index bbde926b6b591d3ba6f9308d52f81ea47e11c278..fb5f36af337fb4693d53fc4264ba2717bc3199ff 100644 (file)
@@ -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();
index af1e36c703de10ac0aea1cefa022b2be28751c04..eaca956d3adcb6a0d42ce7166e7da4ca4c71b94c 100644 (file)
@@ -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',