diff --git a/node-shell/src/components/NodeShellAction.tsx b/node-shell/src/components/NodeShellAction.tsx index 175971b75..b64908935 100644 --- a/node-shell/src/components/NodeShellAction.tsx +++ b/node-shell/src/components/NodeShellAction.tsx @@ -1,18 +1,27 @@ - import { ActionButton } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; -import { Node } from '@kinvolk/headlamp-plugin/lib'; +import Node from '@kinvolk/headlamp-plugin/lib/K8s/node'; +import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils'; import { useState } from 'react'; +import { isEnabled } from '../util'; import { NodeShellTerminal } from './NodeShellTerminal'; export function NodeShellAction({ item }) { const [showShell, setShowShell] = useState(false); + const cluster = getCluster(); function isLinux(item: Node | null): boolean { return item?.status?.nodeInfo?.operatingSystem === 'linux'; } + if (!isEnabled(cluster)) { + return <>; + } return ( <> setShowShell(true)} iconButtonProps={{ @@ -28,5 +37,6 @@ export function NodeShellAction({ item }) { setShowShell(false); }} /> - ) + + ); } diff --git a/node-shell/src/components/NodeShellTerminal.tsx b/node-shell/src/components/NodeShellTerminal.tsx index d6c7216c2..ce558afde 100644 --- a/node-shell/src/components/NodeShellTerminal.tsx +++ b/node-shell/src/components/NodeShellTerminal.tsx @@ -1,16 +1,16 @@ - +import { apply, stream, StreamResultsCb } from '@kinvolk/headlamp-plugin/lib/ApiProxy'; import { Dialog, DialogProps } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; -import DialogContent from '@mui/material/DialogContent'; +import Node from '@kinvolk/headlamp-plugin/lib/K8s/node'; +import Pod, { KubePod } from '@kinvolk/headlamp-plugin/lib/K8s/pod'; +import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils'; import { Box } from '@mui/material'; -import { Node } from '@kinvolk/headlamp-plugin/lib'; -import { useEffect, useRef, useState } from 'react'; -import { Terminal as XTerminal } from '@xterm/xterm'; +import DialogContent from '@mui/material/DialogContent'; import { FitAddon } from '@xterm/addon-fit'; -import Pod, { KubePod } from '@kinvolk/headlamp-plugin/lib/lib/k8s/pod'; -import { apply, stream, StreamResultsCb } from '@kinvolk/headlamp-plugin/lib/ApiProxy'; -import { DEFAULT_NODE_SHELL_LINUX_IMAGE } from './Settings'; +import { Terminal as XTerminal } from '@xterm/xterm'; +import _ from 'lodash'; +import { useEffect, useRef, useState } from 'react'; import { getClusterConfig } from '../util'; -import { getCluster } from '@kinvolk/headlamp-plugin/lib/Utils'; +import { DEFAULT_NODE_SHELL_LINUX_IMAGE, DEFAULT_NODE_SHELL_NAMESPACE } from './Settings'; const decoder = new TextDecoder('utf-8'); const encoder = new TextEncoder(); @@ -26,7 +26,7 @@ enum Channel { interface NodeShellTerminalProps extends DialogProps { item: Node; title: string; - open: boolean + open: boolean; onClose?: () => void; } @@ -37,14 +37,13 @@ interface XTerminalConnected { onClose?: () => void; } - -const shellPod = (name: string, nodeName: string, nodeShellImage: string) => { +const shellPod = (name: string, namespace: string, nodeName: string, nodeShellImage: string) => { return { kind: 'Pod', apiVersion: 'v1', metadata: { name, - namespace: 'kube-system', + namespace, }, spec: { nodeName, @@ -87,13 +86,17 @@ async function shell(item: Node, onExec: StreamResultsCb) { } //const clusterSettings = helpers.loadClusterSettings(cluster); - const config = getClusterConfig(cluster) + const config = getClusterConfig(cluster); let image = config?.image || ''; + let namespace = config?.namespace || ''; const podName = `node-shell-${item.getName()}-${uniqueString()}`; if (image === '') { image = DEFAULT_NODE_SHELL_LINUX_IMAGE; } - const kubePod = shellPod(podName, item.getName(), image!!); + if (namespace === '') { + namespace = DEFAULT_NODE_SHELL_NAMESPACE; + } + const kubePod = shellPod(podName, namespace, item.getName(), image!!); try { await apply(kubePod); } catch (e) { @@ -110,8 +113,9 @@ async function shell(item: Node, onExec: StreamResultsCb) { const stdout = true; const stderr = true; const commandStr = command.map(item => '&command=' + encodeURIComponent(item)).join(''); - const url = `/api/v1/namespaces/kube-system/pods/${podName}/exec?container=shell${commandStr}&stdin=${stdin ? 1 : 0 - }&stderr=${stderr ? 1 : 0}&stdout=${stdout ? 1 : 0}&tty=${tty ? 1 : 0}`; + const url = `/api/v1/namespaces/kube-system/pods/${podName}/exec?container=shell${commandStr}&stdin=${ + stdin ? 1 : 0 + }&stderr=${stderr ? 1 : 0}&stdout=${stdout ? 1 : 0}&tty=${tty ? 1 : 0}`; const additionalProtocols = [ 'v4.channel.k8s.io', 'v3.channel.k8s.io', @@ -135,7 +139,6 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { const fitAddonRef = useRef(null); const streamRef = useRef(null); - const wrappedOnClose = () => { if (!!onClose) { onClose(); @@ -146,7 +149,6 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { } }; - // @todo: Give the real exec type when we have it. function setupTerminal(containerRef: HTMLElement, xterm: XTerminal, fitAddon: FitAddon) { if (!containerRef) { @@ -198,7 +200,6 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { socket.send(buffer); } - function onData(xtermc: XTerminalConnected, bytes: ArrayBuffer) { const xterm = xtermc.xterm; // Only show data from stdout, stderr and server error channel. @@ -210,7 +211,7 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { // The first byte is discarded because it just identifies whether // this data is from stderr, stdout, or stdin. const data = bytes.slice(1); - let text = decoder.decode(data); + const text = decoder.decode(data); // Send resize command to server once connection is establised. if (!xtermc.connected) { @@ -250,7 +251,7 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { if (_.isEmpty(error.metadata) && error.status === 'Success') { return true; } - } catch { } + } catch {} } return false; } @@ -263,7 +264,7 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { if (error.code === 500 && error.status === 'Failure' && error.reason === 'InternalError') { return true; } - } catch { } + } catch {} } // Windows container Error if (channel === 1) { @@ -274,7 +275,6 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { return false; } - function shellConnectFailed(xtermc: XTerminalConnected) { const xterm = xtermc.xterm; xterm.clear(); @@ -354,7 +354,6 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { title={title} {...other} > - ({ height: '100%', @@ -393,5 +392,5 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) { - ) + ); } diff --git a/node-shell/src/components/Settings.tsx b/node-shell/src/components/Settings.tsx index f9f14d3f2..6bf914bea 100644 --- a/node-shell/src/components/Settings.tsx +++ b/node-shell/src/components/Settings.tsx @@ -3,16 +3,18 @@ import { useClustersConf } from '@kinvolk/headlamp-plugin/lib/k8s'; import Box from '@mui/material/Box'; import MenuItem from '@mui/material/MenuItem'; import Select from '@mui/material/Select'; +import Switch from '@mui/material/Switch'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; -export const DEFAULT_NODE_SHELL_LINUX_IMAGE = 'docker.io/library/alpine:latest' +export const DEFAULT_NODE_SHELL_LINUX_IMAGE = 'docker.io/library/alpine:latest'; +export const DEFAULT_NODE_SHELL_NAMESPACE = 'kube-system'; /** * Props for the Settings component. * @interface SettingsProps - * @property {Object.} data - Configuration data for each cluster + * @property {Object.} data - Configuration data for each cluster * @property {Function} onDataChange - Callback function when data changes */ interface SettingsProps { @@ -20,13 +22,15 @@ interface SettingsProps { string, { image?: string; + namespace?: string; + isEnabled?: boolean; } >; onDataChange: (newData: SettingsProps['data']) => void; } /** - * Settings component for configuring Prometheus metrics. + * Settings component for configuring Node-Shell Action. */ export function Settings(props: SettingsProps) { const { data, onDataChange } = props; @@ -44,14 +48,33 @@ export function Settings(props: SettingsProps) { if (selectedCluster && !data?.[selectedCluster]) { onDataChange({ ...data, - [selectedCluster]: { image: DEFAULT_NODE_SHELL_LINUX_IMAGE}, + [selectedCluster]: { image: DEFAULT_NODE_SHELL_LINUX_IMAGE }, }); } }, [selectedCluster, data, onDataChange]); const selectedClusterData = data?.[selectedCluster] || {}; + const isEnabled = selectedClusterData.isEnabled ?? true; const settingsRows = [ + { + name: 'Enable Node Shell', + value: ( + { + const newEnabled = e.target.checked; + onDataChange({ + ...(data || {}), + [selectedCluster]: { + ...((data || {})[selectedCluster] || {}), + isEnabled: newEnabled, + }, + }); + }} + /> + ), + }, { name: 'Node Shell Linux Image', value: ( @@ -68,7 +91,29 @@ export function Settings(props: SettingsProps) { }); }} placeholder={DEFAULT_NODE_SHELL_LINUX_IMAGE} - helperText={'The default image is used for dropping a shell into a node (when not specified directly).'} + helperText={ + 'The default image is used for dropping a shell into a node (when not specified directly).' + } + /> + ), + }, + { + name: 'Namespace', + value: ( + { + const newNamespace = e.target.value; + onDataChange({ + ...(data || {}), + [selectedCluster]: { + ...((data || {})[selectedCluster] || {}), + namespace: newNamespace, + }, + }); + }} + placeholder={DEFAULT_NODE_SHELL_NAMESPACE} + helperText={'The default namespace is kube-system.'} /> ), }, diff --git a/node-shell/src/index.tsx b/node-shell/src/index.tsx index 47ed09920..7706efd66 100644 --- a/node-shell/src/index.tsx +++ b/node-shell/src/index.tsx @@ -1,4 +1,7 @@ -import { registerDetailsViewHeaderActionsProcessor, registerPluginSettings} from '@kinvolk/headlamp-plugin/lib'; +import { + registerDetailsViewHeaderActionsProcessor, + registerPluginSettings, +} from '@kinvolk/headlamp-plugin/lib'; import { NodeShellAction } from './components/NodeShellAction'; import { Settings } from './components/Settings'; @@ -10,8 +13,8 @@ registerDetailsViewHeaderActionsProcessor((resource, actions) => { return actions; } - if (resource.kind !== "Node") { - return actions + if (resource.kind !== 'Node') { + return actions; } actions.splice(0, 0, { diff --git a/node-shell/src/util.ts b/node-shell/src/util.ts index 6f9db2fd2..056d04d70 100644 --- a/node-shell/src/util.ts +++ b/node-shell/src/util.ts @@ -4,17 +4,18 @@ export const PLUGIN_NAME = 'node-shell'; /** * ClusterData type represents the configuration data for a cluster. - * @property {boolean} autoDetect - Whether to auto-detect Prometheus metrics. - * @property {boolean} isMetricsEnabled - Whether metrics are enabled for the cluster. - * @property {string} address - The address of the Prometheus service. - * @property {string} defaultTimespan - The default timespan for metrics. + * @property {boolean} isEnabled - Whether node-shell is enabled for the cluster. + * @property {string} image - Image to create the node shell. + * @property {string} namespace - The namespace to spawn the pod to create a node shell. */ type ClusterData = { image?: string; + namespace?: string; + isEnabled?: boolean; }; /** - * Conf type represents the configuration data for the prometheus plugin. + * Conf type represents the configuration data for the node-shell plugin. * @property {[cluster: string]: ClusterData} - The configuration data for each cluster. */ type Conf = { @@ -22,7 +23,17 @@ type Conf = { }; /** - * getConfigStore returns the config store for the prometheus plugin. + * isEnabled checks if node-shell is enabled for a specific cluster. + * @param {string} cluster - The name of the cluster. + * @returns {boolean} True or null if node-shell is enabled, false otherwise. + */ +export function isEnabled(cluster: string): boolean { + const clusterData = getClusterConfig(cluster); + return clusterData?.isEnabled ?? true; +} + +/** + * getConfigStore returns the config store for the node-shell plugin. * @returns {ConfigStore} The config store. */ export function getConfigStore(): ConfigStore {