diff --git a/packages/eslint-plugin/cspell.wordlist.txt b/packages/eslint-plugin/cspell.wordlist.txt index 291d06432..669f57a7d 100644 --- a/packages/eslint-plugin/cspell.wordlist.txt +++ b/packages/eslint-plugin/cspell.wordlist.txt @@ -67,3 +67,5 @@ filesystems bootloader typeahead immer +networkattachmentdefinitions +nicprofiles \ No newline at end of file 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 ede8db2dc..d469e2329 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 @@ -19,6 +19,7 @@ "A user password for connecting to the Red Hat Virtualization Manager (RHVM) API endpoint.": "A user password for connecting to the Red Hat Virtualization Manager (RHVM) API endpoint.", "A user password for connecting to the vCenter API endpoint.": "A user password for connecting to the vCenter API endpoint.", "Actions": "Actions", + "Add mapping": "Add mapping", "Add source and target providers for the migration.": "Add source and target providers for the migration.", "Application credential ID": "Application credential ID", "Application credential name": "Application credential name", @@ -85,6 +86,7 @@ "Defines the CPU limits allocated to the main container in the controller pod. The default value is 500 milliCPU.": "Defines the CPU limits allocated to the main container in the controller pod. The default value is 500 milliCPU.", "Delete": "Delete", "Delete {{model.label}}": "Delete {{model.label}}", + "Delete mapping": "Delete mapping", "Delete Mapping": "Delete Mapping", "Delete NetworkMap?": "Delete NetworkMap?", "Delete Plan?": "Delete Plan?", @@ -229,14 +231,17 @@ "Namespace is not defined": "Namespace is not defined", "Network for data transfer": "Network for data transfer", "Network interfaces": "Network interfaces", + "Network map:": "Network map:", "NetworkAttachmentDefinitions": "NetworkAttachmentDefinitions", "NetworkMaps": "NetworkMaps", "NetworkMaps for virtualization": "NetworkMaps for virtualization", "Networks": "Networks", + "Networks used by the selected VMs": "Networks used by the selected VMs", "No credentials found.": "No credentials found.", "No inventory data available.": "No inventory data available.", "No NetworkMaps found in namespace <1>{namespace}.": "No NetworkMaps found in namespace <1>{namespace}.", "No NetworkMaps found.": "No NetworkMaps found.", + "No networks in this category": "No networks in this category", "No owner": "No owner", "No Plans found in namespace <1>{namespace}.": "No Plans found in namespace <1>{namespace}.", "No Plans found.": "No Plans found.", @@ -249,6 +254,7 @@ "No secret.": "No secret.", "No StorageMaps found in namespace <1>{namespace}.": "No StorageMaps found in namespace <1>{namespace}.", "No StorageMaps found.": "No StorageMaps found.", + "No storages in this category": "No storages in this category", "Not Ready": "Not Ready", "Note: If 'Skip certificate validation' is selected, migrations from this provider will not be secure.

Insecure migration means that the transferred data is sent over an insecure connection and potentially sensitive data could be exposed.": "Note: If 'Skip certificate validation' is selected, migrations from this provider will not be secure.

Insecure migration means that the transferred data is sent over an insecure connection and potentially sensitive data could be exposed.", "Note: It is strongly recommended to specify a VDDK init image to accelerate migrations.": "Note: It is strongly recommended to specify a VDDK init image to accelerate migrations.", @@ -287,6 +293,8 @@ "OpenStack REST API user name.": "OpenStack REST API user name.", "Operator": "Operator", "Operator conditions define the current state of the controller": "Operator conditions define the current state of the controller", + "Other networks present on the source provider ": "Other networks present on the source provider ", + "Other storages present on the source provider ": "Other storages present on the source provider ", "OvaPath": "OvaPath", "Overview": "Overview", "Owner": "Owner", @@ -365,8 +373,10 @@ "Storage": "Storage", "Storage classes": "Storage classes", "Storage domains": "Storage domains", + "Storage map:": "Storage map:", "StorageMaps": "StorageMaps", "StorageMaps for virtualization": "StorageMaps for virtualization", + "Storages used by the selected VMs": "Storages used by the selected VMs", "Succeeded": "Succeeded", "Target and Source": "Target and Source", "Target namespace": "Target namespace", diff --git a/packages/forklift-console-plugin/src/modules/Providers/hooks/useNetworks.ts b/packages/forklift-console-plugin/src/modules/Providers/hooks/useNetworks.ts new file mode 100644 index 000000000..d60098d31 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/hooks/useNetworks.ts @@ -0,0 +1,64 @@ +import { useMemo } from 'react'; + +import { + OpenShiftNetworkAttachmentDefinition, + OpenstackNetwork, + OVirtNetwork, + ProviderType, + V1beta1Provider, + VSphereNetwork, +} from '@kubev2v/types'; + +import useProviderInventory from './useProviderInventory'; + +export type InventoryNetwork = + | OpenShiftNetworkAttachmentDefinition + | OpenstackNetwork + | OVirtNetwork + | VSphereNetwork; + +export const useSourceNetworks = ( + provider: V1beta1Provider, +): [InventoryNetwork[], boolean, Error] => { + const providerType: ProviderType = provider?.spec?.type as ProviderType; + const { + inventory: networks, + loading, + error, + } = useProviderInventory({ + provider, + subPath: providerType === 'openshift' ? '/networkattachmentdefinitions' : '/networks', + }); + + const typedNetworks = useMemo( + () => + Array.isArray(networks) + ? networks.map((net) => ({ ...net, providerType } as InventoryNetwork)) + : [], + [networks], + ); + + return [typedNetworks, loading, error]; +}; + +export const useOpenShiftNetworks = ( + provider: V1beta1Provider, +): [OpenShiftNetworkAttachmentDefinition[], boolean, Error] => { + const isOpenShift = provider?.spec?.type === 'openshift'; + const { + inventory: networks, + loading, + error, + } = useProviderInventory({ + provider, + subPath: isOpenShift ? '/networkattachmentdefinitions' : '', + }); + + const typedNetworks: OpenShiftNetworkAttachmentDefinition[] = useMemo( + () => + Array.isArray(networks) ? networks.map((net) => ({ ...net, providerType: 'openshift' })) : [], + [networks], + ); + + return [typedNetworks, loading, error]; +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/hooks/useNicProfiles.ts b/packages/forklift-console-plugin/src/modules/Providers/hooks/useNicProfiles.ts new file mode 100644 index 000000000..6693f8271 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/hooks/useNicProfiles.ts @@ -0,0 +1,29 @@ +import { useMemo } from 'react'; + +import { OVirtNicProfile, V1beta1Provider } from '@kubev2v/types'; + +import useProviderInventory from './useProviderInventory'; + +/** + * Works only for oVirt + */ +export const useNicProfiles = (provider: V1beta1Provider): [OVirtNicProfile[], boolean, Error] => { + const isOVirt = provider?.spec?.type === 'ovirt'; + const { + inventory: nicProfiles, + loading, + error, + } = useProviderInventory({ + provider, + subPath: isOVirt ? '/nicprofiles?detail=1' : '', + }); + + const stable = useMemo(() => { + if (!isOVirt) { + return []; + } + return Array.isArray(nicProfiles) ? nicProfiles : []; + }, [isOVirt, nicProfiles]); + + return [stable, loading, error]; +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts index 2f2ecff62..506828835 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/index.ts @@ -1,5 +1,7 @@ // @index(['./*', /style/g], f => `export * from '${f.path}';`) +export * from './networkMapTemplate'; export * from './planTemplate'; export * from './providerTemplate'; export * from './secretTemplate'; +export * from './storageMapTemplate'; // @endindex diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/networkMapTemplate.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/networkMapTemplate.ts new file mode 100644 index 000000000..2106c7fa0 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/networkMapTemplate.ts @@ -0,0 +1,10 @@ +import { V1beta1NetworkMap } from '@kubev2v/types'; + +export const networkMapTemplate: V1beta1NetworkMap = { + apiVersion: 'forklift.konveyor.io/v1beta1', + kind: 'NetworkMap', + metadata: { + name: undefined, + namespace: undefined, + }, +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/storageMapTemplate.ts b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/storageMapTemplate.ts new file mode 100644 index 000000000..6494a930a --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/create/templates/storageMapTemplate.ts @@ -0,0 +1,10 @@ +import { V1beta1StorageMap } from '@kubev2v/types'; + +export const storageMapTemplate: V1beta1StorageMap = { + apiVersion: 'forklift.konveyor.io/v1beta1', + kind: 'StorageMap', + metadata: { + name: undefined, + namespace: undefined, + }, +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/MappingList.tsx b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/MappingList.tsx new file mode 100644 index 000000000..300ebe662 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/MappingList.tsx @@ -0,0 +1,224 @@ +import React, { FC } from 'react'; +import { useForkliftTranslation } from 'src/utils/i18n'; + +import { + Button, + DataList, + DataListAction, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + Select, + SelectGroup, + SelectOption, + SelectVariant, +} from '@patternfly/react-core'; +import { MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; + +import { useToggle } from '../../hooks'; + +import './ProvidersCreateVmMigration.style.css'; + +export interface Mapping { + source: string; + destination: string; +} + +interface MappingListProps { + mappings: Mapping[]; + sources: { + label: string; + usedBySelectedVms: boolean; + isMapped: boolean; + }[]; + availableDestinations: string[]; + replaceMapping: (val: { current: Mapping; next: Mapping }) => void; + deleteMapping: (mapping: Mapping) => void; + addMapping: (mapping: Mapping) => void; + usedSourcesLabel: string; + generalSourcesLabel: string; + noSourcesLabel: string; + isDisabled: boolean; +} + +export const MappingList: FC = ({ + mappings, + sources, + availableDestinations, + replaceMapping, + deleteMapping, + addMapping, + usedSourcesLabel, + generalSourcesLabel, + noSourcesLabel, + isDisabled, +}) => { + const { t } = useForkliftTranslation(); + const usedSources = sources.filter(({ usedBySelectedVms }) => usedBySelectedVms); + const generalSources = sources.filter(({ usedBySelectedVms }) => !usedBySelectedVms); + const allMapped = sources.every(({ isMapped }) => isMapped); + return ( + <> + + {mappings.map(({ source, destination }, index) => ( + + ))} + + + + ); +}; + +interface MappingItemProps { + source: string; + destination: string; + destinations: string[]; + generalSources: { + label: string; + usedBySelectedVms: boolean; + isMapped: boolean; + }[]; + usedSources: { + label: string; + usedBySelectedVms: boolean; + isMapped: boolean; + }[]; + usedSourcesLabel: string; + generalSourcesLabel: string; + noSourcesLabel: string; + index: number; + replaceMapping: (val: { current: Mapping; next: Mapping }) => void; + deleteMapping: (mapping: Mapping) => void; + isDisabled: boolean; +} +const MappingItem: FC = ({ + source, + destination, + destinations, + generalSources, + usedSources, + usedSourcesLabel, + generalSourcesLabel, + noSourcesLabel, + index, + replaceMapping, + deleteMapping, + isDisabled, +}) => { + const { t } = useForkliftTranslation(); + const [isSrcOpen, setToggleSrcOpen] = useToggle(false); + const [isTrgOpen, setToggleTrgOpen] = useToggle(false); + return ( + + + + + , + + + , + ]} + /> + + diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/actions.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/actions.ts index 5cd628816..2f8b0dfe8 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/actions.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/actions.ts @@ -1,4 +1,18 @@ -import { OpenShiftNamespace, V1beta1Plan, V1beta1Provider } from '@kubev2v/types'; +import { + OpenShiftNamespace, + OpenShiftNetworkAttachmentDefinition, + OVirtNicProfile, + V1beta1NetworkMap, + V1beta1Plan, + V1beta1Provider, +} from '@kubev2v/types'; + +import { InventoryNetwork } from '../../hooks/useNetworks'; + +import { Mapping } from './MappingList'; + +export const POD_NETWORK = 'Pod Networking'; +export const DEFAULT_NAMESPACE = 'default'; // action type names export const SET_NAME = 'SET_NAME'; @@ -8,6 +22,14 @@ export const SET_TARGET_NAMESPACE = 'SET_TARGET_NAMESPACE'; export const SET_AVAILABLE_PROVIDERS = 'SET_AVAILABLE_PROVIDERS'; export const SET_EXISTING_PLANS = 'SET_EXISTING_PLANS'; export const SET_AVAILABLE_TARGET_NAMESPACES = 'SET_AVAILABLE_TARGET_NAMESPACES'; +export const REPLACE_NETWORK_MAPPING = 'REPLACE_NETWORK_MAPPING'; +export const REPLACE_STORAGE_MAPPING = 'REPLACE_STORAGE_MAPPING'; +export const SET_AVAILABLE_TARGET_NETWORKS = 'SET_AVAILABLE_TARGET_NETWORKS'; +export const SET_AVAILABLE_SOURCE_NETWORKS = 'SET_AVAILABLE_SOURCE_NETWORKS'; +export const SET_NICK_PROFILES = 'SET_NICK_PROFILES'; +export const SET_EXISTING_NET_MAPS = 'SET_EXISTING_NET_MAPS'; +export const START_CREATE = 'START_CREATE'; +export const SET_NET_MAP = 'SET_NET_MAP'; export type CreateVmMigration = | typeof SET_NAME @@ -16,7 +38,15 @@ export type CreateVmMigration = | typeof SET_TARGET_NAMESPACE | typeof SET_AVAILABLE_PROVIDERS | typeof SET_EXISTING_PLANS - | typeof SET_AVAILABLE_TARGET_NAMESPACES; + | typeof SET_AVAILABLE_TARGET_NAMESPACES + | typeof REPLACE_NETWORK_MAPPING + | typeof REPLACE_STORAGE_MAPPING + | typeof SET_AVAILABLE_TARGET_NETWORKS + | typeof SET_AVAILABLE_SOURCE_NETWORKS + | typeof SET_NICK_PROFILES + | typeof SET_EXISTING_NET_MAPS + | typeof START_CREATE + | typeof SET_NET_MAP; export interface PageAction { type: S; @@ -43,14 +73,54 @@ export interface PlanTargetNamespace { export interface PlanAvailableProviders { availableProviders: V1beta1Provider[]; + loading: boolean; + error?: Error; } export interface PlanExistingPlans { existingPlans: V1beta1Plan[]; + loading: boolean; + error?: Error; +} + +export interface PlanExistingNetMaps { + existingNetMaps: V1beta1NetworkMap[]; + loading: boolean; + error?: Error; } export interface PlanAvailableTargetNamespaces { availableTargetNamespaces: OpenShiftNamespace[]; + loading: boolean; + error?: Error; +} + +export interface PlanAvailableTargetNetworks { + availableTargetNetworks: OpenShiftNetworkAttachmentDefinition[]; + loading: boolean; + error?: Error; +} + +export interface PlanAvailableSourceNetworks { + availableSourceNetworks: InventoryNetwork[]; + loading: boolean; + error?: Error; +} + +export interface PlanNickProfiles { + nickProfiles: OVirtNicProfile[]; + loading: boolean; + error?: Error; +} + +export interface PlanCrateNetMap { + netMap?: V1beta1NetworkMap; + error?: Error; +} + +export interface PlanMapping { + current?: Mapping; + next?: Mapping; } // action creators @@ -85,25 +155,104 @@ export const setPlanName = (name: string): PageAction => ({ type: 'SET_AVAILABLE_PROVIDERS', payload: { - availableProviders, + availableProviders: Array.isArray(availableProviders) ? availableProviders : [], + loading: !loaded, + error, }, }); export const setExistingPlans = ( existingPlans: V1beta1Plan[], + loaded: boolean, + error: Error, ): PageAction => ({ type: 'SET_EXISTING_PLANS', payload: { - existingPlans, + existingPlans: Array.isArray(existingPlans) ? existingPlans : [], + loading: !loaded, + error, + }, +}); + +export const setExistingNetMaps = ( + existingNetMaps: V1beta1NetworkMap[], + loaded: boolean, + error: Error, +): PageAction => ({ + type: 'SET_EXISTING_NET_MAPS', + payload: { + existingNetMaps: Array.isArray(existingNetMaps) ? existingNetMaps : [], + loading: !loaded, + error, }, }); export const setAvailableTargetNamespaces = ( availableTargetNamespaces: OpenShiftNamespace[], + loading: boolean, + error?: Error, ): PageAction => ({ type: 'SET_AVAILABLE_TARGET_NAMESPACES', - payload: { availableTargetNamespaces }, + payload: { availableTargetNamespaces, loading, error }, +}); + +export const replaceStorageMapping = ({ + current, + next, +}: PlanMapping): PageAction => ({ + type: 'REPLACE_STORAGE_MAPPING', + payload: { current, next }, +}); + +export const replaceNetworkMapping = ({ + current, + next, +}: PlanMapping): PageAction => ({ + type: 'REPLACE_NETWORK_MAPPING', + payload: { current, next }, +}); + +export const setAvailableTargetNetworks = ( + availableTargetNetworks: OpenShiftNetworkAttachmentDefinition[], + loading: boolean, + error?: Error, +): PageAction => ({ + type: 'SET_AVAILABLE_TARGET_NETWORKS', + payload: { availableTargetNetworks, loading, error }, +}); + +export const setAvailableSourceNetworks = ( + availableSourceNetworks: InventoryNetwork[], + loading: boolean, + error?: Error, +): PageAction => ({ + type: 'SET_AVAILABLE_SOURCE_NETWORKS', + payload: { availableSourceNetworks, loading, error }, +}); + +export const setNicProfiles = ( + nickProfiles: OVirtNicProfile[], + nicProfilesLoading: boolean, + nicProfilesError: Error, +): PageAction => ({ + type: 'SET_NICK_PROFILES', + payload: { nickProfiles, loading: nicProfilesLoading, error: nicProfilesError }, +}); + +export const startCreate = (): PageAction => ({ + type: 'START_CREATE', + payload: {}, +}); + +export const setNetMap = ({ + netMap, + error, +}: PlanCrateNetMap): PageAction => ({ + type: 'SET_NET_MAP', + payload: { netMap, error }, }); diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/getNetworksUsedBySelectedVMs.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/getNetworksUsedBySelectedVMs.ts new file mode 100644 index 000000000..c4b6f7532 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/getNetworksUsedBySelectedVMs.ts @@ -0,0 +1,43 @@ +import { OVirtNicProfile } from '@kubev2v/types'; + +import { VmData } from '../details'; + +import { POD_NETWORK } from './actions'; + +// based on packages/legacy/src/Plans/components/Wizard/helpers.tsx +export const getNetworksUsedBySelectedVms = ( + selectedVMs: VmData[], + nicProfiles: OVirtNicProfile[], +): string[] => { + return Array.from( + new Set( + selectedVMs + ?.map(({ vm }) => vm) + .flatMap((vm) => { + switch (vm.providerType) { + case 'vsphere': { + return vm.networks?.map((network) => network?.id); + } + case 'openstack': { + return Object.keys(vm?.addresses ?? {}); + } + case 'ovirt': { + const vmNicProfiles = vm.nics?.map((nic) => + nicProfiles.find((nicProfile) => nicProfile?.id === nic?.profile), + ); + const networkIds = vmNicProfiles?.map((nicProfile) => nicProfile?.network); + return networkIds; + } + case 'openshift': { + return vm?.object?.spec?.template?.spec?.networks?.map((network) => + network?.pod ? POD_NETWORK : network?.multus?.networkName, + ); + } + default: + return []; + } + }) + .filter(Boolean), + ), + ); +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer.ts index 387451d57..078271be0 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/reducer.ts @@ -1,125 +1,194 @@ import { FC } from 'react'; import { Draft } from 'immer'; import { isProviderLocalOpenshift } from 'src/utils/resources'; -import { v4 as randomId } from 'uuid'; -import { DefaultRow, ResourceFieldFactory, RowProps, withTr } from '@kubev2v/common'; -import { OpenShiftNamespace, ProviderType, V1beta1Plan, V1beta1Provider } from '@kubev2v/types'; +import { ResourceFieldFactory, RowProps } from '@kubev2v/common'; +import { + OpenShiftNamespace, + OpenshiftResource, + OVirtNicProfile, + V1beta1NetworkMap, + V1beta1Plan, + V1beta1Provider, + V1beta1StorageMap, +} from '@kubev2v/types'; -import { getIsTarget, validateK8sName, Validation } from '../../utils'; -import { planTemplate } from '../create/templates'; -import { toId, VmData } from '../details'; -import { openShiftVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/OpenShiftVirtualMachinesList'; -import { OpenShiftVirtualMachinesCells } from '../details/tabs/VirtualMachines/OpenShiftVirtualMachinesRow'; -import { openStackVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/OpenStackVirtualMachinesList'; -import { OpenStackVirtualMachinesCells } from '../details/tabs/VirtualMachines/OpenStackVirtualMachinesRow'; -import { ovaVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/OvaVirtualMachinesList'; -import { OvaVirtualMachinesCells } from '../details/tabs/VirtualMachines/OvaVirtualMachinesRow'; -import { oVirtVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/OVirtVirtualMachinesList'; -import { OVirtVirtualMachinesCells } from '../details/tabs/VirtualMachines/OVirtVirtualMachinesRow'; -import { vSphereVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/VSphereVirtualMachinesList'; -import { VSphereVirtualMachinesCells } from '../details/tabs/VirtualMachines/VSphereVirtualMachinesRow'; +import { InventoryNetwork } from '../../hooks/useNetworks'; +import { getIsTarget, Validation } from '../../utils'; +import { VmData } from '../details'; import { CreateVmMigration, + DEFAULT_NAMESPACE, PageAction, PlanAvailableProviders, + PlanAvailableSourceNetworks, PlanAvailableTargetNamespaces, - PlanDescription, + PlanAvailableTargetNetworks, + PlanCrateNetMap, + PlanExistingNetMaps, PlanExistingPlans, PlanName, + PlanNickProfiles, PlanTargetNamespace, PlanTargetProvider, + POD_NETWORK, SET_AVAILABLE_PROVIDERS, + SET_AVAILABLE_SOURCE_NETWORKS, SET_AVAILABLE_TARGET_NAMESPACES, - SET_DESCRIPTION, + SET_AVAILABLE_TARGET_NETWORKS, + SET_EXISTING_NET_MAPS, SET_EXISTING_PLANS, SET_NAME, + SET_NET_MAP, + SET_NICK_PROFILES, SET_TARGET_NAMESPACE, SET_TARGET_PROVIDER, + START_CREATE, } from './actions'; +import { getNetworksUsedBySelectedVms } from './getNetworksUsedBySelectedVMs'; +import { Mapping } from './MappingList'; +import { + calculateNetworks, + generateName, + mapSourceNetworksToLabels, + setTargetNamespace, + setTargetProvider, + validatePlanName, + validateTargetNamespace, + validateUniqueName, +} from './stateHelpers'; export interface CreateVmMigrationPageState { - newPlan: V1beta1Plan; + underConstruction: { + plan: V1beta1Plan; + netMap: V1beta1NetworkMap; + storageMap: V1beta1StorageMap; + }; validationError: Error | null; apiError: Error | null; validation: { - name: Validation; + planName: Validation; targetNamespace: Validation; targetProvider: Validation; }; - availableProviders: V1beta1Provider[]; - selectedVms: VmData[]; - existingPlans: V1beta1Plan[]; - vmFieldsFactory: [ResourceFieldFactory, FC>]; - availableTargetNamespaces: OpenShiftNamespace[]; + // data fetched from k8s or inventory + existingResources: { + providers: V1beta1Provider[]; + plans: V1beta1Plan[]; + targetNamespaces: OpenShiftNamespace[]; + targetNetworks: OpenshiftResource[]; + sourceNetworks: InventoryNetwork[]; + targetStorages: unknown[]; + nickProfiles: OVirtNicProfile[]; + netMaps: V1beta1NetworkMap[]; + createdNetMap?: V1beta1NetworkMap; + }; + calculatedOnce: { + // calculated on start (exception:for ovirt/openstack we need to fetch disks) + storagesUsedBySelectedVms: string[]; + // calculated on start (exception:for ovirt we need to fetch nic profiles) + networkIdsUsedBySelectedVms: string[]; + sourceNetworkLabelToId: { [label: string]: string }; + // calculated on start + vmFieldsFactory: [ResourceFieldFactory, FC>]; + }; + // re-calculated on every target namespace change + calculatedPerNamespace: { + // read-only + targetStorages: string[]; + // read-only, human-readable + targetNetworks: string[]; + targetNetworkLabelToId: { [label: string]: string }; + sourceNetworks: { + // read-only + label: string; + usedBySelectedVms: boolean; + // mutated via UI + isMapped: boolean; + }[]; + sourceStorages: string[]; + // mutated, both source and destination human-readable + networkMappings: Mapping[]; + storageMappings: Mapping[]; + }; + receivedAsParams: { + selectedVms: VmData[]; + sourceProvider: V1beta1Provider; + namespace: string; + }; + // placeholder for helper data + workArea: { + targetProvider: V1beta1Provider; + }; + flow: { + editingDone: boolean; + netMapCreated: boolean; + storageMapCreated: boolean; + }; } -const validateUniqueName = (name: string, existingPlanNames: string[]) => - existingPlanNames.every((existingName) => existingName !== name); - -const validatePlanName = (name: string, existingPlans: V1beta1Plan[]) => - validateK8sName(name) && - validateUniqueName( - name, - existingPlans.map((plan) => plan?.metadata?.name ?? ''), - ) - ? 'success' - : 'error'; - -const validateTargetNamespace = (namespace: string, availableNamespaces: OpenShiftNamespace[]) => - validateK8sName(namespace) && availableNamespaces?.find((n) => n.name === namespace) - ? 'success' - : 'error'; - const actions: { [name: string]: ( draft: Draft, action: PageAction, - ) => CreateVmMigrationPageState; + ) => CreateVmMigrationPageState | void; } = { [SET_NAME](draft, { payload: { name } }: PageAction) { - draft.newPlan.metadata.name = name; - draft.validation.name = validatePlanName(name, draft.existingPlans); - return draft; - }, - [SET_DESCRIPTION]( - draft, - { payload: { description } }: PageAction, - ) { - draft.newPlan.spec.description = description; + if (draft.flow.editingDone) { + return; + } + draft.underConstruction.plan.metadata.name = name; + draft.validation.planName = validatePlanName(name, draft.existingResources.plans); return draft; }, [SET_TARGET_NAMESPACE]( draft, { payload: { targetNamespace } }: PageAction, ) { - draft.newPlan.spec.targetNamespace = targetNamespace; - draft.validation.targetNamespace = validateTargetNamespace( - targetNamespace, - draft.availableTargetNamespaces, - ); + if (!draft.flow.editingDone) { + setTargetNamespace(draft, targetNamespace); + } return draft; }, [SET_TARGET_PROVIDER]( draft, { payload: { targetProviderName } }: PageAction, ) { - setTargetProvider(draft, targetProviderName, draft.availableProviders); + const { + underConstruction: { plan }, + existingResources, + flow: { editingDone }, + } = draft; + // avoid side effects if no real change + if (!editingDone && plan.spec.provider?.destination?.name !== targetProviderName) { + setTargetProvider(draft, targetProviderName, existingResources.providers); + } return draft; }, [SET_AVAILABLE_PROVIDERS]( draft, - { payload: { availableProviders } }: PageAction, + { + payload: { availableProviders, loading, error }, + }: PageAction, ) { - draft.availableProviders = availableProviders; - // set the default provider if none is set - // reset the provider if provider was removed + if (loading || error || draft.flow.editingDone) { + return draft; + } + console.warn(SET_AVAILABLE_PROVIDERS, availableProviders); + draft.existingResources.providers = availableProviders; if ( !availableProviders .filter(getIsTarget) - .find((p) => p?.metadata?.name === draft.newPlan.spec.provider.destination?.name) + .find( + (p) => p?.metadata?.name === draft.underConstruction.plan.spec.provider.destination?.name, + ) ) { + // the current provider is missing in the list of available providers + // possible cases: + // 1. no provider set (yet) + // 2. provider got removed in the meantime + // 3. no host provider in the namespace const firstHostProvider = availableProviders.find((p) => isProviderLocalOpenshift(p)); setTargetProvider(draft, firstHostProvider?.metadata?.name, availableProviders); } @@ -127,43 +196,163 @@ const actions: { }, [SET_EXISTING_PLANS]( draft, - { payload: { existingPlans } }: PageAction, + { + payload: { existingPlans, loading, error }, + }: PageAction, ) { - draft.existingPlans = existingPlans; - draft.validation.name = validatePlanName(draft.newPlan.metadata.name, existingPlans); + if (loading || error || draft.flow.editingDone) { + return draft; + } + console.warn(SET_EXISTING_PLANS, existingPlans); + draft.existingResources.plans = existingPlans; + draft.validation.planName = validatePlanName( + draft.underConstruction.plan.metadata.name, + existingPlans, + ); return draft; }, [SET_AVAILABLE_TARGET_NAMESPACES]( draft, { - payload: { availableTargetNamespaces }, + payload: { availableTargetNamespaces, loading, error }, }: PageAction, ) { - draft.availableTargetNamespaces = availableTargetNamespaces; - draft.validation.targetNamespace = validateTargetNamespace( - draft.newPlan.spec.targetNamespace, + const { + existingResources, + validation, + underConstruction: { plan }, + workArea: { targetProvider }, + flow: { editingDone }, + } = draft; + if (loading || error || editingDone) { + return; + } + console.warn(SET_AVAILABLE_TARGET_NAMESPACES, availableTargetNamespaces); + existingResources.targetNamespaces = availableTargetNamespaces; + + validation.targetNamespace = validateTargetNamespace( + plan.spec.targetNamespace, availableTargetNamespaces, ); + if (validation.targetNamespace === 'success') { + return draft; + } + + const targetNamespace = + // use the current namespace (inherited from source provider) + (isProviderLocalOpenshift(targetProvider) && plan.metadata.namespace) || + // use 'default' if exists + (availableTargetNamespaces.find((n) => n.name === DEFAULT_NAMESPACE) && DEFAULT_NAMESPACE) || + // use the first from the list (if exists) + availableTargetNamespaces[0]?.name; + + setTargetNamespace(draft, targetNamespace); return draft; }, -}; + [SET_AVAILABLE_TARGET_NETWORKS]( + draft, + { + payload: { availableTargetNetworks, loading, error }, + }: PageAction, + ) { + if (loading || error || draft.flow.editingDone) { + return draft; + } + console.warn(SET_AVAILABLE_TARGET_NETWORKS, availableTargetNetworks); + draft.existingResources.targetNetworks = availableTargetNetworks; -const setTargetProvider = ( - draft: Draft, - targetProviderName: string, - availableProviders: V1beta1Provider[], -) => { - // there might be no target provider in the namespace - const resolvedTarget = availableProviders - .filter(getIsTarget) - .find((p) => p?.metadata?.name === targetProviderName); - draft.newPlan.spec.provider.destination = resolvedTarget && getObjectRef(resolvedTarget); - draft.newPlan.spec.targetNamespace = isProviderLocalOpenshift(resolvedTarget) - ? draft.newPlan.metadata.namespace - : 'default'; - // assume the value is correct and wait until the namespaces will be loaded for further validation - draft.validation.targetNamespace = 'default'; - draft.validation.targetProvider = resolvedTarget ? 'success' : 'error'; + draft.calculatedPerNamespace = { + ...draft.calculatedPerNamespace, + ...calculateNetworks(draft), + }; + return draft; + }, + [SET_AVAILABLE_SOURCE_NETWORKS]( + draft, + { + payload: { availableSourceNetworks, loading, error }, + }: PageAction, + ) { + if (loading || error || draft.flow.editingDone) { + return draft; + } + console.warn(SET_AVAILABLE_SOURCE_NETWORKS, availableSourceNetworks); + draft.existingResources.sourceNetworks = availableSourceNetworks; + draft.calculatedOnce.sourceNetworkLabelToId = + mapSourceNetworksToLabels(availableSourceNetworks); + draft.calculatedPerNamespace = { + ...draft.calculatedPerNamespace, + ...calculateNetworks(draft), + }; + return draft; + }, + [SET_NICK_PROFILES]( + draft, + { payload: { nickProfiles, loading, error } }: PageAction, + ) { + const { + existingResources, + calculatedOnce, + receivedAsParams: { selectedVms }, + flow: { editingDone }, + } = draft; + if (loading || error || editingDone) { + return; + } + console.warn(SET_NICK_PROFILES, nickProfiles); + existingResources.nickProfiles = nickProfiles; + calculatedOnce.networkIdsUsedBySelectedVms = getNetworksUsedBySelectedVms( + selectedVms, + nickProfiles, + ); + draft.calculatedPerNamespace = { + ...draft.calculatedPerNamespace, + ...calculateNetworks(draft), + }; + }, + [SET_EXISTING_NET_MAPS]( + { + existingResources, + underConstruction: { netMap }, + receivedAsParams: { sourceProvider }, + flow: { editingDone }, + }, + { + payload: { existingNetMaps, loading, error }, + }: PageAction, + ) { + if (loading || error || editingDone) { + return; + } + console.warn(SET_EXISTING_NET_MAPS, existingNetMaps); + existingResources.netMaps = existingNetMaps; + const names = existingNetMaps.map((n) => n.metadata?.name).filter(Boolean); + while (!validateUniqueName(netMap.metadata.name, names)) { + netMap.metadata.name = generateName(sourceProvider.metadata.name); + } + }, + [START_CREATE]({ + flow, + underConstruction: { plan, netMap }, + calculatedOnce: { sourceNetworkLabelToId }, + calculatedPerNamespace: { networkMappings }, + }) { + console.warn(START_CREATE); + flow.editingDone = true; + netMap.spec.map = networkMappings.map(({ source, destination }) => ({ + source: { + id: sourceNetworkLabelToId[source], + }, + destination: + destination === POD_NETWORK + ? { type: 'pod' } + : { name: destination, namespace: plan.spec.targetNamespace, type: 'multus' }, + })); + }, + [SET_NET_MAP](draft, { payload: { netMap } }: PageAction) { + draft.existingResources.createdNetMap = netMap; + draft.flow.netMapCreated = true; + }, }; export const reducer = ( @@ -172,79 +361,3 @@ export const reducer = ( ) => { return actions?.[action?.type]?.(draft, action) ?? draft; }; - -// based on the method used in legacy/src/common/helpers -// and mocks/src/definitions/utils -export const getObjectRef = ( - { apiVersion, kind, metadata: { name, namespace, uid } = {} }: V1beta1Provider = { - apiVersion: undefined, - kind: undefined, - }, -) => ({ - apiVersion, - kind, - name, - namespace, - uid, -}); - -export const createInitialState = ({ - namespace, - sourceProvider, - selectedVms, -}: { - namespace: string; - sourceProvider: V1beta1Provider; - selectedVms: VmData[]; -}): CreateVmMigrationPageState => ({ - newPlan: { - ...planTemplate, - metadata: { - ...planTemplate?.metadata, - name: sourceProvider?.metadata?.name - ? `${sourceProvider?.metadata?.name}-${randomId().substring(0, 8)}` - : undefined, - namespace, - }, - spec: { - ...planTemplate?.spec, - provider: { - source: getObjectRef(sourceProvider), - destination: undefined, - }, - targetNamespace: undefined, - vms: selectedVms.map((data) => ({ name: data.name, id: toId(data) })), - }, - }, - validationError: null, - apiError: null, - availableProviders: [], - selectedVms, - existingPlans: [], - validation: { - name: 'default', - targetNamespace: 'default', - targetProvider: 'default', - }, - vmFieldsFactory: resourceFieldsForType(sourceProvider?.spec?.type as ProviderType), - availableTargetNamespaces: [], -}); - -export const resourceFieldsForType = ( - type: ProviderType, -): [ResourceFieldFactory, FC>] => { - switch (type) { - case 'openshift': - return [openShiftVmFieldsMetadataFactory, withTr(OpenShiftVirtualMachinesCells)]; - case 'openstack': - return [openStackVmFieldsMetadataFactory, withTr(OpenStackVirtualMachinesCells)]; - case 'ova': - return [ovaVmFieldsMetadataFactory, withTr(OvaVirtualMachinesCells)]; - case 'ovirt': - return [oVirtVmFieldsMetadataFactory, withTr(OVirtVirtualMachinesCells)]; - case 'vsphere': - return [vSphereVmFieldsMetadataFactory, withTr(VSphereVirtualMachinesCells)]; - default: - return [() => [], DefaultRow]; - } -}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/stateHelpers.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/stateHelpers.ts new file mode 100644 index 000000000..71d5f8858 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/stateHelpers.ts @@ -0,0 +1,368 @@ +import { FC } from 'react'; +import { Draft } from 'immer'; +import { v4 as randomId } from 'uuid'; + +import { + DefaultRow, + ResourceFieldFactory, + RowProps, + universalComparator, + withTr, +} from '@kubev2v/common'; +import { + OpenShiftNamespace, + ProviderModelGroupVersionKind as ProviderGVK, + ProviderType, + V1beta1Plan, + V1beta1Provider, +} from '@kubev2v/types'; + +import { InventoryNetwork } from '../../hooks/useNetworks'; +import { getIsTarget, validateK8sName } from '../../utils'; +import { networkMapTemplate, planTemplate, storageMapTemplate } from '../create/templates'; +import { toId, VmData } from '../details'; +import { openShiftVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/OpenShiftVirtualMachinesList'; +import { OpenShiftVirtualMachinesCells } from '../details/tabs/VirtualMachines/OpenShiftVirtualMachinesRow'; +import { openStackVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/OpenStackVirtualMachinesList'; +import { OpenStackVirtualMachinesCells } from '../details/tabs/VirtualMachines/OpenStackVirtualMachinesRow'; +import { ovaVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/OvaVirtualMachinesList'; +import { OvaVirtualMachinesCells } from '../details/tabs/VirtualMachines/OvaVirtualMachinesRow'; +import { oVirtVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/OVirtVirtualMachinesList'; +import { OVirtVirtualMachinesCells } from '../details/tabs/VirtualMachines/OVirtVirtualMachinesRow'; +import { vSphereVmFieldsMetadataFactory } from '../details/tabs/VirtualMachines/VSphereVirtualMachinesList'; +import { VSphereVirtualMachinesCells } from '../details/tabs/VirtualMachines/VSphereVirtualMachinesRow'; + +import { POD_NETWORK } from './actions'; +import { getNetworksUsedBySelectedVms } from './getNetworksUsedBySelectedVMs'; +import { CreateVmMigrationPageState } from './reducer'; + +export const validateUniqueName = (name: string, existingNames: string[]) => + existingNames.every((existingName) => existingName !== name); + +export const validatePlanName = (name: string, existingPlans: V1beta1Plan[]) => + validateK8sName(name) && + validateUniqueName( + name, + existingPlans.map((plan) => plan?.metadata?.name ?? ''), + ) + ? 'success' + : 'error'; + +export const validateTargetNamespace = ( + namespace: string, + availableNamespaces: OpenShiftNamespace[], +) => + validateK8sName(namespace) && availableNamespaces?.find((n) => n.name === namespace) + ? 'success' + : 'error'; + +export const calculateNetworks = ( + draft: Draft, +): Partial => { + const { + calculatedPerNamespace: { networkMappings }, + existingResources, + underConstruction: { plan }, + calculatedOnce: { sourceNetworkLabelToId, networkIdsUsedBySelectedVms }, + } = draft; + + const targetNetworkNameToUid = Object.fromEntries( + existingResources.targetNetworks + .filter(({ namespace }) => namespace === plan.spec.targetNamespace) + .map((net) => [net.name, net.uid]), + ); + const targetNetworkLabels = [ + POD_NETWORK, + ...Object.keys(targetNetworkNameToUid).sort((a, b) => universalComparator(a, b, 'en')), + ]; + const defaultDestination = POD_NETWORK; + + const validMappings = networkMappings.filter( + ({ source, destination }) => + (targetNetworkNameToUid[destination] || destination === POD_NETWORK) && + sourceNetworkLabelToId[source], + ); + + const sourceNetworks = Object.keys(sourceNetworkLabelToId) + .sort((a, b) => universalComparator(a, b, 'en')) + .map((label) => ({ + label, + isMapped: validMappings.some(({ source }) => source === label), + usedBySelectedVms: networkIdsUsedBySelectedVms.some( + (id) => id === sourceNetworkLabelToId[label], + ), + })); + + return { + targetNetworks: targetNetworkLabels, + sourceNetworks, + networkMappings: networkMappings.length + ? validMappings + : sourceNetworks + .filter(({ usedBySelectedVms }) => usedBySelectedVms) + .map(({ label }) => ({ + source: label, + destination: defaultDestination, + })), + }; +}; + +export const calculateStorages = ( + draft: Draft, +): Partial => ({}); + +export const setTargetProvider = ( + draft: Draft, + targetProviderName: string, + availableProviders: V1beta1Provider[], +) => { + const { + existingResources, + validation, + underConstruction: { plan, netMap }, + workArea, + } = draft; + + // reset props that depend on the target provider + plan.spec.targetNamespace = undefined; + // temporarily assume no namespace is OK - the validation will continue when new namespaces are loaded + validation.targetNamespace = 'default'; + existingResources.targetNamespaces = []; + existingResources.targetNetworks = []; + existingResources.targetStorages = []; + draft.calculatedPerNamespace = initCalculatedPerNamespaceSlice(); + + // there might be no target provider in the namespace + const resolvedTarget = resolveTargetProvider(targetProviderName, availableProviders); + validation.targetProvider = resolvedTarget ? 'success' : 'error'; + plan.spec.provider.destination = resolvedTarget && getObjectRef(resolvedTarget); + netMap.spec.provider.destination = resolvedTarget && getObjectRef(resolvedTarget); + workArea.targetProvider = resolvedTarget; +}; + +export const setTargetNamespace = ( + draft: Draft, + targetNamespace: string, +): void => { + const { + underConstruction: { plan }, + } = draft; + + plan.spec.targetNamespace = targetNamespace; + draft.validation.targetNamespace = validateTargetNamespace( + targetNamespace, + draft.existingResources.targetNamespaces, + ); + + draft.calculatedPerNamespace = initCalculatedPerNamespaceSlice(); + draft.calculatedPerNamespace = { + ...draft.calculatedPerNamespace, + ...calculateNetworks(draft), + ...calculateStorages(draft), + }; +}; + +export const initCalculatedPerNamespaceSlice = + (): CreateVmMigrationPageState['calculatedPerNamespace'] => ({ + targetNetworks: [], + targetStorages: [], + networkMappings: [], + storageMappings: [], + sourceStorages: [], + sourceNetworks: [], + targetNetworkLabelToId: {}, + }); + +export const resolveTargetProvider = (name: string, availableProviders: V1beta1Provider[]) => + availableProviders.filter(getIsTarget).find((p) => p?.metadata?.name === name); + +// based on the method used in legacy/src/common/helpers +// and mocks/src/definitions/utils +export const getObjectRef = ( + { apiVersion, kind, metadata: { name, namespace, uid } = {} }: V1beta1Provider = { + apiVersion: undefined, + kind: undefined, + }, +) => ({ + apiVersion, + kind, + name, + namespace, + uid, +}); + +export const createInitialState = ({ + namespace, + sourceProvider = { + metadata: { name: 'unknown', namespace: 'unknown' }, + apiVersion: `${ProviderGVK.group}/${ProviderGVK.version}`, + kind: ProviderGVK.kind, + }, + selectedVms = [], +}: { + namespace: string; + sourceProvider: V1beta1Provider; + selectedVms: VmData[]; +}): CreateVmMigrationPageState => ({ + underConstruction: { + plan: { + ...planTemplate, + metadata: { + ...planTemplate?.metadata, + name: generateName(sourceProvider.metadata.name), + namespace, + }, + spec: { + ...planTemplate?.spec, + provider: { + source: getObjectRef(sourceProvider), + destination: undefined, + }, + targetNamespace: undefined, + vms: selectedVms.map((data) => ({ name: data.name, id: toId(data) })), + }, + }, + netMap: { + ...networkMapTemplate, + metadata: { + ...networkMapTemplate?.metadata, + name: generateName(sourceProvider.metadata.name), + namespace, + }, + spec: { + ...networkMapTemplate?.spec, + provider: { + source: getObjectRef(sourceProvider), + destination: undefined, + }, + }, + }, + storageMap: { + ...storageMapTemplate, + metadata: { + ...storageMapTemplate?.metadata, + name: generateName(sourceProvider.metadata.name), + namespace, + }, + }, + }, + validationError: null, + apiError: null, + existingResources: { + plans: [], + providers: [], + targetNamespaces: [], + targetNetworks: [], + sourceNetworks: [], + targetStorages: [], + nickProfiles: [], + netMaps: [], + createdNetMap: undefined, + }, + receivedAsParams: { + selectedVms, + sourceProvider, + namespace, + }, + validation: { + planName: 'default', + targetNamespace: 'default', + targetProvider: 'default', + }, + calculatedOnce: { + vmFieldsFactory: resourceFieldsForType(sourceProvider?.spec?.type as ProviderType), + networkIdsUsedBySelectedVms: + sourceProvider.spec?.type !== 'ovirt' ? getNetworksUsedBySelectedVms(selectedVms, []) : [], + sourceNetworkLabelToId: {}, + storagesUsedBySelectedVms: [], + // storagesUsedBySelectedVms: ['ovirt', 'openstack'].includes(sourceProvider.spec?.type) ? [] : [], + }, + calculatedPerNamespace: { + targetNetworks: [], + targetNetworkLabelToId: {}, + targetStorages: [], + sourceNetworks: [], + networkMappings: [], + sourceStorages: [], + storageMappings: [], + }, + workArea: { + targetProvider: undefined, + }, + flow: { + editingDone: false, + netMapCreated: false, + storageMapCreated: false, + }, +}); + +export const generateName = (base: string) => `${base}-${randomId().substring(0, 8)}`; + +export const resourceFieldsForType = ( + type: ProviderType, +): [ResourceFieldFactory, FC>] => { + switch (type) { + case 'openshift': + return [openShiftVmFieldsMetadataFactory, withTr(OpenShiftVirtualMachinesCells)]; + case 'openstack': + return [openStackVmFieldsMetadataFactory, withTr(OpenStackVirtualMachinesCells)]; + case 'ova': + return [ovaVmFieldsMetadataFactory, withTr(OvaVirtualMachinesCells)]; + case 'ovirt': + return [oVirtVmFieldsMetadataFactory, withTr(OVirtVirtualMachinesCells)]; + case 'vsphere': + return [vSphereVmFieldsMetadataFactory, withTr(VSphereVirtualMachinesCells)]; + default: + return [() => [], DefaultRow]; + } +}; + +export const mapSourceNetworksToLabels = ( + sources: InventoryNetwork[], +): { [label: string]: string } => { + const tuples = sources + .map((net) => { + switch (net.providerType) { + case 'openshift': { + return [`${net.namespace}/${net.name}`, net.uid]; + } + case 'openstack': { + return [net.name, net.id]; + } + case 'ovirt': { + return [net.path, net.id]; + } + case 'vsphere': { + return [net.name, net.id]; + } + default: { + return undefined; + } + } + }) + .filter(Boolean); + const labelToId: { [label: string]: string } = tuples.reduce((acc, [label, id]) => { + if (acc[label] && acc[label] === id) { + //already included + return acc; + } else if (acc[label]) { + // resolve conflict + return { + ...acc, + // existing entry: add suffix with ID + [label]: undefined, + [`${label} (ID: ${acc[label]})`]: acc[label], + // new entry: create with suffix + [`${label} (ID: ${id})`]: id, + }; + } else { + // happy path + return { + ...acc, + [label]: id, + }; + } + }, {}); + + return labelToId; +}; diff --git a/packages/types/src/types/k8s/V1VirtualMachine.ts b/packages/types/src/types/k8s/V1VirtualMachine.ts index 66e8b5233..68ae44e8e 100644 --- a/packages/types/src/types/k8s/V1VirtualMachine.ts +++ b/packages/types/src/types/k8s/V1VirtualMachine.ts @@ -241,7 +241,18 @@ interface V1VirtualMachineInstanceSpec { // Defaults to Pod, if no type is specified. // NetworkSource `json:",inline"` pod?: object; - multus?: object; + multus?: { + // References to a NetworkAttachmentDefinition CRD object. Format: + // , /. If namespace is not + // specified, VMI namespace is assumed. + // NetworkName string `json:"networkName"` + networkName: string; + + // Select the default network and add it to the + // multus-cni.io/default-network annotation. + // Default bool `json:"default,omitempty"` + default?: boolean; + }; }[]; // Set DNS policy for the pod.