From c5d1e99cbe62435177a51a295961b6241693a0d1 Mon Sep 17 00:00:00 2001 From: Dana Orr Date: Thu, 11 Aug 2022 17:01:37 +0300 Subject: [PATCH] Adding the option to enable RDP Service to window vm Signed-off-by: Dana Orr --- locales/en/plugin__kubevirt-plugin.json | 5 +- .../Charts/ComponentReady/ComponentReady.tsx | 12 +++- .../DesktopViewer/Components/RDPConnector.tsx | 22 ++++-- .../Components/RDPServiceModal.tsx | 57 +++++++++++++++ .../Components/RDPServiceNotConfigured.tsx | 34 +++++++++ .../Components/RdpServiceNotConfigured.tsx | 52 -------------- .../DesktopViewer/Components/rdp-service.scss | 5 ++ .../DesktopViewer/DesktopViewer.tsx | 17 +++-- .../DesktopViewer/utils/constants.ts | 3 + .../components/DesktopViewer/utils/types.ts | 6 +- .../components/DesktopViewer/utils/utils.ts | 71 +++++++++++++++++++ src/utils/components/TabModal/TabModal.tsx | 2 +- 12 files changed, 216 insertions(+), 70 deletions(-) create mode 100644 src/utils/components/Consoles/components/DesktopViewer/Components/RDPServiceModal.tsx create mode 100644 src/utils/components/Consoles/components/DesktopViewer/Components/RDPServiceNotConfigured.tsx delete mode 100644 src/utils/components/Consoles/components/DesktopViewer/Components/RdpServiceNotConfigured.tsx create mode 100644 src/utils/components/Consoles/components/DesktopViewer/Components/rdp-service.scss diff --git a/locales/en/plugin__kubevirt-plugin.json b/locales/en/plugin__kubevirt-plugin.json index b89bedf07..26a7019de 100644 --- a/locales/en/plugin__kubevirt-plugin.json +++ b/locales/en/plugin__kubevirt-plugin.json @@ -40,7 +40,7 @@ "<0>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.<1><0>Template<1>metadata<2>ownerReferences": "<0>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.<1><0>Template<1>metadata<2>ownerReferences", "<0>Store the key in a project secret.<1>The key will be stored after the machine is created": "<0>Store the key in a project secret.<1>The key will be stored after the machine is created", "<0>The descheduler can be used to evict a running pod to allow the pod to be rescheduled onto a more suitable node.<1><2>Note: if VirtualMachine have LiveMigration=False condition, edit is disabled.": "<0>The descheduler can be used to evict a running pod to allow the pod to be rescheduled onto a more suitable node.<1><2>Note: if VirtualMachine have LiveMigration=False condition, edit is disabled.", - "<0>This is a Windows VirtualMachine but no Service for the RDP (Remote Desktop Protocol) can be found.<1><2>For better experience accessing Windows console, it is recommended to use the RDP. To do so, create a service:<1><0>exposing the <2>{DEFAULT_RDP_PORT}/tcp port of the VirtualMachine<1>using selector: <2>{TEMPLATE_VM_NAME_LABEL}: {name}<2>Example: virtctl expose VirtualMachine {name} --name {name}-rdp --port [UNIQUE_PORT] --target-port {DEFAULT_RDP_PORT} --type NodePortMake sure, the VirtualMachine object has <3>spec.template.metadata.labels set to <6>{TEMPLATE_VM_NAME_LABEL}: {name}": "<0>This is a Windows VirtualMachine but no Service for the RDP (Remote Desktop Protocol) can be found.<1><2>For better experience accessing Windows console, it is recommended to use the RDP. To do so, create a service:<1><0>exposing the <2>{DEFAULT_RDP_PORT}/tcp port of the VirtualMachine<1>using selector: <2>{TEMPLATE_VM_NAME_LABEL}: {name}<2>Example: virtctl expose VirtualMachine {name} --name {name}-rdp --port [UNIQUE_PORT] --target-port {DEFAULT_RDP_PORT} --type NodePortMake sure, the VirtualMachine object has <3>spec.template.metadata.labels set to <6>{TEMPLATE_VM_NAME_LABEL}: {name}", + "<0>This is a Windows VirtualMachine but no Service for the RDP (Remote Desktop Protocol) can be found.<1><2>For better experience accessing Windows console, it is recommended to use the RDP.<1>Create RDP Service": "<0>This is a Windows VirtualMachine but no Service for the RDP (Remote Desktop Protocol) can be found.<1><2>For better experience accessing Windows console, it is recommended to use the RDP.<1>Create RDP Service", "<0>Unattend.xml<1>Unattend can be used to configure windows setup and can be picked up several times during windows setup/configuration.<2><0>{t('Learn more')}": "<0>Unattend.xml<1>Unattend can be used to configure windows setup and can be picked up several times during windows setup/configuration.<2><0>{t('Learn more')}", "3 (default)": "3 (default)", "5 min": "5 min", @@ -366,6 +366,7 @@ "Example: For Windows, get a link to the ": "Example: For Windows, get a link to the ", "Example: your company name": "Example: your company name", "Explore {{kind}} list": "Explore {{kind}} list", + "Expose RDP Service": "Expose RDP Service", "Expose SSH access": "Expose SSH access", "Feature highlights": "Feature highlights", "Fedora cloud image list ": "Fedora cloud image list ", @@ -646,6 +647,8 @@ "Quick Starts": "Quick Starts", "RDP Address": "RDP Address", "RDP Port": "RDP Port", + "RDP Service": "RDP Service", + "RDP Service is using a node port. Node port requires additional port resources.": "RDP Service is using a node port. Node port requires additional port resources.", "Read about the latest information and key virtualization features on the Virtualization highlights.": "Read about the latest information and key virtualization features on the Virtualization highlights.", "Read more": "Read more", "Read only (ROX)": "Read only (ROX)", diff --git a/src/utils/components/Charts/ComponentReady/ComponentReady.tsx b/src/utils/components/Charts/ComponentReady/ComponentReady.tsx index 3edcd9326..9ba06bd4e 100644 --- a/src/utils/components/Charts/ComponentReady/ComponentReady.tsx +++ b/src/utils/components/Charts/ComponentReady/ComponentReady.tsx @@ -3,16 +3,22 @@ import React from 'react'; import MutedTextSpan from '@kubevirt-utils/components/MutedTextSpan/MutedTextSpan'; import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; import { Bullseye } from '@patternfly/react-core'; +import Loading from '@kubevirt-utils/components/Loading/Loading'; -type ComponentReadyProps = React.PropsWithChildren<{ isReady: boolean; text?: string }>; +type ComponentReadyProps = React.PropsWithChildren<{ + isReady: boolean; + text?: string; + spinner?: boolean; +}>; -const ComponentReady: React.FC = ({ isReady, children, text }) => { +const ComponentReady: React.FC = ({ isReady, children, text, spinner }) => { const { t } = useKubevirtTranslation(); return isReady ? ( <>{children} ) : ( - + {(!spinner || text) && } + {spinner && } ); }; diff --git a/src/utils/components/Consoles/components/DesktopViewer/Components/RDPConnector.tsx b/src/utils/components/Consoles/components/DesktopViewer/Components/RDPConnector.tsx index c225e28c1..10a6025ed 100644 --- a/src/utils/components/Consoles/components/DesktopViewer/Components/RDPConnector.tsx +++ b/src/utils/components/Consoles/components/DesktopViewer/Components/RDPConnector.tsx @@ -1,15 +1,25 @@ +import ComponentReady from '@kubevirt-utils/components/Charts/ComponentReady/ComponentReady'; import React from 'react'; import { RDPConnectorProps } from '../utils/types'; import RDP from './RDP'; -import RdpServiceNotConfigured from './RdpServiceNotConfigured'; +import RDPServiceNotConfigured from './RDPServiceNotConfigured'; -const RDPConnector: React.FC = ({ rdpServiceAddressPort, vm }) => { - return rdpServiceAddressPort ? ( - - ) : ( - +const RDPConnector: React.FC = ({ + rdpServiceAddressPort, + vm, + vmi, + isLoading, +}) => { + return ( + + {rdpServiceAddressPort ? ( + + ) : ( + + )} + ); }; diff --git a/src/utils/components/Consoles/components/DesktopViewer/Components/RDPServiceModal.tsx b/src/utils/components/Consoles/components/DesktopViewer/Components/RDPServiceModal.tsx new file mode 100644 index 000000000..4b45f6397 --- /dev/null +++ b/src/utils/components/Consoles/components/DesktopViewer/Components/RDPServiceModal.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; + +import { V1VirtualMachine, V1VirtualMachineInstance } from '@kubevirt-ui/kubevirt-api/kubevirt'; +import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; +import { Alert, Checkbox, ModalVariant, Stack, StackItem } from '@patternfly/react-core'; +import TabModal from '@kubevirt-utils/components/TabModal/TabModal'; +import ExternalLink from '@kubevirt-utils/components/ExternalLink/ExternalLink'; +import { NODE_PORTS_LINK } from '../utils/constants'; +import { createRDPService } from '../utils/utils'; + +type RDPServiceModalProps = { + vmi: V1VirtualMachineInstance; + vm: V1VirtualMachine; + isOpen: boolean; + onClose: () => void; +}; + +const RDPServiceModal: React.FC = ({ vmi, vm, isOpen, onClose }) => { + const { t } = useKubevirtTranslation(); + const [isChecked, setChecked] = React.useState(false); + + return ( + createRDPService(vm, vmi)} + headerText={t('RDP Service')} + modalVariant={ModalVariant.medium} + > + + + + + + +
+ {t('RDP Service is using a node port. Node port requires additional port resources.')} +
+ +
+
+
+
+
+
+ ); +}; + +export default RDPServiceModal; diff --git a/src/utils/components/Consoles/components/DesktopViewer/Components/RDPServiceNotConfigured.tsx b/src/utils/components/Consoles/components/DesktopViewer/Components/RDPServiceNotConfigured.tsx new file mode 100644 index 000000000..424970908 --- /dev/null +++ b/src/utils/components/Consoles/components/DesktopViewer/Components/RDPServiceNotConfigured.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Trans } from 'react-i18next'; + +import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; +import { RDPServiceNotConfiguredProps } from '../utils/types'; +import { Button, ButtonVariant } from '@patternfly/react-core'; +import RDPServiceModal from './RDPServiceModal'; +import { useModal } from '@kubevirt-utils/components/ModalProvider/ModalProvider'; +import './rdp-service.scss'; +const RDPServiceNotConfigured: React.FC = ({ vm, vmi }) => { + const { t } = useKubevirtTranslation(); + const { createModal } = useModal(); + return ( + + + This is a Windows VirtualMachine but no Service for the RDP (Remote Desktop Protocol) can be + found. + +
+ + For better experience accessing Windows console, it is recommended to use the RDP. + + +
+ ); +}; + +export default RDPServiceNotConfigured; diff --git a/src/utils/components/Consoles/components/DesktopViewer/Components/RdpServiceNotConfigured.tsx b/src/utils/components/Consoles/components/DesktopViewer/Components/RdpServiceNotConfigured.tsx deleted file mode 100644 index 6bcfb1090..000000000 --- a/src/utils/components/Consoles/components/DesktopViewer/Components/RdpServiceNotConfigured.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { Trans } from 'react-i18next'; - -import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation'; - -import { DEFAULT_RDP_PORT, TEMPLATE_VM_NAME_LABEL } from '../utils/constants'; -import { RdpServiceNotConfiguredProps } from '../utils/types'; - -const RdpServiceNotConfigured: React.FC = ({ vm }) => { - const { t } = useKubevirtTranslation(); - const name = vm?.metadata?.name; - - return ( - - - This is a Windows VirtualMachine but no Service for the RDP (Remote Desktop Protocol) can be - found. - -
- - For better experience accessing Windows console, it is recommended to use the RDP. To do so, - create a service: -
    -
  • - exposing the{' '} - - {DEFAULT_RDP_PORT} - /tcp - {' '} - port of the VirtualMachine -
  • -
  • - using selector:{' '} - - {TEMPLATE_VM_NAME_LABEL}: {name} - -
  • -
  • - Example: virtctl expose VirtualMachine {name} --name {name} - -rdp --port [UNIQUE_PORT] --target-port {DEFAULT_RDP_PORT} --type NodePort -
  • -
- Make sure, the VirtualMachine object has spec.template.metadata.labels set to{' '} - - {TEMPLATE_VM_NAME_LABEL}: {name} - -
-
- ); -}; - -export default RdpServiceNotConfigured; diff --git a/src/utils/components/Consoles/components/DesktopViewer/Components/rdp-service.scss b/src/utils/components/Consoles/components/DesktopViewer/Components/rdp-service.scss new file mode 100644 index 000000000..ed4ab296f --- /dev/null +++ b/src/utils/components/Consoles/components/DesktopViewer/Components/rdp-service.scss @@ -0,0 +1,5 @@ +.kv-create-rdp-service-button { + display: block; + margin-top: var(--pf-global--spacer--md) + ; +} diff --git a/src/utils/components/Consoles/components/DesktopViewer/DesktopViewer.tsx b/src/utils/components/Consoles/components/DesktopViewer/DesktopViewer.tsx index 2e737fb8f..882585287 100644 --- a/src/utils/components/Consoles/components/DesktopViewer/DesktopViewer.tsx +++ b/src/utils/components/Consoles/components/DesktopViewer/DesktopViewer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { modelToGroupVersionKind, PodModel, ServiceModel } from '@kubevirt-ui/kubevirt-api/console'; import { IoK8sApiCoreV1Pod, IoK8sApiCoreV1Service } from '@kubevirt-ui/kubevirt-api/kubernetes'; @@ -15,19 +15,19 @@ import { getDefaultNetwork, getRdpAddressPort, getVmRdpNetworks } from './utils/ const DesktopViewer: React.FC = ({ vm, vmi }) => { const { t } = useKubevirtTranslation(); const [isDropdownOpen, setIsDropdownOpen] = React.useState(false); - const [pods] = useK8sWatchResource({ + const [pods, podsLoaded] = useK8sWatchResource({ groupVersionKind: modelToGroupVersionKind(PodModel), isList: true, + namespace: vm.metadata.namespace, }); - const vmPod = getVMIPod(vmi, pods); + const vmPod = useMemo(() => getVMIPod(vmi, pods), [vmi, pods]); - const [services] = useK8sWatchResource({ + const [services, servicesLoaded] = useK8sWatchResource({ groupVersionKind: modelToGroupVersionKind(ServiceModel), isList: true, namespace: vm?.metadata?.namespace, }); - const rdpServiceAddressPort = getRdpAddressPort(vmi, services, vmPod); const networks = getVmRdpNetworks(vm, vmi); const [selectedNetwork, setSelectedNetwork] = React.useState( @@ -72,7 +72,12 @@ const DesktopViewer: React.FC = ({ vm, vmi }) => { {networkType === 'POD' && ( - + )} {networkType === 'MULTUS' && } diff --git a/src/utils/components/Consoles/components/DesktopViewer/utils/constants.ts b/src/utils/components/Consoles/components/DesktopViewer/utils/constants.ts index 2ad2c8ae7..e4af39714 100644 --- a/src/utils/components/Consoles/components/DesktopViewer/utils/constants.ts +++ b/src/utils/components/Consoles/components/DesktopViewer/utils/constants.ts @@ -4,3 +4,6 @@ export const DEFAULT_VV_FILENAME = 'console.vv'; export const DEFAULT_VV_MIMETYPE = 'application/x-virt-viewer'; export const DEFAULT_RDP_FILENAME = 'console.rdp'; export const DEFAULT_RDP_MIMETYPE = 'application/rdp'; +export const VMI_LABEL_AS_RDP_SERVICE_SELECTOR = 'vm.kubevirt.io/name'; +export const NODE_PORTS_LINK = + 'https://access.redhat.com/documentation/en-us/openshift_container_platform/4.10/html/networking/configuring-ingress-cluster-traffic#nw-using-nodeport_configuring-ingress-cluster-traffic-nodeport'; diff --git a/src/utils/components/Consoles/components/DesktopViewer/utils/types.ts b/src/utils/components/Consoles/components/DesktopViewer/utils/types.ts index 47517089d..5c55c93b4 100644 --- a/src/utils/components/Consoles/components/DesktopViewer/utils/types.ts +++ b/src/utils/components/Consoles/components/DesktopViewer/utils/types.ts @@ -86,6 +86,7 @@ export type RDPProps = ConnectWithRemoteViewerProps & { textMoreRDPInfo?: string; /** The information content appearing above the description list for guidelines to install virt-viewer */ textMoreRDPInfoContent?: string | React.ReactNode; + isLoading?: boolean; }; export type ManualConnectionProps = React.HTMLProps & { @@ -157,8 +158,9 @@ export type DesktopViewerProps = { type: string; }; -export type RdpServiceNotConfiguredProps = { +export type RDPServiceNotConfiguredProps = { vm: V1VirtualMachine; + vmi: V1VirtualMachineInstance; }; export type Network = { @@ -170,6 +172,8 @@ export type Network = { export type RDPConnectorProps = { rdpServiceAddressPort: ConsoleDetailPropType; vm: V1VirtualMachine; + vmi: V1VirtualMachineInstance; + isLoading: boolean; }; export type MultusNetworkProps = { diff --git a/src/utils/components/Consoles/components/DesktopViewer/utils/utils.ts b/src/utils/components/Consoles/components/DesktopViewer/utils/utils.ts index 6b1cf4fc2..0f3b6408e 100644 --- a/src/utils/components/Consoles/components/DesktopViewer/utils/utils.ts +++ b/src/utils/components/Consoles/components/DesktopViewer/utils/utils.ts @@ -1,4 +1,10 @@ import { saveAs } from 'file-saver'; +import { ServiceModel } from '@kubevirt-ui/kubevirt-api/console'; +import VirtualMachineInstanceModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineInstanceModel'; +import VirtualMachineModel from '@kubevirt-ui/kubevirt-api/console/models/VirtualMachineModel'; +import { k8sCreate, k8sPatch, K8sResourceCommon } from '@openshift-console/dynamic-plugin-sdk'; + +import { buildOwnerReference } from '../../../../../resources/shared'; import { IoK8sApiCoreV1Pod, @@ -14,6 +20,7 @@ import { DEFAULT_RDP_PORT, DEFAULT_VV_MIMETYPE, TEMPLATE_VM_NAME_LABEL, + VMI_LABEL_AS_RDP_SERVICE_SELECTOR, } from './constants'; import { ConsoleDetailPropType, Network } from './types'; @@ -213,3 +220,67 @@ export const getDefaultNetwork = (networks: Network[]) => { } return null; }; + +export const createRDPService = ( + vm: V1VirtualMachine, + vmi: V1VirtualMachineInstance, +): Promise => { + const { namespace, name } = vm?.metadata || {}; + const vmiLabels = vm?.spec?.template?.metadata?.labels; + const labelSelector = vmiLabels?.[VMI_LABEL_AS_RDP_SERVICE_SELECTOR] || name; + + const vmPromise = k8sPatch({ + model: VirtualMachineModel, + resource: vm, + data: [ + { + op: 'add', + path: `/spec/template/metadata/labels/${VMI_LABEL_AS_RDP_SERVICE_SELECTOR.replaceAll( + '/', + '~1', + )}`, + value: labelSelector, + }, + ], + }); + + const vmiPromise = k8sPatch({ + model: VirtualMachineInstanceModel, + resource: vmi, + data: [ + { + op: 'add', + path: `/metadata/labels/${VMI_LABEL_AS_RDP_SERVICE_SELECTOR.replaceAll('/', '~1')}`, + value: labelSelector, + }, + ], + }); + + const servicePromise = k8sCreate({ + model: ServiceModel, + data: { + kind: ServiceModel.kind, + apiVersion: ServiceModel.apiVersion, + metadata: { + name: `${vm?.metadata?.name}-rdp`, + namespace: vm?.metadata?.namespace, + ownerReferences: [buildOwnerReference(vm, { blockOwnerDeletion: false })], + }, + spec: { + ports: [ + { + port: DEFAULT_RDP_PORT, + targetPort: DEFAULT_RDP_PORT, + }, + ], + type: 'NodePort', + selector: { + [VMI_LABEL_AS_RDP_SERVICE_SELECTOR]: labelSelector, + }, + }, + }, + ns: namespace, + }); + + return Promise.all([vmPromise, vmiPromise, servicePromise]); +}; diff --git a/src/utils/components/TabModal/TabModal.tsx b/src/utils/components/TabModal/TabModal.tsx index e565a812f..6f6348092 100644 --- a/src/utils/components/TabModal/TabModal.tsx +++ b/src/utils/components/TabModal/TabModal.tsx @@ -20,7 +20,7 @@ import './TabModal.scss'; type TabModalProps = { isOpen: boolean; obj?: T; - onSubmit: (obj: T) => Promise; + onSubmit: (obj: T) => Promise; onClose: () => Promise | void; headerText: string; children: React.ReactNode;