-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Add UI changes for kube exec functionality. #41466
base: master
Are you sure you want to change the base?
Conversation
5a2b16a
to
acc7bba
Compare
The PR changelog entry failed validation: Changelog entry not found in the PR body. Please add a "no-changelog" label to the PR, or changelog lines starting with |
@AntonAM - this PR will require admin approval to merge due to its size. Consider breaking it up into a series smaller changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll continue the review on Monday.
const { container } = render( | ||
<DocumentKubeExecWrapper ctx={ctx} consoleCtx={consoleCtx} doc={baseDoc} /> | ||
); | ||
expect(container.firstChild).toMatchSnapshot(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't view this strictly as a requirement since I don't think you usually work on frontend stuff, but if you have time then I'd timebox this to like 30–60 minutes and see how far you can get with the idea below, it'd help us out in the long term! If not then I guess the snapshot test is fine.
I understand that this was mostly copied from DocumentSsh
, but it'd be nice to add a regular test instead of a snapshot test, as we generally want to reduce the number of snapshot tests.
It could boil down to duplicating the setup from the story (so that the test does not depend on the story) and then performing a simple check. It could be just writing to the tty and then checking if that gets rendered on the screen.
If all else fails, the check could end up as data-testid="terminal"
added to the div rendered by Terminal
:
teleport/web/packages/teleport/src/Console/DocumentSsh/Terminal/Terminal.tsx
Lines 169 to 177 in 43d050b
<Flex | |
flexDirection="column" | |
height="100%" | |
width="100%" | |
px="2" | |
style={{ overflow: 'auto' }} | |
> | |
<StyledXterm ref={elementRef} /> | |
</Flex> |
…and then checking if the div got rendered on the screen:
teleport/web/packages/teleport/src/components/ToolTipNoPermBadge/ToolTipNoPermBadge.test.tsx
Line 37 in 43d050b
expect(screen.getByTestId('tooltip-msg')).toBeInTheDocument(); |
(Though using data-testid
should be the last resort.)
export const Connected = () => { | ||
const { ctx, consoleCtx } = getContexts(); | ||
|
||
return ( | ||
<DocumentKubeExecWrapper ctx={ctx} consoleCtx={consoleCtx} doc={baseDoc} /> | ||
); | ||
}; | ||
|
||
export const NotFound = () => { | ||
const { ctx, consoleCtx } = getContexts(); | ||
|
||
const disconnectedDoc = { | ||
...baseDoc, | ||
status: 'disconnected' as const, | ||
}; | ||
|
||
return ( | ||
<DocumentKubeExecWrapper | ||
ctx={ctx} | ||
consoleCtx={consoleCtx} | ||
doc={disconnectedDoc} | ||
/> | ||
); | ||
}; | ||
|
||
export const ServerError = () => { | ||
const { ctx, consoleCtx } = getContexts(); | ||
|
||
const noSidDoc = { | ||
...baseDoc, | ||
sid: '', | ||
}; | ||
|
||
return ( | ||
<DocumentKubeExecWrapper ctx={ctx} consoleCtx={consoleCtx} doc={noSidDoc} /> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In storybook, these three all render as the same to me, I'm not sure if that's what's supposed to happen.
I wanted to compare this against DocumentSsh
stories, but it looks like stories for that component have been broken for some time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, DocumentSsh
was broken, I've fixed it (PR fixing it: #41877 ) and followed same pattern, it also renders basically same 🤷
export type Props = { | ||
ctx: TeleportContext; | ||
consoleCtx: ConsoleCtx; | ||
doc: stores.DocumentKubeExec; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not used and can be removed I suppose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Forgot to use in the DocumentKubeExecWrapper
😅 , fixed 27fc01c
export const DocumentKubeExecWrapper = ({ ctx, consoleCtx, doc }) => { | ||
return ( | ||
<ContextProvider ctx={ctx}> | ||
<TestLayout ctx={consoleCtx}> | ||
<DocumentKubeExec doc={doc} visible={true} /> | ||
</TestLayout> | ||
</ContextProvider> | ||
); | ||
}; | ||
|
||
export function getContexts() { | ||
const ctx = createTeleportContext(); | ||
const consoleCtx = new ConsoleCtx(); | ||
const tty = consoleCtx.createTty(session); | ||
tty.connect = () => null; | ||
consoleCtx.createTty = () => tty; | ||
consoleCtx.storeUser = ctx.storeUser; | ||
|
||
return { ctx, consoleCtx }; | ||
} | ||
|
||
export const baseDoc = { | ||
kind: 'kubeExec', | ||
status: 'connected', | ||
sid: 'sid-value', | ||
clusterId: 'clusterId-value', | ||
serverId: 'serverId-value', | ||
login: 'login-value', | ||
kubeCluster: 'kubeCluster1', | ||
kubeNamespace: 'namespace1', | ||
pod: 'pod1', | ||
container: '', | ||
id: 3, | ||
url: 'fd', | ||
created: new Date(), | ||
command: '/bin/bash', | ||
isInteractive: true, | ||
} as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's one of the problems with exporting stuff out of stories and then importing it in tests. On a conceputal level, I think it's not what one's supposed to (though that's the pattern I think we've been following in a few places).
But a practical problem with it is that Storybook expects each exported member to be a React component, so we end up with a bunch of items that look like stories, but they just don't work.
</Toggle> | ||
<ButtonPrimary onClick={startKubeExecSession}> | ||
Run Command | ||
</ButtonPrimary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we wrap the form into <form>
, add onSubmit
on it and change the button's type to submit
and then add autofocus on the first field, then it'll be possible to navigate the form using keyboard and submit it with Enter.
Patch
Copy then pbpaste | git apply
diff --git a/web/packages/teleport/src/Kubes/ConnectDialog/ConnectDialog.tsx b/web/packages/teleport/src/Kubes/ConnectDialog/ConnectDialog.tsx
index eb06d90ba8..ed7e858f08 100644
--- a/web/packages/teleport/src/Kubes/ConnectDialog/ConnectDialog.tsx
+++ b/web/packages/teleport/src/Kubes/ConnectDialog/ConnectDialog.tsx
@@ -56,7 +56,8 @@ function ConnectDialog(props: Props) {
const [execCommand, setExecCommand] = useState('');
const [execInteractive, setExecInteractive] = useState(true);
- const startKubeExecSession = () => {
+ const startKubeExecSession = event => {
+ event.preventDefault();
const splitPath = execPath.split('/');
const url = cfg.getKubeExecConnectRoute(
@@ -136,58 +137,59 @@ function ConnectDialog(props: Props) {
<Validation>
{() => (
<Box borderTop={1} mb={4} mt={4}>
- <Text mt={3} bold>
- Or exec into a pod on this Kubernetes cluster
- </Text>
- <Flex gap={3}>
- <FieldInput
- value={execPath}
- placeholder="namespace/pod"
- label="Pod to exec into"
- width="50%"
- onChange={e => setExecPath(e.target.value.trim())}
- toolTipContent={
- <Text>
- Specify namespace and pod you want to exec into.
- Optionally you can also specify container by adding it at
- the end: '/namespace/pod/container'
- </Text>
- }
- />
- <FieldInput
- rule={requiredField('Command to execute is required')}
- value={execCommand}
- placeholder="/bin/bash"
- label="Command to execute"
- width="50%"
- onChange={e => setExecCommand(e.target.value)}
- toolTipContent={
- <Text>
- The command that will be executed inside the target pod.
- </Text>
- }
- />
- </Flex>
- <Flex justifyContent="space-between" gap={3}>
- <Toggle
- isToggled={execInteractive}
- onToggle={() => {
- setExecInteractive(b => !b);
- }}
- >
- <Box ml={2} mr={1}>
- Interactive shell
- </Box>
- <ToolTipInfo>
- You can start an interactive shell and have a bidirectional
- communication with the target pod, or you can run one-off
- command and see its output.
- </ToolTipInfo>
- </Toggle>
- <ButtonPrimary onClick={startKubeExecSession}>
- Run Command
- </ButtonPrimary>
- </Flex>
+ <form onSubmit={startKubeExecSession}>
+ <Text mt={3} bold>
+ Or exec into a pod on this Kubernetes cluster
+ </Text>
+ <Flex gap={3}>
+ <FieldInput
+ value={execPath}
+ placeholder="namespace/pod"
+ autoFocus
+ label="Pod to exec into"
+ width="50%"
+ onChange={e => setExecPath(e.target.value.trim())}
+ toolTipContent={
+ <Text>
+ Specify namespace and pod you want to exec into.
+ Optionally you can also specify container by adding it
+ at the end: '/namespace/pod/container'
+ </Text>
+ }
+ />
+ <FieldInput
+ rule={requiredField('Command to execute is required')}
+ value={execCommand}
+ placeholder="/bin/bash"
+ label="Command to execute"
+ width="50%"
+ onChange={e => setExecCommand(e.target.value)}
+ toolTipContent={
+ <Text>
+ The command that will be executed inside the target pod.
+ </Text>
+ }
+ />
+ </Flex>
+ <Flex justifyContent="space-between" gap={3}>
+ <Toggle
+ isToggled={execInteractive}
+ onToggle={() => {
+ setExecInteractive(b => !b);
+ }}
+ >
+ <Box ml={2} mr={1}>
+ Interactive shell
+ </Box>
+ <ToolTipInfo>
+ You can start an interactive shell and have a
+ bidirectional communication with the target pod, or you
+ can run one-off command and see its output.
+ </ToolTipInfo>
+ </Toggle>
+ <ButtonPrimary type="submit">Run Command</ButtonPrimary>
+ </Flex>
+ </form>
</Box>
)}
</Validation>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The biggest problem for me with this PR is that it adds the ability to execute an arbitrary command in a pod by opening the app with a specifically crafted address, but the PR description doesn't mention any considerations regarding the security of this feature.
In Connect, we had support for something like this (but with ssh servers), but we since removed it (see https://github.com/gravitational/teleport-private/issues/176, though the vulnerability persists).
web/packages/teleport/src/Console/DocumentKubeExec/useKubeExecSession.tsx
Outdated
Show resolved
Hide resolved
pod: splits[1], | ||
container: splits?.[2] || '', | ||
isInteractive, | ||
command, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I understand, this accepts any command over a GET request and executes it on the pod without any further input. Have we considered the security implications of it? This feels worthy of an RFD. I tried looking for one, but I couldn't find any about this feature.
We had a slack thread about a similar request from a customer about a month ago.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not visible in the demo video for this PR, but in another one ( https://drive.google.com/file/d/1x517hgCgGsYsp89rzPOo3BQNfpFAasGe/view?usp=drive_link ) it's visible that session MFA is supported for this and you are required to perform the check, according to the cluster/role rules. So there's additional layer of protection for risk-averse clients. (we also until very recently had an assist endpoint to run a ssh command on nodes, but looks like it was deleted)
As I mentioned above, this is an MVP of the feature, and generally we want to change it into more shell-like experience, where you've given a terminal and you can run kubectl ...
commands there, which would eliminate "command in the url" pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the desire to validate an MVP and the support for MFA is a good thing to have there. But at the moment it seems to be a feature that's enabled by default which we didn't put under as much scrutiny as features that went through the RFD process.
If it was an opt-in feature, that would somewhat change things, but I do realize that having to turn it on would make it much less appealing.
value={execPath} | ||
placeholder="namespace/pod" | ||
label="Pod to exec into" | ||
width="50%" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit, but the parent of this element is already a flex container, so you can use flex="1"
instead to give both inputs the same weight and distribute their width accordingly within the parent.
It doesn't make much difference here, but it's a better approach for any other more complex set of divs (as you don't have to calculate the percentages yourself and operate on weights instead, plus a whole other slew of features that flexbox gives you).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those changes to imports in this file seem unnecessary.
It looks like some kind of editor plugin for sorting them alphabetically, which I'd love to enforce down the line with some kind of autofixable eslint rule.
web/packages/teleport/src/config.ts
Outdated
if (!params.container) { | ||
params.container = undefined; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed?
Couple of high level notes without having looked too much at the code: UXThe input with Security
I also agree with Rafal on the above. I think we need some user interaction to prevent people from being phished with a malicious link. One option that jumps to mind would be to split the command input and interactive toggle out of the connect modal and be required input on the new tab. So the flow would be the following:
|
UX: Security: |
@roraback do you have opinions about the design/UX in question? |
Amazing! Is it possible to add support for kubectl debug in addition/instead of exec? Debug is the preferred way to debug pods nowadays. Does this support moderation? I hope my questions are not out of place! Thank you! |
@dmsergeevN26 We're currently evaluating paths to expand kubernetes access in Web UI, this is our first step in this direction. Debug functionality might be implemented in the future. You're feedback is welcomed! |
This PR adds UI for the Kube Exec functionality (backend part was added in #41144).
Mainly it changes appearance of connect dialog for the kube resource and adds new type of document for the console -
DocumentKubeExec
, which is very similar to theDocumentSsh
. Also we allow new option for the terminalConvertEol
that correctly displays output for non-interactive shell, because we get just\n
instead of\r\n
newlines in that mode.UI changes for the Connect dialog:
Demo in action:
kube_exec_demo.mov
Changelog: Add ability to exec into a pod on a Kubernetes cluster using Web UI.