Skip to content

Commit

Permalink
support command line terminal for self-provisioner
Browse files Browse the repository at this point in the history
  • Loading branch information
christianvogt committed May 26, 2020
1 parent c43c0e9 commit c1efd5e
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 62 deletions.
@@ -1,26 +1,19 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { RootState } from '@console/internal/redux';
import { referenceForModel } from '@console/internal/module/k8s/k8s';
import {
useK8sWatchResource,
WatchK8sResource,
} from '@console/internal/components/utils/k8s-watch-hook';
import { StatusBox, LoadError } from '@console/internal/components/utils/status-box';
import { UserKind } from '@console/internal/module/k8s';
import { WorkspaceModel } from '../../models';
import CloudshellExec from './CloudShellExec';
import TerminalLoadingBox from './TerminalLoadingBox';
import {
CLOUD_SHELL_LABEL,
CLOUD_SHELL_IMMUTABLE_ANNOTATION,
CLOUD_SHELL_CREATOR_LABEL,
CloudShellResource,
TerminalInitData,
initTerminal,
getCloudShellNamespace,
setCloudShellNamespace,
} from './cloud-shell-utils';
import CloudShellSetup from './setup/CloudShellSetup';
import './CloudShellTerminal.scss';
import useCloudShellWorkspace from './useCloudShellWorkspace';

type StateProps = {
user: UserKind;
Expand All @@ -33,42 +26,36 @@ type Props = {
type CloudShellTerminalProps = StateProps & Props;

const CloudShellTerminal: React.FC<CloudShellTerminalProps> = ({ user, onCancel }) => {
const uid = user?.metadata?.uid;
const username = user?.metadata?.name;
const isKubeAdmin = !uid && username === 'kube:admin';
const resource: WatchK8sResource = React.useMemo(
() => ({
kind: referenceForModel(WorkspaceModel),
isList: true,
selector: {
matchLabels: {
[CLOUD_SHELL_LABEL]: 'true',
[CLOUD_SHELL_CREATOR_LABEL]: isKubeAdmin ? '' : uid,
},
},
}),
[isKubeAdmin, uid],
);
const [data, loaded, loadError] = useK8sWatchResource<CloudShellResource[]>(resource);
const [namespace, setNamespace] = React.useState(getCloudShellNamespace());

const [initData, setInitData] = React.useState<TerminalInitData>();
const [initError, setInitError] = React.useState<string>();
let workspace: CloudShellResource;
let workspaceName: string;
let workspaceNamespace: string;
let workspacePhase: string;

if (Array.isArray(data)) {
workspace = data.find(
(d) => d?.metadata?.annotations?.[CLOUD_SHELL_IMMUTABLE_ANNOTATION] === 'true',
);
workspacePhase = workspace?.status?.phase;
workspaceName = workspace?.metadata?.name;
workspaceNamespace = workspace?.metadata?.namespace;
}

const [workspace, loaded, loadError] = useCloudShellWorkspace(user, namespace);

const workspacePhase = workspace?.status?.phase;
const workspaceName = workspace?.metadata?.name;
const workspaceNamespace = workspace?.metadata?.namespace;

const username = user?.metadata?.name;

// save the namespace once the workspace has loaded
React.useEffect(() => {
let unmounted = false;
if (loaded && !loadError) {
// workspace may be undefined which is ok
setCloudShellNamespace(workspaceNamespace);
setNamespace(workspaceNamespace);
}
}, [loaded, loadError, workspaceNamespace]);

// clear the init data if the workspace changes
React.useEffect(() => {
setInitData(undefined);
}, [username, workspaceName, workspaceNamespace]);

// initialize the terminal once it is Running
React.useEffect(() => {
let unmounted = false;
if (workspacePhase === 'Running') {
initTerminal(username, workspaceName, workspaceNamespace)
.then((res: TerminalInitData) => {
Expand All @@ -84,17 +71,25 @@ const CloudShellTerminal: React.FC<CloudShellTerminalProps> = ({ user, onCancel
};
}, [username, workspaceName, workspaceNamespace, workspacePhase]);

// failed to load the workspace
if (loadError) {
return (
<StatusBox loaded={loaded} loadError={loadError} label="OpenShift command line terminal" />
);
}

// failed to init the terminal
if (initError) {
return <LoadError message={initError} label="OpenShift command line terminal" />;
}

if (!loaded || (workspaceName && !initData)) {
// loading the workspace resource
if (!loaded) {
return <TerminalLoadingBox message="" />;
}

// waiting for the workspace to start and initialize the terminal
if (workspaceName && !initData) {
return (
<div className="co-cloudshell-terminal__container">
<TerminalLoadingBox />
Expand All @@ -115,7 +110,16 @@ const CloudShellTerminal: React.FC<CloudShellTerminalProps> = ({ user, onCancel
);
}

return <CloudShellSetup onCancel={onCancel} />;
// show the form to let the user create a new workspace
return (
<CloudShellSetup
onCancel={onCancel}
onSubmit={(ns: string) => {
setCloudShellNamespace(ns);
setNamespace(ns);
}}
/>
);
};

// For testing
Expand Down
Expand Up @@ -6,7 +6,7 @@ type TerminalLoadingBoxProps = {
};

const TerminalLoadingBox: React.FC<TerminalLoadingBoxProps> = ({ message }) => (
<LoadingBox message={message || 'Connecting to your OpenShift command line terminal ...'} />
<LoadingBox message={message ?? 'Connecting to your OpenShift command line terminal ...'} />
);

export default TerminalLoadingBox;
@@ -1,15 +1,11 @@
import * as React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import store from '@console/internal/redux';
import { shallow } from 'enzyme';
import CloudShellTab from '../CloudShellTab';
import CloudShellTerminal from '../CloudShellTerminal';

describe('CloudShell Tab Test', () => {
it('should render cloudshellterminal', () => {
const cloudShellTabWrapper = mount(<CloudShellTab />, {
wrappingComponent: ({ children }) => <Provider store={store}>{children}</Provider>,
});
describe('CloudShellTab', () => {
it('should render CloudShellTerminal', () => {
const cloudShellTabWrapper = shallow(<CloudShellTab />);
expect(cloudShellTabWrapper.find(CloudShellTerminal).exists()).toBe(true);
});
});
@@ -1,31 +1,31 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import { useK8sWatchResource } from '@console/internal/components/utils/k8s-watch-hook';
import { StatusBox } from '@console/internal/components/utils/status-box';
import { InternalCloudShellTerminal } from '../CloudShellTerminal';
import TerminalLoadingBox from '../TerminalLoadingBox';
import CloudShellSetup from '../setup/CloudShellSetup';
import { user } from './cloud-shell-test-data';
import useCloudShellWorkspace from '../useCloudShellWorkspace';

jest.mock('@console/internal/components/utils/k8s-watch-hook', () => ({
useK8sWatchResource: jest.fn(),
jest.mock('../useCloudShellWorkspace', () => ({
default: jest.fn(),
}));

describe('CloudShellTerminal', () => {
it('should display loading box', () => {
(useK8sWatchResource as jest.Mock).mockReturnValueOnce([null, false]);
(useCloudShellWorkspace as jest.Mock).mockReturnValueOnce([null, false]);
const wrapper = shallow(<InternalCloudShellTerminal user={user} />);
expect(wrapper.find(TerminalLoadingBox)).toHaveLength(1);
});

it('should display error statusBox', () => {
(useK8sWatchResource as jest.Mock).mockReturnValueOnce([null, false, true]);
(useCloudShellWorkspace as jest.Mock).mockReturnValueOnce([null, false, true]);
const wrapper = shallow(<InternalCloudShellTerminal user={user} />);
expect(wrapper.find(StatusBox)).toHaveLength(1);
});

it('should display form if loaded and no workspace', () => {
(useK8sWatchResource as jest.Mock).mockReturnValueOnce([[], true]);
(useCloudShellWorkspace as jest.Mock).mockReturnValueOnce([[], true]);
const wrapper = shallow(<InternalCloudShellTerminal user={user} />);
expect(wrapper.find(CloudShellSetup)).toHaveLength(1);
});
Expand Down
@@ -1,6 +1,9 @@
import { K8sResourceKind } from '@console/internal/module/k8s';
import { getRandomChars } from '@console/shared';
import { K8sResourceKind, k8sPatch } from '@console/internal/module/k8s';
import { STORAGE_PREFIX, getRandomChars } from '@console/shared';
import { coFetchJSON } from '@console/internal/co-fetch';
import { WorkspaceModel } from '../../models';

const CLOUD_SHELL_NAMESPACE = `${STORAGE_PREFIX}/command-line-terminal-namespace`;

type environment = {
value: string;
Expand Down Expand Up @@ -88,6 +91,14 @@ export const newCloudShellWorkSpace = (name: string, namespace: string): CloudSh
},
});

export const startWorkspace = (workspace: CloudShellResource) => {
return k8sPatch(WorkspaceModel, workspace, {
path: '/spec/started',
op: 'replace',
value: true,
});
};

export const initTerminal = (
username: string,
workspaceName: string,
Expand All @@ -102,3 +113,12 @@ export const initTerminal = (
};
return coFetchJSON.post(url, payload);
};

export const getCloudShellNamespace = () => localStorage.getItem(CLOUD_SHELL_NAMESPACE);
export const setCloudShellNamespace = (namespace: string) => {
if (!namespace) {
localStorage.removeItem(CLOUD_SHELL_NAMESPACE);
} else {
localStorage.setItem(CLOUD_SHELL_NAMESPACE, namespace);
}
};
Expand Up @@ -21,7 +21,7 @@ interface StateProps {
}

type Props = StateProps & {
onSubmit?: () => void;
onSubmit?: (namespace: string) => void;
onCancel?: () => void;
};

Expand Down Expand Up @@ -50,7 +50,7 @@ const CloudShellSetup: React.FunctionComponent<Props> = ({
WorkspaceModel,
newCloudShellWorkSpace(createCloudShellResourceName(), namespace),
);
onSubmit && onSubmit();
onSubmit && onSubmit(namespace);
} catch (err) {
actions.setStatus({ submitError: err.message });
}
Expand Down

0 comments on commit c1efd5e

Please sign in to comment.