Skip to content

Commit

Permalink
Create devworkspaces using generated DevWorkspace and DevWorkspaceTem…
Browse files Browse the repository at this point in the history
…plate (#429)
  • Loading branch information
akurinnoy authored Jan 4, 2022
1 parent 954e3a8 commit 1d2a9ad
Show file tree
Hide file tree
Showing 23 changed files with 1,043 additions and 163 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"start": "${PWD}/local-start.sh $@",
"license:check": "docker run --rm -t -v ${PWD}/:/workspace/project quay.io/che-incubator/dash-licenses:next --check",
"license:generate": "docker run --rm -t -v ${PWD}/:/workspace/project quay.io/che-incubator/dash-licenses:next",
"test": "lerna run test --stream -- $@",
"test": "lerna run test --stream -- --no-cache $@",
"pretest": "yarn run prebuild",
"test:coverage": "yarn run test -- --runInBand --coverage",
"format:check": "yarn workspaces run format:check",
Expand Down
9 changes: 5 additions & 4 deletions packages/dashboard-frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ module.exports = {
'\\.(css|less|sass|scss|styl)$': '<rootDir>/__mocks__/styleMock.js',
'\\.(gif|ttf|eot|svg)$': '<rootDir>/__mocks__/fileMock.js',
'monaco-editor-core': 'monaco-editor-core/esm/vs/editor/editor.main',
'vscode-languageserver-protocol/lib/utils/is': 'vscode-languageserver-protocol/lib/common/utils/is',
'vscode-languageserver-protocol/lib/utils/is':
'vscode-languageserver-protocol/lib/common/utils/is',
'vscode-languageserver-protocol/lib/main': 'vscode-languageserver-protocol/lib/node/main',
},
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json',
}
},
},
setupFilesAfterEnv: ['./jest.setup.ts'],
setupFiles: ['./src/inversify.config.ts'],
Expand All @@ -47,6 +48,6 @@ module.exports = {
branches: 36,
functions: 50,
lines: 50,
}
},
},
}
};
116 changes: 102 additions & 14 deletions packages/dashboard-frontend/src/containers/FactoryLoader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
*/

import { AlertActionLink, AlertVariant } from '@patternfly/react-core';
import axios from 'axios';
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { History } from 'history';
import common from '@eclipse-che/common';
import { delay } from '../../services/helpers/delay';
import { AppState } from '../../store';
import * as FactoryResolverStore from '../../store/FactoryResolver';
import * as WorkspaceStore from '../../store/Workspaces';
import * as WorkspacesStore from '../../store/Workspaces';
import * as DevWorkspacesStore from '../../store/Workspaces/devWorkspaces';
import FactoryLoader from '../../pages/FactoryLoader';
import {
selectAllWorkspaces,
Expand All @@ -41,7 +43,7 @@ import {
selectDefaultNamespace,
selectInfrastructureNamespaces,
} from '../../store/InfrastructureNamespaces/selectors';
import { safeLoad } from 'js-yaml';
import { safeLoad, safeLoadAll } from 'js-yaml';
import updateDevfileMetadata, { FactorySource } from './updateDevfileMetadata';
import { DEVWORKSPACE_DEVFILE_SOURCE } from '../../services/workspace-client/devworkspace/devWorkspaceClient';
import devfileApi from '../../services/devfileApi';
Expand All @@ -53,6 +55,7 @@ const WS_ATTRIBUTES_TO_SAVE: string[] = [
'workspaceDeploymentAnnotations',
'policies.create',
'che-editor',
'devWorkspace',
];

export type CreatePolicy = 'perclick' | 'peruser';
Expand Down Expand Up @@ -81,6 +84,7 @@ type State = {
hasError: boolean;
createPolicy: CreatePolicy;
cheDevworkspaceEnabled: boolean;
createFromDevfile: boolean; // indicates that a devfile is used to create a workspace
};

export class FactoryLoaderContainer extends React.PureComponent<Props, State> {
Expand All @@ -105,6 +109,7 @@ export class FactoryLoaderContainer extends React.PureComponent<Props, State> {
createPolicy,
search,
cheDevworkspaceEnabled,
createFromDevfile: false,
};
}

Expand Down Expand Up @@ -405,8 +410,8 @@ export class FactoryLoaderContainer extends React.PureComponent<Props, State> {
const annotations = workspace.ref.metadata.annotations;
const source = annotations ? annotations[DEVWORKSPACE_DEVFILE_SOURCE] : undefined;
if (source) {
const sourseObj = safeLoad(source) as FactorySource;
return sourseObj?.factory?.params === attrs.factoryParams;
const sourceObj = safeLoad(source) as FactorySource;
return sourceObj?.factory?.params === attrs.factoryParams;
}
// ignore createPolicy for dev workspaces
return false;
Expand Down Expand Up @@ -458,6 +463,67 @@ export class FactoryLoaderContainer extends React.PureComponent<Props, State> {
return workspace;
}

private async createDevWorkspaceFromResources(
devWorkspacePrebuiltResources: string,
factoryParams: string,
): Promise<Workspace | undefined> {
let workspace: Workspace | undefined;

// creation policy is `peruser`
workspace = this.props.allWorkspaces.find(workspace => {
if (isCheWorkspace(workspace.ref)) {
return false;
} else {
const annotations = workspace.ref.metadata.annotations;
const source = annotations ? annotations[DEVWORKSPACE_DEVFILE_SOURCE] : undefined;
if (source) {
const sourceObj = safeLoad(source) as FactorySource;
return sourceObj?.factory?.params === factoryParams;
}
return false;
}
});

if (!workspace) {
try {
let yamlContent: string;
try {
const response = await axios.get(devWorkspacePrebuiltResources);
yamlContent = response.data;
} catch (e) {
const errorMessage = common.helpers.errors.getMessage(e);
console.error(
`Failed to fetch prebuilt resources from ${devWorkspacePrebuiltResources}. ${errorMessage}`,
);
this.showAlert(`Failed to fetch prebuilt resources. ${errorMessage}`);
return;
}

const resources = safeLoadAll(yamlContent);
const devworkspace = resources.find(
resource => resource.kind === 'DevWorkspace',
) as devfileApi.DevWorkspace;
const devworkspaceTemplate = resources.find(
resource => resource.kind === 'DevWorkspaceTemplate',
) as devfileApi.DevWorkspaceTemplate;

await this.props.createWorkspaceFromResources(devworkspace, devworkspaceTemplate);

const namespace = this.props.defaultNamespace?.name;
this.props.setWorkspaceQualifiedName(namespace, devworkspace.metadata.name as string);
workspace = this.props.activeWorkspace;
} catch (e) {
this.showAlert(`Failed to create a workspace. ${e}`);
return;
}
}
if (!workspace) {
this.showAlert('Failed to create a workspace.');
return;
}
return workspace;
}

private tryAgainHandler(): void {
const searchParams = new window.URLSearchParams(this.props.history.location.search);
searchParams.delete('error_code');
Expand Down Expand Up @@ -552,18 +618,32 @@ export class FactoryLoaderContainer extends React.PureComponent<Props, State> {

await delay();

let devfile = await this.resolveDevfile(location);
let workspace: Workspace | undefined;
if (this.props.cheDevworkspaceEnabled && attrs.devWorkspace) {
// create workspace using prebuilt resources
workspace = await this.createDevWorkspaceFromResources(
attrs.devWorkspace,
attrs.factoryParams,
);
} else {
// create workspace using a devfile
this.setState({
createFromDevfile: true,
});

let devfile = await this.resolveDevfile(location);

if (!devfile) {
return;
}
if (!devfile) {
return;
}

devfile = updateDevfileMetadata(devfile, attrs.factoryParams, createPolicy);
this.setState({ currentStep: LoadFactorySteps.APPLYING_DEVFILE });
devfile = updateDevfileMetadata(devfile, attrs.factoryParams, createPolicy);
this.setState({ currentStep: LoadFactorySteps.APPLYING_DEVFILE });

await delay();
await delay();

const workspace = await this.resolveWorkspace(devfile, attrs);
workspace = await this.resolveWorkspace(devfile, attrs);
}

if (!workspace) {
return;
Expand All @@ -578,7 +658,13 @@ export class FactoryLoaderContainer extends React.PureComponent<Props, State> {

render() {
const { workspace } = this.props;
const { currentStep, resolvedDevfileMessage, hasError, cheDevworkspaceEnabled } = this.state;
const {
currentStep,
resolvedDevfileMessage,
hasError,
cheDevworkspaceEnabled,
createFromDevfile,
} = this.state;
const workspaceName = workspace ? workspace.name : '';
const workspaceId = workspace ? workspace.id : '';

Expand All @@ -589,6 +675,7 @@ export class FactoryLoaderContainer extends React.PureComponent<Props, State> {
resolvedDevfileMessage={resolvedDevfileMessage}
workspaceId={workspaceId}
workspaceName={workspaceName}
createFromDevfile={createFromDevfile}
isDevWorkspace={cheDevworkspaceEnabled}
callbacks={this.factoryLoaderCallbacks}
/>
Expand All @@ -610,7 +697,8 @@ const mapStateToProps = (state: AppState) => ({

const connector = connect(mapStateToProps, {
...FactoryResolverStore.actionCreators,
...WorkspaceStore.actionCreators,
...WorkspacesStore.actionCreators,
createWorkspaceFromResources: DevWorkspacesStore.actionCreators.createWorkspaceFromResources,
});

type MappedProps = ConnectedProps<typeof connector>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React from 'react';
import { Provider } from 'react-redux';
import { AlertVariant } from '@patternfly/react-core';
import { RenderResult, render, screen, waitFor } from '@testing-library/react';
import mockAxios from 'axios';
import { ROUTE } from '../../route.enum';
import { getMockRouterProps } from '../../services/__mocks__/router';
import { FakeStoreBuilder } from '../../store/__mocks__/storeBuilder';
Expand Down Expand Up @@ -42,6 +43,18 @@ const requestFactoryResolverMock = jest.fn().mockResolvedValue(undefined);
const setWorkspaceIdMock = jest.fn().mockResolvedValue(undefined);
const clearWorkspaceIdMock = jest.fn().mockResolvedValue(undefined);

const createWorkspaceFromResourcesMock = jest.fn().mockReturnValue(undefined);
jest.mock('../../store/Workspaces/devWorkspaces/index', () => {
return {
actionCreators: {
createWorkspaceFromResources:
(devworkspace: string, devworkspaceTemplate: string) => async (): Promise<void> => {
createWorkspaceFromResourcesMock(devworkspace, devworkspaceTemplate);
},
},
};
});

jest.mock('../../store/Workspaces');
(workspacesActionCreators.requestWorkspace as jest.Mock).mockImplementation(
(id: string) => async () => requestWorkspaceMock(id),
Expand Down Expand Up @@ -167,6 +180,42 @@ describe('Factory Loader container', () => {
});
});

describe('with prebuilt resources', () => {
it('should start creating a devworkspace using pre-generated resources', async () => {
const store = new FakeStoreBuilder()
.withWorkspacesSettings({
'che.devworkspaces.enabled': 'true',
} as che.WorkspaceSettings)
.withInfrastructureNamespace([{ name: namespace, attributes: { phase: 'Active' } }], false)
.build();

const location = 'http://test-location';
const props = getMockRouterProps(ROUTE.LOAD_FACTORY_URL, {
url: location,
});
props.location.search += '&devWorkspace=devWorkspace.yaml';

const yamlContent = `apiVersion: workspace.devfile.io/v1alpha2
kind: DevWorkspaceTemplate
metadata:
name: theia-ide-project
---
apiVersion: workspace.devfile.io/v1alpha2
kind: DevWorkspace
metadata:
name: project`;
(mockAxios.get as jest.Mock).mockResolvedValueOnce(yamlContent);

render(
<Provider store={store}>
<FactoryLoaderContainer {...props} />
</Provider>,
);

await waitFor(() => expect(createWorkspaceFromResourcesMock).toHaveBeenCalled());
});
});

describe('Use a devfile V1', () => {
it('should resolve the factory, create and start a new workspace', async () => {
const location = 'http://test-location';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ function renderComponent(
hasError={hasError}
resolvedDevfileMessage={resolvedDevfileMessage}
isDevWorkspace
createFromDevfile={true}
/>
</Provider>,
);
Expand Down
Loading

0 comments on commit 1d2a9ad

Please sign in to comment.