diff --git a/ci/yaml/ova-test.yaml b/ci/yaml/ova-test.yaml new file mode 100644 index 000000000..1ddcea72f --- /dev/null +++ b/ci/yaml/ova-test.yaml @@ -0,0 +1,9 @@ +apiVersion: forklift.konveyor.io/v1beta1 +kind: Provider +metadata: + name: ova-test + namespace: konveyor-forklift +spec: + secret: {} + type: ova + url: 'https://fake.coml:5000/files/ova' diff --git a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json index 1da4964be..298976598 100644 --- a/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json +++ b/packages/forklift-console-plugin/locales/en/plugin__forklift-console-plugin.json @@ -189,6 +189,7 @@ "Please enter URL for OpenStack services REST APIs.": "Please enter URL for OpenStack services REST APIs.", "Please enter URL for the kubernetes API server, if empty URL default to this cluster.": "Please enter URL for the kubernetes API server, if empty URL default to this cluster.", "Please enter URL for vSphere REST APIs server.": "Please enter URL for vSphere REST APIs server.", + "Please enter URL.": "Please enter URL.", "Pod network": "Pod network", "Product": "Product", "Project": "Project", @@ -231,6 +232,7 @@ "Specify the API end point URL, for example, https:///v3 for OpenStack.": "Specify the API end point URL, for example, https:///v3 for OpenStack.", "Specify the API end point URL, for example, https://:6443 for OpenShift.": "Specify the API end point URL, for example, https://:6443 for OpenShift.", "Specify the API end point URL, for example, https:///sdk for vSphere.": "Specify the API end point URL, for example, https:///sdk for vSphere.", + "Specify the end point to the NFS server path serving the OVA file[s].": "Specify the end point to the NFS server path serving the OVA file[s].", "Specify the VDDK image that you created. some functionality will not be available if the VDDK image is left empty": "Specify the VDDK image that you created. some functionality will not be available if the VDDK image is left empty", "SSHA-1 fingerprint": "SSHA-1 fingerprint", "Staging": "Staging", @@ -270,6 +272,7 @@ "Updated": "Updated", "URL": "URL", "URL must start with https:// or http:// and contain valid hostname and path": "URL must start with https:// or http:// and contain valid hostname and path", + "URL of the OVA provider": "URL of the OVA provider", "URL of the provider": "URL of the provider", "URL of the provider, leave empty to use this providers URL": "URL of the provider, leave empty to use this providers URL", "User Domain Name": "User Domain Name", diff --git a/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/EditProviderURLModal.tsx b/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/EditProviderURLModal.tsx index 50dfa1987..baa3a82c1 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/EditProviderURLModal.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/EditProviderURLModal.tsx @@ -7,6 +7,7 @@ import { EditModalProps } from '../EditModal'; import { OpenshiftEditURLModal } from './OpenshiftEditURLModal'; import { OpenstackEditURLModal } from './OpenstackEditURLModal'; +import { OVAEditURLModal } from './OVAEditURLModal'; import { OvirtEditURLModal } from './OvirtEditURLModal'; import { VSphereEditURLModal } from './VSphereEditURLModal'; @@ -31,6 +32,8 @@ export const EditProviderURLModal: React.FC = (props) return ; case 'vsphere': return ; + case 'ova': + return ; default: return <>; } diff --git a/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/OVAEditURLModal.tsx b/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/OVAEditURLModal.tsx new file mode 100644 index 000000000..e38b99ed0 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/OVAEditURLModal.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ProviderModel } from '@kubev2v/types'; +import { ModalVariant } from '@patternfly/react-core'; + +import { EditModal, ValidationHookType } from '../EditModal'; + +import { patchProviderURL } from './utils/patchProviderURL'; +import { EditProviderURLModalProps } from './EditProviderURLModal'; + +export const OVAEditURLModal: React.FC = (props) => { + const { t } = useForkliftTranslation(); + + const urlValidationHook: ValidationHookType = (value) => { + const isValidURL = true; + + return isValidURL + ? { + validationHelpText: undefined, + validated: 'success', + } + : { + validationHelpText: t( + 'URL must start with https:// or http:// and contain valid hostname and path', + ), + validated: 'error', + }; + }; + + return ( + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/index.ts b/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/index.ts index 4db42c955..e023bdcc1 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/index.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/modals/EditProviderURL/index.ts @@ -2,6 +2,7 @@ export * from './EditProviderURLModal'; export * from './OpenshiftEditURLModal'; export * from './OpenstackEditURLModal'; +export * from './OVAEditURLModal'; export * from './OvirtEditURLModal'; export * from './VSphereEditURLModal'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Providers/utils/components/Galerry/SelectableGallery.tsx b/packages/forklift-console-plugin/src/modules/Providers/utils/components/Galerry/SelectableGallery.tsx index d4926ba40..f56aa62bf 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/utils/components/Galerry/SelectableGallery.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/utils/components/Galerry/SelectableGallery.tsx @@ -33,7 +33,7 @@ interface SelectableGalleryProps { export const SelectableGallery: FC = ({ items, onChange, - sortFunction = ([, a], [, b]) => a.title.localeCompare(b.title), + sortFunction, selectedID, }) => { // State to manage the selected card's id @@ -52,7 +52,9 @@ export const SelectableGallery: FC = ({ }; // Convert the items object to an array and sort it - const sortedItems = Object.entries(items).sort(sortFunction); + const sortedItems = sortFunction + ? Object.entries(items).sort(sortFunction) + : Object.entries(items); return ( diff --git a/packages/forklift-console-plugin/src/modules/Providers/utils/helpers/getIsTarget.ts b/packages/forklift-console-plugin/src/modules/Providers/utils/helpers/getIsTarget.ts index 39481cd77..043bc63d9 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/utils/helpers/getIsTarget.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/utils/helpers/getIsTarget.ts @@ -20,5 +20,5 @@ export function getIsOnlySource(provider: V1beta1Provider): boolean { return SOURCE_ONLY_PROVIDER_TYPES.includes(provider?.spec.type as ProviderType); } -export const SOURCE_ONLY_PROVIDER_TYPES: ProviderType[] = ['vsphere', 'ovirt', 'openstack']; +export const SOURCE_ONLY_PROVIDER_TYPES: ProviderType[] = ['ova', 'vsphere', 'ovirt', 'openstack']; export const TARGET_PROVIDER_TYPES: ProviderType[] = ['openshift']; diff --git a/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/index.ts b/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/index.ts index 7a8a85642..dee0ef1c3 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/index.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/index.ts @@ -1,6 +1,7 @@ // @index(['./*.tsx', './*.ts', /__/g], f => `export * from '${f.path}';`) export * from './openshiftProviderValidator'; export * from './openstackProviderValidator'; +export * from './ovaProviderValidator'; export * from './ovirtProviderValidator'; export * from './providerValidator'; export * from './vsphereProviderValidator'; diff --git a/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/ovaProviderValidator.ts b/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/ovaProviderValidator.ts new file mode 100644 index 000000000..a532d3b4e --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/ovaProviderValidator.ts @@ -0,0 +1,13 @@ +import { V1beta1Provider } from '@kubev2v/types'; + +import { validateK8sName } from '../common'; + +export function ovaProviderValidator(provider: V1beta1Provider) { + const name = provider?.metadata?.name; + + if (!validateK8sName(name)) { + return new Error('invalided kubernetes resource name'); + } + + return null; +} diff --git a/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/providerValidator.ts b/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/providerValidator.ts index 4f34f903b..61ec4fe38 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/providerValidator.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/utils/validators/provider/providerValidator.ts @@ -2,6 +2,7 @@ import { V1beta1Provider } from '@kubev2v/types'; import { openshiftProviderValidator } from './openshiftProviderValidator'; import { openstackProviderValidator } from './openstackProviderValidator'; +import { ovaProviderValidator } from './ovaProviderValidator'; import { ovirtProviderValidator } from './ovirtProviderValidator'; import { vsphereProviderValidator } from './vsphereProviderValidator'; @@ -21,6 +22,9 @@ export function providerValidator(provider: V1beta1Provider) { case 'vsphere': validationError = vsphereProviderValidator(provider); break; + case 'ova': + validationError = ovaProviderValidator(provider); + break; default: validationError = new Error('bad provider type'); } diff --git a/packages/forklift-console-plugin/src/modules/Providers/utils/validators/secret/secretValidator.ts b/packages/forklift-console-plugin/src/modules/Providers/utils/validators/secret/secretValidator.ts index 1c7eaef0e..bd8a8dffe 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/utils/validators/secret/secretValidator.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/utils/validators/secret/secretValidator.ts @@ -21,6 +21,9 @@ export function secretValidator(type: string, secret: V1Secret) { case 'vsphere': validationError = vsphereSecretValidator(secret); break; + case 'ova': + validationError = null; + break; default: validationError = new Error('bad provider type'); } diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/components/EditProvider.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/create/components/EditProvider.tsx index aca1167e1..f13021a81 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/create/components/EditProvider.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/components/EditProvider.tsx @@ -9,6 +9,7 @@ import { import { OpenshiftProviderFormCreate } from './OpenshiftProviderCreateForm'; import { OpenstackProviderCreateForm } from './OpenstackProviderCreateForm'; +import { OVAProviderCreateForm } from './OVAProviderCreateForm'; import { OvirtProviderCreateForm } from './OvirtProviderCreateForm'; import { ProvidersCreateFormProps } from './ProviderCreateForm'; import { VSphereProviderCreateForm } from './VSphereProviderCreateForm'; @@ -48,6 +49,12 @@ export const EditProvider: React.FC = ({ ); + case 'ova': + return ( + <> + + + ); default: return <>; } diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/components/OVAProviderCreateForm.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/create/components/OVAProviderCreateForm.tsx new file mode 100644 index 000000000..699fd9b8a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/components/OVAProviderCreateForm.tsx @@ -0,0 +1,78 @@ +import React, { useCallback, useReducer } from 'react'; +import { Validation } from 'src/modules/Providers/utils'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { V1beta1Provider } from '@kubev2v/types'; +import { Form, FormGroup, TextInput } from '@patternfly/react-core'; + +export interface OVAProviderCreateFormProps { + provider: V1beta1Provider; + onChange: (newValue: V1beta1Provider) => void; +} + +export const OVAProviderCreateForm: React.FC = ({ + provider, + onChange, +}) => { + const { t } = useForkliftTranslation(); + + const url = provider?.spec?.url || ''; + + const initialState = { + validation: { + url: 'default' as Validation, + }, + }; + + const reducer = (state, action) => { + switch (action.type) { + case 'SET_FIELD_VALIDATED': + return { + ...state, + validation: { + ...state.validation, + [action.payload.field]: action.payload.validationState, + }, + }; + default: + return state; + } + }; + + const [state, dispatch] = useReducer(reducer, initialState); + + const handleChange = useCallback( + (id, value) => { + const trimmedValue = value.trim(); + + if (id === 'url') { + const validationState = 'success'; + dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: id, validationState } }); + + onChange({ ...provider, spec: { ...provider.spec, url: trimmedValue } }); + } + }, + [provider], + ); + + return ( +
+ + handleChange('url', value)} + /> + +
+ ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/components/index.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/components/index.ts index a421326e6..a7b3803dc 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/create/components/index.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/components/index.ts @@ -1,6 +1,8 @@ // @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './EditProvider'; export * from './OpenshiftProviderCreateForm'; export * from './OpenstackProviderCreateForm'; +export * from './OVAProviderCreateForm'; export * from './OvirtProviderCreateForm'; export * from './providerCardItems'; export * from './ProviderCreateForm'; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/components/providerCardItems.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/create/components/providerCardItems.tsx index 48ab9bc6b..fbdd882fe 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/create/components/providerCardItems.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/components/providerCardItems.tsx @@ -3,6 +3,7 @@ import { SelectableGalleryItem } from 'src/modules/Providers/utils/components/Ga import openshiftImg from '../images/openshift.svg'; import openstackImg from '../images/openstack.svg'; +import ovaImg from '../images/ova.svg'; import redhatImg from '../images/redhat.svg'; import vmImg from '../images/vm.svg'; @@ -42,6 +43,15 @@ const redhatLogo = ( /> ); +const ovaLogo = ( + PatternFly logo +); + export const providerCardItems: Record = { openshift: { title: 'OpenShift Virtualization', @@ -63,4 +73,9 @@ export const providerCardItems: Record = { logo: vmLogo, content: "vSphere is VMware's cloud computing virtualization platform.", }, + ova: { + title: 'Open Virtual Appliance (OVA)', + logo: ovaLogo, + content: 'OVA file is a virtual appliance used by virtualization applications.', + }, }; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/images/ova.svg b/packages/forklift-console-plugin/src/modules/Providers/views/create/images/ova.svg new file mode 100644 index 000000000..21334cd74 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/images/ova.svg @@ -0,0 +1,130 @@ + +Open Virtualization Alliance logoimage/svg+xmlOpen Virtualization Alliance logo diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/utils/createSecret.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/utils/createSecret.ts index e0d97235a..bcd6341aa 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/create/utils/createSecret.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/utils/createSecret.ts @@ -35,6 +35,7 @@ export async function createSecret(provider: V1beta1Provider, secret: V1Secret) labels: { ...secret?.metadata?.labels, createdForProviderType: provider?.spec?.type, + createdForResourceType: 'provider', }, }, data: { ...secret?.data, url: encodedURL }, diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/DetailsSection.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/DetailsSection.tsx index d337c3d87..b8694c5db 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/DetailsSection.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/DetailsSection.tsx @@ -4,6 +4,7 @@ import { ProviderData } from 'src/modules/Providers/utils'; import { OpenshiftDetailsSection } from './OpenshiftDetailsSection'; import { OpenstackDetailsSection } from './OpenstackDetailsSection'; +import { OVADetailsSection } from './OVADetailsSection'; import { OvirtDetailsSection } from './OvirtDetailsSection'; import { VSphereDetailsSection } from './VSphereDetailsSection'; @@ -19,6 +20,8 @@ const DetailsSection_: React.FC = (props) => { return ; case 'vsphere': return ; + case 'ova': + return ; default: return <>; } diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/OVADetailsSection.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/OVADetailsSection.tsx new file mode 100644 index 000000000..345ffc981 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/OVADetailsSection.tsx @@ -0,0 +1,124 @@ +import React from 'react'; +import { EditProviderURLModal, useModal } from 'src/modules/Providers/modals'; +import { HELP_LINK_HREF } from 'src/utils/constants'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { ResourceLink, Timestamp } from '@openshift-console/dynamic-plugin-sdk'; +import { DescriptionList, Label, Text } from '@patternfly/react-core'; + +import { DetailsItem, OwnerReferencesItem } from '../../../../utils'; + +import { DetailsSectionProps } from './DetailsSection'; + +export const OVADetailsSection: React.FC = ({ data }) => { + const { t } = useForkliftTranslation(); + const { showModal } = useModal(); + + const { provider } = data; + + return ( + + + {provider?.spec?.type}{' '} + {!provider?.spec?.url && ( + + )} + + } + moreInfoLink={HELP_LINK_HREF} + helpContent={ + {t(`Allowed values are openshift, ovirt, vsphere, and openstack.`)} + } + crumbs={['Provider', 'spec', 'type']} + /> + + + + + {t( + 'Name is primarily intended for creation idempotence and configuration definition. Cannot be updated.', + )} + + } + crumbs={['Provider', 'metadata', 'name']} + /> + + + } + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={t( + `Namespace defines the space within which each name must be unique. + An empty namespace is equivalent to the "default" namespace, but "default" is the canonical representation. + Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.`, + )} + crumbs={['Provider', 'metadata', 'namespace']} + /> + + {t('Empty')}} + moreInfoLink={ + 'https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces' + } + helpContent={{t(`The provider URL. Empty may be used for the host provider.`)}} + crumbs={['Provider', 'spec', 'url']} + onEdit={ + provider?.spec?.url && (() => showModal()) + } + /> + + } + helpContent={ + + {t( + `CreationTimestamp is a timestamp representing the server time when this object was created. + It is not guaranteed to be set in happens-before order across separate operations. + Clients may not set this value. It is represented in RFC3339 form and is in UTC.`, + )} + + } + crumbs={['Provider', 'metadata', 'creationTimestamp']} + /> + + } + helpContent={ + + {t( + `List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. + If this object is managed by a controller, then an entry in this list will point to this controller, + with the controller field set to true. There cannot be more than one managing controller.`, + )} + + } + crumbs={['Provider', 'metadata', 'ownerReferences']} + /> + + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/index.ts b/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/index.ts index ec4ca3979..e2c889439 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/index.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/details/components/DetailsSection/index.ts @@ -2,6 +2,7 @@ export * from './DetailsSection'; export * from './OpenshiftDetailsSection'; export * from './OpenstackDetailsSection'; +export * from './OVADetailsSection'; export * from './OvirtDetailsSection'; export * from './VSphereDetailsSection'; // @endindex diff --git a/packages/forklift-console-plugin/src/utils/enums.ts b/packages/forklift-console-plugin/src/utils/enums.ts index d7170cdf1..e676e5132 100644 --- a/packages/forklift-console-plugin/src/utils/enums.ts +++ b/packages/forklift-console-plugin/src/utils/enums.ts @@ -1,6 +1,6 @@ import { K8sConditionStatus } from '@kubev2v/common'; -import { PlanState, ProviderType } from '@kubev2v/legacy/common/constants'; -import { PlanType } from '@kubev2v/legacy/queries/types'; +import { PlanState } from '@kubev2v/legacy/common/constants'; +import { ProviderType } from '@kubev2v/types'; import { MappingStatus, ProviderStatus } from './types'; @@ -9,6 +9,7 @@ export const PROVIDERS: Record = { ovirt: 'oVirt', openstack: 'OpenStack', openshift: 'KubeVirt', + ova: 'OVA', }; export const CONDITIONS: Record = { @@ -30,7 +31,7 @@ export const PROVIDER_STATUS: Record = { Unknown: 'Unknown', }; -export const PLAN_TYPE: Record = { +export const PLAN_TYPE: Record = { Warm: 'Warm', Cold: 'Cold', }; diff --git a/packages/types/src/types/ProviderSecret.ts b/packages/types/src/types/ProviderSecret.ts index 8279a3c6a..0a63f9d54 100644 --- a/packages/types/src/types/ProviderSecret.ts +++ b/packages/types/src/types/ProviderSecret.ts @@ -7,7 +7,7 @@ import { VSphereProviderSecret, } from './secret'; -export type ProviderType = 'openshift' | 'vsphere' | 'ovirt' | 'openstack'; +export type ProviderType = 'openshift' | 'vsphere' | 'ovirt' | 'openstack' | 'ova'; /** * Provider secret containing credentials and other confidential information