Skip to content

Commit

Permalink
Merge pull request #1073 from upalatucci/ssh-select-load-balancer
Browse files Browse the repository at this point in the history
[release-4.12] Bug 2172832: CNV-23494 Ssh select load balancer
  • Loading branch information
openshift-merge-robot authored Feb 23, 2023
2 parents aafe50d + de594fa commit f572b47
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 127 deletions.
7 changes: 4 additions & 3 deletions locales/en/plugin__kubevirt-plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"A DataImportCron will also be created, which will define a cron job for recurring polling/importing of the disk image": "A DataImportCron will also be created, which will define a cron job for recurring polling/importing of the disk image",
"A list of matching Nodes will be provided on label input below.": "A list of matching Nodes will be provided on label input below.",
"A Project name must consist of lower case alphanumeric characters or ', and must start and end with an alphanumeric character (e.g. 'my-name' or '123-abc'). You must create a Namespace to be able to create projects that begin with 'openshift-', 'kubernetes-', or 'kube-'.": "A Project name must consist of lower case alphanumeric characters or ', and must start and end with an alphanumeric character (e.g. 'my-name' or '123-abc'). You must create a Namespace to be able to create projects that begin with 'openshift-', 'kubernetes-', or 'kube-'.",
"A Service of type NodePort opens a specific port on all Nodes in the cluster.\nIf the Node is publicly accessible, any traffic that is sent to this port is forwarded to the Service.": "A Service of type NodePort opens a specific port on all Nodes in the cluster.\nIf the Node is publicly accessible, any traffic that is sent to this port is forwarded to the Service.",
"A value of X means that the X latest versions will be kept": "A value of X means that the X latest versions will be kept",
"Access Mode": "Access Mode",
"Access to public boot sources": "Access to public boot sources",
Expand Down Expand Up @@ -119,6 +118,7 @@
"Are you sure you want to leave this page?": "Are you sure you want to leave this page?",
"Are you sure you want to restore {{name}}?": "Are you sure you want to restore {{name}}?",
"As new versions of a DataSource become available older versions will be replaced": "As new versions of a DataSource become available older versions will be replaced",
"Assigns an external IP address to the VirtualMachine. This option requires a LoadBalancer Service backend": "Assigns an external IP address to the VirtualMachine. This option requires a LoadBalancer Service backend",
"Attach a virtual function network device to the VirtualMachine for high performance": "Attach a virtual function network device to the VirtualMachine for high performance",
"Attach an existing secret": "Attach an existing secret",
"Attach existing sysprep": "Attach existing sysprep",
Expand Down Expand Up @@ -221,7 +221,6 @@
"Create": "Create",
"Create a new blank PVC": "Create a new blank PVC",
"Create a new custom Template": "Create a new custom Template",
"Create a Service to expose your VirtualMachine for SSH access": "Create a Service to expose your VirtualMachine for SSH access",
"Create a Virtual Machine from a template": "Create a Virtual Machine from a template",
"Create an empty disk.": "Create an empty disk.",
"Create DataSource": "Create DataSource",
Expand Down Expand Up @@ -450,7 +449,6 @@
"Learn how to create, import, and run virtual machines on OpenShift with step-by-step instructions and tasks.": "Learn how to create, import, and run virtual machines on OpenShift with step-by-step instructions and tasks.",
"Learn how to use VirtualMachines": "Learn how to use VirtualMachines",
"Learn more": "Learn more",
"Learn more about configuring ingress cluster traffic using a NodePort.": "Learn more about configuring ingress cluster traffic using a NodePort.",
"Learn more about Operators": "Learn more about Operators",
"Learn more about Red Hat support": "Learn more about Red Hat support",
"Learn more about snapshots": "Learn more about snapshots",
Expand Down Expand Up @@ -599,6 +597,7 @@
"Only VM-related alerts in your project will be shown": "Only VM-related alerts in your project will be shown",
"Open Console": "Open Console",
"Open web console": "Open web console",
"Opens a specific port on all Nodes in the cluster. If the Node is publicly accessible, any traffic that is sent to this port is forwarded to the Service": "Opens a specific port on all Nodes in the cluster. If the Node is publicly accessible, any traffic that is sent to this port is forwarded to the Service",
"OpenShift Data Foundation": "OpenShift Data Foundation",
"OpenShift Data Foundation (ODF)": "OpenShift Data Foundation (ODF)",
"OpenShift Data Foundation (recommended for full functionality) or another persistent storage service is required for OpenShift Virtualization": "OpenShift Data Foundation (recommended for full functionality) or another persistent storage service is required for OpenShift Virtualization",
Expand Down Expand Up @@ -775,7 +774,9 @@
"SSH access": "SSH access",
"SSH access using the virtctl command is possible only when the API server is reachable.": "SSH access using the virtctl command is possible only when the API server is reachable.",
"SSH Key is invalid": "SSH Key is invalid",
"SSH over LoadBalancer": "SSH over LoadBalancer",
"SSH over NodePort": "SSH over NodePort",
"SSH service type": "SSH service type",
"SSH using virtctl": "SSH using virtctl",
"Start": "Start",
"Start cloned VM": "Start cloned VM",
Expand Down
32 changes: 0 additions & 32 deletions src/utils/components/SSHAccess/components/SSHCheckbox.tsx

This file was deleted.

93 changes: 39 additions & 54 deletions src/utils/components/SSHAccess/components/SSHCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';

import { IoK8sApiCoreV1Service } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { V1VirtualMachine, V1VirtualMachineInstance } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { CLOUD_INIT_MISSING_USERNAME } from '@kubevirt-utils/components/Consoles/utils/constants';
import Loading from '@kubevirt-utils/components/Loading/Loading';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import {
Expand All @@ -12,17 +11,15 @@ import {
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
Popover,
Stack,
StackItem,
Tooltip,
} from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';

import { SERVICE_TYPES } from '../constants';
import useSSHCommand from '../useSSHCommand';
import { createSSHService, deleteSSHService } from '../utils';

import SSHCheckbox from './SSHCheckbox';
import SSHServiceSelect from './SSHServiceSelect';

type SSHCommandProps = {
vmi: V1VirtualMachineInstance;
Expand All @@ -38,23 +35,30 @@ const SSHCommand: React.FC<SSHCommandProps> = ({
sshServiceLoaded,
}) => {
const { t } = useKubevirtTranslation();
const [sshService, setSSHService] = useState<IoK8sApiCoreV1Service>();
const { command, user, sshServiceRunning } = useSSHCommand(vm, sshService);
const [sshService, setSSHService] = useState<IoK8sApiCoreV1Service | null>();
const { command, sshServiceRunning } = useSSHCommand(vm, sshService);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error>();

const onSSHChange = (enableSSH: boolean) => {
const onSSHChange = async (newServiceType: SERVICE_TYPES) => {
setLoading(true);

const request = enableSSH
? createSSHService(vm, vmi).then(setSSHService)
: deleteSSHService(sshService).then(() => setSSHService(undefined));
try {
if (sshService) {
await deleteSSHService(sshService);
}

request.catch(setError).finally(() => setLoading(false));
if (newServiceType && newServiceType !== SERVICE_TYPES.NONE) {
const newService = await createSSHService(vm, vmi, newServiceType);
setSSHService(newService);
}
} catch (apiError) {
setError(apiError);
} finally {
setLoading(false);
}
};

const hasNoUsername = user === CLOUD_INIT_MISSING_USERNAME;

useEffect(() => {
setSSHService(initialSSHService);
setError(undefined);
Expand All @@ -63,53 +67,34 @@ const SSHCommand: React.FC<SSHCommandProps> = ({
return (
<DescriptionListGroup>
<DescriptionListTerm className="pf-u-font-size-xs">
{t('SSH over NodePort')}{' '}
<Popover
aria-label={'Help'}
position="right"
bodyContent={
<span>
{t(
'A Service of type NodePort opens a specific port on all Nodes in the cluster.\nIf the Node is publicly accessible, any traffic that is sent to this port is forwarded to the Service.',
)}{' '}
<a
target="_blank"
href="https://access.redhat.com/documentation/en-us/openshift_container_platform/4.11/html-single/networking/index#configuring-ingress-cluster-traffic-nodeport"
rel="noreferrer"
>
{t('Learn more about configuring ingress cluster traffic using a NodePort.')}
</a>
</span>
}
>
<HelpIcon />
</Popover>
{t('SSH service type')}
</DescriptionListTerm>

<DescriptionListDescription>
<Stack hasGutter>
<StackItem>
<Tooltip content={CLOUD_INIT_MISSING_USERNAME} isVisible={hasNoUsername}>
<SSHCheckbox
sshServiceRunning={Boolean(sshService)}
setSSHServiceRunning={onSSHChange}
isDisabled={loading || hasNoUsername}
/>
</Tooltip>
</StackItem>
{sshServiceLoaded && !loading ? (
sshServiceRunning && (
<>
<StackItem>
<ClipboardCopy
isReadOnly
data-test="ssh-command-copy"
clickTip={t('Copied')}
hoverTip={t('Copy to clipboard')}
>
{command}
</ClipboardCopy>
<SSHServiceSelect
sshService={sshService}
sshServiceLoaded={sshServiceLoaded}
onSSHChange={onSSHChange}
/>
</StackItem>
)

{sshServiceRunning && (
<StackItem>
<ClipboardCopy
isReadOnly
data-test="ssh-command-copy"
clickTip={t('Copied')}
hoverTip={t('Copy to clipboard')}
>
{command}
</ClipboardCopy>
</StackItem>
)}
</>
) : (
<Loading />
)}
Expand Down
74 changes: 74 additions & 0 deletions src/utils/components/SSHAccess/components/SSHServiceSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React, { FC, useMemo } from 'react';

import { IoK8sApiCoreV1Service } from '@kubevirt-ui/kubevirt-api/kubernetes';
import { useKubevirtTranslation } from '@kubevirt-utils/hooks/useKubevirtTranslation';
import { useK8sModels } from '@openshift-console/dynamic-plugin-sdk';
import { Select, SelectOption, SelectVariant } from '@patternfly/react-core';

import { METALLB_GROUP, SERVICE_TYPES } from '../constants';

type SSHServiceSelectProps = {
sshService: IoK8sApiCoreV1Service;
sshServiceLoaded: boolean;
onSSHChange: (serviceType: SERVICE_TYPES) => void;
};

const SSHServiceSelect: FC<SSHServiceSelectProps> = ({
sshService,
sshServiceLoaded,
onSSHChange,
}) => {
const { t } = useKubevirtTranslation();
const [isOpen, setIsOpen] = React.useState(false);
const [models] = useK8sModels();

const sshServiceType = sshService?.spec?.type ?? SERVICE_TYPES.NONE;

const hasSomeMetalCrd = useMemo(
() =>
Object.keys(models).some((modelGroupVersionKind) =>
modelGroupVersionKind.startsWith(METALLB_GROUP),
),
[models],
);

const handleChange = (event: React.ChangeEvent<Element>, newValue: string | undefined) => {
setIsOpen(false);

if (newValue === sshServiceType) return;
onSSHChange(newValue as SERVICE_TYPES);
};

return (
<Select
menuAppendTo="parent"
isOpen={isOpen}
onToggle={setIsOpen}
onSelect={handleChange}
variant={SelectVariant.single}
selections={sshServiceType}
isDisabled={!sshServiceLoaded}
>
<SelectOption value={SERVICE_TYPES.NONE}>{t('None')}</SelectOption>
<SelectOption
value={SERVICE_TYPES.NODE_PORT}
description={t(
'Opens a specific port on all Nodes in the cluster. If the Node is publicly accessible, any traffic that is sent to this port is forwarded to the Service',
)}
>
{t('SSH over NodePort')}
</SelectOption>
<SelectOption
isDisabled={!hasSomeMetalCrd}
value={SERVICE_TYPES.LOAD_BALANCER}
description={t(
'Assigns an external IP address to the VirtualMachine. This option requires a LoadBalancer Service backend',
)}
>
{t('SSH over LoadBalancer')}
</SelectOption>
</Select>
);
};

export default SSHServiceSelect;
8 changes: 8 additions & 0 deletions src/utils/components/SSHAccess/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ export const PORT = 22000;
export const SSH_PORT = 22;

export const VMI_LABEL_AS_SSH_SERVICE_SELECTOR = 'kubevirt.io/domain';

export enum SERVICE_TYPES {
NONE = 'None',
LOAD_BALANCER = 'LoadBalancer',
NODE_PORT = 'NodePort',
}

export const METALLB_GROUP = 'metallb.io';
12 changes: 10 additions & 2 deletions src/utils/components/SSHAccess/useSSHCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { V1VirtualMachine } from '@kubevirt-ui/kubevirt-api/kubevirt';
import { getCloudInitCredentials } from '@kubevirt-utils/resources/vmi';
import { getSSHNodePort } from '@kubevirt-utils/utils/utils';

import { SERVICE_TYPES } from './constants';

export type useSSHCommandResult = {
command: string;
user: string;
Expand All @@ -14,11 +16,17 @@ const useSSHCommand = (
vm: V1VirtualMachine,
sshService: IoK8sApiCoreV1Service,
): useSSHCommandResult => {
const consoleHostname = window.location.hostname; // fallback to console hostname
const consoleHostname =
sshService?.spec?.type === SERVICE_TYPES.LOAD_BALANCER
? sshService?.status?.loadBalancer?.ingress?.[0]?.ip
: window.location.hostname; // fallback to console hostname

const { users } = getCloudInitCredentials(vm);
const user = users?.[0]?.name;
const sshServicePort = getSSHNodePort(sshService);
const sshServicePort =
sshService?.spec?.type === SERVICE_TYPES.LOAD_BALANCER
? sshService?.spec?.ports?.[0]?.port
: getSSHNodePort(sshService);

let command = 'ssh ';

Expand Down
Loading

0 comments on commit f572b47

Please sign in to comment.