feat: Add a Web Shell terminal to Pod's Container Cards
Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> Update .changeset/forty-rocks-jog.md Co-authored-by: Jamie Klassen <jklassen@vmware.com> Signed-off-by: Carlos Esteban Lopez Jaramillo <luchillo17@gmail.com> fix: Address PR comments Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> fix: Use sh instead of bash Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> fix: Move K8sTerminal into a dialog Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> fix: Use discoveryApi instead of configApi Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> fix: Update api-report.md Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> fix: Revert PodDrawer.tsx Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> test: Polyfill TextEncoder Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> feat: Refactor K8sTerminal inputs to receive only what it needs Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> test: Add tests Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> Update plugins/kubernetes/src/components/K8sTerminal/K8sTerminal.test.tsx Co-authored-by: Jamie Klassen <jklassen@vmware.com> Signed-off-by: Carlos Esteban Lopez Jaramillo <luchillo17@gmail.com> test: Fix tests for K8sTerminal Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> test: Fix isolated modules error Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> fix: Update api reports Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> Update plugins/kubernetes/src/components/K8sTerminal/K8sTerminalDialog.tsx Co-authored-by: Jamie Klassen <jklassen@vmware.com> Signed-off-by: Carlos Esteban Lopez Jaramillo <luchillo17@gmail.com> Update plugins/kubernetes/src/components/K8sTerminal/K8sTerminal.test.tsx Co-authored-by: Jamie Klassen <jklassen@vmware.com> Signed-off-by: Carlos Esteban Lopez Jaramillo <luchillo17@gmail.com> fix: Check for cluster size 1 & small refactors Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> chore: Update changeset Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> fix: Check client side auth providers to disable web shell Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> fix: Move K8sAPI logic to custom hook with mock Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> test: Fix hook mock warning & add internal tag for mock Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> Update plugins/kubernetes/src/components/K8sTerminal/K8sTerminal.test.tsx Co-authored-by: Jamie Klassen <jklassen@vmware.com> Signed-off-by: Carlos Esteban Lopez Jaramillo <luchillo17@gmail.com> Update .changeset/forty-rocks-jog.md Co-authored-by: Jamie Klassen <jklassen@vmware.com> Signed-off-by: Carlos Esteban Lopez Jaramillo <luchillo17@gmail.com> Update plugins/kubernetes/src/components/K8sTerminal/K8sTerminal.test.tsx Co-authored-by: Jamie Klassen <jklassen@vmware.com> Signed-off-by: Carlos Esteban Lopez Jaramillo <luchillo17@gmail.com> refactor: Rename stuff as requested in PR Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> chore: Update API Reports Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com> test: Fix mocked hook that was renamed Signed-off-by: Carlos Esteban Lopez <lcarlosesteb@vmware.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import './matchMedia.mock';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
renderWithEffects,
|
||||
TestApiProvider,
|
||||
textContentMatcher,
|
||||
wrapInTestApp,
|
||||
} from '@backstage/test-utils';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import WS from 'jest-websocket-mock';
|
||||
import React from 'react';
|
||||
|
||||
import { PodExecTerminal } from './PodExecTerminal';
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
describe('K8sTerminal', () => {
|
||||
const clusterName = 'cluster1';
|
||||
const containerName = 'container2';
|
||||
const podName = 'pod1';
|
||||
const podNamespace = 'podNamespace';
|
||||
|
||||
const mockDiscoveryApi: Partial<DiscoveryApi> = {
|
||||
getBaseUrl: () => Promise.resolve('http://localhost'),
|
||||
};
|
||||
|
||||
it('Should render an XTerm web terminal', async () => {
|
||||
const { getByText } = await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
|
||||
<PodExecTerminal
|
||||
clusterName={clusterName}
|
||||
containerName={containerName}
|
||||
podName={podName}
|
||||
podNamespace={podNamespace}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
getByText(textContentMatcher('Starting terminal, please wait...')),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should connect to WebSocket server & render response', async () => {
|
||||
const server = new WS(
|
||||
'ws://localhost/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh',
|
||||
);
|
||||
|
||||
const { getByText } = await renderWithEffects(
|
||||
wrapInTestApp(
|
||||
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
|
||||
<PodExecTerminal
|
||||
clusterName={clusterName}
|
||||
containerName={containerName}
|
||||
podName={podName}
|
||||
podNamespace={podNamespace}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
// xterm uses a "W" character as a "measure element" -- if it exists, the terminal rendered correctly
|
||||
expect(getByText(textContentMatcher('W'))).toBeInTheDocument();
|
||||
|
||||
await server.connected;
|
||||
|
||||
const { buffer } = Uint8Array.from([
|
||||
1,
|
||||
...textEncoder.encode('hello world'),
|
||||
]);
|
||||
|
||||
server.send(buffer);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByText(textContentMatcher('hello world'))).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import 'xterm/css/xterm.css';
|
||||
|
||||
import { discoveryApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
|
||||
import { PodExecTerminalAttachAddon } from './PodExecTerminalAttachAddon';
|
||||
|
||||
/**
|
||||
* Props drilled down to the K8sTerminal component
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface K8sTerminalProps {
|
||||
clusterName: string;
|
||||
containerName: string;
|
||||
podName: string;
|
||||
podNamespace: string;
|
||||
}
|
||||
|
||||
const hasSocketProtocol = (url: string | URL) =>
|
||||
/wss?:\/\//.test(url.toString());
|
||||
|
||||
/**
|
||||
* Opens a Terminal shell to the given pod's container in a Dialog
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PodExecTerminal = (props: K8sTerminalProps) => {
|
||||
const { containerName, podNamespace, podName } = props;
|
||||
|
||||
const [baseUrl, setBaseUrl] = useState(window.location.host);
|
||||
|
||||
const terminalRef = React.useRef(null);
|
||||
const discoveryApi = useApi(discoveryApiRef);
|
||||
const namespace = podNamespace ?? 'default';
|
||||
|
||||
useEffect(() => {
|
||||
discoveryApi
|
||||
.getBaseUrl('kubernetes')
|
||||
.then(url => url ?? window.location.host)
|
||||
.then(url => url.replace(/^http(s?):\/\//, 'ws$1://'))
|
||||
.then(url => setBaseUrl(url));
|
||||
}, [discoveryApi]);
|
||||
|
||||
const urlParams = useMemo(() => {
|
||||
const params = new URLSearchParams({
|
||||
container: containerName,
|
||||
stdin: 'true',
|
||||
stdout: 'true',
|
||||
stderr: 'true',
|
||||
tty: 'true',
|
||||
command: '/bin/sh',
|
||||
});
|
||||
return params;
|
||||
}, [containerName]);
|
||||
|
||||
const socketUrl = useMemo(() => {
|
||||
if (!hasSocketProtocol(baseUrl)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return new URL(
|
||||
`${baseUrl}/proxy/api/v1/namespaces/${namespace}/pods/${podName}/exec?${urlParams}`,
|
||||
);
|
||||
}, [baseUrl, namespace, podName, urlParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasSocketProtocol(socketUrl)) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const terminal = new Terminal();
|
||||
const fitAddon = new FitAddon();
|
||||
terminal.loadAddon(fitAddon);
|
||||
|
||||
if (terminalRef.current) {
|
||||
terminal.open(terminalRef.current);
|
||||
fitAddon.fit();
|
||||
}
|
||||
|
||||
terminal.writeln('Starting terminal, please wait...');
|
||||
|
||||
const socket = new WebSocket(socketUrl, ['channel.k8s.io']);
|
||||
|
||||
socket.onopen = () => {
|
||||
terminal.clear();
|
||||
const attachAddon = new PodExecTerminalAttachAddon(socket, {
|
||||
bidirectional: true,
|
||||
});
|
||||
|
||||
terminal.loadAddon(attachAddon);
|
||||
};
|
||||
|
||||
socket.onclose = () => {
|
||||
terminal.writeln('Socket connection closed');
|
||||
};
|
||||
|
||||
return () => {
|
||||
terminal?.clear();
|
||||
socket?.close();
|
||||
};
|
||||
}, [baseUrl, socketUrl]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="terminal"
|
||||
ref={terminalRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { AttachAddon, IAttachOptions } from 'xterm-addon-attach';
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
export class PodExecTerminalAttachAddon extends AttachAddon {
|
||||
constructor(socket: WebSocket, options?: IAttachOptions) {
|
||||
super(socket, options);
|
||||
|
||||
const thisAddon = this as any;
|
||||
|
||||
// These methods are private in the original AttachAddon,
|
||||
// thus have to override at runtime like this
|
||||
thisAddon._sendBinary = (data: string) => {
|
||||
if (!thisAddon._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = Uint8Array.from([0, ...textEncoder.encode(data)]);
|
||||
|
||||
thisAddon._socket.send(buffer);
|
||||
};
|
||||
|
||||
thisAddon._sendData = (data: string) => {
|
||||
if (!thisAddon._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
|
||||
thisAddon._sendBinary(data);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
Button,
|
||||
createStyles,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { useIsPodExecTerminalSupported } from '../../hooks';
|
||||
import { PodExecTerminal, K8sTerminalProps } from './PodExecTerminal';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
dialogPaper: { minHeight: 'calc(100% - 64px)' },
|
||||
dialogContent: { flexBasis: 0 },
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens a Terminal shell to the given pod's container in a Dialog
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const PodExecTerminalDialog = (props: K8sTerminalProps) => {
|
||||
const classes = useStyles();
|
||||
const { clusterName, containerName, podName } = props;
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const isPodExecTerminalSupported = useIsPodExecTerminalSupported();
|
||||
|
||||
const openDialog = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isPodExecTerminalSupported.loading &&
|
||||
isPodExecTerminalSupported.value && (
|
||||
<Dialog
|
||||
maxWidth={false}
|
||||
fullWidth
|
||||
open={open}
|
||||
onClose={closeDialog}
|
||||
PaperProps={{ className: classes.dialogPaper }}
|
||||
>
|
||||
<DialogTitle id="dialog-title">
|
||||
{podName} - {containerName} terminal shell on cluster{' '}
|
||||
{clusterName}
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={closeDialog}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent className={classes.dialogContent}>
|
||||
<PodExecTerminal {...props} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-label="open terminal"
|
||||
component="label"
|
||||
disabled={
|
||||
isPodExecTerminalSupported.loading ||
|
||||
!isPodExecTerminalSupported.value
|
||||
}
|
||||
onClick={openDialog}
|
||||
startIcon={<OpenInBrowserIcon />}
|
||||
>
|
||||
Terminal
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './PodExecTerminal';
|
||||
export * from './PodExecTerminalDialog';
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -22,6 +22,8 @@ import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { ContainerCard } from './ContainerCard';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
jest.mock('../../../hooks/useIsPodExecTerminalSupported');
|
||||
|
||||
const now = DateTime.now();
|
||||
const oneHourAgo = now.minus({ hours: 1 }).toISO();
|
||||
const twoHoursAgo = now.minus({ hours: 2 }).toISO();
|
||||
|
||||
@@ -13,9 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import { IContainer, IContainerStatus } from 'kubernetes-models/v1';
|
||||
import { StructuredMetadataTable } from '@backstage/core-components';
|
||||
import { ClientContainerStatus } from '@backstage/plugin-kubernetes-common';
|
||||
import {
|
||||
Card,
|
||||
CardActions,
|
||||
@@ -24,14 +23,14 @@ import {
|
||||
Grid,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import { IContainer, IContainerStatus } from 'kubernetes-models/v1';
|
||||
import { DateTime } from 'luxon';
|
||||
import React from 'react';
|
||||
|
||||
import { PodScope, PodLogsDialog } from '../PodLogs';
|
||||
import { StructuredMetadataTable } from '@backstage/core-components';
|
||||
import { ClientContainerStatus } from '@backstage/plugin-kubernetes-common';
|
||||
import { ResourceUtilization } from '../../ResourceUtilization';
|
||||
import { bytesToMiB, formatMillicores } from '../../../utils/resources';
|
||||
import { PodExecTerminalDialog } from '../../PodExecTerminal/PodExecTerminalDialog';
|
||||
import { ResourceUtilization } from '../../ResourceUtilization';
|
||||
import { PodLogsDialog, PodScope } from '../PodLogs';
|
||||
|
||||
const getContainerHealthChecks = (
|
||||
containerSpec: IContainer,
|
||||
@@ -222,13 +221,19 @@ export const ContainerCard: React.FC<ContainerCardProps> = ({
|
||||
)}
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions disableSpacing>
|
||||
<CardActions>
|
||||
<PodLogsDialog
|
||||
containerScope={{
|
||||
containerName: containerStatus.name,
|
||||
...podScope,
|
||||
}}
|
||||
/>
|
||||
<PodExecTerminalDialog
|
||||
clusterName={podScope.clusterName}
|
||||
containerName={containerStatus.name}
|
||||
podName={podScope.podName}
|
||||
podNamespace={podScope.podNamespace}
|
||||
/>
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
|
||||
+58
-49
@@ -17,66 +17,75 @@
|
||||
import React from 'react';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { TestApiProvider, wrapInTestApp } from '@backstage/test-utils';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { PodDrawer } from '.';
|
||||
import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
jest.mock('../../../hooks/useIsPodExecTerminalSupported');
|
||||
|
||||
describe('PodDrawer', () => {
|
||||
it('Should show title and container names', async () => {
|
||||
const mockDiscoveryApi: Partial<DiscoveryApi> = {
|
||||
getBaseUrl: () => Promise.resolve('http://localhost'),
|
||||
};
|
||||
|
||||
const { getAllByText, getByText } = render(
|
||||
wrapInTestApp(
|
||||
<PodDrawer
|
||||
{...({
|
||||
open: true,
|
||||
podAndErrors: {
|
||||
clusterName: 'some-cluster-1',
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
},
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: {
|
||||
podIP: '127.0.0.1',
|
||||
containerStatuses: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
errors: [
|
||||
{
|
||||
type: 'some-error',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occurrenceCount: 1,
|
||||
sourceRef: {
|
||||
<TestApiProvider apis={[[discoveryApiRef, mockDiscoveryApi]]}>
|
||||
<PodDrawer
|
||||
{...({
|
||||
open: true,
|
||||
podAndErrors: {
|
||||
clusterName: 'some-cluster-1',
|
||||
pod: {
|
||||
metadata: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: [
|
||||
{
|
||||
type: 'logs',
|
||||
container: 'some-container',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
actions: ['fix1', 'fix2'],
|
||||
},
|
||||
],
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
status: {
|
||||
podIP: '127.0.0.1',
|
||||
containerStatuses: [
|
||||
{
|
||||
name: 'some-container',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any)}
|
||||
/>,
|
||||
errors: [
|
||||
{
|
||||
type: 'some-error',
|
||||
severity: 10,
|
||||
message: 'some error message',
|
||||
occurrenceCount: 1,
|
||||
sourceRef: {
|
||||
name: 'some-pod',
|
||||
namespace: 'some-namespace',
|
||||
kind: 'Pod',
|
||||
apiGroup: 'v1',
|
||||
},
|
||||
proposedFix: [
|
||||
{
|
||||
type: 'logs',
|
||||
container: 'some-container',
|
||||
errorType: 'some error type',
|
||||
rootCauseExplanation: 'some root cause',
|
||||
actions: ['fix1', 'fix2'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any)}
|
||||
/>
|
||||
</TestApiProvider>,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -27,3 +27,4 @@ export * from './Pods';
|
||||
export * from './ServicesAccordions';
|
||||
export * from './KubernetesContent';
|
||||
export * from './ResourceUtilization';
|
||||
export * from './PodExecTerminal';
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { AsyncState } from 'react-use/lib/useAsync';
|
||||
|
||||
export const useIsPodExecTerminalSupported: () => AsyncState<boolean> = jest.fn(
|
||||
() => ({ loading: false, value: false } as AsyncState<boolean>),
|
||||
);
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './useIsPodExecTerminalSupported';
|
||||
export * from './useKubernetesObjects';
|
||||
export * from './useCustomResources';
|
||||
export * from './PodNamesWithErrors';
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
|
||||
import { useIsPodExecTerminalSupported } from './useIsPodExecTerminalSupported';
|
||||
|
||||
jest.mock('@backstage/core-plugin-api');
|
||||
|
||||
describe('useIsClusterShellEnabled', () => {
|
||||
let clusters: { authProvider: string }[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
(useApi as any).mockReturnValue({
|
||||
getClusters: () => Promise.resolve(clusters),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
condition: 'valid Shell setup',
|
||||
returnValue: true,
|
||||
testClusters: [{ authProvider: 'localKubectlProxy' }],
|
||||
},
|
||||
{ condition: 'clusters empty', returnValue: false, testClusters: [] },
|
||||
{
|
||||
condition: 'multi cluster setup',
|
||||
returnValue: false,
|
||||
testClusters: [
|
||||
{ authProvider: 'localKubectlProxy' },
|
||||
{ authProvider: 'serviceAccount' },
|
||||
],
|
||||
},
|
||||
{
|
||||
condition: 'AuthProvider is aks',
|
||||
returnValue: false,
|
||||
testClusters: [{ authProvider: 'aks' }],
|
||||
},
|
||||
{
|
||||
condition: 'AuthProvider is google',
|
||||
returnValue: false,
|
||||
testClusters: [{ authProvider: 'google' }],
|
||||
},
|
||||
{
|
||||
condition: 'AuthProvider start with oidc',
|
||||
returnValue: false,
|
||||
testClusters: [{ authProvider: 'oidc.okta' }],
|
||||
},
|
||||
])(
|
||||
'Should return $returnValue if $condition',
|
||||
async ({ testClusters, returnValue }) => {
|
||||
clusters = testClusters;
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useIsPodExecTerminalSupported(),
|
||||
);
|
||||
|
||||
expect(result.current.loading).toEqual(true);
|
||||
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current.loading).toEqual(false);
|
||||
expect(result.current.value).toBe(returnValue);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
|
||||
|
||||
import { kubernetesApiRef } from '../api';
|
||||
|
||||
/**
|
||||
* Check if conditions for K8sTerminal through K8sProxy are met
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export const useIsPodExecTerminalSupported = (): AsyncState<boolean> => {
|
||||
const kubernetesApi = useApi(kubernetesApiRef);
|
||||
|
||||
return useAsync(async () => {
|
||||
const clusters = await kubernetesApi.getClusters();
|
||||
|
||||
if (clusters.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { authProvider } = clusters[0];
|
||||
const isClientAuthProvider = ['aks', 'google', 'oidc'].some(
|
||||
authProviderName => authProvider.includes(authProviderName),
|
||||
);
|
||||
|
||||
return !isClientAuthProvider;
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user