Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MTV-1794] Update provider vCenter validation #1523

Merged
merged 1 commit into from
Mar 20, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -588,7 +588,7 @@
"The target namespace is the namespace within your selected target provider that your virtual machines will be migrated to. This is different from the namespace that your migration plan will be created in and where your provider was created.": "The target namespace is the namespace within your selected target provider that your virtual machines will be migrated to. This is different from the namespace that your migration plan will be created in and where your provider was created.",
"The URL of the OpenStack Identity (Keystone) endpoint, for example: https://identity_service.com:5000/v3": "The URL of the OpenStack Identity (Keystone) endpoint, for example: https://identity_service.com:5000/v3",
"The URL of the Red Hat Virtualization Manager (RHVM) API endpoint, for example: https://rhv-host-example.com/ovirt-engine/api .": "The URL of the Red Hat Virtualization Manager (RHVM) API endpoint, for example: https://rhv-host-example.com/ovirt-engine/api .",
"The URL of the vCenter API endpoint, for example: https://vCenter-host-example.com/sdk .": "The URL of the vCenter API endpoint, for example: https://vCenter-host-example.com/sdk .",
"The URL of the vCenter API endpoint, for example: https://vCenter-host-example.com/sdk.": "The URL of the vCenter API endpoint, for example: https://vCenter-host-example.com/sdk.",
"The username for the ESXi host admin": "The username for the ESXi host admin",
"The virtual machines will be permanently deleted from your migration plan.": "The virtual machines will be permanently deleted from your migration plan.",
"The VM specific template will override the template set in the plan.": "The VM specific template will override the template set in the plan.",
1 change: 0 additions & 1 deletion packages/forklift-console-plugin/package.json
Original file line number Diff line number Diff line change
@@ -41,7 +41,6 @@
"jsonpath": "^1.1.1",
"jsrsasign": "11.1.0",
"luxon": "^3.5.0",
"node-forge": "^1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-i18next": "^11.14.3",
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ export type EditProviderURLModalProps = Modify<
label?: string;
model?: K8sModel;
jsonPath?: string | string[];
insecureSkipVerify?: string;
}
>;

Original file line number Diff line number Diff line change
@@ -10,18 +10,22 @@ import { EditModal, ValidationHookType } from '../EditModal';
import { patchProviderURL } from './utils/patchProviderURL';
import { EditProviderURLModalProps } from './EditProviderURLModal';

export const VSphereEditURLModal: React.FC<EditProviderURLModalProps> = (props) => {
export const VSphereEditURLModal: React.FC<EditProviderURLModalProps> = ({
title,
label,
resource: provider,
insecureSkipVerify,
...props
}) => {
const { t } = useForkliftTranslation();
const provider = props.resource;

let validationHook: ValidationHookType;

// VCenter of ESXi
const sdkEndpoint = provider?.spec?.settings?.['sdkEndpoint'] || '';
if (sdkEndpoint === 'esxi') {
validationHook = validateEsxiURL;
} else {
validationHook = validateVCenterURL;
validationHook = (url) => validateVCenterURL(url, insecureSkipVerify);
}

const ModalBody = (
@@ -39,14 +43,15 @@ export const VSphereEditURLModal: React.FC<EditProviderURLModalProps> = (props)
return (
<EditModal
{...props}
resource={provider}
jsonPath={'spec.url'}
title={props?.title || t('Edit URL')}
label={props?.label || t('URL')}
title={title || t('Edit URL')}
label={label || t('URL')}
model={ProviderModel}
variant={ModalVariant.large}
body={ModalBody}
helperText={t(
'The URL of the vCenter API endpoint, for example: https://vCenter-host-example.com/sdk .',
'The URL of the vCenter API endpoint, for example: https://vCenter-host-example.com/sdk.',
)}
onConfirmHook={patchProviderURL}
validationHook={validationHook}
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ export function providerValidator(
validationError = ovirtProviderValidator(provider);
break;
case 'vsphere':
validationError = vsphereProviderValidator(provider, subType, secret?.data?.cacert);
validationError = vsphereProviderValidator(provider, subType, secret);
break;
case 'ova':
validationError = ovaProviderValidator(provider);
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
import { pki } from 'node-forge';

import { safeBase64Decode } from '../../../helpers';
import { validateIpv4, validateURL, ValidationMsg } from '../../common';

export const urlMatchesCertFqdn = (urlHostname: string, caCert: string): boolean => {
try {
const decodedCaCert = safeBase64Decode(caCert);
const cert = pki.certificateFromPem(decodedCaCert);
const dnsAltName = cert.extensions
.find((ext) => ext.name === 'subjectAltName')
?.altNames.find((altName) => altName.type === 2)?.value;
const commonName = cert.subject.attributes.find((attr) => attr.name === 'commonName')?.value;

return urlHostname === (dnsAltName || commonName);
} catch (e) {
console.error('Unable to parse certificate object from PEM.');
}

return false;
};

export const validateVCenterURL = (url: string, caCert?: string): ValidationMsg => {
export const validateVCenterURL = (
url: string | number,
insecureSkipVerify?: string,
): ValidationMsg => {
// For a newly opened form where the field is not set yet, set the validation type to default.
if (url === undefined) {
return {
@@ -38,6 +22,7 @@ export const validateVCenterURL = (url: string, caCert?: string): ValidationMsg
const isValidURL = validateURL(trimmedUrl);
const urlObject = getUrlObject(url);
const urlHostname = urlObject?.hostname;
const isSecure = !insecureSkipVerify || safeBase64Decode(insecureSkipVerify) === 'false';

if (trimmedUrl === '') {
return {
@@ -53,18 +38,13 @@ export const validateVCenterURL = (url: string, caCert?: string): ValidationMsg
};
}

if (urlObject?.protocol === 'https:') {
if (validateIpv4(urlHostname)) {
return {
type: 'error',
msg: 'Invalid URL. The URL must be a fully qualified domain name (FQDN).',
};
}
if (isSecure) {
const isValidIpAddress = validateIpv4(urlHostname);

if (caCert && !urlMatchesCertFqdn(urlHostname, caCert)) {
if (isValidIpAddress) {
return {
type: 'error',
msg: 'Invalid URL. The URL must be a fully qualified domain name (FQDN) and match the FQDN in the certificate you uploaded.',
type: 'warning',
msg: 'The URL is not a fully qualified domain name (FQDN). If the certificate is not skipped and does not match the URL, the connection might fail.',
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { V1beta1Provider } from '@kubev2v/types';
import { IoK8sApiCoreV1Secret } from '@kubev2v/types';

import { validateK8sName, validateURL, ValidationMsg } from '../../common';
import { SecretSubType } from '../secretValidator';
@@ -9,7 +10,7 @@ import { validateVDDKImage } from './validateVDDKImage';
export function vsphereProviderValidator(
provider: V1beta1Provider,
subType?: SecretSubType,
caCert?: string,
secret?: IoK8sApiCoreV1Secret,
): ValidationMsg {
const name = provider?.metadata?.name;
const url = provider?.spec?.url || '';
@@ -23,8 +24,8 @@ export function vsphereProviderValidator(
}

if (
subType === 'vcenter' && caCert
? validateVCenterURL(url, caCert).type === 'error'
subType === 'vcenter'
? validateVCenterURL(url, secret?.data?.insecureSkipVerify).type === 'error'
: !validateURL(url)
) {
return { type: 'error', msg: 'invalid URL' };
Original file line number Diff line number Diff line change
@@ -73,7 +73,7 @@ export const EditProvider: React.FC<ProvidersCreateFormProps> = ({
<>
<VCenterProviderCreateForm
provider={newProvider}
caCert={newSecret.data.cacert}
secret={newSecret}
onChange={onNewProviderChange}
/>

Original file line number Diff line number Diff line change
@@ -8,19 +8,19 @@ import {
import { useForkliftTranslation } from 'src/utils/i18n';

import { FormGroupWithHelpText } from '@kubev2v/common';
import { V1beta1Provider } from '@kubev2v/types';
import { IoK8sApiCoreV1Secret, V1beta1Provider } from '@kubev2v/types';
import { Alert, Checkbox, Form, Popover, Radio, TextInput } from '@patternfly/react-core';
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';

export interface VCenterProviderCreateFormProps {
provider: V1beta1Provider;
caCert: string;
secret: IoK8sApiCoreV1Secret;
onChange: (newValue: V1beta1Provider) => void;
}

export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps> = ({
provider,
caCert,
secret,
onChange,
}) => {
const { t } = useForkliftTranslation();
@@ -33,7 +33,7 @@ export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps>

const initialState = {
validation: {
url: validateVCenterURL(url, caCert),
url: validateVCenterURL(url, secret?.data?.insecureSkipVerify),
vddkInitImage: validateVDDKImage(vddkInitImage),
},
};
@@ -42,9 +42,12 @@ export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps>
useEffect(() => {
dispatch({
type: 'SET_FIELD_VALIDATED',
payload: { field: 'url', validationState: validateVCenterURL(url, caCert) },
payload: {
field: 'url',
validationState: validateVCenterURL(url, secret?.data?.insecureSkipVerify),
},
});
}, [caCert]);
}, [secret]);

const reducer = (state, action) => {
switch (action.type) {
@@ -127,14 +130,14 @@ export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps>

if (id === 'url') {
// Validate URL - VCenter of ESXi
const validationState = validateVCenterURL(trimmedValue, caCert);
const validationState = validateVCenterURL(trimmedValue, secret?.data?.insecureSkipVerify);

dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: 'url', validationState } });

onChange({ ...provider, spec: { ...provider.spec, url: trimmedValue } });
}
},
[provider, caCert],
[provider, secret],
);

const onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void = (event) => {
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@ import React from 'react';
import { EditProviderURLModal, useModal } from 'src/modules/Providers/modals';
import { useForkliftTranslation } from 'src/utils/i18n';

import { IoK8sApiCoreV1Secret } from '@kubev2v/types';
import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';

import { DetailsItem } from '../../../../../utils';

import { ProviderDetailsItemProps } from './ProviderDetailsItem';
@@ -15,6 +18,13 @@ export const URLDetailsItem: React.FC<ProviderDetailsItemProps> = ({
const { t } = useForkliftTranslation();
const { showModal } = useModal();

const [secret] = useK8sWatchResource<IoK8sApiCoreV1Secret>({
groupVersionKind: { version: 'v1', kind: 'Secret' },
namespaced: true,
namespace: provider?.spec?.secret?.namespace,
name: provider?.spec?.secret?.name,
});

const defaultMoreInfoLink =
'https://docs.redhat.com/en/documentation/migration_toolkit_for_virtualization/2.7/html-single/installing_and_using_the_migration_toolkit_for_virtualization/index#adding-source-providers';
const defaultHelpContent =
@@ -31,7 +41,13 @@ export const URLDetailsItem: React.FC<ProviderDetailsItemProps> = ({
onEdit={
canPatch &&
provider?.spec?.url &&
(() => showModal(<EditProviderURLModal resource={provider} />))
(() =>
showModal(
<EditProviderURLModal
resource={provider}
insecureSkipVerify={secret?.data?.insecureSkipVerify}
/>,
))
}
/>
);