mirror of
https://github.com/ceph/ceph
synced 2026-08-01 22:45:39 +00:00
Merge 3544a4ad5d into 8482cb11bc
This commit is contained in:
commit
7158bf075d
@ -11,6 +11,7 @@ import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggle
|
||||
import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service';
|
||||
import { SharedModule } from '~/app/shared/shared.module';
|
||||
import { TextLabelListComponent } from '~/app/shared/components/text-label-list/text-label-list.component';
|
||||
import { CertificateAuthorityFormComponent } from '~/app/shared/components/certificate-authority-form/certificate-authority-form.component';
|
||||
import { IscsiSettingComponent } from './iscsi-setting/iscsi-setting.component';
|
||||
import { IscsiTabsComponent } from './iscsi-tabs/iscsi-tabs.component';
|
||||
import { IscsiTargetDetailsComponent } from './iscsi-target-details/iscsi-target-details.component';
|
||||
@ -152,7 +153,8 @@ import { NvmeofEditAuthenticationComponent } from './nvmeof-edit-authentication/
|
||||
ThemeModule,
|
||||
NvmeofSetupCardsComponent,
|
||||
NvmeofGatewayGroupFilterComponent,
|
||||
TextLabelListComponent
|
||||
TextLabelListComponent,
|
||||
CertificateAuthorityFormComponent
|
||||
],
|
||||
declarations: [
|
||||
RbdListComponent,
|
||||
|
||||
@ -91,6 +91,7 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges
|
||||
orchStatus: OrchestratorStatus | undefined;
|
||||
private destroy$ = new Subject<void>();
|
||||
private sub: Subscription | undefined;
|
||||
private pendingHostReload = false;
|
||||
|
||||
constructor(
|
||||
private authStorageService: AuthStorageService,
|
||||
@ -303,9 +304,12 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges
|
||||
this.tableContext =
|
||||
context || this.tableContext || new CdTableFetchDataContext(() => undefined);
|
||||
if (this.isLoadingHosts) {
|
||||
// Edit preselection may arrive while the initial fetch is in-flight
|
||||
this.pendingHostReload = true;
|
||||
return;
|
||||
}
|
||||
this.isLoadingHosts = true;
|
||||
this.pendingHostReload = false;
|
||||
|
||||
if (this.sub) {
|
||||
this.sub.unsubscribe();
|
||||
@ -323,6 +327,10 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this.isLoadingHosts = false;
|
||||
if (this.pendingHostReload) {
|
||||
this.pendingHostReload = false;
|
||||
this.getHosts(this.tableContext);
|
||||
}
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
@ -452,9 +460,9 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy, OnChanges
|
||||
},
|
||||
{
|
||||
label: $localize`Encryption`,
|
||||
value: serviceSpec.spec?.enable_auth ? $localize`Enabled` : $localize`Disabled`,
|
||||
value: serviceSpec.spec?.encryption_key ? $localize`Enabled` : $localize`Disabled`,
|
||||
type: 'status',
|
||||
statusIcon: serviceSpec.spec?.enable_auth ? 'success' : 'error'
|
||||
statusIcon: serviceSpec.spec?.encryption_key ? 'success' : 'error'
|
||||
},
|
||||
{
|
||||
label: $localize`mTLS`,
|
||||
|
||||
@ -87,225 +87,125 @@
|
||||
cdsCol
|
||||
class="cds-pt-3 cds-pb-3"
|
||||
>
|
||||
<cd-nvmeof-gateway-node (hostsLoaded)="onHostsLoaded($event)"></cd-nvmeof-gateway-node>
|
||||
<cd-nvmeof-gateway-node
|
||||
[preSelectedHostnames]="preSelectedHostnames"
|
||||
(hostsLoaded)="onHostsLoaded($event)"
|
||||
></cd-nvmeof-gateway-node>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Encryption — same cdsRow/cdsCol layout as the nodes table for horizontal alignment -->
|
||||
<div
|
||||
cdsRow
|
||||
class="form-item cds-mb-0"
|
||||
class="form-item"
|
||||
>
|
||||
<div cdsCol>
|
||||
<cds-tile [cdsLayer]="1">
|
||||
<!-- Encryption -->
|
||||
<div
|
||||
cdsRow
|
||||
class="form-item"
|
||||
>
|
||||
<div cdsCol>
|
||||
<div class="cds--stack cds--stack-horizontal">
|
||||
<label
|
||||
class="cds--type-label-01"
|
||||
for="enableEncryption"
|
||||
i18n
|
||||
>Enable encryption</label
|
||||
>
|
||||
<cds-tag
|
||||
type="green"
|
||||
size="sm"
|
||||
i18n
|
||||
>Recommended</cds-tag
|
||||
>
|
||||
</div>
|
||||
<cds-checkbox
|
||||
id="enableEncryption"
|
||||
formControlName="enableEncryption"
|
||||
>
|
||||
<span
|
||||
class="cds--type-body-01"
|
||||
i18n
|
||||
>Secures group metadata and unlocks advanced authentication features.</span
|
||||
>
|
||||
</cds-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!groupForm.controls.enableEncryption.value) {
|
||||
<div
|
||||
cdsRow
|
||||
class="cds-mt-2"
|
||||
>
|
||||
<div cdsCol>
|
||||
<cd-alert-panel
|
||||
type="warning"
|
||||
spacingClass="mb-0"
|
||||
i18n
|
||||
>
|
||||
<span class="cds--type-heading-01"
|
||||
>Encryption is required if you plan to use DH-CHAP or mTLS
|
||||
authentication.</span
|
||||
>
|
||||
</cd-alert-panel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (groupForm.controls.enableEncryption.value) {
|
||||
<div
|
||||
cdsRow
|
||||
class="form-item"
|
||||
>
|
||||
<div cdsCol>
|
||||
<cds-textarea-label
|
||||
labelInputID="encryptionKey"
|
||||
helperText="Provide the encryption key used for securing the gateway group."
|
||||
i18n
|
||||
i18n-helperText
|
||||
>
|
||||
Encryption key
|
||||
<textarea
|
||||
cdsTextArea
|
||||
id="encryptionKey"
|
||||
formControlName="encryptionKey"
|
||||
rows="4"
|
||||
cols="100"
|
||||
cdsTheme="g10"
|
||||
>
|
||||
</textarea>
|
||||
</cds-textarea-label>
|
||||
<span
|
||||
class="invalid-feedback"
|
||||
*ngIf="groupForm.showError('encryptionKey', formDir, 'required')"
|
||||
i18n
|
||||
>This field is required.</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</cds-tile>
|
||||
|
||||
<cds-tile
|
||||
[cdsLayer]="1"
|
||||
class="cds-mt-3"
|
||||
<div
|
||||
[cdsStack]="'horizontal'"
|
||||
[gap]="3"
|
||||
class="cds-mb-2"
|
||||
>
|
||||
<div
|
||||
cdsRow
|
||||
class="form-item"
|
||||
<label
|
||||
class="cds--type-label-01"
|
||||
for="enableEncryption"
|
||||
i18n
|
||||
>Enable encryption</label
|
||||
>
|
||||
<div cdsCol>
|
||||
<div class="cds--stack cds--stack-horizontal cds-mb-3">
|
||||
<label
|
||||
class="cds--type-label-01"
|
||||
for="enableMtls"
|
||||
i18n
|
||||
>Enable Mutual TLS (mTLS)</label
|
||||
>
|
||||
</div>
|
||||
<cds-checkbox
|
||||
id="enableMtls"
|
||||
formControlName="enableMtls"
|
||||
>
|
||||
<span
|
||||
class="cds--type-body-01"
|
||||
i18n
|
||||
>Use mutual TLS (mTLS) to encrypt control and monitoring commands exchanged with
|
||||
NVMe-oF gateways.</span
|
||||
>
|
||||
</cds-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<cds-tag
|
||||
type="green"
|
||||
size="sm"
|
||||
i18n
|
||||
>Recommended</cds-tag
|
||||
>
|
||||
</div>
|
||||
<cds-checkbox
|
||||
id="enableEncryption"
|
||||
formControlName="enableEncryption"
|
||||
>
|
||||
<span
|
||||
class="cds--type-body-01 cds-mb-3"
|
||||
i18n
|
||||
>Secures group metadata and unlocks advanced authentication features.</span
|
||||
>
|
||||
</cds-checkbox>
|
||||
|
||||
@if (!groupForm.controls.enableMtls.value) {
|
||||
<div
|
||||
cdsRow
|
||||
class="cds-mt-3"
|
||||
@if (!groupForm.controls.enableEncryption.value) {
|
||||
<cd-alert-panel
|
||||
type="warning"
|
||||
i18n
|
||||
>
|
||||
<span class="cds--type-heading-01"
|
||||
>Encryption is required if you plan to use DH-CHAP or mTLS authentication.</span
|
||||
>
|
||||
<div cdsCol>
|
||||
<cd-alert-panel
|
||||
type="warning"
|
||||
spacingClass="mb-0"
|
||||
class="cds--type-heading-01"
|
||||
i18n
|
||||
>
|
||||
Disabling mTLS leaves gateway control and monitoring communication unencrypted.
|
||||
</cd-alert-panel>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</cd-alert-panel>
|
||||
}
|
||||
|
||||
@if (groupForm.controls.enableMtls.value) {
|
||||
<div
|
||||
cdsRow
|
||||
class="form-item"
|
||||
@if (groupForm.controls.enableEncryption.value) {
|
||||
<cds-textarea-label
|
||||
labelInputID="encryptionKey"
|
||||
helperText="Optional. Provide the encryption key used for securing the gateway group."
|
||||
i18n
|
||||
i18n-helperText
|
||||
>
|
||||
<textarea
|
||||
class="w-100"
|
||||
cdsTextArea
|
||||
id="encryptionKey"
|
||||
formControlName="encryptionKey"
|
||||
rows="5"
|
||||
cdsTheme="g10"
|
||||
>
|
||||
<div cdsCol>
|
||||
<label
|
||||
class="cds--label fw-bold"
|
||||
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="internal"
|
||||
[checked]="
|
||||
groupForm.controls.certificateType.value === CertificateType.internal
|
||||
"
|
||||
(change)="onCertificateTypeChange(CertificateType.internal)"
|
||||
i18n
|
||||
>
|
||||
Internal
|
||||
</cds-radio>
|
||||
<cds-radio
|
||||
value="external"
|
||||
[checked]="
|
||||
groupForm.controls.certificateType.value === CertificateType.external
|
||||
"
|
||||
(change)="onCertificateTypeChange(CertificateType.external)"
|
||||
i18n
|
||||
>
|
||||
External
|
||||
</cds-radio>
|
||||
</cds-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</cds-tile>
|
||||
</textarea>
|
||||
</cds-textarea-label>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (
|
||||
groupForm.controls.enableMtls.value &&
|
||||
groupForm.controls.certificateType.value === CertificateType.internal
|
||||
) {
|
||||
<div
|
||||
cdsRow
|
||||
class="form-item"
|
||||
>
|
||||
<div cdsCol>
|
||||
<!-- mTLS — same cdsRow/cdsCol layout as the nodes table for horizontal alignment -->
|
||||
<div
|
||||
cdsRow
|
||||
class="form-item"
|
||||
>
|
||||
<div cdsCol>
|
||||
<label
|
||||
class="cds--type-label-01 cds-mb-3"
|
||||
for="enableMtls"
|
||||
i18n
|
||||
>Enable Mutual TLS (mTLS)</label
|
||||
>
|
||||
<cds-checkbox
|
||||
id="enableMtls"
|
||||
formControlName="enableMtls"
|
||||
>
|
||||
<span
|
||||
class="cds--type-body-01 cds-mb-3"
|
||||
i18n
|
||||
>Use mutual TLS (mTLS) to encrypt control and monitoring commands exchanged with
|
||||
NVMe-oF gateways.</span
|
||||
>
|
||||
</cds-checkbox>
|
||||
|
||||
@if (!groupForm.controls.enableMtls.value) {
|
||||
<cd-alert-panel
|
||||
type="info"
|
||||
spacingClass="mb-3"
|
||||
type="warning"
|
||||
class="cds--type-heading-01"
|
||||
i18n
|
||||
>
|
||||
Certificate will be generated automatically by Cephadm CA for internal certificate
|
||||
type.
|
||||
Disabling mTLS leaves gateway control and monitoring communication unencrypted.
|
||||
</cd-alert-panel>
|
||||
<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
|
||||
}
|
||||
|
||||
@if (groupForm.controls.enableMtls.value) {
|
||||
<cd-certificate-authority-form
|
||||
[formGroup]="groupForm"
|
||||
[editing]="editing"
|
||||
[currentCertificate]="currentCertificate"
|
||||
[showCertSourceChangeWarning]="showCertSourceChangeWarning"
|
||||
(certificateTypeChange)="onCertificateTypeChange($event)"
|
||||
>
|
||||
</cd-text-label-list>
|
||||
</div>
|
||||
</cd-certificate-authority-form>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (
|
||||
groupForm.controls.enableMtls.value &&
|
||||
@ -351,9 +251,9 @@
|
||||
cdsTextArea
|
||||
id="rootCACertText"
|
||||
formControlName="rootCACert"
|
||||
class="w-100"
|
||||
placeholder="Paste certificate or private key PEM content"
|
||||
i18n-placeholder
|
||||
cols="100"
|
||||
rows="4"
|
||||
>
|
||||
</textarea>
|
||||
@ -407,9 +307,9 @@
|
||||
cdsTextArea
|
||||
id="clientCertText"
|
||||
formControlName="clientCert"
|
||||
class="w-100"
|
||||
placeholder="Paste certificate or private key PEM content"
|
||||
i18n-placeholder
|
||||
cols="100"
|
||||
rows="4"
|
||||
>
|
||||
</textarea>
|
||||
@ -463,9 +363,9 @@
|
||||
cdsTextArea
|
||||
id="clientKeyText"
|
||||
formControlName="clientKey"
|
||||
class="w-100"
|
||||
placeholder="Paste certificate or private key PEM content"
|
||||
i18n-placeholder
|
||||
cols="100"
|
||||
rows="4"
|
||||
>
|
||||
</textarea>
|
||||
@ -519,9 +419,9 @@
|
||||
cdsTextArea
|
||||
id="serverCertText"
|
||||
formControlName="serverCert"
|
||||
class="w-100"
|
||||
placeholder="Paste certificate or private key PEM content"
|
||||
i18n-placeholder
|
||||
cols="100"
|
||||
rows="4"
|
||||
>
|
||||
</textarea>
|
||||
@ -575,9 +475,9 @@
|
||||
cdsTextArea
|
||||
id="serverKeyText"
|
||||
formControlName="serverKey"
|
||||
class="w-100"
|
||||
placeholder="Paste certificate or private key PEM content"
|
||||
i18n-placeholder
|
||||
cols="100"
|
||||
rows="4"
|
||||
>
|
||||
</textarea>
|
||||
|
||||
@ -2,15 +2,17 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { PaginateObservable } from '~/app/shared/api/paginate.model';
|
||||
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
|
||||
import { SharedModule } from '~/app/shared/shared.module';
|
||||
import { CertificateType } from '~/app/shared/models/service.interface';
|
||||
|
||||
import { NvmeofGroupFormComponent } from './nvmeof-group-form.component';
|
||||
import { CheckboxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular';
|
||||
@ -32,7 +34,13 @@ describe('NvmeofGroupFormComponent', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [NvmeofGroupFormComponent],
|
||||
providers: [NgbActiveModal],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { params: of({}) }
|
||||
}
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgbTypeaheadModule,
|
||||
@ -132,7 +140,7 @@ describe('NvmeofGroupFormComponent', () => {
|
||||
|
||||
expect(cephServiceService.create).toHaveBeenCalledWith({
|
||||
service_type: 'nvmeof',
|
||||
service_id: 'nvmeof.default',
|
||||
service_id: 'default',
|
||||
group: 'default',
|
||||
placement: {
|
||||
hosts: ['host1', 'host2']
|
||||
@ -196,7 +204,7 @@ describe('NvmeofGroupFormComponent', () => {
|
||||
expect(cephServiceService.create).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
service_type: 'nvmeof',
|
||||
service_id: 'nvmeof.mtls-internal',
|
||||
service_id: 'mtls-internal',
|
||||
ssl: true,
|
||||
enable_auth: true,
|
||||
certificate_source: 'cephadm-signed',
|
||||
@ -226,7 +234,7 @@ describe('NvmeofGroupFormComponent', () => {
|
||||
|
||||
expect(cephServiceService.create).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
service_id: 'nvmeof.mtls-external',
|
||||
service_id: 'mtls-external',
|
||||
ssl: true,
|
||||
enable_auth: true,
|
||||
certificate_source: 'inline',
|
||||
@ -251,4 +259,151 @@ describe('NvmeofGroupFormComponent', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/block/nvmeof/gateways');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edit mode', () => {
|
||||
const mockCertificate = {
|
||||
cert_name: 'nvmeof.Test1',
|
||||
scope: 'service',
|
||||
requires_certificate: true,
|
||||
status: 'valid',
|
||||
days_to_expiration: 90,
|
||||
signed_by: 'cephadm',
|
||||
has_certificate: true,
|
||||
certificate_source: 'cephadm-signed',
|
||||
expiry_date: '2026-12-01T00:00:00Z',
|
||||
issuer: 'Cephadm',
|
||||
common_name: 'nvmeof.Test1'
|
||||
};
|
||||
|
||||
const mockService = {
|
||||
service_name: 'nvmeof.Test1',
|
||||
service_type: 'nvmeof',
|
||||
service_id: 'Test1',
|
||||
unmanaged: false,
|
||||
certificate: mockCertificate,
|
||||
placement: { hosts: ['ceph-node-01', 'ceph-node-02'] },
|
||||
spec: {
|
||||
group: 'Test1',
|
||||
encryption_key: 'existing-encryption-key',
|
||||
enable_auth: true,
|
||||
ssl: true,
|
||||
certificate_source: 'cephadm-signed',
|
||||
custom_sans: ['gw1.local']
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(nvmeofService, 'listGatewayGroups').and.returnValue(of([[mockService]]));
|
||||
spyOn(cephServiceService, 'list').and.returnValue(
|
||||
new PaginateObservable<any>(of([mockService]))
|
||||
);
|
||||
|
||||
component.editing = true;
|
||||
component.gatewayGroupName = 'Test1';
|
||||
component.action = 'Edit';
|
||||
component.createForm();
|
||||
component.loadGatewayGroupData('Test1');
|
||||
form = component.groupForm;
|
||||
});
|
||||
|
||||
it('should resolve group by spec.group and preselect hosts', () => {
|
||||
expect(nvmeofService.listGatewayGroups).toHaveBeenCalled();
|
||||
expect(cephServiceService.list).toHaveBeenCalled();
|
||||
expect(component.preSelectedHostnames).toEqual(['ceph-node-01', 'ceph-node-02']);
|
||||
expect(form.controls.groupName.value).toBe('Test1');
|
||||
});
|
||||
|
||||
it('should prepopulate encryption checkbox and key when encryption_key is present', () => {
|
||||
expect(form.controls.enableEncryption.value).toBe(true);
|
||||
expect(form.controls.encryptionKey.value).toBe('existing-encryption-key');
|
||||
});
|
||||
|
||||
it('should prepopulate mTLS from enable_auth and load current certificate', () => {
|
||||
expect(form.controls.enableMtls.value).toBe(true);
|
||||
expect(form.controls.certificateType.value).toBe(CertificateType.internal);
|
||||
expect(form.controls.custom_sans.value).toEqual(['gw1.local']);
|
||||
expect(component.currentCertificate).toEqual(mockCertificate);
|
||||
expect(component.currentSpecCertificateSource).toBe('cephadm-signed');
|
||||
});
|
||||
|
||||
it('should prepopulate external cert fields when certificate_source is inline', () => {
|
||||
const inlineService = {
|
||||
...mockService,
|
||||
certificate: {
|
||||
...mockCertificate,
|
||||
signed_by: 'external',
|
||||
certificate_source: 'inline'
|
||||
},
|
||||
spec: {
|
||||
...mockService.spec,
|
||||
certificate_source: 'inline',
|
||||
root_ca_cert: 'root-pem',
|
||||
client_cert: 'client-pem',
|
||||
client_key: 'client-key-pem',
|
||||
server_cert: 'server-pem',
|
||||
server_key: 'server-key-pem'
|
||||
}
|
||||
};
|
||||
(nvmeofService.listGatewayGroups as jasmine.Spy).and.returnValue(of([[inlineService]]));
|
||||
(cephServiceService.list as jasmine.Spy).and.returnValue(
|
||||
new PaginateObservable<any>(of([inlineService]))
|
||||
);
|
||||
|
||||
component.createForm();
|
||||
component.loadGatewayGroupData('Test1');
|
||||
|
||||
expect(component.groupForm.controls.certificateType.value).toBe(CertificateType.external);
|
||||
expect(component.groupForm.controls.rootCACert.value).toBe('root-pem');
|
||||
expect(component.groupForm.controls.clientCert.value).toBe('client-pem');
|
||||
expect(component.groupForm.controls.clientKey.value).toBe('client-key-pem');
|
||||
expect(component.groupForm.controls.serverCert.value).toBe('server-pem');
|
||||
expect(component.groupForm.controls.serverKey.value).toBe('server-key-pem');
|
||||
});
|
||||
|
||||
it('should show cert source change warning when switching type on edit', () => {
|
||||
component.onCertificateTypeChange(CertificateType.external);
|
||||
expect(component.showCertSourceChangeWarning).toBe(true);
|
||||
|
||||
component.onCertificateTypeChange(CertificateType.internal);
|
||||
expect(component.showCertSourceChangeWarning).toBe(false);
|
||||
});
|
||||
|
||||
it('should keep form valid when encryption key is cleared or encryption is unchecked', () => {
|
||||
form.controls.encryptionKey.setValue('');
|
||||
expect(form.valid).toBe(true);
|
||||
expect(component.isCreateDisabled).toBe(false);
|
||||
|
||||
form.controls.enableEncryption.setValue(false);
|
||||
expect(form.controls.encryptionKey.value).toBeNull();
|
||||
expect(form.valid).toBe(true);
|
||||
expect(component.isCreateDisabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should update service on submit in edit mode using existing service_id', () => {
|
||||
spyOn(cephServiceService, 'update').and.returnValue(of({}));
|
||||
spyOn(taskWrapperService, 'wrapTaskAroundCall').and.callFake(({ call }) => call);
|
||||
spyOn(router, 'navigateByUrl');
|
||||
|
||||
component.gatewayNodeComponent = {
|
||||
getSelectedHosts: (): any[] => [{ hostname: 'ceph-node-01' }],
|
||||
getSelectedHostnames: (): string[] => ['ceph-node-01']
|
||||
} as any;
|
||||
|
||||
component.onSubmit();
|
||||
|
||||
expect(cephServiceService.update).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
service_type: 'nvmeof',
|
||||
service_id: 'Test1',
|
||||
group: 'Test1',
|
||||
placement: { hosts: ['ceph-node-01'] },
|
||||
encryption_key: 'existing-encryption-key',
|
||||
ssl: true,
|
||||
enable_auth: true,
|
||||
certificate_source: 'cephadm-signed'
|
||||
})
|
||||
);
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/block/nvmeof/gateways');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { HttpParams } from '@angular/common/http';
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { UntypedFormControl, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { of } from 'rxjs';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
|
||||
import { CdForm } from '~/app/shared/forms/cd-form';
|
||||
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
|
||||
@ -13,7 +17,11 @@ import { CephServiceService } from '~/app/shared/api/ceph-service.service';
|
||||
import { FinishedTask } from '~/app/shared/models/finished-task';
|
||||
import { CdValidators } from '~/app/shared/forms/cd-validators';
|
||||
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
|
||||
import { CertificateType } from '~/app/shared/models/service.interface';
|
||||
import {
|
||||
CephServiceCertificate,
|
||||
CephServiceSpec,
|
||||
CertificateType
|
||||
} from '~/app/shared/models/service.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'cd-nvmeof-group-form',
|
||||
@ -34,7 +42,11 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
|
||||
hasAvailableNodes = true;
|
||||
editing = false;
|
||||
gatewayGroupName = '';
|
||||
existingServiceData: any = null;
|
||||
existingServiceData: CephServiceSpec | null = null;
|
||||
preSelectedHostnames: string[] = [];
|
||||
currentCertificate: CephServiceCertificate = null;
|
||||
currentSpecCertificateSource = '';
|
||||
showCertSourceChangeWarning = false;
|
||||
|
||||
constructor(
|
||||
private authStorageService: AuthStorageService,
|
||||
@ -97,11 +109,18 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
|
||||
});
|
||||
|
||||
this.groupForm.get('enableEncryption')?.valueChanges.subscribe((enabled) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const encryptionConfigControl = this.groupForm.get('encryptionConfig');
|
||||
const encryptionKeyControl = this.groupForm.get('encryptionKey');
|
||||
|
||||
if (!enabled) {
|
||||
// Encryption is optional — clearing the toggle must not leave the form invalid
|
||||
encryptionKeyControl?.setValue(null, { emitEvent: false });
|
||||
encryptionConfigControl?.setValue(null, { emitEvent: false });
|
||||
encryptionKeyControl?.setErrors(null);
|
||||
encryptionConfigControl?.setErrors(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!encryptionKeyControl?.value && encryptionConfigControl?.value) {
|
||||
encryptionKeyControl.setValue(encryptionConfigControl.value, { emitEvent: false });
|
||||
}
|
||||
@ -112,43 +131,90 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
|
||||
loadGatewayGroupData(groupName: string) {
|
||||
this.nvmeofService.listGatewayGroups().subscribe(
|
||||
(gatewayGroups: any) => {
|
||||
const groups = gatewayGroups?.[0] ?? [];
|
||||
const group = groups.find((g: any) => g.spec?.group === groupName);
|
||||
|
||||
if (group) {
|
||||
this.existingServiceData = group;
|
||||
const spec = group.spec || {};
|
||||
|
||||
this.groupForm.patchValue({
|
||||
groupName: groupName,
|
||||
unmanaged: spec.unmanaged || false,
|
||||
enableEncryption: spec.ssl || false,
|
||||
enableMtls: spec.ssl || false,
|
||||
certificateType:
|
||||
spec.certificate_source === 'inline'
|
||||
? CertificateType.external
|
||||
: CertificateType.internal,
|
||||
encryptionKey: spec.encryption_key || '',
|
||||
custom_sans: spec.custom_sans || []
|
||||
});
|
||||
|
||||
if (spec.certificate_source === 'inline') {
|
||||
this.groupForm.patchValue({
|
||||
rootCACert: spec.root_ca_cert || null,
|
||||
clientCert: spec.client_cert || null,
|
||||
clientKey: spec.client_key || null,
|
||||
serverCert: spec.server_cert || null,
|
||||
serverKey: spec.server_key || null
|
||||
});
|
||||
// Resolve by spec.group — service_name may be nvmeof.<group> or nvmeof.<pool>.<group>
|
||||
this.nvmeofService
|
||||
.listGatewayGroups()
|
||||
.pipe(
|
||||
switchMap((gatewayGroups: CephServiceSpec[][]) => {
|
||||
const groups = gatewayGroups?.[0] ?? [];
|
||||
const group = groups.find((g: CephServiceSpec) => g.spec?.group === groupName);
|
||||
if (!group) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
const serviceName = group.service_name;
|
||||
if (!serviceName) {
|
||||
return of(group);
|
||||
}
|
||||
|
||||
// Enrich with certificate metadata (same as service edit form)
|
||||
return this.cephServiceService
|
||||
.list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), serviceName)
|
||||
.observable.pipe(
|
||||
catchError(() => of(group)),
|
||||
switchMap((response: CephServiceSpec[] | CephServiceSpec) => {
|
||||
if (Array.isArray(response) && response[0]) {
|
||||
return of({
|
||||
...group,
|
||||
...response[0],
|
||||
// Prefer placement/hosts from gateway-group listing
|
||||
placement: group.placement || response[0].placement,
|
||||
// Prefer enriched service spec (certs / encryption_key)
|
||||
spec: { ...group.spec, ...response[0].spec }
|
||||
} as CephServiceSpec);
|
||||
}
|
||||
return of(group);
|
||||
})
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe((group: CephServiceSpec | null) => {
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
(_error) => {
|
||||
// Error loading gateway group data
|
||||
}
|
||||
);
|
||||
this.populateFormFromService(group, groupName);
|
||||
});
|
||||
}
|
||||
|
||||
private populateFormFromService(group: CephServiceSpec, groupName: string) {
|
||||
this.existingServiceData = group;
|
||||
const spec = group.spec || ({} as any);
|
||||
const encryptionKey = spec.encryption_key || '';
|
||||
const enableMtls = !!spec.enable_auth;
|
||||
|
||||
this.preSelectedHostnames = group.placement?.hosts || [];
|
||||
|
||||
if (group.certificate) {
|
||||
this.currentCertificate = group.certificate;
|
||||
}
|
||||
if (spec.certificate_source) {
|
||||
this.currentSpecCertificateSource = spec.certificate_source;
|
||||
}
|
||||
|
||||
this.groupForm.patchValue({
|
||||
groupName: groupName,
|
||||
unmanaged: group.unmanaged || false,
|
||||
enableEncryption: !!encryptionKey,
|
||||
encryptionKey: encryptionKey,
|
||||
encryptionConfig: encryptionKey,
|
||||
enableMtls: enableMtls,
|
||||
certificateType:
|
||||
enableMtls && spec.certificate_source !== 'cephadm-signed'
|
||||
? CertificateType.external
|
||||
: CertificateType.internal,
|
||||
custom_sans: spec.custom_sans || []
|
||||
});
|
||||
|
||||
// Cert PEM fields are optional on edit; prepopulate when present.
|
||||
if (enableMtls) {
|
||||
this.groupForm.patchValue({
|
||||
rootCACert: spec.root_ca_cert || null,
|
||||
clientCert: spec.client_cert || null,
|
||||
clientKey: spec.client_key || null,
|
||||
serverCert: spec.server_cert || null,
|
||||
serverKey: spec.server_key || null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onHostsLoaded(count: number): void {
|
||||
@ -156,7 +222,7 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
|
||||
get isCreateDisabled(): boolean {
|
||||
if (!this.hasAvailableNodes) {
|
||||
if (!this.hasAvailableNodes && !(this.editing && this.preSelectedHostnames.length > 0)) {
|
||||
return true;
|
||||
}
|
||||
if (!this.groupForm) {
|
||||
@ -172,16 +238,22 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
|
||||
if (errors && errors.cdSubmitButton) {
|
||||
return true;
|
||||
}
|
||||
if (this.gatewayNodeComponent) {
|
||||
const selected = this.gatewayNodeComponent.getSelectedHostnames?.() || [];
|
||||
if (selected.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (this.getSelectedOrPreselectedHosts().length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private getSelectedOrPreselectedHosts(): string[] {
|
||||
const selected = this.gatewayNodeComponent?.getSelectedHostnames?.() || [];
|
||||
if (selected.length > 0) {
|
||||
return selected;
|
||||
}
|
||||
// Edit: fall back to loaded placement while table selection syncs
|
||||
return this.editing ? this.preSelectedHostnames : [];
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
if (this.groupForm.invalid) {
|
||||
return;
|
||||
@ -193,19 +265,24 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
|
||||
const formValues = this.groupForm.getRawValue();
|
||||
const selectedHostnames = this.gatewayNodeComponent?.getSelectedHostnames() || [];
|
||||
const selectedHostnames = this.getSelectedOrPreselectedHosts();
|
||||
if (selectedHostnames.length === 0) {
|
||||
this.groupForm.setErrors({ cdSubmitButton: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const groupName = this.editing ? this.gatewayGroupName : formValues.groupName;
|
||||
const serviceName = `nvmeof.${groupName}`;
|
||||
// Match service-form: service_id is the group name (not nvmeof.<group>)
|
||||
const serviceId = this.editing ? this.existingServiceData?.service_id || groupName : groupName;
|
||||
const serviceName =
|
||||
this.editing && this.existingServiceData?.service_name
|
||||
? this.existingServiceData.service_name
|
||||
: `nvmeof.${serviceId}`;
|
||||
const taskUrl = this.editing ? `service/${URLVerbs.EDIT}` : `service/${URLVerbs.CREATE}`;
|
||||
|
||||
const serviceSpec: Record<string, any> = {
|
||||
service_type: 'nvmeof',
|
||||
service_id: serviceName,
|
||||
service_id: serviceId,
|
||||
group: groupName,
|
||||
placement: {
|
||||
hosts: selectedHostnames
|
||||
@ -234,11 +311,21 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
|
||||
}
|
||||
|
||||
if (formValues.certificateType === CertificateType.external) {
|
||||
serviceSpec['root_ca_cert'] = formValues.rootCACert;
|
||||
serviceSpec['client_cert'] = formValues.clientCert;
|
||||
serviceSpec['client_key'] = formValues.clientKey;
|
||||
serviceSpec['server_cert'] = formValues.serverCert;
|
||||
serviceSpec['server_key'] = formValues.serverKey;
|
||||
if (formValues.rootCACert) {
|
||||
serviceSpec['root_ca_cert'] = formValues.rootCACert;
|
||||
}
|
||||
if (formValues.clientCert) {
|
||||
serviceSpec['client_cert'] = formValues.clientCert;
|
||||
}
|
||||
if (formValues.clientKey) {
|
||||
serviceSpec['client_key'] = formValues.clientKey;
|
||||
}
|
||||
if (formValues.serverCert) {
|
||||
serviceSpec['server_cert'] = formValues.serverCert;
|
||||
}
|
||||
if (formValues.serverKey) {
|
||||
serviceSpec['server_key'] = formValues.serverKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -282,6 +369,15 @@ export class NvmeofGroupFormComponent extends CdForm implements OnInit {
|
||||
|
||||
onCertificateTypeChange(type: CertificateType): void {
|
||||
this.groupForm.get('certificateType')?.setValue(type);
|
||||
|
||||
if (this.editing && this.currentCertificate?.has_certificate) {
|
||||
const originalSource =
|
||||
this.currentSpecCertificateSource === 'cephadm-signed'
|
||||
? CertificateType.internal
|
||||
: CertificateType.external;
|
||||
this.showCertSourceChangeWarning = type !== originalSource;
|
||||
}
|
||||
|
||||
if (type === CertificateType.internal) {
|
||||
this.groupForm.patchValue({
|
||||
rootCACert: null,
|
||||
|
||||
@ -1,141 +1,118 @@
|
||||
<div [formGroup]="formGroup">
|
||||
<div
|
||||
[formGroup]="formGroup"
|
||||
[cdsStack]="'vertical'"
|
||||
[gap]="5"
|
||||
>
|
||||
@if (editing && currentCertificate?.has_certificate) {
|
||||
<div class="form-item">
|
||||
<div
|
||||
class="cds-mt-5"
|
||||
[cdsStack]="'vertical'"
|
||||
[gap]="3"
|
||||
>
|
||||
<span
|
||||
class="cds--label cds--type-heading-compact-01"
|
||||
class="cds--type-heading-compact-01"
|
||||
i18n
|
||||
>Current Certificate</span
|
||||
>
|
||||
<div
|
||||
cdsRow
|
||||
[condensed]="true"
|
||||
[cdsStack]="'horizontal'"
|
||||
[gap]="5"
|
||||
>
|
||||
<div
|
||||
cdsCol
|
||||
[columnNumbers]="[
|
||||
['sm', 4],
|
||||
['md', 4]
|
||||
]"
|
||||
[cdsStack]="'vertical'"
|
||||
[gap]="1"
|
||||
>
|
||||
<div
|
||||
cdsStack="vertical"
|
||||
[gap]="1"
|
||||
<span
|
||||
class="cds--type-label-01"
|
||||
i18n
|
||||
>Certificate</span
|
||||
>
|
||||
<span
|
||||
class="cds--type-body-compact-01"
|
||||
[title]="currentCertificate.cert_name"
|
||||
>{{ currentCertificate.cert_name }}</span
|
||||
>
|
||||
<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]
|
||||
]"
|
||||
[cdsStack]="'vertical'"
|
||||
[gap]="1"
|
||||
>
|
||||
<div
|
||||
cdsStack="vertical"
|
||||
[gap]="1"
|
||||
<span
|
||||
class="cds--type-label-01"
|
||||
i18n
|
||||
>Valid Until</span
|
||||
>
|
||||
<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>
|
||||
<span class="cds--type-body-compact-01">
|
||||
{{ currentCertificate.expiry_date | cdDate }} •
|
||||
{{ currentCertificate.days_to_expiration }}
|
||||
<span i18n>days left</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
cdsRow
|
||||
[condensed]="true"
|
||||
class="cds-mt-3"
|
||||
>
|
||||
<div
|
||||
cdsCol
|
||||
[columnNumbers]="[
|
||||
['sm', 4],
|
||||
['md', 4]
|
||||
]"
|
||||
[cdsStack]="'vertical'"
|
||||
[gap]="1"
|
||||
>
|
||||
<div
|
||||
cdsStack="vertical"
|
||||
[gap]="1"
|
||||
<span
|
||||
class="cds--type-label-01"
|
||||
i18n
|
||||
>Status</span
|
||||
>
|
||||
<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
|
||||
[cdsStack]="'horizontal'"
|
||||
[gap]="2"
|
||||
>
|
||||
<cd-icon
|
||||
[type]="statusIconMap[currentCertificate.status] || statusIconMap['default']"
|
||||
></cd-icon>
|
||||
@switch (currentCertificate.status) {
|
||||
@case ('valid') {
|
||||
<span i18n>Valid</span>
|
||||
}
|
||||
</div>
|
||||
@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
|
||||
cdsCol
|
||||
[columnNumbers]="[
|
||||
['sm', 4],
|
||||
['md', 4]
|
||||
]"
|
||||
[cdsStack]="'vertical'"
|
||||
[gap]="1"
|
||||
>
|
||||
<div
|
||||
cdsStack="vertical"
|
||||
[gap]="1"
|
||||
<span
|
||||
class="cds--type-label-01"
|
||||
i18n
|
||||
>Issuer</span
|
||||
>
|
||||
<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>
|
||||
<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 class="form-item">
|
||||
<div
|
||||
[cdsStack]="'vertical'"
|
||||
[gap]="2"
|
||||
>
|
||||
<label
|
||||
class="cds--label cds--type-heading-compact-01"
|
||||
class="cds--type-heading-compact-01 cds-mb-3"
|
||||
for="certificateType"
|
||||
i18n
|
||||
>Choose Certificate Authority</label
|
||||
@ -167,7 +144,7 @@
|
||||
@if (showCertSourceChangeWarning) {
|
||||
<cd-alert-panel
|
||||
type="warning"
|
||||
spacingClass="mb-3"
|
||||
spacingClass="mb-0"
|
||||
i18n
|
||||
>
|
||||
Changing the certificate source will redeploy the service daemons to apply the new certificate
|
||||
@ -178,37 +155,33 @@
|
||||
@if (formGroup.controls.certificateType.value === CertificateType.internal) {
|
||||
<cd-alert-panel
|
||||
type="info"
|
||||
spacingClass="mb-3"
|
||||
spacingClass="mb-0"
|
||||
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>
|
||||
<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>
|
||||
@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>
|
||||
<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>
|
||||
|
||||
@ -4,6 +4,12 @@
|
||||
i18n-helperText
|
||||
i18n
|
||||
>
|
||||
@if ($index === 0) {
|
||||
{{ label }}
|
||||
} @else {
|
||||
<!-- Invisible character keeps subsequent inputs aligned -->
|
||||
​
|
||||
}
|
||||
<div class="cds-input-group">
|
||||
<input
|
||||
cdsText
|
||||
@ -22,6 +28,5 @@
|
||||
</cds-icon-button>
|
||||
}
|
||||
</div>
|
||||
{{ $index === 0 ? label : '' }}
|
||||
</cds-text-label>
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user