Skip to content

Commit e424ea7

Browse files
committed
Resolves: MTV-3738 | Provider create form - VMware vSphere
Signed-off-by: Jeff Puzzo <[email protected]>
1 parent 83501cb commit e424ea7

20 files changed

+1401
-334
lines changed

locales/en/plugin__forklift-console-plugin.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@
238238
"Condition": "Condition",
239239
"Conditions": "Conditions",
240240
"Conditions not found": "Conditions not found",
241+
"Configure a VDDK image for a more efficient migration by enabling direct, block-level access to a VM's disk data.": "Configure a VDDK image for a more efficient migration by enabling direct, block-level access to a VM's disk data.",
241242
"Configure certificate validation": "Configure certificate validation",
242243
"Configure disk decryption settings including passphrases for LUKS-encrypted devices or network-bound disk encryption (NBDE/Clevis) for the VMs you want to migrate.": "Configure disk decryption settings including passphrases for LUKS-encrypted devices or network-bound disk encryption (NBDE/Clevis) for the VMs you want to migrate.",
243244
"Confirm selections": "Confirm selections",
@@ -437,6 +438,7 @@
437438
"For applications that need to be available 24/7, warm migration is the preferred choice to ensure business continuity.": "For applications that need to be available 24/7, warm migration is the preferred choice to ensure business continuity.",
438439
"For each LUKS-encrypted device, migration toolkit for virtualization (MTV) tries each passphrase until one unlocks the device.": "For each LUKS-encrypted device, migration toolkit for virtualization (MTV) tries each passphrase until one unlocks the device.",
439440
"For example: 10.10.0.10:/ova": "For example: 10.10.0.10:/ova",
441+
"For example: https://host-example.com/sdk.": "For example: https://host-example.com/sdk.",
440442
"For VMs used for development, testing, or other non-essential tasks, the downtime is unlikely to have a major business impact.": "For VMs used for development, testing, or other non-essential tasks, the downtime is unlikely to have a major business impact.",
441443
"Forklift controller logs": "Forklift controller logs",
442444
"Forklift controller logs capture migration toolkit for virtualization (MTV) related events.": "Forklift controller logs capture migration toolkit for virtualization (MTV) related events.",
@@ -565,6 +567,7 @@
565567
"Manage columns": "Manage columns",
566568
"Manage the SSL/TLS certificate used to secure the connection to the provider. You can upload a custom CA certificate or skip this step.": "Manage the SSL/TLS certificate used to secure the connection to the provider. You can upload a custom CA certificate or skip this step.",
567569
"Managed resource": "Managed resource",
570+
"Manually specify the VDDK image URL": "Manually specify the VDDK image URL",
568571
"Map": "Map",
569572
"Mappings": "Mappings",
570573
"Maximum concurrent VM migrations": "Maximum concurrent VM migrations",
@@ -750,6 +753,8 @@
750753
"Page table": "Page table",
751754
"Passphrases for LUKS encrypted devices": "Passphrases for LUKS encrypted devices",
752755
"Password": "Password",
756+
"Password for connecting to the vSphere API endpoint.": "Password for connecting to the vSphere API endpoint.",
757+
"Password input": "Password input",
753758
"Password is required": "Password is required",
754759
"Path": "Path",
755760
"paused": "paused",
@@ -946,6 +951,7 @@
946951
"Skip certificate validation": "Skip certificate validation",
947952
"Skip guest conversion": "Skip guest conversion",
948953
"Skip to review": "Skip to review",
954+
"Skip VDDK setup (not recommended)": "Skip VDDK setup (not recommended)",
949955
"Skip VMware Virtual Disk Development Kit (VDDK) SDK acceleration (not recommended).": "Skip VMware Virtual Disk Development Kit (VDDK) SDK acceleration (not recommended).",
950956
"Small precopy interval": "Small precopy interval",
951957
"Snapshot polling interval": "Snapshot polling interval",
@@ -1107,11 +1113,13 @@
11071113
"Upload": "Upload",
11081114
"Upload a CA certificate to be trusted when connecting to Openshift API endpoint, or leave empty to use the system CA certificate.": "Upload a CA certificate to be trusted when connecting to Openshift API endpoint, or leave empty to use the system CA certificate.",
11091115
"Upload a VDDK archive and build a VDDK init image from it.": "Upload a VDDK archive and build a VDDK init image from it.",
1116+
"Upload a VDDK archive to generate the image URL": "Upload a VDDK archive to generate the image URL",
11101117
"Upload an OVA file which will be added to provider's virtual machines. The uploaded file name extension should be .ova": "Upload an OVA file which will be added to provider's virtual machines. The uploaded file name extension should be .ova",
11111118
"Upload local OVA file": "Upload local OVA file",
11121119
"Upload local OVA files": "Upload local OVA files",
11131120
"Uploading...": "Uploading...",
11141121
"URL": "URL",
1122+
"URL is required": "URL is required",
11151123
"URL of the API endpoint of the Red Hat Virtualization Manager (RHVM) on which the source VM is mounted. Ensure that the URL includes the path leading to the RHVM API server, usually /ovirt-engine/api. For example, https://rhv-host-example.com/ovirt-engine/api.": "URL of the API endpoint of the Red Hat Virtualization Manager (RHVM) on which the source VM is mounted. Ensure that the URL includes the path leading to the RHVM API server, usually /ovirt-engine/api. For example, https://rhv-host-example.com/ovirt-engine/api.",
11161124
"URL of the API endpoint of the vCenter on which the source VM is mounted. Ensure that the URL includes the sdk path, usually <1>/sdk</1>.<3></3><4></4>For example: <6>https://vCenter-host-example.com/sdk</6>.<8></8><9></9>If a certificate for FQDN is specified, the value of this field needs to match the FQDN in the certificate.": "URL of the API endpoint of the vCenter on which the source VM is mounted. Ensure that the URL includes the sdk path, usually <1>/sdk</1>.<3></3><4></4>For example: <6>https://vCenter-host-example.com/sdk</6>.<8></8><9></9>If a certificate for FQDN is specified, the value of this field needs to match the FQDN in the certificate.",
11171125
"URL of the NFS file share that serves the OVA., for example, 10.10.0.10:/ova": "URL of the NFS file share that serves the OVA., for example, 10.10.0.10:/ova",
@@ -1145,6 +1153,7 @@
11451153
"User password for connecting to the Red Hat Virtualization Manager (RHVM) API endpoint": "User password for connecting to the Red Hat Virtualization Manager (RHVM) API endpoint",
11461154
"Username": "Username",
11471155
"Username for connecting to OpenStack Identity (Keystone)": "Username for connecting to OpenStack Identity (Keystone)",
1156+
"Username for connecting to the vSphere API endpoint.": "Username for connecting to the vSphere API endpoint.",
11481157
"Username is required": "Username is required",
11491158
"Username is required. The username usually includes @ character, for example: name@internal": "Username is required. The username usually includes @ character, for example: name@internal",
11501159
"Validation Failed": "Validation Failed",
@@ -1171,6 +1180,7 @@
11711180
"View storage map details": "View storage map details",
11721181
"Virtual appliance used by virtualization applications. Only supports OVA files created by VMware vSphere.": "Virtual appliance used by virtualization applications. Only supports OVA files created by VMware vSphere.",
11731182
"Virtual Disk Development Kit (VDDK) container init image path. The path must be empty or a valid container image path in the format of <2>registry_route_or_server_path/vddk:&#8249;tag&#8250;</2>.<4></4><5></5>To accelerate migration and reduce the risk of a plan failing, it is strongly recommended to specify a VDDK init image.": "Virtual Disk Development Kit (VDDK) container init image path. The path must be empty or a valid container image path in the format of <2>registry_route_or_server_path/vddk:&#8249;tag&#8250;</2>.<4></4><5></5>To accelerate migration and reduce the risk of a plan failing, it is strongly recommended to specify a VDDK init image.",
1183+
"Virtual Disk Development Kit (VDDK) setup": "Virtual Disk Development Kit (VDDK) setup",
11741184
"Virtual machine": "Virtual machine",
11751185
"Virtual machines": "Virtual machines",
11761186
"Virtualization platform from Red Hat. Currently in maintenance for existing customers only.": "Virtualization platform from Red Hat. Currently in maintenance for existing customers only.",
@@ -1190,13 +1200,15 @@
11901200
"VMs included in live migrations migrate without downtime.": "VMs included in live migrations migrate without downtime.",
11911201
"VMs included in warm migrations migrate with minimal downtime.": "VMs included in warm migrations migrate with minimal downtime.",
11921202
"VMware only: vSphere product name.": "VMware only: vSphere product name.",
1203+
"VMware Virtual Disk Development Kit (VDDK) image.": "VMware Virtual Disk Development Kit (VDDK) image.",
11931204
"VMware vSphere": "VMware vSphere",
11941205
"VMware vSphere UI": "VMware vSphere UI",
11951206
"VMware: A popular method is to use a Storage vMotion. This migration process automatically renames the VM's files and folder on the datastore to match the new name you have given it in the vSphere Client. You can also manually remove the VM from inventory, rename the files and folders, edit the .vmx file to update the references, and then re-add the VM to the inventory.": "VMware: A popular method is to use a Storage vMotion. This migration process automatically renames the VM's files and folder on the datastore to match the new name you have given it in the vSphere Client. You can also manually remove the VM from inventory, rename the files and folders, edit the .vmx file to update the references, and then re-add the VM to the inventory.",
11961207
"Volume name template": "Volume name template",
11971208
"Volume name template is a template for generating volume interface names in the target virtual machine.": "Volume name template is a template for generating volume interface names in the target virtual machine.",
11981209
"Volume types": "Volume types",
11991210
"Volumes": "Volumes",
1211+
"vSphere endpoint": "vSphere endpoint",
12001212
"vSphere product name.": "vSphere product name.",
12011213
"vSphere XCOPY": "vSphere XCOPY",
12021214
"Waiting": "Waiting",

src/modules/Providers/utils/validators/provider/vsphere/validateVCenterURL.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const validateVCenterURL = (
2020
// For a newly opened form where the field is not set yet, set the validation type to default.
2121
if (url === undefined) {
2222
return {
23-
msg: 'The URL is required, URL of the vCenter API endpoint for example: https://host-example.com/sdk .',
23+
msg: 'The URL is required, URL of the vCenter API endpoint for example: https://host-example.com/sdk.',
2424
type: ValidationState.Default,
2525
};
2626
}
@@ -38,14 +38,14 @@ export const validateVCenterURL = (
3838

3939
if (trimmedUrl === '') {
4040
return {
41-
msg: 'The URL is required, URL of the vCenter API endpoint for example: https://host-example.com/sdk .',
41+
msg: 'The URL is required, URL of the vCenter API endpoint for example: https://host-example.com/sdk.',
4242
type: ValidationState.Error,
4343
};
4444
}
4545

4646
if (!isValidURL) {
4747
return {
48-
msg: 'The URL is invalid. URL should include the schema and path, for example: https://host-example.com/sdk .',
48+
msg: 'The URL is invalid. URL should include the schema and path, for example: https://host-example.com/sdk.',
4949
type: ValidationState.Error,
5050
};
5151
}
@@ -63,12 +63,12 @@ export const validateVCenterURL = (
6363

6464
if (!trimmedUrl.endsWith('sdk'))
6565
return {
66-
msg: 'The URL does not end with a /sdk path, for example a URL with sdk path: https://host-example.com/sdk .',
66+
msg: 'The URL does not end with a /sdk path, for example a URL with sdk path: https://host-example.com/sdk.',
6767
type: ValidationState.Warning,
6868
};
6969

7070
return {
71-
msg: 'The URL of the vCenter API endpoint for example: https://host-example.com/sdk .',
71+
msg: 'The URL of the vCenter API endpoint for example: https://host-example.com/sdk.',
7272
type: ValidationState.Success,
7373
};
7474
};

src/providers/create-new/ProviderTypeFields.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,35 @@ import OvirtUrlField from './fields/ovirt/OvirtUrlField';
1717
import ProviderNameField from './fields/ProviderNameField';
1818
import ProviderProjectField from './fields/ProviderProjectField';
1919
import ProviderTypeField from './fields/ProviderTypeField';
20+
import VsphereCredentialsFields from './fields/vsphere/VsphereCredentialsFields';
21+
import VsphereEndpointTypeField from './fields/vsphere/VsphereEndpointTypeField';
22+
import VsphereUrlField from './fields/vsphere/VsphereUrlField';
23+
import VsphereVddkField from './fields/vsphere/VsphereVddkField';
2024
import type { CreateProviderFormData } from './types';
2125

2226
const ProviderTypeFields: FC = () => {
2327
const { t } = useForkliftTranslation();
2428
const { control } = useFormContext<CreateProviderFormData>();
2529

26-
const [selectedProviderType, openshiftUrl, ovirtUrl] = useWatch({
30+
const [selectedProviderType, openshiftUrl, ovirtUrl, vsphereUrl] = useWatch({
2731
control,
2832
name: [
2933
ProviderFormFieldId.ProviderType,
3034
ProviderFormFieldId.OpenshiftUrl,
3135
ProviderFormFieldId.OvirtUrl,
36+
ProviderFormFieldId.VsphereUrl,
3237
],
3338
});
3439

3540
return (
3641
<>
3742
<ProviderProjectField />
3843
<ProviderTypeField />
44+
3945
{selectedProviderType && <ProviderNameField />}
46+
4047
{selectedProviderType === PROVIDER_TYPES.ova && <NfsDirectoryField />}
48+
4149
{selectedProviderType === PROVIDER_TYPES.openshift && (
4250
<>
4351
<OpenShiftUrlField />
@@ -46,6 +54,7 @@ const ProviderTypeFields: FC = () => {
4654
<CertificateValidationField />
4755
</>
4856
)}
57+
4958
{selectedProviderType === PROVIDER_TYPES.openstack && (
5059
<>
5160
<OpenStackUrlField />
@@ -54,6 +63,7 @@ const ProviderTypeFields: FC = () => {
5463
<CertificateValidationField />
5564
</>
5665
)}
66+
5767
{selectedProviderType === PROVIDER_TYPES.ovirt && (
5868
<>
5969
<OvirtUrlField />
@@ -62,6 +72,17 @@ const ProviderTypeFields: FC = () => {
6272
<CertificateValidationField />
6373
</>
6474
)}
75+
76+
{selectedProviderType === PROVIDER_TYPES.vsphere && (
77+
<>
78+
<VsphereEndpointTypeField />
79+
<VsphereUrlField />
80+
<VsphereVddkField />
81+
<SectionHeading text={t('Provider credentials')} />
82+
{vsphereUrl?.trim() && <VsphereCredentialsFields />}
83+
<CertificateValidationField />
84+
</>
85+
)}
6586
</>
6687
);
6788
};
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { beforeEach, describe, expect, it } from '@jest/globals';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import { userEvent } from '@testing-library/user-event';
4+
5+
import CreateProviderForm from '../CreateProviderForm';
6+
7+
import {
8+
clearAllProviderMocks,
9+
mockCreateProvider,
10+
mockCreateProviderSecret,
11+
mockNavigate,
12+
mockPatchProviderSecretOwner,
13+
} from './test-utils';
14+
15+
jest.mock('react-router-dom-v5-compat', () => ({
16+
...jest.requireActual('react-router-dom-v5-compat'),
17+
useNavigate: () => mockNavigate,
18+
}));
19+
20+
jest.mock('@utils/analytics/hooks/useForkliftAnalytics', () => ({
21+
useForkliftAnalytics: () => ({
22+
trackEvent: jest.fn(),
23+
}),
24+
}));
25+
26+
jest.mock('src/providers/create/utils/createProvider', () => ({
27+
createProvider: (...args: unknown[]) => mockCreateProvider(...args),
28+
}));
29+
30+
jest.mock('src/providers/create/utils/createProviderSecret', () => ({
31+
createProviderSecret: (...args: unknown[]) => mockCreateProviderSecret(...args),
32+
}));
33+
34+
jest.mock('src/providers/create/utils/patchProviderSecretOwner', () => ({
35+
patchProviderSecretOwner: (...args: unknown[]) => mockPatchProviderSecretOwner(...args),
36+
}));
37+
38+
jest.mock('@utils/hooks/useWatchProjectNames', () => ({
39+
__esModule: true,
40+
default: () => [['test-namespace'], true],
41+
}));
42+
43+
jest.mock('../CreateProviderFormContextProvider', () => {
44+
const { CreateProviderFormContext } = jest.requireActual('../constants');
45+
return {
46+
__esModule: true,
47+
default: ({ children }: { children: React.ReactNode }) => (
48+
<CreateProviderFormContext.Provider
49+
value={{
50+
providerNames: ['existing-provider'],
51+
providerNamesLoaded: true,
52+
}}
53+
>
54+
{children}
55+
</CreateProviderFormContext.Provider>
56+
),
57+
};
58+
});
59+
60+
jest.mock('@openshift-console/dynamic-plugin-sdk', () => ({
61+
useActiveNamespace: () => ['test-namespace', jest.fn()],
62+
useK8sWatchResource: () => [[], true, null],
63+
useModal: () => ({ isOpen: false, showModal: jest.fn(), hideModal: jest.fn() }),
64+
useAccessReview: () => [true, false],
65+
ProjectModel: {
66+
apiGroup: 'project.openshift.io',
67+
plural: 'projects',
68+
},
69+
}));
70+
71+
jest.mock('@utils/hooks/useDefaultProject', () => ({
72+
useDefaultProject: () => 'test-namespace',
73+
}));
74+
75+
jest.mock('../fields/ProviderTypeField', () => {
76+
const { useController } = jest.requireActual('react-hook-form');
77+
const { useCreateProviderFormContext } = jest.requireActual(
78+
'../hooks/useCreateProviderFormContext',
79+
);
80+
81+
return {
82+
__esModule: true,
83+
default: () => {
84+
const { control } = useCreateProviderFormContext();
85+
const { field } = useController({
86+
control,
87+
name: 'providerType',
88+
});
89+
90+
return (
91+
<div>
92+
<label htmlFor="provider-type-select">Provider type</label>
93+
<select id="provider-type-select" data-testid="provider-type-select" {...field}>
94+
<option value="">Select a provider type</option>
95+
<option value="openshift">OpenShift Virtualization</option>
96+
</select>
97+
</div>
98+
);
99+
},
100+
};
101+
});
102+
103+
describe('CreateProviderForm - OpenShift Provider', () => {
104+
beforeEach(() => {
105+
clearAllProviderMocks();
106+
});
107+
108+
describe('Initial rendering', () => {
109+
it('renders provider project and provider type fields', () => {
110+
render(<CreateProviderForm />);
111+
112+
expect(screen.getByText('Provider project')).toBeInTheDocument();
113+
expect(screen.getByLabelText(/provider type/i)).toBeInTheDocument();
114+
});
115+
116+
it('disables create button when form is invalid', () => {
117+
render(<CreateProviderForm />);
118+
119+
const submitButton = screen.getByRole('button', { name: /create provider/i });
120+
expect(submitButton).toBeDisabled();
121+
});
122+
123+
it('does not show provider name field before selecting type', () => {
124+
render(<CreateProviderForm />);
125+
126+
expect(screen.queryByRole('textbox', { name: /provider name/i })).not.toBeInTheDocument();
127+
});
128+
});
129+
130+
it('shows OpenShift-specific fields after selecting OpenShift type', async () => {
131+
const user = userEvent.setup();
132+
render(<CreateProviderForm />);
133+
134+
await user.selectOptions(screen.getByLabelText(/provider type/i), 'openshift');
135+
136+
await waitFor(() => {
137+
expect(screen.getByRole('textbox', { name: /provider name/i })).toBeInTheDocument();
138+
expect(screen.getByRole('textbox', { name: /api endpoint url/i })).toBeInTheDocument();
139+
expect(
140+
screen.getByRole('radio', { name: /skip certificate validation/i }),
141+
).toBeInTheDocument();
142+
});
143+
144+
expect(
145+
screen.queryByRole('textbox', { name: /service account bearer token/i }),
146+
).not.toBeInTheDocument();
147+
});
148+
149+
it('shows bearer token field when URL is provided for remote cluster', async () => {
150+
const user = userEvent.setup();
151+
render(<CreateProviderForm />);
152+
153+
await user.selectOptions(screen.getByLabelText(/provider type/i), 'openshift');
154+
155+
await waitFor(() => {
156+
expect(screen.getByRole('textbox', { name: /api endpoint url/i })).toBeInTheDocument();
157+
});
158+
159+
const urlInput = screen.getByRole('textbox', { name: /api endpoint url/i });
160+
await user.type(urlInput, 'https://api.example.com:6443');
161+
162+
await waitFor(() => {
163+
expect(screen.getByLabelText(/service account bearer token/i)).toBeInTheDocument();
164+
});
165+
});
166+
167+
it('hides OVA fields when OpenShift provider is selected', async () => {
168+
const user = userEvent.setup();
169+
render(<CreateProviderForm />);
170+
171+
await user.selectOptions(screen.getByLabelText(/provider type/i), 'openshift');
172+
173+
await waitFor(() => {
174+
expect(screen.getByRole('textbox', { name: /api endpoint url/i })).toBeInTheDocument();
175+
});
176+
177+
expect(screen.queryByLabelText(/nfs shared directory/i)).not.toBeInTheDocument();
178+
});
179+
});

0 commit comments

Comments
 (0)