Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frontend: Add attach to pod option #1030

Merged
merged 1 commit into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 53 additions & 28 deletions frontend/src/components/common/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const useStyle = makeStyles(theme => ({

interface TerminalProps extends DialogProps {
item: Pod;
isAttach?: boolean;
onClose?: () => void;
}

Expand All @@ -72,11 +73,11 @@ interface XTerminalConnected {
type execReturn = ReturnType<Pod['exec']>;

export default function Terminal(props: TerminalProps) {
const { item, onClose, ...other } = props;
const { item, onClose, isAttach, ...other } = props;
const classes = useStyle();
const [terminalContainerRef, setTerminalContainerRef] = React.useState<HTMLElement | null>(null);
const [container, setContainer] = React.useState<string | null>(null);
const execRef = React.useRef<execReturn | null>(null);
const execOrAttachRef = React.useRef<execReturn | null>(null);
const fitAddonRef = React.useRef<FitAddon | null>(null);
const xtermRef = React.useRef<XTerminalConnected | null>(null);
const [shells, setShells] = React.useState({
Expand Down Expand Up @@ -119,7 +120,7 @@ export default function Terminal(props: TerminalProps) {
}
}

if (arg.type === 'keydown' && arg.code === 'Enter') {
if (!isAttach && arg.type === 'keydown' && arg.code === 'Enter') {
if (xtermRef.current?.reconnectOnEnter) {
setShells(shells => ({
...shells,
Expand All @@ -137,7 +138,7 @@ export default function Terminal(props: TerminalProps) {
}

function send(channel: number, data: string) {
const socket = execRef.current!.getSocket();
const socket = execOrAttachRef.current!.getSocket();

// We should only send data if the socket is ready.
if (!socket || socket.readyState !== 1) {
Expand All @@ -162,21 +163,20 @@ export default function Terminal(props: TerminalProps) {
// The first byte is discarded because it just identifies whether
// this data is from stderr, stdout, or stdin.
const data = bytes.slice(1);
const text = decoder.decode(data);

if (bytes.byteLength < 2) {
return;
}
let text = decoder.decode(data);

// to check if we are connecting to the socket for the first time
let firstConnect = false;
// Send resize command to server once connection is establised.
if (!xtermc.connected && !!text) {
if (!xtermc.connected) {
xterm.clear();
(async function () {
send(4, `{"Width":${xterm.cols},"Height":${xterm.rows}}`);
})();
// On server error, don't set it as connected
if (channel !== Channel.ServerError) {
xtermc.connected = true;
firstConnect = true;
console.debug('Terminal is now connected');
joaquimrocha marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand All @@ -186,8 +186,8 @@ export default function Terminal(props: TerminalProps) {
onClose();
}

if (execRef.current) {
execRef.current?.cancel();
if (execOrAttachRef.current) {
execOrAttachRef.current?.cancel();
}

return;
Expand All @@ -197,12 +197,22 @@ export default function Terminal(props: TerminalProps) {
shellConnectFailed(xtermc);
return;
}

if (isAttach) {
// in case of attach if we didn't recieve any data from the process we should notify the user that if any data comes
// we will be showing it in the terminal
if (firstConnect && !text) {
text =
t(
"Any new output for this container's process should be shown below. In case it doesn't show up, press enter…"
) + '\r\n';
}
text = text.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
}
xterm.write(text);
}

function tryNextShell() {
if (shells.available.length > 0) {
if (!isAttach && shells.available.length > 0) {
setShells(currentShell => ({
...currentShell,
currentIdx: (currentShell.currentIdx + 1) % currentShell.available.length,
Expand Down Expand Up @@ -259,7 +269,7 @@ export default function Terminal(props: TerminalProps) {

if (xtermRef.current) {
xtermRef.current.xterm.dispose();
execRef.current?.cancel();
execOrAttachRef.current?.cancel();
}

const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator?.platform) >= 0;
Expand All @@ -278,17 +288,28 @@ export default function Terminal(props: TerminalProps) {
fitAddonRef.current = new FitAddon();
xtermRef.current.xterm.loadAddon(fitAddonRef.current);

const command = getCurrentShellCommand();

xtermRef.current.xterm.writeln(t('Trying to run "{{command}}"…', { command }) + '\n');

(async function () {
execRef.current = await item.exec(
container,
(items: ArrayBuffer) => onData(xtermRef.current!, items),
{ command: [command], failCb: () => shellConnectFailed(xtermRef.current!) }
);

if (isAttach) {
xtermRef?.current?.xterm.writeln(
t('Trying to attach to the container {{ container }}…', { container }) + '\n'
);

execOrAttachRef.current = await item.attach(
container,
(items: ArrayBuffer) => onData(xtermRef.current!, items),
{ failCb: () => shellConnectFailed(xtermRef.current!) }
);
} else {
const command = getCurrentShellCommand();

xtermRef?.current?.xterm.writeln(t('Trying to run "{{command}}"…', { command }) + '\n');

execOrAttachRef.current = await item.exec(
container,
(items: ArrayBuffer) => onData(xtermRef.current!, items),
{ command: [command], failCb: () => shellConnectFailed(xtermRef.current!) }
);
}
setupTerminal(terminalContainerRef, xtermRef.current!.xterm, fitAddonRef.current!);
})();

Expand All @@ -300,7 +321,7 @@ export default function Terminal(props: TerminalProps) {

return function cleanup() {
xtermRef.current?.xterm.dispose();
execRef.current?.cancel();
execOrAttachRef.current?.cancel();
window.removeEventListener('resize', handler);
};
},
Expand All @@ -319,7 +340,7 @@ export default function Terminal(props: TerminalProps) {
);

React.useEffect(() => {
if (shells.available.length === 0) {
if (!isAttach && shells.available.length === 0) {
setShells({
available: getAvailableShells(),
currentIdx: 0,
Expand Down Expand Up @@ -384,7 +405,11 @@ export default function Terminal(props: TerminalProps) {
}}
keepMounted
withFullScreen
title={t('Terminal: {{ itemName }}', { itemName: item.metadata.name })}
title={
isAttach
? t('Attach: {{ itemName }}', { itemName: item.metadata.name })
: t('Terminal: {{ itemName }}', { itemName: item.metadata.name })
}
{...other}
>
<DialogContent className={classes.dialogContent}>
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/components/pod/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export default function PodDetails(props: PodDetailsProps) {
const [showLogs, setShowLogs] = React.useState(!!showLogsDefault);
const [showTerminal, setShowTerminal] = React.useState(false);
const { t } = useTranslation('glossary');
const [isAttached, setIsAttached] = React.useState(false);

return (
<DetailsGrid
Expand All @@ -314,6 +315,13 @@ export default function PodDetails(props: PodDetailsProps) {
</IconButton>
</Tooltip>
</AuthVisible>,
<AuthVisible item={item} authVerb="get" subresource="attach">
<Tooltip title={t('Attach') as string}>
<IconButton aria-label={t('attach') as string} onClick={() => setIsAttached(true)}>
<Icon icon="mdi:connection" />
</IconButton>
</Tooltip>
</AuthVisible>,
]
}
extraInfo={item =>
Expand Down Expand Up @@ -356,9 +364,13 @@ export default function PodDetails(props: PodDetailsProps) {
/>
<Terminal
key="terminal"
open={showTerminal}
open={showTerminal || isAttached}
item={item}
onClose={() => setShowTerminal(false)}
onClose={() => {
setShowTerminal(false);
setIsAttached(false);
}}
isAttach={isAttached}
/>
</>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ exports[`Storyshots Pod/PodDetailsView Error 1`] = `
<div
class="MuiGrid-root MuiGrid-item"
/>
<div
class="MuiGrid-root MuiGrid-item"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -949,6 +952,9 @@ exports[`Storyshots Pod/PodDetailsView Initializing 1`] = `
<div
class="MuiGrid-root MuiGrid-item"
/>
<div
class="MuiGrid-root MuiGrid-item"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -2040,6 +2046,9 @@ exports[`Storyshots Pod/PodDetailsView Liveness Failed 1`] = `
<div
class="MuiGrid-root MuiGrid-item"
/>
<div
class="MuiGrid-root MuiGrid-item"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -3006,6 +3015,9 @@ exports[`Storyshots Pod/PodDetailsView Pull Back Off 1`] = `
<div
class="MuiGrid-root MuiGrid-item"
/>
<div
class="MuiGrid-root MuiGrid-item"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -3841,6 +3853,9 @@ exports[`Storyshots Pod/PodDetailsView Running 1`] = `
<div
class="MuiGrid-root MuiGrid-item"
/>
<div
class="MuiGrid-root MuiGrid-item"
/>
</div>
</div>
</div>
Expand Down Expand Up @@ -4713,6 +4728,9 @@ exports[`Storyshots Pod/PodDetailsView Successful 1`] = `
<div
class="MuiGrid-root MuiGrid-item"
/>
<div
class="MuiGrid-root MuiGrid-item"
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ exports[`Storyshots Pod/PodLogs Logs 1`] = `
<div
class="MuiGrid-root MuiGrid-item"
/>
<div
class="MuiGrid-root MuiGrid-item"
/>
<div
class="MuiGrid-root MuiGrid-item"
>
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/lib/k8s/pod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,18 @@ class Pod extends makeKubeObject<KubePod>('Pod') {
return cancel;
}

attach(container: string, onAttach: StreamResultsCb, options: StreamArgs = {}) {
const url = `/api/v1/namespaces/${this.getNamespace()}/pods/${this.getName()}/attach?container=${container}&stdin=true&stderr=true&stdout=true&tty=true`;
const additionalProtocols = [
'v4.channel.k8s.io',
'v3.channel.k8s.io',
'v2.channel.k8s.io',
'channel.k8s.io',
];

return stream(url, onAttach, { additionalProtocols, isJson: false, ...options });
}

exec(container: string, onExec: StreamResultsCb, options: ExecOptions = {}) {
const { command = ['sh'], ...streamOpts } = options;
const commandStr = command.map(item => '&command=' + encodeURIComponent(item)).join('');
Expand Down