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

Add UI changes for kube exec functionality. #41466

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open

Add UI changes for kube exec functionality. #41466

wants to merge 5 commits into from

Conversation

AntonAM
Copy link
Contributor

@AntonAM AntonAM commented May 13, 2024

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 the DocumentSsh. Also we allow new option for the terminal ConvertEol 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:
kubeexec_connect_dialog

Demo in action:

kube_exec_demo.mov

Changelog: Add ability to exec into a pod on a Kubernetes cluster using Web UI.

@AntonAM AntonAM force-pushed the kweb-ui branch 5 times, most recently from 5a2b16a to acc7bba Compare May 16, 2024 14:43
@AntonAM AntonAM marked this pull request as ready for review May 16, 2024 16:16
Copy link

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 changelog: followed by the changelog entries for the PR.

@public-teleport-github-review-bot

@AntonAM - this PR will require admin approval to merge due to its size. Consider breaking it up into a series smaller changes.

Copy link
Member

@ravicious ravicious left a 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();
Copy link
Member

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:

<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:

expect(screen.getByTestId('tooltip-msg')).toBeInTheDocument();

(Though using data-testid should be the last resort.)

Comment on lines +37 to +73
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} />
);
};
Copy link
Member

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.

Copy link
Contributor Author

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 🤷

Comment on lines +75 to +79
export type Props = {
ctx: TeleportContext;
consoleCtx: ConsoleCtx;
doc: stores.DocumentKubeExec;
};
Copy link
Member

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.

Copy link
Contributor Author

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

Comment on lines 81 to 118
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;
Copy link
Member

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.

exported-stories

</Toggle>
<ButtonPrimary onClick={startKubeExecSession}>
Run Command
</ButtonPrimary>
Copy link
Member

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>

@ravicious ravicious self-requested a review May 17, 2024 13:57
Copy link
Member

@ravicious ravicious left a 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/consoleContext.tsx Outdated Show resolved Hide resolved
pod: splits[1],
container: splits?.[2] || '',
isInteractive,
command,
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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%"
Copy link
Member

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).

Copy link
Member

@ravicious ravicious May 20, 2024

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.

Comment on lines 578 to 580
if (!params.container) {
params.container = undefined;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

@rosstimothy
Copy link
Contributor

Couple of high level notes without having looked too much at the code:

UX

The input with namespace/pod was a hacky thing we did during the initial POC. I think we should split them into two separate inputs here.

Security

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.

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:

  1. Connect button determines namespace and pod
  2. New tab is opened and connection upgrade occurs
  3. Server side waits for a message via the WS with the rest of the information
  4. UI presents some way for users to determine the command and whether an interactive session is required
  5. UI sends users additional input via the WS
  6. Server proceeds with connection to kube

@AntonAM
Copy link
Contributor Author

AntonAM commented May 22, 2024

@rosstimothy

UX:
I don't really see benefit of splitting input into two, namespace/pod seems like a natural format, also we have optional container one could specify with namespace/pod/container, which would require third input.

Security:
Ok, I'll look into splitting the flow 👍

@rosstimothy
Copy link
Contributor

UX: I don't really see benefit of splitting input into two, namespace/pod seems like a natural format, also we have optional container one could specify with namespace/pod/container, which would require third input.

@roraback do you have opinions about the design/UX in question?

@dmsergeevN26
Copy link

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!

@AntonAM
Copy link
Contributor Author

AntonAM commented May 28, 2024

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants