Skip to content

Commit

Permalink
frontend: Add attach to pod
Browse files Browse the repository at this point in the history
We add a new button to the pod details page that allows the user to
attach to the pod's container(by default the first available one),
using the terminal component. We also update the terminal component
to accept a new prop called isAttach which allows us to differentiate
b/w a exec and a attach session.
  • Loading branch information
ashu8912 committed Mar 20, 2023
1 parent ece7c84 commit f05c54d
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 23 deletions.
75 changes: 54 additions & 21 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 @@ -76,7 +77,7 @@ export default function Terminal(props: TerminalProps) {
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 execAndAttachRef = 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 @@ -137,7 +138,7 @@ export default function Terminal(props: TerminalProps) {
}

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

// We should only send data if the socket is ready.
if (!socket || socket.readyState !== 1) {
Expand All @@ -162,9 +163,9 @@ 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);
let text = decoder.decode(data);

if (bytes.byteLength < 2) {
if (bytes.byteLength < 1) {
return;
}

Expand All @@ -177,7 +178,6 @@ export default function Terminal(props: TerminalProps) {
// On server error, don't set it as connected
if (channel !== Channel.ServerError) {
xtermc.connected = true;
console.debug('Terminal is now connected');
}
}

Expand All @@ -186,8 +186,8 @@ export default function Terminal(props: TerminalProps) {
onClose();
}

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

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

xterm.write(text);
// replace \n with \r only if there is \r before \n
// give me the regular expression for the above
text.replace(/\r\n/g, '\r').replace(/\n/g, '\r');
if (props.isAttach) {
// in case of attach if we didn't recieve any data from the process we should try to reconnect
if (!text) {
text = "If you don't see a command prompt, try pressing enter.";
}
// in case of attach we should replace \n with \r\n because it's not formatted the way it is for exec so we have to manually do this
xterm.write(
text.replace(/(\r)?\n/g, function (match, p1) {
// If there is a \r before \n, replace \n with \r\n
if (p1 === '\r') {
return '';
}
// Otherwise, just return \r\n
else {
return '\r\n';
}
})
);
} else {
xterm.write(text);
}
}

function tryNextShell() {
Expand Down Expand Up @@ -259,7 +281,7 @@ export default function Terminal(props: TerminalProps) {

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

const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator?.platform) >= 0;
Expand All @@ -278,17 +300,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 (props.isAttach) {
xtermRef?.current?.xterm.writeln(
t(`Trying to attach to the container ${container}...`, { container }) + '\n'
);

execAndAttachRef.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');

execAndAttachRef.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 +333,7 @@ export default function Terminal(props: TerminalProps) {

return function cleanup() {
xtermRef.current?.xterm.dispose();
execRef.current?.cancel();
execAndAttachRef.current?.cancel();
window.removeEventListener('resize', handler);
};
},
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

0 comments on commit f05c54d

Please sign in to comment.