From e80c61c4f11928b3105dd353f9a4633283d2de51 Mon Sep 17 00:00:00 2001 From: Phillip Rhodes Date: Thu, 19 Dec 2024 08:39:14 -0500 Subject: [PATCH] Add registry credentials to the template wizard flow --- .../DiskModal/ContainerDiskModal.tsx | 6 +- src/utils/components/DiskModal/DiskModal.tsx | 25 +++-- .../DiskModal/RegistryDiskModal.tsx | 73 +++++++------ .../DiskSourceContainer.tsx | 102 ++++++++++++++---- .../DiskSourceSelect/utils/constants.ts | 2 + .../DiskModal/components/utils/constants.ts | 4 + src/utils/components/DiskModal/utils/form.ts | 8 +- src/utils/components/DiskModal/utils/types.ts | 2 + src/utils/resources/secret/utils.ts | 9 +- src/utils/resources/template/utils/helpers.ts | 5 +- src/utils/resources/vm/utils/selectors.ts | 19 ++++ src/utils/resources/vm/utils/source.ts | 10 +- .../Sources/ContainerSource.tsx | 10 +- .../hooks/useCreateDrawerForm.tsx | 17 ++- .../utils/WizardVMContext/utils/tabs-data.ts | 2 + .../useRegistryCredentials.ts | 35 ++++++ .../useRegistryCredentials/utils/types.ts | 1 + .../useRegistryCredentials/utils/utils.ts | 14 +++ src/views/catalog/utils/useWizardVmCreate.ts | 22 +++- .../wizard/components/WizardFooter.tsx | 2 +- .../DeleteVMModal/DeleteVMModal.tsx | 5 +- .../hooks/useDeleteVMResources.ts | 22 +++- 22 files changed, 311 insertions(+), 84 deletions(-) create mode 100644 src/views/catalog/utils/useRegistryCredentials/useRegistryCredentials.ts create mode 100644 src/views/catalog/utils/useRegistryCredentials/utils/types.ts create mode 100644 src/views/catalog/utils/useRegistryCredentials/utils/utils.ts diff --git a/src/utils/components/DiskModal/ContainerDiskModal.tsx b/src/utils/components/DiskModal/ContainerDiskModal.tsx index 06f1514bb..475102201 100644 --- a/src/utils/components/DiskModal/ContainerDiskModal.tsx +++ b/src/utils/components/DiskModal/ContainerDiskModal.tsx @@ -61,7 +61,11 @@ const ContainerDiskModal: FC = ({
- + diff --git a/src/utils/components/DiskModal/DiskModal.tsx b/src/utils/components/DiskModal/DiskModal.tsx index c947d535e..ade75210a 100644 --- a/src/utils/components/DiskModal/DiskModal.tsx +++ b/src/utils/components/DiskModal/DiskModal.tsx @@ -1,5 +1,6 @@ import React, { FC } from 'react'; +import { WizardVMContextProvider } from '@catalog/utils/WizardVMContext'; import { getName, getNamespace } from '@kubevirt-utils/resources/shared'; import { getDataVolumeTemplates, getVolumes } from '@kubevirt-utils/resources/vm'; import { isEmpty } from '@kubevirt-utils/utils/utils'; @@ -32,17 +33,19 @@ const DiskModal: FC = ({ const Modal = modalsBySource[createDiskSource || editDiskSource]; return ( - + + + ); }; diff --git a/src/utils/components/DiskModal/RegistryDiskModal.tsx b/src/utils/components/DiskModal/RegistryDiskModal.tsx index ce25ccd12..d796f3bb0 100644 --- a/src/utils/components/DiskModal/RegistryDiskModal.tsx +++ b/src/utils/components/DiskModal/RegistryDiskModal.tsx @@ -1,66 +1,75 @@ import React, { FC } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; +import useRegistryCredentials from '@catalog/utils/useRegistryCredentials/useRegistryCredentials'; +import AdvancedSettings from '@kubevirt-utils/components/DiskModal/components/AdvancedSettings/AdvancedSettings'; +import BootSourceCheckbox from '@kubevirt-utils/components/DiskModal/components/BootSourceCheckbox/BootSourceCheckbox'; +import DiskInterfaceSelect from '@kubevirt-utils/components/DiskModal/components/DiskInterfaceSelect/DiskInterfaceSelect'; +import DiskNameInput from '@kubevirt-utils/components/DiskModal/components/DiskNameInput/DiskNameInput'; +import DiskSizeInput from '@kubevirt-utils/components/DiskModal/components/DiskSizeInput/DiskSizeInput'; +import DiskSourceContainer from '@kubevirt-utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceContainer/DiskSourceContainer'; +import DiskTypeSelect from '@kubevirt-utils/components/DiskModal/components/DiskTypeSelect/DiskTypeSelect'; +import PendingChanges from '@kubevirt-utils/components/DiskModal/components/PendingChanges'; +import StorageClassAndPreallocation from '@kubevirt-utils/components/DiskModal/components/StorageClassAndPreallocation/StorageClassAndPreallocation'; +import { + REGISTRY_CREDENTIALS_FIELD, + REGISTRYURL_DATAVOLUME_FIELD, +} from '@kubevirt-utils/components/DiskModal/components/utils/constants'; +import { diskModalTitle, getOS } from '@kubevirt-utils/components/DiskModal/utils/helpers'; +import { submit } from '@kubevirt-utils/components/DiskModal/utils/submit'; +import TabModal from '@kubevirt-utils/components/TabModal/TabModal'; import { getNamespace } from '@kubevirt-utils/resources/shared'; import { isEmpty } from '@kubevirt-utils/utils/utils'; import { Form } from '@patternfly/react-core'; import { isRunning } from '@virtualmachines/utils'; -import TabModal from '../TabModal/TabModal'; - -import AdvancedSettings from './components/AdvancedSettings/AdvancedSettings'; -import BootSourceCheckbox from './components/BootSourceCheckbox/BootSourceCheckbox'; -import DiskInterfaceSelect from './components/DiskInterfaceSelect/DiskInterfaceSelect'; -import DiskNameInput from './components/DiskNameInput/DiskNameInput'; -import DiskSizeInput from './components/DiskSizeInput/DiskSizeInput'; -import DiskSourceContainer from './components/DiskSourceSelect/components/DiskSourceContainer/DiskSourceContainer'; -import DiskTypeSelect from './components/DiskTypeSelect/DiskTypeSelect'; -import PendingChanges from './components/PendingChanges'; -import StorageClassAndPreallocation from './components/StorageClassAndPreallocation/StorageClassAndPreallocation'; -import { REGISTRYURL_DATAVOLUME_FIELD } from './components/utils/constants'; import { getDefaultCreateValues, getDefaultEditValues } from './utils/form'; -import { diskModalTitle, getOS } from './utils/helpers'; -import { submit } from './utils/submit'; import { SourceTypes, V1DiskFormState, V1SubDiskModalProps } from './utils/types'; -const RegistryDiskModal: FC = ({ - editDiskName, - isCreated, - isOpen, - onClose, - onSubmit, - pvc, - vm, -}) => { - const os = getOS(vm); - const isVMRunning = isRunning(vm); +const RegistryDiskModal: FC = (props) => { + const { editDiskName, isCreated, isOpen, onClose, onSubmit, pvc, vm } = props; + const { decodedRegistryCredentials, updateRegistryCredentials } = useRegistryCredentials(); const isEditDisk = !isEmpty(editDiskName); - const namespace = getNamespace(vm); + + const defaultValues = isEditDisk + ? getDefaultEditValues(vm, editDiskName, decodedRegistryCredentials) + : getDefaultCreateValues(vm, SourceTypes.REGISTRY); const methods = useForm({ - defaultValues: isEditDisk - ? getDefaultEditValues(vm, editDiskName) - : getDefaultCreateValues(vm, SourceTypes.REGISTRY), + defaultValues, mode: 'all', }); const { formState: { isSubmitting, isValid }, handleSubmit, + watch, } = methods; + const formRegistryCredentials = watch(REGISTRY_CREDENTIALS_FIELD); + const { password, username } = formRegistryCredentials; + const credentialsValid = (username && password) || (!username && !password); + + const handleSubmitForm = () => { + updateRegistryCredentials(formRegistryCredentials); + return handleSubmit(async (data) => submit({ data, editDiskName, onSubmit, pvc, vm }))(); + }; + + const os = getOS(vm); + const isVMRunning = isRunning(vm); + const namespace = getNamespace(vm); + return ( - handleSubmit(async (data) => submit({ data, editDiskName, onSubmit, pvc, vm }))() - } closeOnSubmit={isValid} headerText={diskModalTitle(isEditDisk, isVMRunning)} + isDisabled={!credentialsValid} isLoading={isSubmitting} isOpen={isOpen} onClose={onClose} + onSubmit={handleSubmitForm} > diff --git a/src/utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceContainer/DiskSourceContainer.tsx b/src/utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceContainer/DiskSourceContainer.tsx index 3b9f91f48..be9fd1220 100644 --- a/src/utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceContainer/DiskSourceContainer.tsx +++ b/src/utils/components/DiskModal/components/DiskSourceSelect/components/DiskSourceContainer/DiskSourceContainer.tsx @@ -3,49 +3,109 @@ import { FieldPath, useFormContext } from 'react-hook-form'; import { V1DiskFormState } from '@kubevirt-utils/components/DiskModal/utils/types'; import FormGroupHelperText from '@kubevirt-utils/components/FormGroupHelperText/FormGroupHelperText'; +import { FormPasswordInput } from '@kubevirt-utils/components/FormPasswordInput/FormPasswordInput'; +import { FormTextInput } from '@kubevirt-utils/components/FormTextInput/FormTextInput'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import { OS_NAME_TYPES } from '@kubevirt-utils/resources/template'; import { isUpstream } from '@kubevirt-utils/utils/utils'; import { FormGroup, TextInput, ValidatedOptions } from '@patternfly/react-core'; -import { diskSourceEphemeralFieldID } from '../../utils/constants'; +import { REGISTRY_PASSWORD_FIELD, REGISTRY_USERNAME_FIELD } from '../../../utils/constants'; +import { + diskSourceEphemeralFieldID, + diskSourcePasswordFieldID, + diskSourceUsernameFieldID, +} from '../../utils/constants'; import { OS_REGISTERY_LINKS } from './utils/constants'; type DiskSourceUrlInputProps = { fieldName: FieldPath; + isEphemeralDiskSource?: boolean; os: string; }; -const DiskSourceContainer: FC = ({ fieldName, os }) => { +const DiskSourceContainer: FC = ({ + fieldName, + isEphemeralDiskSource = false, + os, +}) => { const { t } = useKubevirtTranslation(); - const { getFieldState, register } = useFormContext(); + const { + formState: { errors }, + getFieldState, + register, + } = useFormContext(); + const isRHELOS = os?.includes(OS_NAME_TYPES.rhel); - // we show feodra on upstream and rhel on downstream, and default as fedora if not exists. + // we show fedora on upstream and rhel on downstream, and default as fedora if not exists. const exampleURL = isRHELOS && isUpstream ? OS_REGISTERY_LINKS.fedora : OS_REGISTERY_LINKS[os] || OS_REGISTERY_LINKS.fedora; const { error } = getFieldState(fieldName); + return ( - - - - {error ? ( - error?.message - ) : ( - <> - {t('Example: ')} - {exampleURL} - - )} - - + <> + + + + {error ? ( + error?.message + ) : ( + <> + {t('Example: ')} + {exampleURL} + + )} + + + {!isEphemeralDiskSource && ( + <> + + + + + + + + )} + ); }; diff --git a/src/utils/components/DiskModal/components/DiskSourceSelect/utils/constants.ts b/src/utils/components/DiskModal/components/DiskSourceSelect/utils/constants.ts index 9ef88543c..c2912fd44 100644 --- a/src/utils/components/DiskModal/components/DiskSourceSelect/utils/constants.ts +++ b/src/utils/components/DiskModal/components/DiskSourceSelect/utils/constants.ts @@ -12,6 +12,8 @@ export const diskSourceURLFieldID = 'disk-source-url'; export const ephemeralDiskSizeFieldID = 'ephemeral-disk-size'; export const diskSourceFieldID = 'disk-source'; export const diskSourceEphemeralFieldID = 'disk-source-container'; +export const diskSourceUsernameFieldID = 'disk-source-username'; +export const diskSourcePasswordFieldID = 'disk-source-password'; export const optionLabelMapper: { [key in SourceTypes]: string } = { [SourceTypes.BLANK]: t('Empty disk (blank)'), diff --git a/src/utils/components/DiskModal/components/utils/constants.ts b/src/utils/components/DiskModal/components/utils/constants.ts index 13c8ba155..456a43874 100644 --- a/src/utils/components/DiskModal/components/utils/constants.ts +++ b/src/utils/components/DiskModal/components/utils/constants.ts @@ -39,3 +39,7 @@ export const CONTAINERDISK_IMAGE_FIELD = 'volume.containerDisk.image'; export const PVC_CLAIMNAME_FIELD = 'volume.persistentVolumeClaim.claimName'; export const REGISTRYURL_DATAVOLUME_FIELD = 'dataVolumeTemplate.spec.source.registry.url'; + +export const REGISTRY_CREDENTIALS_FIELD = 'registryCredentials'; +export const REGISTRY_USERNAME_FIELD = 'registryCredentials.username'; +export const REGISTRY_PASSWORD_FIELD = 'registryCredentials.password'; diff --git a/src/utils/components/DiskModal/utils/form.ts b/src/utils/components/DiskModal/utils/form.ts index 445069198..dc0c2b802 100644 --- a/src/utils/components/DiskModal/utils/form.ts +++ b/src/utils/components/DiskModal/utils/form.ts @@ -1,3 +1,4 @@ +import { RegistryCredentials } from '@catalog/utils/useRegistryCredentials/utils/types'; import DataSourceModel from '@kubevirt-ui/kubevirt-api/console/models/DataSourceModel'; import { V1DataVolumeTemplateSpec, @@ -46,7 +47,11 @@ const createInitialStateFromSource: Record< (dataVolumeTemplate.spec.source.snapshot = { name: '', namespace: '' }), }; -export const getDefaultEditValues = (vm: V1VirtualMachine, editDiskName?: string) => { +export const getDefaultEditValues = ( + vm: V1VirtualMachine, + editDiskName?: string, + registryCredentials?: RegistryCredentials, +) => { const isBootSource = getBootDisk(vm)?.name === editDiskName; let diskToEdit = getDisks(vm)?.find((disk) => disk.name === editDiskName); const volumeToEdit = getVolumes(vm)?.find((volume) => volume.name === editDiskName); @@ -60,6 +65,7 @@ export const getDefaultEditValues = (vm: V1VirtualMachine, editDiskName?: string dataVolumeTemplate, disk: diskToEdit, isBootSource, + registryCredentials, volume: volumeToEdit, }; }; diff --git a/src/utils/components/DiskModal/utils/types.ts b/src/utils/components/DiskModal/utils/types.ts index 3765fe7f1..bb91184a3 100644 --- a/src/utils/components/DiskModal/utils/types.ts +++ b/src/utils/components/DiskModal/utils/types.ts @@ -1,3 +1,4 @@ +import { RegistryCredentials } from '@catalog/utils/useRegistryCredentials/utils/types'; import { V1beta1DataVolume } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models'; import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes'; import { @@ -58,6 +59,7 @@ export type V1DiskFormState = { disk: V1Disk; expandPVCSize?: string; isBootSource: boolean; + registryCredentials?: RegistryCredentials; storageClassProvisioner?: string; storageProfileSettingsApplied?: boolean; uploadFile?: { file: File; filename: string }; diff --git a/src/utils/resources/secret/utils.ts b/src/utils/resources/secret/utils.ts index 951fcc0ab..e1fc0c8c2 100644 --- a/src/utils/resources/secret/utils.ts +++ b/src/utils/resources/secret/utils.ts @@ -8,7 +8,7 @@ import { } from '@kubevirt-utils/components/SSHSecretModal/utils/types'; import { SecretModel } from '@kubevirt-utils/models'; import { isEmpty } from '@kubevirt-utils/utils/utils'; -import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk'; +import { k8sCreate, k8sDelete } from '@openshift-console/dynamic-plugin-sdk'; import { getName } from '../shared'; @@ -94,3 +94,10 @@ export const createSecret: CreateSecretType = ({ namespace, password, secretName model: SecretModel, ns: namespace, }); + +export const deleteSecret = (secret: IoK8sApiCoreV1Secret) => + secret && + k8sDelete({ + model: SecretModel, + resource: secret, + }); diff --git a/src/utils/resources/template/utils/helpers.ts b/src/utils/resources/template/utils/helpers.ts index c9a0e04fe..0c6fb1a40 100644 --- a/src/utils/resources/template/utils/helpers.ts +++ b/src/utils/resources/template/utils/helpers.ts @@ -4,7 +4,7 @@ import { TemplateParameter, V1Template } from '@kubevirt-ui/kubevirt-api/console import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel'; import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; import { getAnnotation } from '@kubevirt-utils/resources/shared'; -import { getRootDataVolumeTemplateSpec } from '@kubevirt-utils/resources/vm'; +import { vmBootDiskSourceIsRegistry } from '@kubevirt-utils/resources/vm/utils/source'; import { generatePrettyName } from '@kubevirt-utils/utils/utils'; import { ANNOTATIONS } from './annotations'; @@ -74,6 +74,5 @@ export const generateParamsWithPrettyName = (template: V1Template) => { export const bootDiskSourceIsRegistry = (template: V1Template) => { const vmObject: V1VirtualMachine = getTemplateVirtualMachineObject(template); - const rootDataVolumeTemplateSpec = getRootDataVolumeTemplateSpec(vmObject); - return Boolean(rootDataVolumeTemplateSpec?.spec?.source?.registry); + return vmBootDiskSourceIsRegistry(vmObject); }; diff --git a/src/utils/resources/vm/utils/selectors.ts b/src/utils/resources/vm/utils/selectors.ts index 13df70c70..1f2239b00 100644 --- a/src/utils/resources/vm/utils/selectors.ts +++ b/src/utils/resources/vm/utils/selectors.ts @@ -11,6 +11,7 @@ import { V1PreferenceMatcher, V1VirtualMachine, V1VirtualMachineCondition, + V1Volume, } from '@kubevirt-ui/kubevirt-api/kubevirt'; import { DYNAMIC_CREDENTIALS_SUPPORT } from '@kubevirt-utils/components/DynamicSSHKeyInjection/constants/constants'; import { ROOTDISK } from '@kubevirt-utils/constants/constants'; @@ -85,12 +86,30 @@ export const getDataVolumeTemplates = (vm: V1VirtualMachine) => vm?.spec?.dataVo * @param {V1VirtualMachine} vm the virtual machine * @returns the virtual machine's root data volume */ +export const getRootDataVolume = (vm: V1VirtualMachine): V1Volume => + getVolumes(vm)?.find((v) => v.name === ROOTDISK); + +/** + * A selector for the virtual machine's root data volume template spec + * @param {V1VirtualMachine} vm the virtual machine + * @returns the virtual machine's root data volume template spec + */ export const getRootDataVolumeTemplateSpec = (vm: V1VirtualMachine): V1DataVolumeTemplateSpec => { const volume = getVolumes(vm)?.find((v) => v.name === ROOTDISK); return vm.spec.dataVolumeTemplates?.find((dv) => dv.metadata.name === volume?.dataVolume?.name); }; +/** + * A selector for the virtual machine's root data volume secretRef + * @param {V1VirtualMachine} vm + * @returns the virtual machine's root data volume secretRef + */ +export const getRootDiskSecretRef = (vm: V1VirtualMachine): string => { + const dataVolumeTemplateSpec = getRootDataVolumeTemplateSpec(vm); + return dataVolumeTemplateSpec?.spec?.source?.registry?.secretRef; +}; + /** * A selector for the virtual machine's config maps * @param {V1VirtualMachine} vm the virtual machine diff --git a/src/utils/resources/vm/utils/source.ts b/src/utils/resources/vm/utils/source.ts index fa4f69619..b147bb181 100644 --- a/src/utils/resources/vm/utils/source.ts +++ b/src/utils/resources/vm/utils/source.ts @@ -14,7 +14,10 @@ import { DATA_SOURCE_CRONJOB_LABEL, } from '@kubevirt-utils/resources/template'; import { TemplateBootSource } from '@kubevirt-utils/resources/template/hooks/useVmTemplateSource/utils'; -import { getDataVolumeTemplates } from '@kubevirt-utils/resources/vm'; +import { + getDataVolumeTemplates, + getRootDataVolumeTemplateSpec, +} from '@kubevirt-utils/resources/vm'; import { getBootDisk, getVolumes } from './selectors'; @@ -133,3 +136,8 @@ export const getVMBootSourceLabel = ( return BOOT_SOURCE_LABELS[bootSourceType] || 'N/A'; }; + +export const vmBootDiskSourceIsRegistry = (vm: V1VirtualMachine) => { + const rootDataVolumeTemplateSpec = getRootDataVolumeTemplateSpec(vm); + return Boolean(rootDataVolumeTemplateSpec?.spec?.source?.registry); +}; diff --git a/src/views/catalog/templatescatalog/components/TemplatesCatalogDrawer/StorageSection/CustomizeSource/Sources/ContainerSource.tsx b/src/views/catalog/templatescatalog/components/TemplatesCatalogDrawer/StorageSection/CustomizeSource/Sources/ContainerSource.tsx index 42968cf8a..c0b9721db 100644 --- a/src/views/catalog/templatescatalog/components/TemplatesCatalogDrawer/StorageSection/CustomizeSource/Sources/ContainerSource.tsx +++ b/src/views/catalog/templatescatalog/components/TemplatesCatalogDrawer/StorageSection/CustomizeSource/Sources/ContainerSource.tsx @@ -68,12 +68,14 @@ const ContainerSource: FC = ({ > handleCredentialsChange(e, 'username')} type="text" - validated={validated} /> = ({ label={t('Password')} > handleCredentialsChange(e, 'password')} type="text" - validated={validated} /> diff --git a/src/views/catalog/templatescatalog/components/TemplatesCatalogDrawer/hooks/useCreateDrawerForm.tsx b/src/views/catalog/templatescatalog/components/TemplatesCatalogDrawer/hooks/useCreateDrawerForm.tsx index 2eef20029..f56bff2ba 100644 --- a/src/views/catalog/templatescatalog/components/TemplatesCatalogDrawer/hooks/useCreateDrawerForm.tsx +++ b/src/views/catalog/templatescatalog/components/TemplatesCatalogDrawer/hooks/useCreateDrawerForm.tsx @@ -36,7 +36,7 @@ import { DISABLED_GUEST_SYSTEM_LOGS_ACCESS } from '@kubevirt-utils/hooks/useFeat import { useFeatures } from '@kubevirt-utils/hooks/useFeatures/useFeatures'; import useKubevirtUserSettings from '@kubevirt-utils/hooks/useKubevirtUserSettings/useKubevirtUserSettings'; import { RHELAutomaticSubscriptionData } from '@kubevirt-utils/hooks/useRHELAutomaticSubscription/utils/types'; -import { createSecret } from '@kubevirt-utils/resources/secret/utils'; +import { createSecret, encodeSecretKey } from '@kubevirt-utils/resources/secret/utils'; import { getAnnotation, getLabel, getName, getResourceUrl } from '@kubevirt-utils/resources/shared'; import { ANNOTATIONS, @@ -53,9 +53,9 @@ import { } from '@kubevirt-utils/utils/headless-service'; import { addRandomSuffix, ensurePath, isEmpty } from '@kubevirt-utils/utils/utils'; import { k8sCreate, useAccessReview, useK8sModels } from '@openshift-console/dynamic-plugin-sdk'; +import { getLabels } from '@overview/OverviewTab/inventory-card/utils/flattenTemplates'; import { VM_FOLDER_LABEL } from '@virtualmachines/tree/utils/constants'; -import { getLabels } from '../../../../../clusteroverview/OverviewTab/inventory-card/utils/flattenTemplates'; import { allRequiredParametersAreFulfilled, hasValidSource, uploadFiles } from '../utils'; import useCreateVMName from './useCreateVMName'; @@ -272,6 +272,12 @@ const useCreateDrawerForm = ( template, ANNOTATIONS.displayName, ); + + ensurePath(tabsDataDraft, 'disks.disks.rootDiskRegistryCredentials'); + tabsDataDraft.disks.rootDiskRegistryCredentials = { + password: encodeSecretKey(password), + username: encodeSecretKey(username), + }; }); updateTabsData((currentTabsData) => { @@ -309,10 +315,12 @@ const useCreateDrawerForm = ( ); }; + const credentialsValid = (username && password) || (!username && !password); + return { createError, folder: getLabel(vm, VM_FOLDER_LABEL), - isCustomizeDisabled: !processedTemplateAccessReview || isCustomizing, + isCustomizeDisabled: !processedTemplateAccessReview || !credentialsValid || isCustomizing, isCustomizeLoading: isCustomizing || modelsLoading, isQuickCreateDisabled: !isBootSourceAvailable || @@ -321,7 +329,8 @@ const useCreateDrawerForm = ( isEmpty(models) || !allRequiredParametersAreFulfilled(template) || !hasValidSource(template) || - storageClassRequiredMissing, + storageClassRequiredMissing || + !credentialsValid, isQuickCreateLoading: isQuickCreating || modelsLoading, nameField, onChangeFolder, diff --git a/src/views/catalog/utils/WizardVMContext/utils/tabs-data.ts b/src/views/catalog/utils/WizardVMContext/utils/tabs-data.ts index 1745f6704..49dcaca98 100644 --- a/src/views/catalog/utils/WizardVMContext/utils/tabs-data.ts +++ b/src/views/catalog/utils/WizardVMContext/utils/tabs-data.ts @@ -1,3 +1,4 @@ +import { RegistryCredentials } from '@catalog/utils/useRegistryCredentials/utils/types'; import { V1beta1DataVolume } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models'; import { RHELAutomaticSubscriptionData } from '@kubevirt-utils/hooks/useRHELAutomaticSubscription/utils/types'; import { OS_NAME_TYPES } from '@kubevirt-utils/resources/template'; @@ -8,6 +9,7 @@ export type TabsData = { authorizedSSHKey?: string; disks?: { dataVolumesToAddOwnerRef?: V1beta1DataVolume[]; + rootDiskRegistryCredentials?: RegistryCredentials; }; overview?: { templateMetadata?: { diff --git a/src/views/catalog/utils/useRegistryCredentials/useRegistryCredentials.ts b/src/views/catalog/utils/useRegistryCredentials/useRegistryCredentials.ts new file mode 100644 index 000000000..4f68fd392 --- /dev/null +++ b/src/views/catalog/utils/useRegistryCredentials/useRegistryCredentials.ts @@ -0,0 +1,35 @@ +import { RegistryCredentials } from '@catalog/utils/useRegistryCredentials/utils/types'; +import { getDecodedRegistryCredentials } from '@catalog/utils/useRegistryCredentials/utils/utils'; +import { useWizardVMContext } from '@catalog/utils/WizardVMContext'; +import { encodeSecretKey } from '@kubevirt-utils/resources/secret/utils'; + +type UseRegistryCredentials = () => { + decodedRegistryCredentials: RegistryCredentials; + encodedRegistryCredentials: RegistryCredentials; + updateRegistryCredentials: (decodedCredentials: RegistryCredentials) => void; +}; + +const useRegistryCredentials: UseRegistryCredentials = () => { + const { tabsData, updateTabsData } = useWizardVMContext(); + const encodedRegistryCredentials = tabsData?.disks?.rootDiskRegistryCredentials; + const decodedRegistryCredentials = getDecodedRegistryCredentials( + encodedRegistryCredentials, + ) as RegistryCredentials; + + const updateRegistryCredentials = (decodedCredentials: RegistryCredentials) => + updateTabsData((currentTabsData) => { + const { password, username } = decodedCredentials; + currentTabsData.disks.rootDiskRegistryCredentials = { + password: encodeSecretKey(password), + username: encodeSecretKey(username), + }; + }); + + return { + decodedRegistryCredentials, + encodedRegistryCredentials, + updateRegistryCredentials, + }; +}; + +export default useRegistryCredentials; diff --git a/src/views/catalog/utils/useRegistryCredentials/utils/types.ts b/src/views/catalog/utils/useRegistryCredentials/utils/types.ts new file mode 100644 index 000000000..64baf1bde --- /dev/null +++ b/src/views/catalog/utils/useRegistryCredentials/utils/types.ts @@ -0,0 +1 @@ +export type RegistryCredentials = { password: string; username: string }; diff --git a/src/views/catalog/utils/useRegistryCredentials/utils/utils.ts b/src/views/catalog/utils/useRegistryCredentials/utils/utils.ts new file mode 100644 index 000000000..fb71e9c16 --- /dev/null +++ b/src/views/catalog/utils/useRegistryCredentials/utils/utils.ts @@ -0,0 +1,14 @@ +import { Buffer } from 'buffer'; + +import { RegistryCredentials } from '@catalog/utils/useRegistryCredentials/utils/types'; +import { isEmpty } from '@kubevirt-utils/utils/utils'; + +const decodeValue = (value: string): string => Buffer.from(value || '', 'base64').toString(); + +export const getDecodedRegistryCredentials = (encodedCredentials: RegistryCredentials) => + encodedCredentials && !isEmpty(encodedCredentials) + ? Object.entries(encodedCredentials)?.reduce((acc, [key, value]) => { + acc[key] = decodeValue(value); + return acc; + }, {}) + : {}; diff --git a/src/views/catalog/utils/useWizardVmCreate.ts b/src/views/catalog/utils/useWizardVmCreate.ts index b726af0d2..789591e2a 100644 --- a/src/views/catalog/utils/useWizardVmCreate.ts +++ b/src/views/catalog/utils/useWizardVmCreate.ts @@ -1,6 +1,7 @@ import { useState } from 'react'; import produce from 'immer'; +import useRegistryCredentials from '@catalog/utils/useRegistryCredentials/useRegistryCredentials'; import { V1Devices, V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt'; import { logTemplateFlowEvent } from '@kubevirt-utils/extensions/telemetry/telemetry'; import { @@ -9,11 +10,14 @@ import { } from '@kubevirt-utils/extensions/telemetry/utils/constants'; import { addUploadDataVolumeOwnerReference } from '@kubevirt-utils/hooks/useCDIUpload/utils'; import useKubevirtUserSettings from '@kubevirt-utils/hooks/useKubevirtUserSettings/useKubevirtUserSettings'; -import { getName } from '@kubevirt-utils/resources/shared'; +import { createSecret } from '@kubevirt-utils/resources/secret/utils'; +import { getName, getNamespace } from '@kubevirt-utils/resources/shared'; +import { vmBootDiskSourceIsRegistry } from '@kubevirt-utils/resources/vm/utils/source'; import { HEADLESS_SERVICE_LABEL, HEADLESS_SERVICE_NAME, } from '@kubevirt-utils/utils/headless-service'; +import { addRandomSuffix } from '@kubevirt-utils/utils/utils'; import { K8sResourceCommon, useK8sModels } from '@openshift-console/dynamic-plugin-sdk'; import { getLabels } from '../../clusteroverview/OverviewTab/inventory-card/utils/flattenTemplates'; @@ -34,6 +38,7 @@ type UseWizardVmCreateValues = { export const useWizardVmCreate = (): UseWizardVmCreateValues => { const { tabsData, vm } = useWizardVMContext(); + const { decodedRegistryCredentials } = useRegistryCredentials(); const [models] = useK8sModels(); const [authorizedSSHKeys, updateAuthorizedSSHKeys] = useKubevirtUserSettings('ssh'); @@ -45,6 +50,18 @@ export const useWizardVmCreate = (): UseWizardVmCreateValues => { setLoaded(false); setError(undefined); + const { password, username } = decodedRegistryCredentials; + const addSecret = username && password && vmBootDiskSourceIsRegistry(vm); + const imageSecretName = addRandomSuffix(getName(vm)); + if (addSecret) { + await createSecret({ + namespace: getNamespace(vm), + password, + secretName: imageSecretName, + username, + }); + } + const vmToCreate = produce(vm, (vmDraft) => { if (isDisableGuestSystemAccessLog) { const devices = (vmDraft.spec.template.spec.domain.devices) as V1Devices & { @@ -56,6 +73,9 @@ export const useWizardVmCreate = (): UseWizardVmCreateValues => { if (!getLabels(vmDraft.spec.template)) vmDraft.spec.template.metadata.labels = {}; vmDraft.spec.template.metadata.labels[HEADLESS_SERVICE_LABEL] = HEADLESS_SERVICE_NAME; + + if (addSecret) + vmDraft.spec.dataVolumeTemplates[0].spec.source.registry.secretRef = imageSecretName; }); const createdObjects = await createMultipleResources( diff --git a/src/views/catalog/wizard/components/WizardFooter.tsx b/src/views/catalog/wizard/components/WizardFooter.tsx index 759a5a72d..2d9971d8d 100644 --- a/src/views/catalog/wizard/components/WizardFooter.tsx +++ b/src/views/catalog/wizard/components/WizardFooter.tsx @@ -45,7 +45,7 @@ export const WizardFooter: FC<{ namespace: string }> = ({ namespace }) => { DISABLED_GUEST_SYSTEM_LOGS_ACCESS, ); - const onCreate = () => + const onCreate = async () => createVM({ isDisableGuestSystemAccessLog, onFullfilled: (createdVM) => { diff --git a/src/views/virtualmachines/actions/components/DeleteVMModal/DeleteVMModal.tsx b/src/views/virtualmachines/actions/components/DeleteVMModal/DeleteVMModal.tsx index d9d01db99..cabff9c77 100644 --- a/src/views/virtualmachines/actions/components/DeleteVMModal/DeleteVMModal.tsx +++ b/src/views/virtualmachines/actions/components/DeleteVMModal/DeleteVMModal.tsx @@ -16,6 +16,7 @@ import { GracePeriodInput } from '@kubevirt-utils/components/GracePeriodInput/Gr import TabModal from '@kubevirt-utils/components/TabModal/TabModal'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import { useLastNamespacePath } from '@kubevirt-utils/hooks/useLastNamespacePath'; +import { deleteSecret } from '@kubevirt-utils/resources/secret/utils'; import { buildOwnerReference } from '@kubevirt-utils/resources/shared'; import { k8sDelete } from '@openshift-console/dynamic-plugin-sdk'; import { ButtonVariant, Stack, StackItem } from '@patternfly/react-core'; @@ -49,7 +50,7 @@ const DeleteVMModal: FC = ({ isOpen, onClose, vm }) => { const [snapshotsToSave, setSnapshotsToSave] = useState([]); - const { dataVolumes, loaded, pvcs, snapshots } = useDeleteVMResources(vm); + const { dataVolumes, loaded, pvcs, secrets, snapshots } = useDeleteVMResources(vm); const lastNamespacePath = useLastNamespacePath(); const onDelete = async (updatedVM: V1VirtualMachine) => { @@ -64,6 +65,8 @@ const DeleteVMModal: FC = ({ isOpen, onClose, vm }) => { await Promise.allSettled(updateSnapshotResources(snapshotsToSave, vmOwnerRef)); + await deleteSecret(secrets?.[0]); + await k8sDelete({ json: gracePeriodCheckbox ? { apiVersion: 'v1', gracePeriodSeconds, kind: 'DeleteOptions' } diff --git a/src/views/virtualmachines/actions/components/DeleteVMModal/hooks/useDeleteVMResources.ts b/src/views/virtualmachines/actions/components/DeleteVMModal/hooks/useDeleteVMResources.ts index 5412d6fec..94ffbb8cc 100644 --- a/src/views/virtualmachines/actions/components/DeleteVMModal/hooks/useDeleteVMResources.ts +++ b/src/views/virtualmachines/actions/components/DeleteVMModal/hooks/useDeleteVMResources.ts @@ -1,16 +1,19 @@ import { modelToGroupVersionKind, PersistentVolumeClaimModel, + SecretModel, } from '@kubevirt-ui/kubevirt-api/console'; import DataVolumeModel from '@kubevirt-ui/kubevirt-api/console/models/DataVolumeModel'; import VirtualMachineSnapshotModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineSnapshotModel'; import { V1beta1DataVolume } from '@kubevirt-ui/kubevirt-api/containerized-data-importer/models'; +import { IoK8sApiCoreV1Secret } from '@kubevirt-ui/kubevirt-api/kubernetes'; import { IoK8sApiCoreV1PersistentVolumeClaim } from '@kubevirt-ui/kubevirt-api/kubernetes/models'; import { V1beta1VirtualMachineSnapshot, V1VirtualMachine, } from '@kubevirt-ui/kubevirt-api/kubevirt'; -import { getVolumes } from '@kubevirt-utils/resources/vm'; +import { getName } from '@kubevirt-utils/resources/shared'; +import { getRootDiskSecretRef, getVolumes } from '@kubevirt-utils/resources/vm'; import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; import useDataVolumeConvertedVolumeNames from './useDataVolumeConvertedVolumeNames'; @@ -20,6 +23,7 @@ type UseDeleteVMResources = (vm: V1VirtualMachine) => { error: any; loaded: boolean; pvcs: IoK8sApiCoreV1PersistentVolumeClaim[]; + secrets: IoK8sApiCoreV1Secret[]; snapshots: V1beta1VirtualMachineSnapshot[]; }; @@ -63,11 +67,23 @@ const useDeleteVMResources: UseDeleteVMResources = (vm) => { namespaced: true, }); + const [secrets, secretsLoaded, secretsLoadError] = useK8sWatchResource({ + groupVersionKind: modelToGroupVersionKind(SecretModel), + isList: true, + namespace, + namespaced: true, + optional: true, + }); + + const dvSecretRef = getRootDiskSecretRef(vm); + const filteredSecret = secrets?.find((secret) => getName(secret) === dvSecretRef); + return { dataVolumes: filteredDataVolumes, - error: snapshotsLoadError || dataVolumesLoadError || pvcsLoadError, - loaded: snapshotsLoaded && dataVolumesLoaded && pvcsLoaded, + error: snapshotsLoadError || dataVolumesLoadError || pvcsLoadError || secretsLoadError, + loaded: snapshotsLoaded && dataVolumesLoaded && pvcsLoaded && secretsLoaded, pvcs: filteredPvcs, + secrets: filteredSecret ? [filteredSecret] : [], snapshots: snapshots?.filter( (snapshot) => snapshot?.metadata?.ownerReferences?.some((ref) => ref?.name === vm?.metadata?.name) ||