import { MultiClusterListComponent } from './multi-cluster/multi-cluster-list/multi-cluster-list.component';
import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
import { MultiClusterDetailsComponent } from './multi-cluster/multi-cluster-details/multi-cluster-details.component';
+import { CertificateAuthorityFormComponent } from '~/app/shared/components/certificate-authority-form/certificate-authority-form.component';
import { TextLabelListComponent } from '~/app/shared/components/text-label-list/text-label-list.component';
@NgModule({
TagModule,
TabsModule,
TextLabelListComponent,
+ CertificateAuthorityFormComponent,
SelectModule,
LayoutModule,
NumberModule,
serviceForm.controls.service_type.value
)
) {
- <ng-container *ngTemplateOutlet="certificateAuthorityManagement"></ng-container>
+ <cd-certificate-authority-form
+ [formGroup]="serviceForm"
+ [editing]="editing"
+ [currentCertificate]="currentCertificate"
+ [showCertSourceChangeWarning]="showCertSourceChangeWarning"
+ (certificateTypeChange)="onCertificateTypeChange($event)"
+ >
+ </cd-certificate-authority-form>
}
<!-- ssl_cert - Only show when SSL is enabled AND certificate type is external -->
</fieldset>
</div>
@if (serviceForm.controls.enable_mtls.value) {
- <ng-container *ngTemplateOutlet="certificateAuthorityManagement"></ng-container>
+ <cd-certificate-authority-form
+ [formGroup]="serviceForm"
+ [editing]="editing"
+ [currentCertificate]="currentCertificate"
+ [showCertSourceChangeWarning]="showCertSourceChangeWarning"
+ (certificateTypeChange)="onCertificateTypeChange($event)"
+ >
+ </cd-certificate-authority-form>
@if (serviceForm.controls.certificateType.value === CertificateType.external) {
<!-- root_ca_cert -->
</span>
</ng-template>
-<ng-template #certificateAuthorityManagement>
- <div [formGroup]="serviceForm">
- <!-- Current Certificate Section - Only shown in Edit mode when certificate exists -->
- @if (editing && currentCertificate?.has_certificate) {
- <div class="form-item">
- <legend
- class="cds--label cds--type-heading-compact-01"
- i18n
- >
- Current Certificate
- </legend>
- <div class="row">
- <div class="col-6">
- <legend
- class="cds--label"
- i18n
- >
- Certificate
- </legend>
- <div>{{ currentCertificate.cert_name }}</div>
- </div>
- <div class="col-6">
- <legend
- class="cds--label"
- i18n
- >
- Valid Until
- </legend>
- <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">
- <legend
- class="cds--label"
- i18n
- >
- Status
- </legend>
- <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">
- <legend
- class="cds--label"
- i18n
- >
- Issuer
- </legend>
- <div>
- @if (currentCertificate.signed_by === 'cephadm') {
- <span i18n>Internal (Cephadm CA)</span>
- } @else {
- {{ currentCertificate.issuer || 'External' }}
- }
- </div>
- </div>
- </div>
- </div>
- }
-
- <!-- Certificate Authority Selection -->
- <div class="form-item">
- <label
- class="cds--label cds--type-heading-compact-01"
- for="certificateType"
- 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."
- i18n-helperText
- >
- <cds-radio
- [value]="CertificateType.internal"
- (change)="onCertificateTypeChange(CertificateType.internal)"
- i18n
- >
- Internal
- </cds-radio>
- <cds-radio
- [value]="CertificateType.external"
- (change)="onCertificateTypeChange(CertificateType.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 === CertificateType.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>
- @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>
-
<ng-template
#fileUploaderTextarea
let-controlName="controlName"
CephServiceSpec,
CertificateType,
QatOptions,
- QatSepcs,
- CERTIFICATE_STATUS_ICON_MAP
+ QatSepcs
} from '~/app/shared/models/service.interface';
import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
hostsAndLabels$: Observable<{ hosts: { content: string }[]; labels: { content: string }[] }>;
currentCertificate: CephServiceCertificate = null;
currentSpecCertificateSource: string = null;
- statusIconMap = CERTIFICATE_STATUS_ICON_MAP;
constructor(
public actionLabels: ActionLabelsI18n,
--- /dev/null
+<div [formGroup]="formGroup">
+ @if (editing && currentCertificate?.has_certificate) {
+ <div class="form-item">
+ <span
+ class="cds--label cds--type-heading-compact-01"
+ i18n
+ >Current Certificate</span
+ >
+ <div
+ cdsRow
+ [condensed]="true"
+ >
+ <div
+ cdsCol
+ [columnNumbers]="[
+ ['sm', 4],
+ ['md', 4]
+ ]"
+ >
+ <div
+ cdsStack="vertical"
+ [gap]="1"
+ >
+ <span
+ class="cds--type-label-01"
+ i18n
+ >Certificate</span
+ >
+ <span class="cds--type-body-compact-01">{{ currentCertificate.cert_name }}</span>
+ </div>
+ </div>
+ <div
+ cdsCol
+ [columnNumbers]="[
+ ['sm', 4],
+ ['md', 4]
+ ]"
+ >
+ <div
+ cdsStack="vertical"
+ [gap]="1"
+ >
+ <span
+ class="cds--type-label-01"
+ i18n
+ >Valid Until</span
+ >
+ <span class="cds--type-body-compact-01">
+ {{ currentCertificate.expiry_date | cdDate }} •
+ {{ currentCertificate.days_to_expiration }}
+ <span i18n>days left</span>
+ </span>
+ </div>
+ </div>
+ </div>
+ <div
+ cdsRow
+ [condensed]="true"
+ class="cds-mt-3"
+ >
+ <div
+ cdsCol
+ [columnNumbers]="[
+ ['sm', 4],
+ ['md', 4]
+ ]"
+ >
+ <div
+ cdsStack="vertical"
+ [gap]="1"
+ >
+ <span
+ class="cds--type-label-01"
+ i18n
+ >Status</span
+ >
+ <div
+ cdsStack="horizontal"
+ [gap]="2"
+ >
+ <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>
+ <div
+ cdsCol
+ [columnNumbers]="[
+ ['sm', 4],
+ ['md', 4]
+ ]"
+ >
+ <div
+ cdsStack="vertical"
+ [gap]="1"
+ >
+ <span
+ class="cds--type-label-01"
+ i18n
+ >Issuer</span
+ >
+ <span class="cds--type-body-compact-01">
+ @if (currentCertificate.signed_by === 'cephadm') {
+ <span i18n>Internal (Cephadm CA)</span>
+ } @else {
+ {{ currentCertificate.issuer || 'External' }}
+ }
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ }
+
+ <div class="form-item">
+ <label
+ class="cds--label cds--type-heading-compact-01"
+ for="certificateType"
+ i18n
+ >Choose Certificate Authority</label
+ >
+ <cds-radio-group
+ id="certificateType"
+ 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."
+ i18n-helperText
+ >
+ <cds-radio
+ [value]="CertificateType.internal"
+ (change)="onCertificateTypeChange(CertificateType.internal)"
+ i18n
+ >
+ Internal
+ </cds-radio>
+ <cds-radio
+ [value]="CertificateType.external"
+ (change)="onCertificateTypeChange(CertificateType.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 (formGroup.controls.certificateType.value === CertificateType.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>
+ @if (
+ formGroup.controls.service_type?.value === 'rgw' &&
+ formGroup.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>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { expect as jestExpect } from '@jest/globals';
+
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CertificateType } from '~/app/shared/models/service.interface';
+import { CertificateAuthorityFormComponent } from './certificate-authority-form.component';
+
+describe('CertificateAuthorityFormComponent', () => {
+ let component: CertificateAuthorityFormComponent;
+ let fixture: ComponentFixture<CertificateAuthorityFormComponent>;
+ let formBuilder: CdFormBuilder;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ReactiveFormsModule, CertificateAuthorityFormComponent]
+ }).compileComponents();
+
+ formBuilder = new CdFormBuilder();
+ fixture = TestBed.createComponent(CertificateAuthorityFormComponent);
+ component = fixture.componentInstance;
+ component.formGroup = formBuilder.group({
+ certificateType: [CertificateType.internal],
+ custom_sans: [[]],
+ service_type: ['rgw'],
+ virtual_host_enabled: [false],
+ wildcard_enabled: [false]
+ });
+ });
+
+ it('should create', () => {
+ fixture.detectChanges();
+ jestExpect(component).toBeTruthy();
+ });
+
+ it('should emit certificateTypeChange', () => {
+ const emitSpy = jest.spyOn(component.certificateTypeChange, 'emit');
+ component.onCertificateTypeChange(CertificateType.external);
+ jestExpect(emitSpy).toHaveBeenCalledWith(CertificateType.external);
+ });
+});
--- /dev/null
+import { CommonModule } from '@angular/common';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { ReactiveFormsModule } from '@angular/forms';
+import { CheckboxModule, GridModule, LayoutModule, RadioModule } from 'carbon-components-angular';
+
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import {
+ CephServiceCertificate,
+ CertificateType,
+ CERTIFICATE_STATUS_ICON_MAP
+} from '~/app/shared/models/service.interface';
+import { PipesModule } from '~/app/shared/pipes/pipes.module';
+import { ComponentsModule } from '../components.module';
+import { TextLabelListComponent } from '../text-label-list/text-label-list.component';
+
+@Component({
+ selector: 'cd-certificate-authority-form',
+ templateUrl: './certificate-authority-form.component.html',
+ styleUrls: ['./certificate-authority-form.component.scss'],
+ standalone: true,
+ imports: [
+ CommonModule,
+ ReactiveFormsModule,
+ RadioModule,
+ CheckboxModule,
+ GridModule,
+ LayoutModule,
+ PipesModule,
+ ComponentsModule,
+ TextLabelListComponent
+ ]
+})
+export class CertificateAuthorityFormComponent {
+ readonly CertificateType = CertificateType;
+ readonly statusIconMap = CERTIFICATE_STATUS_ICON_MAP;
+
+ @Input() formGroup: CdFormGroup;
+ @Input() editing = false;
+ @Input() currentCertificate: CephServiceCertificate = null;
+ @Input() showCertSourceChangeWarning = false;
+
+ @Output() certificateTypeChange = new EventEmitter<CertificateType>();
+
+ onCertificateTypeChange(type: CertificateType): void {
+ this.certificateTypeChange.emit(type);
+ }
+}