Skip to content

Commit

Permalink
feat(web-terminal): impr README, add isWebTerminalAvailable util and …
Browse files Browse the repository at this point in the history
…fix some other small bugs (janus-idp#1036)
  • Loading branch information
christoph-jerolimov authored Jan 8, 2024
1 parent d148b72 commit a3d6d86
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 42 deletions.
96 changes: 79 additions & 17 deletions plugins/web-terminal/README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,100 @@
# Web terminal plugin for Backstage
# Web Terminal plugin for Backstage

This plugin provides a frontend for [`webterminal proxy`](https://github.com/janus-idp/webterminal-proxy) and shows a terminal for catalog entities with an Kubernetes API-Server annotation (`kubernetes.io/api-server`).

Users first enter their user token from the cluster, and then the plugin setups environment. Once it is set up, it connects to `webterminal-proxy`, which finishes setups and passes data between the frontend plugin and pod.

This plugin provides a frontend for [`webterminal proxy`](https://github.com/janus-idp/webterminal-proxy). Users first enter their user token from the cluster, and then the plugin setups environment. Once it is set up, it connects to `webterminal-proxy`, which finishes setups and passes data between the frontend plugin and pod.
This plugin uses [`xterm.js`](http://xtermjs.org/) to simulate a regular terminal.

## Prerequisites

Before we can install this plugin, we need to fulfill the following requirements:

1. Deployed [`webterminal-proxy`](https://github.com/janus-idp/webterminal-proxy)
2. Installed [Web terminal operator](https://docs.openshift.com/container-platform/4.8/web_console/odc-about-web-terminal.html#odc-installing-web-terminal_odc-about-web-terminal)
1. Installed [Web Terminal operator](https://docs.openshift.com/container-platform/latest/web_console/web_terminal/installing-web-terminal.html)
2. Deployed [`webterminal-proxy`](https://github.com/janus-idp/webterminal-proxy)

## Installation

1. Install the Web Terminal plugin using the following command:

```console
yarn workspace app add @janus-idp/backstage-plugin-web-terminal
```

2. Enable an additional tab on the entity view page using the `packages/app/src/components/catalog/EntityPage.tsx` file as follows:

```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
/* highlight-add-start */
import {
isWebTerminalAvailable,
WebTerminal,
} from '@janus-idp/backstage-plugin-web-terminal';

/* highlight-add-end */

const serviceEntityPage = (
<EntityLayout>
// ...
{/* highlight-add-start */}
<EntityLayout.Route
if={isWebTerminalAvailable}
path="/webterminal"
title="Web Terminal"
>
<WebTerminal />
</EntityLayout.Route>
{/* highlight-add-end */}
</EntityLayout>
);
```

## Usage
3. Alternative you can add the WebTerminal to an existing page:

```tsx title="packages/app/src/components/catalog/EntityPage.tsx"
/* highlight-add-start */
import {
isWebTerminalAvailable,
WebTerminal,
} from '@janus-idp/backstage-plugin-web-terminal';

/* highlight-add-end */

<Grid container spacing={3}>
{/* highlight-add-start */}
<EntitySwitch>
<EntitySwitch.Case if={isWebTerminalAvailable}>
<Grid item md={6}>
<WebTerminal />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
{/* highlight-add-end */}
</Grid>;
```

4. Annotate your entity using the following annotations:

```yaml
metadata:
annotations:
'kubernetes.io/api-server': `<CLUSTER-URL>',
```

## Configuration

You have to define the location of the `webterminal-proxy` in `app-config.yaml`:

```yaml
webTerminal:
webSocketUrl: 'wss://example.com:3000'
restServerUrl: 'https://example.com:3000/rest'
webSocketUrl: 'wss://example.com:3000/webterminal'
restServerUrl: 'https://example.com:3000/webterminal/rest'
```

Optionally, you can also define the default namespace for the terminal; otherwise, `openshift-terminal` will be used:

```yaml
webTerminal:
webSocketUrl: 'wss://example.com:3000'
restServerUrl: 'https://example.com:3000/rest'
webSocketUrl: 'wss://example.com:3000/webterminal'
restServerUrl: 'https://example.com:3000/webterminal/rest'
defaultNamespace: 'default'
```

Next, you can include the `WebTerminal` component in your catalog resource page within the entity context:

```typescript
<Grid item>
<WebTerminal />
</Grid>
```
1 change: 1 addition & 0 deletions plugins/web-terminal/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
},
"configSchema": "schema.d.ts",
"dependencies": {
"@backstage/catalog-model": "^1.4.3",
"@backstage/config": "^1.1.1",
"@backstage/core-components": "^0.13.6",
"@backstage/core-plugin-api": "^1.7.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';

import { TerminalComponent } from './TerminalComponent';
import { KUBERNETES_API_SERVER } from './utils/annotations';

const DOMAIN_URL = 'mock-domain.com/webterminal';
const API_URL = 'https://api.cluster.com';
const NAMESPACES_URL = `${API_URL}/api/v1/namespaces`;
const NAMESPACE = 'web-terminal-service-catalog';
const WORKSPACES_URL = `${API_URL}/apis/workspace.devfile.io/v1alpha2/namespaces/${NAMESPACE}/devworkspaces`;
const CREATED_WORKSPACE_URL = `${WORKSPACES_URL}/web-terminal-c5e12`;

const entityMock = {
metadata: {
annotations: {
'kubernetes.io/api-server': API_URL,
[KUBERNETES_API_SERVER]: API_URL,
},
name: 'cluster',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useCallback, useRef } from 'react';

import { InfoCard, Progress } from '@backstage/core-components';
import { InfoCard, Progress, WarningPanel } from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { useEntity } from '@backstage/plugin-catalog-react';

Expand All @@ -14,11 +14,13 @@ import {
getDefaultNamespace,
getNamespaces,
getWorkspace,
waitBetweenRetries,
} from './utils';

import './static/xterm.css';

import { NamespacePickerDialog } from '../NamespacePickerDialog';
import { KUBERNETES_API_SERVER } from './utils/annotations';

const useStyles = makeStyles({
term: {
Expand Down Expand Up @@ -53,9 +55,10 @@ export const TerminalComponent = () => {
);

const { entity } = useEntity();
const cluster = entity.metadata.annotations?.[
'kubernetes.io/api-server'
]?.replace(/(https?:\/\/)/, '');
const cluster = entity.metadata.annotations?.[KUBERNETES_API_SERVER]?.replace(
/(https?:\/\/)/,
'',
);
const classes = useStyles();
const tokenRef = React.useRef<HTMLInputElement>(null);
const termRef = React.useRef(null);
Expand Down Expand Up @@ -109,7 +112,13 @@ export const TerminalComponent = () => {
}
let workspaceID;
let phase;
while (phase !== 'Running') {
const waitUntil = Date.now() + 5 * 60 * 1000; // wait max 5 minutes
for (
let retry = 0;
retry < 1000 && Date.now() < waitUntil && phase !== 'Running';
retry++
) {
await waitBetweenRetries(retry);
[workspaceID, phase] = await getWorkspace(
restServerUrl,
link,
Expand Down Expand Up @@ -173,24 +182,32 @@ export const TerminalComponent = () => {
return (
<div>
<InfoCard title="Web Terminal" noPadding>
<form onSubmit={handleSubmit} className={classes.formDisplay}>
<TextField
data-testid="token-input"
label="Token"
type="password"
variant="outlined"
inputRef={tokenRef}
required
{cluster ? (
<form onSubmit={handleSubmit} className={classes.formDisplay}>
<TextField
data-testid="token-input"
label="Token"
type="password"
variant="outlined"
inputRef={tokenRef}
required
/>
<Button
data-testid="submit-token-button"
type="submit"
color="primary"
variant="contained"
>
Submit
</Button>
</form>
) : (
<WarningPanel
title="Entity missing Kubernetes API annotation"
message={`Entity "${entity.metadata.name}" must have the "${KUBERNETES_API_SERVER}" annotation to setup a web terminal.`}
defaultExpanded
/>
<Button
data-testid="submit-token-button"
type="submit"
color="primary"
variant="contained"
>
Submit
</Button>
</form>
)}
{displayModal && cluster && token && (
<NamespacePickerDialog
onInit={() => getNamespaces(restServerUrl, cluster, token)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Entity } from '@backstage/catalog-model';

export const KUBERNETES_API_SERVER = 'kubernetes.io/api-server';

/** @public */
export const isWebTerminalAvailable = (entity: Entity): boolean =>
Boolean(entity.metadata.annotations?.[KUBERNETES_API_SERVER]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getTimeInMsBetweenRetries } from './helpers';

describe('getTimeInMsBetweenRetries', () => {
it('should return 0 for retry 0', () => {
expect(getTimeInMsBetweenRetries(0)).toBe(0);
});

it('should return more then 0 for the next retries', () => {
expect(getTimeInMsBetweenRetries(1)).toBeGreaterThan(0);
expect(getTimeInMsBetweenRetries(2)).toBeGreaterThan(0);
});

it('should return not go above 5 seconds', () => {
expect(getTimeInMsBetweenRetries(9)).toBe(3000);
expect(getTimeInMsBetweenRetries(10)).toBe(5000);
expect(getTimeInMsBetweenRetries(11)).toBe(5000);
expect(getTimeInMsBetweenRetries(100)).toBe(5000);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,19 @@ const OPENSHIFT_TERMINAL_DEFAULT_NAMESPACE = 'openshift-terminal';
export const getDefaultNamespace = (config: Config) =>
config.getOptionalString('webTerminal.defaultNamespace') ??
OPENSHIFT_TERMINAL_DEFAULT_NAMESPACE;

const timeInMsBetweenRetries = [
0, 100, 500, 1000, 1000, 1000, 2000, 2000, 2000, 3000, 5000,
];

export const getTimeInMsBetweenRetries = (retry: number) => {
return timeInMsBetweenRetries[
Math.min(retry, timeInMsBetweenRetries.length - 1)
];
};

export const waitBetweenRetries = (retry: number) => {
return new Promise<void>(resolve => {
setTimeout(() => resolve(), getTimeInMsBetweenRetries(retry));
});
};
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { createWorkspace, getWorkspace, getNamespaces } from './requests';
export { getDefaultNamespace } from './helpers';
export { getDefaultNamespace, waitBetweenRetries } from './helpers';
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const getWorkspace = async (
},
);
const data = await response.json();
return [data.status.devworkspaceId, data.status.phase];
return [data.status?.devworkspaceId, data.status?.phase];
};

export const getNamespaces = async (
Expand Down
1 change: 1 addition & 0 deletions plugins/web-terminal/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { webTerminalPlugin, WebTerminal } from './plugin';
export { isWebTerminalAvailable } from './components/TerminalComponent/utils/annotations';

0 comments on commit a3d6d86

Please sign in to comment.