From e021ffce49f72df162c15096cab221c1fd3ed3c0 Mon Sep 17 00:00:00 2001 From: suomiy Date: Wed, 18 Sep 2019 14:39:00 +0200 Subject: [PATCH 1/6] kubevirt: add Networking Tab to CreateVMWizard - add/update/remove of network interfaces according to new design - add PXE network selector --- .../create-vm-wizard-footer.tsx | 2 + .../create-vm-wizard/create-vm-wizard.tsx | 91 +++++++--- .../create-vm-wizard/redux/actions.ts | 102 ++++++----- .../redux/initial-state/initial-state.ts | 2 +- .../networks-tab-initial-state.ts | 28 +-- .../vm-settings-tab-initial-state.ts | 2 +- .../redux/internal-actions.ts | 30 +++- .../create-vm-wizard/redux/reducers.ts | 37 +++- .../prefill-vm-template-state-update.ts | 50 ++++-- .../create-vm-wizard/redux/types.ts | 24 ++- .../create-vm-wizard/redux/utils.ts | 5 +- .../validations/networks-tab-validation.ts | 93 ++++++++++ .../validations/vm-settings-tab-validation.ts | 23 +-- .../create-vm-wizard/resource-load-errors.tsx | 2 +- .../selectors/immutable/networks.ts | 2 +- .../selectors/immutable/selectors.ts | 17 +- .../create-vm-wizard/selectors/selectors.ts | 18 ++ .../create-vm-wizard/strings/networking.ts | 4 + .../create-vm-wizard/strings/strings.ts | 2 +- .../tabs/networking-tab/networking-tab.scss | 14 ++ .../tabs/networking-tab/networking-tab.tsx | 167 ++++++++++++++++++ .../tabs/networking-tab/pxe-networks.tsx | 98 ++++++++++ .../tabs/networking-tab/types.tsx | 17 ++ .../vm-wizard-nic-modal-enhanced.tsx | 107 +++++++++++ .../tabs/networking-tab/vm-wizard-nic-row.tsx | 77 ++++++++ .../tabs/result-tab/result-tab.tsx | 2 +- .../tabs/review-tab/networking-review.scss | 14 ++ .../tabs/review-tab/networking-review.tsx | 44 +++++ .../tabs/review-tab/review-tab.scss | 4 + .../tabs/review-tab/review-tab.tsx | 5 + .../tabs/vm-settings-tab/user-templates.tsx | 2 +- .../src/components/create-vm-wizard/types.ts | 32 +++- .../modals/nic-modal/nic-modal-enhanced.tsx | 4 +- .../components/modals/nic-modal/nic-modal.tsx | 30 ++-- .../src/components/table/validation-cell.scss | 3 + .../src/components/table/validation-cell.tsx | 27 +++ .../src/components/vm-disks/disk-row.tsx | 1 - .../src/components/vm-nics/nic-row.tsx | 128 ++++++++++---- .../src/components/vm-nics/types.ts | 29 +-- .../src/components/vm-nics/vm-nics.tsx | 164 ++++++++++------- .../kubevirt-plugin/src/selectors/utils.ts | 2 +- .../src/selectors/vm/selectors.ts | 4 +- .../kubevirt-plugin/src/utils/immutable.ts | 16 +- .../kubevirt-plugin/src/utils/sort.ts | 22 ++- .../kubevirt-plugin/src/utils/utils.ts | 14 ++ .../src/utils/validations/strings.ts | 1 + .../src/utils/validations/types.ts | 2 +- .../src/utils/validations/vm/nic.ts | 30 +++- 48 files changed, 1324 insertions(+), 270 deletions(-) create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/networking.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.scss create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/pxe-networks.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/types.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/vm-wizard-nic-modal-enhanced.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/vm-wizard-nic-row.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/networking-review.scss create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/networking-review.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/table/validation-cell.scss create mode 100644 frontend/packages/kubevirt-plugin/src/components/table/validation-cell.tsx diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx index f7341039c93..687d84fd794 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard-footer.tsx @@ -53,6 +53,7 @@ const CreateVMWizardFooterComponent: React.FC { const jumpToStepID = (isValid && diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx index 24f15599862..3392b65c601 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx @@ -33,12 +33,15 @@ import { } from '../../utils/immutable'; import { getTemplateOperatingSystems } from '../../selectors/vm-template/advanced'; import { ResultsWrapper } from '../../k8s/enhancedK8sMethods/types'; +import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; +import { NetworkInterfaceWrapper } from '../../k8s/wrapper/vm/network-interface-wrapper'; import { ChangedCommonData, CommonData, CreateVMWizardComponentProps, DetectCommonDataChanges, VMSettingsField, + VMWizardNetwork, VMWizardProps, VMWizardTab, } from './types'; @@ -47,14 +50,55 @@ import { vmWizardActions } from './redux/actions'; import { ActionType } from './redux/types'; import { iGetCommonData, iGetCreateVMWizardTabs } from './selectors/immutable/selectors'; import { isStepLocked, isStepPending, isStepValid } from './selectors/immutable/wizard-selectors'; -import { VMSettingsTab } from './tabs/vm-settings-tab/vm-settings-tab'; import { ResourceLoadErrors } from './resource-load-errors'; import { CreateVMWizardFooter } from './create-vm-wizard-footer'; +import { VMSettingsTab } from './tabs/vm-settings-tab/vm-settings-tab'; +import { NetworkingTab } from './tabs/networking-tab/networking-tab'; import { ReviewTab } from './tabs/review-tab/review-tab'; import { ResultTab } from './tabs/result-tab/result-tab'; import './create-vm-wizard.scss'; +// TODO remove after moving create functions from kubevirt-web-ui-components +/** * + * kubevirt-web-ui-components InterOP + */ +const kubevirtInterOP = ({ activeNamespace, vmSettings, networks, storages, templates }) => { + const clonedVMsettings = _.cloneDeep(vmSettings); + const clonedNetworks = _.cloneDeep(networks); + const clonedStorages = _.cloneDeep(storages); + + clonedVMsettings.namespace = { value: activeNamespace }; + const operatingSystems = getTemplateOperatingSystems(templates); + const osField = clonedVMsettings[VMSettingsField.OPERATING_SYSTEM]; + const osID = osField.value; + osField.value = operatingSystems.find(({ id }) => id === osID); + + const interOPNetworks = clonedNetworks.map(({ networkInterface, network }) => { + const networkInterfaceWrapper = NetworkInterfaceWrapper.initialize(networkInterface); + const networkWrapper = NetworkWrapper.initialize(network); + + return { + name: networkInterfaceWrapper.getName(), + mac: networkInterfaceWrapper.getMACAddress(), + binding: networkInterfaceWrapper.getTypeValue(), + isBootable: networkInterfaceWrapper.isFirstBootableDevice(), + network: networkWrapper.getReadableName(), + networkType: networkWrapper.getTypeValue(), + templateNetwork: { + network, + interface: networkInterface, + }, + }; + }); + + return { + interOPVMSettings: clonedVMsettings, + interOPNetworks, + interOPStorages: clonedStorages, + }; +}; + export class CreateVMWizardComponent extends React.Component { private isClosed = false; @@ -93,6 +137,12 @@ export class CreateVMWizardComponent extends React.Component( + iGetIn(this.props.stepData, [VMWizardTab.NETWORKING, 'value']), + ); + const storages = immutableListToShallowJS( + iGetIn(this.props.stepData, [VMWizardTab.STORAGE, 'value']), + ); const templates = immutableListToShallowJS( concatImmutableLists( iGetLoadedData(this.props[VMWizardProps.commonTemplates]), @@ -100,25 +150,20 @@ export class CreateVMWizardComponent extends React.Component id === osID); - /** - * END kubevirt-web-ui-components InterOP - */ + const { interOPVMSettings, interOPNetworks, interOPStorages } = kubevirtInterOP({ + vmSettings, + networks, + storages, + templates, + activeNamespace: this.props.activeNamespace, + }); create( enhancedK8sMethods, templates, - vmSettings, - iGetIn(this.props.stepData, [VMWizardTab.NETWORKS, 'value']).toJS(), - iGetIn(this.props.stepData, [VMWizardTab.STORAGE, 'value']).toJS(), + interOPVMSettings, + interOPNetworks, + interOPStorages, immutableListToShallowJS(iGetLoadedData(this.props[VMWizardProps.persistentVolumeClaims])), units, ) @@ -148,9 +193,15 @@ export class CreateVMWizardComponent extends React.Component ), }, - // { - // id: VMWizardTab.NETWORKS, - // }, + { + id: VMWizardTab.NETWORKING, + component: ( + <> + + + + ), + }, // { // id: VMWizardTab.STORAGE, // }, @@ -276,7 +327,7 @@ export const CreateVMWizardPageComponent: React.FC (dispatch, getState) => { - const prevState = getState(); // must be called before dispatch +const withUpdateAndValidateState = ( + id: string, + resolveAction, + changedCommonData?: Set, +) => (dispatch, getState) => { + const prevState = getState(); // must be called before dispatch in resolveAction - dispatch( - vmWizardInternalActions[InternalActionType.Create](id, { - tabs: ALL_VM_WIZARD_TABS.reduce((initial, tabKey) => { - initial[tabKey] = getTabInitialState(tabKey, commonData); - return initial; - }, {}), - commonData, - }), - ); + resolveAction(dispatch, getState); + + updateAndValidateState({ + id, + dispatch, + changedCommonData: changedCommonData || new Set(), + getState, + prevState, + }); +}; - updateAndValidateState({ +export const vmWizardActions: VMWizardActions = { + [ActionType.Create]: (id, commonData: CommonData) => + withUpdateAndValidateState( id, - changedCommonData: new Set(DetectCommonDataChanges), - dispatch, - getState, - prevState, - }); - }, + (dispatch) => + dispatch( + vmWizardInternalActions[InternalActionType.Create](id, { + tabs: ALL_VM_WIZARD_TABS.reduce((initial, tabKey) => { + initial[tabKey] = getTabInitialState(tabKey, commonData); + return initial; + }, {}), + commonData, + }), + ), + new Set(DetectCommonDataChanges), + ), [ActionType.Dispose]: (id) => (dispatch, getState) => { const prevState = getState(); // must be called before dispatch cleanup({ @@ -48,34 +62,32 @@ export const vmWizardActions: VMWizardActions = { dispatch(vmWizardInternalActions[InternalActionType.Dispose](id)); }, - [ActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: string) => ( - dispatch, - getState, - ) => { - const prevState = getState(); // must be called before dispatch - dispatch(vmWizardInternalActions[InternalActionType.SetVmSettingsFieldValue](id, key, value)); - - updateAndValidateState({ + [ActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: string) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.SetVmSettingsFieldValue](id, key, value)), + ), + [ActionType.UpdateCommonData]: (id, commonData: CommonData, changedProps: ChangedCommonData) => + withUpdateAndValidateState( id, - dispatch, - changedCommonData: new Set(), - getState, - prevState, - }); - }, - [ActionType.UpdateCommonData]: (id, commonData: CommonData, changedProps: ChangedCommonData) => ( - dispatch, - getState, - ) => { - const prevState = getState(); // must be called before dispatch - - dispatch(vmWizardInternalActions[InternalActionType.UpdateCommonData](id, commonData)); - - updateAndValidateState({ id, dispatch, changedCommonData: changedProps, getState, prevState }); - }, - [ActionType.SetNetworks]: (id, value: any, isValid: boolean, isLocked: boolean) => (dispatch) => { - dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, value, isValid, isLocked)); + (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.UpdateCommonData](id, commonData)), + changedProps, + ), + [ActionType.SetTabLocked]: (id, tab: VMWizardTab, isLocked: boolean) => (dispatch) => { + dispatch(vmWizardInternalActions[InternalActionType.SetTabLocked](id, tab, isLocked)); }, + [ActionType.UpdateNIC]: (id, network: VMWizardNetwork) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.UpdateNIC](id, network)), + ), + [ActionType.RemoveNIC]: (id, networkID: string) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.RemoveNIC](id, networkID)), + ), + [ActionType.SetNetworks]: (id, networks: VMWizardNetwork[]) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, networks)), + ), [ActionType.SetStorages]: (id, value: any, isValid: boolean, isLocked: boolean) => (dispatch) => { dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, value, isValid, isLocked)); }, diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts index a85bb786017..2d4017648a8 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts @@ -7,7 +7,7 @@ import { getReviewInitialState } from './review-tab-initial-state'; const initialStateGetterResolver = { [VMWizardTab.VM_SETTINGS]: getVmSettingsInitialState, - [VMWizardTab.NETWORKS]: getNetworksInitialState, + [VMWizardTab.NETWORKING]: getNetworksInitialState, [VMWizardTab.STORAGE]: getStorageInitialState, [VMWizardTab.REVIEW]: getReviewInitialState, [VMWizardTab.RESULT]: getResultInitialState, diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/networks-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/networks-tab-initial-state.ts index 0e2ea0e4638..fec79c89713 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/networks-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/networks-tab-initial-state.ts @@ -1,15 +1,21 @@ -import { NetworkInterfaceType, NetworkType, POD_NETWORK } from '../../../../constants/vm'; +import { VMWizardNetwork, VMWizardNetworkType } from '../../types'; +import { NetworkInterfaceWrapper } from '../../../../k8s/wrapper/vm/network-interface-wrapper'; +import { NetworkInterfaceModel, NetworkType } from '../../../../constants/vm/network'; +import { NetworkWrapper } from '../../../../k8s/wrapper/vm/network-wrapper'; +import { getSequenceName } from '../../../../utils/strings'; -export const podNetwork = { - rootNetwork: {}, - id: 0, - name: 'nic0', - mac: '', - network: POD_NETWORK, - editable: true, - edit: false, - networkType: NetworkType.POD, - binding: NetworkInterfaceType.MASQUERADE, +export const podNetwork: VMWizardNetwork = { + id: '0', + type: VMWizardNetworkType.UI_DEFAULT_POD_NETWORK, + networkInterface: NetworkInterfaceWrapper.initializeFromSimpleData({ + name: getSequenceName('nic'), + model: NetworkInterfaceModel.VIRTIO, + interfaceType: NetworkType.POD.getDefaultInterfaceType(), + }).asResource(), + network: NetworkWrapper.initializeFromSimpleData({ + name: getSequenceName('nic'), + type: NetworkType.POD, + }).asResource(), }; export const getNetworksInitialState = () => ({ diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts index ccd78d58327..6cbe859336b 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts @@ -9,7 +9,7 @@ export const getInitialVmSettings = (common: CommonData) => { } = common; const provisionSources = [ - // ProvisionSource.PXE, // TODO: uncomment when storage tab is implemented + ProvisionSource.PXE, ProvisionSource.URL, ProvisionSource.CONTAINER, // ProvisionSource.CLONED_DISK, // TODO: uncomment when storage tab is implemented diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts index ad32d2ed158..47fdecde4ac 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts @@ -1,4 +1,4 @@ -import { VMSettingsField, VMWizardTab } from '../types'; +import { VMSettingsField, VMWizardNetwork, VMWizardTab } from '../types'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { ActionBatch, InternalActionType, WizardInternalActionDispatcher } from './types'; @@ -47,6 +47,14 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.SetTabValidity, }), + [InternalActionType.SetTabLocked]: (id, tab: VMWizardTab, isLocked: boolean) => ({ + payload: { + id, + tab, + isLocked, + }, + type: InternalActionType.SetTabLocked, + }), [InternalActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: string) => ({ payload: { id, @@ -85,12 +93,24 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.UpdateVmSettings, }), - [InternalActionType.SetNetworks]: (id, value, isValid: boolean, isLocked: boolean) => ({ + [InternalActionType.UpdateNIC]: (id, network: VMWizardNetwork) => ({ payload: { id, - value, - isValid, - isLocked, + network, + }, + type: InternalActionType.UpdateNIC, + }), + [InternalActionType.RemoveNIC]: (id, networkID: string) => ({ + payload: { + id, + networkID, + }, + type: InternalActionType.RemoveNIC, + }), + [InternalActionType.SetNetworks]: (id, networks: VMWizardNetwork[]) => ({ + payload: { + id, + value: networks, }, type: InternalActionType.SetNetworks, }), diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts index c1148426ced..123045429c0 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts @@ -1,5 +1,7 @@ +import * as _ from 'lodash'; import { Map as ImmutableMap, fromJS } from 'immutable'; import { VMWizardTab } from '../types'; +import { iGet } from '../../../utils/immutable'; import { InternalActionType, WizardInternalAction } from './types'; // Merge deep in without updating the keys with undefined values @@ -34,6 +36,25 @@ const setObjectValues = (state, path, obj) => { : state; }; +const updateIDItemInList = (state, path, item?) => { + const itemID = iGet(item, 'id'); + return state.updateIn(path, (items) => { + const networkIndex = item ? items.findIndex((t) => iGet(t, 'id') === itemID) : -1; + if (networkIndex === -1) { + const maxID = items.map((t) => iGet(t, 'id')).max() || 0; + return items.push(item.set('id', _.toString(_.toSafeInteger(maxID) + 1))); + } + return items.set(networkIndex, item); + }); +}; + +const removeIDItemFromList = (state, path, itemID?) => { + return state.updateIn(path, (items) => { + const networkIndex = itemID == null ? -1 : items.findIndex((t) => iGet(t, 'id') === itemID); + return networkIndex === -1 ? items : items.delete(networkIndex); + }); +}; + export default (state, action: WizardInternalAction) => { if (!state) { return ImmutableMap(); @@ -46,8 +67,20 @@ export default (state, action: WizardInternalAction) => { return state.set(dialogId, fromJS(payload.value)); case InternalActionType.Dispose: return state.delete(dialogId); + case InternalActionType.UpdateNIC: + return updateIDItemInList( + state, + [dialogId, 'tabs', VMWizardTab.NETWORKING, 'value'], + fromJS(payload.network), + ); + case InternalActionType.RemoveNIC: + return removeIDItemFromList( + state, + [dialogId, 'tabs', VMWizardTab.NETWORKING, 'value'], + payload.networkID, + ); case InternalActionType.SetNetworks: - return setTabKeys(state, VMWizardTab.NETWORKS, action); + return setTabKeys(state, VMWizardTab.NETWORKING, action); case InternalActionType.SetStorages: return setTabKeys(state, VMWizardTab.STORAGE, action); case InternalActionType.SetResults: @@ -67,6 +100,8 @@ export default (state, action: WizardInternalAction) => { [dialogId, 'tabs', payload.tab, 'hasAllRequiredFilled'], payload.hasAllRequiredFilled, ); + case InternalActionType.SetTabLocked: + return state.setIn([dialogId, 'tabs', payload.tab, 'isLocked'], payload.isLocked); case InternalActionType.SetVmSettingsFieldValue: return state.setIn( [dialogId, 'tabs', VMWizardTab.VM_SETTINGS, 'value', payload.key, 'value'], diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts index 929b2ad3ef4..61d0e92472f 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts @@ -1,14 +1,19 @@ -import * as _ from 'lodash'; +import { createBasicLookup } from '@console/shared/src'; import { InternalActionType, UpdateOptions } from '../../types'; import { iGetVmSettingValue } from '../../../selectors/immutable/vm-settings'; -import { VMSettingsField, VMWizardProps } from '../../../types'; +import { + VMSettingsField, + VMWizardNetwork, + VMWizardNetworkType, + VMWizardProps, +} from '../../../types'; import { iGetLoadedCommonData, iGetName } from '../../../selectors/immutable/selectors'; import { concatImmutableLists, immutableListToShallowJS } from '../../../../../utils/immutable'; import { iGetNetworks as getDialogNetworks } from '../../../selectors/immutable/networks'; import { iGetStorages } from '../../../selectors/immutable/storage'; import { podNetwork } from '../../initial-state/networks-tab-initial-state'; import { vmWizardInternalActions } from '../../internal-actions'; -import { CUSTOM_FLAVOR } from '../../../../../constants/vm'; +import { CUSTOM_FLAVOR, NetworkInterfaceModel } from '../../../../../constants/vm'; import { DEFAULT_CPU, getCloudInitUserData, @@ -25,12 +30,14 @@ import { getTemplateOperatingSystems, getTemplateWorkloadProfiles, } from '../../../../../selectors/vm-template/advanced'; -import { ProvisionSource } from '../../../../../types/vm'; +import { ProvisionSource, V1Network } from '../../../../../types/vm'; import { getTemplateProvisionSource, getTemplateStorages, } from '../../../../../selectors/vm-template/combined'; import { getFlavors } from '../../../../../selectors/vm-template/combined-dependent'; +import { getSimpleName } from '../../../../../selectors/utils'; +import { getNextIDResolver } from '../../../../../utils/utils'; // used by user template; currently we do not support PROVISION_SOURCE_IMPORT const provisionSourceDataFieldResolver = { @@ -53,15 +60,17 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio const vmSettingsUpdate = {}; // filter out oldTemplates - const networkRowsUpdate = immutableListToShallowJS(getDialogNetworks(state, id)).filter( - (network) => !network.templateNetwork, - ); + let networksUpdate = immutableListToShallowJS( + getDialogNetworks(state, id), + ).filter((network) => network.type !== VMWizardNetworkType.TEMPLATE); + const getNextNetworkID = getNextIDResolver(networksUpdate); + const storageRowsUpdate = immutableListToShallowJS(iGetStorages(state, id)).filter( (storage) => !(storage.templateStorage || storage.rootStorage), ); - if (!networkRowsUpdate.find((row) => row.rootNetwork)) { - networkRowsUpdate.push(podNetwork); + if (!networksUpdate.find((row) => !!row.network.pod)) { + networksUpdate.unshift({ ...podNetwork, id: getNextNetworkID() }); } if (iUserTemplate) { @@ -111,24 +120,27 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio } } + const networkLookup = createBasicLookup(getNetworks(vm), getSimpleName); // prefill networks - const templateNetworks = getInterfaces(vm).map((i) => ({ - templateNetwork: { - network: getNetworks(vm).find((n) => n.name === i.name), - interface: i, + const templateNetworks = getInterfaces(vm).map((intface) => ({ + id: getNextNetworkID(), + type: VMWizardNetworkType.TEMPLATE, + network: networkLookup[getSimpleName(intface)], + networkInterface: { + ...intface, + model: intface.model || NetworkInterfaceModel.VIRTIO.getValue(), }, })); - // do not add root interface if there already is one or autoAttachPodInterface is set to false + // remove pod networks if there is already one in template or autoAttachPodInterface is set to false if ( - templateNetworks.some((network) => network.templateNetwork.network.pod) || + templateNetworks.some((network) => !!network.network.pod) || !hasAutoAttachPodInterface(vm, true) ) { - const index = _.findIndex(networkRowsUpdate, (network: any) => network.rootNetwork); - networkRowsUpdate.splice(index, 1); + networksUpdate = networksUpdate.filter((network) => !network.network.pod); } - networkRowsUpdate.push(...templateNetworks); + networksUpdate.push(...templateNetworks); // prefill storage const templateStorages = getTemplateStorages(userTemplate, dataVolumes).map((storage) => ({ @@ -153,6 +165,6 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio } dispatch(vmWizardInternalActions[InternalActionType.UpdateVmSettings](id, vmSettingsUpdate)); - dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, networkRowsUpdate)); + dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, networksUpdate)); dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, storageRowsUpdate)); }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts index 174d69000af..63b5e7e4eba 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts @@ -3,18 +3,22 @@ import { ChangedCommonDataProp, VMSettingsField, VMSettingsFieldType, + VMWizardNetwork, VMWizardTab, } from '../types'; import { ValidationObject } from '../../../utils/validations/types'; export enum ActionType { - Create = 'KubevirtVMWizardCreate', - Dispose = 'KubevirtVMWizardDispose', - UpdateCommonData = 'KubevirtVMWizardUpdateCommonData', - SetVmSettingsFieldValue = 'KubevirtVMWizardSetVmSettingsFieldValue', - SetNetworks = 'KubevirtVMWizardSetNetworks', - SetStorages = 'KubevirtVMWizardSetStorages', - SetResults = 'KubevirtVMWizardSetResults', + Create = 'KubevirtVMWizardExternalCreate', + Dispose = 'KubevirtVMWiExternalDispose', + UpdateCommonData = 'KubevirtVMWizardExternalUpdateCommonData', + SetVmSettingsFieldValue = 'KubevirtVMWizardExternalSetVmSettingsFieldValue', + SetTabLocked = 'KubevirtVMWizardExternalSetTabLocked', + RemoveNIC = 'KubevirtVMWizardExternalRemoveNIC', + UpdateNIC = 'KubevirtVMWizardExternalUpdateNIC', + SetNetworks = 'KubevirtVMWizardExternalSetNetworks', + SetStorages = 'KubevirtVMWizardExternalSetStorages', + SetResults = 'KubevirtVMWizardExternalSetResults', } // should not be called directly from outside redux code (e.g. stateUpdate) @@ -24,11 +28,14 @@ export enum InternalActionType { Update = 'KubevirtVMWizardUpdateInternal', UpdateCommonData = 'KubevirtVMWizardUpdateCommonData', SetTabValidity = 'KubevirtVMWizardSetTabValidityInternal', + SetTabLocked = 'KubevirtVMWizardSetTabLocked', SetVmSettingsFieldValue = 'KubevirtVMWizardSetVmSettingsFieldValueInternal', SetInVmSettings = 'KubevirtVMWizardSetInVmSettingsInternal', SetInVmSettingsBatch = 'KubevirtVMWizardSetInVmSettingsBatchInternal', UpdateVmSettingsField = 'KubevirtVMWizardUpdateVmSettingsFieldInternal', UpdateVmSettings = 'KubevirtVMWizardUpdateVmSettingsInternal', + RemoveNIC = 'KubevirtVMWizardRemoveNIC', + UpdateNIC = 'KubevirtVMWizardUpdateNIC', SetNetworks = 'KubevirtVMWizardSetNetworks', SetStorages = 'KubevirtVMWizardSetStorages', SetResults = 'KubevirtVMWizardSetResults', @@ -47,6 +54,9 @@ export type WizardInternalAction = { key?: VMSettingsField; tab?: VMWizardTab; batch?: ActionBatch; + network?: VMWizardNetwork; + networks?: VMWizardNetwork[]; + networkID?: string; }; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts index f6b08d810f1..ea4da25a8f3 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts @@ -5,8 +5,9 @@ import { setVmSettingsTabValidity, validateVmSettings, } from './validations/vm-settings-tab-validation'; +import { setNetworksTabValidity, validateNetworks } from './validations/networks-tab-validation'; -const UPDATE_TABS = [VMWizardTab.VM_SETTINGS]; +const UPDATE_TABS = [VMWizardTab.VM_SETTINGS, VMWizardTab.NETWORKING]; const updaterResolver = { [VMWizardTab.VM_SETTINGS]: updateVmSettingsState, @@ -14,10 +15,12 @@ const updaterResolver = { const validateTabResolver = { [VMWizardTab.VM_SETTINGS]: validateVmSettings, + [VMWizardTab.NETWORKING]: validateNetworks, }; const isTabValidResolver = { [VMWizardTab.VM_SETTINGS]: setVmSettingsTabValidity, + [VMWizardTab.NETWORKING]: setNetworksTabValidity, }; export const updateAndValidateState = (options: UpdateOptions) => { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts new file mode 100644 index 00000000000..00e763962cf --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts @@ -0,0 +1,93 @@ +import { VMSettingsField, VMWizardTab } from '../../types'; +import { InternalActionType, UpdateOptions } from '../types'; +import { vmWizardInternalActions } from '../internal-actions'; +import { validateNIC } from '../../../../utils/validations/vm'; +import { iGetIn } from '../../../../utils/immutable'; +import { iGetNetworks } from '../../selectors/immutable/networks'; +import { checkTabValidityChanged } from '../../selectors/immutable/selectors'; +import { iGetVmSettingValue } from '../../selectors/immutable/vm-settings'; +import { ProvisionSource } from '../../../../types/vm'; +import { getNetworksWithWrappers } from '../../selectors/selectors'; + +export const validateNetworks = (options: UpdateOptions) => { + const { id, prevState, dispatch, getState } = options; + const state = getState(); + + const prevINetworks = iGetNetworks(prevState, id); + const iNetworks = iGetNetworks(state, id); + + if ( + prevINetworks && + iNetworks && + prevINetworks.size === iNetworks.size && + !prevINetworks.find( + (prevINetwork, prevINetworkIndex) => prevINetwork !== iNetworks.get(prevINetworkIndex), + ) + ) { + return; + } + + const networks = getNetworksWithWrappers(state, id); + + const validatedNetworks = networks.map( + ({ networkInterfaceWrapper, networkWrapper, ...networkBundle }) => { + const otherNetworkBundles = networks.filter((n) => n.id !== networkBundle.id); // to discard networks with a same name + const usedMultusNetworkNames = new Set( + otherNetworkBundles + .map(({ networkWrapper: nw }) => nw.getMultusNetworkName()) + .filter((n) => n), + ); + const usedInterfacesNames = new Set( + otherNetworkBundles.map(({ networkInterfaceWrapper: niw }) => niw.getName()), + ); + + return { + ...networkBundle, + validation: validateNIC(networkInterfaceWrapper, networkWrapper, { + usedInterfacesNames, + usedMultusNetworkNames, + }), + }; + }, + ); + + dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, validatedNetworks)); +}; + +export const setNetworksTabValidity = (options: UpdateOptions) => { + const { id, dispatch, getState } = options; + const state = getState(); + const iNetworks = iGetNetworks(state, id); + + let hasAllRequiredFilled = iNetworks.every((iNetwork) => + iGetIn(iNetwork, ['validation', 'hasAllRequiredFilled']), + ); + + if ( + hasAllRequiredFilled && + iGetVmSettingValue(state, id, VMSettingsField.PROVISION_SOURCE_TYPE) === ProvisionSource.PXE + ) { + hasAllRequiredFilled = !!iNetworks.find( + (networkBundle) => + !iGetIn(networkBundle, ['network', 'pod']) && + iGetIn(networkBundle, ['networkInterface', 'bootOrder']) === 1, + ); + } + + let isValid = hasAllRequiredFilled; + + if (isValid) { + isValid = iNetworks.every((iNetwork) => iGetIn(iNetwork, ['validation', 'isValid'])); + } + + if (checkTabValidityChanged(state, id, VMWizardTab.NETWORKING, isValid, hasAllRequiredFilled)) { + dispatch( + vmWizardInternalActions[InternalActionType.SetTabValidity]( + id, + VMWizardTab.NETWORKING, + isValid, + hasAllRequiredFilled, + ), + ); + } +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts index 36cdfebf344..de8bd7d326d 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts @@ -2,10 +2,10 @@ import { isEmpty } from 'lodash'; import { List } from 'immutable'; import { VMSettingsField, VMWizardProps, VMWizardTab } from '../../types'; import { - iGetFieldKey, - iGetVmSettings, hasVmSettingsChanged, + iGetFieldKey, iGetFieldValue, + iGetVmSettings, isFieldRequired, } from '../../selectors/immutable/vm-settings'; import { @@ -27,6 +27,7 @@ import { import { concatImmutableLists, immutableListToShallowJS } from '../../../../utils/immutable'; import { getFieldTitle } from '../../utils/vm-settings-tab-utils'; import { + checkTabValidityChanged, iGetCommonData, iGetLoadedCommonData, iGetName, @@ -163,12 +164,14 @@ export const setVmSettingsTabValidity = (options: UpdateOptions) => { ); } - dispatch( - vmWizardInternalActions[InternalActionType.SetTabValidity]( - id, - VMWizardTab.VM_SETTINGS, - isValid, - hasAllRequiredFilled, - ), - ); + if (checkTabValidityChanged(state, id, VMWizardTab.VM_SETTINGS, isValid, hasAllRequiredFilled)) { + dispatch( + vmWizardInternalActions[InternalActionType.SetTabValidity]( + id, + VMWizardTab.VM_SETTINGS, + isValid, + hasAllRequiredFilled, + ), + ); + } }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx index 3c905d9ea70..bbd568be9d9 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx @@ -22,7 +22,7 @@ const stateToProps = (state, { wizardReduxID }) => ({ errors: [ asError(state, wizardReduxID, VMWizardProps.commonTemplates), asError(state, wizardReduxID, VMWizardProps.userTemplates), - asError(state, wizardReduxID, VMWizardProps.networkConfigs), + asError(state, wizardReduxID, VMWizardProps.networkAttachmentDefinitions), asError(state, wizardReduxID, VMWizardProps.persistentVolumeClaims), asError(state, wizardReduxID, VMWizardProps.dataVolumes), asError(state, wizardReduxID, VMWizardProps.storageClasses), diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/networks.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/networks.ts index 6997327d3ed..27fbb1d4dcf 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/networks.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/networks.ts @@ -3,4 +3,4 @@ import { VMWizardTab } from '../../types'; import { iGetCreateVMWizardTabs } from './selectors'; export const iGetNetworks = (state, id: string) => - iGetIn(iGetCreateVMWizardTabs(state, id), [VMWizardTab.NETWORKS, 'value']); + iGetIn(iGetCreateVMWizardTabs(state, id), [VMWizardTab.NETWORKING, 'value']); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/selectors.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/selectors.ts index 3c687831a41..c26c6d82607 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/selectors.ts @@ -1,13 +1,28 @@ import { K8sResourceKind } from '@console/internal/module/k8s'; import { iGet, iGetIn, iGetLoadedData } from '../../../../utils/immutable'; import { getCreateVMWizards } from '../selectors'; -import { CommonDataProp } from '../../types'; +import { CommonDataProp, VMWizardTab } from '../../types'; +import { hasStepAllRequiredFilled, isStepValid } from './wizard-selectors'; export const iGetCreateVMWizard = (state, reduxID: string) => iGet(getCreateVMWizards(state), reduxID); export const iGetCreateVMWizardTabs = (state, reduxID: string) => iGet(iGetCreateVMWizard(state, reduxID), 'tabs'); +export const checkTabValidityChanged = ( + state, + reduxID: string, + tab: VMWizardTab, + nextIsValid, + nextHasAllRequiredFilled, +) => { + const tabs = iGetCreateVMWizardTabs(state, reduxID); + return ( + isStepValid(tabs, tab) !== nextIsValid || + hasStepAllRequiredFilled(tabs, tab) !== nextHasAllRequiredFilled + ); +}; + export const iGetCommonData = (state, reduxID: string, key: CommonDataProp) => { const wizard = iGetCreateVMWizard(state, reduxID); const data = iGetIn(wizard, ['commonData', 'data', key]); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts index c962e44bcc3..121b91e2f8c 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts @@ -1,5 +1,23 @@ import { get } from 'lodash'; import { Map } from 'immutable'; +import { iGetIn, immutableListToShallowJS } from '../../../utils/immutable'; +import { VMWizardNetwork, VMWizardNetworkWithWrappers, VMWizardTab } from '../types'; +import { NetworkInterfaceWrapper } from '../../../k8s/wrapper/vm/network-interface-wrapper'; +import { NetworkWrapper } from '../../../k8s/wrapper/vm/network-wrapper'; export const getCreateVMWizards = (state): Map => get(state, ['kubevirt', 'createVmWizards']); + +export const getNetworks = (state, id: string): VMWizardNetwork[] => + immutableListToShallowJS( + iGetIn(getCreateVMWizards(state), [id, 'tabs', VMWizardTab.NETWORKING, 'value']), + ); + +export const getNetworksWithWrappers = (state, id: string): VMWizardNetworkWithWrappers[] => + getNetworks(state, id).map(({ network, networkInterface, ...rest }) => ({ + networkInterfaceWrapper: NetworkInterfaceWrapper.initialize(networkInterface), + networkWrapper: NetworkWrapper.initialize(network), + networkInterface, + network, + ...rest, + })); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/networking.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/networking.ts new file mode 100644 index 00000000000..d84fbb360fb --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/networking.ts @@ -0,0 +1,4 @@ +export const ADD_NETWORK_INTERFACE = 'Add Network Interface'; +export const PXE_NIC_NOT_FOUND_ERROR = 'A PXE-capable network interface could not be found.'; +export const PXE_INFO = 'Pod network is not PXE bootable'; +export const SELECT_PXE_NIC = '--- Select PXE network interface ---'; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts index fccadae4727..61617689e91 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts @@ -11,7 +11,7 @@ export const getCreateVMLikeEntityLabel = (isTemplate: boolean) => export const TabTitleResolver = { [VMWizardTab.VM_SETTINGS]: 'General', - [VMWizardTab.NETWORKS]: 'Networking', + [VMWizardTab.NETWORKING]: 'Networking', [VMWizardTab.STORAGE]: 'Storage', [VMWizardTab.REVIEW]: 'Review', [VMWizardTab.RESULT]: 'Result', diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.scss b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.scss new file mode 100644 index 00000000000..c5f780378f5 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.scss @@ -0,0 +1,14 @@ +.kubevirt-create-vm-modal__networking-tab-container { + display: flex; + flex-direction: column; + min-height: 100%; +} + +.kubevirt-create-vm-modal__networking-tab-main { + flex: 1; +} + +.kubevirt-create-vm-modal__networking-tab-pxe { + margin-top: 1rem; + margin-bottom: 3rem; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.tsx new file mode 100644 index 00000000000..bb1cca86c78 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.tsx @@ -0,0 +1,167 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { + Bullseye, + Button, + ButtonVariant, + EmptyState, + EmptyStateVariant, + Split, + SplitItem, + Title, +} from '@patternfly/react-core'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { iGetCreateVMWizardTabs } from '../../selectors/immutable/selectors'; +import { isStepLocked } from '../../selectors/immutable/wizard-selectors'; +import { + VMSettingsField, + VMWizardNetwork, + VMWizardNetworkWithWrappers, + VMWizardTab, +} from '../../types'; +import { VMNicsTable } from '../../../vm-nics/vm-nics'; +import { nicTableColumnClasses } from '../../../vm-nics/utils'; +import { vmWizardActions } from '../../redux/actions'; +import { ActionType } from '../../redux/types'; +import { ADD_NETWORK_INTERFACE } from '../../strings/networking'; +import { iGetVmSettingValue } from '../../selectors/immutable/vm-settings'; +import { ProvisionSource } from '../../../../types/vm'; +import { getNetworksWithWrappers } from '../../selectors/selectors'; +import { wrapWithProgress } from '../../../../utils/utils'; +import { vmWizardNicModalEnhanced } from './vm-wizard-nic-modal-enhanced'; +import { VMWizardNicRow } from './vm-wizard-nic-row'; +import { VMWizardNetworkBundle } from './types'; +import { PXENetworks } from './pxe-networks'; + +import './networking-tab.scss'; + +const getNicsData = (networks: VMWizardNetworkWithWrappers[]): VMWizardNetworkBundle[] => + networks.map((wizardNetworkData) => { + const { networkInterfaceWrapper, networkWrapper } = wizardNetworkData; + return { + wizardNetworkData, + // for sorting + name: networkInterfaceWrapper.getName(), + model: networkInterfaceWrapper.getReadableModel(), + networkName: networkWrapper.getReadableName(), + interfaceType: networkInterfaceWrapper.getTypeValue(), + macAddress: networkInterfaceWrapper.getMACAddress(), + }; + }); + +const NetworkingTabComponent: React.FC = ({ + isPXENICRequired, + wizardReduxID, + isLocked, + setTabLocked, + removeNIC, + updateNetworks, + networks, +}) => { + const hasNetworks = networks.length > 0; + + const withProgress = wrapWithProgress(setTabLocked); + + const addButtonProps = { + id: 'add-nic', + onClick: () => + withProgress( + vmWizardNicModalEnhanced({ + wizardReduxID, + }).result, + ), + isDisabled: isLocked, + }; + + return ( +
+ + + + Network Interfaces + + + {hasNetworks && ( + + + + )} + + {hasNetworks && ( + <> +
+ +
+ {isPXENICRequired && ( +
+ +
+ )} + + )} + {!hasNetworks && ( + + + + No network interface added + + + + + )} +
+ ); +}; + +type NetworkingTabComponentProps = { + isLocked: boolean; + isPXENICRequired: boolean; + wizardReduxID: string; + networks: VMWizardNetworkWithWrappers[]; + removeNIC: (id: string) => void; + setTabLocked: (isLocked: boolean) => void; + updateNetworks: (networks: VMWizardNetwork[]) => void; +}; + +const stateToProps = (state, { wizardReduxID }) => { + const stepData = iGetCreateVMWizardTabs(state, wizardReduxID); + return { + isLocked: isStepLocked(stepData, VMWizardTab.NETWORKING), + networks: getNetworksWithWrappers(state, wizardReduxID), + isPXENICRequired: + iGetVmSettingValue(state, wizardReduxID, VMSettingsField.PROVISION_SOURCE_TYPE) === + ProvisionSource.PXE, + }; +}; + +const dispatchToProps = (dispatch, { wizardReduxID }) => ({ + setTabLocked: (isLocked) => { + dispatch( + vmWizardActions[ActionType.SetTabLocked](wizardReduxID, VMWizardTab.NETWORKING, isLocked), + ); + }, + removeNIC: (id: string) => { + dispatch(vmWizardActions[ActionType.RemoveNIC](wizardReduxID, id)); + }, + updateNetworks: (networks: VMWizardNetwork[]) => { + dispatch(vmWizardActions[ActionType.SetNetworks](wizardReduxID, networks)); + }, +}); + +export const NetworkingTab = connect( + stateToProps, + dispatchToProps, +)(NetworkingTabComponent); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/pxe-networks.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/pxe-networks.tsx new file mode 100644 index 00000000000..2fe3d67dd72 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/pxe-networks.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { Form, FormSelect, FormSelectOption } from '@patternfly/react-core'; +import { VMWizardNetwork, VMWizardNetworkType, VMWizardNetworkWithWrappers } from '../../types'; +import { NetworkInterfaceWrapper } from '../../../../k8s/wrapper/vm/network-interface-wrapper'; +import { PXE_INFO, PXE_NIC_NOT_FOUND_ERROR, SELECT_PXE_NIC } from '../../strings/networking'; +import { FormRow } from '../../../form/form-row'; +import { ValidationErrorType } from '../../../../utils/validations/types'; +import { FormSelectPlaceholderOption } from '../../../form/form-select-placeholder-option'; +import { ignoreCaseSort } from '../../../../utils/sort'; + +import './networking-tab.scss'; + +const PXE_BOOTSOURCE_ID = 'pxe-bootsource'; + +type PXENetworksProps = { + isDisabled: boolean; + networks: VMWizardNetworkWithWrappers[]; + updateNetworks: (networks: VMWizardNetwork[]) => void; +}; + +export const PXENetworks: React.FC = ({ + isDisabled, + updateNetworks, + networks, +}) => { + const pxeNetworks = networks.filter((n) => !n.networkWrapper.isPodNetwork()); + const hasPXENetworks = pxeNetworks.length > 0; + + const selectedPXE = pxeNetworks.find((network) => + network.networkInterfaceWrapper.isFirstBootableDevice(), + ); + + const onPXENetworkChange = (id: string) => { + const bootOrderIndexes = networks + .map((wizardNetwork) => + wizardNetwork.id === id || wizardNetwork.type !== VMWizardNetworkType.TEMPLATE + ? null + : wizardNetwork.networkInterfaceWrapper.getBootOrder(), + ) + .filter((b) => b != null) + .sort(); + updateNetworks( + // TODO: include disks in the computation and maybe move somewhere else (state update) + networks.map( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ networkInterfaceWrapper, networkWrapper: unused, ...wizardNetwork }) => { + if (wizardNetwork.id === id || networkInterfaceWrapper.hasBootOrder()) { + return { + ...wizardNetwork, + networkInterface: NetworkInterfaceWrapper.mergeWrappers( + networkInterfaceWrapper, + NetworkInterfaceWrapper.initializeFromSimpleData({ + bootOrder: + wizardNetwork.id === id + ? 1 + : bootOrderIndexes.indexOf(networkInterfaceWrapper.getBootOrder()) + 2, + }), + ).asResource(), + }; + } + return wizardNetwork; + }, + ), + ); + }; + + return ( +
+ + + + {ignoreCaseSort(pxeNetworks, null, (n) => n.networkWrapper.getReadableName()).map( + (network) => ( + + ), + )} + + +
+ ); +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/types.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/types.tsx new file mode 100644 index 00000000000..a1c069ea03a --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/types.tsx @@ -0,0 +1,17 @@ +import { NetworkSimpleData } from '../../../vm-nics/types'; +import { VMWizardNetworkWithWrappers } from '../../types'; + +export type VMWizardNetworkBundle = NetworkSimpleData & { + wizardNetworkData: VMWizardNetworkWithWrappers; +}; + +export type VMWizardNicRowActionOpts = { + wizardReduxID: string; + removeNIC?: (id: string) => void; + withProgress?: (promise: Promise) => void; +}; + +export type VMWizardNicRowCustomData = VMWizardNicRowActionOpts & { + columnClasses: string[]; + isDisabled: boolean; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/vm-wizard-nic-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/vm-wizard-nic-modal-enhanced.tsx new file mode 100644 index 00000000000..735fff25660 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/vm-wizard-nic-modal-enhanced.tsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { FirehoseResult } from '@console/internal/components/utils'; +import { createModalLauncher, ModalComponentProps } from '@console/internal/components/factory'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { NetworkAttachmentDefinitionModel } from '../../../../models'; +import { NetworkWrapper } from '../../../../k8s/wrapper/vm/network-wrapper'; +import { NetworkInterfaceWrapper } from '../../../../k8s/wrapper/vm/network-interface-wrapper'; +import { iGetCommonData } from '../../selectors/immutable/selectors'; +import { + VMWizardNetwork, + VMWizardNetworkType, + VMWizardNetworkWithWrappers, + VMWizardProps, +} from '../../types'; +import { NICModal } from '../../../modals/nic-modal'; +import { iFirehoseResultToJS } from '../../../../utils/immutable'; +import { NetworkType } from '../../../../constants/vm'; +import { vmWizardActions } from '../../redux/actions'; +import { ActionType } from '../../redux/types'; +import { getNetworksWithWrappers } from '../../selectors/selectors'; + +const VMWizardNICModal: React.FC = (props) => { + const { + id, + type, + addUpdateNIC, + networks, + networkInterfaceWrapper = NetworkInterfaceWrapper.EMPTY, + networkWrapper = NetworkWrapper.EMPTY, + ...restProps + } = props; + + const usedInterfacesNames: Set = new Set( + networks + .map(({ networkInterfaceWrapper: nicWrapper }) => nicWrapper.getName()) + .filter((n) => n && n !== networkInterfaceWrapper.getName()), + ); + + const usedMultusNetworkNames: Set = new Set( + networks + .filter( + ({ networkWrapper: usedNetworkWrapper }) => + usedNetworkWrapper.getType() === NetworkType.MULTUS && + usedNetworkWrapper.getMultusNetworkName() !== networkWrapper.getMultusNetworkName(), + ) + .map(({ networkWrapper: usedNetworkWrapper }) => usedNetworkWrapper.getMultusNetworkName()), + ); + + const allowPodNetwork = + networkWrapper.isPodNetwork() || + !networks.find(({ networkWrapper: usedNetworkWrapper }) => usedNetworkWrapper.isPodNetwork()); + + return ( + { + addUpdateNIC({ + id, + type: type || VMWizardNetworkType.UI_INPUT, + networkInterface: resultNetworkInterfaceWrapper.asResource(), + network: resultNetworkWrapper.asResource(), + }); + return Promise.resolve(); + }} + /> + ); +}; + +type VMWizardNICModalProps = ModalComponentProps & { + id?: string; + type?: VMWizardNetworkType; + networkInterfaceWrapper?: NetworkInterfaceWrapper; + networkWrapper?: NetworkWrapper; + networks?: VMWizardNetworkWithWrappers[]; + nads?: FirehoseResult; + addUpdateNIC: (network: VMWizardNetwork) => void; +}; + +const stateToProps = (state, { wizardReduxID }) => { + const hasNADs = !!state.k8s.getIn(['RESOURCES', 'models', NetworkAttachmentDefinitionModel.kind]); + return { + hasNADs, + networks: getNetworksWithWrappers(state, wizardReduxID), + nads: iFirehoseResultToJS( + iGetCommonData(state, wizardReduxID, VMWizardProps.networkAttachmentDefinitions), + ), + }; +}; + +const dispatchToProps = (dispatch, { wizardReduxID }) => ({ + addUpdateNIC: (network: VMWizardNetwork) => { + dispatch(vmWizardActions[ActionType.UpdateNIC](wizardReduxID, network)); + }, +}); + +const VMWizardNICModalConnected = connect( + stateToProps, + dispatchToProps, +)(VMWizardNICModal); + +export const vmWizardNicModalEnhanced = createModalLauncher(VMWizardNICModalConnected); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/vm-wizard-nic-row.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/vm-wizard-nic-row.tsx new file mode 100644 index 00000000000..3ac05c55974 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/vm-wizard-nic-row.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { Kebab, KebabOption } from '@console/internal/components/utils'; +import { NicSimpleRow } from '../../../vm-nics/nic-row'; +import { VMWizardNetworkWithWrappers } from '../../types'; +import { VMWizardNetworkBundle, VMWizardNicRowActionOpts, VMWizardNicRowCustomData } from './types'; +import { vmWizardNicModalEnhanced } from './vm-wizard-nic-modal-enhanced'; + +const menuActionEdit = ( + { networkInterfaceWrapper, networkWrapper, id, type }: VMWizardNetworkWithWrappers, + { wizardReduxID, withProgress }: VMWizardNicRowActionOpts, +): KebabOption => ({ + label: 'Edit', + callback: () => + withProgress( + vmWizardNicModalEnhanced({ + wizardReduxID, + id, + type, + networkInterfaceWrapper, + networkWrapper, + }).result, + ), +}); + +const menuActionRemove = ( + { id }: VMWizardNetworkWithWrappers, + { withProgress, removeNIC }: VMWizardNicRowActionOpts, +): KebabOption => ({ + label: 'Delete', + callback: () => + withProgress( + new Promise((resolve) => { + removeNIC(id); + resolve(); + }), + ), +}); + +const getActions = ( + wizardNetworkData: VMWizardNetworkWithWrappers, + opts: VMWizardNicRowActionOpts, +) => { + const actions = [menuActionEdit, menuActionRemove]; + return actions.map((a) => a(wizardNetworkData, opts)); +}; + +export type VMWizardNicRowProps = { + obj: VMWizardNetworkBundle; + customData: VMWizardNicRowCustomData; + index: number; + style: object; +}; + +export const VMWizardNicRow: React.FC = ({ + obj: { name, wizardNetworkData, ...restData }, + customData: { isDisabled, columnClasses, removeNIC, withProgress, wizardReduxID }, + index, + style, +}) => { + return ( + + } + /> + ); +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab.tsx index df6458d6e28..5885c51172b 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab.tsx @@ -10,7 +10,7 @@ import { RequestResultsPart } from './request-results-part'; import './result-tab.scss'; -export const ResultTabComponent: React.FC = ({ +const ResultTabComponent: React.FC = ({ wizardReduxID, isValid, isPending, diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/networking-review.scss b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/networking-review.scss new file mode 100644 index 00000000000..5ca7c223198 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/networking-review.scss @@ -0,0 +1,14 @@ +@import '~patternfly/dist/sass/patternfly/color-variables'; + +.kubevirt-create-vm-modal__review-tab-networking { + dt { + color: $color-pf-black-600; + text-transform: capitalize; + } +} + +.kubevirt-create-vm-modal__review-tab-networking-simple-list { + list-style-type: none; + margin: 0; + padding: 0; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/networking-review.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/networking-review.tsx new file mode 100644 index 00000000000..a0113af3b06 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/networking-review.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import * as classNames from 'classnames'; +import { connect } from 'react-redux'; +import { VMWizardNetworkWithWrappers } from '../../types'; +import { getNetworksWithWrappers } from '../../selectors/selectors'; + +import './networking-review.scss'; + +const NetworkingReviewConnected: React.FC = ({ + networks, + className, +}) => { + return ( +
+
Network Interfaces
+
+
    + {networks.map(({ id, networkInterfaceWrapper, networkWrapper }) => ( +
  • + {_.compact([ + networkInterfaceWrapper.getName(), + networkInterfaceWrapper.getReadableModel(), + networkWrapper.getReadableName(), + networkInterfaceWrapper.getMACAddress(), + ]).join(' - ')} +
  • + ))} +
+
+
+ ); +}; + +type NetworkingTabComponentProps = { + networks: VMWizardNetworkWithWrappers[]; + className: string; +}; + +const stateToProps = (state, { wizardReduxID }) => ({ + networks: getNetworksWithWrappers(state, wizardReduxID), +}); + +export const NetworkingReview = connect(stateToProps)(NetworkingReviewConnected); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-tab.scss b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-tab.scss index d707bd0f008..f1556509317 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-tab.scss +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-tab.scss @@ -1,3 +1,7 @@ .kubevirt-create-vm-modal__review-tab-title { margin-bottom: 1em; } + +.kubevirt-create-vm-modal__review-tab-lower-section { + margin-top: 20px; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-tab.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-tab.tsx index 8a2c95985e2..3a93a987f2d 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-tab.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-tab.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Title } from '@patternfly/react-core'; import { VMSettingsTab } from '../vm-settings-tab/vm-settings-tab'; +import { NetworkingReview } from './networking-review'; import './review-tab.scss'; @@ -11,6 +12,10 @@ export const ReviewTab: React.FC = ({ wizardReduxID }) => { Review and confirm your settings + ); }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/user-templates.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/user-templates.tsx index 88ad474aeeb..50d9e0942ba 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/user-templates.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/user-templates.tsx @@ -13,7 +13,7 @@ import { iGetName } from '../../selectors/immutable/selectors'; export const UserTemplates: React.FC = React.memo( ({ userTemplateField, userTemplates, commonTemplates, dataVolumes, onChange }) => { const data = iGetLoadedData(userTemplates); - const names = + const names: string[] = data && data .toIndexedSeq() diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/types.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/types.ts index 275c94a9c54..85cd108df90 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/types.ts @@ -1,11 +1,14 @@ import { FirehoseResult } from '@console/internal/components/utils'; import { TemplateKind } from '@console/internal/module/k8s'; import { getStringEnumValues } from '../../utils/types'; -import { VMKind } from '../../types/vm'; +import { V1Network, V1NetworkInterface, VMKind } from '../../types/vm'; +import { NetworkInterfaceWrapper } from '../../k8s/wrapper/vm/network-interface-wrapper'; +import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; +import { UINetworkInterfaceValidation } from '../../utils/validations/vm'; export enum VMWizardTab { // order important VM_SETTINGS = 'VM_SETTINGS', - NETWORKS = 'NETWORKS', + NETWORKING = 'NETWORKING', STORAGE = 'STORAGE', REVIEW = 'REVIEW', RESULT = 'RESULT', @@ -18,7 +21,7 @@ export enum VMWizardProps { virtualMachines = 'virtualMachines', userTemplates = 'userTemplates', commonTemplates = 'commonTemplates', - networkConfigs = 'networkConfigs', + networkAttachmentDefinitions = 'networkAttachmentDefinitions', storageClasses = 'storageClasses', persistentVolumeClaims = 'persistentVolumeClaims', dataVolumes = 'dataVolumes', @@ -67,7 +70,7 @@ export type ChangedCommonDataProp = | VMWizardProps.userTemplates | VMWizardProps.persistentVolumeClaims | VMWizardProps.commonTemplates - | VMWizardProps.networkConfigs + | VMWizardProps.networkAttachmentDefinitions | VMWizardProps.storageClasses; export type CommonDataProp = VMWizardProps.isCreateTemplate | ChangedCommonDataProp; @@ -81,7 +84,7 @@ export const DetectCommonDataChanges = new Set([ VMWizardProps.userTemplates, VMWizardProps.commonTemplates, VMWizardProps.persistentVolumeClaims, - VMWizardProps.networkConfigs, + VMWizardProps.networkAttachmentDefinitions, ]); export type CommonData = { @@ -106,3 +109,22 @@ export type CreateVMWizardComponentProps = { onResultsChanged: (results, isValid: boolean, isLocked: boolean, isPending: boolean) => void; lockTab: (tabID: VMWizardTab) => void; }; + +export enum VMWizardNetworkType { + TEMPLATE = 'TEMPLATE', + UI_DEFAULT_POD_NETWORK = 'UI_DEFAULT_POD_NETWORK', + UI_INPUT = 'UI_INPUT', +} + +export type VMWizardNetwork = { + id?: string; + type: VMWizardNetworkType; + network: V1Network; + networkInterface: V1NetworkInterface; + validation?: UINetworkInterfaceValidation; +}; + +export type VMWizardNetworkWithWrappers = VMWizardNetwork & { + networkInterfaceWrapper: NetworkInterfaceWrapper; + networkWrapper: NetworkWrapper; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx index 496a4612f31..6d82bd15255 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx @@ -121,13 +121,13 @@ type NICModalFirehoseProps = ModalComponentProps & { hasNADs: boolean; }; -const cloneVMModalStateToProps = ({ k8s }) => { +const nicModalStateToProps = ({ k8s }) => { const hasNADs = !!k8s.getIn(['RESOURCES', 'models', NetworkAttachmentDefinitionModel.kind]); return { hasNADs, }; }; -const NICModalConnected = connect(cloneVMModalStateToProps)(NICModalFirehose); +const NICModalConnected = connect(nicModalStateToProps)(NICModalFirehose); export const nicModalEnhanced = createModalLauncher(NICModalConnected); diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx index 46a15f25cfd..c317aa41f1f 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx @@ -94,17 +94,19 @@ export const Network: React.FC = ({ : '--- Select Network Definition ---' } /> - {ignoreCaseSort(networkChoices, ['readableName']).map((networkWrapper: NetworkWrapper) => { - const readableName = networkWrapper.getReadableName(); - return ( - - ); - })} + {ignoreCaseSort(networkChoices, undefined, (n) => n.getReadableName()).map( + (networkWrapper: NetworkWrapper) => { + const readableName = networkWrapper.getReadableName(); + return ( + + ); + }, + )} ); @@ -112,7 +114,6 @@ export const Network: React.FC = ({ export const NICModal = withHandlePromise((props: NICModalProps) => { const { - network, nads, usedInterfacesNames, usedMultusNetworkNames, @@ -126,6 +127,7 @@ export const NICModal = withHandlePromise((props: NICModalProps) => { } = props; const asId = prefixedID.bind(null, 'nic'); const nic = props.nic || NetworkInterfaceWrapper.EMPTY; + const network = props.network || NetworkWrapper.EMPTY; const isEditing = nic !== NetworkInterfaceWrapper.EMPTY; const [name, setName] = React.useState( @@ -288,8 +290,8 @@ export const NICModal = withHandlePromise((props: NICModalProps) => { }); export type NICModalProps = { - nic: NetworkInterfaceWrapper; - network: NetworkWrapper; + nic?: NetworkInterfaceWrapper; + network?: NetworkWrapper; onSubmit: (networkInterface: NetworkInterfaceWrapper, network: NetworkWrapper) => Promise; nads?: FirehoseResult; usedInterfacesNames: Set; diff --git a/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.scss b/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.scss new file mode 100644 index 00000000000..681821d32b2 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.scss @@ -0,0 +1,3 @@ +.kubevirt-nic-row__cell--error { + color: var(--pf-global--danger-color--100); +} diff --git a/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.tsx b/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.tsx new file mode 100644 index 00000000000..9ee22043adc --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { ValidationErrorType, ValidationObject } from '../../utils/validations/types'; + +import './validation-cell.scss'; + +export type SimpleCellProps = { + children?: React.ReactNode; + validation?: ValidationObject; +}; + +export const ValidationCell: React.FC = ({ children, validation }) => { + return ( + <> + {children} + {validation && validation.type !== ValidationErrorType.TrivialError && ( +
+ {validation.message} +
+ )} + + ); +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx index 8ba6d208dfb..cc46513934f 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx @@ -54,7 +54,6 @@ export const DiskRow: React.FC = ({ diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx index d405086bb55..a0933c832ad 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx @@ -9,17 +9,30 @@ import { isVM } from '../../selectors/vm'; import { dimensifyRow } from '../../utils/table'; import { VMLikeEntityKind } from '../../types'; import { nicModalEnhanced } from '../modals/nic-modal/nic-modal-enhanced'; -import { nicTableColumnClasses } from './utils'; -import { VMNicRowProps } from './types'; +import { ValidationCell } from '../table/validation-cell'; +import { + VMNicRowActionOpts, + NetworkBundle, + NetworkSimpleData, + NetworkSimpleDataValidation, + VMNicRowCustomData, +} from './types'; -const menuActionEdit = (nic, network, vmLikeEntity: VMLikeEntityKind): KebabOption => ({ +const menuActionEdit = ( + nic, + network, + vmLikeEntity: VMLikeEntityKind, + { withProgress }: VMNicRowActionOpts, +): KebabOption => ({ label: 'Edit', callback: () => - nicModalEnhanced({ - vmLikeEntity, - nic, - network, - }), + withProgress( + nicModalEnhanced({ + vmLikeEntity, + nic, + network, + }).result, + ), accessReview: asAccessReview( isVM(vmLikeEntity) ? VirtualMachineModel : TemplateModel, vmLikeEntity, @@ -27,14 +40,21 @@ const menuActionEdit = (nic, network, vmLikeEntity: VMLikeEntityKind): KebabOpti ), }); -const menuActionDelete = (nic, network, vmLikeEntity: VMLikeEntityKind): KebabOption => ({ +const menuActionDelete = ( + nic, + network, + vmLikeEntity: VMLikeEntityKind, + { withProgress }: VMNicRowActionOpts, +): KebabOption => ({ label: 'Delete', callback: () => - deleteDeviceModal({ - deviceType: DeviceType.NIC, - device: nic, - vmLikeEntity, - }), + withProgress( + deleteDeviceModal({ + deviceType: DeviceType.NIC, + device: nic, + vmLikeEntity, + }).result, + ), accessReview: asAccessReview( isVM(vmLikeEntity) ? VirtualMachineModel : TemplateModel, vmLikeEntity, @@ -42,34 +62,78 @@ const menuActionDelete = (nic, network, vmLikeEntity: VMLikeEntityKind): KebabOp ), }); -const getActions = (nic, network, vmLikeEntity: VMLikeEntityKind) => { +const getActions = (nic, network, vmLikeEntity: VMLikeEntityKind, opts: VMNicRowActionOpts) => { const actions = [menuActionEdit, menuActionDelete]; - return actions.map((a) => a(nic, network, vmLikeEntity)); + return actions.map((a) => a(nic, network, vmLikeEntity, opts)); }; -export const NicRow: React.FC = ({ - obj: { name, model, networkName, interfaceType, macAddress, nic, network }, - customData: { vmLikeEntity }, +export type VMNicSimpleRowProps = { + data: NetworkSimpleData; + validation?: NetworkSimpleDataValidation; + columnClasses: string[]; + actionsComponent: React.ReactNode; + index: number; + style: object; +}; + +export const NicSimpleRow: React.FC = ({ + data: { name, model, networkName, interfaceType, macAddress }, + validation = {}, + columnClasses, + actionsComponent, index, style, }) => { - const dimensify = dimensifyRow(nicTableColumnClasses); + const dimensify = dimensifyRow(columnClasses); return ( - {name} - {model || DASH} - {networkName || DASH} - {interfaceType || DASH} - {macAddress || DASH} - - + + {name} + + + {model || DASH} + + + {networkName || DASH} + + + + {interfaceType || DASH} + + + + {macAddress || DASH} + {actionsComponent} ); }; + +export type VMNicRowProps = { + obj: NetworkBundle; + customData: VMNicRowCustomData; + index: number; + style: object; +}; + +export const NicRow: React.FC = ({ + obj: { name, nic, network, ...restData }, + customData: { isDisabled, withProgress, vmLikeEntity, columnClasses }, + index, + style, +}) => ( + + } + /> +); diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/types.ts b/frontend/packages/kubevirt-plugin/src/components/vm-nics/types.ts index eff2354fcd9..a0a716d4958 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/types.ts @@ -1,22 +1,31 @@ import { VMLikeEntityKind } from '../../types'; +import { ValidationObject } from '../../utils/validations/types'; -export type NetworkBundle = { +export type NetworkSimpleData = { name?: string; model?: string; - networkName: string; + networkName?: string; interfaceType?: string; macAddress?: string; +}; + +export type NetworkSimpleDataValidation = { + name?: ValidationObject; + model?: ValidationObject; + network?: ValidationObject; + interfaceType?: ValidationObject; + macAddress?: ValidationObject; +}; + +export type NetworkBundle = NetworkSimpleData & { nic: any; network: any; }; +export type VMNicRowActionOpts = { withProgress: (promise: Promise) => void }; + export type VMNicRowCustomData = { vmLikeEntity: VMLikeEntityKind; -}; - -export type VMNicRowProps = { - obj: NetworkBundle; - customData: VMNicRowCustomData; - index: number; - style: object; -}; + columnClasses: string[]; + isDisabled: boolean; +} & VMNicRowActionOpts; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx index 920f74a5898..b44901580a8 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/vm-nics.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; -import { Button } from 'patternfly-react'; import { Table } from '@console/internal/components/factory'; import { sortable } from '@patternfly/react-table'; import { createBasicLookup } from '@console/shared'; +import { useSafetyFirst } from '@console/internal/components/safety-first'; +import { Button, ButtonVariant } from '@patternfly/react-core'; import { VMLikeEntityKind } from '../../types'; import { getInterfaces, getNetworks, asVM } from '../../selectors/vm'; import { dimensifyHeader } from '../../utils/table'; @@ -11,6 +12,7 @@ import { NetworkInterfaceWrapper } from '../../k8s/wrapper/vm/network-interface- import { nicModalEnhanced } from '../modals/nic-modal/nic-modal-enhanced'; import { getSimpleName } from '../../selectors/utils'; import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; +import { wrapWithProgress } from '../../utils/utils'; import { NicRow } from './nic-row'; import { NetworkBundle } from './types'; import { nicTableColumnClasses } from './utils'; @@ -36,69 +38,101 @@ const getNicsData = (vmLikeEntity: VMLikeEntityKind): NetworkBundle[] => { }); }; -export const VMNics: React.FC = ({ obj: vmLikeEntity }) => ( -
-
-
- +export type VMNicsTableProps = { + data?: any[]; + customData?: object; + row: React.ComponentClass | React.ComponentType; + columnClasses: string[]; +}; + +export const VMNicsTable: React.FC = ({ + data, + customData, + row: Row, + columnClasses, +}) => { + return ( + + dimensifyHeader( + [ + { + title: 'Name', + sortField: 'name', + transforms: [sortable], + }, + { + title: 'Model', + sortField: 'model', + transforms: [sortable], + }, + { + title: 'Network', + sortField: 'networkName', + transforms: [sortable], + }, + { + title: 'Type', + sortField: 'interfaceType', + transforms: [sortable], + }, + { + title: 'MAC Address', + sortField: 'macAddress', + transforms: [sortable], + }, + { + title: '', + }, + ], + columnClasses, + ) + } + Row={Row} + customData={{ ...customData, columnClasses }} + virtualize + loaded + /> + ); +}; + +export const VMNics: React.FC = ({ obj: vmLikeEntity }) => { + const [isLocked, setIsLocked] = useSafetyFirst(false); + const withProgress = wrapWithProgress(setIsLocked); + return ( +
+
+
+ +
+
+
+
-
-
- dimensifyHeader( - [ - { - title: 'Name', - sortField: 'name', - transforms: [sortable], - }, - { - title: 'Model', - sortField: 'model', - transforms: [sortable], - }, - { - title: 'Network', - sortField: 'networkName', - transforms: [sortable], - }, - { - title: 'Type', - sortField: 'interfaceType', - transforms: [sortable], - }, - { - title: 'MAC Address', - sortField: 'macAddress', - transforms: [sortable], - }, - { - title: '', - }, - ], - nicTableColumnClasses, - ) - } - Row={NicRow} - customData={{ - vmLikeEntity, - }} - virtualize - loaded - /> - - -); + ); +}; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/utils.ts b/frontend/packages/kubevirt-plugin/src/selectors/utils.ts index 6fca265a73e..6b336c6fdb1 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/utils.ts @@ -16,4 +16,4 @@ export const findKeySuffixValue = (obj: StringHashMap, keyPrefix: string) => { return index > 0 ? key.substring(index + 1) : null; }; -export const getSimpleName = (obj) => obj && obj.name; +export const getSimpleName = (obj): string => obj && obj.name; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts index 5817a53ab93..cfc2788b399 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts @@ -7,7 +7,7 @@ import { TEMPLATE_OS_NAME_ANNOTATION, TEMPLATE_WORKLOAD_LABEL, } from '../../constants/vm'; -import { V1NetworkInterface, VMKind, VMLikeEntityKind, CPURaw } from '../../types'; +import { V1Network, V1NetworkInterface, VMKind, VMLikeEntityKind, CPURaw } from '../../types'; import { findKeySuffixValue, getSimpleName, getValueByPrefix } from '../utils'; import { getAnnotations, getLabels } from '../selectors'; import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; @@ -27,7 +27,7 @@ export const getInterfaces = (vm: VMKind, defaultValue = []): V1NetworkInterface ? defaultValue : vm.spec.template.spec.domain.devices.interfaces; -export const getNetworks = (vm: VMKind, defaultValue = []) => +export const getNetworks = (vm: VMKind, defaultValue = []): V1Network[] => _.get(vm, 'spec.template.spec.networks') == null ? defaultValue : vm.spec.template.spec.networks; export const getVolumes = (vm: VMKind, defaultValue = []) => _.get(vm, 'spec.template.spec.volumes') == null ? defaultValue : vm.spec.template.spec.volumes; diff --git a/frontend/packages/kubevirt-plugin/src/utils/immutable.ts b/frontend/packages/kubevirt-plugin/src/utils/immutable.ts index 4732feaf434..53e6b63c691 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/immutable.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/immutable.ts @@ -3,7 +3,21 @@ import { List } from 'immutable'; export const concatImmutableLists = (...args) => args.filter((list) => list).reduce((acc, nextArray) => acc.concat(nextArray), List()); -export const immutableListToShallowJS = (list, defaultValue = []) => +export const iFirehoseResultToJS = (immutableValue, isList = true) => { + if (!immutableValue) { + return {}; + } + + const data = immutableValue.get('data'); + + return { + data: data && isList ? data.toArray().map((p) => p.toJSON()) : data.toJS(), + loadError: immutableValue.get('loadError'), + loaded: immutableValue.get('loaded'), + }; +}; + +export const immutableListToShallowJS = (list, defaultValue: A[] = []): A[] => list ? list.toArray().map((p) => p.toJSON()) : defaultValue; export const hasTruthyValue = (obj) => !!(obj && !!obj.find((value) => value)); diff --git a/frontend/packages/kubevirt-plugin/src/utils/sort.ts b/frontend/packages/kubevirt-plugin/src/utils/sort.ts index 08810ebad51..99c95c14257 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/sort.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/sort.ts @@ -30,9 +30,19 @@ export const flavorSort = (array = []) => return resolvedFlavorA - resolvedFlavorB; }); -export const ignoreCaseSort = (array = [], byPath: string[] = undefined) => - array.sort((a, b) => - (byPath ? _.get(a, byPath, '') : a) - .toLowerCase() - .localeCompare((byPath ? _.get(b, byPath, '') : b).toLowerCase()), - ); +export const ignoreCaseSort = ( + array: T[] = [], + byPath: string[] = undefined, + byValueResolver: (item: T) => string = undefined, +) => { + const resolve = (v) => { + const result = _.isFunction(byValueResolver) + ? byValueResolver(v) + : byPath + ? _.get(v, byPath, '') + : v; + + return result == null ? '' : result.toLowerCase(); + }; + return array.sort((a, b) => resolve(a).localeCompare(resolve(b))); +}; diff --git a/frontend/packages/kubevirt-plugin/src/utils/utils.ts b/frontend/packages/kubevirt-plugin/src/utils/utils.ts index cf3213e7552..1de3edc4fc2 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/utils.ts @@ -1,5 +1,6 @@ import { referenceForModel } from '@console/internal/module/k8s'; import { getName, getNamespace } from '@console/shared/src'; +import * as _ from 'lodash'; import { VirtualMachineModel } from '../models'; export const getSequence = (from, to) => Array.from({ length: to - from + 1 }, (v, i) => i + from); @@ -19,6 +20,19 @@ export const setNativeValue = (element, value) => { export const getFullResourceId = (obj) => `${referenceForModel(obj)}~${getNamespace(obj)}~${getName(obj)}`; +export const getNextIDResolver = (entities: { id?: string }[]) => { + let maxNetworkID = + _.max(entities.map((entity) => (entity.id == null ? 0 : _.toSafeInteger(entity.id)))) || 0; + return () => _.toString(++maxNetworkID); +}; + +export const wrapWithProgress = (setProgress: (inProgress: boolean) => void) => ( + promise: Promise, +) => { + setProgress(true); + promise.then(() => setProgress(false)).catch(() => setProgress(false)); +}; + export const getVMLikeModelName = (isCreateTemplate: boolean) => isCreateTemplate ? 'virtual machine template' : VirtualMachineModel.label.toLowerCase(); diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/strings.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/strings.ts index 189e539322e..1739bb2eb87 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/strings.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/strings.ts @@ -15,3 +15,4 @@ export const VIRTUAL_MACHINE_TEMPLATE_EXISTS = 'is already used in another templ export const MAC_ADDRESS_INVALID_ERROR = 'Invalid MAC address format'; export const NIC_NAME_EXISTS = 'Interface with this name already exists'; +export const NETWORK_MULTUS_NAME_EXISTS = 'Multus network with this name already exists'; diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/types.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/types.ts index 457c31bf8c1..9db7da6be72 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/types.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/types.ts @@ -1,6 +1,6 @@ export enum ValidationErrorType { Error = 'error', - TrivialError = 'trivial-error', + TrivialError = 'trivial-error', // should not be visible but affects data validation Warn = 'warning', Info = 'info', } diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/nic.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/nic.ts index 70e6241eda4..adacefab211 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/nic.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/nic.ts @@ -1,9 +1,10 @@ import { getValidationObject, validateDNS1123SubdomainValue } from '../common'; import { makeSentence } from '../../grammar'; -import { MAC_ADDRESS_INVALID_ERROR, NIC_NAME_EXISTS } from '../strings'; +import { MAC_ADDRESS_INVALID_ERROR, NETWORK_MULTUS_NAME_EXISTS, NIC_NAME_EXISTS } from '../strings'; import { ValidationObject } from '../types'; import { NetworkInterfaceWrapper } from '../../../k8s/wrapper/vm/network-interface-wrapper'; import { NetworkWrapper } from '../../../k8s/wrapper/vm/network-wrapper'; +import { NetworkType } from '../../../constants/vm'; import { isValidMAC } from './validations'; export const validateNicName = ( @@ -20,19 +21,41 @@ export const validateNicName = ( return validation; }; +export const validateNetwork = ( + network: NetworkWrapper, + usedMultusNetworkNames: Set, +): ValidationObject => { + if ( + network.getType() === NetworkType.MULTUS && + usedMultusNetworkNames && + usedMultusNetworkNames.has(network.getMultusNetworkName()) + ) { + return getValidationObject(NETWORK_MULTUS_NAME_EXISTS); + } + + return null; +}; + export const validateMACAddress = (mac: string): ValidationObject => { - const isValid = mac === '' || (mac && isValidMAC(mac)); + const isValid = !mac || isValidMAC(mac); return isValid ? null : getValidationObject(makeSentence(MAC_ADDRESS_INVALID_ERROR)); }; export const validateNIC = ( interfaceWrapper: NetworkInterfaceWrapper, network: NetworkWrapper, - { usedInterfacesNames }: { usedInterfacesNames?: Set }, + { + usedInterfacesNames, + usedMultusNetworkNames, + }: { + usedInterfacesNames?: Set; + usedMultusNetworkNames?: Set; + }, ): UINetworkInterfaceValidation => { const validations = { name: validateNicName(interfaceWrapper && interfaceWrapper.getName(), usedInterfacesNames), macAddress: validateMACAddress(interfaceWrapper && interfaceWrapper.getMACAddress()), + network: validateNetwork(network, usedMultusNetworkNames), }; const hasAllRequiredFilled = @@ -55,6 +78,7 @@ export type UINetworkInterfaceValidation = { validations: { name?: ValidationObject; macAddress?: ValidationObject; + network?: ValidationObject; }; isValid: boolean; hasAllRequiredFilled: boolean; From 64b1ccdcdc909a4537052ed52c9be82e9c70c008 Mon Sep 17 00:00:00 2001 From: suomiy Date: Mon, 7 Oct 2019 14:46:05 +0200 Subject: [PATCH 2/6] kubevirt: remove background color from ResultsRow --- .../create-vm-wizard/tabs/result-tab/result-tab-row.scss | 5 ----- .../create-vm-wizard/tabs/result-tab/result-tab-row.tsx | 8 +------- .../kubevirt-plugin/src/selectors/vm/selectors.ts | 2 +- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab-row.scss b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab-row.scss index ef24fa44448..2bd7f37e4a2 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab-row.scss +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab-row.scss @@ -10,8 +10,3 @@ .kubevirt-create-vm-modal__result-tab-row { text-align: left; } - -.kubevirt-create-vm-modal__result-tab-row--error { - background-color: #ffe6e6; - border-color: var(--pf-global--danger-color--200); -} diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab-row.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab-row.tsx index b5970fb500d..210be530190 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/result-tab/result-tab-row.tsx @@ -16,13 +16,7 @@ export const ResultTabRow: React.FC = ({ title, content, isEr 'kubevirt-create-vm-modal___result-tab-row-container--error': isError, })} > -
-        {content}
-      
+
{content}
); }; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts index cfc2788b399..aa58d34093d 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/selectors.ts @@ -32,7 +32,7 @@ export const getNetworks = (vm: VMKind, defaultValue = []): V1Network[] => export const getVolumes = (vm: VMKind, defaultValue = []) => _.get(vm, 'spec.template.spec.volumes') == null ? defaultValue : vm.spec.template.spec.volumes; export const getDataVolumeTemplates = (vm: VMKind, defaultValue = []) => - _.get(vm, 'spec.dataVolumeTemplates') == null ? defaultValue : vm.spec.dataVolumeTemplate; + _.get(vm, 'spec.dataVolumeTemplates') == null ? defaultValue : vm.spec.dataVolumeTemplates; export const getOperatingSystem = (vm: VMLikeEntityKind) => findKeySuffixValue(getLabels(vm), TEMPLATE_OS_LABEL); From 121c1b0244872a4f4395fe6ed356c5e6f2384c39 Mon Sep 17 00:00:00 2001 From: suomiy Date: Thu, 19 Sep 2019 12:31:19 +0200 Subject: [PATCH 3/6] kubevirt: add storage types --- .../src/types/vm/disk/V1CDRomTarget.ts | 38 ++++++ .../vm/disk/V1CloudInitConfigDriveSource.ts | 58 +++++++++ .../types/vm/disk/V1CloudInitNoCloudSource.ts | 58 +++++++++ .../types/vm/disk/V1ConfigMapVolumeSource.ts | 32 +++++ .../types/vm/disk/V1ContainerDiskSource.ts | 44 +++++++ .../src/types/vm/disk/V1DataVolumeSource.ts | 26 ++++ .../src/types/vm/disk/V1Disk.ts | 79 ++++++++++++ .../src/types/vm/disk/V1DiskTarget.ts | 38 ++++++ .../src/types/vm/disk/V1EmptyDiskSource.ts | 26 ++++ .../types/vm/disk/V1EphemeralVolumeSource.ts | 28 +++++ .../src/types/vm/disk/V1FloppyTarget.ts | 32 +++++ .../src/types/vm/disk/V1HostDisk.ts | 44 +++++++ .../src/types/vm/disk/V1Initializer.ts | 26 ++++ .../src/types/vm/disk/V1Initializers.ts | 35 ++++++ .../src/types/vm/disk/V1LabelSelector.ts | 34 ++++++ .../vm/disk/V1LabelSelectorRequirement.ts | 38 ++++++ .../src/types/vm/disk/V1ListMeta.ts | 38 ++++++ .../types/vm/disk/V1LocalObjectReference.ts | 26 ++++ .../src/types/vm/disk/V1LunTarget.ts | 32 +++++ .../src/types/vm/disk/V1ObjectMeta.ts | 113 ++++++++++++++++++ .../src/types/vm/disk/V1OwnerReference.ts | 56 +++++++++ .../vm/disk/V1PersistentVolumeClaimSpec.ts | 66 ++++++++++ .../V1PersistentVolumeClaimVolumeSource.ts | 32 +++++ .../types/vm/disk/V1ResourceRequirements.ts | 38 ++++++ .../src/types/vm/disk/V1SecretVolumeSource.ts | 32 +++++ .../vm/disk/V1ServiceAccountVolumeSource.ts | 26 ++++ .../src/types/vm/disk/V1Status.ts | 71 +++++++++++ .../src/types/vm/disk/V1StatusCause.ts | 38 ++++++ .../src/types/vm/disk/V1StatusDetails.ts | 58 +++++++++ .../vm/disk/V1TypedLocalObjectReference.ts | 38 ++++++ .../src/types/vm/disk/V1Volume.ts | 104 ++++++++++++++++ .../src/types/vm/disk/V1alpha1DataVolume.ts | 54 +++++++++ .../types/vm/disk/V1alpha1DataVolumeSource.ts | 61 ++++++++++ .../vm/disk/V1alpha1DataVolumeSourceHTTP.ts | 38 ++++++ .../vm/disk/V1alpha1DataVolumeSourcePVC.ts | 32 +++++ .../disk/V1alpha1DataVolumeSourceRegistry.ts | 38 ++++++ .../vm/disk/V1alpha1DataVolumeSourceS3.ts | 32 +++++ .../types/vm/disk/V1alpha1DataVolumeSpec.ts | 41 +++++++ .../types/vm/disk/V1alpha1DataVolumeStatus.ts | 32 +++++ 39 files changed, 1732 insertions(+) create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CDRomTarget.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CloudInitConfigDriveSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CloudInitNoCloudSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ConfigMapVolumeSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ContainerDiskSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1DataVolumeSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Disk.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1DiskTarget.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1EmptyDiskSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1EphemeralVolumeSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1FloppyTarget.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1HostDisk.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Initializer.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Initializers.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LabelSelector.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LabelSelectorRequirement.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ListMeta.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LocalObjectReference.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LunTarget.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ObjectMeta.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1OwnerReference.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimSpec.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimVolumeSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ResourceRequirements.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1SecretVolumeSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ServiceAccountVolumeSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Status.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1StatusCause.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1StatusDetails.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1TypedLocalObjectReference.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Volume.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolume.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSource.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceHTTP.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourcePVC.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceRegistry.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceS3.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSpec.ts create mode 100644 frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeStatus.ts diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CDRomTarget.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CDRomTarget.ts new file mode 100644 index 00000000000..727304fd4be --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CDRomTarget.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface V1CDRomTarget + */ +export interface V1CDRomTarget { + /** + * Bus indicates the type of disk device to emulate. supported values: virtio, sata, scsi. + * @type {string} + * @memberof V1CDRomTarget + */ + bus?: string; + /** + * ReadOnly. Defaults to true. + * @type {boolean} + * @memberof V1CDRomTarget + */ + readonly?: boolean; + /** + * Tray indicates if the tray of the device is open or closed. Allowed values are \"open\" and \"closed\". Defaults to closed. +optional + * @type {string} + * @memberof V1CDRomTarget + */ + tray?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CloudInitConfigDriveSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CloudInitConfigDriveSource.ts new file mode 100644 index 00000000000..e8f5a731499 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CloudInitConfigDriveSource.ts @@ -0,0 +1,58 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1LocalObjectReference } from './V1LocalObjectReference'; + +/** + * Represents a cloud-init config drive user data source. More info: https://cloudinit.readthedocs.io/en/latest/topics/datasources/configdrive.html + * @export + * @interface V1CloudInitConfigDriveSource + */ +export interface V1CloudInitConfigDriveSource { + /** + * NetworkData contains config drive inline cloud-init networkdata. + optional + * @type {string} + * @memberof V1CloudInitConfigDriveSource + */ + networkData?: string; + /** + * NetworkDataBase64 contains config drive cloud-init networkdata as a base64 encoded string. + optional + * @type {string} + * @memberof V1CloudInitConfigDriveSource + */ + networkDataBase64?: string; + /** + * + * @type {V1LocalObjectReference} + * @memberof V1CloudInitConfigDriveSource + */ + networkDataSecretRef?: V1LocalObjectReference; + /** + * + * @type {V1LocalObjectReference} + * @memberof V1CloudInitConfigDriveSource + */ + secretRef?: V1LocalObjectReference; + /** + * UserData contains config drive inline cloud-init userdata. + optional + * @type {string} + * @memberof V1CloudInitConfigDriveSource + */ + userData?: string; + /** + * UserDataBase64 contains config drive cloud-init userdata as a base64 encoded string. + optional + * @type {string} + * @memberof V1CloudInitConfigDriveSource + */ + userDataBase64?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CloudInitNoCloudSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CloudInitNoCloudSource.ts new file mode 100644 index 00000000000..4605d57ecac --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1CloudInitNoCloudSource.ts @@ -0,0 +1,58 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1LocalObjectReference } from './V1LocalObjectReference'; + +/** + * Represents a cloud-init nocloud user data source. More info: http://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html + * @export + * @interface V1CloudInitNoCloudSource + */ +export interface V1CloudInitNoCloudSource { + /** + * NetworkData contains NoCloud inline cloud-init networkdata. + optional + * @type {string} + * @memberof V1CloudInitNoCloudSource + */ + networkData?: string; + /** + * NetworkDataBase64 contains NoCloud cloud-init networkdata as a base64 encoded string. + optional + * @type {string} + * @memberof V1CloudInitNoCloudSource + */ + networkDataBase64?: string; + /** + * + * @type {V1LocalObjectReference} + * @memberof V1CloudInitNoCloudSource + */ + networkDataSecretRef?: V1LocalObjectReference; + /** + * + * @type {V1LocalObjectReference} + * @memberof V1CloudInitNoCloudSource + */ + secretRef?: V1LocalObjectReference; + /** + * UserData contains NoCloud inline cloud-init userdata. + optional + * @type {string} + * @memberof V1CloudInitNoCloudSource + */ + userData?: string; + /** + * UserDataBase64 contains NoCloud cloud-init userdata as a base64 encoded string. + optional + * @type {string} + * @memberof V1CloudInitNoCloudSource + */ + userDataBase64?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ConfigMapVolumeSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ConfigMapVolumeSource.ts new file mode 100644 index 00000000000..8906314747d --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ConfigMapVolumeSource.ts @@ -0,0 +1,32 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * ConfigMapVolumeSource adapts a ConfigMap into a volume. More info: https://kubernetes.io/docs/concepts/storage/volumes/#configmap + * @export + * @interface V1ConfigMapVolumeSource + */ +export interface V1ConfigMapVolumeSource { + /** + * Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * @type {string} + * @memberof V1ConfigMapVolumeSource + */ + name?: string; + /** + * Specify whether the ConfigMap or it\'s keys must be defined +optional + * @type {boolean} + * @memberof V1ConfigMapVolumeSource + */ + optional?: boolean; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ContainerDiskSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ContainerDiskSource.ts new file mode 100644 index 00000000000..5ed16f9dc16 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ContainerDiskSource.ts @@ -0,0 +1,44 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * Represents a docker image with an embedded disk. + * @export + * @interface V1ContainerDiskSource + */ +export interface V1ContainerDiskSource { + /** + * Image is the name of the image with the embedded disk. + * @type {string} + * @memberof V1ContainerDiskSource + */ + image: string; + /** + * Image pull policy. One of Always, Never, IfNotPresent. Defaults to Always if :latest tag is specified, or IfNotPresent otherwise. Cannot be updated. More info: https://kubernetes.io/docs/concepts/containers/images#updating-images +optional + * @type {string} + * @memberof V1ContainerDiskSource + */ + imagePullPolicy?: string; + /** + * ImagePullSecret is the name of the Docker registry secret required to pull the image. The secret must already exist. + * @type {string} + * @memberof V1ContainerDiskSource + */ + imagePullSecret?: string; + /** + * Path defines the path to disk file in the container + * @type {string} + * @memberof V1ContainerDiskSource + */ + path?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1DataVolumeSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1DataVolumeSource.ts new file mode 100644 index 00000000000..e909043e51b --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1DataVolumeSource.ts @@ -0,0 +1,26 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface V1DataVolumeSource + */ +export interface V1DataVolumeSource { + /** + * Name represents the name of the DataVolume in the same namespace + * @type {string} + * @memberof V1DataVolumeSource + */ + name: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Disk.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Disk.ts new file mode 100644 index 00000000000..8d612ae2b20 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Disk.ts @@ -0,0 +1,79 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1CDRomTarget } from './V1CDRomTarget'; +import { V1DiskTarget } from './V1DiskTarget'; +import { V1FloppyTarget } from './V1FloppyTarget'; +import { V1LunTarget } from './V1LunTarget'; + +/** + * + * @export + * @interface V1Disk + */ +export interface V1Disk { + /** + * BootOrder is an integer value > 0, used to determine ordering of boot devices. Lower values take precedence. Each disk or interface that has a boot order must have a unique value. Disks without a boot order are not tried if a disk with a boot order exists. +optional + * @type {number} + * @memberof V1Disk + */ + bootOrder?: number; + /** + * Cache specifies which kvm disk cache mode should be used. +optional + * @type {string} + * @memberof V1Disk + */ + cache?: string; + /** + * + * @type {V1CDRomTarget} + * @memberof V1Disk + */ + cdrom?: V1CDRomTarget; + /** + * dedicatedIOThread indicates this disk should have an exclusive IO Thread. Enabling this implies useIOThreads = true. Defaults to false. +optional + * @type {boolean} + * @memberof V1Disk + */ + dedicatedIOThread?: boolean; + /** + * + * @type {V1DiskTarget} + * @memberof V1Disk + */ + disk?: V1DiskTarget; + /** + * + * @type {V1FloppyTarget} + * @memberof V1Disk + */ + floppy?: V1FloppyTarget; + /** + * + * @type {V1LunTarget} + * @memberof V1Disk + */ + lun?: V1LunTarget; + /** + * Name is the device name + * @type {string} + * @memberof V1Disk + */ + name: string; + /** + * Serial provides the ability to specify a serial number for the disk device. +optional + * @type {string} + * @memberof V1Disk + */ + serial?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1DiskTarget.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1DiskTarget.ts new file mode 100644 index 00000000000..1e18bb3a2c2 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1DiskTarget.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface V1DiskTarget + */ +export interface V1DiskTarget { + /** + * Bus indicates the type of disk device to emulate. supported values: virtio, sata, scsi. + * @type {string} + * @memberof V1DiskTarget + */ + bus?: string; + /** + * If specified, the virtual disk will be placed on the guests pci address with the specifed PCI address. For example: 0000:81:01.10 +optional + * @type {string} + * @memberof V1DiskTarget + */ + pciAddress?: string; + /** + * ReadOnly. Defaults to false. + * @type {boolean} + * @memberof V1DiskTarget + */ + readonly?: boolean; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1EmptyDiskSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1EmptyDiskSource.ts new file mode 100644 index 00000000000..b596424cf3d --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1EmptyDiskSource.ts @@ -0,0 +1,26 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * EmptyDisk represents a temporary disk which shares the vmis lifecycle. + * @export + * @interface V1EmptyDiskSource + */ +export interface V1EmptyDiskSource { + /** + * Capacity of the sparse disk. + * @type {string} + * @memberof V1EmptyDiskSource + */ + capacity: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1EphemeralVolumeSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1EphemeralVolumeSource.ts new file mode 100644 index 00000000000..4e68bb5afc7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1EphemeralVolumeSource.ts @@ -0,0 +1,28 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1PersistentVolumeClaimVolumeSource } from './V1PersistentVolumeClaimVolumeSource'; + +/** + * + * @export + * @interface V1EphemeralVolumeSource + */ +export interface V1EphemeralVolumeSource { + /** + * + * @type {V1PersistentVolumeClaimVolumeSource} + * @memberof V1EphemeralVolumeSource + */ + persistentVolumeClaim?: V1PersistentVolumeClaimVolumeSource; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1FloppyTarget.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1FloppyTarget.ts new file mode 100644 index 00000000000..01ebcf63e37 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1FloppyTarget.ts @@ -0,0 +1,32 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface V1FloppyTarget + */ +export interface V1FloppyTarget { + /** + * ReadOnly. Defaults to false. + * @type {boolean} + * @memberof V1FloppyTarget + */ + readonly?: boolean; + /** + * Tray indicates if the tray of the device is open or closed. Allowed values are \"open\" and \"closed\". Defaults to closed. +optional + * @type {string} + * @memberof V1FloppyTarget + */ + tray?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1HostDisk.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1HostDisk.ts new file mode 100644 index 00000000000..6c33c1a1fae --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1HostDisk.ts @@ -0,0 +1,44 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * Represents a disk created on the cluster level + * @export + * @interface V1HostDisk + */ +export interface V1HostDisk { + /** + * Capacity of the sparse disk +optional + * @type {string} + * @memberof V1HostDisk + */ + capacity?: string; + /** + * The path to HostDisk image located on the cluster + * @type {string} + * @memberof V1HostDisk + */ + path: string; + /** + * Shared indicate whether the path is shared between nodes + * @type {boolean} + * @memberof V1HostDisk + */ + shared?: boolean; + /** + * Contains information if disk.img exists or should be created allowed options are \'Disk\' and \'DiskOrCreate\' + * @type {string} + * @memberof V1HostDisk + */ + type: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Initializer.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Initializer.ts new file mode 100644 index 00000000000..8fc95dcdcf7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Initializer.ts @@ -0,0 +1,26 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * Initializer is information about an initializer that has not yet completed. + * @export + * @interface V1Initializer + */ +export interface V1Initializer { + /** + * name of the process that is responsible for initializing this object. + * @type {string} + * @memberof V1Initializer + */ + name: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Initializers.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Initializers.ts new file mode 100644 index 00000000000..36e7ca29cb9 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Initializers.ts @@ -0,0 +1,35 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1Initializer } from './V1Initializer'; +import { V1Status } from './V1Status'; + +/** + * Initializers tracks the progress of initialization. + * @export + * @interface V1Initializers + */ +export interface V1Initializers { + /** + * Pending is a list of initializers that must execute in order before this object is visible. When the last pending initializer is removed, and no failing result is set, the initializers struct will be set to nil and the object is considered as initialized and visible to all clients. + * @type {Array} + * @memberof V1Initializers + */ + pending: V1Initializer[]; + /** + * + * @type {V1Status} + * @memberof V1Initializers + */ + result?: V1Status; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LabelSelector.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LabelSelector.ts new file mode 100644 index 00000000000..16c4efb3014 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LabelSelector.ts @@ -0,0 +1,34 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1LabelSelectorRequirement } from './V1LabelSelectorRequirement'; + +/** + * A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects. + * @export + * @interface V1LabelSelector + */ +export interface V1LabelSelector { + /** + * matchExpressions is a list of label selector requirements. The requirements are ANDed. + * @type {Array} + * @memberof V1LabelSelector + */ + matchExpressions?: V1LabelSelectorRequirement[]; + /** + * matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed. + * @type {object} + * @memberof V1LabelSelector + */ + matchLabels?: object; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LabelSelectorRequirement.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LabelSelectorRequirement.ts new file mode 100644 index 00000000000..ae893253677 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LabelSelectorRequirement.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. + * @export + * @interface V1LabelSelectorRequirement + */ +export interface V1LabelSelectorRequirement { + /** + * key is the label key that the selector applies to. + * @type {string} + * @memberof V1LabelSelectorRequirement + */ + key: string; + /** + * operator represents a key\'s relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. + * @type {string} + * @memberof V1LabelSelectorRequirement + */ + operator: string; + /** + * values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. + * @type {Array} + * @memberof V1LabelSelectorRequirement + */ + values?: string[]; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ListMeta.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ListMeta.ts new file mode 100644 index 00000000000..622d0b3bcaa --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ListMeta.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}. + * @export + * @interface V1ListMeta + */ +export interface V1ListMeta { + /** + * continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message. + * @type {string} + * @memberof V1ListMeta + */ + _continue?: string; + /** + * String that identifies the server\'s internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#concurrency-control-and-consistency + * @type {string} + * @memberof V1ListMeta + */ + resourceVersion?: string; + /** + * selfLink is a URL representing this object. Populated by the system. Read-only. + * @type {string} + * @memberof V1ListMeta + */ + selfLink?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LocalObjectReference.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LocalObjectReference.ts new file mode 100644 index 00000000000..7af7145976b --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LocalObjectReference.ts @@ -0,0 +1,26 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace. + * @export + * @interface V1LocalObjectReference + */ +export interface V1LocalObjectReference { + /** + * Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * @type {string} + * @memberof V1LocalObjectReference + */ + name?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LunTarget.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LunTarget.ts new file mode 100644 index 00000000000..4c64cc5b568 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1LunTarget.ts @@ -0,0 +1,32 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface V1LunTarget + */ +export interface V1LunTarget { + /** + * Bus indicates the type of disk device to emulate. supported values: virtio, sata, scsi. + * @type {string} + * @memberof V1LunTarget + */ + bus?: string; + /** + * ReadOnly. Defaults to false. + * @type {boolean} + * @memberof V1LunTarget + */ + readonly?: boolean; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ObjectMeta.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ObjectMeta.ts new file mode 100644 index 00000000000..c5ab19d5f05 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ObjectMeta.ts @@ -0,0 +1,113 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1Initializers } from './V1Initializers'; +import { V1OwnerReference } from './V1OwnerReference'; + +/** + * ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create. + * @export + * @interface V1ObjectMeta + */ +export interface V1ObjectMeta { + /** + * Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: http://kubernetes.io/docs/user-guide/annotations + * @type {object} + * @memberof V1ObjectMeta + */ + annotations?: object; + /** + * The name of the cluster which the object belongs to. This is used to distinguish resources with same name and namespace in different clusters. This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request. + * @type {string} + * @memberof V1ObjectMeta + */ + clusterName?: string; + /** + * Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only. + * @type {number} + * @memberof V1ObjectMeta + */ + deletionGracePeriodSeconds?: number; + /** + * DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested. Populated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + * @type {string} + * @memberof V1ObjectMeta + */ + deletionTimestamp?: string; + /** + * Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. + * @type {Array} + * @memberof V1ObjectMeta + */ + finalizers?: string[]; + /** + * GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server. If this field is specified and the generated name exists, the server will NOT return a 409 - instead, it will either return 201 Created or 500 with Reason ServerTimeout indicating a unique name could not be found in the time allotted, and the client should retry (optionally after the time indicated in the Retry-After header). Applied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#idempotency + * @type {string} + * @memberof V1ObjectMeta + */ + generateName?: string; + /** + * A sequence number representing a specific generation of the desired state. Populated by the system. Read-only. + * @type {number} + * @memberof V1ObjectMeta + */ + generation?: number; + /** + * + * @type {V1Initializers} + * @memberof V1ObjectMeta + */ + initializers?: V1Initializers; + /** + * Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: http://kubernetes.io/docs/user-guide/labels + * @type {object} + * @memberof V1ObjectMeta + */ + labels?: object; + /** + * Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names + * @type {string} + * @memberof V1ObjectMeta + */ + name?: string; + /** + * Namespace defines the space within 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. Must be a DNS_LABEL. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/namespaces + * @type {string} + * @memberof V1ObjectMeta + */ + namespace?: string; + /** + * 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. + * @type {Array} + * @memberof V1ObjectMeta + */ + ownerReferences?: V1OwnerReference[]; + /** + * An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources. Populated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#concurrency-control-and-consistency + * @type {string} + * @memberof V1ObjectMeta + */ + resourceVersion?: string; + /** + * SelfLink is a URL representing this object. Populated by the system. Read-only. + * @type {string} + * @memberof V1ObjectMeta + */ + selfLink?: string; + /** + * UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations. Populated by the system. Read-only. More info: http://kubernetes.io/docs/user-guide/identifiers#uids + * @type {string} + * @memberof V1ObjectMeta + */ + uid?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1OwnerReference.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1OwnerReference.ts new file mode 100644 index 00000000000..29838e3358c --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1OwnerReference.ts @@ -0,0 +1,56 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field. + * @export + * @interface V1OwnerReference + */ +export interface V1OwnerReference { + /** + * API version of the referent. + * @type {string} + * @memberof V1OwnerReference + */ + apiVersion: string; + /** + * If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned. + * @type {boolean} + * @memberof V1OwnerReference + */ + blockOwnerDeletion?: boolean; + /** + * If true, this reference points to the managing controller. + * @type {boolean} + * @memberof V1OwnerReference + */ + controller?: boolean; + /** + * Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds + * @type {string} + * @memberof V1OwnerReference + */ + kind: string; + /** + * Name of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#names + * @type {string} + * @memberof V1OwnerReference + */ + name: string; + /** + * UID of the referent. More info: http://kubernetes.io/docs/user-guide/identifiers#uids + * @type {string} + * @memberof V1OwnerReference + */ + uid: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimSpec.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimSpec.ts new file mode 100644 index 00000000000..89100dc8caa --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimSpec.ts @@ -0,0 +1,66 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1LabelSelector } from './V1LabelSelector'; +import { V1ResourceRequirements } from './V1ResourceRequirements'; +import { V1TypedLocalObjectReference } from './V1TypedLocalObjectReference'; + +/** + * PersistentVolumeClaimSpec describes the common attributes of storage devices and allows a Source for provider-specific attributes + * @export + * @interface V1PersistentVolumeClaimSpec + */ +export interface V1PersistentVolumeClaimSpec { + /** + * AccessModes contains the desired access modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + * @type {Array} + * @memberof V1PersistentVolumeClaimSpec + */ + accessModes?: object[]; + /** + * + * @type {V1TypedLocalObjectReference} + * @memberof V1PersistentVolumeClaimSpec + */ + dataSource?: V1TypedLocalObjectReference; + /** + * + * @type {V1ResourceRequirements} + * @memberof V1PersistentVolumeClaimSpec + */ + resources?: V1ResourceRequirements; + /** + * + * @type {V1LabelSelector} + * @memberof V1PersistentVolumeClaimSpec + */ + selector?: V1LabelSelector; + /** + * Name of the StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + * @type {string} + * @memberof V1PersistentVolumeClaimSpec + */ + storageClassName?: string; + /** + * + * @type {object} + * @memberof V1PersistentVolumeClaimSpec + */ + volumeMode?: object; + /** + * VolumeName is the binding reference to the PersistentVolume backing this claim. + * @type {string} + * @memberof V1PersistentVolumeClaimSpec + */ + volumeName?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimVolumeSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimVolumeSource.ts new file mode 100644 index 00000000000..52009b75fc2 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimVolumeSource.ts @@ -0,0 +1,32 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * PersistentVolumeClaimVolumeSource references the user\'s PVC in the same namespace. This volume finds the bound PV and mounts that volume for the pod. A PersistentVolumeClaimVolumeSource is, essentially, a wrapper around another type of volume that is owned by someone else (the system). + * @export + * @interface V1PersistentVolumeClaimVolumeSource + */ +export interface V1PersistentVolumeClaimVolumeSource { + /** + * ClaimName is the name of a PersistentVolumeClaim in the same namespace as the pod using this volume. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims + * @type {string} + * @memberof V1PersistentVolumeClaimVolumeSource + */ + claimName: string; + /** + * Will force the ReadOnly setting in VolumeMounts. Default false. + * @type {boolean} + * @memberof V1PersistentVolumeClaimVolumeSource + */ + readOnly?: boolean; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ResourceRequirements.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ResourceRequirements.ts new file mode 100644 index 00000000000..b2f18b4fe31 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ResourceRequirements.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * + * @export + * @interface V1ResourceRequirements + */ +export interface V1ResourceRequirements { + /** + * Limits describes the maximum amount of compute resources allowed. Valid resource keys are \"memory\" and \"cpu\". +optional + * @type {object} + * @memberof V1ResourceRequirements + */ + limits?: object; + /** + * Don\'t ask the scheduler to take the guest-management overhead into account. Instead put the overhead only into the container\'s memory limit. This can lead to crashes if all memory is in use on a node. Defaults to false. + * @type {boolean} + * @memberof V1ResourceRequirements + */ + overcommitGuestOverhead?: boolean; + /** + * Requests is a description of the initial vmi resources. Valid resource keys are \"memory\" and \"cpu\". +optional + * @type {object} + * @memberof V1ResourceRequirements + */ + requests?: object; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1SecretVolumeSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1SecretVolumeSource.ts new file mode 100644 index 00000000000..f23ee5ad78c --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1SecretVolumeSource.ts @@ -0,0 +1,32 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * SecretVolumeSource adapts a Secret into a volume. + * @export + * @interface V1SecretVolumeSource + */ +export interface V1SecretVolumeSource { + /** + * Specify whether the Secret or it\'s keys must be defined +optional + * @type {boolean} + * @memberof V1SecretVolumeSource + */ + optional?: boolean; + /** + * Name of the secret in the pod\'s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret +optional + * @type {string} + * @memberof V1SecretVolumeSource + */ + secretName?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ServiceAccountVolumeSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ServiceAccountVolumeSource.ts new file mode 100644 index 00000000000..bedfd0c5a98 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ServiceAccountVolumeSource.ts @@ -0,0 +1,26 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * ServiceAccountVolumeSource adapts a ServiceAccount into a volume. + * @export + * @interface V1ServiceAccountVolumeSource + */ +export interface V1ServiceAccountVolumeSource { + /** + * Name of the service account in the pod\'s namespace to use. More info: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ + * @type {string} + * @memberof V1ServiceAccountVolumeSource + */ + serviceAccountName?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Status.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Status.ts new file mode 100644 index 00000000000..e6f32857165 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Status.ts @@ -0,0 +1,71 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1ListMeta } from './V1ListMeta'; +import { V1StatusDetails } from './V1StatusDetails'; + +/** + * Status is a return value for calls that don\'t return other objects. + * @export + * @interface V1Status + */ +export interface V1Status { + /** + * APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources + * @type {string} + * @memberof V1Status + */ + apiVersion?: string; + /** + * Suggested HTTP return code for this status, 0 if not set. + * @type {number} + * @memberof V1Status + */ + code?: number; + /** + * + * @type {V1StatusDetails} + * @memberof V1Status + */ + details?: V1StatusDetails; + /** + * Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds + * @type {string} + * @memberof V1Status + */ + kind?: string; + /** + * A human-readable description of the status of this operation. + * @type {string} + * @memberof V1Status + */ + message?: string; + /** + * + * @type {V1ListMeta} + * @memberof V1Status + */ + metadata?: V1ListMeta; + /** + * A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it. + * @type {string} + * @memberof V1Status + */ + reason?: string; + /** + * Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status + * @type {string} + * @memberof V1Status + */ + status?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1StatusCause.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1StatusCause.ts new file mode 100644 index 00000000000..c1b297fd8e5 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1StatusCause.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered. + * @export + * @interface V1StatusCause + */ +export interface V1StatusCause { + /** + * The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional. Examples: \"name\" - the field \"name\" on the current resource \"items[0].name\" - the field \"name\" on the first array entry in \"items\" + * @type {string} + * @memberof V1StatusCause + */ + field?: string; + /** + * A human-readable description of the cause of the error. This field may be presented as-is to a reader. + * @type {string} + * @memberof V1StatusCause + */ + message?: string; + /** + * A machine-readable description of the cause of the error. If this value is empty there is no information available. + * @type {string} + * @memberof V1StatusCause + */ + reason?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1StatusDetails.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1StatusDetails.ts new file mode 100644 index 00000000000..917d6b4d68a --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1StatusDetails.ts @@ -0,0 +1,58 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1StatusCause } from './V1StatusCause'; + +/** + * StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined. + * @export + * @interface V1StatusDetails + */ +export interface V1StatusDetails { + /** + * The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes. + * @type {Array} + * @memberof V1StatusDetails + */ + causes?: V1StatusCause[]; + /** + * The group attribute of the resource associated with the status StatusReason. + * @type {string} + * @memberof V1StatusDetails + */ + group?: string; + /** + * The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds + * @type {string} + * @memberof V1StatusDetails + */ + kind?: string; + /** + * The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described). + * @type {string} + * @memberof V1StatusDetails + */ + name?: string; + /** + * If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action. + * @type {number} + * @memberof V1StatusDetails + */ + retryAfterSeconds?: number; + /** + * UID of the resource. (when there is a single resource which can be described). More info: http://kubernetes.io/docs/user-guide/identifiers#uids + * @type {string} + * @memberof V1StatusDetails + */ + uid?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1TypedLocalObjectReference.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1TypedLocalObjectReference.ts new file mode 100644 index 00000000000..1c80d4b0ef0 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1TypedLocalObjectReference.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * TypedLocalObjectReference contains enough information to let you locate the typed referenced object inside the same namespace. + * @export + * @interface V1TypedLocalObjectReference + */ +export interface V1TypedLocalObjectReference { + /** + * APIGroup is the group for the resource being referenced. If APIGroup is not specified, the specified Kind must be in the core API group. For any other third-party types, APIGroup is required. + * @type {string} + * @memberof V1TypedLocalObjectReference + */ + apiGroup: string; + /** + * Kind is the type of resource being referenced + * @type {string} + * @memberof V1TypedLocalObjectReference + */ + kind: string; + /** + * Name is the name of resource being referenced + * @type {string} + * @memberof V1TypedLocalObjectReference + */ + name: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Volume.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Volume.ts new file mode 100644 index 00000000000..dc54d65d1f4 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1Volume.ts @@ -0,0 +1,104 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1CloudInitConfigDriveSource } from './V1CloudInitConfigDriveSource'; +import { V1CloudInitNoCloudSource } from './V1CloudInitNoCloudSource'; +import { V1ConfigMapVolumeSource } from './V1ConfigMapVolumeSource'; +import { V1ContainerDiskSource } from './V1ContainerDiskSource'; +import { V1DataVolumeSource } from './V1DataVolumeSource'; +import { V1EmptyDiskSource } from './V1EmptyDiskSource'; +import { V1EphemeralVolumeSource } from './V1EphemeralVolumeSource'; +import { V1HostDisk } from './V1HostDisk'; +import { V1PersistentVolumeClaimVolumeSource } from './V1PersistentVolumeClaimVolumeSource'; +import { V1SecretVolumeSource } from './V1SecretVolumeSource'; +import { V1ServiceAccountVolumeSource } from './V1ServiceAccountVolumeSource'; + +/** + * Volume represents a named volume in a vmi. + * @export + * @interface V1Volume + */ +export interface V1Volume { + /** + * + * @type {V1CloudInitConfigDriveSource} + * @memberof V1Volume + */ + cloudInitConfigDrive?: V1CloudInitConfigDriveSource; + /** + * + * @type {V1CloudInitNoCloudSource} + * @memberof V1Volume + */ + cloudInitNoCloud?: V1CloudInitNoCloudSource; + /** + * + * @type {V1ConfigMapVolumeSource} + * @memberof V1Volume + */ + configMap?: V1ConfigMapVolumeSource; + /** + * + * @type {V1ContainerDiskSource} + * @memberof V1Volume + */ + containerDisk?: V1ContainerDiskSource; + /** + * + * @type {V1DataVolumeSource} + * @memberof V1Volume + */ + dataVolume?: V1DataVolumeSource; + /** + * + * @type {V1EmptyDiskSource} + * @memberof V1Volume + */ + emptyDisk?: V1EmptyDiskSource; + /** + * + * @type {V1EphemeralVolumeSource} + * @memberof V1Volume + */ + ephemeral?: V1EphemeralVolumeSource; + /** + * + * @type {V1HostDisk} + * @memberof V1Volume + */ + hostDisk?: V1HostDisk; + /** + * Volume\'s name. Must be a DNS_LABEL and unique within the vmi. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + * @type {string} + * @memberof V1Volume + */ + name: string; + /** + * + * @type {V1PersistentVolumeClaimVolumeSource} + * @memberof V1Volume + */ + persistentVolumeClaim?: V1PersistentVolumeClaimVolumeSource; + /** + * + * @type {V1SecretVolumeSource} + * @memberof V1Volume + */ + secret?: V1SecretVolumeSource; + /** + * + * @type {V1ServiceAccountVolumeSource} + * @memberof V1Volume + */ + serviceAccount?: V1ServiceAccountVolumeSource; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolume.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolume.ts new file mode 100644 index 00000000000..27fe82345bf --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolume.ts @@ -0,0 +1,54 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1ObjectMeta } from './V1ObjectMeta'; +import { V1alpha1DataVolumeSpec } from './V1alpha1DataVolumeSpec'; +import { V1alpha1DataVolumeStatus } from './V1alpha1DataVolumeStatus'; + +/** + * DataVolume provides a representation of our data volume +genclient +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + * @export + * @interface V1alpha1DataVolume + */ +export interface V1alpha1DataVolume { + /** + * APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources + * @type {string} + * @memberof V1alpha1DataVolume + */ + apiVersion?: string; + /** + * Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds + * @type {string} + * @memberof V1alpha1DataVolume + */ + kind?: string; + /** + * + * @type {V1ObjectMeta} + * @memberof V1alpha1DataVolume + */ + metadata?: V1ObjectMeta; + /** + * + * @type {V1alpha1DataVolumeSpec} + * @memberof V1alpha1DataVolume + */ + spec: V1alpha1DataVolumeSpec; + /** + * + * @type {V1alpha1DataVolumeStatus} + * @memberof V1alpha1DataVolume + */ + status?: V1alpha1DataVolumeStatus; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSource.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSource.ts new file mode 100644 index 00000000000..cec7495735e --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSource.ts @@ -0,0 +1,61 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1alpha1DataVolumeSourceHTTP } from './V1alpha1DataVolumeSourceHTTP'; +import { V1alpha1DataVolumeSourcePVC } from './V1alpha1DataVolumeSourcePVC'; +import { V1alpha1DataVolumeSourceRegistry } from './V1alpha1DataVolumeSourceRegistry'; +import { V1alpha1DataVolumeSourceS3 } from './V1alpha1DataVolumeSourceS3'; + +/** + * DataVolumeSource represents the source for our Data Volume, this can be HTTP, S3, Registry or an existing PVC + * @export + * @interface V1alpha1DataVolumeSource + */ +export interface V1alpha1DataVolumeSource { + /** + * DataVolumeBlankImage provides the parameters to create a new raw blank image for the PVC + * @type {object} + * @memberof V1alpha1DataVolumeSource + */ + blank?: object; + /** + * + * @type {V1alpha1DataVolumeSourceHTTP} + * @memberof V1alpha1DataVolumeSource + */ + http?: V1alpha1DataVolumeSourceHTTP; + /** + * + * @type {V1alpha1DataVolumeSourcePVC} + * @memberof V1alpha1DataVolumeSource + */ + pvc?: V1alpha1DataVolumeSourcePVC; + /** + * + * @type {V1alpha1DataVolumeSourceRegistry} + * @memberof V1alpha1DataVolumeSource + */ + registry?: V1alpha1DataVolumeSourceRegistry; + /** + * + * @type {V1alpha1DataVolumeSourceS3} + * @memberof V1alpha1DataVolumeSource + */ + s3?: V1alpha1DataVolumeSourceS3; + /** + * DataVolumeSourceUpload provides the parameters to create a Data Volume by uploading the source + * @type {object} + * @memberof V1alpha1DataVolumeSource + */ + upload?: object; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceHTTP.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceHTTP.ts new file mode 100644 index 00000000000..66b962671de --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceHTTP.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * DataVolumeSourceHTTP provides the parameters to create a Data Volume from an HTTP source + * @export + * @interface V1alpha1DataVolumeSourceHTTP + */ +export interface V1alpha1DataVolumeSourceHTTP { + /** + * CertConfigMap provides a reference to the Registry certs + * @type {string} + * @memberof V1alpha1DataVolumeSourceHTTP + */ + certConfigMap?: string; + /** + * SecretRef provides the secret reference needed to access the HTTP source + * @type {string} + * @memberof V1alpha1DataVolumeSourceHTTP + */ + secretRef?: string; + /** + * URL is the URL of the http source + * @type {string} + * @memberof V1alpha1DataVolumeSourceHTTP + */ + url?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourcePVC.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourcePVC.ts new file mode 100644 index 00000000000..0de02d097c7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourcePVC.ts @@ -0,0 +1,32 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * DataVolumeSourcePVC provides the parameters to create a Data Volume from an existing PVC + * @export + * @interface V1alpha1DataVolumeSourcePVC + */ +export interface V1alpha1DataVolumeSourcePVC { + /** + * + * @type {string} + * @memberof V1alpha1DataVolumeSourcePVC + */ + name?: string; + /** + * + * @type {string} + * @memberof V1alpha1DataVolumeSourcePVC + */ + namespace?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceRegistry.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceRegistry.ts new file mode 100644 index 00000000000..144ffb0f9aa --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceRegistry.ts @@ -0,0 +1,38 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * DataVolumeSourceRegistry provides the parameters to create a Data Volume from an registry source + * @export + * @interface V1alpha1DataVolumeSourceRegistry + */ +export interface V1alpha1DataVolumeSourceRegistry { + /** + * CertConfigMap provides a reference to the Registry certs + * @type {string} + * @memberof V1alpha1DataVolumeSourceRegistry + */ + certConfigMap?: string; + /** + * SecretRef provides the secret reference needed to access the Registry source + * @type {string} + * @memberof V1alpha1DataVolumeSourceRegistry + */ + secretRef?: string; + /** + * URL is the url of the Registry source + * @type {string} + * @memberof V1alpha1DataVolumeSourceRegistry + */ + url?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceS3.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceS3.ts new file mode 100644 index 00000000000..b9597159bda --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSourceS3.ts @@ -0,0 +1,32 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * DataVolumeSourceS3 provides the parameters to create a Data Volume from an S3 source + * @export + * @interface V1alpha1DataVolumeSourceS3 + */ +export interface V1alpha1DataVolumeSourceS3 { + /** + * SecretRef provides the secret reference needed to access the S3 source + * @type {string} + * @memberof V1alpha1DataVolumeSourceS3 + */ + secretRef?: string; + /** + * URL is the url of the S3 source + * @type {string} + * @memberof V1alpha1DataVolumeSourceS3 + */ + url?: string; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSpec.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSpec.ts new file mode 100644 index 00000000000..ec125acb7ba --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeSpec.ts @@ -0,0 +1,41 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { V1PersistentVolumeClaimSpec } from './V1PersistentVolumeClaimSpec'; +import { V1alpha1DataVolumeSource } from './V1alpha1DataVolumeSource'; + +/** + * DataVolumeSpec defines our specification for a DataVolume type + * @export + * @interface V1alpha1DataVolumeSpec + */ +export interface V1alpha1DataVolumeSpec { + /** + * DataVolumeContentType options: \"kubevirt\", \"archive\" + * @type {string} + * @memberof V1alpha1DataVolumeSpec + */ + contentType?: string; + /** + * + * @type {V1PersistentVolumeClaimSpec} + * @memberof V1alpha1DataVolumeSpec + */ + pvc: V1PersistentVolumeClaimSpec; + /** + * + * @type {V1alpha1DataVolumeSource} + * @memberof V1alpha1DataVolumeSpec + */ + source: V1alpha1DataVolumeSource; +} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeStatus.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeStatus.ts new file mode 100644 index 00000000000..4305660e26a --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1alpha1DataVolumeStatus.ts @@ -0,0 +1,32 @@ +// tslint:disable +/** + * KubeVirt API + * This is KubeVirt API an add-on for Kubernetes. + * + * The version of the OpenAPI document: 1.0.0 + * Contact: kubevirt-dev@googlegroups.com + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +/** + * DataVolumeStatus provides the parameters to store the phase of the Data Volume + * @export + * @interface V1alpha1DataVolumeStatus + */ +export interface V1alpha1DataVolumeStatus { + /** + * Phase is the current phase of the data volume + * @type {string} + * @memberof V1alpha1DataVolumeStatus + */ + phase?: string; + /** + * + * @type {string} + * @memberof V1alpha1DataVolumeStatus + */ + progress?: string; +} From 0d69dff8452f8e7784531e6ff6f3908a6844e247 Mon Sep 17 00:00:00 2001 From: suomiy Date: Fri, 27 Sep 2019 18:16:47 +0200 Subject: [PATCH 4/6] kubevirt: refactor createDiskRow to DiskModal - add edit nic functionality - add sources (Container, URL, Attach PVC, Attach Cloned PVC)) --- .../storage-tab-initial-state.ts | 5 +- .../src/components/form/form-row.tsx | 9 +- .../form/k8s-resource-select-row.tsx | 79 ++++ .../components/form/size-unit-form-row.scss | 7 + .../components/form/size-unit-form-row.tsx | 72 ++++ .../delete-device-modal.tsx | 4 +- .../modals/disk-modal/disk-modal-enhanced.tsx | 152 ++++++++ .../modals/disk-modal/disk-modal.tsx | 355 ++++++++++++++++++ .../src/components/modals/disk-modal/index.ts | 1 + .../modals/disk-modal/storage-ui-source.ts | 97 +++++ .../modals/nic-modal/nic-modal-enhanced.tsx | 4 +- .../components/modals/nic-modal/nic-modal.tsx | 6 +- .../vm-disks/_create-device-row.scss | 21 -- .../components/vm-disks/create-disk-row.tsx | 167 -------- .../src/components/vm-disks/disk-row.tsx | 168 +++++++-- .../src/components/vm-disks/types.ts | 51 +-- .../src/components/vm-disks/utils.ts | 10 + .../src/components/vm-disks/vm-disks.tsx | 231 ++++++------ .../src/components/vm-nics/nic-row.tsx | 9 +- .../src/constants/pvc/constants.ts | 8 + .../src/constants/pvc/index.ts | 1 + .../kubevirt-plugin/src/constants/vm/index.ts | 1 + .../src/constants/vm/storage.ts | 4 - .../vm/storage/data-volume-source-type.ts | 31 ++ .../src/constants/vm/storage/disk-bus.ts | 35 ++ .../src/constants/vm/storage/disk-type.ts | 28 ++ .../src/constants/vm/storage/index.ts | 4 + .../src/constants/vm/storage/volume-type.ts | 34 ++ .../src/k8s/patches/vm/utils.ts | 1 + .../src/k8s/patches/vm/vm-disk-patches.ts | 135 +++++-- .../src/k8s/patches/vm/vm-nic-patches.ts | 12 +- .../config-map/storage-class/constants.ts | 3 + .../config-map/storage-class/index.ts | 1 + .../storage-class/storageClassConfigMap.ts | 44 +++ .../kubevirt-plugin/src/k8s/utils/patch.ts | 22 +- .../src/k8s/wrapper/vm/data-volume-wrapper.ts | 129 +++++++ .../src/k8s/wrapper/vm/disk-wrapper.ts | 56 +++ .../src/k8s/wrapper/vm/volume-wrapper.ts | 82 ++++ .../src/selectors/config-map/sc-defaults.ts | 42 +++ .../src/selectors/vm/volume.ts | 3 + .../vm/disk/V1PersistentVolumeClaimSpec.ts | 4 +- .../src/utils/validations/strings.ts | 2 + .../src/utils/validations/vm/disk.ts | 116 +++++- 43 files changed, 1806 insertions(+), 440 deletions(-) create mode 100644 frontend/packages/kubevirt-plugin/src/components/form/k8s-resource-select-row.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/form/size-unit-form-row.scss create mode 100644 frontend/packages/kubevirt-plugin/src/components/form/size-unit-form-row.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/index.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/storage-ui-source.ts delete mode 100644 frontend/packages/kubevirt-plugin/src/components/vm-disks/_create-device-row.scss delete mode 100644 frontend/packages/kubevirt-plugin/src/components/vm-disks/create-disk-row.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/vm-disks/utils.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/pvc/constants.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/pvc/index.ts delete mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/storage.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/storage/data-volume-source-type.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/storage/disk-bus.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/storage/disk-type.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/storage/index.ts create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/storage/volume-type.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/constants.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/index.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/storageClassConfigMap.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/data-volume-wrapper.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/disk-wrapper.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/volume-wrapper.ts create mode 100644 frontend/packages/kubevirt-plugin/src/selectors/config-map/sc-defaults.ts diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts index 143fe5fa496..f085e722531 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts @@ -1,6 +1,5 @@ // left intentionally empty import { ProvisionSource } from '../../../../types/vm'; -import { StorageType } from '../../../../constants/vm/storage'; const rootDisk = { rootStorage: {}, @@ -9,11 +8,11 @@ const rootDisk = { }; export const rootContainerDisk = { ...rootDisk, - storageType: StorageType.CONTAINER, + // storageType: StorageType.CONTAINER, TODO!! }; export const rootDataVolumeDisk = { ...rootDisk, - storageType: StorageType.DATAVOLUME, + // storageType: StorageType.DATAVOLUME, TODO!! size: 10, }; export const getInitialDisk = (provisionSource: ProvisionSource) => { diff --git a/frontend/packages/kubevirt-plugin/src/components/form/form-row.tsx b/frontend/packages/kubevirt-plugin/src/components/form/form-row.tsx index 7ff3f7f5622..9b2080d1a99 100644 --- a/frontend/packages/kubevirt-plugin/src/components/form/form-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/form/form-row.tsx @@ -16,6 +16,7 @@ export const FormRow: React.FC = ({ isLoading, validationMessage, validationType, + validation, children, }) => { if (isHidden) { @@ -27,8 +28,8 @@ export const FormRow: React.FC = ({ label={title} isRequired={isRequired} fieldId={fieldId} - isValid={validationType !== ValidationErrorType.Error} - helperTextInvalid={validationMessage} + isValid={((validation && validation.type) || validationType) !== ValidationErrorType.Error} + helperTextInvalid={(validation && validation.message) || validationMessage} > {help && ( @@ -63,5 +64,9 @@ type FormRowProps = { isLoading?: boolean; validationMessage?: string; validationType?: ValidationErrorType; + validation?: { + message?: string; + type?: ValidationErrorType; + }; children?: React.ReactNode; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/form/k8s-resource-select-row.tsx b/frontend/packages/kubevirt-plugin/src/components/form/k8s-resource-select-row.tsx new file mode 100644 index 00000000000..322389ac1d2 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/form/k8s-resource-select-row.tsx @@ -0,0 +1,79 @@ +import { FirehoseResult } from '@console/internal/components/utils'; +import { K8sKind, K8sResourceKind } from '@console/internal/module/k8s'; +import * as React from 'react'; +import { FormSelect, FormSelectOption } from '@patternfly/react-core'; +import { getName } from '@console/shared/src'; +import { ValidationErrorType, ValidationObject } from '../../utils/validations/types'; +import { getLoadedData, getLoadError, isLoaded } from '../../utils'; +import { ignoreCaseSort } from '../../utils/sort'; +import { FormRow } from './form-row'; +import { asFormSelectValue, FormSelectPlaceholderOption } from './form-select-placeholder-option'; + +type K8sResourceSelectProps = { + id: string; + isDisabled: boolean; + isPlaceholderDisabled?: boolean; + hasPlaceholder?: boolean; + data?: FirehoseResult; + name?: string; + onChange: (name: string) => void; + model: K8sKind; + title?: string; + validation?: ValidationObject; + filter?: (obj: K8sResourceKind) => boolean; +}; + +export const K8sResourceSelectRow: React.FC = ({ + id, + isDisabled, + isPlaceholderDisabled, + hasPlaceholder, + data, + onChange, + name, + model, + title, + validation, + filter, +}) => { + const isLoading = !isLoaded(data); + const loadError = getLoadError(data, model); + + let loadedData = getLoadedData(data, []); + + if (filter) { + loadedData = loadedData.filter(filter); + } + + return ( + + + {hasPlaceholder && ( + + )} + {ignoreCaseSort(loadedData, ['metadata', 'name']).map((entity) => { + const selectName = getName(entity); + return ; + })} + + + ); +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/form/size-unit-form-row.scss b/frontend/packages/kubevirt-plugin/src/components/form/size-unit-form-row.scss new file mode 100644 index 00000000000..780e741e45e --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/form/size-unit-form-row.scss @@ -0,0 +1,7 @@ +.kubevirt-size-unit-form-row__size { + max-width: 100% !important; +} + +.kubevirt-size-unit-form-row__unit { + width: 6em; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/form/size-unit-form-row.tsx b/frontend/packages/kubevirt-plugin/src/components/form/size-unit-form-row.tsx new file mode 100644 index 00000000000..227d1ceaa15 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/form/size-unit-form-row.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { FormSelect, FormSelectOption, Split, SplitItem } from '@patternfly/react-core'; +import { ValidationObject } from '../../utils/validations/types'; +import { prefixedID } from '../../utils'; +import { getStringEnumValues } from '../../utils/types'; +import { FormRow } from './form-row'; +import { Integer } from './integer/integer'; + +import './size-unit-form-row.scss'; + +export enum BinaryUnit { + Mi = 'Mi', + Gi = 'Gi', + Ti = 'Ti', +} + +type SizeUnitFormRowProps = { + size: string; + title?: string; + unit: BinaryUnit; + validation: ValidationObject; + id?: string; + isDisabled?: boolean; + isRequired?: boolean; + onSizeChanged: (size: string) => void; + onUnitChanged: (unit: BinaryUnit) => void; +}; +export const SizeUnitFormRow: React.FC = ({ + title = 'Size', + size, + unit, + validation, + id, + isRequired, + isDisabled, + onSizeChanged, + onUnitChanged, +}) => ( + + + + onSizeChanged(v), [onSizeChanged])} + /> + + + onUnitChanged(u as BinaryUnit), [onUnitChanged])} + value={unit} + id={prefixedID(id, 'unit')} + isDisabled={isDisabled} + > + {getStringEnumValues(BinaryUnit).map((u) => { + return ; + })} + + + + +); diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/delete-device-modal/delete-device-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/delete-device-modal/delete-device-modal.tsx index 6db91868461..a564a5dfdd9 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/delete-device-modal/delete-device-modal.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/delete-device-modal/delete-device-modal.tsx @@ -11,7 +11,7 @@ import { getName } from '@console/shared'; import { VMLikeEntityKind } from '../../../types'; import { getVMLikeModel } from '../../../selectors/vm'; import { getRemoveDiskPatches } from '../../../k8s/patches/vm/vm-disk-patches'; -import { getRemoveNicPatches } from '../../../k8s/patches/vm/vm-nic-patches'; +import { getRemoveNICPatches } from '../../../k8s/patches/vm/vm-nic-patches'; export enum DeviceType { NIC = 'NIC', @@ -42,7 +42,7 @@ export const DeleteDeviceModal = withHandlePromise((props: DeleteDeviceModalProp patches = getRemoveDiskPatches(vmLikeEntity, device); break; case DeviceType.NIC: - patches = getRemoveNicPatches(vmLikeEntity, device); + patches = getRemoveNICPatches(vmLikeEntity, device); break; default: return; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx new file mode 100644 index 00000000000..caa4170de67 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx @@ -0,0 +1,152 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Firehose, FirehoseResult } from '@console/internal/components/utils'; +import { createModalLauncher, ModalComponentProps } from '@console/internal/components/factory'; +import { k8sPatch } from '@console/internal/module/k8s'; +import { getName, getNamespace } from '@console/shared/src'; +import { + NamespaceModel, + PersistentVolumeClaimModel, + ProjectModel, + StorageClassModel, +} from '@console/internal/models'; +import { getLoadedData } from '../../../utils'; +import { asVM, getVMLikeModel, getDisks, getDataVolumeTemplates } from '../../../selectors/vm'; +import { VMLikeEntityKind } from '../../../types'; +import { getSimpleName } from '../../../selectors/utils'; +import { DiskWrapper } from '../../../k8s/wrapper/vm/disk-wrapper'; +import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; +import { getUpdateDiskPatches } from '../../../k8s/patches/vm/vm-disk-patches'; +import { DiskModal } from './disk-modal'; + +const DiskModalFirehoseComponent: React.FC = (props) => { + const { disk, volume, dataVolume, vmLikeEntity, vmLikeEntityLoading, ...restProps } = props; + + const vmLikeFinal = getLoadedData(vmLikeEntityLoading, vmLikeEntity); // default old snapshot before loading a new one + const vm = asVM(vmLikeFinal); + + const diskWrapper = disk ? DiskWrapper.initialize(disk) : DiskWrapper.EMPTY; + const volumeWrapper = volume ? VolumeWrapper.initialize(volume) : VolumeWrapper.EMPTY; + const dataVolumeWrapper = dataVolume + ? DataVolumeWrapper.initialize(dataVolume) + : DataVolumeWrapper.EMPTY; + + const usedDiskNames: Set = new Set( + getDisks(vm) + .map(getSimpleName) + .filter((n) => n && n !== diskWrapper.getName()), + ); + + const usedPVCNames: Set = new Set( + getDataVolumeTemplates(vm) + .map((dv) => getName(dv)) + .filter((n) => n && n !== dataVolumeWrapper.getName()), + ); + + const onSubmit = async (resultDisk, resultVolume, resultDataVolume) => + k8sPatch( + getVMLikeModel(vmLikeEntity), + vmLikeEntity, + await getUpdateDiskPatches(vmLikeEntity, { + disk: DiskWrapper.mergeWrappers(diskWrapper, resultDisk).asResource(), + volume: VolumeWrapper.mergeWrappers(volumeWrapper, resultVolume).asResource(), + dataVolume: + resultDataVolume && + DataVolumeWrapper.mergeWrappers(dataVolumeWrapper, resultDataVolume).asResource(), + oldDiskName: diskWrapper.getName(), + oldVolumeName: volumeWrapper.getName(), + oldDataVolumeName: dataVolumeWrapper.getName(), + }), + ); + + return ( + + ); +}; + +type DiskModalFirehoseComponentProps = ModalComponentProps & { + disk?: any; + volume?: any; + dataVolume?: any; + namespace: string; + onNamespaceChanged: (namespace: string) => void; + storageClasses?: FirehoseResult; + persistentVolumeClaims?: FirehoseResult; + vmLikeEntityLoading?: FirehoseResult; + vmLikeEntity: VMLikeEntityKind; +}; + +const DiskModalFirehose: React.FC = (props) => { + const { vmLikeEntity, useProjects, ...restProps } = props; + + const vmName = getName(vmLikeEntity); + const vmNamespace = getNamespace(vmLikeEntity); + + const [namespace, setNamespace] = React.useState(vmNamespace); + + const resources = [ + { + kind: (useProjects ? ProjectModel : NamespaceModel).kind, + isList: true, + prop: 'namespaces', + }, + { + kind: getVMLikeModel(vmLikeEntity).kind, + name: vmName, + namespace: vmNamespace, + prop: 'vmLikeEntityLoading', + }, + { + kind: StorageClassModel.kind, + isList: true, + prop: 'storageClasses', + }, + { + kind: PersistentVolumeClaimModel.kind, + isList: true, + namespace, + prop: 'persistentVolumeClaims', + }, + ]; + + return ( + + setNamespace(n)} + {...restProps} + /> + + ); +}; + +type DiskModalFirehoseProps = ModalComponentProps & { + vmLikeEntity: VMLikeEntityKind; + disk?: any; + volume?: any; + dataVolume?: any; + useProjects: boolean; +}; + +const diskModalStateToProps = ({ k8s }) => { + const useProjects = k8s.hasIn(['RESOURCES', 'models', ProjectModel.kind]); + return { + useProjects, + }; +}; + +const DiskModalConnected = connect(diskModalStateToProps)(DiskModalFirehose); + +export const diskModalEnhanced = createModalLauncher(DiskModalConnected); diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal.tsx new file mode 100644 index 00000000000..a709618e166 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal.tsx @@ -0,0 +1,355 @@ +import * as React from 'react'; +import { Form, FormSelect, FormSelectOption, TextInput } from '@patternfly/react-core'; +import { + FirehoseResult, + HandlePromiseProps, + validate, + withHandlePromise, +} from '@console/internal/components/utils'; +import { + createModalLauncher, + ModalBody, + ModalComponentProps, + ModalTitle, +} from '@console/internal/components/factory'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { + NamespaceModel, + PersistentVolumeClaimModel, + StorageClassModel, +} from '@console/internal/models'; +import { getName } from '@console/shared/src'; +import { getLoadedData, prefixedID } from '../../../utils'; +import { validateDisk } from '../../../utils/validations/vm'; +import { isValidationError } from '../../../utils/validations/common'; +import { FormRow } from '../../form/form-row'; +import { + asFormSelectValue, + FormSelectPlaceholderOption, +} from '../../form/form-select-placeholder-option'; +import { getDialogUIError, getSequenceName } from '../../../utils/strings'; +import { ModalFooter } from '../modal/modal-footer'; +import { useShowErrorToggler } from '../../../hooks/use-show-error-toggler'; +import { DiskWrapper } from '../../../k8s/wrapper/vm/disk-wrapper'; +import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; +import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; +import { DiskBus, DiskType } from '../../../constants/vm/storage'; +import { getPvcStorageSize } from '../../../selectors/pvc/selectors'; +import { K8sResourceSelectRow } from '../../form/k8s-resource-select-row'; +import { SizeUnitFormRow, BinaryUnit } from '../../form/size-unit-form-row'; +import { StorageUISource } from './storage-ui-source'; + +export const DiskModal = withHandlePromise((props: DiskModalProps) => { + const { + storageClasses, + usedPVCNames, + persistentVolumeClaims, + vmName, + vmNamespace, + namespace, + namespaces, + onNamespaceChanged, + usedDiskNames, + onSubmit, + inProgress, + errorMessage, + handlePromise, + close, + cancel, + } = props; + const asId = prefixedID.bind(null, 'disk'); + const disk = props.disk || DiskWrapper.EMPTY; + const volume = props.volume || VolumeWrapper.EMPTY; + const dataVolume = props.dataVolume || DataVolumeWrapper.EMPTY; + const isEditing = disk !== DiskWrapper.EMPTY; + + const [source, setSource] = React.useState( + StorageUISource.fromTypes(volume.getType(), dataVolume.getType()) || StorageUISource.BLANK, + ); + + const [url, setURL] = React.useState(dataVolume.getURL); + + const [containerImage, setContainerImage] = React.useState( + volume.getContainerImage() || '', + ); + + const [pvcName, setPVCName] = React.useState(source.getPVCName(volume, dataVolume)); + + const [name, setName] = React.useState( + disk.getName() || getSequenceName('disk', usedDiskNames), + ); + const [bus, setBus] = React.useState( + disk.getDiskBus() || (isEditing ? null : DiskBus.VIRTIO), + ); + const [storageClassName, setStorageClassName] = React.useState( + dataVolume.getStorageClassName(), + ); + + const [size, setSize] = React.useState(`${dataVolume.getSize().value}`); + const [unit, setUnit] = React.useState(dataVolume.getSize().unit || BinaryUnit.Gi); + + const resultDisk = DiskWrapper.initializeFromSimpleData({ + name, + bus, + type: DiskType.DISK, + }); + + const resultDataVolumeName = prefixedID(vmName, name); + const resultVolume = VolumeWrapper.initializeFromSimpleData( + { + name, + type: source.getVolumeType(), + typeData: { + name: resultDataVolumeName, + claimName: pvcName, + image: containerImage, + }, + }, + { sanitizeTypeData: true }, + ); + + let resultDataVolume; + if (source.requiresDatavolume()) { + resultDataVolume = DataVolumeWrapper.initializeFromSimpleData( + { + name: resultDataVolumeName, + storageClassName: storageClassName || undefined, + type: source.getDataVolumeSourceType(), + size, + unit, + typeData: { name: pvcName, namespace, url }, + }, + { sanitizeTypeData: true }, + ); + } + + const { + validations: { + name: nameValidation, + size: sizeValidation, + container: containerValidation, + pvc: pvcValidation, + url: urlValidation, + }, + isValid, + hasAllRequiredFilled, + } = validateDisk(resultDisk, resultVolume, resultDataVolume, { usedDiskNames, usedPVCNames }); + + const [showUIError, setShowUIError] = useShowErrorToggler(false, isValid, isValid); + + const submit = (e) => { + e.preventDefault(); + + if (isValid) { + // eslint-disable-next-line promise/catch-or-return + handlePromise(onSubmit(resultDisk, resultVolume, resultDataVolume)).then(close); + } else { + setShowUIError(true); + } + }; + + const onSourceChanged = (uiSource) => { + setSize(''); + setUnit('Gi'); + setURL(''); + setPVCName(''); + setContainerImage(''); + setStorageClassName(''); + onNamespaceChanged(vmNamespace); + setSource(StorageUISource.fromString(uiSource)); + }; + + const onPVCChanged = (newPVCName) => { + setPVCName(newPVCName); + if (source === StorageUISource.ATTACH_CLONED_DISK) { + const newSizeBundle = getPvcStorageSize( + getLoadedData(persistentVolumeClaims).find((p) => getName(p) === newPVCName), + ); + const [newSize, newUnit] = validate.split(newSizeBundle); + setSize(newSize); + setUnit(newUnit); + } + }; + + return ( +
+ {isEditing ? 'Edit' : 'Add'} Disk + +
+ + + {StorageUISource.getAll().map((uiType) => { + return ( + + ); + })} + + + {source.requiresURL() && ( + + setURL(v)} + /> + + )} + {source.requiresContainerImage() && ( + + setContainerImage(v)} + /> + + )} + {source.requiresNamespace() && ( + { + setPVCName(''); + onNamespaceChanged(sc); + }} + /> + )} + {source.requiresPVC() && ( + !(usedPVCNames && usedPVCNames.has(getName(p)))} + /> + )} + + setName(v), [setName])} + /> + + {source.requiresDatavolume() && ( + + )} + + setBus(DiskBus.fromString(diskBus)), [ + setBus, + ])} + value={asFormSelectValue(bus)} + id={asId('interface')} + isDisabled={inProgress} + > + + {DiskBus.getAll().map((b) => { + return ( + + ); + })} + + + {source.requiresDatavolume() && ( + setStorageClassName(sc)} + /> + )} + +
+ { + e.stopPropagation(); + cancel(); + }} + /> +
+ ); +}); + +export type DiskModalProps = { + disk?: DiskWrapper; + volume?: VolumeWrapper; + dataVolume?: DataVolumeWrapper; + onSubmit: ( + disk: DiskWrapper, + volume: VolumeWrapper, + dataVolume: DataVolumeWrapper, + ) => Promise; + namespaces?: FirehoseResult; + storageClasses?: FirehoseResult; + persistentVolumeClaims?: FirehoseResult; + vmName: string; + vmNamespace: string; + namespace: string; + onNamespaceChanged: (namespace: string) => void; + usedDiskNames: Set; + usedPVCNames: Set; +} & ModalComponentProps & + HandlePromiseProps; + +export const diskModal = createModalLauncher(DiskModal); diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/index.ts b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/index.ts new file mode 100644 index 00000000000..bec8ea64614 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/index.ts @@ -0,0 +1 @@ +export * from './disk-modal'; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/storage-ui-source.ts b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/storage-ui-source.ts new file mode 100644 index 00000000000..6999382c552 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/storage-ui-source.ts @@ -0,0 +1,97 @@ +/* eslint-disable lines-between-class-members */ + +import { ValueEnum, VolumeType } from '../../../constants'; +import { DataVolumeSourceType } from '../../../constants/vm/storage'; +import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; + +export class StorageUISource extends ValueEnum { + static readonly BLANK = new StorageUISource( + 'Blank', + VolumeType.DATA_VOLUME, + DataVolumeSourceType.BLANK, + ); + static readonly URL = new StorageUISource( + 'URL', + VolumeType.DATA_VOLUME, + DataVolumeSourceType.HTTP, + ); + static readonly CONTAINER = new StorageUISource('Container', VolumeType.CONTAINER_DISK); + static readonly ATTACH_CLONED_DISK = new StorageUISource( + 'Attach Cloned Disk', + VolumeType.DATA_VOLUME, + DataVolumeSourceType.PVC, + ); + static readonly ATTACH_DISK = new StorageUISource( + 'Attach Disk', + VolumeType.PERSISTENT_VOLUME_CLAIM, + undefined, + ); + + private readonly volumeType: VolumeType; + private readonly dataVolumeSourceType: DataVolumeSourceType; + + private static readonly ALL = Object.freeze( + ValueEnum.getAllClassEnumProperties(StorageUISource), + ); + + private static readonly stringMapper = StorageUISource.ALL.reduce( + (accumulator, volumeType: StorageUISource) => ({ + ...accumulator, + [volumeType.value]: volumeType, + }), + {}, + ); + + protected constructor( + value: string, + volumeType: VolumeType, + dataVolumeSourceType?: DataVolumeSourceType, + ) { + super(value); + this.volumeType = volumeType; + this.dataVolumeSourceType = dataVolumeSourceType; + } + + static getAll = () => StorageUISource.ALL; + + static fromSerialized = (volumeType: { value: string }): StorageUISource => + StorageUISource.fromString(volumeType && volumeType.value); + + static fromString = (model: string): StorageUISource => StorageUISource.stringMapper[model]; + + static fromTypes = (volumeType: VolumeType, dataVolumeSourceType?: DataVolumeSourceType) => + StorageUISource.ALL.find( + (storageUIType) => + storageUIType.volumeType == volumeType && // eslint-disable-line eqeqeq + storageUIType.dataVolumeSourceType == dataVolumeSourceType, // eslint-disable-line eqeqeq + ); + + getVolumeType = () => this.volumeType; + + getDataVolumeSourceType = () => this.dataVolumeSourceType; + + requiresPVC = () => + this === StorageUISource.ATTACH_DISK || this === StorageUISource.ATTACH_CLONED_DISK; + + requiresContainerImage = () => this === StorageUISource.CONTAINER; + + requiresURL = () => this === StorageUISource.URL; + + requiresDatavolume = () => !!this.dataVolumeSourceType; + + requiresNamespace = () => this === StorageUISource.ATTACH_CLONED_DISK; + + isEditingSupported = () => !this.dataVolumeSourceType; + + getPVCName = (volume: VolumeWrapper, dataVolume: DataVolumeWrapper) => { + if (this === StorageUISource.ATTACH_DISK) { + return volume.getPersistentVolumeClaimName(); + } + if (this === StorageUISource.ATTACH_CLONED_DISK) { + return dataVolume.getPesistentVolumeClaimName(); + } + + return null; + }; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx index 6d82bd15255..cff6b63010b 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal-enhanced.tsx @@ -10,7 +10,7 @@ import { NetworkType } from '../../../constants/vm'; import { getInterfaces, getUsedNetworks, asVM, getVMLikeModel } from '../../../selectors/vm'; import { NetworkInterfaceWrapper } from '../../../k8s/wrapper/vm/network-interface-wrapper'; import { VMLikeEntityKind } from '../../../types'; -import { getAddNicPatches } from '../../../k8s/patches/vm/vm-nic-patches'; +import { getUpdateNICPatches } from '../../../k8s/patches/vm/vm-nic-patches'; import { getSimpleName } from '../../../selectors/utils'; import { NetworkWrapper } from '../../../k8s/wrapper/vm/network-wrapper'; import { NICModal } from './nic-modal'; @@ -50,7 +50,7 @@ const NICModalFirehoseComponent: React.FC = (pro k8sPatch( getVMLikeModel(vmLikeEntity), vmLikeEntity, - getAddNicPatches(vmLikeEntity, { + getUpdateNICPatches(vmLikeEntity, { nic: NetworkInterfaceWrapper.mergeWrappers( nicWrapper, resultNetworkInterfaceWrapper, diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx index c317aa41f1f..ae4643987e2 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/nic-modal/nic-modal.tsx @@ -195,8 +195,7 @@ export const NICModal = withHandlePromise((props: NICModalProps) => { fieldId={asId('name')} isRequired isLoading={!usedInterfacesNames} - validationMessage={nameValidation && nameValidation.message} - validationType={nameValidation && nameValidation.type} + validation={nameValidation} > { => { - const storageClassConfigMap = await getStorageClassConfigMap({ k8sGet }); - return k8sPatch( - getVMLikeModel(vmLikeEntity), - vmLikeEntity, - getAddDiskPatches(vmLikeEntity, disk, storageClassConfigMap), - ); -}; - -type StorageClassColumn = { - storageClass: string; - onChange: (string) => void; - storageClasses: FirehoseResult; - creating: boolean; -}; - -const StorageClassColumn: React.FC = ({ - storageClass, - onChange, - storageClasses, - creating, -}) => { - if (storageClasses.loaded) { - const loadedClasses = storageClasses.data; - const storageClassValue = - storageClass || - (loadedClasses.length === 0 - ? '--- No Storage Class Available ---' - : '--- Select Storage Class ---'); - return ( - getName(sc))} - value={storageClassValue} - onChange={onChange} - disabled={loadedClasses.length === 0 || creating} - /> - ); - } - return ; -}; - -type CreateDiskRowProps = VMDiskRowProps & { storageClasses?: FirehoseResult }; - -export const CreateDiskRow: React.FC = ({ - storageClasses, - customData: { vm, vmLikeEntity, diskLookup, onCreateRowDismiss, onCreateRowError, forceRerender }, - index, - style, -}) => { - const [creating, setCreating] = useSafetyFirst(false); - const [name, setName] = React.useState(''); - const [size, setSize] = React.useState(''); - const [storageClass, setStorageClass] = React.useState(null); - - const id = 'create-disk-row'; - - const nameError = validateDiskName(name, diskLookup); - const isValid = !nameError && size; - - const bus = getVmPreferableDiskBus(vm); - return ( - - - - { - setName(v); - forceRerender(); - }} - value={name} - /> - - {nameError && nameError.type === ValidationErrorType.Error && nameError.message} - - - - - - Gi - - {bus} - - - - - { - setCreating(true); - createDisk({ vmLikeEntity, disk: { name, size, bus, storageClass } }) - .then(onCreateRowDismiss) - .catch((error) => { - onCreateRowError((error && error.message) || GENERAL_ERROR_MSG); - setCreating(false); - }); - }} - disabled={!isValid} - /> - - - ); -}; - -export const CreateDiskRowFirehose: React.FC = (props) => { - const resources = [ - getResource(StorageClassModel, { - prop: 'storageClasses', - }), - ]; - - return ( - - - - ); -}; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx index cc46513934f..2ba4dc827a9 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx @@ -8,21 +8,62 @@ import { } from '@console/internal/components/utils'; import { getDeletetionTimestamp, DASH } from '@console/shared'; import { TemplateModel } from '@console/internal/models'; -import { BUS_VIRTIO } from '../../constants/vm'; import { deleteDeviceModal, DeviceType } from '../modals/delete-device-modal'; import { VMLikeEntityKind } from '../../types'; -import { getDiskBus, isVM } from '../../selectors/vm'; +import { asVM, isVM, isVMRunning } from '../../selectors/vm'; import { VirtualMachineModel } from '../../models'; -import { VMDiskRowProps } from './types'; +import { dimensifyRow } from '../../utils/table'; +import { ValidationCell } from '../table/validation-cell'; +import { VMNicRowActionOpts } from '../vm-nics/types'; +import { diskModalEnhanced } from '../modals/disk-modal/disk-modal-enhanced'; +import { + StorageBundle, + StorageSimpleData, + StorageSimpleDataValidation, + VMStorageRowActionOpts, + VMStorageRowCustomData, +} from './types'; -const menuActionDelete = (vmLikeEntity: VMLikeEntityKind, disk): KebabOption => ({ +const menuActionEdit = ( + disk, + volume, + dataVolume, + vmLikeEntity: VMLikeEntityKind, + { withProgress }: VMNicRowActionOpts, +): KebabOption => ({ + label: 'Edit', + callback: () => + withProgress( + diskModalEnhanced({ + vmLikeEntity, + disk, + volume, + dataVolume, + }).result, + ), + accessReview: asAccessReview( + isVM(vmLikeEntity) ? VirtualMachineModel : TemplateModel, + vmLikeEntity, + 'patch', + ), +}); + +const menuActionDelete = ( + disk, + volume, + dataVolume, + vmLikeEntity: VMLikeEntityKind, + { withProgress }: VMNicRowActionOpts, +): KebabOption => ({ label: 'Delete', callback: () => - deleteDeviceModal({ - deviceType: DeviceType.DISK, - device: disk, - vmLikeEntity, - }), + withProgress( + deleteDeviceModal({ + deviceType: DeviceType.DISK, + device: disk, + vmLikeEntity, + }).result, + ), accessReview: asAccessReview( isVM(vmLikeEntity) ? VirtualMachineModel : TemplateModel, vmLikeEntity, @@ -30,34 +71,103 @@ const menuActionDelete = (vmLikeEntity: VMLikeEntityKind, disk): KebabOption => ), }); -const getActions = (vmLikeEntity: VMLikeEntityKind, disk) => { - const actions = [menuActionDelete]; - return actions.map((a) => a(vmLikeEntity, disk)); +const getActions = ( + disk, + volume, + dataVolume, + vmLikeEntity: VMLikeEntityKind, + opts: VMStorageRowActionOpts, +) => { + const actions = []; + if (isVMRunning(asVM(vmLikeEntity))) { + return actions; + } + if (opts.isEditingEnabled) { + actions.push(menuActionEdit); + } + actions.push(menuActionDelete); + return actions.map((a) => a(disk, volume, dataVolume, vmLikeEntity, opts)); }; -export const DiskRow: React.FC = ({ - obj: { disk, size, storageClass }, - customData: { vmLikeEntity }, +export type VMDiskSimpleRowProps = { + data: StorageSimpleData; + validation?: StorageSimpleDataValidation; + columnClasses: string[]; + actionsComponent: React.ReactNode; + index: number; + style: object; +}; + +export const DiskSimpleRow: React.FC = ({ + data: { name, size, diskInterface, storageClass }, + validation = {}, + columnClasses, + actionsComponent, index, style, }) => { - const diskName = disk.name; - const sizeColumn = size === undefined ? : size; - const storageColumn = storageClass === undefined ? : storageClass; + const dimensify = dimensifyRow(columnClasses); + const isSizeLoading = size === undefined; + const isStorageClassLoading = size === undefined; return ( - - {diskName} - {sizeColumn || DASH} - {getDiskBus(disk, BUS_VIRTIO)} - {storageColumn || DASH} - - + + + {name} + + + {isSizeLoading && } + {!isSizeLoading && ( + {size || DASH} + )} + + {diskInterface} + + + {isStorageClassLoading && } + {!isStorageClassLoading && ( + + {storageClass || DASH} + + )} + + {actionsComponent} ); }; + +export type VMDiskRowProps = { + obj: StorageBundle; + customData: VMStorageRowCustomData; + index: number; + style: object; +}; + +export const DiskRow: React.FC = ({ + obj: { name, disk, volume, dataVolume, isEditingEnabled, ...restData }, + customData: { isDisabled, withProgress, vmLikeEntity, columnClasses }, + index, + style, +}) => { + return ( + + } + /> + ); +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/types.ts b/frontend/packages/kubevirt-plugin/src/components/vm-disks/types.ts index 2ac77bec828..f3ed805065f 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/types.ts @@ -1,29 +1,34 @@ -import { EntityMap } from '@console/shared'; -import { VMLikeEntityKind, VMKind } from '../../types'; +import { VMLikeEntityKind } from '../../types'; +import { ValidationObject } from '../../utils/validations/types'; -export enum StorageRowType { - STORAGE_TYPE_VM = 'storage-type-vm', - STORAGE_TYPE_CREATE = 'storage-type-create', -} +export type StorageSimpleData = { + name?: string; + diskInterface?: string; + size?: string; + storageClass?: string; +}; + +export type StorageSimpleDataValidation = { + name?: ValidationObject; + diskInterface?: ValidationObject; + size?: ValidationObject; + storageClass?: ValidationObject; +}; -export type StorageBundle = { - name: string; - size: string; - storageClass: string; - storageType: StorageRowType; +export type StorageBundle = StorageSimpleData & { disk: any; + volume: any; + dataVolume: any; + isEditingEnabled: boolean; }; -export type VMDiskRowProps = { - obj: StorageBundle; - index: number; - style: object; - customData: { - vmLikeEntity: VMLikeEntityKind; - vm: VMKind; - diskLookup: EntityMap; - onCreateRowDismiss: () => void; - onCreateRowError: (error: string) => void; - forceRerender: () => void; - }; +export type VMStorageRowActionOpts = { + withProgress: (promise: Promise) => void; + isEditingEnabled: boolean; }; + +export type VMStorageRowCustomData = { + vmLikeEntity: VMLikeEntityKind; + columnClasses: string[]; + isDisabled: boolean; +} & VMStorageRowActionOpts; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/utils.ts b/frontend/packages/kubevirt-plugin/src/components/vm-disks/utils.ts new file mode 100644 index 00000000000..05481e1eaf7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/utils.ts @@ -0,0 +1,10 @@ +import * as classNames from 'classnames'; +import { Kebab } from '@console/internal/components/utils'; + +export const diskTableColumnClasses = [ + classNames('col-lg-3'), + classNames('col-lg-3'), + classNames('col-lg-3'), + classNames('col-lg-3'), + Kebab.columnClass, +]; diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx index 88050672717..9547de1bb6a 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx @@ -1,23 +1,15 @@ import * as React from 'react'; -import { Button } from 'patternfly-react'; -import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; +import { Button, ButtonVariant } from '@patternfly/react-core'; import { Table } from '@console/internal/components/factory'; import { PersistentVolumeClaimModel } from '@console/internal/models'; -import { Firehose, FirehoseResult, Kebab } from '@console/internal/components/utils'; +import { Firehose, FirehoseResult } from '@console/internal/components/utils'; import { getNamespace, getName, createBasicLookup, createLookup } from '@console/shared'; import { useSafetyFirst } from '@console/internal/components/safety-first'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { sortable } from '@patternfly/react-table'; import { DataVolumeModel } from '../../models'; import { VMLikeEntityKind } from '../../types'; -import { - asVM, - getDataVolumeTemplates, - getDisks, - getVolumeDataVolumeName, - getVolumePersistentVolumeClaimName, - getVolumes, -} from '../../selectors/vm'; +import { asVM, getDataVolumeTemplates, getDisks, getVolumes, isVM } from '../../selectors/vm'; import { getPvcStorageClassName, getPvcStorageSize } from '../../selectors/pvc/selectors'; import { getDataVolumeStorageClassName, @@ -26,52 +18,44 @@ import { import { VMLikeEntityTabProps } from '../vms/types'; import { getResource } from '../../utils'; import { getSimpleName } from '../../selectors/utils'; +import { wrapWithProgress } from '../../utils/utils'; +import { dimensifyHeader } from '../../utils/table'; +import { DiskWrapper } from '../../k8s/wrapper/vm/disk-wrapper'; +import { VolumeWrapper } from '../../k8s/wrapper/vm/volume-wrapper'; +import { DiskType, VolumeType } from '../../constants/vm/storage'; +import { diskModalEnhanced } from '../modals/disk-modal/disk-modal-enhanced'; +import { StorageUISource } from '../modals/disk-modal/storage-ui-source'; +import { DataVolumeWrapper } from '../../k8s/wrapper/vm/data-volume-wrapper'; +import { StorageBundle } from './types'; import { DiskRow } from './disk-row'; -import { StorageBundle, StorageRowType, VMDiskRowProps } from './types'; -import { CreateDiskRowFirehose } from './create-disk-row'; - -export const VMDiskRow: React.FC = (props) => { - switch (props.obj.storageType) { - case StorageRowType.STORAGE_TYPE_VM: - return ; - case StorageRowType.STORAGE_TYPE_CREATE: - return ; - default: - return null; - } -}; - -const getStoragesData = ( - { - vmLikeEntity, - datavolumes, - pvcs, - }: { - vmLikeEntity: VMLikeEntityKind; - pvcs: FirehoseResult; - datavolumes: FirehoseResult; - }, - addNewDisk: boolean, - rerenderFlag: boolean, -): StorageBundle[] => { +import { diskTableColumnClasses } from './utils'; + +const getStoragesData = ({ + vmLikeEntity, + datavolumes, + pvcs, +}: { + vmLikeEntity: VMLikeEntityKind; + pvcs: FirehoseResult; + datavolumes: FirehoseResult; +}): StorageBundle[] => { const vm = asVM(vmLikeEntity); const pvcLookup = createLookup(pvcs, getName); const datavolumeLookup = createLookup(datavolumes, getName); const volumeLookup = createBasicLookup(getVolumes(vm), getSimpleName); - const datavolumeTemplatesLookup = createBasicLookup(getDataVolumeTemplates(vm), getName); + const datavolumeTemplatesLookup = createBasicLookup(getDataVolumeTemplates(vm), getName); - const disksWithType = getDisks(vm).map((disk) => { - const volume = volumeLookup[disk.name]; - - const pvcName = getVolumePersistentVolumeClaimName(volume); - const dataVolumeName = getVolumeDataVolumeName(volume); + return getDisks(vm).map((disk) => { + const diskWrapper = DiskWrapper.initialize(disk); + const volume = volumeLookup[diskWrapper.getName()]; + const volumeWrapper = VolumeWrapper.initialize(volume); let size = null; let storageClass = null; - if (pvcName) { - const pvc = pvcLookup[pvcName]; + if (volumeWrapper.getType() === VolumeType.PERSISTENT_VOLUME_CLAIM) { + const pvc = pvcLookup[volumeWrapper.getPersistentVolumeClaimName()]; if (pvc) { size = getPvcStorageSize(pvc); storageClass = getPvcStorageClassName(pvc); @@ -79,9 +63,10 @@ const getStoragesData = ( size = undefined; storageClass = undefined; } - } else if (dataVolumeName) { + } else if (volumeWrapper.getType() === VolumeType.DATA_VOLUME) { const dataVolumeTemplate = - datavolumeTemplatesLookup[dataVolumeName] || datavolumeLookup[dataVolumeName]; + datavolumeTemplatesLookup[volumeWrapper.getDataVolumeName()] || + datavolumeLookup[volumeWrapper.getDataVolumeName()]; if (dataVolumeTemplate) { size = getDataVolumeStorageSize(dataVolumeTemplate); @@ -92,62 +77,47 @@ const getStoragesData = ( } } + const dataVolume = datavolumeTemplatesLookup[volumeWrapper.getDataVolumeName()]; + const source = StorageUISource.fromTypes( + volumeWrapper.getType(), + DataVolumeWrapper.initialize(dataVolume).getType(), + ); + const isTemplate = vmLikeEntity && !isVM(vmLikeEntity); return { - ...disk, // for sorting + disk, + volume, + dataVolume, + isEditingEnabled: isTemplate || (source && source.isEditingSupported()), + // for sorting + name: diskWrapper.getName(), + diskInterface: + diskWrapper.getType() === DiskType.DISK ? diskWrapper.getReadableDiskBus() : undefined, size, storageClass, - storageType: StorageRowType.STORAGE_TYPE_VM, - disk, }; }); - - return addNewDisk - ? [{ storageType: StorageRowType.STORAGE_TYPE_CREATE, rerenderFlag }, ...disksWithType] - : disksWithType; }; -export const VMDisks: React.FC = ({ vmLikeEntity, pvcs, datavolumes }) => { - const [isCreating, setIsCreating] = useSafetyFirst(false); - const [createError, setCreateError] = useSafetyFirst(null); - const [forceRerenderFlag, setForceRerenderFlag] = useSafetyFirst(false); // TODO: HACK: fire changes in Virtualize Table for CreateNicRow. Remove after deprecating CreateNicRow - - const vm = asVM(vmLikeEntity); +export type VMDisksTableProps = { + data?: any[]; + customData?: object; + row: React.ComponentClass | React.ComponentType; + columnClasses: string[]; +}; +export const VMDisksTable: React.FC = ({ + data, + customData, + row: Row, + columnClasses, +}) => { return ( -
-
-
- -
-
-
- {createError && ( - setCreateError(null)} />} - /> - )} -
[ +
+ dimensifyHeader( + [ { title: 'Name', sortField: 'name', @@ -160,7 +130,7 @@ export const VMDisks: React.FC = ({ vmLikeEntity, pvcs, datavolume }, { title: 'Interface', - sortField: 'disk.bus', + sortField: 'diskInterface', transforms: [sortable], }, { @@ -170,37 +140,64 @@ export const VMDisks: React.FC = ({ vmLikeEntity, pvcs, datavolume }, { title: '', - props: { className: Kebab.columnClass }, }, - ]} - Row={VMDiskRow} + ], + columnClasses, + ) + } + Row={Row} + customData={{ ...customData, columnClasses }} + virtualize + loaded + /> + ); +}; + +type VMDisksProps = { + vmLikeEntity?: VMLikeEntityKind; + pvcs?: FirehoseResult; + datavolumes?: FirehoseResult; +}; + +export const VMDisks: React.FC = ({ vmLikeEntity, pvcs, datavolumes }) => { + const [isLocked, setIsLocked] = useSafetyFirst(false); + const withProgress = wrapWithProgress(setIsLocked); + return ( +
+
+
+ +
+
+
+ { - setIsCreating(false); - }, - onCreateRowError: (error) => { - setIsCreating(false); - setCreateError(error); - }, - forceRerender: () => setForceRerenderFlag(!forceRerenderFlag), + withProgress, + isDisabled: isLocked, }} - virtualize - loaded + row={DiskRow} + columnClasses={diskTableColumnClasses} />
); }; -interface VMDisksProps { - vmLikeEntity?: VMLikeEntityKind; - pvcs?: FirehoseResult; - datavolumes?: FirehoseResult; -} - export const VMDisksFirehose: React.FC = ({ obj: vmLikeEntity }) => { const namespace = getNamespace(vmLikeEntity); diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx index a0933c832ad..c2edb7f822a 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-nics/nic-row.tsx @@ -5,7 +5,7 @@ import { DASH, getDeletetionTimestamp } from '@console/shared/src'; import { TemplateModel } from '@console/internal/models'; import { deleteDeviceModal, DeviceType } from '../modals/delete-device-modal'; import { VirtualMachineModel } from '../../models'; -import { isVM } from '../../selectors/vm'; +import { asVM, isVM, isVMRunning } from '../../selectors/vm'; import { dimensifyRow } from '../../utils/table'; import { VMLikeEntityKind } from '../../types'; import { nicModalEnhanced } from '../modals/nic-modal/nic-modal-enhanced'; @@ -63,6 +63,9 @@ const menuActionDelete = ( }); const getActions = (nic, network, vmLikeEntity: VMLikeEntityKind, opts: VMNicRowActionOpts) => { + if (isVMRunning(asVM(vmLikeEntity))) { + return []; + } const actions = [menuActionEdit, menuActionDelete]; return actions.map((a) => a(nic, network, vmLikeEntity, opts)); }; @@ -131,7 +134,9 @@ export const NicRow: React.FC = ({ actionsComponent={ } diff --git a/frontend/packages/kubevirt-plugin/src/constants/pvc/constants.ts b/frontend/packages/kubevirt-plugin/src/constants/pvc/constants.ts new file mode 100644 index 00000000000..23a40847608 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/pvc/constants.ts @@ -0,0 +1,8 @@ +export const PVC_ACCESSMODE_RWO = 'ReadWriteOnce'; +export const PVC_ACCESSMODE_RWM = 'ReadWriteMany'; + +export const PVC_VOLUMEMODE_FS = 'Filesystem'; +export const PVC_VOLUMEMODE_BLOCK = 'Block'; + +export const PVC_ACCESSMODE_DEFAULT = PVC_ACCESSMODE_RWO; +export const PVC_VOLUMEMODE_DEFAULT = PVC_VOLUMEMODE_FS; diff --git a/frontend/packages/kubevirt-plugin/src/constants/pvc/index.ts b/frontend/packages/kubevirt-plugin/src/constants/pvc/index.ts new file mode 100644 index 00000000000..c94f80f843a --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/pvc/index.ts @@ -0,0 +1 @@ +export * from './constants'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/index.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/index.ts index 6af1fb13e01..1ba15252508 100644 --- a/frontend/packages/kubevirt-plugin/src/constants/vm/index.ts +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/index.ts @@ -1,2 +1,3 @@ export * from './constants'; export * from './network'; +export * from './storage'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/storage.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/storage.ts deleted file mode 100644 index be82778c21e..00000000000 --- a/frontend/packages/kubevirt-plugin/src/constants/vm/storage.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum StorageType { // TODO add other types / refactor - DATAVOLUME = 'datavolume', // compatible with web-ui-components constants - CONTAINER = 'container', -} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/storage/data-volume-source-type.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/data-volume-source-type.ts new file mode 100644 index 00000000000..4a901f9c2d9 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/data-volume-source-type.ts @@ -0,0 +1,31 @@ +/* eslint-disable lines-between-class-members */ +import { ValueEnum } from '../../value-enum'; + +export class DataVolumeSourceType extends ValueEnum { + static readonly BLANK = new DataVolumeSourceType('blank'); + static readonly HTTP = new DataVolumeSourceType('http'); + static readonly PVC = new DataVolumeSourceType('pvc'); + static readonly REGISTRY = new DataVolumeSourceType('registry'); + static readonly S3 = new DataVolumeSourceType('s3'); + static readonly UPLOAD = new DataVolumeSourceType('upload'); + + private static readonly ALL = Object.freeze( + ValueEnum.getAllClassEnumProperties(DataVolumeSourceType), + ); + + private static readonly stringMapper = DataVolumeSourceType.ALL.reduce( + (accumulator, dataVolumeSourceType: DataVolumeSourceType) => ({ + ...accumulator, + [dataVolumeSourceType.value]: dataVolumeSourceType, + }), + {}, + ); + + static getAll = () => DataVolumeSourceType.ALL; + + static fromSerialized = (volumeType: { value: string }): DataVolumeSourceType => + DataVolumeSourceType.fromString(volumeType && volumeType.value); + + static fromString = (model: string): DataVolumeSourceType => + DataVolumeSourceType.stringMapper[model]; +} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/storage/disk-bus.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/disk-bus.ts new file mode 100644 index 00000000000..3f47fe3f224 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/disk-bus.ts @@ -0,0 +1,35 @@ +/* eslint-disable lines-between-class-members */ +import { ValueEnum } from '../../value-enum'; +import { READABLE_VIRTIO } from '../constants'; + +export class DiskBus extends ValueEnum { + static readonly VIRTIO = new DiskBus('virtio'); + static readonly SATA = new DiskBus('sata'); + static readonly SCSI = new DiskBus('scsi'); + + private static readonly ALL = Object.freeze( + ValueEnum.getAllClassEnumProperties(DiskBus), + ); + + private static readonly stringMapper = DiskBus.ALL.reduce( + (accumulator, diskBusType: DiskBus) => ({ + ...accumulator, + [diskBusType.value]: diskBusType, + }), + {}, + ); + + static getAll = () => DiskBus.ALL; + + static fromSerialized = (diskBusType: { value: string }): DiskBus => + DiskBus.fromString(diskBusType && diskBusType.value); + + static fromString = (model: string): DiskBus => DiskBus.stringMapper[model]; + + toString = () => { + if (this === DiskBus.VIRTIO) { + return READABLE_VIRTIO; + } + return this.value; + }; +} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/storage/disk-type.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/disk-type.ts new file mode 100644 index 00000000000..ff584b95345 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/disk-type.ts @@ -0,0 +1,28 @@ +/* eslint-disable lines-between-class-members */ +import { ValueEnum } from '../../value-enum'; + +export class DiskType extends ValueEnum { + static readonly DISK = new DiskType('disk'); + static readonly CDROM = new DiskType('cdrom'); + static readonly FLOPPY = new DiskType('floppy'); + static readonly LUN = new DiskType('lun'); + + private static readonly ALL = Object.freeze( + ValueEnum.getAllClassEnumProperties(DiskType), + ); + + private static readonly stringMapper = DiskType.ALL.reduce( + (accumulator, diskType: DiskType) => ({ + ...accumulator, + [diskType.value]: diskType, + }), + {}, + ); + + static getAll = () => DiskType.ALL; + + static fromSerialized = (diskType: { value: string }): DiskType => + DiskType.fromString(diskType && diskType.value); + + static fromString = (model: string): DiskType => DiskType.stringMapper[model]; +} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/storage/index.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/index.ts new file mode 100644 index 00000000000..e3671478d93 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/index.ts @@ -0,0 +1,4 @@ +export * from './data-volume-source-type'; +export * from './disk-bus'; +export * from './disk-type'; +export * from './volume-type'; diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/storage/volume-type.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/volume-type.ts new file mode 100644 index 00000000000..be1980392c1 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/storage/volume-type.ts @@ -0,0 +1,34 @@ +/* eslint-disable lines-between-class-members */ +import { ValueEnum } from '../../value-enum'; + +export class VolumeType extends ValueEnum { + static readonly CLOUD_INIT_CONFIG_DRIVE = new VolumeType('cloudInitConfigDrive'); + static readonly CLOUD_INIT_NO_CLOUD = new VolumeType('cloudInitNoCloud'); + static readonly CONFIG_MAP = new VolumeType('configMap'); + static readonly CONTAINER_DISK = new VolumeType('containerDisk'); + static readonly DATA_VOLUME = new VolumeType('dataVolume'); + static readonly EMPTY_DISK = new VolumeType('emptyDisk'); + static readonly EPHEMERAL = new VolumeType('ephemeral'); + static readonly PERSISTENT_VOLUME_CLAIM = new VolumeType('persistentVolumeClaim'); + static readonly SECRET = new VolumeType('secret'); + static readonly SERVICE_ACCOUNT = new VolumeType('serviceAccount'); + + private static readonly ALL = Object.freeze( + ValueEnum.getAllClassEnumProperties(VolumeType), + ); + + private static readonly stringMapper = VolumeType.ALL.reduce( + (accumulator, volumeType: VolumeType) => ({ + ...accumulator, + [volumeType.value]: volumeType, + }), + {}, + ); + + static getAll = () => VolumeType.ALL; + + static fromSerialized = (volumeType: { value: string }): VolumeType => + VolumeType.fromString(volumeType && volumeType.value); + + static fromString = (model: string): VolumeType => VolumeType.stringMapper[model]; +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/utils.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/utils.ts index c63a27da8d7..d60a64665eb 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/utils.ts @@ -19,6 +19,7 @@ export const getShiftBootOrderPatches = ( .map((device) => { const patchedDevice = _.cloneDeep(device); patchedDevice.bootOrder = getDeviceBootOrder(patchedDevice) - 1; + return new PatchBuilder(path) .setListUpdate(patchedDevice, devicesWithoutRemovedDevice, getSimpleName) .build(); diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts index 9fa3c22bd53..7ae285ce22c 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts @@ -1,70 +1,125 @@ -import { getName } from '@console/shared'; -import { getAddDiskPatch, getDeviceBootOrderPatch } from 'kubevirt-web-ui-components'; -import { ConfigMapKind, Patch } from '@console/internal/module/k8s'; +import { getName } from '@console/shared/src'; +import { Patch, k8sGet } from '@console/internal/module/k8s'; import { getDataVolumeTemplates, - getDeviceBootOrder, getDisks, + getInterfaces, getVolumeDataVolumeName, getVolumes, } from '../../../selectors/vm'; import { getVMLikePatches } from '../vm-template'; import { VMLikeEntityKind } from '../../../types'; +import { PatchBuilder } from '../../utils/patch'; +import { getSimpleName } from '../../../selectors/utils'; +import { DiskWrapper } from '../../wrapper/vm/disk-wrapper'; +import { V1Disk } from '../../../types/vm/disk/V1Disk'; +import { V1Volume } from '../../../types/vm/disk/V1Volume'; +import { V1alpha1DataVolume } from '../../../types/vm/disk/V1alpha1DataVolume'; +import { getStorageClassConfigMap } from '../../requests/config-map/storage-class'; +import { DataVolumeWrapper } from '../../wrapper/vm/data-volume-wrapper'; +import { + getDefaultSCAccessMode, + getDefaultSCVolumeMode, +} from '../../../selectors/config-map/sc-defaults'; +import { getShiftBootOrderPatches } from './utils'; export const getRemoveDiskPatches = (vmLikeEntity: VMLikeEntityKind, disk): Patch[] => { return getVMLikePatches(vmLikeEntity, (vm) => { - const diskName = disk.name; + const diskWrapper = DiskWrapper.initialize(disk); + const diskName = diskWrapper.getName(); const disks = getDisks(vm); const volumes = getVolumes(vm); + const volume = volumes.find((v) => getSimpleName(v) === diskName); - const diskIndex = disks.findIndex((d) => d.name === diskName); - const volumeIndex = volumes.findIndex((v) => v.name === diskName); - - const patches: Patch[] = []; - - if (diskIndex >= 0) { - patches.push({ - op: 'remove', - path: `/spec/template/spec/domain/devices/disks/${diskIndex}`, - }); - } - - if (volumeIndex >= 0) { - patches.push({ - op: 'remove', - path: `/spec/template/spec/volumes/${volumeIndex}`, - }); - } + const patches = [ + new PatchBuilder('/spec/template/spec/domain/devices/disks') + .setListRemove(disk, disks, getSimpleName) + .build(), + new PatchBuilder('/spec/template/spec/volumes') + .setListRemove(volume, volumes, getSimpleName) + .build(), + ]; - const dataVolumeName = getVolumeDataVolumeName(volumes[volumeIndex]); + const dataVolumeName = getVolumeDataVolumeName(volume); if (dataVolumeName) { - const dataVolumeIndex = getDataVolumeTemplates(vm).findIndex( - (dataVolume) => getName(dataVolume) === dataVolumeName, + patches.push( + new PatchBuilder('/spec/dataVolumeTemplates') + .setListRemoveSimpleValue(dataVolumeName, getDataVolumeTemplates(vm), getName) + .build(), ); - if (dataVolumeIndex >= 0) { - patches.push({ - op: 'remove', - path: `/spec/dataVolumeTemplates/${dataVolumeIndex}`, - }); - } } - const bootOrderIndex = getDeviceBootOrder(disk); - if (bootOrderIndex != null) { - return [...patches, ...getDeviceBootOrderPatch(vm, 'disks', diskName)]; + if (diskWrapper.hasBootOrder()) { + return [ + ...patches, + ...getShiftBootOrderPatches( + '/spec/template/spec/domain/devices/disks', + disks, + diskName, + diskWrapper.getBootOrder(), + ), + ...getShiftBootOrderPatches( + '/spec/template/spec/domain/devices/interfaces', + getInterfaces(vm), + null, + diskWrapper.getBootOrder(), + ), + ]; } return patches; }); }; -export const getAddDiskPatches = ( +export const getUpdateDiskPatches = async ( vmLikeEntity: VMLikeEntityKind, - disk: object, - storageClassConfigMap: ConfigMapKind, -): Patch[] => { + { + disk, + volume, + dataVolume, + oldDiskName, + oldVolumeName, + oldDataVolumeName, + }: { + disk: V1Disk; + volume: V1Volume; + dataVolume: V1alpha1DataVolume; + oldDiskName: string; + oldVolumeName: string; + oldDataVolumeName: string; + }, +): Promise => { + let finalDataVolume; + if (dataVolume) { + const dataVolumeWrapper = DataVolumeWrapper.initialize(dataVolume); + const storageClassConfigMap = await getStorageClassConfigMap({ k8sGet }); + const storageClassName = dataVolumeWrapper.getStorageClassName(); + + finalDataVolume = DataVolumeWrapper.mergeWrappers( + DataVolumeWrapper.initializeFromSimpleData({ + accessModes: [getDefaultSCAccessMode(storageClassConfigMap, storageClassName)], + volumeMode: getDefaultSCVolumeMode(storageClassConfigMap, storageClassName), + }), + dataVolumeWrapper, + ).asResource(); + } return getVMLikePatches(vmLikeEntity, (vm) => { - return getAddDiskPatch(vm, disk, storageClassConfigMap); + const disks = getDisks(vm, null); + const volumes = getVolumes(vm, null); + const dataVolumeTemplates = getDataVolumeTemplates(vm, null); + + return [ + new PatchBuilder('/spec/template/spec/domain/devices/disks') + .setListUpdate(disk, disks, getSimpleName, oldDiskName) + .build(), + new PatchBuilder('/spec/template/spec/volumes') + .setListUpdate(volume, volumes, getSimpleName, oldVolumeName) + .build(), + finalDataVolume && + new PatchBuilder('/spec/dataVolumeTemplates') + .setListUpdate(finalDataVolume, dataVolumeTemplates, getSimpleName, oldDataVolumeName) + .build(), + ].filter((patch) => patch); }); }; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-nic-patches.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-nic-patches.ts index e57395328ba..1524599cc57 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-nic-patches.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-nic-patches.ts @@ -9,7 +9,7 @@ import { NetworkWrapper } from '../../wrapper/vm/network-wrapper'; import { NetworkInterfaceWrapper } from '../../wrapper/vm/network-interface-wrapper'; import { getShiftBootOrderPatches } from './utils'; -export const getRemoveNicPatches = (vmLikeEntity: VMLikeEntityKind, nic: any): Patch[] => { +export const getRemoveNICPatches = (vmLikeEntity: VMLikeEntityKind, nic: any): Patch[] => { return getVMLikePatches(vmLikeEntity, (vm) => { const nicName = nic.name; const nics = getInterfaces(vm); @@ -60,7 +60,7 @@ export const getRemoveNicPatches = (vmLikeEntity: VMLikeEntityKind, nic: any): P }); }; -export const getAddNicPatches = ( +export const getUpdateNICPatches = ( vmLikeEntity: VMLikeEntityKind, { nic, @@ -75,14 +75,10 @@ export const getAddNicPatches = ( return [ new PatchBuilder('/spec/template/spec/domain/devices/interfaces') - .setListUpdate(nic, nics, (currentNIC) => - currentNIC === nic ? oldNICName : getSimpleName(currentNIC), - ) + .setListUpdate(nic, nics, getSimpleName, oldNICName) .build(), new PatchBuilder('/spec/template/spec/networks') - .setListUpdate(network, networks, (currentNetwork) => - currentNetwork === network ? oldNetworkName : getSimpleName(currentNetwork), - ) + .setListUpdate(network, networks, getSimpleName, oldNetworkName) .build(), ].filter((patch) => patch); }); diff --git a/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/constants.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/constants.ts new file mode 100644 index 00000000000..1f9bf3add96 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/constants.ts @@ -0,0 +1,3 @@ +export const STORAGE_CLASS_CONFIG_MAP_NAME = 'kubevirt-storage-class-defaults'; +// Different releases, different locations. Respect the order when resolving. Otherwise the configMap name/namespace is considered as well-known. +export const STORAGE_CLASS_CONFIG_MAP_NAMESPACES = ['openshift-cnv', 'openshift']; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/index.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/index.ts new file mode 100644 index 00000000000..3ee498e7134 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/index.ts @@ -0,0 +1 @@ +export * from './storageClassConfigMap'; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/storageClassConfigMap.ts b/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/storageClassConfigMap.ts new file mode 100644 index 00000000000..4507e151ef7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/requests/config-map/storage-class/storageClassConfigMap.ts @@ -0,0 +1,44 @@ +import { ConfigMapModel } from '@console/internal/models'; +import { ConfigMapKind } from '@console/internal/module/k8s'; +import { STORAGE_CLASS_CONFIG_MAP_NAME, STORAGE_CLASS_CONFIG_MAP_NAMESPACES } from './constants'; + +const { warn } = console; + +const getStorageClassConfigMapInNamespace = async ({ + k8sGet, + namespace, +}: { + namespace: string; + k8sGet: (...opts: any) => Promise; +}): Promise => { + try { + return await k8sGet(ConfigMapModel, STORAGE_CLASS_CONFIG_MAP_NAME, namespace, null, { + disableHistory: true, + }); + } catch (e) { + return null; + } +}; + +export const getStorageClassConfigMap = async (props: { + k8sGet: (...opts: any[]) => Promise; +}): Promise => { + // query namespaces sequentially to respect order + // eslint-disable-next-line @typescript-eslint/prefer-for-of + for (let index = 0; index < STORAGE_CLASS_CONFIG_MAP_NAMESPACES.length; index++) { + // eslint-disable-next-line no-await-in-loop + const configMap = await getStorageClassConfigMapInNamespace({ + namespace: STORAGE_CLASS_CONFIG_MAP_NAMESPACES[index], + ...props, + }); + if (configMap) { + return configMap; + } + } + warn( + `The ${STORAGE_CLASS_CONFIG_MAP_NAME} can not be found in none of following namespaces: `, + JSON.stringify(STORAGE_CLASS_CONFIG_MAP_NAMESPACES), + '. The PVCs will be created with default values.', + ); + return null; +}; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/utils/patch.ts b/frontend/packages/kubevirt-plugin/src/k8s/utils/patch.ts index 6b90a6c8dcc..c49210bd4f2 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/utils/patch.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/utils/patch.ts @@ -39,12 +39,19 @@ export class PatchBuilder { return this; }; - setListRemove = (value: T, items: T[], compareGetter?: (t: T) => any) => { + setListRemove = (value: T, items: T[], compareGetter?: (t: T) => any) => + this.setListRemoveSimpleValue( + compareGetter ? compareGetter(value) : value, + items, + compareGetter, + ); + + setListRemoveSimpleValue = (value: T | U, items: T[], compareGetter?: (t: T) => U) => { this.value = undefined; this.operation = PatchOperation.REMOVE; if (items) { const foundIndex = items.findIndex((t) => - compareGetter ? compareGetter(t) === compareGetter(value) : t === value, + compareGetter ? compareGetter(t) === (value as U) : t === (value as T), ); if (foundIndex < 0) { this.valid = false; // do not do anything @@ -57,11 +64,18 @@ export class PatchBuilder { return this; }; - setListUpdate = (value: T, items?: T[], compareGetter?: (t: T) => any) => { + setListUpdate = ( + value: T, + items?: T[], + compareGetter?: (t: T) => U, + oldSimpleValue?: T | U, + ) => { if (items) { this.value = value; const foundIndex = items.findIndex((t) => - compareGetter ? compareGetter(t) === compareGetter(value) : t === value, + compareGetter + ? compareGetter(t) === ((oldSimpleValue as U) || compareGetter(value)) + : t === (oldSimpleValue || value), ); if (foundIndex < 0) { this.valueIndex = items.length; diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/data-volume-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/data-volume-wrapper.ts new file mode 100644 index 00000000000..b143b6a0b9a --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/data-volume-wrapper.ts @@ -0,0 +1,129 @@ +import * as _ from 'lodash'; +import { getName } from '@console/shared/src'; +import { validate } from '@console/internal/components/utils'; +import { ObjectWithTypePropertyWrapper } from '../common/object-with-type-property-wrapper'; +import { V1alpha1DataVolume } from '../../../types/vm/disk/V1alpha1DataVolume'; +import { DataVolumeSourceType } from '../../../constants/vm/storage'; +import { + getDataVolumeStorageClassName, + getDataVolumeStorageSize, +} from '../../../selectors/dv/selectors'; + +type CombinedTypeData = { + name?: string; + namespace?: string; + url?: string; +}; + +const sanitizeTypeData = (type: DataVolumeSourceType, typeData: CombinedTypeData) => { + if (!type || !typeData) { + return null; + } + const { name, namespace, url } = typeData; + + if (type === DataVolumeSourceType.BLANK) { + return {}; + } + if (type === DataVolumeSourceType.HTTP) { + return { url }; + } + if (type === DataVolumeSourceType.PVC) { + return { name, namespace }; + } + + return null; +}; + +export class DataVolumeWrapper extends ObjectWithTypePropertyWrapper< + V1alpha1DataVolume, + DataVolumeSourceType +> { + static readonly EMPTY = new DataVolumeWrapper(); + + static mergeWrappers = (...datavolumeWrappers: DataVolumeWrapper[]): DataVolumeWrapper => + ObjectWithTypePropertyWrapper.defaultMergeWrappersWithType( + DataVolumeWrapper, + datavolumeWrappers, + ); + + static initializeFromSimpleData = ( + params?: { + name?: string; + type?: DataVolumeSourceType; + typeData?: CombinedTypeData; + accessModes?: object[] | string[]; + volumeMode?: object | string; + size?: string | number; + unit?: string; + storageClassName?: string; + }, + opts?: { sanitizeTypeData: boolean }, + ) => { + if (!params) { + return DataVolumeWrapper.EMPTY; + } + const { name, type, typeData, accessModes, volumeMode, size, unit, storageClassName } = params; + const resources = + size == null + ? undefined + : { + requests: { + storage: size && unit ? `${size}${unit}` : size, + }, + }; + + return new DataVolumeWrapper( + { + metadata: { + name, + }, + spec: { + pvc: { + accessModes: _.cloneDeep(accessModes), + volumeMode: _.cloneDeep(volumeMode), + resources, + storageClassName, + }, + source: {}, + }, + }, + { + initializeWithType: type, + initializeWithTypeData: + opts && opts.sanitizeTypeData ? sanitizeTypeData(type, typeData) : _.cloneDeep(typeData), + }, + ); + }; + + static initialize = (dataVolumeTemplate?: V1alpha1DataVolume, copy?: boolean) => + new DataVolumeWrapper(dataVolumeTemplate, copy && { copy }); + + protected constructor( + dataVolumeTemplate?: V1alpha1DataVolume, + opts?: { + initializeWithType?: DataVolumeSourceType; + initializeWithTypeData?: any; + copy?: boolean; + }, + ) { + super(dataVolumeTemplate, opts, DataVolumeSourceType, ['spec', 'source']); + } + + getName = () => getName(this.data as any); + + getStorageClassName = () => getDataVolumeStorageClassName(this.data as any); + + getPesistentVolumeClaimName = () => this.getIn(['spec', 'source', 'pvc', 'name']); + + getURL = () => this.getIn(['spec', 'source', 'http', 'url']); + + getSize = (): { value: number; unit: string } => { + const parts = validate.split(getDataVolumeStorageSize(this.data as any) || ''); + return { + value: parts[0], + unit: parts[1], + }; + }; + + hasSize = () => this.getSize().value > 0; +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/disk-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/disk-wrapper.ts new file mode 100644 index 00000000000..9bdbaec17c2 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/disk-wrapper.ts @@ -0,0 +1,56 @@ +import { ObjectWithTypePropertyWrapper } from '../common/object-with-type-property-wrapper'; +import { V1Disk } from '../../../types/vm/disk/V1Disk'; +import { DiskType, DiskBus } from '../../../constants/vm/storage'; + +export class DiskWrapper extends ObjectWithTypePropertyWrapper { + static readonly EMPTY = new DiskWrapper(); + + static mergeWrappers = (...disks: DiskWrapper[]): DiskWrapper => + ObjectWithTypePropertyWrapper.defaultMergeWrappersWithType(DiskWrapper, disks); + + static initializeFromSimpleData = (params?: { + name?: string; + type?: DiskType; + bus?: DiskBus; + bootOrder?: number; + }) => { + if (!params) { + return DiskWrapper.EMPTY; + } + const { name, type, bus, bootOrder } = params; + return new DiskWrapper( + { + name, + bootOrder, + }, + { + initializeWithType: type, + initializeWithTypeData: type === DiskType.DISK ? { bus: bus.getValue() } : undefined, + }, + ); + }; + + static initialize = (disk?: V1Disk, copy?: boolean) => new DiskWrapper(disk, copy && { copy }); + + protected constructor( + disk?: V1Disk, + opts?: { initializeWithType?: DiskType; initializeWithTypeData?: any; copy?: boolean }, + ) { + super(disk, opts, DiskType); + } + + getName = () => this.get('name'); + + getDiskBus = (): DiskBus => DiskBus.fromString(this.getIn(['disk', 'bus'])); + + getReadableDiskBus = () => { + const diskBus = this.getDiskBus(); + return diskBus && diskBus.toString(); + }; + + getBootOrder = () => this.get('bootOrder'); + + isFirstBootableDevice = () => this.getBootOrder() === 1; + + hasBootOrder = () => this.getBootOrder() != null; +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/volume-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/volume-wrapper.ts new file mode 100644 index 00000000000..f286062586e --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/volume-wrapper.ts @@ -0,0 +1,82 @@ +import * as _ from 'lodash'; +import { ObjectWithTypePropertyWrapper } from '../common/object-with-type-property-wrapper'; +import { V1Volume } from '../../../types/vm/disk/V1Volume'; +import { VolumeType } from '../../../constants/vm/storage'; +import { + getVolumeContainerImage, + getVolumeDataVolumeName, + getVolumePersistentVolumeClaimName, +} from '../../../selectors/vm'; + +type CombinedTypeData = { + name?: string; + claimName?: string; + image?: string; +}; + +const sanitizeTypeData = (type: VolumeType, typeData: CombinedTypeData) => { + if (!type || !typeData) { + return null; + } + const { name, claimName, image } = typeData; + + if (type === VolumeType.DATA_VOLUME) { + return { name }; + } + if (type === VolumeType.PERSISTENT_VOLUME_CLAIM) { + return { claimName }; + } + + if (type === VolumeType.CONTAINER_DISK) { + return { image }; + } + + return null; +}; + +export class VolumeWrapper extends ObjectWithTypePropertyWrapper { + static readonly EMPTY = new VolumeWrapper(); + + static mergeWrappers = (...volumes: VolumeWrapper[]): VolumeWrapper => + ObjectWithTypePropertyWrapper.defaultMergeWrappersWithType(VolumeWrapper, volumes); + + static initializeFromSimpleData = ( + params?: { + name?: string; + type?: VolumeType; + typeData?: CombinedTypeData; + }, + opts?: { sanitizeTypeData: boolean }, + ) => { + if (!params) { + return VolumeWrapper.EMPTY; + } + const { name, type, typeData } = params; + return new VolumeWrapper( + { name }, + { + initializeWithType: type, + initializeWithTypeData: + opts && opts.sanitizeTypeData ? sanitizeTypeData(type, typeData) : _.cloneDeep(typeData), + }, + ); + }; + + static initialize = (volume?: V1Volume, copy?: boolean) => + new VolumeWrapper(volume, copy && { copy }); + + protected constructor( + volume?: V1Volume, + opts?: { initializeWithType?: VolumeType; initializeWithTypeData?: any; copy?: boolean }, + ) { + super(volume, opts, VolumeType); + } + + getName = () => this.get('name'); + + getPersistentVolumeClaimName = () => getVolumePersistentVolumeClaimName(this.data); + + getDataVolumeName = () => getVolumeDataVolumeName(this.data); + + getContainerImage = () => getVolumeContainerImage(this.data); +} diff --git a/frontend/packages/kubevirt-plugin/src/selectors/config-map/sc-defaults.ts b/frontend/packages/kubevirt-plugin/src/selectors/config-map/sc-defaults.ts new file mode 100644 index 00000000000..144252d50d0 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/selectors/config-map/sc-defaults.ts @@ -0,0 +1,42 @@ +import * as _ from 'lodash'; +import { ConfigMapKind } from '@console/internal/module/k8s'; +import { PVC_ACCESSMODE_DEFAULT, PVC_VOLUMEMODE_DEFAULT } from '../../constants/pvc'; + +const getSCConfigMapAttribute = ( + storageClassConfigMap: ConfigMapKind, + storageClassName: string, + attributeName: string, + defaultValue: string, +): string => { + const hasSubAttribute = + storageClassName && + attributeName && + _.has(storageClassConfigMap, ['data', `${storageClassName}.${attributeName}`]); + return _.get( + storageClassConfigMap, + ['data', hasSubAttribute ? `${storageClassName}.${attributeName}` : attributeName], + defaultValue, + ); +}; + +export const getDefaultSCAccessMode = ( + storageClassConfigMap: ConfigMapKind, + storageClassName: string, +) => + getSCConfigMapAttribute( + storageClassConfigMap, + storageClassName, + 'accessMode', + PVC_ACCESSMODE_DEFAULT, + ); + +export const getDefaultSCVolumeMode = ( + storageClassConfigMap: ConfigMapKind, + storageClassName: string, +) => + getSCConfigMapAttribute( + storageClassConfigMap, + storageClassName, + 'volumeMode', + PVC_VOLUMEMODE_DEFAULT, + ); diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm/volume.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm/volume.ts index bf207df2f45..f269aca726a 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm/volume.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/vm/volume.ts @@ -5,3 +5,6 @@ export const getVolumePersistentVolumeClaimName = (volume) => export const getVolumeDataVolumeName = (volume) => _.get(volume, 'dataVolume.name'); export const getVolumeCloudInitUserData = (volume) => _.get(volume, 'cloudInitNoCloud.userData'); + +export const getVolumeContainerImage = (volume) => + volume && volume.containerDisk && volume.containerDisk.image; diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimSpec.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimSpec.ts index 89100dc8caa..e58aaca2991 100644 --- a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimSpec.ts +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1PersistentVolumeClaimSpec.ts @@ -26,7 +26,7 @@ export interface V1PersistentVolumeClaimSpec { * @type {Array} * @memberof V1PersistentVolumeClaimSpec */ - accessModes?: object[]; + accessModes?: object[] | string[]; /** * * @type {V1TypedLocalObjectReference} @@ -56,7 +56,7 @@ export interface V1PersistentVolumeClaimSpec { * @type {object} * @memberof V1PersistentVolumeClaimSpec */ - volumeMode?: object; + volumeMode?: object | string; /** * VolumeName is the binding reference to the PersistentVolume backing this claim. * @type {string} diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/strings.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/strings.ts index 1739bb2eb87..e7203c4df8e 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/strings.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/strings.ts @@ -16,3 +16,5 @@ export const VIRTUAL_MACHINE_TEMPLATE_EXISTS = 'is already used in another templ export const MAC_ADDRESS_INVALID_ERROR = 'Invalid MAC address format'; export const NIC_NAME_EXISTS = 'Interface with this name already exists'; export const NETWORK_MULTUS_NAME_EXISTS = 'Multus network with this name already exists'; + +export const POSITIVE_SIZE_ERROR = 'must be positive'; diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/disk.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/disk.ts index b119bafb1b6..50bafa9bc3f 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/disk.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/disk.ts @@ -1,21 +1,117 @@ -import { EntityMap } from '@console/shared'; -import { validateDNS1123SubdomainValue } from '../common'; -import { addMissingSubject } from '../../grammar'; -import { ValidationErrorType } from '../types'; +import { + getValidationObject, + validateDNS1123SubdomainValue, + validateTrim, + validateURL, +} from '../common'; +import { addMissingSubject, makeSentence } from '../../grammar'; +import { DiskWrapper } from '../../../k8s/wrapper/vm/disk-wrapper'; +import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; +import { ValidationErrorType, ValidationObject } from '../types'; +import { POSITIVE_SIZE_ERROR } from '../strings'; +import { StorageUISource } from '../../../components/modals/disk-modal/storage-ui-source'; -export const validateDiskName = (name: string, diskLookup: EntityMap) => { +const validateDiskName = (name: string, usedDiskNames: Set): ValidationObject => { let validation = validateDNS1123SubdomainValue(name); if (validation) { validation.message = addMissingSubject(validation.message, 'Name'); } - if (!validation && diskLookup[name]) { - validation = { - type: ValidationErrorType.Error, - message: 'Disk with this name already exists!', - }; + if (!validation && usedDiskNames && usedDiskNames.has(name)) { + validation = getValidationObject('Disk with this name already exists!'); } return validation; }; + +const validatePVCName = (pvcName: string, usedPVCNames: Set): ValidationObject => { + if (usedPVCNames && usedPVCNames.has(pvcName)) { + getValidationObject('PVC with this name is already used by this VM!'); + } + + return null; +}; + +const getEmptyDiskSizeValidation = (): ValidationObject => + getValidationObject( + makeSentence(addMissingSubject(POSITIVE_SIZE_ERROR, 'Size')), + ValidationErrorType.TrivialError, + ); + +export const validateDisk = ( + disk: DiskWrapper, + volume: VolumeWrapper, + dataVolume: DataVolumeWrapper, + { + usedDiskNames, + usedPVCNames, + }: { + usedDiskNames?: Set; + usedPVCNames?: Set; + }, +): UIDiskValidation => { + const validations = { + name: validateDiskName(disk && disk.getName(), usedDiskNames), + size: null, + url: null, + container: null, + pvc: null, + }; + let hasAllRequiredFilled = disk && disk.getName() && volume && volume.hasType(); + + const addRequired = (addon) => { + if (hasAllRequiredFilled) { + hasAllRequiredFilled = hasAllRequiredFilled && addon; + } + }; + + const type = StorageUISource.fromTypes(volume.getType(), dataVolume && dataVolume.getType()); + + if (type) { + if (type.requiresURL()) { + const url = dataVolume && dataVolume.getURL(); + addRequired(url); + validations.url = validateURL(url, { subject: 'URL' }); + } + + if (type.requiresContainerImage()) { + const container = volume.getContainerImage(); + addRequired(container); + validations.container = validateTrim(container, { subject: 'Container' }); + } + + if (type.requiresDatavolume()) { + addRequired(dataVolume); + if (!dataVolume || !dataVolume.hasSize()) { + addRequired(null); + validations.size = getEmptyDiskSizeValidation(); + } + } + + if (type.requiresPVC()) { + const pvcName = type.getPVCName(volume, dataVolume); + addRequired(pvcName); + validations.pvc = validatePVCName(pvcName, usedPVCNames); + } + } + + return { + validations, + hasAllRequiredFilled: !!hasAllRequiredFilled, + isValid: !!hasAllRequiredFilled && !Object.keys(validations).find((key) => validations[key]), + }; +}; + +export type UIDiskValidation = { + validations: { + name?: ValidationObject; + size?: ValidationObject; + url?: ValidationObject; + container?: ValidationObject; + pvc?: ValidationObject; + }; + isValid: boolean; + hasAllRequiredFilled: boolean; +}; From 7275a267be473c8500ddf7f704ad7484dd79e5e5 Mon Sep 17 00:00:00 2001 From: suomiy Date: Thu, 3 Oct 2019 11:10:31 +0200 Subject: [PATCH 5/6] kubevirt: add Storage Tab to CreateVMWizard - remove unnecesary resource loading for the whole wizard - fix bootOrder changes - create ReviewList component - create CombinedDisk and ProvisionSource wrappers --- .../create-vm-wizard/create-vm-wizard.tsx | 117 ++++++--- .../create-vm-wizard/form/form-field-row.tsx | 10 +- .../create-vm-wizard/form/form-field.tsx | 9 +- .../create-vm-wizard/redux/actions.ts | 30 ++- .../storage-tab-initial-state.ts | 80 ++++-- .../vm-settings-tab-initial-state.ts | 14 +- .../redux/internal-actions.ts | 36 ++- .../create-vm-wizard/redux/reducers.ts | 89 ++++++- .../prefill-vm-template-state-update.ts | 109 ++++---- .../vmSettings/storage-tab-state-update.ts | 61 +++++ .../vm-settings-tab-state-update.ts | 57 +---- .../create-vm-wizard/redux/types.ts | 30 ++- .../create-vm-wizard/redux/utils.ts | 7 +- .../validations/networks-tab-validation.ts | 11 +- .../validations/storage-tab-validation.ts | 91 +++++++ .../redux/validations/utils.ts | 4 + .../validations/vm-settings-tab-validation.ts | 26 +- .../create-vm-wizard/resource-load-errors.tsx | 4 - .../selectors/immutable/storage.ts | 12 +- .../selectors/immutable/vm-settings.ts | 4 + .../create-vm-wizard/selectors/selectors.ts | 27 +- .../create-vm-wizard/strings/storage.ts | 3 + .../create-vm-wizard/strings/vm-settings.ts | 16 +- ...e-networks.tsx => network-boot-source.tsx} | 51 +--- .../tabs/networking-tab/networking-tab.tsx | 51 ++-- .../vm-wizard-nic-modal-enhanced.tsx | 38 ++- .../tabs/review-tab/networking-review.tsx | 40 ++- ...etworking-review.scss => review-list.scss} | 4 +- .../tabs/review-tab/review-list.tsx | 30 +++ .../tabs/review-tab/review-tab.tsx | 5 + .../tabs/review-tab/storage-review.tsx | 69 ++++++ .../tabs/storage-tab/storage-boot-source.tsx | 68 +++++ .../tabs/storage-tab/storage-tab.scss | 14 ++ .../tabs/storage-tab/storage-tab.tsx | 210 ++++++++++++++++ .../tabs/storage-tab/types.tsx | 17 ++ .../vm-wizard-storage-modal-enhanced.tsx | 147 +++++++++++ .../storage-tab/vm-wizard-storage-row.tsx | 95 +++++++ .../tabs/vm-settings-tab/container-source.tsx | 50 ++++ .../tabs/vm-settings-tab/url-source.tsx | 47 ++++ .../tabs/vm-settings-tab/user-templates.tsx | 7 +- .../tabs/vm-settings-tab/vm-settings-tab.tsx | 51 ++-- .../src/components/create-vm-wizard/types.ts | 43 +++- .../modals/disk-modal/disk-modal-enhanced.tsx | 20 +- .../modals/disk-modal/disk-modal.tsx | 24 +- .../modals/disk-modal/storage-ui-source.ts | 13 - .../src/components/table/validation-cell.scss | 2 +- .../src/components/table/validation-cell.tsx | 7 +- .../src/components/vm-disks/disk-row.tsx | 37 ++- .../src/components/vm-disks/types.ts | 7 +- .../src/components/vm-disks/vm-disks.tsx | 89 ++----- .../src/constants/vm-templates/constants.ts | 2 + .../src/constants/vm/constants.ts | 5 + .../src/constants/vm/provision-source.ts | 117 +++++++++ .../src/k8s/patches/vm/vm-disk-patches.ts | 2 +- .../src/k8s/wrapper/vm/combined-disk.ts | 232 ++++++++++++++++++ .../src/k8s/wrapper/vm/data-volume-wrapper.ts | 13 + .../src/k8s/wrapper/vm/disk-wrapper.ts | 2 +- .../src/selectors/dv/selectors.ts | 26 -- .../src/selectors/vm-template/combined.ts | 109 -------- .../src/types/vm/disk/V1ObjectMeta.ts | 4 +- .../kubevirt-plugin/src/types/vm/index.ts | 9 - .../kubevirt-plugin/src/utils/immutable.ts | 2 + .../kubevirt-plugin/src/utils/utils.ts | 7 +- .../src/utils/validations/vm/disk.ts | 19 +- .../src/utils/validations/vm/vm.ts | 12 +- 65 files changed, 1982 insertions(+), 662 deletions(-) create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/storage-tab-state-update.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/storage-tab-validation.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/storage.ts rename frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/{pxe-networks.tsx => network-boot-source.tsx} (50%) rename frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/{networking-review.scss => review-list.scss} (62%) create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/review-list.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/review-tab/storage-review.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/storage-boot-source.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/storage-tab.scss create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/storage-tab.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/types.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-modal-enhanced.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-row.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/container-source.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/url-source.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/constants/vm/provision-source.ts create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/combined-disk.ts delete mode 100644 frontend/packages/kubevirt-plugin/src/selectors/vm-template/combined.ts diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx index 3392b65c601..716565e78c2 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx @@ -3,11 +3,7 @@ import * as _ from 'lodash'; import { connect } from 'react-redux'; import { createVm, createVmTemplate } from 'kubevirt-web-ui-components'; import { Wizard } from '@patternfly/react-core'; -import { - PersistentVolumeClaimModel, - StorageClassModel, - TemplateModel, -} from '@console/internal/models'; +import { TemplateModel } from '@console/internal/models'; import { Firehose, history, @@ -15,13 +11,15 @@ import { makeReduxID, units, } from '@console/internal/components/utils'; +import { k8sGet, TemplateKind } from '@console/internal/module/k8s'; import { withReduxID } from '../../utils/redux/common'; +import { VirtualMachineModel } from '../../models'; import { - DataVolumeModel, - NetworkAttachmentDefinitionModel, - VirtualMachineModel, -} from '../../models'; -import { TEMPLATE_TYPE_BASE, TEMPLATE_TYPE_LABEL, TEMPLATE_TYPE_VM } from '../../constants/vm'; + TEMPLATE_TYPE_BASE, + TEMPLATE_TYPE_LABEL, + TEMPLATE_TYPE_VM, + VolumeType, +} from '../../constants/vm'; import { getResource } from '../../utils'; import { EnhancedK8sMethods } from '../../k8s/enhancedK8sMethods/enhancedK8sMethods'; import { cleanupAndGetResults, getResults } from '../../k8s/enhancedK8sMethods/k8sMethodsUtils'; @@ -35,6 +33,14 @@ import { getTemplateOperatingSystems } from '../../selectors/vm-template/advance import { ResultsWrapper } from '../../k8s/enhancedK8sMethods/types'; import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; import { NetworkInterfaceWrapper } from '../../k8s/wrapper/vm/network-interface-wrapper'; +import { VolumeWrapper } from '../../k8s/wrapper/vm/volume-wrapper'; +import { DiskWrapper } from '../../k8s/wrapper/vm/disk-wrapper'; +import { DataVolumeWrapper } from '../../k8s/wrapper/vm/data-volume-wrapper'; +import { + getDefaultSCAccessMode, + getDefaultSCVolumeMode, +} from '../../selectors/config-map/sc-defaults'; +import { getStorageClassConfigMap } from '../../k8s/requests/config-map/storage-class'; import { ChangedCommonData, CommonData, @@ -43,6 +49,7 @@ import { VMSettingsField, VMWizardNetwork, VMWizardProps, + VMWizardStorage, VMWizardTab, } from './types'; import { CREATE_VM, CREATE_VM_TEMPLATE, TabTitleResolver } from './strings/strings'; @@ -56,6 +63,7 @@ import { VMSettingsTab } from './tabs/vm-settings-tab/vm-settings-tab'; import { NetworkingTab } from './tabs/networking-tab/networking-tab'; import { ReviewTab } from './tabs/review-tab/review-tab'; import { ResultTab } from './tabs/result-tab/result-tab'; +import { StorageTab } from './tabs/storage-tab/storage-tab'; import './create-vm-wizard.scss'; @@ -63,7 +71,19 @@ import './create-vm-wizard.scss'; /** * * kubevirt-web-ui-components InterOP */ -const kubevirtInterOP = ({ activeNamespace, vmSettings, networks, storages, templates }) => { +const kubevirtInterOP = async ({ + activeNamespace, + vmSettings, + networks, + storages, + templates, +}: { + activeNamespace: string; + vmSettings: any; + networks: VMWizardNetwork[]; + storages: VMWizardStorage[]; + templates: TemplateKind[]; +}) => { const clonedVMsettings = _.cloneDeep(vmSettings); const clonedNetworks = _.cloneDeep(networks); const clonedStorages = _.cloneDeep(storages); @@ -92,10 +112,48 @@ const kubevirtInterOP = ({ activeNamespace, vmSettings, networks, storages, temp }; }); + const storageClassConfigMap = await getStorageClassConfigMap({ k8sGet }); + + const interOPStorages = clonedStorages.map(({ disk, volume, dataVolume }) => { + const diskWrapper = DiskWrapper.initialize(disk); + const volumeWrapper = VolumeWrapper.initialize(volume); + const dataVolumeWrapper = dataVolume && DataVolumeWrapper.initialize(dataVolume); + + return { + name: diskWrapper.getName(), + isBootable: diskWrapper.isFirstBootableDevice(), + storageType: + volumeWrapper.getType() === VolumeType.DATA_VOLUME && dataVolume ? 'datavolume' : undefined, + templateStorage: { + volume, + disk, + dataVolumeTemplate: dataVolumeWrapper + ? DataVolumeWrapper.mergeWrappers( + dataVolumeWrapper, + DataVolumeWrapper.initializeFromSimpleData({ + accessModes: + dataVolumeWrapper.getAccessModes() || + getDefaultSCAccessMode( + storageClassConfigMap, + dataVolumeWrapper.getStorageClassName(), + ), + volumeMode: + dataVolumeWrapper.getVolumeMode() || + getDefaultSCVolumeMode( + storageClassConfigMap, + dataVolumeWrapper.getStorageClassName(), + ), + }), + ).asResource() + : undefined, + }, + }; + }); + return { interOPVMSettings: clonedVMsettings, interOPNetworks, - interOPStorages: clonedStorages, + interOPStorages, }; }; @@ -131,7 +189,7 @@ export class CreateVMWizardComponent extends React.Component { this.props.onResultsChanged({ errors: [], requestResults: [] }, null, true, true); // reset const create = this.props.isCreateTemplate ? createVmTemplate : createVm; @@ -150,7 +208,7 @@ export class CreateVMWizardComponent extends React.Component getResults(enhancedK8sMethods)) @@ -173,7 +231,7 @@ export class CreateVMWizardComponent extends React.Component console.error(e)); // eslint-disable-line no-console - } + }; render() { const { isCreateTemplate, reduxID, stepData } = this.props; @@ -202,9 +260,15 @@ export class CreateVMWizardComponent extends React.Component ), }, - // { - // id: VMWizardTab.STORAGE, - // }, + { + id: VMWizardTab.STORAGE, + component: ( + <> + + + + ), + }, { id: VMWizardTab.REVIEW, component: , @@ -291,10 +355,10 @@ const wizardDispatchToProps = (dispatch, props) => ({ ); }, onClose: () => { + dispatch(vmWizardActions[ActionType.Dispose](props.reduxID, props)); if (props.onClose) { props.onClose(); } - dispatch(vmWizardActions[ActionType.Dispose](props.reduxID, props)); }, }); @@ -325,19 +389,6 @@ export const CreateVMWizardPageComponent: React.FC { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field-row.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field-row.tsx index f6abd2eba56..9e4c43f9642 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field-row.tsx @@ -4,6 +4,7 @@ import { getFieldHelp, getFieldId, getFieldTitle } from '../utils/vm-settings-ta import { iGetFieldValue, isFieldHidden, isFieldRequired } from '../selectors/immutable/vm-settings'; import { iGet, iGetIn, iGetIsLoaded } from '../../../utils/immutable'; import { FormRow } from '../../form/form-row'; +import { ValidationObject } from '../../../utils/validations/types'; import { FormFieldContext } from './form-field-context'; import { FormFieldType } from './form-field'; import { FormFieldReviewContext } from './form-field-review-context'; @@ -18,6 +19,7 @@ export const FormFieldRow: React.FC = ({ fieldType, children, loadingResources, + validation, }) => { const fieldKey = iGet(field, 'key'); @@ -41,9 +43,10 @@ export const FormFieldRow: React.FC = ({ } help={getFieldHelp(fieldKey, iGetFieldValue(field))} isRequired={isFieldRequired(field)} - validationMessage={iGetIn(field, ['validation', 'message'])} - validationType={iGetIn(field, ['validation', 'type'])} + validationMessage={validation ? undefined : iGetIn(field, ['validation', 'message'])} + validationType={validation ? undefined : iGetIn(field, ['validation', 'type'])} isLoading={loading} + validation={validation} > {children} @@ -60,6 +63,7 @@ type FieldFormRowProps = { fieldType: FormFieldType; children?: React.ReactNode; loadingResources?: { [k: string]: any }; + validation?: ValidationObject; }; export const FormFieldMemoRow = React.memo( @@ -67,5 +71,7 @@ export const FormFieldMemoRow = React.memo( (prevProps, nextProps) => prevProps.field === nextProps.field && prevProps.fieldType === nextProps.fieldType && + _.get(prevProps.validation, ['type']) === _.get(nextProps.validation, ['type']) && + _.get(prevProps.validation, ['message']) === _.get(nextProps.validation, ['message']) && isLoading(prevProps.loadingResources) === isLoading(nextProps.loadingResources), ); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field.tsx index 6247bdfee50..18cc14bebc7 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/form/form-field.tsx @@ -36,7 +36,7 @@ const setSupported = (fieldType: FormFieldType, supportedTypes: Set = ({ children, isDisabled }) => { +export const FormField: React.FC = ({ children, isDisabled, value }) => { return ( {({ @@ -49,7 +49,7 @@ export const FormField: React.FC = ({ children, isDisabled }) => isLoading: boolean; }) => { const set = setSupported.bind(undefined, fieldType); - const value = iGetFieldValue(field); + const val = value || iGetFieldValue(field); const key = iGetFieldKey(field); const disabled = isDisabled || isFieldDisabled(field) || isLoading; @@ -57,8 +57,8 @@ export const FormField: React.FC = ({ children, isDisabled }) => children, _.omitBy( { - value: hasValue.has(fieldType) ? value || getPlaceholder(key) || '' : undefined, - isChecked: set(hasIsChecked, value), + value: hasValue.has(fieldType) ? val || getPlaceholder(key) || '' : undefined, + isChecked: set(hasIsChecked, val), isDisabled: set(hasIsDisabled, disabled), disabled: set(hasDisabled, disabled), isRequired: set(hasIsRequired, isFieldRequired(field)), @@ -77,4 +77,5 @@ export const FormField: React.FC = ({ children, isDisabled }) => type FormFieldProps = { children: React.ReactNode; isDisabled?: boolean; + value?: any; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts index 348bc8bf6df..5054819e810 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts @@ -7,7 +7,9 @@ import { VMSettingsField, VMWizardNetwork, VMWizardTab, + VMWizardStorage, } from '../types'; +import { DeviceType } from '../../../constants/vm'; import { cleanup, updateAndValidateState } from './utils'; import { getTabInitialState } from './initial-state'; // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -84,13 +86,31 @@ export const vmWizardActions: VMWizardActions = { withUpdateAndValidateState(id, (dispatch) => dispatch(vmWizardInternalActions[InternalActionType.RemoveNIC](id, networkID)), ), - [ActionType.SetNetworks]: (id, networks: VMWizardNetwork[]) => + + [ActionType.UpdateStorage]: (id, storage: VMWizardStorage) => withUpdateAndValidateState(id, (dispatch) => - dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, networks)), + dispatch(vmWizardInternalActions[InternalActionType.UpdateStorage](id, storage)), + ), + [ActionType.RemoveStorage]: (id, storageID: string) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.RemoveStorage](id, storageID)), + ), + [ActionType.SetDeviceBootOrder]: ( + id, + deviceID: string, + deviceType: DeviceType, + bootOrder: number, + ) => + withUpdateAndValidateState(id, (dispatch) => + dispatch( + vmWizardInternalActions[InternalActionType.SetDeviceBootOrder]( + id, + deviceID, + deviceType, + bootOrder, + ), + ), ), - [ActionType.SetStorages]: (id, value: any, isValid: boolean, isLocked: boolean) => (dispatch) => { - dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, value, isValid, isLocked)); - }, [ActionType.SetResults]: ( id, value: any, diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts index f085e722531..ce5275dab61 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/storage-tab-initial-state.ts @@ -1,29 +1,65 @@ -// left intentionally empty -import { ProvisionSource } from '../../../../types/vm'; +import { VMWizardStorage, VMWizardStorageType } from '../../types'; +import { DiskWrapper } from '../../../../k8s/wrapper/vm/disk-wrapper'; +import { + DataVolumeSourceType, + DiskBus, + DiskType, + VolumeType, +} from '../../../../constants/vm/storage'; +import { VolumeWrapper } from '../../../../k8s/wrapper/vm/volume-wrapper'; +import { prefixedID } from '../../../../utils'; +import { DataVolumeWrapper } from '../../../../k8s/wrapper/vm/data-volume-wrapper'; +import { BinaryUnit } from '../../../form/size-unit-form-row'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; +import { VM_TEMPLATE_NAME_PARAMETER } from '../../../../constants/vm-templates'; -const rootDisk = { - rootStorage: {}, - name: 'rootdisk', - isBootable: true, -}; -export const rootContainerDisk = { - ...rootDisk, - // storageType: StorageType.CONTAINER, TODO!! +const ROOT_DISK_NAME = 'rootdisk'; + +const containerStorage: VMWizardStorage = { + type: VMWizardStorageType.PROVISION_SOURCE_DISK, + disk: DiskWrapper.initializeFromSimpleData({ + name: ROOT_DISK_NAME, + type: DiskType.DISK, + bus: DiskBus.VIRTIO, + bootOrder: 1, + }).asResource(), + volume: VolumeWrapper.initializeFromSimpleData({ + name: ROOT_DISK_NAME, + type: VolumeType.CONTAINER_DISK, + typeData: { image: '' }, + }).asResource(), }; -export const rootDataVolumeDisk = { - ...rootDisk, - // storageType: StorageType.DATAVOLUME, TODO!! - size: 10, + +const urlStorage = { + type: VMWizardStorageType.PROVISION_SOURCE_DISK, + disk: DiskWrapper.initializeFromSimpleData({ + name: ROOT_DISK_NAME, + type: DiskType.DISK, + bus: DiskBus.VIRTIO, + bootOrder: 1, + }).asResource(), + volume: VolumeWrapper.initializeFromSimpleData({ + name: ROOT_DISK_NAME, + type: VolumeType.DATA_VOLUME, + typeData: { name: prefixedID(VM_TEMPLATE_NAME_PARAMETER, ROOT_DISK_NAME) }, + }).asResource(), + dataVolume: DataVolumeWrapper.initializeFromSimpleData({ + name: prefixedID(VM_TEMPLATE_NAME_PARAMETER, ROOT_DISK_NAME), + type: DataVolumeSourceType.HTTP, + typeData: { url: '' }, + size: 10, + unit: BinaryUnit.Gi, + }).asResource(), }; -export const getInitialDisk = (provisionSource: ProvisionSource) => { - switch (provisionSource) { - case ProvisionSource.URL: - return rootDataVolumeDisk; - case ProvisionSource.CONTAINER: - return rootContainerDisk; - default: - return null; + +export const getProvisionSourceStorage = (provisionSource: ProvisionSource): VMWizardStorage => { + if (provisionSource === ProvisionSource.URL) { + return urlStorage; + } + if (provisionSource === ProvisionSource.CONTAINER) { + return containerStorage; } + return null; }; export const getStorageInitialState = () => ({ diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts index 6cbe859336b..b1a01b91687 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts @@ -1,7 +1,7 @@ import { OrderedSet } from 'immutable'; import { CommonData, VMSettingsField, VMWizardProps } from '../../types'; import { asHidden, asRequired } from '../../utils/utils'; -import { ProvisionSource } from '../../../../types/vm'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; export const getInitialVmSettings = (common: CommonData) => { const { @@ -12,8 +12,8 @@ export const getInitialVmSettings = (common: CommonData) => { ProvisionSource.PXE, ProvisionSource.URL, ProvisionSource.CONTAINER, - // ProvisionSource.CLONED_DISK, // TODO: uncomment when storage tab is implemented - ]; + ProvisionSource.DISK, + ].map((source) => source.getValue()); const fields = { [VMSettingsField.NAME]: { @@ -28,8 +28,12 @@ export const getInitialVmSettings = (common: CommonData) => { isRequired: asRequired(true), sources: OrderedSet(provisionSources), }, - [VMSettingsField.CONTAINER_IMAGE]: {}, - [VMSettingsField.IMAGE_URL]: {}, + [VMSettingsField.CONTAINER_IMAGE]: { + skipValidation: true, // validated in storage tab + }, + [VMSettingsField.IMAGE_URL]: { + skipValidation: true, // validated in storage tab + }, [VMSettingsField.OPERATING_SYSTEM]: { isRequired: asRequired(true), }, diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts index 47fdecde4ac..634f78d4990 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts @@ -1,4 +1,5 @@ -import { VMSettingsField, VMWizardNetwork, VMWizardTab } from '../types'; +import { VMSettingsField, VMWizardNetwork, VMWizardStorage, VMWizardTab } from '../types'; +import { DeviceType } from '../../../constants/vm'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { ActionBatch, InternalActionType, WizardInternalActionDispatcher } from './types'; @@ -107,6 +108,35 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.RemoveNIC, }), + + [InternalActionType.UpdateStorage]: (id, storage: VMWizardStorage) => ({ + payload: { + id, + storage, + }, + type: InternalActionType.UpdateStorage, + }), + [InternalActionType.RemoveStorage]: (id, storageID: string) => ({ + payload: { + id, + storageID, + }, + type: InternalActionType.RemoveStorage, + }), + [InternalActionType.SetDeviceBootOrder]: ( + id, + deviceID: string, + deviceType: DeviceType, + bootOrder: number, + ) => ({ + payload: { + id, + deviceID, + deviceType, + bootOrder, + }, + type: InternalActionType.SetDeviceBootOrder, + }), [InternalActionType.SetNetworks]: (id, networks: VMWizardNetwork[]) => ({ payload: { id, @@ -114,12 +144,10 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.SetNetworks, }), - [InternalActionType.SetStorages]: (id, value, isValid: boolean, isLocked: boolean) => ({ + [InternalActionType.SetStorages]: (id, value: VMWizardStorage[]) => ({ payload: { id, value, - isValid, - isLocked, }, type: InternalActionType.SetStorages, }), diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts index 123045429c0..0c10b747653 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts @@ -1,9 +1,74 @@ import * as _ from 'lodash'; -import { Map as ImmutableMap, fromJS } from 'immutable'; +import { fromJS, Map as ImmutableMap } from 'immutable'; import { VMWizardTab } from '../types'; import { iGet } from '../../../utils/immutable'; +import { DeviceType } from '../../../constants/vm'; import { InternalActionType, WizardInternalAction } from './types'; +const sequentializeBootOrderIndexes = (state, dialogId: string) => { + const bootOrderIndexes = [ + ...state + .getIn([dialogId, 'tabs', VMWizardTab.NETWORKING, 'value']) + .toArray() + .map((network) => network.getIn(['networkInterface', 'bootOrder'])), + ...state + .getIn([dialogId, 'tabs', VMWizardTab.STORAGE, 'value']) + .toArray() + .map((storage) => storage.getIn(['disk', 'bootOrder'])), + ] + .filter((bootOrder) => bootOrder != null) + .sort((a, b) => a - b); + + return [DeviceType.NIC, DeviceType.DISK].reduce((newState, deviceType) => { + const tab = deviceType === DeviceType.DISK ? VMWizardTab.STORAGE : VMWizardTab.NETWORKING; + const deviceName = deviceType === DeviceType.DISK ? 'disk' : 'networkInterface'; + + return newState.updateIn([dialogId, 'tabs', tab, 'value'], (deviceWrappers) => { + return deviceWrappers.map((deviceWrapper) => { + const oldBootOrder = deviceWrapper.getIn([deviceName, 'bootOrder']); + + if (oldBootOrder != null) { + const newBootOrder = bootOrderIndexes.indexOf(oldBootOrder) + 1; + if (newBootOrder !== oldBootOrder) { + return deviceWrapper.setIn([deviceName, 'bootOrder'], newBootOrder); + } + } + return deviceWrapper; + }); + }); + }, state); +}; + +const setDeviceBootOrder = ( + state, + dialogId: string, + deviceID: string, + updatedDeviceType: DeviceType, + updatedDeviceBootOrder: number, +) => { + const resultState = [DeviceType.NIC, DeviceType.DISK].reduce((newState, devType) => { + const tab = devType === DeviceType.DISK ? VMWizardTab.STORAGE : VMWizardTab.NETWORKING; + const deviceName = devType === DeviceType.DISK ? 'disk' : 'networkInterface'; + + return newState.updateIn([dialogId, 'tabs', tab, 'value'], (deviceWrappers) => { + return deviceWrappers.map((deviceWrapper) => { + const wrapperID = deviceWrapper.get('id'); + const oldBootOrder = deviceWrapper.getIn([deviceName, 'bootOrder']); + const isUpdatedDevice = updatedDeviceType === devType && wrapperID === deviceID; + if (isUpdatedDevice || (oldBootOrder != null && updatedDeviceBootOrder <= oldBootOrder)) { + return deviceWrapper.setIn( + [deviceName, 'bootOrder'], + isUpdatedDevice ? updatedDeviceBootOrder : oldBootOrder + 1, + ); + } + return deviceWrapper; + }); + }); + }, state); + + return sequentializeBootOrderIndexes(resultState, dialogId); +}; + // Merge deep in without updating the keys with undefined values const mergeDeepInSpecial = (state, path: string[], value) => state.updateIn(path, (oldValue) => { @@ -39,7 +104,7 @@ const setObjectValues = (state, path, obj) => { const updateIDItemInList = (state, path, item?) => { const itemID = iGet(item, 'id'); return state.updateIn(path, (items) => { - const networkIndex = item ? items.findIndex((t) => iGet(t, 'id') === itemID) : -1; + const networkIndex = itemID != null ? items.findIndex((t) => iGet(t, 'id') === itemID) : -1; if (networkIndex === -1) { const maxID = items.map((t) => iGet(t, 'id')).max() || 0; return items.push(item.set('id', _.toString(_.toSafeInteger(maxID) + 1))); @@ -79,6 +144,26 @@ export default (state, action: WizardInternalAction) => { [dialogId, 'tabs', VMWizardTab.NETWORKING, 'value'], payload.networkID, ); + case InternalActionType.UpdateStorage: + return updateIDItemInList( + state, + [dialogId, 'tabs', VMWizardTab.STORAGE, 'value'], + fromJS(payload.storage), + ); + case InternalActionType.RemoveStorage: + return removeIDItemFromList( + state, + [dialogId, 'tabs', VMWizardTab.STORAGE, 'value'], + payload.storageID, + ); + case InternalActionType.SetDeviceBootOrder: + return setDeviceBootOrder( + state, + dialogId, + payload.deviceID, + payload.deviceType, + payload.bootOrder, + ); case InternalActionType.SetNetworks: return setTabKeys(state, VMWizardTab.NETWORKING, action); case InternalActionType.SetStorages: diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts index 61d0e92472f..78942d7a330 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts @@ -1,26 +1,35 @@ -import { createBasicLookup } from '@console/shared/src'; +import { createBasicLookup, getName } from '@console/shared/src'; import { InternalActionType, UpdateOptions } from '../../types'; -import { iGetVmSettingValue } from '../../../selectors/immutable/vm-settings'; +import { iGetProvisionSource, iGetVmSettingValue } from '../../../selectors/immutable/vm-settings'; import { VMSettingsField, VMWizardNetwork, VMWizardNetworkType, VMWizardProps, + VMWizardStorage, + VMWizardStorageType, } from '../../../types'; import { iGetLoadedCommonData, iGetName } from '../../../selectors/immutable/selectors'; import { concatImmutableLists, immutableListToShallowJS } from '../../../../../utils/immutable'; -import { iGetNetworks as getDialogNetworks } from '../../../selectors/immutable/networks'; -import { iGetStorages } from '../../../selectors/immutable/storage'; +import { iGetNetworks } from '../../../selectors/immutable/networks'; import { podNetwork } from '../../initial-state/networks-tab-initial-state'; import { vmWizardInternalActions } from '../../internal-actions'; -import { CUSTOM_FLAVOR, NetworkInterfaceModel } from '../../../../../constants/vm'; +import { + CUSTOM_FLAVOR, + DiskBus, + DiskType, + NetworkInterfaceModel, +} from '../../../../../constants/vm'; import { DEFAULT_CPU, getCloudInitUserData, getCPU, + getDataVolumeTemplates, + getDisks, getInterfaces, getMemory, getNetworks, + getVolumes, hasAutoAttachPodInterface, parseCPU, } from '../../../../../selectors/vm'; @@ -30,20 +39,16 @@ import { getTemplateOperatingSystems, getTemplateWorkloadProfiles, } from '../../../../../selectors/vm-template/advanced'; -import { ProvisionSource, V1Network } from '../../../../../types/vm'; -import { - getTemplateProvisionSource, - getTemplateStorages, -} from '../../../../../selectors/vm-template/combined'; +import { V1Network } from '../../../../../types/vm'; import { getFlavors } from '../../../../../selectors/vm-template/combined-dependent'; import { getSimpleName } from '../../../../../selectors/utils'; import { getNextIDResolver } from '../../../../../utils/utils'; - -// used by user template; currently we do not support PROVISION_SOURCE_IMPORT -const provisionSourceDataFieldResolver = { - [ProvisionSource.CONTAINER]: VMSettingsField.CONTAINER_IMAGE, - [ProvisionSource.URL]: VMSettingsField.IMAGE_URL, -}; +import { ProvisionSource } from '../../../../../constants/vm/provision-source'; +import { DiskWrapper } from '../../../../../k8s/wrapper/vm/disk-wrapper'; +import { V1Volume } from '../../../../../types/vm/disk/V1Volume'; +import { VolumeWrapper } from '../../../../../k8s/wrapper/vm/volume-wrapper'; +import { getProvisionSourceStorage } from '../../initial-state/storage-tab-initial-state'; +import { iGetStorages } from '../../../selectors/immutable/storage'; export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptions) => { const state = getState(); @@ -60,23 +65,26 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio const vmSettingsUpdate = {}; // filter out oldTemplates - let networksUpdate = immutableListToShallowJS( - getDialogNetworks(state, id), - ).filter((network) => network.type !== VMWizardNetworkType.TEMPLATE); + let networksUpdate = immutableListToShallowJS(iGetNetworks(state, id)).filter( + (network) => network.type !== VMWizardNetworkType.TEMPLATE, + ); const getNextNetworkID = getNextIDResolver(networksUpdate); - const storageRowsUpdate = immutableListToShallowJS(iGetStorages(state, id)).filter( - (storage) => !(storage.templateStorage || storage.rootStorage), + const storagesUpdate = immutableListToShallowJS(iGetStorages(state, id)).filter( + (storage) => + ![ + VMWizardStorageType.PROVISION_SOURCE_DISK, + VMWizardStorageType.TEMPLATE, + VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK, + ].includes(storage.type), ); + const getNextStorageID = getNextIDResolver(storagesUpdate); if (!networksUpdate.find((row) => !!row.network.pod)) { networksUpdate.unshift({ ...podNetwork, id: getNextNetworkID() }); } if (iUserTemplate) { - const dataVolumes = immutableListToShallowJS( - iGetLoadedCommonData(state, id, VMWizardProps.userTemplates), - ); const userTemplate = iUserTemplate.toJS(); const vm = selectVM(userTemplate); @@ -109,20 +117,14 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio } // update provision source - const provisionSource = getTemplateProvisionSource(userTemplate, dataVolumes); - if (provisionSource.type === ProvisionSource.UNKNOWN) { - vmSettingsUpdate[VMSettingsField.PROVISION_SOURCE_TYPE] = { value: null }; - } else { - vmSettingsUpdate[VMSettingsField.PROVISION_SOURCE_TYPE] = { value: provisionSource.type }; - const dataFieldName = provisionSourceDataFieldResolver[provisionSource.type]; - if (dataFieldName) { - vmSettingsUpdate[dataFieldName] = { value: provisionSource.source }; - } - } + const provisionSourceDetails = ProvisionSource.getProvisionSourceDetails(userTemplate); + vmSettingsUpdate[VMSettingsField.PROVISION_SOURCE_TYPE] = { + value: provisionSourceDetails.type ? provisionSourceDetails.type.getValue() : null, + }; const networkLookup = createBasicLookup(getNetworks(vm), getSimpleName); // prefill networks - const templateNetworks = getInterfaces(vm).map((intface) => ({ + const templateNetworks: VMWizardNetwork[] = getInterfaces(vm).map((intface) => ({ id: getNextNetworkID(), type: VMWizardNetworkType.TEMPLATE, network: networkLookup[getSimpleName(intface)], @@ -142,12 +144,30 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio networksUpdate.push(...templateNetworks); - // prefill storage - const templateStorages = getTemplateStorages(userTemplate, dataVolumes).map((storage) => ({ - templateStorage: storage, - rootStorage: storage.disk.bootOrder === 1 ? {} : undefined, - })); - storageRowsUpdate.push(...templateStorages); + const volumeLookup = createBasicLookup(getVolumes(vm), getSimpleName); + const datavolumeTemplatesLookup = createBasicLookup(getDataVolumeTemplates(vm), getName); + // // prefill storage + const templateStorages: VMWizardStorage[] = getDisks(vm).map((disk) => { + const diskWrapper = DiskWrapper.initialize(disk); + const volume = volumeLookup[diskWrapper.getName()]; + const volumeWrapper = VolumeWrapper.initialize(volume); + return { + id: getNextStorageID(), + type: diskWrapper.isFirstBootableDevice() + ? VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK + : VMWizardStorageType.TEMPLATE, + volume, + dataVolume: datavolumeTemplatesLookup[volumeWrapper.getDataVolumeName()], + disk: + diskWrapper.getType() === DiskType.DISK && !diskWrapper.getDiskBus() + ? DiskWrapper.mergeWrappers( + diskWrapper, + DiskWrapper.initializeFromSimpleData({ type: DiskType.DISK, bus: DiskBus.VIRTIO }), + ).asResource() + : disk, + }; + }); + storagesUpdate.unshift(...templateStorages); } else { const iCommonTemplates = iGetLoadedCommonData(state, id, VMWizardProps.commonTemplates); @@ -162,9 +182,14 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio if (flavors.length === 1) { vmSettingsUpdate[VMSettingsField.FLAVOR] = { value: flavors[0] }; } + + const newSourceStorage = getProvisionSourceStorage(iGetProvisionSource(state, id)); + if (newSourceStorage) { + storagesUpdate.unshift({ ...newSourceStorage, id: getNextStorageID() }); + } } dispatch(vmWizardInternalActions[InternalActionType.UpdateVmSettings](id, vmSettingsUpdate)); dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, networksUpdate)); - dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, storageRowsUpdate)); + dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, storagesUpdate)); }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/storage-tab-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/storage-tab-state-update.ts new file mode 100644 index 00000000000..fb9ba55b1b7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/storage-tab-state-update.ts @@ -0,0 +1,61 @@ +import { + hasVmSettingsChanged, + iGetProvisionSource, +} from '../../../selectors/immutable/vm-settings'; +import { VMSettingsField } from '../../../types'; +import { InternalActionType, UpdateOptions } from '../../types'; +import { iGetProvisionSourceStorage } from '../../../selectors/immutable/storage'; +import { getProvisionSourceStorage } from '../../initial-state/storage-tab-initial-state'; +import { VolumeWrapper } from '../../../../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../../../../k8s/wrapper/vm/data-volume-wrapper'; +import { StorageUISource } from '../../../../modals/disk-modal/storage-ui-source'; +import { getNextIDResolver } from '../../../../../utils/utils'; +import { getStorages } from '../../../selectors/selectors'; +import { vmWizardInternalActions } from '../../internal-actions'; + +export const prefillInitialDiskUpdater = ({ id, prevState, dispatch, getState }: UpdateOptions) => { + const state = getState(); + if (!hasVmSettingsChanged(prevState, state, id, VMSettingsField.PROVISION_SOURCE_TYPE)) { + return; + } + + const iOldSourceStorage = iGetProvisionSourceStorage(state, id); + const oldSourceStorage = iOldSourceStorage && iOldSourceStorage.toJSON(); + + const newSourceStorage = getProvisionSourceStorage(iGetProvisionSource(state, id)); + const oldType = + oldSourceStorage && + StorageUISource.fromTypes( + VolumeWrapper.initialize(oldSourceStorage.volume).getType(), + DataVolumeWrapper.initialize(oldSourceStorage.dataVolume).getType(), + ); + + const newType = + newSourceStorage && + StorageUISource.fromTypes( + VolumeWrapper.initialize(newSourceStorage.volume).getType(), + DataVolumeWrapper.initialize(newSourceStorage.dataVolume).getType(), + ); + + if (newType !== oldType) { + if (!newSourceStorage) { + if (oldSourceStorage) { + dispatch( + vmWizardInternalActions[InternalActionType.RemoveStorage](id, oldSourceStorage.id), + ); + } + } else { + dispatch( + vmWizardInternalActions[InternalActionType.UpdateStorage](id, { + id: oldSourceStorage ? oldSourceStorage.id : getNextIDResolver(getStorages(state, id))(), + ...newSourceStorage, + }), + ); + } + } +}; + +export const updateStorageTabState = (options: UpdateOptions) => + [prefillInitialDiskUpdater].forEach((updater) => { + updater && updater(options); + }); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts index 36677a0320a..a180069b2b6 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/vm-settings-tab-state-update.ts @@ -1,5 +1,6 @@ import { hasVmSettingsChanged, + iGetProvisionSource, iGetVmSettingAttribute, iGetVmSettingValue, } from '../../../selectors/immutable/vm-settings'; @@ -12,16 +13,10 @@ import { iGetLoadedCommonData, iGetName, } from '../../../selectors/immutable/selectors'; -import { - iGetIsLoaded, - iGetLoadedData, - immutableListToShallowJS, -} from '../../../../../utils/immutable'; -import { ProvisionSource } from '../../../../../types/vm'; +import { iGetIsLoaded, iGetLoadedData } from '../../../../../utils/immutable'; import { ignoreCaseSort } from '../../../../../utils/sort'; import { CUSTOM_FLAVOR } from '../../../../../constants/vm'; -import { iGetStorages } from '../../../selectors/immutable/storage'; -import { getInitialDisk } from '../../initial-state/storage-tab-initial-state'; +import { ProvisionSource } from '../../../../../constants/vm/provision-source'; import { prefillVmTemplateUpdater } from './prefill-vm-template-state-update'; export const selectUserTemplateOnLoadedUpdater = ({ @@ -36,8 +31,7 @@ export const selectUserTemplateOnLoadedUpdater = ({ iGetVmSettingAttribute(state, id, VMSettingsField.USER_TEMPLATE, 'initialized') || !( changedCommonData.has(VMWizardProps.userTemplates) || - changedCommonData.has(VMWizardProps.commonTemplates) || - changedCommonData.has(VMWizardProps.dataVolumes) + changedCommonData.has(VMWizardProps.commonTemplates) ) ) { return; @@ -45,13 +39,8 @@ export const selectUserTemplateOnLoadedUpdater = ({ const iUserTemplatesWrapper = iGetCommonData(state, id, VMWizardProps.userTemplates); const iCommonTemplatesWrapper = iGetCommonData(state, id, VMWizardProps.commonTemplates); // flavor prefill - const iDataVolumeWrapper = iGetCommonData(state, id, VMWizardProps.dataVolumes); // template prefill - if ( - !iGetIsLoaded(iUserTemplatesWrapper) || - !iGetIsLoaded(iCommonTemplatesWrapper) || - !iGetIsLoaded(iDataVolumeWrapper) - ) { + if (!iGetIsLoaded(iUserTemplatesWrapper) || !iGetIsLoaded(iCommonTemplatesWrapper)) { return; } @@ -120,19 +109,17 @@ export const provisioningSourceUpdater = ({ id, prevState, dispatch, getState }: ) { return; } - const source = iGetVmSettingValue(state, id, VMSettingsField.PROVISION_SOURCE_TYPE); + const source = iGetProvisionSource(state, id); const isContainer = source === ProvisionSource.CONTAINER; const isUrl = source === ProvisionSource.URL; dispatch( vmWizardInternalActions[InternalActionType.UpdateVmSettings](id, { [VMSettingsField.CONTAINER_IMAGE]: { - value: isContainer ? undefined : null, isRequired: asRequired(isContainer, VMSettingsField.PROVISION_SOURCE_TYPE), isHidden: asHidden(!isContainer, VMSettingsField.PROVISION_SOURCE_TYPE), }, [VMSettingsField.IMAGE_URL]: { - value: isUrl ? undefined : null, isRequired: asRequired(isUrl, VMSettingsField.PROVISION_SOURCE_TYPE), isHidden: asHidden(!isUrl, VMSettingsField.PROVISION_SOURCE_TYPE), }, @@ -140,37 +127,6 @@ export const provisioningSourceUpdater = ({ id, prevState, dispatch, getState }: ); }; -// TODO: move this logic to StorageTab? -export const prefillInitialDiskUpdater = ({ id, prevState, dispatch, getState }: UpdateOptions) => { - const state = getState(); - if ( - !hasVmSettingsChanged( - prevState, - state, - id, - VMSettingsField.PROVISION_SOURCE_TYPE, - VMSettingsField.USER_TEMPLATE, - ) - ) { - return; - } - - const storageRowsUpdate = immutableListToShallowJS(iGetStorages(state, id)).filter( - (storage) => storage.templateStorage || !storage.rootStorage, - ); - // template pre-fills its own storages - if (!iGetVmSettingValue(state, id, VMSettingsField.USER_TEMPLATE)) { - const storage = getInitialDisk( - iGetVmSettingValue(state, id, VMSettingsField.PROVISION_SOURCE_TYPE), - ); - if (storage) { - storageRowsUpdate.push(storage); - } - } - - dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, storageRowsUpdate)); -}; - export const flavorUpdater = ({ id, prevState, dispatch, getState }: UpdateOptions) => { const state = getState(); if (!hasVmSettingsChanged(prevState, state, id, VMSettingsField.FLAVOR)) { @@ -200,7 +156,6 @@ export const updateVmSettingsState = (options: UpdateOptions) => selectUserTemplateOnLoadedUpdater, selectedUserTemplateUpdater, provisioningSourceUpdater, - prefillInitialDiskUpdater, flavorUpdater, ].forEach((updater) => { updater && updater(options); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts index 63b5e7e4eba..6a4a210c682 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts @@ -4,9 +4,11 @@ import { VMSettingsField, VMSettingsFieldType, VMWizardNetwork, + VMWizardStorage, VMWizardTab, } from '../types'; import { ValidationObject } from '../../../utils/validations/types'; +import { DeviceType } from '../../../constants/vm'; export enum ActionType { Create = 'KubevirtVMWizardExternalCreate', @@ -16,8 +18,9 @@ export enum ActionType { SetTabLocked = 'KubevirtVMWizardExternalSetTabLocked', RemoveNIC = 'KubevirtVMWizardExternalRemoveNIC', UpdateNIC = 'KubevirtVMWizardExternalUpdateNIC', - SetNetworks = 'KubevirtVMWizardExternalSetNetworks', - SetStorages = 'KubevirtVMWizardExternalSetStorages', + SetDeviceBootOrder = 'KubevirtVMWizardExternalSetDeviceBootOrder', + RemoveStorage = 'KubevirtVMWizardExternalRemoveStorage', + UpdateStorage = 'KubevirtVMWizardExternalUpdateStorage', SetResults = 'KubevirtVMWizardExternalSetResults', } @@ -25,17 +28,20 @@ export enum ActionType { export enum InternalActionType { Create = 'KubevirtVMWizardCreate', Dispose = 'KubevirtVMWizardDispose', - Update = 'KubevirtVMWizardUpdateInternal', + Update = 'KubevirtVMWizardUpdate', UpdateCommonData = 'KubevirtVMWizardUpdateCommonData', - SetTabValidity = 'KubevirtVMWizardSetTabValidityInternal', + SetTabValidity = 'KubevirtVMWizardSetTabValidity', SetTabLocked = 'KubevirtVMWizardSetTabLocked', - SetVmSettingsFieldValue = 'KubevirtVMWizardSetVmSettingsFieldValueInternal', - SetInVmSettings = 'KubevirtVMWizardSetInVmSettingsInternal', - SetInVmSettingsBatch = 'KubevirtVMWizardSetInVmSettingsBatchInternal', - UpdateVmSettingsField = 'KubevirtVMWizardUpdateVmSettingsFieldInternal', - UpdateVmSettings = 'KubevirtVMWizardUpdateVmSettingsInternal', + SetVmSettingsFieldValue = 'KubevirtVMWizardSetVmSettingsFieldValue', + SetInVmSettings = 'KubevirtVMWizardSetInVmSettings', + SetInVmSettingsBatch = 'KubevirtVMWizardSetInVmSettingsBatch', + UpdateVmSettingsField = 'KubevirtVMWizardUpdateVmSettingsField', + UpdateVmSettings = 'KubevirtVMWizardUpdateVmSettings', RemoveNIC = 'KubevirtVMWizardRemoveNIC', UpdateNIC = 'KubevirtVMWizardUpdateNIC', + SetDeviceBootOrder = 'KubevirtVMWizardSetDeviceBootOrder', + RemoveStorage = 'KubevirtVMWizardRemoveStorage', + UpdateStorage = 'KubevirtVMWizardUpdateStorage', SetNetworks = 'KubevirtVMWizardSetNetworks', SetStorages = 'KubevirtVMWizardSetStorages', SetResults = 'KubevirtVMWizardSetResults', @@ -55,8 +61,12 @@ export type WizardInternalAction = { tab?: VMWizardTab; batch?: ActionBatch; network?: VMWizardNetwork; - networks?: VMWizardNetwork[]; networkID?: string; + storage?: VMWizardStorage; + storageID?: string; + deviceID?: string; + deviceType?: DeviceType; + bootOrder?: number; }; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts index ea4da25a8f3..93975a18339 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/utils.ts @@ -1,26 +1,31 @@ import { VMWizardTab } from '../types'; import { UpdateOptions } from './types'; import { updateVmSettingsState } from './stateUpdate/vmSettings/vm-settings-tab-state-update'; +import { updateStorageTabState } from './stateUpdate/vmSettings/storage-tab-state-update'; import { setVmSettingsTabValidity, validateVmSettings, } from './validations/vm-settings-tab-validation'; import { setNetworksTabValidity, validateNetworks } from './validations/networks-tab-validation'; +import { setStoragesTabValidity, validateStorages } from './validations/storage-tab-validation'; -const UPDATE_TABS = [VMWizardTab.VM_SETTINGS, VMWizardTab.NETWORKING]; +const UPDATE_TABS = [VMWizardTab.VM_SETTINGS, VMWizardTab.NETWORKING, VMWizardTab.STORAGE]; const updaterResolver = { [VMWizardTab.VM_SETTINGS]: updateVmSettingsState, + [VMWizardTab.STORAGE]: updateStorageTabState, }; const validateTabResolver = { [VMWizardTab.VM_SETTINGS]: validateVmSettings, [VMWizardTab.NETWORKING]: validateNetworks, + [VMWizardTab.STORAGE]: validateStorages, }; const isTabValidResolver = { [VMWizardTab.VM_SETTINGS]: setVmSettingsTabValidity, [VMWizardTab.NETWORKING]: setNetworksTabValidity, + [VMWizardTab.STORAGE]: setStoragesTabValidity, }; export const updateAndValidateState = (options: UpdateOptions) => { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts index 00e763962cf..66f4e5f9987 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/networks-tab-validation.ts @@ -1,13 +1,13 @@ -import { VMSettingsField, VMWizardTab } from '../../types'; +import { VMWizardTab } from '../../types'; import { InternalActionType, UpdateOptions } from '../types'; import { vmWizardInternalActions } from '../internal-actions'; import { validateNIC } from '../../../../utils/validations/vm'; import { iGetIn } from '../../../../utils/immutable'; import { iGetNetworks } from '../../selectors/immutable/networks'; import { checkTabValidityChanged } from '../../selectors/immutable/selectors'; -import { iGetVmSettingValue } from '../../selectors/immutable/vm-settings'; -import { ProvisionSource } from '../../../../types/vm'; +import { iGetProvisionSource } from '../../selectors/immutable/vm-settings'; import { getNetworksWithWrappers } from '../../selectors/selectors'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; export const validateNetworks = (options: UpdateOptions) => { const { id, prevState, dispatch, getState } = options; @@ -63,10 +63,7 @@ export const setNetworksTabValidity = (options: UpdateOptions) => { iGetIn(iNetwork, ['validation', 'hasAllRequiredFilled']), ); - if ( - hasAllRequiredFilled && - iGetVmSettingValue(state, id, VMSettingsField.PROVISION_SOURCE_TYPE) === ProvisionSource.PXE - ) { + if (hasAllRequiredFilled && iGetProvisionSource(state, id) === ProvisionSource.PXE) { hasAllRequiredFilled = !!iNetworks.find( (networkBundle) => !iGetIn(networkBundle, ['network', 'pod']) && diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/storage-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/storage-tab-validation.ts new file mode 100644 index 00000000000..a10f2bcb954 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/storage-tab-validation.ts @@ -0,0 +1,91 @@ +import { VMWizardTab } from '../../types'; +import { InternalActionType, UpdateOptions } from '../types'; +import { vmWizardInternalActions } from '../internal-actions'; +import { validateDisk } from '../../../../utils/validations/vm'; +import { iGetIn } from '../../../../utils/immutable'; +import { checkTabValidityChanged } from '../../selectors/immutable/selectors'; +import { getStoragesWithWrappers } from '../../selectors/selectors'; +import { iGetStorages } from '../../selectors/immutable/storage'; +import { iGetProvisionSource } from '../../selectors/immutable/vm-settings'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; + +export const validateStorages = (options: UpdateOptions) => { + const { id, prevState, dispatch, getState } = options; + const state = getState(); + + const prevIStorages = iGetStorages(prevState, id); + const iStorages = iGetStorages(state, id); + + if ( + prevIStorages && + iStorages && + prevIStorages.size === iStorages.size && + !prevIStorages.find( + (prevINetwork, prevINetworkIndex) => prevINetwork !== iStorages.get(prevINetworkIndex), + ) + ) { + return; + } + + const storages = getStoragesWithWrappers(state, id); + + const validatedStorages = storages.map( + ({ diskWrapper, volumeWrapper, dataVolumeWrapper, ...storageBundle }) => { + const otherStorageBundles = storages.filter((n) => n.id !== storageBundle.id); // to discard networks with a same name + const usedDiskNames = new Set(otherStorageBundles.map(({ diskWrapper: dw }) => dw.getName())); + + const usedPVCNames: Set = new Set( + otherStorageBundles + .filter(({ dataVolume }) => dataVolume) + .map(({ dataVolumeWrapper: dvw }) => dvw.getName()), + ); + + return { + ...storageBundle, + validation: validateDisk(diskWrapper, volumeWrapper, dataVolumeWrapper, { + usedDiskNames, + usedPVCNames, + }), + }; + }, + ); + + dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, validatedStorages)); +}; + +export const setStoragesTabValidity = (options: UpdateOptions) => { + const { id, dispatch, getState } = options; + const state = getState(); + const iStorages = iGetStorages(state, id); + + let hasAllRequiredFilled = iStorages.every((iStorage) => + iGetIn(iStorage, ['validation', 'hasAllRequiredFilled']), + ); + + if (hasAllRequiredFilled && iGetProvisionSource(state, id) === ProvisionSource.DISK) { + hasAllRequiredFilled = !!iStorages.find( + (storageBundle) => + iGetIn(storageBundle, ['disk', 'bootOrder']) === 1 && + iGetIn(storageBundle, ['disk', 'disk']) && + (iGetIn(storageBundle, ['volume', 'dataVolume']) || + iGetIn(storageBundle, ['volume', 'persistentVolumeClaim'])), + ); + } + + let isValid = hasAllRequiredFilled; + + if (isValid) { + isValid = iStorages.every((iStorage) => iGetIn(iStorage, ['validation', 'isValid'])); + } + + if (checkTabValidityChanged(state, id, VMWizardTab.STORAGE, isValid, hasAllRequiredFilled)) { + dispatch( + vmWizardInternalActions[InternalActionType.SetTabValidity]( + id, + VMWizardTab.STORAGE, + isValid, + hasAllRequiredFilled, + ), + ); + } +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils.ts index 59c395f6d9f..757bf8c9295 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/utils.ts @@ -18,6 +18,10 @@ export const getValidationUpdate = ( const field = fields.get(validationFieldKey); + if (field.get('skipValidation')) { + return updateAcc; + } + const detectValues = _.isFunction(detectValueChanges) ? detectValueChanges(field, options) : detectValueChanges; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts index de8bd7d326d..152afaab272 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/validations/vm-settings-tab-validation.ts @@ -24,7 +24,7 @@ import { VIRTUAL_MACHINE_EXISTS, VIRTUAL_MACHINE_TEMPLATE_EXISTS, } from '../../../../utils/validations/strings'; -import { concatImmutableLists, immutableListToShallowJS } from '../../../../utils/immutable'; +import { concatImmutableLists } from '../../../../utils/immutable'; import { getFieldTitle } from '../../utils/vm-settings-tab-utils'; import { checkTabValidityChanged, @@ -33,11 +33,7 @@ import { iGetName, immutableListToShallowMetadataJS, } from '../../selectors/immutable/selectors'; -import { - validatePositiveInteger, - validateTrim, - validateURL, -} from '../../../../utils/validations/common'; +import { validatePositiveInteger } from '../../../../utils/validations/common'; import { getValidationUpdate } from './utils'; const validateVm: VmSettingsValidator = (field, options) => { @@ -78,11 +74,7 @@ export const validateUserTemplate: VmSettingsValidator = (field, options) => { (template) => iGetName(template) === userTemplateName, ); - const dataVolumes = immutableListToShallowJS( - iGetLoadedCommonData(state, id, VMWizardProps.userTemplates), - ); - - return validateUserTemplateProvisionSource(userTemplate && userTemplate.toJSON(), dataVolumes); + return validateUserTemplateProvisionSource(userTemplate && userTemplate.toJSON()); }; const asVMSettingsFieldValidator = ( @@ -111,19 +103,11 @@ const validationConfig: VMSettingsValidationConfig = { }, validator: validateVm, }, - [VMSettingsField.CONTAINER_IMAGE]: { - detectValueChanges: [VMSettingsField.CONTAINER_IMAGE], - validator: asVMSettingsFieldValidator(validateTrim), - }, [VMSettingsField.USER_TEMPLATE]: { detectValueChanges: [VMSettingsField.USER_TEMPLATE], - detectCommonDataChanges: [VMWizardProps.userTemplates, VMWizardProps.dataVolumes], + detectCommonDataChanges: [VMWizardProps.userTemplates], validator: validateUserTemplate, }, - [VMSettingsField.IMAGE_URL]: { - detectValueChanges: [VMSettingsField.IMAGE_URL], - validator: asVMSettingsFieldValidator(validateURL), - }, [VMSettingsField.CPU]: { detectValueChanges: [VMSettingsField.CPU], validator: asVMSettingsFieldValidator(validatePositiveInteger), @@ -153,7 +137,7 @@ export const setVmSettingsTabValidity = (options: UpdateOptions) => { // check if all required fields are defined const hasAllRequiredFilled = vmSettings - .filter((field) => isFieldRequired(field)) + .filter((field) => isFieldRequired(field) && !field.get('skipValidation')) .every((field) => field.get('value')); let isValid = hasAllRequiredFilled; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx index bbd568be9d9..14b2b61c712 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx @@ -22,10 +22,6 @@ const stateToProps = (state, { wizardReduxID }) => ({ errors: [ asError(state, wizardReduxID, VMWizardProps.commonTemplates), asError(state, wizardReduxID, VMWizardProps.userTemplates), - asError(state, wizardReduxID, VMWizardProps.networkAttachmentDefinitions), - asError(state, wizardReduxID, VMWizardProps.persistentVolumeClaims), - asError(state, wizardReduxID, VMWizardProps.dataVolumes), - asError(state, wizardReduxID, VMWizardProps.storageClasses), asError(state, wizardReduxID, VMWizardProps.virtualMachines, AlertVariant.warning), // for validation only ], }); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts index 7767d701d58..777143507b9 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts @@ -1,6 +1,14 @@ -import { iGetIn } from '../../../../utils/immutable'; -import { VMWizardTab } from '../../types'; +import { iGet, iGetIn } from '../../../../utils/immutable'; +import { VMWizardStorageType, VMWizardTab } from '../../types'; import { iGetCreateVMWizardTabs } from './selectors'; export const iGetStorages = (state, id: string) => iGetIn(iGetCreateVMWizardTabs(state, id), [VMWizardTab.STORAGE, 'value']); + +export const iGetProvisionSourceStorage = (state, id: string) => + iGetStorages(state, id).find((storage) => + [ + VMWizardStorageType.PROVISION_SOURCE_DISK, + VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK, + ].includes(iGet(storage, 'type')), + ); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/vm-settings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/vm-settings.ts index 794aab78e6f..c81fd040852 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/vm-settings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/vm-settings.ts @@ -1,5 +1,6 @@ import { hasTruthyValue, iGet, iGetIn } from '../../../../utils/immutable'; import { VMSettingsField, VMWizardTab } from '../../types'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; import { iGetCreateVMWizardTabs } from './selectors'; export const VM_SETTINGS_METADATA_ID = 'VM_SETTINGS_METADATA_ID'; @@ -35,3 +36,6 @@ export const iGetVmSettingValue = ( key: VMSettingsField, defaultValue = undefined, ) => iGetVmSettingAttribute(state, id, key, 'value', defaultValue); + +export const iGetProvisionSource = (state, id: string) => + ProvisionSource.fromString(iGetVmSettingValue(state, id, VMSettingsField.PROVISION_SOURCE_TYPE)); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts index 121b91e2f8c..8d10315fc04 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/selectors.ts @@ -1,9 +1,18 @@ import { get } from 'lodash'; import { Map } from 'immutable'; import { iGetIn, immutableListToShallowJS } from '../../../utils/immutable'; -import { VMWizardNetwork, VMWizardNetworkWithWrappers, VMWizardTab } from '../types'; +import { + VMWizardNetwork, + VMWizardNetworkWithWrappers, + VMWizardStorage, + VMWizardStorageWithWrappers, + VMWizardTab, +} from '../types'; import { NetworkInterfaceWrapper } from '../../../k8s/wrapper/vm/network-interface-wrapper'; import { NetworkWrapper } from '../../../k8s/wrapper/vm/network-wrapper'; +import { DiskWrapper } from '../../../k8s/wrapper/vm/disk-wrapper'; +import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; export const getCreateVMWizards = (state): Map => get(state, ['kubevirt', 'createVmWizards']); @@ -13,6 +22,11 @@ export const getNetworks = (state, id: string): VMWizardNetwork[] => iGetIn(getCreateVMWizards(state), [id, 'tabs', VMWizardTab.NETWORKING, 'value']), ); +export const getStorages = (state, id: string): VMWizardStorage[] => + immutableListToShallowJS( + iGetIn(getCreateVMWizards(state), [id, 'tabs', VMWizardTab.STORAGE, 'value']), + ); + export const getNetworksWithWrappers = (state, id: string): VMWizardNetworkWithWrappers[] => getNetworks(state, id).map(({ network, networkInterface, ...rest }) => ({ networkInterfaceWrapper: NetworkInterfaceWrapper.initialize(networkInterface), @@ -21,3 +35,14 @@ export const getNetworksWithWrappers = (state, id: string): VMWizardNetworkWithW network, ...rest, })); + +export const getStoragesWithWrappers = (state, id: string): VMWizardStorageWithWrappers[] => + getStorages(state, id).map(({ disk, volume, dataVolume, ...rest }) => ({ + diskWrapper: DiskWrapper.initialize(disk), + volumeWrapper: VolumeWrapper.initialize(volume), + dataVolumeWrapper: dataVolume && DataVolumeWrapper.initialize(dataVolume), + disk, + volume, + dataVolume, + ...rest, + })); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/storage.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/storage.ts new file mode 100644 index 00000000000..939db2b9cb2 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/storage.ts @@ -0,0 +1,3 @@ +export const ADD_DISK = 'Add Disk'; +export const NO_BOOTABLE_ATTACHED_DISK_ERROR = 'A bootable attached disk could not be found'; +export const SELECT_BOOTABLE_DISK = '--- Select Bootable Disk ---'; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts index b3de2c7ee59..257e77256c4 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts @@ -1,5 +1,5 @@ import { VMSettingsField, VMSettingsRenderableFieldResolver } from '../types'; -import { ProvisionSource } from '../../../types/vm'; +import { ProvisionSource } from '../../../constants/vm/provision-source'; export const titleResolver: VMSettingsRenderableFieldResolver = { [VMSettingsField.NAME]: 'Name', @@ -32,17 +32,15 @@ export const placeholderResolver = { }; const provisionSourceHelpResolver = { - [ProvisionSource.URL]: - 'An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from.', - [ProvisionSource.PXE]: 'Discover provisionable virtual machines over the network.', - [ProvisionSource.CONTAINER]: - 'Ephemeral virtual machine disk image which will be pulled from container registry.', - [ProvisionSource.IMPORT]: 'Import a virtual machine from external service using a provider.', - [ProvisionSource.CLONED_DISK]: 'Select an existing PVC in Storage tab', + [ProvisionSource.URL.getValue()]: 'An external URL to the .iso, .img, .qcow2 or .raw that the virtual machine should be created from.', + [ProvisionSource.PXE.getValue()]: 'Discover provisionable virtual machines over the network.', + [ProvisionSource.CONTAINER.getValue()]: 'Ephemeral virtual machine disk image which will be pulled from container registry.', + [ProvisionSource.IMPORT.getValue()]: 'Import a virtual machine from external service using a provider.', + [ProvisionSource.DISK.getValue()]: 'Select an existing PVC in Storage tab', }; export const helpResolver = { - [VMSettingsField.PROVISION_SOURCE_TYPE]: (sourceType: ProvisionSource) => + [VMSettingsField.PROVISION_SOURCE_TYPE]: (sourceType: string) => provisionSourceHelpResolver[sourceType], [VMSettingsField.PROVIDER]: (provider) => `Not Implemented for ${provider}!!!`, [VMSettingsField.FLAVOR]: () => diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/pxe-networks.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/network-boot-source.tsx similarity index 50% rename from frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/pxe-networks.tsx rename to frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/network-boot-source.tsx index 2fe3d67dd72..4ebeb763731 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/pxe-networks.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/network-boot-source.tsx @@ -1,26 +1,23 @@ import * as React from 'react'; import { Form, FormSelect, FormSelectOption } from '@patternfly/react-core'; -import { VMWizardNetwork, VMWizardNetworkType, VMWizardNetworkWithWrappers } from '../../types'; -import { NetworkInterfaceWrapper } from '../../../../k8s/wrapper/vm/network-interface-wrapper'; +import { VMWizardNetworkWithWrappers } from '../../types'; import { PXE_INFO, PXE_NIC_NOT_FOUND_ERROR, SELECT_PXE_NIC } from '../../strings/networking'; import { FormRow } from '../../../form/form-row'; import { ValidationErrorType } from '../../../../utils/validations/types'; import { FormSelectPlaceholderOption } from '../../../form/form-select-placeholder-option'; import { ignoreCaseSort } from '../../../../utils/sort'; -import './networking-tab.scss'; - const PXE_BOOTSOURCE_ID = 'pxe-bootsource'; -type PXENetworksProps = { +type NetworkBootSourceProps = { isDisabled: boolean; networks: VMWizardNetworkWithWrappers[]; - updateNetworks: (networks: VMWizardNetwork[]) => void; + onBootOrderChanged: (deviceID: string, bootOrder: number) => void; }; -export const PXENetworks: React.FC = ({ +export const NetworkBootSource: React.FC = ({ isDisabled, - updateNetworks, + onBootOrderChanged, networks, }) => { const pxeNetworks = networks.filter((n) => !n.networkWrapper.isPodNetwork()); @@ -30,44 +27,10 @@ export const PXENetworks: React.FC = ({ network.networkInterfaceWrapper.isFirstBootableDevice(), ); - const onPXENetworkChange = (id: string) => { - const bootOrderIndexes = networks - .map((wizardNetwork) => - wizardNetwork.id === id || wizardNetwork.type !== VMWizardNetworkType.TEMPLATE - ? null - : wizardNetwork.networkInterfaceWrapper.getBootOrder(), - ) - .filter((b) => b != null) - .sort(); - updateNetworks( - // TODO: include disks in the computation and maybe move somewhere else (state update) - networks.map( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ({ networkInterfaceWrapper, networkWrapper: unused, ...wizardNetwork }) => { - if (wizardNetwork.id === id || networkInterfaceWrapper.hasBootOrder()) { - return { - ...wizardNetwork, - networkInterface: NetworkInterfaceWrapper.mergeWrappers( - networkInterfaceWrapper, - NetworkInterfaceWrapper.initializeFromSimpleData({ - bootOrder: - wizardNetwork.id === id - ? 1 - : bootOrderIndexes.indexOf(networkInterfaceWrapper.getBootOrder()) + 2, - }), - ).asResource(), - }; - } - return wizardNetwork; - }, - ), - ); - }; - return (
= ({ onBootOrderChanged(id, 1)} isRequired isDisabled={isDisabled} > diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.tsx index bb1cca86c78..fc5eb1407e8 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/networking-tab/networking-tab.tsx @@ -13,25 +13,21 @@ import { import { PlusCircleIcon } from '@patternfly/react-icons'; import { iGetCreateVMWizardTabs } from '../../selectors/immutable/selectors'; import { isStepLocked } from '../../selectors/immutable/wizard-selectors'; -import { - VMSettingsField, - VMWizardNetwork, - VMWizardNetworkWithWrappers, - VMWizardTab, -} from '../../types'; +import { VMWizardNetworkWithWrappers, VMWizardTab } from '../../types'; import { VMNicsTable } from '../../../vm-nics/vm-nics'; import { nicTableColumnClasses } from '../../../vm-nics/utils'; import { vmWizardActions } from '../../redux/actions'; import { ActionType } from '../../redux/types'; import { ADD_NETWORK_INTERFACE } from '../../strings/networking'; -import { iGetVmSettingValue } from '../../selectors/immutable/vm-settings'; -import { ProvisionSource } from '../../../../types/vm'; +import { iGetProvisionSource } from '../../selectors/immutable/vm-settings'; import { getNetworksWithWrappers } from '../../selectors/selectors'; import { wrapWithProgress } from '../../../../utils/utils'; +import { ProvisionSource } from '../../../../constants/vm/provision-source'; +import { DeviceType } from '../../../../constants/vm'; import { vmWizardNicModalEnhanced } from './vm-wizard-nic-modal-enhanced'; import { VMWizardNicRow } from './vm-wizard-nic-row'; import { VMWizardNetworkBundle } from './types'; -import { PXENetworks } from './pxe-networks'; +import { NetworkBootSource } from './network-boot-source'; import './networking-tab.scss'; @@ -50,15 +46,15 @@ const getNicsData = (networks: VMWizardNetworkWithWrappers[]): VMWizardNetworkBu }); const NetworkingTabComponent: React.FC = ({ - isPXENICRequired, + isBootNICRequired, wizardReduxID, isLocked, setTabLocked, removeNIC, - updateNetworks, + onBootOrderChanged, networks, }) => { - const hasNetworks = networks.length > 0; + const showNetworks = networks.length > 0 || isBootNICRequired; const withProgress = wrapWithProgress(setTabLocked); @@ -81,7 +77,7 @@ const NetworkingTabComponent: React.FC = ({ Network Interfaces - {hasNetworks && ( + {showNetworks && ( + + )} + + {showStorages && ( + <> +
+ +
+ {isBootDiskRequired && ( +
+ +
+ )} + + )} + {!showStorages && ( + + + + No disks attached + + + + + )} + + ); +}; + +type StorageTabFirehoseProps = { + isLocked: boolean; + isBootDiskRequired: boolean; + wizardReduxID: string; + storages: VMWizardStorageWithWrappers[]; + removeStorage: (id: string) => void; + setTabLocked: (isLocked: boolean) => void; + onBootOrderChanged: (deviceID: string, bootOrder: number) => void; + persistentVolumeClaims: FirehoseResult; +}; + +const StorageTabConnected: React.FC = ({ namespace, ...rest }) => ( + + + +); + +type StorageTabConnectedProps = StorageTabFirehoseProps & { + namespace: string; +}; + +const stateToProps = (state, { wizardReduxID }) => { + const stepData = iGetCreateVMWizardTabs(state, wizardReduxID); + return { + namespace: iGetCommonData(state, wizardReduxID, VMWizardProps.activeNamespace), + isLocked: isStepLocked(stepData, VMWizardTab.STORAGE), + storages: getStoragesWithWrappers(state, wizardReduxID), + isBootDiskRequired: iGetProvisionSource(state, wizardReduxID) === ProvisionSource.DISK, + }; +}; + +const dispatchToProps = (dispatch, { wizardReduxID }) => ({ + setTabLocked: (isLocked) => { + dispatch( + vmWizardActions[ActionType.SetTabLocked](wizardReduxID, VMWizardTab.STORAGE, isLocked), + ); + }, + removeStorage: (id: string) => { + dispatch(vmWizardActions[ActionType.RemoveStorage](wizardReduxID, id)); + }, + onBootOrderChanged: (deviceID: string, bootOrder: number) => { + dispatch( + vmWizardActions[ActionType.SetDeviceBootOrder]( + wizardReduxID, + deviceID, + DeviceType.DISK, + bootOrder, + ), + ); + }, +}); + +export const StorageTab = connect( + stateToProps, + dispatchToProps, +)(StorageTabConnected); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/types.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/types.tsx new file mode 100644 index 00000000000..9b246079f22 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/types.tsx @@ -0,0 +1,17 @@ +import { VMWizardStorageWithWrappers } from '../../types'; +import { StorageSimpleData } from '../../../vm-disks/types'; + +export type VMWizardStorageBundle = StorageSimpleData & { + wizardStorageData: VMWizardStorageWithWrappers; +}; + +export type VMWizardStorageRowActionOpts = { + wizardReduxID: string; + removeStorage?: (id: string) => void; + withProgress?: (promise: Promise) => void; +}; + +export type VMWizardStorageRowCustomData = VMWizardStorageRowActionOpts & { + columnClasses: string[]; + isDisabled: boolean; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-modal-enhanced.tsx new file mode 100644 index 00000000000..23ca32da512 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-modal-enhanced.tsx @@ -0,0 +1,147 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Firehose } from '@console/internal/components/utils'; +import { createModalLauncher, ModalComponentProps } from '@console/internal/components/factory'; +import { + NamespaceModel, + PersistentVolumeClaimModel, + ProjectModel, + StorageClassModel, +} from '@console/internal/models'; +import { iGetCommonData } from '../../selectors/immutable/selectors'; +import { + VMWizardProps, + VMWizardStorage, + VMWizardStorageType, + VMWizardStorageWithWrappers, +} from '../../types'; +import { vmWizardActions } from '../../redux/actions'; +import { ActionType } from '../../redux/types'; +import { getStoragesWithWrappers } from '../../selectors/selectors'; +import { DiskWrapper } from '../../../../k8s/wrapper/vm/disk-wrapper'; +import { VolumeWrapper } from '../../../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../../../k8s/wrapper/vm/data-volume-wrapper'; +import { DiskModal } from '../../../modals/disk-modal'; +import { VM_TEMPLATE_NAME_PARAMETER } from '../../../../constants/vm-templates'; + +const VMWizardNICModal: React.FC = (props) => { + const { + id, + type, + namespace: vmNamespace, + useProjects, + addUpdateStorage, + storages, + diskWrapper = DiskWrapper.EMPTY, + volumeWrapper = VolumeWrapper.EMPTY, + dataVolumeWrapper, + ...restProps + } = props; + const filteredStorages = storages.filter( + (storage) => + storage && + storage.diskWrapper.getName() && + storage.diskWrapper.getName() !== diskWrapper.getName(), + ); + + const usedDiskNames: Set = new Set( + filteredStorages.map(({ diskWrapper: dw }) => dw.getName()), + ); + + const usedPVCNames: Set = new Set( + filteredStorages + .filter(({ dataVolume }) => dataVolume) + .map(({ dataVolumeWrapper: dvw }) => dvw.getName()), + ); + + const [namespace, setNamespace] = React.useState(vmNamespace); + + const resources = [ + { + kind: (useProjects ? ProjectModel : NamespaceModel).kind, + isList: true, + prop: 'namespaces', + }, + { + kind: StorageClassModel.kind, + isList: true, + prop: 'storageClasses', + }, + { + kind: PersistentVolumeClaimModel.kind, + isList: true, + namespace, + prop: 'persistentVolumeClaims', + }, + ]; + + return ( + + setNamespace(n)} + usedDiskNames={usedDiskNames} + usedPVCNames={usedPVCNames} + disk={diskWrapper} + volume={volumeWrapper} + dataVolume={dataVolumeWrapper} + disableSourceChange={[ + VMWizardStorageType.PROVISION_SOURCE_DISK, + VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK, + ].includes(type)} + onSubmit={(resultDiskWrapper, resultVolumeWrapper, resultDataVolumeWrapper) => { + addUpdateStorage({ + id, + type: type || VMWizardStorageType.UI_INPUT, + disk: DiskWrapper.mergeWrappers(diskWrapper, resultDiskWrapper).asResource(), + volume: VolumeWrapper.mergeWrappers(volumeWrapper, resultVolumeWrapper).asResource(), + dataVolume: + resultDataVolumeWrapper && + DataVolumeWrapper.mergeWrappers( + dataVolumeWrapper, + resultDataVolumeWrapper, + ).asResource(), + }); + return Promise.resolve(); + }} + /> + + ); +}; + +type VMWizardStorageModalProps = ModalComponentProps & { + id?: string; + namespace: string; + useProjects?: boolean; + type?: VMWizardStorageType; + diskWrapper?: DiskWrapper; + volumeWrapper?: VolumeWrapper; + dataVolumeWrapper?: DataVolumeWrapper; + storages: VMWizardStorageWithWrappers[]; + addUpdateStorage: (storage: VMWizardStorage) => void; +}; + +const stateToProps = (state, { wizardReduxID }) => { + const useProjects = state.k8s.hasIn(['RESOURCES', 'models', ProjectModel.kind]); + return { + useProjects, + namespace: iGetCommonData(state, wizardReduxID, VMWizardProps.activeNamespace), + storages: getStoragesWithWrappers(state, wizardReduxID), + }; +}; + +const dispatchToProps = (dispatch, { wizardReduxID }) => ({ + addUpdateStorage: (storage: VMWizardStorage) => { + dispatch(vmWizardActions[ActionType.UpdateStorage](wizardReduxID, storage)); + }, +}); + +const VMWizardStorageModalConnected = connect( + stateToProps, + dispatchToProps, +)(VMWizardNICModal); + +export const vmWizardStorageModalEnhanced = createModalLauncher(VMWizardStorageModalConnected); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-row.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-row.tsx new file mode 100644 index 00000000000..a0bdb28e7d7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/storage-tab/vm-wizard-storage-row.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { Kebab, KebabOption } from '@console/internal/components/utils'; +import { VMWizardStorageType, VMWizardStorageWithWrappers } from '../../types'; +import { DiskSimpleRow } from '../../../vm-disks/disk-row'; +import { + VMWizardStorageBundle, + VMWizardStorageRowActionOpts, + VMWizardStorageRowCustomData, +} from './types'; +import { vmWizardStorageModalEnhanced } from './vm-wizard-storage-modal-enhanced'; + +const menuActionEdit = ( + { diskWrapper, volumeWrapper, dataVolumeWrapper, id, type }: VMWizardStorageWithWrappers, + { wizardReduxID, withProgress }: VMWizardStorageRowActionOpts, +): KebabOption => ({ + label: 'Edit', + callback: () => + withProgress( + vmWizardStorageModalEnhanced({ + wizardReduxID, + id, + type, + diskWrapper, + volumeWrapper, + dataVolumeWrapper, + }).result, + ), +}); + +const menuActionRemove = ( + { id }: VMWizardStorageWithWrappers, + { withProgress, removeStorage }: VMWizardStorageRowActionOpts, +): KebabOption => ({ + label: 'Delete', + callback: () => + withProgress( + new Promise((resolve) => { + removeStorage(id); + resolve(); + }), + ), +}); + +const getActions = ( + wizardNetworkData: VMWizardStorageWithWrappers, + opts: VMWizardStorageRowActionOpts, +) => { + const actions = [menuActionEdit]; + + if ( + ![ + VMWizardStorageType.PROVISION_SOURCE_DISK, + VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK, + ].includes(wizardNetworkData.type) + ) { + actions.push(menuActionRemove); + } + return actions.map((a) => a(wizardNetworkData, opts)); +}; + +export type VMWizardNicRowProps = { + obj: VMWizardStorageBundle; + customData: VMWizardStorageRowCustomData; + index: number; + style: object; +}; + +export const VmWizardStorageRow: React.FC = ({ + obj: { name, wizardStorageData, ...restData }, + customData: { isDisabled, columnClasses, removeStorage, withProgress, wizardReduxID }, + index, + style, +}) => { + const validations = _.get(wizardStorageData, ['validation', 'validations'], {}); + return ( + + } + /> + ); +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/container-source.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/container-source.tsx new file mode 100644 index 00000000000..5f43a590e9a --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/container-source.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { TextInput } from '@patternfly/react-core'; +import { toShallowJS } from '../../../../utils/immutable'; +import { FormFieldRow } from '../../form/form-field-row'; +import { FormField, FormFieldType } from '../../form/form-field'; +import { VMWizardStorage } from '../../types'; +import { VolumeWrapper } from '../../../../k8s/wrapper/vm/volume-wrapper'; +import { VolumeType } from '../../../../constants/vm/storage'; + +export const ContainerSource: React.FC = React.memo( + ({ field, provisionSourceStorage, onProvisionSourceStorageChange }) => { + const storage: VMWizardStorage = toShallowJS(provisionSourceStorage); + const volumeWrapper = VolumeWrapper.initialize(storage && storage.volume); + + return ( + + + + onProvisionSourceStorageChange({ + ...storage, + volume: VolumeWrapper.mergeWrappers( + volumeWrapper, + VolumeWrapper.initializeFromSimpleData({ + type: VolumeType.CONTAINER_DISK, + typeData: { image }, + }), + ).asResource(), + }) + } + /> + + + ); + }, +); + +type ContainerSourceProps = { + field: any; + provisionSourceStorage: any; + onProvisionSourceStorageChange: (provisionSourceStorage: any) => void; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/url-source.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/url-source.tsx new file mode 100644 index 00000000000..2a892707d4e --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/url-source.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import { TextInput } from '@patternfly/react-core'; +import { toShallowJS } from '../../../../utils/immutable'; +import { FormFieldRow } from '../../form/form-field-row'; +import { FormField, FormFieldType } from '../../form/form-field'; +import { VMWizardStorage } from '../../types'; +import { DataVolumeSourceType } from '../../../../constants/vm/storage'; +import { DataVolumeWrapper } from '../../../../k8s/wrapper/vm/data-volume-wrapper'; + +export const URLSource: React.FC = React.memo( + ({ field, provisionSourceStorage, onProvisionSourceStorageChange }) => { + const storage: VMWizardStorage = toShallowJS(provisionSourceStorage); + const dataVolumeWrapper = DataVolumeWrapper.initialize(storage && storage.dataVolume); + + return ( + + + + onProvisionSourceStorageChange({ + ...storage, + dataVolume: DataVolumeWrapper.mergeWrappers( + dataVolumeWrapper, + DataVolumeWrapper.initializeFromSimpleData({ + type: DataVolumeSourceType.HTTP, + typeData: { url }, + }), + ).asResource(), + }) + } + /> + + + ); + }, +); + +type URLSourceProps = { + field: any; + provisionSourceStorage: any; + onProvisionSourceStorageChange: (provisionSourceStorage: any) => void; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/user-templates.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/user-templates.tsx index 50d9e0942ba..7bd115a80bb 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/user-templates.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/user-templates.tsx @@ -11,7 +11,7 @@ import { nullOnEmptyChange } from '../../utils/utils'; import { iGetName } from '../../selectors/immutable/selectors'; export const UserTemplates: React.FC = React.memo( - ({ userTemplateField, userTemplates, commonTemplates, dataVolumes, onChange }) => { + ({ userTemplateField, userTemplates, commonTemplates, onChange }) => { const data = iGetLoadedData(userTemplates); const names: string[] = data && @@ -29,7 +29,6 @@ export const UserTemplates: React.FC = React.memo( loadingResources={{ userTemplates, commonTemplates, - dataVolumes, }} > @@ -46,8 +45,7 @@ export const UserTemplates: React.FC = React.memo( ); }, (prevProps, nextProps) => - iGetIsLoaded(prevProps.dataVolumes) === iGetIsLoaded(nextProps.dataVolumes) && // wait for dataVolumes; required when pre-filling template - iGetIsLoaded(prevProps.commonTemplates) === iGetIsLoaded(nextProps.commonTemplates) && // wait -||- + iGetIsLoaded(prevProps.commonTemplates) === iGetIsLoaded(nextProps.commonTemplates) && // wait for commonTemplates; required when pre-filling template prevProps.userTemplateField === nextProps.userTemplateField && prevProps.userTemplates === nextProps.userTemplates, ); @@ -56,6 +54,5 @@ type UserTemplatesProps = { userTemplateField: any; userTemplates: any; commonTemplates: any; - dataVolumes: any; onChange: (key: string, value: string) => void; }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/vm-settings-tab.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/vm-settings-tab.tsx index 82f5e0de65b..2ace3d3fbfc 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/vm-settings-tab.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/vm-settings-tab/vm-settings-tab.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { + Checkbox, FormSelect, FormSelectOption, TextArea, TextInput, - Checkbox, } from '@patternfly/react-core'; import { connect } from 'react-redux'; import { iGet, iGetIn } from '../../../../utils/immutable'; @@ -12,16 +12,24 @@ import { FormFieldMemoRow } from '../../form/form-field-row'; import { FormField, FormFieldType } from '../../form/form-field'; import { FormSelectPlaceholderOption } from '../../../form/form-select-placeholder-option'; import { vmWizardActions } from '../../redux/actions'; -import { VMSettingsField, VMSettingsRenderableField, VMWizardProps } from '../../types'; +import { + VMSettingsField, + VMSettingsRenderableField, + VMWizardProps, + VMWizardStorage, +} from '../../types'; import { iGetVmSettings } from '../../selectors/immutable/vm-settings'; import { ActionType } from '../../redux/types'; import { getFieldId, getPlaceholder } from '../../utils/vm-settings-tab-utils'; import { iGetCommonData } from '../../selectors/immutable/selectors'; import { FormFieldForm } from '../../form/form-field-form'; +import { iGetProvisionSourceStorage } from '../../selectors/immutable/storage'; import { WorkloadProfile } from './workload-profile'; import { OSFlavor } from './os-flavor'; import { UserTemplates } from './user-templates'; import { MemoryCPU } from './memory-cpu'; +import { ContainerSource } from './container-source'; +import { URLSource } from './url-source'; import './vm-settings-tab.scss'; @@ -35,7 +43,13 @@ export class VMSettingsTabComponent extends React.Component (value) => this.props.onFieldChange(key, value); render() { - const { userTemplates, commonTemplates, dataVolumes, isReview } = this.props; + const { + userTemplates, + commonTemplates, + provisionSourceStorage, + updateStorage, + isReview, + } = this.props; return ( @@ -45,7 +59,6 @@ export class VMSettingsTabComponent extends React.Component )} @@ -67,22 +80,16 @@ export class VMSettingsTabComponent extends React.Component - - - - - - + - - - - + onProvisionSourceStorageChange={updateStorage} + provisionSourceStorage={provisionSourceStorage} + /> ({ vmSettings: iGetVmSettings(state, wizardReduxID), commonTemplates: iGetCommonData(state, wizardReduxID, VMWizardProps.commonTemplates), userTemplates: iGetCommonData(state, wizardReduxID, VMWizardProps.userTemplates), - dataVolumes: iGetCommonData(state, wizardReduxID, VMWizardProps.dataVolumes), + provisionSourceStorage: iGetProvisionSourceStorage(state, wizardReduxID), }); type VMSettingsTabComponentProps = { onFieldChange: (key: VMSettingsRenderableField, value: string) => void; + updateStorage: (storage: VMWizardStorage) => void; vmSettings: any; + provisionSourceStorage: VMWizardStorage; commonTemplates: any; userTemplates: any; - dataVolumes: any; isReview: boolean; }; const dispatchToProps = (dispatch, props) => ({ onFieldChange: (key, value) => dispatch(vmWizardActions[ActionType.SetVmSettingsFieldValue](props.wizardReduxID, key, value)), + updateStorage: (storage: VMWizardStorage) => { + dispatch(vmWizardActions[ActionType.UpdateStorage](props.wizardReduxID, storage)); + }, }); export const VMSettingsTab = connect( diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/types.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/types.ts index 85cd108df90..09b9b7aa628 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/types.ts @@ -4,7 +4,13 @@ import { getStringEnumValues } from '../../utils/types'; import { V1Network, V1NetworkInterface, VMKind } from '../../types/vm'; import { NetworkInterfaceWrapper } from '../../k8s/wrapper/vm/network-interface-wrapper'; import { NetworkWrapper } from '../../k8s/wrapper/vm/network-wrapper'; -import { UINetworkInterfaceValidation } from '../../utils/validations/vm'; +import { UIDiskValidation, UINetworkInterfaceValidation } from '../../utils/validations/vm'; +import { V1Disk } from '../../types/vm/disk/V1Disk'; +import { V1Volume } from '../../types/vm/disk/V1Volume'; +import { V1alpha1DataVolume } from '../../types/vm/disk/V1alpha1DataVolume'; +import { DiskWrapper } from '../../k8s/wrapper/vm/disk-wrapper'; +import { VolumeWrapper } from '../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../k8s/wrapper/vm/data-volume-wrapper'; export enum VMWizardTab { // order important VM_SETTINGS = 'VM_SETTINGS', @@ -21,10 +27,6 @@ export enum VMWizardProps { virtualMachines = 'virtualMachines', userTemplates = 'userTemplates', commonTemplates = 'commonTemplates', - networkAttachmentDefinitions = 'networkAttachmentDefinitions', - storageClasses = 'storageClasses', - persistentVolumeClaims = 'persistentVolumeClaims', - dataVolumes = 'dataVolumes', } export const ALL_VM_WIZARD_TABS = getStringEnumValues(VMWizardTab); @@ -66,12 +68,8 @@ export type VMSettingsFieldType = { export type ChangedCommonDataProp = | VMWizardProps.activeNamespace | VMWizardProps.virtualMachines - | VMWizardProps.dataVolumes | VMWizardProps.userTemplates - | VMWizardProps.persistentVolumeClaims - | VMWizardProps.commonTemplates - | VMWizardProps.networkAttachmentDefinitions - | VMWizardProps.storageClasses; + | VMWizardProps.commonTemplates; export type CommonDataProp = VMWizardProps.isCreateTemplate | ChangedCommonDataProp; @@ -80,11 +78,8 @@ export type ChangedCommonData = Set; export const DetectCommonDataChanges = new Set([ VMWizardProps.activeNamespace, VMWizardProps.virtualMachines, - VMWizardProps.dataVolumes, VMWizardProps.userTemplates, VMWizardProps.commonTemplates, - VMWizardProps.persistentVolumeClaims, - VMWizardProps.networkAttachmentDefinitions, ]); export type CommonData = { @@ -128,3 +123,25 @@ export type VMWizardNetworkWithWrappers = VMWizardNetwork & { networkInterfaceWrapper: NetworkInterfaceWrapper; networkWrapper: NetworkWrapper; }; + +export enum VMWizardStorageType { + TEMPLATE = 'TEMPLATE', + PROVISION_SOURCE_TEMPLATE_DISK = 'PROVISION_SOURCE_TEMPLATE_DISK', + PROVISION_SOURCE_DISK = 'PROVISION_SOURCE_DISK', + UI_INPUT = 'UI_INPUT', +} + +export type VMWizardStorage = { + id?: string; + type: VMWizardStorageType; + disk: V1Disk; + volume: V1Volume; + dataVolume?: V1alpha1DataVolume; + validation?: UIDiskValidation; +}; + +export type VMWizardStorageWithWrappers = VMWizardStorage & { + diskWrapper: DiskWrapper; + volumeWrapper: VolumeWrapper; + dataVolumeWrapper?: DataVolumeWrapper; +}; diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx index caa4170de67..5fd7096a9b1 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/disk-modal-enhanced.tsx @@ -11,13 +11,13 @@ import { StorageClassModel, } from '@console/internal/models'; import { getLoadedData } from '../../../utils'; -import { asVM, getVMLikeModel, getDisks, getDataVolumeTemplates } from '../../../selectors/vm'; +import { asVM, getVMLikeModel } from '../../../selectors/vm'; import { VMLikeEntityKind } from '../../../types'; -import { getSimpleName } from '../../../selectors/utils'; import { DiskWrapper } from '../../../k8s/wrapper/vm/disk-wrapper'; import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; import { getUpdateDiskPatches } from '../../../k8s/patches/vm/vm-disk-patches'; +import { CombinedDiskFactory } from '../../../k8s/wrapper/vm/combined-disk'; import { DiskModal } from './disk-modal'; const DiskModalFirehoseComponent: React.FC = (props) => { @@ -32,17 +32,7 @@ const DiskModalFirehoseComponent: React.FC = (p ? DataVolumeWrapper.initialize(dataVolume) : DataVolumeWrapper.EMPTY; - const usedDiskNames: Set = new Set( - getDisks(vm) - .map(getSimpleName) - .filter((n) => n && n !== diskWrapper.getName()), - ); - - const usedPVCNames: Set = new Set( - getDataVolumeTemplates(vm) - .map((dv) => getName(dv)) - .filter((n) => n && n !== dataVolumeWrapper.getName()), - ); + const combinedDiskFactory = CombinedDiskFactory.initializeFromVMLikeEntity(vmLikeFinal); const onSubmit = async (resultDisk, resultVolume, resultDataVolume) => k8sPatch( @@ -63,8 +53,8 @@ const DiskModalFirehoseComponent: React.FC = (p return ( { @@ -50,6 +51,7 @@ export const DiskModal = withHandlePromise((props: DiskModalProps) => { namespaces, onNamespaceChanged, usedDiskNames, + disableSourceChange, onSubmit, inProgress, errorMessage, @@ -73,7 +75,13 @@ export const DiskModal = withHandlePromise((props: DiskModalProps) => { volume.getContainerImage() || '', ); - const [pvcName, setPVCName] = React.useState(source.getPVCName(volume, dataVolume)); + const [pvcName, setPVCName] = React.useState( + new CombinedDisk({ + diskWrapper: disk, + volumeWrapper: volume, + dataVolumeWrapper: dataVolume, + }).getPVCName(source), + ); const [name, setName] = React.useState( disk.getName() || getSequenceName('disk', usedDiskNames), @@ -149,6 +157,9 @@ export const DiskModal = withHandlePromise((props: DiskModalProps) => { }; const onSourceChanged = (uiSource) => { + if (disableSourceChange) { + return; + } setSize(''); setUnit('Gi'); setURL(''); @@ -181,7 +192,7 @@ export const DiskModal = withHandlePromise((props: DiskModalProps) => { onChange={onSourceChanged} value={asFormSelectValue(source)} id={asId('source')} - isDisabled={inProgress} + isDisabled={inProgress || disableSourceChange} > {StorageUISource.getAll().map((uiType) => { return ( @@ -199,7 +210,7 @@ export const DiskModal = withHandlePromise((props: DiskModalProps) => { { { { { export type DiskModalProps = { disk?: DiskWrapper; + disableSourceChange?: boolean; volume?: VolumeWrapper; dataVolume?: DataVolumeWrapper; onSubmit: ( diff --git a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/storage-ui-source.ts b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/storage-ui-source.ts index 6999382c552..5a312dad2da 100644 --- a/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/storage-ui-source.ts +++ b/frontend/packages/kubevirt-plugin/src/components/modals/disk-modal/storage-ui-source.ts @@ -2,8 +2,6 @@ import { ValueEnum, VolumeType } from '../../../constants'; import { DataVolumeSourceType } from '../../../constants/vm/storage'; -import { VolumeWrapper } from '../../../k8s/wrapper/vm/volume-wrapper'; -import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; export class StorageUISource extends ValueEnum { static readonly BLANK = new StorageUISource( @@ -83,15 +81,4 @@ export class StorageUISource extends ValueEnum { requiresNamespace = () => this === StorageUISource.ATTACH_CLONED_DISK; isEditingSupported = () => !this.dataVolumeSourceType; - - getPVCName = (volume: VolumeWrapper, dataVolume: DataVolumeWrapper) => { - if (this === StorageUISource.ATTACH_DISK) { - return volume.getPersistentVolumeClaimName(); - } - if (this === StorageUISource.ATTACH_CLONED_DISK) { - return dataVolume.getPesistentVolumeClaimName(); - } - - return null; - }; } diff --git a/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.scss b/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.scss index 681821d32b2..4d50272230c 100644 --- a/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.scss +++ b/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.scss @@ -1,3 +1,3 @@ -.kubevirt-nic-row__cell--error { +.kubevirt-validation-cell__cell--error { color: var(--pf-global--danger-color--100); } diff --git a/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.tsx b/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.tsx index 9ee22043adc..fbd81b80f75 100644 --- a/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/table/validation-cell.tsx @@ -13,10 +13,13 @@ export const ValidationCell: React.FC = ({ children, validation return ( <> {children} - {validation && validation.type !== ValidationErrorType.TrivialError && ( + {validation && (
{validation.message} diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx index 2ba4dc827a9..dc76f78519c 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/disk-row.tsx @@ -16,6 +16,7 @@ import { dimensifyRow } from '../../utils/table'; import { ValidationCell } from '../table/validation-cell'; import { VMNicRowActionOpts } from '../vm-nics/types'; import { diskModalEnhanced } from '../modals/disk-modal/disk-modal-enhanced'; +import { CombinedDisk } from '../../k8s/wrapper/vm/combined-disk'; import { StorageBundle, StorageSimpleData, @@ -25,9 +26,7 @@ import { } from './types'; const menuActionEdit = ( - disk, - volume, - dataVolume, + disk: CombinedDisk, vmLikeEntity: VMLikeEntityKind, { withProgress }: VMNicRowActionOpts, ): KebabOption => ({ @@ -36,9 +35,9 @@ const menuActionEdit = ( withProgress( diskModalEnhanced({ vmLikeEntity, - disk, - volume, - dataVolume, + disk: disk.diskWrapper.asResource(), + volume: disk.volumeWrapper.asResource(), + dataVolume: disk.dataVolumeWrapper && disk.dataVolumeWrapper.asResource(), }).result, ), accessReview: asAccessReview( @@ -49,9 +48,7 @@ const menuActionEdit = ( }); const menuActionDelete = ( - disk, - volume, - dataVolume, + disk: CombinedDisk, vmLikeEntity: VMLikeEntityKind, { withProgress }: VMNicRowActionOpts, ): KebabOption => ({ @@ -60,7 +57,7 @@ const menuActionDelete = ( withProgress( deleteDeviceModal({ deviceType: DeviceType.DISK, - device: disk, + device: disk.diskWrapper.asResource(), vmLikeEntity, }).result, ), @@ -72,9 +69,7 @@ const menuActionDelete = ( }); const getActions = ( - disk, - volume, - dataVolume, + disk: CombinedDisk, vmLikeEntity: VMLikeEntityKind, opts: VMStorageRowActionOpts, ) => { @@ -82,11 +77,14 @@ const getActions = ( if (isVMRunning(asVM(vmLikeEntity))) { return actions; } - if (opts.isEditingEnabled) { + + const isTemplate = vmLikeEntity && !isVM(vmLikeEntity); + if (disk.getSource() && (isTemplate || disk.isEditingSupported())) { actions.push(menuActionEdit); } + actions.push(menuActionDelete); - return actions.map((a) => a(disk, volume, dataVolume, vmLikeEntity, opts)); + return actions.map((a) => a(disk, vmLikeEntity, opts)); }; export type VMDiskSimpleRowProps = { @@ -145,27 +143,26 @@ export type VMDiskRowProps = { }; export const DiskRow: React.FC = ({ - obj: { name, disk, volume, dataVolume, isEditingEnabled, ...restData }, + obj: { disk, ...restData }, customData: { isDisabled, withProgress, vmLikeEntity, columnClasses }, index, style, }) => { return ( } /> diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/types.ts b/frontend/packages/kubevirt-plugin/src/components/vm-disks/types.ts index f3ed805065f..a81b17c026a 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/types.ts @@ -1,5 +1,6 @@ import { VMLikeEntityKind } from '../../types'; import { ValidationObject } from '../../utils/validations/types'; +import { CombinedDisk } from '../../k8s/wrapper/vm/combined-disk'; export type StorageSimpleData = { name?: string; @@ -16,15 +17,11 @@ export type StorageSimpleDataValidation = { }; export type StorageBundle = StorageSimpleData & { - disk: any; - volume: any; - dataVolume: any; - isEditingEnabled: boolean; + disk: CombinedDisk; }; export type VMStorageRowActionOpts = { withProgress: (promise: Promise) => void; - isEditingEnabled: boolean; }; export type VMStorageRowCustomData = { diff --git a/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx b/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx index 9547de1bb6a..7979e69079a 100644 --- a/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/vm-disks/vm-disks.tsx @@ -3,29 +3,19 @@ import { Button, ButtonVariant } from '@patternfly/react-core'; import { Table } from '@console/internal/components/factory'; import { PersistentVolumeClaimModel } from '@console/internal/models'; import { Firehose, FirehoseResult } from '@console/internal/components/utils'; -import { getNamespace, getName, createBasicLookup, createLookup } from '@console/shared'; +import { getNamespace } from '@console/shared'; import { useSafetyFirst } from '@console/internal/components/safety-first'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { sortable } from '@patternfly/react-table'; import { DataVolumeModel } from '../../models'; import { VMLikeEntityKind } from '../../types'; -import { asVM, getDataVolumeTemplates, getDisks, getVolumes, isVM } from '../../selectors/vm'; -import { getPvcStorageClassName, getPvcStorageSize } from '../../selectors/pvc/selectors'; -import { - getDataVolumeStorageClassName, - getDataVolumeStorageSize, -} from '../../selectors/dv/selectors'; import { VMLikeEntityTabProps } from '../vms/types'; import { getResource } from '../../utils'; -import { getSimpleName } from '../../selectors/utils'; import { wrapWithProgress } from '../../utils/utils'; import { dimensifyHeader } from '../../utils/table'; -import { DiskWrapper } from '../../k8s/wrapper/vm/disk-wrapper'; -import { VolumeWrapper } from '../../k8s/wrapper/vm/volume-wrapper'; -import { DiskType, VolumeType } from '../../constants/vm/storage'; import { diskModalEnhanced } from '../modals/disk-modal/disk-modal-enhanced'; -import { StorageUISource } from '../modals/disk-modal/storage-ui-source'; -import { DataVolumeWrapper } from '../../k8s/wrapper/vm/data-volume-wrapper'; +import { CombinedDiskFactory } from '../../k8s/wrapper/vm/combined-disk'; +import { V1alpha1DataVolume } from '../../types/vm/disk/V1alpha1DataVolume'; import { StorageBundle } from './types'; import { DiskRow } from './disk-row'; import { diskTableColumnClasses } from './utils'; @@ -37,65 +27,22 @@ const getStoragesData = ({ }: { vmLikeEntity: VMLikeEntityKind; pvcs: FirehoseResult; - datavolumes: FirehoseResult; + datavolumes: FirehoseResult; }): StorageBundle[] => { - const vm = asVM(vmLikeEntity); - - const pvcLookup = createLookup(pvcs, getName); - const datavolumeLookup = createLookup(datavolumes, getName); - const volumeLookup = createBasicLookup(getVolumes(vm), getSimpleName); - const datavolumeTemplatesLookup = createBasicLookup(getDataVolumeTemplates(vm), getName); - - return getDisks(vm).map((disk) => { - const diskWrapper = DiskWrapper.initialize(disk); - const volume = volumeLookup[diskWrapper.getName()]; - const volumeWrapper = VolumeWrapper.initialize(volume); - - let size = null; - let storageClass = null; - - if (volumeWrapper.getType() === VolumeType.PERSISTENT_VOLUME_CLAIM) { - const pvc = pvcLookup[volumeWrapper.getPersistentVolumeClaimName()]; - if (pvc) { - size = getPvcStorageSize(pvc); - storageClass = getPvcStorageClassName(pvc); - } else if (!pvcs.loaded) { - size = undefined; - storageClass = undefined; - } - } else if (volumeWrapper.getType() === VolumeType.DATA_VOLUME) { - const dataVolumeTemplate = - datavolumeTemplatesLookup[volumeWrapper.getDataVolumeName()] || - datavolumeLookup[volumeWrapper.getDataVolumeName()]; - - if (dataVolumeTemplate) { - size = getDataVolumeStorageSize(dataVolumeTemplate); - storageClass = getDataVolumeStorageClassName(dataVolumeTemplate); - } else if (!datavolumes.loaded) { - size = undefined; - storageClass = undefined; - } - } + const combinedDiskFactory = CombinedDiskFactory.initializeFromVMLikeEntity( + vmLikeEntity, + datavolumes, + pvcs, + ); - const dataVolume = datavolumeTemplatesLookup[volumeWrapper.getDataVolumeName()]; - const source = StorageUISource.fromTypes( - volumeWrapper.getType(), - DataVolumeWrapper.initialize(dataVolume).getType(), - ); - const isTemplate = vmLikeEntity && !isVM(vmLikeEntity); - return { - disk, - volume, - dataVolume, - isEditingEnabled: isTemplate || (source && source.isEditingSupported()), - // for sorting - name: diskWrapper.getName(), - diskInterface: - diskWrapper.getType() === DiskType.DISK ? diskWrapper.getReadableDiskBus() : undefined, - size, - storageClass, - }; - }); + return combinedDiskFactory.getCombinedDisks().map((disk) => ({ + disk, + // for sorting + name: disk.getName(), + diskInterface: disk.getDiskInterface(), + size: disk.getSize(), + storageClass: disk.getStorageClassName(), + })); }; export type VMDisksTableProps = { @@ -156,7 +103,7 @@ export const VMDisksTable: React.FC = ({ type VMDisksProps = { vmLikeEntity?: VMLikeEntityKind; pvcs?: FirehoseResult; - datavolumes?: FirehoseResult; + datavolumes?: FirehoseResult; }; export const VMDisks: React.FC = ({ vmLikeEntity, pvcs, datavolumes }) => { diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm-templates/constants.ts b/frontend/packages/kubevirt-plugin/src/constants/vm-templates/constants.ts index 9cf750fb99d..6ddd1520159 100644 --- a/frontend/packages/kubevirt-plugin/src/constants/vm-templates/constants.ts +++ b/frontend/packages/kubevirt-plugin/src/constants/vm-templates/constants.ts @@ -1,2 +1,4 @@ export const VM_TEMPLATE_LABEL_PLURAL = 'Virtual Machine Templates'; export const VM_TEMPLATE_CREATE_HEADER = 'Create Virtual Machine Template'; + +export const VM_TEMPLATE_NAME_PARAMETER = '${NAME}'; // eslint-disable-line no-template-curly-in-string diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts index ce03f827767..0f8d59e8234 100644 --- a/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/constants.ts @@ -25,3 +25,8 @@ export const VM_DETAIL_OVERVIEW_HREF = 'overview'; export const VM_DETAIL_DISKS_HREF = 'disks'; export const VM_DETAIL_NETWORKS_HREF = 'nics'; export const VM_DETAIL_CONSOLES_HREF = 'consoles'; + +export enum DeviceType { + NIC = 'NIC', + DISK = 'DISK', +} diff --git a/frontend/packages/kubevirt-plugin/src/constants/vm/provision-source.ts b/frontend/packages/kubevirt-plugin/src/constants/vm/provision-source.ts new file mode 100644 index 00000000000..308db6c44d3 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/constants/vm/provision-source.ts @@ -0,0 +1,117 @@ +/* eslint-disable lines-between-class-members */ +import { getName, getNamespace } from '@console/shared/src'; +import { ValueEnum } from '../value-enum'; +import { + asVM, + getDataVolumeTemplates, + getDisks, + getInterfaces, + getVolumeDataVolumeName, + getVolumes, +} from '../../selectors/vm'; +import { VMLikeEntityKind } from '../../types'; +import { StorageUISource } from '../../components/modals/disk-modal/storage-ui-source'; +import { VolumeWrapper } from '../../k8s/wrapper/vm/volume-wrapper'; +import { DataVolumeWrapper } from '../../k8s/wrapper/vm/data-volume-wrapper'; +import { VolumeType } from './storage'; + +type ProvisionSourceDetails = { + type?: ProvisionSource; + source?: string; + error?: string; +}; + +export class ProvisionSource extends ValueEnum { + static readonly PXE = new ProvisionSource('PXE'); + static readonly CONTAINER = new ProvisionSource('Container'); + static readonly URL = new ProvisionSource('URL'); + static readonly IMPORT = new ProvisionSource('Import'); + static readonly DISK = new ProvisionSource('Disk'); + + private static readonly ALL = Object.freeze( + ValueEnum.getAllClassEnumProperties(ProvisionSource), + ); + + private static readonly stringMapper = ProvisionSource.ALL.reduce( + (accumulator, provisionSource: ProvisionSource) => ({ + ...accumulator, + [provisionSource.value]: provisionSource, + }), + {}, + ); + + static getAll = () => ProvisionSource.ALL; + + static fromSerialized = (provisionSource: { value: string }): ProvisionSource => + ProvisionSource.fromString(provisionSource && provisionSource.value); + + static fromString = (source: string): ProvisionSource => ProvisionSource.stringMapper[source]; + + static getProvisionSourceDetails = (vmLikeEntity: VMLikeEntityKind): ProvisionSourceDetails => { + const vm = asVM(vmLikeEntity); + if (getInterfaces(vm).some((i) => i.bootOrder === 1)) { + return { + type: ProvisionSource.PXE, + }; + } + + const bootDisk = getDisks(vm).find((disk) => disk.bootOrder === 1); + if (bootDisk) { + const volume = getVolumes(vm).find((vol) => vol.name === bootDisk.name); + const volumeWrapper = VolumeWrapper.initialize(volume); + let dataVolumeWrapper; + + if (volumeWrapper.getType() === VolumeType.DATA_VOLUME) { + const dataVolume = getDataVolumeTemplates(vm).find( + (dv) => getName(dv) === getVolumeDataVolumeName(volume), + ); + if (!dataVolume) { + return { + error: `Datavolume ${volumeWrapper.getDataVolumeName()} does not exist.`, + }; + } + dataVolumeWrapper = DataVolumeWrapper.initialize(dataVolume); + } + + const type = StorageUISource.fromTypes( + volumeWrapper.getType(), + dataVolumeWrapper && dataVolumeWrapper.getType(), + ); + + switch (type) { + case StorageUISource.CONTAINER: + return { + type: ProvisionSource.CONTAINER, + source: volumeWrapper.getContainerImage(), + }; + case StorageUISource.URL: + return { + type: ProvisionSource.URL, + source: dataVolumeWrapper.getURL(), + }; + case StorageUISource.ATTACH_CLONED_DISK: + return { + type: ProvisionSource.DISK, + source: `${dataVolumeWrapper.getPesistentVolumeClaimNamespace()}/${dataVolumeWrapper.getPesistentVolumeClaimName()}`, + }; + case StorageUISource.ATTACH_DISK: + return { + type: ProvisionSource.DISK, + source: `${getNamespace(vmLikeEntity)}/${volumeWrapper.getPersistentVolumeClaimName()}`, + }; + case StorageUISource.BLANK: + return { + error: `Datavolume ${volumeWrapper.getDataVolumeName()} does not have a supported source (${type}).`, + }; + default: + return { + error: `Datavolume ${volumeWrapper.getDataVolumeName()} does not have a supported source.`, + }; + } + } + + return { + error: 'No bootable device found.', + }; + }; +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts index 7ae285ce22c..6ba96eb93b5 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/patches/vm/vm-disk-patches.ts @@ -118,7 +118,7 @@ export const getUpdateDiskPatches = async ( .build(), finalDataVolume && new PatchBuilder('/spec/dataVolumeTemplates') - .setListUpdate(finalDataVolume, dataVolumeTemplates, getSimpleName, oldDataVolumeName) + .setListUpdate(finalDataVolume, dataVolumeTemplates, getName, oldDataVolumeName) .build(), ].filter((patch) => patch); }); diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/combined-disk.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/combined-disk.ts new file mode 100644 index 00000000000..ddb4783983c --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/combined-disk.ts @@ -0,0 +1,232 @@ +import * as _ from 'lodash'; +import { K8sResourceKind } from '@console/internal/module/k8s'; +import { createBasicLookup, getName, getNamespace } from '@console/shared/src'; +import { FirehoseResult } from '@console/internal/components/utils'; +import { V1Disk } from '../../../types/vm/disk/V1Disk'; +import { V1Volume } from '../../../types/vm/disk/V1Volume'; +import { V1alpha1DataVolume } from '../../../types/vm/disk/V1alpha1DataVolume'; +import { getSimpleName } from '../../../selectors/utils'; +import { DiskType, VolumeType } from '../../../constants/vm/storage'; +import { VMLikeEntityKind } from '../../../types'; +import { asVM, getDataVolumeTemplates, getDisks, getVolumes } from '../../../selectors/vm'; +import { getLoadedData, isLoaded } from '../../../utils'; +import { getPvcStorageClassName, getPvcStorageSize } from '../../../selectors/pvc/selectors'; +import { StorageUISource } from '../../../components/modals/disk-modal/storage-ui-source'; +import { DiskWrapper } from './disk-wrapper'; +import { DataVolumeWrapper } from './data-volume-wrapper'; +import { VolumeWrapper } from './volume-wrapper'; + +export class CombinedDisk { + private readonly dataVolumesLoading: boolean; + + private readonly pvcsLoading: boolean; + + private readonly source: StorageUISource; + + readonly diskWrapper: DiskWrapper; + + readonly volumeWrapper: VolumeWrapper; + + readonly dataVolumeWrapper?: DataVolumeWrapper; + + readonly pvc?: K8sResourceKind; + + constructor({ + diskWrapper, + volumeWrapper, + dataVolumeWrapper, + pvc, + dataVolumesLoading, + pvcsLoading, + }: { + diskWrapper: DiskWrapper; + volumeWrapper: VolumeWrapper; + dataVolumeWrapper?: DataVolumeWrapper; + pvc?: K8sResourceKind; + dataVolumesLoading?: boolean; + pvcsLoading?: boolean; + }) { + this.diskWrapper = diskWrapper; + this.volumeWrapper = volumeWrapper; + this.dataVolumeWrapper = dataVolumeWrapper; + this.pvc = pvc; + this.dataVolumesLoading = dataVolumesLoading; + this.pvcsLoading = pvcsLoading; + this.source = StorageUISource.fromTypes( + volumeWrapper.getType(), + dataVolumeWrapper && dataVolumeWrapper.getType(), + ); + } + + getSource = () => this.source; + + isEditingSupported = () => !!this.source && this.source.isEditingSupported(); + + getName = () => this.diskWrapper.getName(); + + getDiskInterface = () => + this.diskWrapper.getType() === DiskType.DISK + ? this.diskWrapper.getReadableDiskBus() + : undefined; + + getSize = () => + this.volumeTypeOperation(getPvcStorageSize, (dataVolumeWrapper) => + dataVolumeWrapper.getReadabableSize(), + ); + + getStorageClassName = () => + this.volumeTypeOperation(getPvcStorageClassName, (dataVolumeWrapper) => + dataVolumeWrapper.getStorageClassName(), + ); + + getPVCName = (source?: StorageUISource) => { + const resolvedSource = + source || + StorageUISource.fromTypes( + this.volumeWrapper.getType(), + this.dataVolumeWrapper && this.dataVolumeWrapper.getType(), + ); + if (resolvedSource === StorageUISource.ATTACH_DISK) { + return this.volumeWrapper.getPersistentVolumeClaimName(); + } + if (resolvedSource === StorageUISource.ATTACH_CLONED_DISK) { + return this.dataVolumeWrapper.getPesistentVolumeClaimName(); + } + + return null; + }; + + toString = () => { + return _.compact([ + this.getName(), + this.getSize(), + this.getDiskInterface(), + this.getStorageClassName(), + ]).join(' - '); + }; + + private volumeTypeOperation = ( + onPVC: (pvc: K8sResourceKind) => string, + onDataVolumeWrapper: (dataVolumeWrapper: DataVolumeWrapper) => string, + ) => { + const volumeType = this.volumeWrapper.getType(); + if (volumeType === VolumeType.PERSISTENT_VOLUME_CLAIM) { + if (this.pvc) { + return onPVC(this.pvc); + } + if (this.pvcsLoading) { + return undefined; + } + } else if (volumeType === VolumeType.DATA_VOLUME) { + if (this.dataVolumeWrapper) { + return onDataVolumeWrapper(this.dataVolumeWrapper); + } + if (this.dataVolumesLoading) { + return undefined; + } + } + return null; + }; +} + +export class CombinedDiskFactory { + private readonly disks: V1Disk[]; + + private readonly volumes: V1Volume[]; + + private readonly dataVolumes: V1alpha1DataVolume[]; + + private readonly pvcs: K8sResourceKind[]; + + private readonly dataVolumesLoading: boolean; + + private readonly pvcsLoading: boolean; + + static initializeFromVMLikeEntity = ( + vmLikeEntity: VMLikeEntityKind, + datavolumes?: FirehoseResult, + pvcs?: FirehoseResult, + ) => { + const vm = asVM(vmLikeEntity); + + return new CombinedDiskFactory({ + disks: getDisks(vm), + volumes: getVolumes(vm), + dataVolumes: [...getLoadedData(datavolumes, []), ...getDataVolumeTemplates(vm)], + pvcs: getLoadedData(pvcs), + dataVolumesLoading: !isLoaded(datavolumes), + pvcsLoading: !isLoaded(pvcs), + namespace: getNamespace(vmLikeEntity), + }); + }; + + constructor({ + disks, + volumes, + dataVolumes, + dataVolumesLoading, + pvcs, + pvcsLoading, + namespace, + }: { + disks: V1Disk[]; + volumes: V1Volume[]; + dataVolumes?: V1alpha1DataVolume[]; + dataVolumesLoading?: boolean; + pvcs?: K8sResourceKind[]; + pvcsLoading?: boolean; + namespace: string; + }) { + this.disks = disks; + this.volumes = volumes; + this.dataVolumes = + dataVolumes && + dataVolumes.filter((dataVolume) => { + const ns = getNamespace(dataVolume); + return !ns || ns === namespace; + }); + this.pvcs = + pvcs && + pvcs.filter((pvc) => { + const ns = getNamespace(pvc); + return !ns || ns === namespace; + }); + this.dataVolumesLoading = dataVolumesLoading; + this.pvcsLoading = pvcsLoading; + } + + getCombinedDisks = (): CombinedDisk[] => { + const volumeLookup = createBasicLookup(this.volumes, getSimpleName); + const datavolumeLookup = createBasicLookup(this.dataVolumes, getName); + const pvcLookup = createBasicLookup(this.pvcs, getName); + + return this.disks.map((disk) => { + const diskWrapper = DiskWrapper.initialize(disk); + const volume = volumeLookup[diskWrapper.getName()]; + const volumeWrapper = VolumeWrapper.initialize(volume); + const dataVolume = + volumeWrapper.getType() === VolumeType.DATA_VOLUME + ? datavolumeLookup[volumeWrapper.getDataVolumeName()] + : undefined; + const pvc = + volumeWrapper.getType() === VolumeType.PERSISTENT_VOLUME_CLAIM + ? pvcLookup[volumeWrapper.getPersistentVolumeClaimName()] + : undefined; + + return new CombinedDisk({ + diskWrapper, + volumeWrapper, + dataVolumeWrapper: dataVolume && DataVolumeWrapper.initialize(dataVolume), + pvc, + dataVolumesLoading: this.dataVolumesLoading, + pvcsLoading: this.pvcsLoading, + }); + }); + }; + + getUsedDiskNames = (excludeName: string): Set => + new Set(this.disks.map(getSimpleName).filter((n) => n && n !== excludeName)); + + getUsedDataVolumeNames = (excludeName: string): Set => + new Set(this.dataVolumes.map((dv) => getName(dv)).filter((n) => n && n !== excludeName)); +} diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/data-volume-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/data-volume-wrapper.ts index b143b6a0b9a..dec84a43130 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/data-volume-wrapper.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/data-volume-wrapper.ts @@ -5,8 +5,10 @@ import { ObjectWithTypePropertyWrapper } from '../common/object-with-type-proper import { V1alpha1DataVolume } from '../../../types/vm/disk/V1alpha1DataVolume'; import { DataVolumeSourceType } from '../../../constants/vm/storage'; import { + getDataVolumeAccessModes, getDataVolumeStorageClassName, getDataVolumeStorageSize, + getDataVolumeVolumeMode, } from '../../../selectors/dv/selectors'; type CombinedTypeData = { @@ -115,6 +117,8 @@ export class DataVolumeWrapper extends ObjectWithTypePropertyWrapper< getPesistentVolumeClaimName = () => this.getIn(['spec', 'source', 'pvc', 'name']); + getPesistentVolumeClaimNamespace = () => this.getIn(['spec', 'source', 'pvc', 'namespace']); + getURL = () => this.getIn(['spec', 'source', 'http', 'url']); getSize = (): { value: number; unit: string } => { @@ -125,5 +129,14 @@ export class DataVolumeWrapper extends ObjectWithTypePropertyWrapper< }; }; + getReadabableSize = () => { + const { value, unit } = this.getSize(); + return `${value} ${unit}`; + }; + hasSize = () => this.getSize().value > 0; + + getAccessModes = () => getDataVolumeAccessModes(this.data); + + getVolumeMode = () => getDataVolumeVolumeMode(this.data); } diff --git a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/disk-wrapper.ts b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/disk-wrapper.ts index 9bdbaec17c2..3b39bb41bb0 100644 --- a/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/disk-wrapper.ts +++ b/frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/disk-wrapper.ts @@ -25,7 +25,7 @@ export class DiskWrapper extends ObjectWithTypePropertyWrapper }, { initializeWithType: type, - initializeWithTypeData: type === DiskType.DISK ? { bus: bus.getValue() } : undefined, + initializeWithTypeData: type === DiskType.DISK && bus ? { bus: bus.getValue() } : undefined, }, ); }; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/dv/selectors.ts b/frontend/packages/kubevirt-plugin/src/selectors/dv/selectors.ts index 54676779c19..8c5d1b4bfe8 100644 --- a/frontend/packages/kubevirt-plugin/src/selectors/dv/selectors.ts +++ b/frontend/packages/kubevirt-plugin/src/selectors/dv/selectors.ts @@ -1,7 +1,6 @@ import * as _ from 'lodash'; import { K8sResourceKind } from '@console/internal/module/k8s'; import { getStorageSize } from '../selectors'; -import { DataVolumeSource } from '../../types/dv'; export const getDataVolumeResources = (dataVolume: K8sResourceKind) => _.get(dataVolume, 'spec.pvc.resources'); @@ -15,28 +14,3 @@ export const getDataVolumeVolumeMode = (dataVolume: K8sResourceKind) => _.get(dataVolume, 'spec.pvc.volumeMode'); export const getDataVolumeStorageClassName = (dataVolume: K8sResourceKind): string => _.get(dataVolume, 'spec.pvc.storageClassName'); - -export const getDataVolumeSourceType = (dataVolume: K8sResourceKind) => { - const source = _.get(dataVolume, 'spec.source'); - if (_.get(source, 'http')) { - return { - type: DataVolumeSource.URL, - url: _.get(dataVolume, 'spec.source.http.url'), - }; - } - if (_.get(source, 'pvc')) { - return { - type: DataVolumeSource.PVC, - name: _.get(dataVolume, 'spec.source.pvc.name'), - namespace: _.get(dataVolume, 'spec.source.pvc.namespace'), - }; - } - if (_.get(source, 'blank')) { - return { - type: DataVolumeSource.BLANK, - }; - } - return { - type: DataVolumeSource.UNKNOWN, - }; -}; diff --git a/frontend/packages/kubevirt-plugin/src/selectors/vm-template/combined.ts b/frontend/packages/kubevirt-plugin/src/selectors/vm-template/combined.ts deleted file mode 100644 index 2ff81bf2d08..00000000000 --- a/frontend/packages/kubevirt-plugin/src/selectors/vm-template/combined.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { K8sResourceKind, TemplateKind } from '@console/internal/module/k8s'; -import { getName, getNamespace } from '@console/shared'; -import { - getDataVolumeTemplates, - getDisks, - getInterfaces, - getVolumeDataVolumeName, - getVolumes, -} from '../vm'; -import { ProvisionSource } from '../../types/vm'; -import { getDataVolumeSourceType } from '../dv/selectors'; -import { DataVolumeSource } from '../../types/dv'; -import { selectVM } from './selectors'; - -export const getTemplateProvisionSource = ( - template: TemplateKind, - dataVolumes: K8sResourceKind[], -): { type: ProvisionSource; source?: string; error?: string } => { - const vm = selectVM(template); - if (getInterfaces(vm).some((i) => i.bootOrder === 1)) { - return { - type: ProvisionSource.PXE, - }; - } - const bootDisk = getDisks(vm).find((disk) => disk.bootOrder === 1); - if (bootDisk) { - const bootVolume = getVolumes(vm).find((volume) => volume.name === bootDisk.name); - if (bootVolume && bootVolume.containerDisk) { - return { - type: ProvisionSource.CONTAINER, - source: bootVolume.containerDisk.image, - }; - } - if (bootVolume && bootVolume.dataVolume) { - let dataVolume = getDataVolumeTemplates(vm).find( - (dv) => getName(dv) === getVolumeDataVolumeName(bootVolume), - ); - if (!dataVolume) { - dataVolume = dataVolumes.find( - (d) => - getName(d) === getVolumeDataVolumeName(bootVolume) && - getNamespace(d) === getNamespace(template), - ); - } - if (dataVolume) { - const source = getDataVolumeSourceType(dataVolume); - switch (source.type) { - case DataVolumeSource.URL: - return { - type: ProvisionSource.URL, - source: source.url, - }; - case DataVolumeSource.PVC: - return { - type: ProvisionSource.CLONED_DISK, - source: `${source.namespace}/${source.name}`, - }; - case DataVolumeSource.BLANK: - return { - type: ProvisionSource.UNKNOWN, - error: `Datavolume ${bootVolume.dataVolume.name} does not have a supported source (${ - source.type - }).`, - }; - case DataVolumeSource.UNKNOWN: - default: - return { - type: ProvisionSource.UNKNOWN, - error: `Datavolume ${bootVolume.dataVolume.name} does not have a supported source.`, - }; - } - } else { - return { - type: ProvisionSource.UNKNOWN, - error: `Datavolume ${bootVolume.dataVolume.name} does not exist.`, - }; - } - } - } - return { - type: ProvisionSource.UNKNOWN, - error: 'No bootable device found.', - }; -}; - -export const getTemplateStorages = (template: TemplateKind, dataVolumes: K8sResourceKind[]) => { - const vm = selectVM(template); - - const volumes = getVolumes(vm); - const dataVolumeTemplates = getDataVolumeTemplates(vm); - return getDisks(vm).map((disk) => { - const volume = volumes.find((v) => v.name === disk.name); - const storage: any = { - disk, - volume, - }; - if (getVolumeDataVolumeName(volume)) { - storage.dataVolumeTemplate = dataVolumeTemplates.find( - (d) => getName(d) === getVolumeDataVolumeName(volume), - ); - storage.dataVolume = dataVolumes.find( - (d) => - getName(d) === getVolumeDataVolumeName(volume) && - getNamespace(d) === getNamespace(template), - ); - } - return storage; - }); -}; diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ObjectMeta.ts b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ObjectMeta.ts index c5ab19d5f05..4aaf4654f0c 100644 --- a/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ObjectMeta.ts +++ b/frontend/packages/kubevirt-plugin/src/types/vm/disk/V1ObjectMeta.ts @@ -25,7 +25,7 @@ export interface V1ObjectMeta { * @type {object} * @memberof V1ObjectMeta */ - annotations?: object; + annotations?: { [key: string]: string }; /** * The name of the cluster which the object belongs to. This is used to distinguish resources with same name and namespace in different clusters. This field is not set anywhere right now and apiserver is going to ignore it if set in create or update request. * @type {string} @@ -73,7 +73,7 @@ export interface V1ObjectMeta { * @type {object} * @memberof V1ObjectMeta */ - labels?: object; + labels?: { [key: string]: string }; /** * Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: http://kubernetes.io/docs/user-guide/identifiers#names * @type {string} diff --git a/frontend/packages/kubevirt-plugin/src/types/vm/index.ts b/frontend/packages/kubevirt-plugin/src/types/vm/index.ts index 0167b02723e..c9b9671f44b 100644 --- a/frontend/packages/kubevirt-plugin/src/types/vm/index.ts +++ b/frontend/packages/kubevirt-plugin/src/types/vm/index.ts @@ -92,12 +92,3 @@ export type V1Network = { pod?: {}; genie?: {}; }; - -export enum ProvisionSource { - PXE = 'PXE', - CONTAINER = 'Container', - URL = 'URL', - IMPORT = 'Import', - CLONED_DISK = 'Cloned Disk', // PVC or upload image to PVC - UNKNOWN = 'Unknown', -} diff --git a/frontend/packages/kubevirt-plugin/src/utils/immutable.ts b/frontend/packages/kubevirt-plugin/src/utils/immutable.ts index 53e6b63c691..6f94b021bc1 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/immutable.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/immutable.ts @@ -25,6 +25,8 @@ export const hasTruthyValue = (obj) => !!(obj && !!obj.find((value) => value)); export const iGet = (obj, key: string, defaultValue = undefined) => obj ? obj.get(key, defaultValue) : defaultValue; +export const toShallowJS = (obj, defaultValue = undefined) => (obj ? obj.toJSON() : defaultValue); + export const iGetIn = (obj, path: string[], defaultValue = undefined) => obj ? obj.getIn(path, defaultValue) : defaultValue; diff --git a/frontend/packages/kubevirt-plugin/src/utils/utils.ts b/frontend/packages/kubevirt-plugin/src/utils/utils.ts index 1de3edc4fc2..49e48070ddc 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/utils.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/utils.ts @@ -30,7 +30,12 @@ export const wrapWithProgress = (setProgress: (inProgress: boolean) => void) => promise: Promise, ) => { setProgress(true); - promise.then(() => setProgress(false)).catch(() => setProgress(false)); + promise + .then(() => setProgress(false)) + .catch((reason) => { + setProgress(false); + throw reason; + }); }; export const getVMLikeModelName = (isCreateTemplate: boolean) => diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/disk.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/disk.ts index 50bafa9bc3f..f382898a0d2 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/disk.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/disk.ts @@ -11,6 +11,7 @@ import { DataVolumeWrapper } from '../../../k8s/wrapper/vm/data-volume-wrapper'; import { ValidationErrorType, ValidationObject } from '../types'; import { POSITIVE_SIZE_ERROR } from '../strings'; import { StorageUISource } from '../../../components/modals/disk-modal/storage-ui-source'; +import { CombinedDisk } from '../../../k8s/wrapper/vm/combined-disk'; const validateDiskName = (name: string, usedDiskNames: Set): ValidationObject => { let validation = validateDNS1123SubdomainValue(name); @@ -67,22 +68,22 @@ export const validateDisk = ( } }; - const type = StorageUISource.fromTypes(volume.getType(), dataVolume && dataVolume.getType()); + const source = StorageUISource.fromTypes(volume.getType(), dataVolume && dataVolume.getType()); - if (type) { - if (type.requiresURL()) { + if (source) { + if (source.requiresURL()) { const url = dataVolume && dataVolume.getURL(); addRequired(url); validations.url = validateURL(url, { subject: 'URL' }); } - if (type.requiresContainerImage()) { + if (source.requiresContainerImage()) { const container = volume.getContainerImage(); addRequired(container); validations.container = validateTrim(container, { subject: 'Container' }); } - if (type.requiresDatavolume()) { + if (source.requiresDatavolume()) { addRequired(dataVolume); if (!dataVolume || !dataVolume.hasSize()) { addRequired(null); @@ -90,8 +91,12 @@ export const validateDisk = ( } } - if (type.requiresPVC()) { - const pvcName = type.getPVCName(volume, dataVolume); + if (source.requiresPVC()) { + const pvcName = new CombinedDisk({ + diskWrapper: disk, + volumeWrapper: volume, + dataVolumeWrapper: dataVolume, + }).getPVCName(source); addRequired(pvcName); validations.pvc = validatePVCName(pvcName, usedPVCNames); } diff --git a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/vm.ts b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/vm.ts index 46f85cb3bf0..8d3423feba8 100644 --- a/frontend/packages/kubevirt-plugin/src/utils/validations/vm/vm.ts +++ b/frontend/packages/kubevirt-plugin/src/utils/validations/vm/vm.ts @@ -1,12 +1,11 @@ -import { K8sResourceKind, TemplateKind } from '@console/internal/module/k8s'; +import { TemplateKind } from '@console/internal/module/k8s'; import { ValidationErrorType, ValidationObject } from '../types'; import { getValidationObject, validateDNS1123SubdomainValue, validateEntityAlreadyExists, } from '../common'; -import { getTemplateProvisionSource } from '../../../selectors/vm-template/combined'; -import { ProvisionSource } from '../../../types/vm'; +import { ProvisionSource } from '../../../constants/vm/provision-source'; export const validateVmLikeEntityName = ( value: string, @@ -28,11 +27,10 @@ export const validateVmLikeEntityName = ( export const validateUserTemplateProvisionSource = ( userTemplate: TemplateKind, - dataVolumes: K8sResourceKind[], ): ValidationObject => { - const provisionSource = getTemplateProvisionSource(userTemplate, dataVolumes); + const provisionSourceDetails = ProvisionSource.getProvisionSourceDetails(userTemplate); - return provisionSource.type === ProvisionSource.UNKNOWN - ? getValidationObject(`Could not select Provision Source. ${provisionSource.error}`) + return provisionSourceDetails.error + ? getValidationObject(`Could not select Provision Source. ${provisionSourceDetails.error}`) : null; }; From c1908fdbed777999f986fe31070061ea539bc19f Mon Sep 17 00:00:00 2001 From: suomiy Date: Mon, 7 Oct 2019 13:41:12 +0200 Subject: [PATCH 6/6] kubevirt: add Advanced Cloud-init Tab to CreateVMWizard - cloud init data is persisted inside the volume - fix disks editing - create InlineBooleanRadio component --- .../create-vm-wizard/create-vm-wizard.tsx | 74 +++- .../create-vm-wizard/redux/actions.ts | 7 +- .../cloud-init-tab-initial-state.ts | 11 + .../redux/initial-state/initial-state.ts | 2 + .../vm-settings-tab-initial-state.ts | 5 - .../redux/internal-actions.ts | 18 +- .../create-vm-wizard/redux/reducers.ts | 5 + .../prefill-vm-template-state-update.ts | 50 ++- .../create-vm-wizard/redux/types.ts | 5 +- .../create-vm-wizard/resource-load-errors.tsx | 2 +- .../selectors/immutable/cloud-init.ts | 15 + .../selectors/immutable/storage.ts | 3 + .../create-vm-wizard/strings/strings.ts | 1 + .../create-vm-wizard/strings/vm-settings.ts | 5 - .../tabs/cloud-init-tab/cloud-init-tab.scss | 16 + .../tabs/cloud-init-tab/cloud-init-tab.tsx | 339 ++++++++++++++++++ .../tabs/vm-settings-tab/vm-settings-tab.scss | 2 +- .../src/components/create-vm-wizard/types.ts | 10 +- .../utils/vm-settings-tab-utils.ts | 5 - .../src/components/errors/errors.tsx | 32 +- .../src/components/form/form-row.tsx | 2 +- .../components/inline-boolean-radio/index.ts | 1 + .../inline-boolean-radio.scss | 19 + .../inline-boolean-radio.tsx | 67 ++++ .../modals/clone-vm-modal/clone-vm-modal.tsx | 2 +- .../modals/disk-modal/disk-modal.tsx | 63 ++-- .../modals/disk-modal/storage-ui-source.ts | 9 +- .../src/components/vm-disks/disk-row.tsx | 2 +- .../src/constants/vm/constants.ts | 2 + .../object-with-type-property-wrapper.ts | 4 +- .../k8s/wrapper/vm/cloud-init-data-helper.tsx | 129 +++++++ .../src/k8s/wrapper/vm/volume-wrapper.ts | 29 +- .../kubevirt-plugin/src/utils/immutable.ts | 3 + .../src/utils/validations/vm/disk.ts | 11 +- 34 files changed, 836 insertions(+), 114 deletions(-) create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/cloud-init-tab-initial-state.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/cloud-init.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.scss create mode 100644 frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/components/inline-boolean-radio/index.ts create mode 100644 frontend/packages/kubevirt-plugin/src/components/inline-boolean-radio/inline-boolean-radio.scss create mode 100644 frontend/packages/kubevirt-plugin/src/components/inline-boolean-radio/inline-boolean-radio.tsx create mode 100644 frontend/packages/kubevirt-plugin/src/k8s/wrapper/vm/cloud-init-data-helper.tsx diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx index 716565e78c2..8d6c9ba32d2 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/create-vm-wizard.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { connect } from 'react-redux'; import { createVm, createVmTemplate } from 'kubevirt-web-ui-components'; -import { Wizard } from '@patternfly/react-core'; +import { Wizard, WizardStep } from '@patternfly/react-core'; import { TemplateModel } from '@console/internal/models'; import { Firehose, @@ -64,6 +64,7 @@ import { NetworkingTab } from './tabs/networking-tab/networking-tab'; import { ReviewTab } from './tabs/review-tab/review-tab'; import { ResultTab } from './tabs/result-tab/result-tab'; import { StorageTab } from './tabs/storage-tab/storage-tab'; +import { CloudInitTab } from './tabs/cloud-init-tab/cloud-init-tab'; import './create-vm-wizard.scss'; @@ -269,6 +270,20 @@ export class CreateVMWizardComponent extends React.Component ), }, + { + name: 'Advanced', + steps: [ + { + id: VMWizardTab.ADVANCED_CLOUD_INIT, + component: ( + <> + + + + ), + }, + ], + }, { id: VMWizardTab.REVIEW, component: , @@ -283,6 +298,44 @@ export class CreateVMWizardComponent extends React.Component isStepLocked(stepData, id)); + const calculateSteps = (initialSteps, initialAccumulator: WizardStep[] = []): WizardStep[] => + initialSteps.reduce((stepAcc: WizardStep[], step: any) => { + const isFirstStep = _.isEmpty(stepAcc); + let innerSteps; + if (step.steps) { + // pass reference to last step but remove it afterwards + innerSteps = calculateSteps(step.steps, isFirstStep ? [] : [_.last(stepAcc)]); + if (!isFirstStep) { + innerSteps.shift(); + } + } + let prevStep; + if (!isFirstStep) { + prevStep = _.last(stepAcc); + while (prevStep.steps) { + prevStep = _.last(prevStep.steps); + } + } + const isPrevStepValid = isFirstStep || isStepValid(stepData, prevStep.id as VMWizardTab); + const canJumpToPrevStep = isFirstStep || prevStep.canJumpTo; + + const calculatedStep = { + ...step, + name: TabTitleResolver[step.id] || step.name, + canJumpTo: isStepLocked(stepData, VMWizardTab.RESULT) // request finished + ? step.id === VMWizardTab.RESULT + : !isLocked && isPrevStepValid && canJumpToPrevStep && step.id !== VMWizardTab.RESULT, + component: step.component, + }; + + if (innerSteps) { + calculatedStep.steps = innerSteps; + } + + stepAcc.push(calculatedStep); + return stepAcc; + }, initialAccumulator); + return (
{!isStepValid(stepData, VMWizardTab.RESULT) && ( @@ -299,24 +352,7 @@ export class CreateVMWizardComponent extends React.Component { - const prevStep = stepAcc[idx - 1]; - const isPrevStepValid = idx === 0 ? true : isStepValid(stepData, prevStep.id); - const canJumpToPrevStep = idx === 0 ? true : prevStep.canJumpTo; - - stepAcc.push({ - ...step, - name: TabTitleResolver[step.id], - canJumpTo: isStepLocked(stepData, VMWizardTab.RESULT) // request finished - ? step.id === VMWizardTab.RESULT - : !isLocked && - isPrevStepValid && - canJumpToPrevStep && - step.id !== VMWizardTab.RESULT, - component: step.component, - }); - return stepAcc; - }, [])} + steps={calculateSteps(steps)} footer={} />
diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts index 5054819e810..7f81822d9eb 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/actions.ts @@ -8,6 +8,7 @@ import { VMWizardNetwork, VMWizardTab, VMWizardStorage, + CloudInitField, } from '../types'; import { DeviceType } from '../../../constants/vm'; import { cleanup, updateAndValidateState } from './utils'; @@ -64,10 +65,14 @@ export const vmWizardActions: VMWizardActions = { dispatch(vmWizardInternalActions[InternalActionType.Dispose](id)); }, - [ActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: string) => + [ActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: any) => withUpdateAndValidateState(id, (dispatch) => dispatch(vmWizardInternalActions[InternalActionType.SetVmSettingsFieldValue](id, key, value)), ), + [ActionType.SetCloudInitFieldValue]: (id, key: CloudInitField, value: any) => + withUpdateAndValidateState(id, (dispatch) => + dispatch(vmWizardInternalActions[InternalActionType.SetCloudInitFieldValue](id, key, value)), + ), [ActionType.UpdateCommonData]: (id, commonData: CommonData, changedProps: ChangedCommonData) => withUpdateAndValidateState( id, diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/cloud-init-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/cloud-init-tab-initial-state.ts new file mode 100644 index 00000000000..ee382ee15a9 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/cloud-init-tab-initial-state.ts @@ -0,0 +1,11 @@ +import { CloudInitField } from '../../types'; + +export const getCloudInitInitialState = () => ({ + value: { + [CloudInitField.IS_FORM]: { + value: true, + }, + }, + isValid: true, + hasAllRequiredFilled: true, +}); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts index 2d4017648a8..7a1c7ad17d1 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/initial-state.ts @@ -4,11 +4,13 @@ import { getNetworksInitialState } from './networks-tab-initial-state'; import { getStorageInitialState } from './storage-tab-initial-state'; import { getResultInitialState } from './result-tab-initial-state'; import { getReviewInitialState } from './review-tab-initial-state'; +import { getCloudInitInitialState } from './cloud-init-tab-initial-state'; const initialStateGetterResolver = { [VMWizardTab.VM_SETTINGS]: getVmSettingsInitialState, [VMWizardTab.NETWORKING]: getNetworksInitialState, [VMWizardTab.STORAGE]: getStorageInitialState, + [VMWizardTab.ADVANCED_CLOUD_INIT]: getCloudInitInitialState, [VMWizardTab.REVIEW]: getReviewInitialState, [VMWizardTab.RESULT]: getResultInitialState, }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts index b1a01b91687..368749c5340 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/initial-state/vm-settings-tab-initial-state.ts @@ -48,11 +48,6 @@ export const getInitialVmSettings = (common: CommonData) => { [VMSettingsField.START_VM]: { isHidden: asHidden(isCreateTemplate, VMWizardProps.isCreateTemplate), }, - [VMSettingsField.USE_CLOUD_INIT]: {}, - [VMSettingsField.USE_CLOUD_INIT_CUSTOM_SCRIPT]: {}, - [VMSettingsField.HOST_NAME]: {}, - [VMSettingsField.AUTHKEYS]: {}, - [VMSettingsField.CLOUD_INIT_CUSTOM_SCRIPT]: {}, }; Object.keys(fields).forEach((k) => { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts index 634f78d4990..bb89934b47c 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/internal-actions.ts @@ -1,4 +1,10 @@ -import { VMSettingsField, VMWizardNetwork, VMWizardStorage, VMWizardTab } from '../types'; +import { + CloudInitField, + VMSettingsField, + VMWizardNetwork, + VMWizardStorage, + VMWizardTab, +} from '../types'; import { DeviceType } from '../../../constants/vm'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { ActionBatch, InternalActionType, WizardInternalActionDispatcher } from './types'; @@ -56,7 +62,7 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.SetTabLocked, }), - [InternalActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: string) => ({ + [InternalActionType.SetVmSettingsFieldValue]: (id, key: VMSettingsField, value: any) => ({ payload: { id, key, @@ -64,6 +70,14 @@ export const vmWizardInternalActions: VMWizardInternalActions = { }, type: InternalActionType.SetVmSettingsFieldValue, }), + [InternalActionType.SetCloudInitFieldValue]: (id, key: CloudInitField, value: any) => ({ + payload: { + id, + key, + value, + }, + type: InternalActionType.SetCloudInitFieldValue, + }), [InternalActionType.UpdateVmSettingsField]: (id, key: VMSettingsField, value) => ({ payload: { id, diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts index 0c10b747653..ae2756dfa63 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/reducers.ts @@ -192,6 +192,11 @@ export default (state, action: WizardInternalAction) => { [dialogId, 'tabs', VMWizardTab.VM_SETTINGS, 'value', payload.key, 'value'], fromJS(payload.value), ); + case InternalActionType.SetCloudInitFieldValue: + return state.setIn( + [dialogId, 'tabs', VMWizardTab.ADVANCED_CLOUD_INIT, 'value', payload.key, 'value'], + fromJS(payload.value), + ); case InternalActionType.SetInVmSettings: return state.setIn( [dialogId, 'tabs', VMWizardTab.VM_SETTINGS, 'value', ...payload.path], diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts index 78942d7a330..4e344d814f2 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/stateUpdate/vmSettings/prefill-vm-template-state-update.ts @@ -2,6 +2,7 @@ import { createBasicLookup, getName } from '@console/shared/src'; import { InternalActionType, UpdateOptions } from '../../types'; import { iGetProvisionSource, iGetVmSettingValue } from '../../../selectors/immutable/vm-settings'; import { + CloudInitField, VMSettingsField, VMWizardNetwork, VMWizardNetworkType, @@ -19,10 +20,10 @@ import { DiskBus, DiskType, NetworkInterfaceModel, + VolumeType, } from '../../../../../constants/vm'; import { DEFAULT_CPU, - getCloudInitUserData, getCPU, getDataVolumeTemplates, getDisks, @@ -46,9 +47,10 @@ import { getNextIDResolver } from '../../../../../utils/utils'; import { ProvisionSource } from '../../../../../constants/vm/provision-source'; import { DiskWrapper } from '../../../../../k8s/wrapper/vm/disk-wrapper'; import { V1Volume } from '../../../../../types/vm/disk/V1Volume'; -import { VolumeWrapper } from '../../../../../k8s/wrapper/vm/volume-wrapper'; +import { MutableVolumeWrapper, VolumeWrapper } from '../../../../../k8s/wrapper/vm/volume-wrapper'; import { getProvisionSourceStorage } from '../../initial-state/storage-tab-initial-state'; -import { iGetStorages } from '../../../selectors/immutable/storage'; +import { CloudInitDataHelper } from '../../../../../k8s/wrapper/vm/cloud-init-data-helper'; +import { getStorages } from '../../../selectors/selectors'; export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptions) => { const state = getState(); @@ -62,6 +64,7 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio ? iUserTemplates.find((template) => iGetName(template) === userTemplateName) : null; + let isCloudInitForm = null; const vmSettingsUpdate = {}; // filter out oldTemplates @@ -70,13 +73,14 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio ); const getNextNetworkID = getNextIDResolver(networksUpdate); - const storagesUpdate = immutableListToShallowJS(iGetStorages(state, id)).filter( + const storagesUpdate = getStorages(state, id).filter( (storage) => ![ VMWizardStorageType.PROVISION_SOURCE_DISK, VMWizardStorageType.TEMPLATE, VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK, - ].includes(storage.type), + ].includes(storage.type) && + VolumeWrapper.initialize(storage.volume).getType() !== VolumeType.CLOUD_INIT_NO_CLOUD, ); const getNextStorageID = getNextIDResolver(storagesUpdate); @@ -106,16 +110,6 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio const [workload] = getTemplateWorkloadProfiles([userTemplate]); vmSettingsUpdate[VMSettingsField.WORKLOAD_PROFILE] = { value: workload }; - // update cloud-init - const cloudInitUserData = getCloudInitUserData(vm); - if (cloudInitUserData) { - vmSettingsUpdate[VMSettingsField.USE_CLOUD_INIT] = { value: true }; - vmSettingsUpdate[VMSettingsField.USE_CLOUD_INIT_CUSTOM_SCRIPT] = { value: true }; - vmSettingsUpdate[VMSettingsField.CLOUD_INIT_CUSTOM_SCRIPT] = { - value: cloudInitUserData || '', - }; - } - // update provision source const provisionSourceDetails = ProvisionSource.getProvisionSourceDetails(userTemplate); vmSettingsUpdate[VMSettingsField.PROVISION_SOURCE_TYPE] = { @@ -149,8 +143,23 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio // // prefill storage const templateStorages: VMWizardStorage[] = getDisks(vm).map((disk) => { const diskWrapper = DiskWrapper.initialize(disk); - const volume = volumeLookup[diskWrapper.getName()]; + let volume = volumeLookup[diskWrapper.getName()]; const volumeWrapper = VolumeWrapper.initialize(volume); + + if (volumeWrapper.getType() === VolumeType.CLOUD_INIT_NO_CLOUD) { + const helper = new CloudInitDataHelper(volumeWrapper.getCloudInitNoCloud()); + if (helper.includesOnlyFormValues()) { + isCloudInitForm = true; + helper.makeFormCompliant(); + volume = new MutableVolumeWrapper(volume, { copy: true }) + .replaceTypeData(helper.asCloudInitNoCloudSource()) + .asMutableResource(); + // do not overwrite with more cloud-init disks + } else if (isCloudInitForm == null) { + isCloudInitForm = false; + } + } + return { id: getNextStorageID(), type: diskWrapper.isFirstBootableDevice() @@ -192,4 +201,13 @@ export const prefillVmTemplateUpdater = ({ id, dispatch, getState }: UpdateOptio dispatch(vmWizardInternalActions[InternalActionType.UpdateVmSettings](id, vmSettingsUpdate)); dispatch(vmWizardInternalActions[InternalActionType.SetNetworks](id, networksUpdate)); dispatch(vmWizardInternalActions[InternalActionType.SetStorages](id, storagesUpdate)); + if (isCloudInitForm != null) { + dispatch( + vmWizardInternalActions[InternalActionType.SetCloudInitFieldValue]( + id, + CloudInitField.IS_FORM, + isCloudInitForm, + ), + ); + } }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts index 6a4a210c682..41fa590f14b 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/redux/types.ts @@ -1,6 +1,7 @@ import { ChangedCommonData, ChangedCommonDataProp, + CloudInitField, VMSettingsField, VMSettingsFieldType, VMWizardNetwork, @@ -15,6 +16,7 @@ export enum ActionType { Dispose = 'KubevirtVMWiExternalDispose', UpdateCommonData = 'KubevirtVMWizardExternalUpdateCommonData', SetVmSettingsFieldValue = 'KubevirtVMWizardExternalSetVmSettingsFieldValue', + SetCloudInitFieldValue = 'KubevirtVMWizardExternalSetCloudInitFieldValue', SetTabLocked = 'KubevirtVMWizardExternalSetTabLocked', RemoveNIC = 'KubevirtVMWizardExternalRemoveNIC', UpdateNIC = 'KubevirtVMWizardExternalUpdateNIC', @@ -33,6 +35,7 @@ export enum InternalActionType { SetTabValidity = 'KubevirtVMWizardSetTabValidity', SetTabLocked = 'KubevirtVMWizardSetTabLocked', SetVmSettingsFieldValue = 'KubevirtVMWizardSetVmSettingsFieldValue', + SetCloudInitFieldValue = 'KubevirtVMWizardSetCloudInitFieldValue', SetInVmSettings = 'KubevirtVMWizardSetInVmSettings', SetInVmSettingsBatch = 'KubevirtVMWizardSetInVmSettingsBatch', UpdateVmSettingsField = 'KubevirtVMWizardUpdateVmSettingsField', @@ -57,7 +60,7 @@ export type WizardInternalAction = { isPending?: boolean; hasAllRequiredFilled?: boolean; path?: string[]; - key?: VMSettingsField; + key?: VMSettingsField | CloudInitField; tab?: VMWizardTab; batch?: ActionBatch; network?: VMWizardNetwork; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx index 14b2b61c712..4b35143cbdd 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/resource-load-errors.tsx @@ -23,7 +23,7 @@ const stateToProps = (state, { wizardReduxID }) => ({ asError(state, wizardReduxID, VMWizardProps.commonTemplates), asError(state, wizardReduxID, VMWizardProps.userTemplates), asError(state, wizardReduxID, VMWizardProps.virtualMachines, AlertVariant.warning), // for validation only - ], + ].filter((err) => err && err.message), }); export const ResourceLoadErrors = connect(stateToProps)(Errors); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/cloud-init.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/cloud-init.ts new file mode 100644 index 00000000000..d6b307cfe18 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/cloud-init.ts @@ -0,0 +1,15 @@ +import { iGetIn } from '../../../../utils/immutable'; +import { CloudInitField, VMWizardTab } from '../../types'; +import { iGetCreateVMWizardTabs } from './selectors'; + +export const iGetCloudInitValue = ( + state, + id: string, + key: CloudInitField, + defaultValue = undefined, +) => + iGetIn( + iGetCreateVMWizardTabs(state, id), + [VMWizardTab.ADVANCED_CLOUD_INIT, 'value', key, 'value'], + defaultValue, + ); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts index 777143507b9..0389c70ffe4 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/selectors/immutable/storage.ts @@ -12,3 +12,6 @@ export const iGetProvisionSourceStorage = (state, id: string) => VMWizardStorageType.PROVISION_SOURCE_TEMPLATE_DISK, ].includes(iGet(storage, 'type')), ); + +export const iGetCloudInitNoCloudStorage = (state, id: string) => + iGetStorages(state, id).find((storage) => iGetIn(storage, ['volume', 'cloudInitNoCloud'])); diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts index 61617689e91..72e92c83dcc 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/strings.ts @@ -13,6 +13,7 @@ export const TabTitleResolver = { [VMWizardTab.VM_SETTINGS]: 'General', [VMWizardTab.NETWORKING]: 'Networking', [VMWizardTab.STORAGE]: 'Storage', + [VMWizardTab.ADVANCED_CLOUD_INIT]: 'Cloud-init', [VMWizardTab.REVIEW]: 'Review', [VMWizardTab.RESULT]: 'Result', }; diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts index 257e77256c4..5671c5f0b5d 100644 --- a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/strings/vm-settings.ts @@ -15,11 +15,6 @@ export const titleResolver: VMSettingsRenderableFieldResolver = { [VMSettingsField.CPU]: 'CPUs', [VMSettingsField.WORKLOAD_PROFILE]: 'Workload Profile', [VMSettingsField.START_VM]: 'Start virtual machine on creation', - [VMSettingsField.USE_CLOUD_INIT]: 'Use cloud-init', - [VMSettingsField.USE_CLOUD_INIT_CUSTOM_SCRIPT]: 'Use custom script', - [VMSettingsField.HOST_NAME]: 'Hostname', - [VMSettingsField.AUTHKEYS]: 'Authenticated SSH Keys', - [VMSettingsField.CLOUD_INIT_CUSTOM_SCRIPT]: 'Custom Script', }; export const placeholderResolver = { diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.scss b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.scss new file mode 100644 index 00000000000..f4fbc62b330 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.scss @@ -0,0 +1,16 @@ +.kubevirt-create-vm-modal__cloud-init-base64 > input[type="checkbox"] { + margin: -0.1em 0 0 !important; +} + +.kubevirt-create-vm-modal__cloud-init-custom-script-text-area { + height: 50vh !important; + resize: vertical; +} + +.kubevirt-create-vm-modal__cloud-init-ssh-keys-text-area { + resize: vertical; +} + +.kubevirt-create-vm-modal__cloud-init-ssh-keys-row { + padding-bottom: 1rem; +} diff --git a/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.tsx b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.tsx new file mode 100644 index 00000000000..c3b9185af75 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/src/components/create-vm-wizard/tabs/cloud-init-tab/cloud-init-tab.tsx @@ -0,0 +1,339 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { + AlertVariant, + Button, + ButtonVariant, + Checkbox, + Form, + Split, + SplitItem, + TextArea, + TextInput, +} from '@patternfly/react-core'; +import { MinusCircleIcon, PlusCircleIcon } from '@patternfly/react-icons'; +import { confirmModal } from '@console/internal/components/modals'; +import { CloudInitField, VMWizardStorage, VMWizardStorageType } from '../../types'; +import { vmWizardActions } from '../../redux/actions'; +import { ActionType } from '../../redux/types'; +import { iGetCloudInitNoCloudStorage } from '../../selectors/immutable/storage'; +import { MutableVolumeWrapper, VolumeWrapper } from '../../../../k8s/wrapper/vm/volume-wrapper'; +import { iGet, iGetIn, ihasIn, toJS, toShallowJS } from '../../../../utils/immutable'; +import { DiskBus, DiskType, VolumeType } from '../../../../constants/vm/storage'; +import { FormRow } from '../../../form/form-row'; +import { joinIDs, prefixedID } from '../../../../utils'; +import { Errors } from '../../../errors/errors'; +import { CLOUDINIT_DISK } from '../../../../constants/vm'; +import { DiskWrapper } from '../../../../k8s/wrapper/vm/disk-wrapper'; +import { InlineBooleanRadio } from '../../../inline-boolean-radio'; +import { iGetCloudInitValue } from '../../selectors/immutable/cloud-init'; +import { + CloudInitDataFormKeys, + CloudInitDataHelper, +} from '../../../../k8s/wrapper/vm/cloud-init-data-helper'; + +import './cloud-init-tab.scss'; + +type CustomScriptProps = { + id: string; + isDisabled?: boolean; + value: string; + onChange: (value: string) => void; +}; + +const CustomScript: React.FC = ({ id, isDisabled, value, onChange }) => ( + +