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 index 300ebe662..ed7dd98ef 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/MappingList.tsx +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/MappingList.tsx @@ -35,7 +35,7 @@ interface MappingListProps { availableDestinations: string[]; replaceMapping: (val: { current: Mapping; next: Mapping }) => void; deleteMapping: (mapping: Mapping) => void; - addMapping: (mapping: Mapping) => void; + addMapping: () => void; usedSourcesLabel: string; generalSourcesLabel: string; noSourcesLabel: string; @@ -80,13 +80,7 @@ export const MappingList: FC = ({ ))} 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 2f8b0dfe8..edc56bc46 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 @@ -24,12 +24,14 @@ 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 ADD_NETWORK_MAPPING = 'ADD_NETWORK_MAPPING'; +export const DELETE_NETWORK_MAPPING = 'DELETE_NETWORK_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 const SET_ERROR = 'SET_ERROR'; export type CreateVmMigration = | typeof SET_NAME @@ -40,13 +42,15 @@ export type CreateVmMigration = | typeof SET_EXISTING_PLANS | typeof SET_AVAILABLE_TARGET_NAMESPACES | typeof REPLACE_NETWORK_MAPPING + | typeof ADD_NETWORK_MAPPING + | typeof DELETE_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; + | typeof SET_ERROR; export interface PageAction { type: S; @@ -113,14 +117,13 @@ export interface PlanNickProfiles { error?: Error; } -export interface PlanCrateNetMap { - netMap?: V1beta1NetworkMap; - error?: Error; +export interface PlanError { + error: Error; } export interface PlanMapping { - current?: Mapping; - next?: Mapping; + current: Mapping; + next: Mapping; } // action creators @@ -209,6 +212,11 @@ export const replaceStorageMapping = ({ payload: { current, next }, }); +export const addNetworkMapping = (): PageAction => ({ + type: 'ADD_NETWORK_MAPPING', + payload: {}, +}); + export const replaceNetworkMapping = ({ current, next, @@ -217,6 +225,14 @@ export const replaceNetworkMapping = ({ payload: { current, next }, }); +export const deleteNetworkMapping = ({ + source, + destination, +}: Mapping): PageAction => ({ + type: 'DELETE_NETWORK_MAPPING', + payload: { source, destination }, +}); + export const setAvailableTargetNetworks = ( availableTargetNetworks: OpenShiftNetworkAttachmentDefinition[], loading: boolean, @@ -249,10 +265,7 @@ export const startCreate = (): PageAction => ({ payload: {}, }); -export const setNetMap = ({ - netMap, - error, -}: PlanCrateNetMap): PageAction => ({ - type: 'SET_NET_MAP', - payload: { netMap, error }, +export const setError = (error: Error): PageAction => ({ + type: 'SET_ERROR', + payload: { error }, }); 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 078271be0..02a68d64f 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 @@ -18,29 +18,33 @@ import { getIsTarget, Validation } from '../../utils'; import { VmData } from '../details'; import { + ADD_NETWORK_MAPPING, CreateVmMigration, DEFAULT_NAMESPACE, + DELETE_NETWORK_MAPPING, PageAction, PlanAvailableProviders, PlanAvailableSourceNetworks, PlanAvailableTargetNamespaces, PlanAvailableTargetNetworks, - PlanCrateNetMap, + PlanError, PlanExistingNetMaps, PlanExistingPlans, + PlanMapping, PlanName, PlanNickProfiles, PlanTargetNamespace, PlanTargetProvider, POD_NETWORK, + REPLACE_NETWORK_MAPPING, SET_AVAILABLE_PROVIDERS, SET_AVAILABLE_SOURCE_NETWORKS, SET_AVAILABLE_TARGET_NAMESPACES, SET_AVAILABLE_TARGET_NETWORKS, + SET_ERROR, SET_EXISTING_NET_MAPS, SET_EXISTING_PLANS, SET_NAME, - SET_NET_MAP, SET_NICK_PROFILES, SET_TARGET_NAMESPACE, SET_TARGET_PROVIDER, @@ -65,8 +69,7 @@ export interface CreateVmMigrationPageState { netMap: V1beta1NetworkMap; storageMap: V1beta1StorageMap; }; - validationError: Error | null; - apiError: Error | null; + validation: { planName: Validation; targetNamespace: Validation; @@ -83,6 +86,8 @@ export interface CreateVmMigrationPageState { nickProfiles: OVirtNicProfile[]; netMaps: V1beta1NetworkMap[]; createdNetMap?: V1beta1NetworkMap; + createdStorageMap?: V1beta1StorageMap; + createdPlan?: V1beta1Plan; }; calculatedOnce: { // calculated on start (exception:for ovirt/openstack we need to fetch disks) @@ -123,8 +128,8 @@ export interface CreateVmMigrationPageState { }; flow: { editingDone: boolean; - netMapCreated: boolean; - storageMapCreated: boolean; + validationError: Error | null; + apiError?: Error; }; } @@ -333,7 +338,7 @@ const actions: { }, [START_CREATE]({ flow, - underConstruction: { plan, netMap }, + underConstruction: { plan, netMap, storageMap }, calculatedOnce: { sourceNetworkLabelToId }, calculatedPerNamespace: { networkMappings }, }) { @@ -348,10 +353,79 @@ const actions: { ? { type: 'pod' } : { name: destination, namespace: plan.spec.targetNamespace, type: 'multus' }, })); + storageMap.spec.map = []; + }, + [SET_ERROR]({ flow }, { payload: { error } }: PageAction) { + console.warn(SET_ERROR); + flow.apiError = error; + }, + [ADD_NETWORK_MAPPING]({ calculatedPerNamespace: cpn }) { + const firstUsedByVms = cpn.sourceNetworks.find( + ({ usedBySelectedVms, isMapped }) => usedBySelectedVms && !isMapped, + ); + const firstGeneral = cpn.sourceNetworks.find( + ({ usedBySelectedVms, isMapped }) => !usedBySelectedVms && !isMapped, + ); + const nextSource = firstUsedByVms || firstGeneral; + const nextDest = cpn.targetNetworks[0]; + + console.warn(ADD_NETWORK_MAPPING, nextSource, nextDest); + if (nextDest && nextSource) { + cpn.sourceNetworks = cpn.sourceNetworks.map((m) => ({ + ...m, + isMapped: m.label === nextSource.label ? true : m.isMapped, + })); + cpn.networkMappings = [ + ...cpn.networkMappings, + { source: nextSource.label, destination: cpn.targetNetworks[0] }, + ]; + } }, - [SET_NET_MAP](draft, { payload: { netMap } }: PageAction) { - draft.existingResources.createdNetMap = netMap; - draft.flow.netMapCreated = true; + [DELETE_NETWORK_MAPPING]( + { calculatedPerNamespace: cpn }, + { payload: { source } }: PageAction, + ) { + const currentSource = cpn.sourceNetworks.find( + ({ label, isMapped }) => label === source && isMapped, + ); + console.warn(DELETE_NETWORK_MAPPING, source, currentSource); + if (currentSource) { + cpn.sourceNetworks = cpn.sourceNetworks.map((m) => ({ + ...m, + isMapped: m.label === source ? false : m.isMapped, + })); + cpn.networkMappings = cpn.networkMappings.filter( + ({ source }) => source !== currentSource.label, + ); + } + }, + [REPLACE_NETWORK_MAPPING]( + { calculatedPerNamespace: cpn }, + { payload: { current, next } }: PageAction, + ) { + console.warn(REPLACE_NETWORK_MAPPING, current, next); + const currentSource = cpn.sourceNetworks.find( + ({ label, isMapped }) => label === current.source && isMapped, + ); + const nextSource = cpn.sourceNetworks.find(({ label }) => label === next.source); + const nextDest = cpn.targetNetworks.find((label) => label === next.destination); + const sourceChanged = currentSource.label !== nextSource.label; + const destinationChanged = current.destination !== nextDest; + + if (!currentSource || !nextSource || !nextDest || (!sourceChanged && !destinationChanged)) { + return; + } + + if (sourceChanged) { + const labelToMappingState = { [currentSource.label]: false, [nextSource.label]: true }; + cpn.sourceNetworks = cpn.sourceNetworks.map((m) => ({ + ...m, + isMapped: labelToMappingState[m.label] ?? m.isMapped, + })); + } + + const mappingIndex = cpn.networkMappings.findIndex(({ source }) => source === current.source); + mappingIndex > -1 && cpn.networkMappings.splice(mappingIndex, 1, next); }, }; 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 index 71d5f8858..77e86d3eb 100644 --- a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/stateHelpers.ts +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/stateHelpers.ts @@ -10,6 +10,7 @@ import { withTr, } from '@kubev2v/common'; import { + IoK8sApimachineryPkgApisMetaV1ObjectMeta, OpenShiftNamespace, ProviderModelGroupVersionKind as ProviderGVK, ProviderType, @@ -60,7 +61,6 @@ export const calculateNetworks = ( draft: Draft, ): Partial => { const { - calculatedPerNamespace: { networkMappings }, existingResources, underConstruction: { plan }, calculatedOnce: { sourceNetworkLabelToId, networkIdsUsedBySelectedVms }, @@ -77,33 +77,28 @@ export const calculateNetworks = ( ]; 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( + .map((label) => { + const usedBySelectedVms = networkIdsUsedBySelectedVms.some( (id) => id === sourceNetworkLabelToId[label], - ), - })); + ); + return { + label, + usedBySelectedVms, + isMapped: usedBySelectedVms, + }; + }); return { targetNetworks: targetNetworkLabels, sourceNetworks, - networkMappings: networkMappings.length - ? validMappings - : sourceNetworks - .filter(({ usedBySelectedVms }) => usedBySelectedVms) - .map(({ label }) => ({ - source: label, - destination: defaultDestination, - })), + networkMappings: sourceNetworks + .filter(({ usedBySelectedVms }) => usedBySelectedVms) + .map(({ label }) => ({ + source: label, + destination: defaultDestination, + })), }; }; @@ -119,7 +114,7 @@ export const setTargetProvider = ( const { existingResources, validation, - underConstruction: { plan, netMap }, + underConstruction: { plan, netMap, storageMap }, workArea, } = draft; @@ -137,6 +132,7 @@ export const setTargetProvider = ( validation.targetProvider = resolvedTarget ? 'success' : 'error'; plan.spec.provider.destination = resolvedTarget && getObjectRef(resolvedTarget); netMap.spec.provider.destination = resolvedTarget && getObjectRef(resolvedTarget); + storageMap.spec.provider.destination = resolvedTarget && getObjectRef(resolvedTarget); workArea.targetProvider = resolvedTarget; }; @@ -179,7 +175,15 @@ export const resolveTargetProvider = (name: string, availableProviders: V1beta1P // 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, + kind, + metadata: { name, namespace, uid } = {}, + }: { + apiVersion: string; + kind: string; + metadata?: IoK8sApimachineryPkgApisMetaV1ObjectMeta; + } = { apiVersion: undefined, kind: undefined, }, @@ -244,10 +248,16 @@ export const createInitialState = ({ name: generateName(sourceProvider.metadata.name), namespace, }, + spec: { + ...storageMapTemplate?.spec, + provider: { + source: getObjectRef(sourceProvider), + destination: undefined, + }, + }, }, }, - validationError: null, - apiError: null, + existingResources: { plans: [], providers: [], @@ -291,8 +301,8 @@ export const createInitialState = ({ }, flow: { editingDone: false, - netMapCreated: false, - storageMapCreated: false, + validationError: undefined, + apiError: undefined, }, }); @@ -342,8 +352,11 @@ export const mapSourceNetworksToLabels = ( }) .filter(Boolean); const labelToId: { [label: string]: string } = tuples.reduce((acc, [label, id]) => { - if (acc[label] && acc[label] === id) { - //already included + if (acc[label] === id) { + //already included - no collisions + return acc; + } else if (acc[withSuffix(label, id)] === id) { + //already included with suffix - there was a collision before return acc; } else if (acc[label]) { // resolve conflict @@ -351,9 +364,9 @@ export const mapSourceNetworksToLabels = ( ...acc, // existing entry: add suffix with ID [label]: undefined, - [`${label} (ID: ${acc[label]})`]: acc[label], + [withSuffix(label, acc[label])]: acc[label], // new entry: create with suffix - [`${label} (ID: ${id})`]: id, + [withSuffix(label, id)]: id, }; } else { // happy path @@ -366,3 +379,5 @@ export const mapSourceNetworksToLabels = ( return labelToId; }; + +const withSuffix = (label: string, id: string) => `${label} (ID: ${id}})`; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts new file mode 100644 index 000000000..bd8baf3fb --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useFetchEffects.ts @@ -0,0 +1,134 @@ +import { Dispatch, useEffect } from 'react'; +import { useHistory } from 'react-router'; +import { useImmerReducer } from 'use-immer'; + +import { + NetworkMapModelGroupVersionKind, + PlanModelGroupVersionKind, + ProviderModelGroupVersionKind, + ProviderModelRef, + V1beta1NetworkMap, + V1beta1Plan, + V1beta1Provider, +} from '@kubev2v/types'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; + +import { useNamespaces } from '../../hooks/useNamespaces'; +import { useOpenShiftNetworks, useSourceNetworks } from '../../hooks/useNetworks'; +import { useNicProfiles } from '../../hooks/useNicProfiles'; +import { getResourceUrl } from '../../utils'; + +import { + CreateVmMigration, + PageAction, + setAvailableProviders, + setAvailableSourceNetworks, + setAvailableTargetNamespaces, + setAvailableTargetNetworks, + setExistingNetMaps, + setExistingPlans, + setNicProfiles, +} from './actions'; +import { useCreateVmMigrationData } from './ProvidersCreateVmMigrationContext'; +import { CreateVmMigrationPageState, reducer } from './reducer'; +import { createInitialState } from './stateHelpers'; + +export const useFetchEffects = (): [ + CreateVmMigrationPageState, + Dispatch>, + boolean, +] => { + const history = useHistory(); + + const { data: { selectedVms = [], provider: sourceProvider = undefined } = {} } = + useCreateVmMigrationData(); + // error state - the page was entered directly without choosing the VMs + const emptyContext = !selectedVms?.length || !sourceProvider; + const namespace = sourceProvider?.metadata?.namespace ?? ''; + // error recovery - redirect to provider list + useEffect(() => { + if (emptyContext) { + history.push( + getResourceUrl({ + reference: ProviderModelRef, + namespace: namespace, + }), + ); + } + }, [emptyContext]); + + const [state, dispatch] = useImmerReducer( + reducer, + { namespace, sourceProvider, selectedVms }, + createInitialState, + ); + const { + workArea: { targetProvider }, + } = state; + + const [providers, providersLoaded, providerError] = useK8sWatchResource({ + groupVersionKind: ProviderModelGroupVersionKind, + namespaced: true, + isList: true, + namespace, + }); + useEffect( + () => dispatch(setAvailableProviders(providers, providersLoaded, providerError)), + [providers], + ); + + const [plans, plansLoaded, plansError] = useK8sWatchResource({ + groupVersionKind: PlanModelGroupVersionKind, + namespaced: true, + isList: true, + namespace, + }); + useEffect( + () => dispatch(setExistingPlans(plans, plansLoaded, plansError)), + [plans, plansLoaded, plansError], + ); + + const [netMaps, netMapsLoaded, netMapsError] = useK8sWatchResource({ + groupVersionKind: NetworkMapModelGroupVersionKind, + namespaced: true, + isList: true, + namespace, + }); + useEffect( + () => dispatch(setExistingNetMaps(netMaps, netMapsLoaded, netMapsError)), + [netMaps, netMapsLoaded, netMapsError], + ); + + const [namespaces, nsLoading, nsError] = useNamespaces(targetProvider); + useEffect( + () => dispatch(setAvailableTargetNamespaces(namespaces, nsLoading, nsError)), + [namespaces, nsLoading, nsError], + ); + + const [targetNetworks, targetNetworksLoading, targetNetworksError] = + useOpenShiftNetworks(targetProvider); + useEffect( + () => + dispatch( + setAvailableTargetNetworks(targetNetworks, targetNetworksLoading, targetNetworksError), + ), + [targetNetworks, targetNetworksLoading, targetNetworksError], + ); + + const [sourceNetworks, sourceNetworksLoading, sourceNetworksError] = + useSourceNetworks(sourceProvider); + useEffect( + () => + dispatch( + setAvailableSourceNetworks(sourceNetworks, sourceNetworksLoading, sourceNetworksError), + ), + [sourceNetworks, sourceNetworksLoading, sourceNetworksError], + ); + + const [nicProfiles, nicProfilesLoading, nicProfilesError] = useNicProfiles(sourceProvider); + useEffect( + () => dispatch(setNicProfiles(nicProfiles, nicProfilesLoading, nicProfilesError)), + [nicProfiles, nicProfilesLoading, nicProfilesError], + ); + return [state, dispatch, emptyContext]; +}; diff --git a/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useSaveEffect.ts b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useSaveEffect.ts new file mode 100644 index 000000000..b61d84f04 --- /dev/null +++ b/packages/forklift-console-plugin/src/modules/Providers/views/migrate/useSaveEffect.ts @@ -0,0 +1,110 @@ +import { useEffect, useRef } from 'react'; +import { useHistory } from 'react-router'; +import { produce } from 'immer'; + +import { + NetworkMapModel, + PlanModel, + PlanModelRef, + StorageMapModel, + V1beta1NetworkMap, + V1beta1Plan, + V1beta1StorageMap, +} from '@kubev2v/types'; +import { k8sCreate, K8sModel, k8sPatch } from '@openshift-console/dynamic-plugin-sdk'; + +import { getResourceUrl } from '../../utils'; + +import { setError } from './actions'; +import { CreateVmMigrationPageState } from './reducer'; +import { getObjectRef } from './stateHelpers'; + +const createStorage = (storageMap: V1beta1StorageMap) => + k8sCreate({ + model: StorageMapModel, + data: storageMap, + }); + +const createNetwork = (netMap: V1beta1NetworkMap) => + k8sCreate({ + model: NetworkMapModel, + data: netMap, + }); + +const createPlan = async ( + plan: V1beta1Plan, + netMap: V1beta1NetworkMap, + storageMap: V1beta1StorageMap, +) => { + const createdPlan = await k8sCreate({ + model: PlanModel, + data: plan, + }); + const ownerReferences = [getObjectRef(createdPlan)]; + return [ownerReferences, netMap, storageMap]; +}; + +const addOwnerRef = async (model: K8sModel, resource, ownerReferences) => { + return await k8sPatch({ + model, + resource, + data: [ + { + op: 'add', + path: '/metadata/ownerReferences', + value: ownerReferences, + }, + ], + }); +}; + +export const useSaveEffect = (state: CreateVmMigrationPageState, dispatch) => { + const history = useHistory(); + const mounted = useRef(true); + useEffect( + () => () => { + mounted.current = false; + }, + [], + ); + + useEffect(() => { + const { + flow, + underConstruction: { plan, netMap, storageMap }, + } = state; + if (!flow.editingDone || !mounted.current) { + return; + } + + Promise.all([createStorage(storageMap), createNetwork(netMap)]) + .then(([storageMap, netMap]) => + createPlan( + produce(plan, (draft) => { + draft.spec.map.network = getObjectRef(netMap); + draft.spec.map.storage = getObjectRef(storageMap); + }), + netMap, + storageMap, + ), + ) + .then(([ownerReferences, netMap, storageMap]) => + Promise.all([ + addOwnerRef(StorageMapModel, storageMap, ownerReferences), + addOwnerRef(NetworkMapModel, netMap, ownerReferences), + ]), + ) + .then( + () => + mounted.current && + history.push( + getResourceUrl({ + reference: PlanModelRef, + namespace: plan.metadata.namespace, + name: plan.metadata.name, + }), + ), + ) + .catch((error) => mounted.current && dispatch(setError(error))); + }, [state.flow.editingDone, state.underConstruction.storageMap]); +};