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:
Carlos Esteban Lopez
2023-09-01 15:25:32 -05:00
parent 1963d3dc24
commit 7032c214f3
20 changed files with 769 additions and 89 deletions
+20
View File
@@ -347,6 +347,18 @@ export const JobsAccordions: ({
jobs,
}: JobsAccordionsProps) => React_2.JSX.Element;
// @public
export interface K8sTerminalProps {
// (undocumented)
clusterName: string;
// (undocumented)
containerName: string;
// (undocumented)
podName: string;
// (undocumented)
podNamespace: string;
}
// Warning: (ae-missing-release-tag) "KubernetesApi" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -631,6 +643,14 @@ export const PodDrawer: ({
open,
}: PodDrawerProps) => React_2.JSX.Element;
// @public
export const PodExecTerminal: (props: K8sTerminalProps) => React_2.JSX.Element;
// @public
export const PodExecTerminalDialog: (
props: K8sTerminalProps,
) => React_2.JSX.Element;
// @public
export const PodLogs: React_2.FC<PodLogsProps>;
+6 -1
View File
@@ -53,7 +53,10 @@
"kubernetes-models": "^4.1.0",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"react-use": "^17.2.4"
"react-use": "^17.2.4",
"xterm": "^5.2.1",
"xterm-addon-attach": "^0.8.0",
"xterm-addon-fit": "^0.7.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0",
@@ -70,6 +73,8 @@
"@testing-library/react": "^12.1.3",
"@testing-library/react-hooks": "^8.0.0",
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"jest-websocket-mock": "^2.4.1",
"msw": "^1.0.0"
},
"files": [
@@ -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>
);
@@ -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>),
);
+1
View File
@@ -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;
});
};