Skip to content

Commit dff0ea0

Browse files
authored
Merge pull request #4168 from dorser/dorser/node-debugger
Add attach-based node debugger workflow that mirrors kubectl debug node
2 parents 56a1508 + 9f4ec16 commit dff0ea0

29 files changed

+130
-83
lines changed

frontend/src/components/App/Settings/NodeShellSettings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export default function NodeShellSettings(props: SettingsProps) {
238238
error={!isValidNamespace}
239239
helperText={
240240
isValidNamespace
241-
? t('translation|The default namespace is kube-system.')
241+
? t('translation|The default namespace is default.')
242242
: invalidNamespaceMessage
243243
}
244244
variant="outlined"

frontend/src/components/App/Settings/__snapshots__/NodeShellSettings.Default.stories.storyshot

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
aria-invalid="false"
8686
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputSizeSmall MuiInputBase-inputAdornedEnd css-19qh8xo-MuiInputBase-input-MuiOutlinedInput-input"
8787
id=":mock-test-id:"
88-
placeholder="docker.io/library/alpine:latest"
88+
placeholder="docker.io/library/busybox:latest"
8989
type="text"
9090
value=""
9191
/>
@@ -131,7 +131,7 @@
131131
aria-invalid="false"
132132
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputSizeSmall MuiInputBase-inputAdornedEnd css-19qh8xo-MuiInputBase-input-MuiOutlinedInput-input"
133133
id=":mock-test-id:"
134-
placeholder="kube-system"
134+
placeholder="default"
135135
type="text"
136136
value=""
137137
/>
@@ -154,7 +154,7 @@
154154
class="MuiFormHelperText-root MuiFormHelperText-sizeSmall MuiFormHelperText-contained css-153yz37-MuiFormHelperText-root"
155155
id=":mock-test-id:"
156156
>
157-
The default namespace is kube-system.
157+
The default namespace is default.
158158
</p>
159159
</div>
160160
</dd>

frontend/src/components/node/NodeShellAction.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,12 @@ export function NodeShellAction(props: NodeShellTerminalProps) {
6767
<ActionButton
6868
description={
6969
isLinux(item)
70-
? t('Node Shell')
71-
: t('Node shell is not supported in this OS: {{ nodeOS }}', {
70+
? t('Debug Node')
71+
: t('Debug node is not supported in this OS: {{ nodeOS }}', {
7272
nodeOS: item?.status?.nodeInfo?.operatingSystem,
7373
})
7474
}
75-
icon="mdi:console"
75+
icon="mdi:bug"
7676
onClick={() => {
7777
Activity.launch({
7878
id: activityId,

frontend/src/components/node/NodeShellTerminal.tsx

Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import { getCluster } from '../../lib/cluster';
2929
import { apply } from '../../lib/k8s/api/v1/apply';
3030
import { stream, StreamResultsCb } from '../../lib/k8s/api/v1/streamingApi';
3131
import Node from '../../lib/k8s/node';
32-
import Pod, { KubePod } from '../../lib/k8s/pod';
32+
import { KubePod } from '../../lib/k8s/pod';
3333

3434
const decoder = new TextDecoder('utf-8');
3535
const encoder = new TextEncoder();
@@ -51,7 +51,6 @@ interface XTerminalConnected {
5151
xterm: XTerminal;
5252
connected: boolean;
5353
reconnectOnEnter: boolean;
54-
onClose?: () => void;
5554
}
5655

5756
const shellPod = (name: string, namespace: string, nodeName: string, nodeShellImage: string) => {
@@ -65,7 +64,7 @@ const shellPod = (name: string, namespace: string, nodeName: string, nodeShellIm
6564
spec: {
6665
nodeName,
6766
restartPolicy: 'Never',
68-
terminationGracePeriodSeconds: 0,
67+
terminationGracePeriodSeconds: 30,
6968
hostPID: true,
7069
hostIPC: true,
7170
hostNetwork: true,
@@ -74,26 +73,45 @@ const shellPod = (name: string, namespace: string, nodeName: string, nodeShellIm
7473
operator: 'Exists',
7574
},
7675
],
77-
priorityClassName: 'system-node-critical',
7876
containers: [
7977
{
80-
name: 'shell',
78+
name: 'debugger',
8179
image: nodeShellImage,
82-
securityContext: {
83-
privileged: true,
80+
terminationMessagePolicy: 'File',
81+
tty: true,
82+
stdin: true,
83+
stdinOnce: true,
84+
volumeMounts: [
85+
{
86+
mountPath: '/host',
87+
name: 'host-root',
88+
},
89+
],
90+
},
91+
],
92+
volumes: [
93+
{
94+
name: 'host-root',
95+
hostPath: {
96+
path: '/',
97+
type: 'Directory',
8498
},
85-
command: ['nsenter'],
86-
args: ['-t', '1', '-m', '-u', '-i', '-n', 'sleep', '14000'],
8799
},
88100
],
89101
},
90102
} as unknown as KubePod;
91103
};
92104

93105
function uniqueString() {
94-
const timestamp = Date.now().toString(36);
95-
const randomNum = Math.random().toString(36).substring(2, 5);
96-
return `${timestamp}-${randomNum}`;
106+
const alphabet = '23456789abcdefghjkmnpqrstuvwxyz';
107+
let res = '';
108+
109+
for (let i = 0; i < 5; i++) {
110+
const idx = Math.floor(Math.random() * alphabet.length);
111+
res += alphabet[idx];
112+
}
113+
114+
return res;
97115
}
98116

99117
async function shell(item: Node, onExec: StreamResultsCb) {
@@ -106,25 +124,19 @@ async function shell(item: Node, onExec: StreamResultsCb) {
106124
const config = clusterSettings.nodeShellTerminal;
107125
const linuxImage = config?.linuxImage || DEFAULT_NODE_SHELL_LINUX_IMAGE;
108126
const namespace = config?.namespace || DEFAULT_NODE_SHELL_NAMESPACE;
109-
const podName = `node-shell-${item.getName()}-${uniqueString()}`;
110-
const kubePod = shellPod(podName, namespace, item.getName(), linuxImage!!);
127+
const podName = `node-debugger-${item.getName()}-${uniqueString()}`;
128+
const kubePod = shellPod(podName, namespace, item.getName(), linuxImage);
111129
try {
112130
await apply(kubePod);
113131
} catch (e) {
114-
console.error('Error:NodeShell: creating pod', e);
132+
console.error('Error:DebugNode: creating pod', e);
115133
return {};
116134
}
117-
const command = [
118-
'sh',
119-
'-c',
120-
'((clear && bash) || (clear && zsh) || (clear && ash) || (clear && sh))',
121-
];
122135
const tty = true;
123136
const stdin = true;
124137
const stdout = true;
125138
const stderr = true;
126-
const commandStr = command.map(item => '&command=' + encodeURIComponent(item)).join('');
127-
const url = `/api/v1/namespaces/${namespace}/pods/${podName}/exec?container=shell${commandStr}&stdin=${
139+
const url = `/api/v1/namespaces/${namespace}/pods/${podName}/attach?container=debugger&stdin=${
128140
stdin ? 1 : 0
129141
}&stderr=${stderr ? 1 : 0}&stdout=${stdout ? 1 : 0}&tty=${tty ? 1 : 0}`;
130142
const additionalProtocols = [
@@ -133,18 +145,8 @@ async function shell(item: Node, onExec: StreamResultsCb) {
133145
'v2.channel.k8s.io',
134146
'channel.k8s.io',
135147
];
136-
const onClose = async () => {
137-
const pod = new Pod(kubePod);
138-
try {
139-
await pod.delete();
140-
} catch (e) {
141-
console.error('Error:NodeShell: deleting pod', e);
142-
return {};
143-
}
144-
};
145148
return {
146149
stream: stream(url, onExec, { additionalProtocols, isJson: false }),
147-
onClose: onClose,
148150
};
149151
}
150152

@@ -154,15 +156,45 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) {
154156
const xtermRef = useRef<XTerminalConnected | null>(null);
155157
const fitAddonRef = useRef<FitAddon | null>(null);
156158
const streamRef = useRef<any | null>(null);
159+
const exitSentRef = useRef(false);
160+
const pendingExitRef = useRef(false);
161+
162+
const sendExitIfPossible = () => {
163+
if (exitSentRef.current) {
164+
return true;
165+
}
166+
167+
const socket = streamRef.current?.getSocket();
168+
if (!socket || socket.readyState !== WebSocket.OPEN) {
169+
return false;
170+
}
171+
172+
send(0, 'exit\r');
173+
exitSentRef.current = true;
174+
pendingExitRef.current = false;
175+
setTimeout(() => streamRef.current?.cancel(), 1000);
176+
return true;
177+
};
178+
179+
const requestShellExit = (reason: string) => {
180+
if (exitSentRef.current) {
181+
return;
182+
}
183+
184+
const sent = sendExitIfPossible();
185+
if (!sent) {
186+
console.debug('Queueing exit for shell (not yet connected)', { reason });
187+
pendingExitRef.current = true;
188+
} else {
189+
console.debug('Exit command sent to shell', { reason });
190+
}
191+
};
157192

158193
const wrappedOnClose = () => {
194+
requestShellExit('dialog-close');
159195
if (!!onClose) {
160196
onClose();
161197
}
162-
163-
if (!!xtermRef.current?.onClose) {
164-
xtermRef.current?.onClose();
165-
}
166198
};
167199

168200
// @todo: Give the real exec type when we have it.
@@ -260,6 +292,9 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) {
260292
if (channel !== Channel.ServerError) {
261293
xtermc.connected = true;
262294
console.debug('Terminal is now connected');
295+
if (pendingExitRef.current && !exitSentRef.current) {
296+
sendExitIfPossible();
297+
}
263298
}
264299
}
265300

@@ -348,11 +383,10 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) {
348383

349384
(async function () {
350385
xtermRef?.current?.xterm.writeln('Trying to open a shell');
351-
const { stream, onClose } = await shell(item, (items: ArrayBuffer) =>
386+
const { stream } = await shell(item, (items: ArrayBuffer) =>
352387
onData(xtermRef.current!, items)
353388
);
354389
streamRef.current = stream;
355-
xtermRef.current!.onClose = onClose;
356390

357391
setupTerminal(terminalContainerRef, xtermRef.current!.xterm, fitAddonRef.current!);
358392
})();
@@ -364,6 +398,7 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) {
364398
window.addEventListener('resize', handler);
365399

366400
return function cleanup() {
401+
requestShellExit('component-unmount');
367402
xtermRef.current?.xterm.dispose();
368403
streamRef.current?.cancel();
369404
window.removeEventListener('resize', handler);
@@ -373,6 +408,18 @@ export function NodeShellTerminal(props: NodeShellTerminalProps) {
373408
[terminalContainerRef]
374409
);
375410

411+
useEffect(() => {
412+
const handleBeforeUnload = () => {
413+
requestShellExit('window-beforeunload');
414+
};
415+
416+
window.addEventListener('beforeunload', handleBeforeUnload);
417+
418+
return () => {
419+
window.removeEventListener('beforeunload', handleBeforeUnload);
420+
};
421+
}, []);
422+
376423
return (
377424
<DialogContent
378425
sx={theme => ({

frontend/src/helpers/clusterSettings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export interface ClusterSettings {
3131
};
3232
}
3333

34-
export const DEFAULT_NODE_SHELL_LINUX_IMAGE = 'docker.io/library/alpine:latest';
35-
export const DEFAULT_NODE_SHELL_NAMESPACE = 'kube-system';
34+
export const DEFAULT_NODE_SHELL_LINUX_IMAGE = 'docker.io/library/busybox:latest';
35+
export const DEFAULT_NODE_SHELL_NAMESPACE = 'default';
3636

3737
/**
3838
* Stores the cluster settings in local storage.

frontend/src/i18n/locales/de/glossary.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@
147147
"OS image": "OS-Bild",
148148
"Kernel version": "Kernel-Version",
149149
"Container Runtime": "Container-Laufzeit",
150-
"Node Shell": "",
151-
"Node shell is not supported in this OS: {{ nodeOS }}": "",
150+
"Debug Node": "",
151+
"Debug node is not supported in this OS: {{ nodeOS }}": "",
152152
"Shell: {{ itemName }}": "",
153153
"Logs: {{ itemName }}": "Ereignisprotokolle: {{ itemName }}",
154154
"Seconds": "Sekunden",

frontend/src/i18n/locales/de/translation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
"Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Namespaces dürfen nur alphanumerische Kleinbuchstaben oder \"-\" enthalten und müssen mit einem alphanumerischen Zeichen beginnen und enden.",
129129
"Node Shell Settings": "",
130130
"The default image is used for dropping a shell into a node (when not specified directly).": "",
131-
"The default namespace is kube-system.": "",
131+
"The default namespace is default.": "",
132132
"Enter a value between {{ minRows }} and {{ maxRows }}.": "Geben Sie einen Wert zwischen {{ minRows }} und {{ maxRows }} ein.",
133133
"Custom row value": "Benutzerdefinierter Zeilenwert",
134134
"Custom value": "Benutzerdefinierter Wert",

frontend/src/i18n/locales/en/glossary.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@
147147
"OS image": "OS image",
148148
"Kernel version": "Kernel version",
149149
"Container Runtime": "Container Runtime",
150-
"Node Shell": "Node Shell",
151-
"Node shell is not supported in this OS: {{ nodeOS }}": "Node shell is not supported in this OS: {{ nodeOS }}",
150+
"Debug Node": "Debug Node",
151+
"Debug node is not supported in this OS: {{ nodeOS }}": "Debug node is not supported in this OS: {{ nodeOS }}",
152152
"Shell: {{ itemName }}": "Shell: {{ itemName }}",
153153
"Logs: {{ itemName }}": "Logs: {{ itemName }}",
154154
"Seconds": "Seconds",

frontend/src/i18n/locales/en/translation.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
"Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.",
129129
"Node Shell Settings": "Node Shell Settings",
130130
"The default image is used for dropping a shell into a node (when not specified directly).": "The default image is used for dropping a shell into a node (when not specified directly).",
131-
"The default namespace is kube-system.": "The default namespace is kube-system.",
131+
"The default namespace is default.": "The default namespace is default.",
132132
"Enter a value between {{ minRows }} and {{ maxRows }}.": "Enter a value between {{ minRows }} and {{ maxRows }}.",
133133
"Custom row value": "Custom row value",
134134
"Custom value": "Custom value",

frontend/src/i18n/locales/es/glossary.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,8 @@
147147
"OS image": "Imagen del SO",
148148
"Kernel version": "Versión del Kernel",
149149
"Container Runtime": "Container Runtime",
150-
"Node Shell": "",
151-
"Node shell is not supported in this OS: {{ nodeOS }}": "",
150+
"Debug Node": "",
151+
"Debug node is not supported in this OS: {{ nodeOS }}": "",
152152
"Shell: {{ itemName }}": "",
153153
"Logs: {{ itemName }}": "Registros: {{ itemName }}",
154154
"Seconds": "Segundos",

0 commit comments

Comments
 (0)