Fix handling of error status of a workspace (#13804)

* fixed handling of error status of a workspace after it has been started.

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* workaround for devserver proxy to replace header 'origin' with target host

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>

* use string interpolation instead of concatenation

Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com>
7.20.x
Oleksii Kurinnyi 2019-09-05 12:59:49 +03:00 committed by GitHub
parent 9072be96a6
commit 208c4fb918
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 682 additions and 688 deletions

View File

@ -7,7 +7,7 @@
"test": "jest",
"test:watch": "jest --watch",
"compile": "webpack --config webpack.prod.js",
"start": "webpack-dev-server --open --config webpack.dev.js",
"start": "webpack-dev-server --open --disable-host-check --config webpack.dev.js",
"format": "tsfmt -r --useTsfmt tsfmt.json",
"lint:fix": "tslint -c tslint.json --fix --project .",
"build": "yarn run format && yarn run compile"

View File

@ -16,7 +16,7 @@ import { CheJsonRpcApiClient } from './che-json-rpc-api-service';
import { ICommunicationClient, CODE_REQUEST_TIMEOUT, CommunicationClientEvent } from './json-rpc-client';
import { WorkspaceLoader } from '../workspace-loader';
enum MasterChannels {
export enum MasterChannels {
ENVIRONMENT_OUTPUT = 'runtime/log',
ENVIRONMENT_STATUS = 'machine/statusChanged',
INSTALLER_OUTPUT = 'installer/log',
@ -25,13 +25,6 @@ enum MasterChannels {
const SUBSCRIBE: string = 'subscribe';
const UNSUBSCRIBE: string = 'unsubscribe';
export interface WorkspaceStatusChangedEvent {
status: string;
prevStatus: string;
workspaceId: string;
error: string;
}
/**
* Client API for workspace master interactions.
*
@ -105,12 +98,12 @@ export class CheJsonRpcMasterApi {
*
* @returns {Promise<void>}
*/
connect(): Promise<void> {
async connect(): Promise<void> {
const entryPointFunction = () => {
const entryPoint = this.entryPoint + this.loader.getAuthenticationToken();
if (this.clientId) {
let clientId = `clientId=${this.clientId}`;
// in case of reconnection
// in case of re-connection
// we need to test entrypoint on existing query parameters
// to add already gotten clientId
if (/\?/.test(entryPoint) === false) {
@ -123,9 +116,8 @@ export class CheJsonRpcMasterApi {
return entryPoint;
};
return this.cheJsonRpcApi.connect(entryPointFunction).then(() =>
this.fetchClientId()
);
await this.cheJsonRpcApi.connect(entryPointFunction);
return await this.fetchClientId();
}
/**
@ -220,10 +212,9 @@ export class CheJsonRpcMasterApi {
*
* @returns {Promise<void>}
*/
fetchClientId(): Promise<void> {
return this.cheJsonRpcApi.request('websocketIdService/getId').then((data: string[]) => {
this.clientId = data[0];
});
async fetchClientId(): Promise<void> {
const data = await this.cheJsonRpcApi.request('websocketIdService/getId');
this.clientId = data[0];
}
/**

View File

@ -12,9 +12,10 @@
'use strict';
import { WebsocketClient } from './json-rpc/websocket-client';
import { CheJsonRpcMasterApi, WorkspaceStatusChangedEvent } from './json-rpc/che-json-rpc-master-api';
import { CheJsonRpcMasterApi } from './json-rpc/che-json-rpc-master-api';
import { Loader } from './loader/loader';
import { che } from '@eclipse-che/api';
import { Deferred } from './json-rpc/util';
// tslint:disable:no-any
@ -22,13 +23,17 @@ const WEBSOCKET_CONTEXT = '/api/websocket';
export class WorkspaceLoader {
workspace: che.workspace.Workspace;
startAfterStopping = false;
private workspace: che.workspace.Workspace;
private runtimeIsAccessible: Deferred<void>;
constructor(private readonly loader: Loader,
private readonly keycloak?: any) {
constructor(
private readonly loader: Loader,
private readonly keycloak?: any
) {
/** Ask dashboard to show the IDE. */
window.parent.postMessage('show-ide', '*');
this.runtimeIsAccessible = new Deferred<void>();
}
async load(): Promise<void> {
@ -45,7 +50,6 @@ export class WorkspaceLoader {
await this.openIDE();
} catch (err) {
if (err) {
console.error(err);
this.loader.error(err);
} else {
this.loader.error('Unknown error has happened, try to reload page');
@ -56,7 +60,7 @@ export class WorkspaceLoader {
}
/**
* Returns workspace key from current address or empty string when it is undefined.
* Returns workspace key from current location or empty string when it is undefined.
*/
getWorkspaceKey(): string {
const result: string = window.location.pathname.substr(1);
@ -83,47 +87,49 @@ export class WorkspaceLoader {
*
* @param workspaceId workspace id
*/
getWorkspace(workspaceId: string): Promise<che.workspace.Workspace> {
async getWorkspace(workspaceId: string): Promise<che.workspace.Workspace> {
const request = new XMLHttpRequest();
request.open('GET', '/api/workspace/' + workspaceId);
return this.setAuthorizationHeader(request).then((xhr: XMLHttpRequest) =>
new Promise<che.workspace.Workspace>((resolve, reject) => {
xhr.send();
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) { return; }
if (xhr.status !== 200) {
const errorMessage = 'Failed to get the workspace: "' + this.getRequestErrorMessage(xhr) + '"';
reject(new Error(errorMessage));
return;
}
resolve(JSON.parse(xhr.responseText));
};
}));
const requestWithAuth = await this.setAuthorizationHeader(request);
return new Promise<che.workspace.Workspace>((resolve, reject) => {
requestWithAuth.send();
requestWithAuth.onreadystatechange = () => {
if (requestWithAuth.readyState !== 4) {
return;
}
if (requestWithAuth.status !== 200) {
reject(new Error(`Failed to get the workspace: "${this.getRequestErrorMessage(requestWithAuth)}"`));
return;
}
resolve(JSON.parse(requestWithAuth.responseText));
};
});
}
/**
* Start current workspace.
*/
startWorkspace(): Promise<che.workspace.Workspace> {
async startWorkspace(): Promise<che.workspace.Workspace> {
const request = new XMLHttpRequest();
request.open('POST', `/api/workspace/${this.workspace.id}/runtime`);
return this.setAuthorizationHeader(request).then((xhr: XMLHttpRequest) =>
new Promise<che.workspace.Workspace>((resolve, reject) => {
xhr.send();
xhr.onreadystatechange = () => {
if (xhr.readyState !== 4) { return; }
if (xhr.status !== 200) {
const errorMessage = 'Failed to start the workspace: "' + this.getRequestErrorMessage(xhr) + '"';
reject(new Error(errorMessage));
return;
}
resolve(JSON.parse(xhr.responseText));
};
}));
const requestWithAuth = await this.setAuthorizationHeader(request);
return new Promise<che.workspace.Workspace>((resolve, reject) => {
requestWithAuth.send();
requestWithAuth.onreadystatechange = () => {
if (requestWithAuth.readyState !== 4) {
return;
}
if (requestWithAuth.status !== 200) {
reject(new Error(`Failed to start the workspace: "${this.getRequestErrorMessage(requestWithAuth)}"`));
return;
}
resolve(JSON.parse(requestWithAuth.responseText));
};
});
}
getRequestErrorMessage(xhr: XMLHttpRequest): string {
let errorMessage;
private getRequestErrorMessage(xhr: XMLHttpRequest): string {
let errorMessage: string;
try {
const response = JSON.parse(xhr.responseText);
errorMessage = response.message;
@ -143,118 +149,129 @@ export class WorkspaceLoader {
/**
* Handles workspace status.
*/
handleWorkspace(): Promise<void> {
private async handleWorkspace(): Promise<void> {
if (this.workspace.status === 'RUNNING') {
return new Promise((resolve, reject) => {
this.checkWorkspaceRuntime().then(resolve, reject);
});
} else if (this.workspace.status === 'STOPPING') {
this.startAfterStopping = true;
return await this.checkWorkspaceRuntime();
}
const masterApiConnectionPromise = new Promise((resolve, reject) => {
if (this.workspace.status === 'STOPPED') {
this.startWorkspace().then(resolve, reject);
} else {
resolve();
const masterApi = await this.connectMasterApi();
this.subscribeWorkspaceEvents(masterApi);
masterApi.addListener('open', async () => {
try {
await this.checkWorkspaceRuntime();
this.runtimeIsAccessible.resolve();
} catch (e) { }
});
if (this.workspace.status === 'STOPPED') {
try {
await this.startWorkspace();
} catch (e) {
this.runtimeIsAccessible.reject(e);
}
}).then(() => this.connectMasterApi());
}
const runningOnConnectionPromise = masterApiConnectionPromise
.then((masterApi: CheJsonRpcMasterApi) =>
new Promise((resolve, reject) => {
masterApi.addListener('open', () => {
this.checkWorkspaceRuntime().then(resolve, reject);
});
}));
const runningOnStatusChangePromise = masterApiConnectionPromise
.then((masterApi: CheJsonRpcMasterApi) =>
this.subscribeWorkspaceEvents(masterApi));
return Promise.race([runningOnConnectionPromise, runningOnStatusChangePromise]);
return this.runtimeIsAccessible.promise;
}
/**
* Shows environment outputs.
* Shows environment and installer outputs.
*
* @param message output message
* @param message
*/
onEnvironmentOutput(message): void {
this.loader.log(message);
private onLogOutput(message: che.workspace.event.RuntimeLogEvent | che.workspace.event.InstallerLogEvent): void {
this.loader.log(message.text);
}
connectMasterApi(): Promise<CheJsonRpcMasterApi> {
return new Promise((resolve, reject) => {
const entryPoint = this.websocketBaseURL() + WEBSOCKET_CONTEXT;
const master = new CheJsonRpcMasterApi(new WebsocketClient(), entryPoint, this);
master.connect()
.then(() => resolve(master))
.catch((error: any) => reject(error));
});
/**
* Resolves deferred when workspace is running and runtime is accessible.
*
* @param message
*/
private async onWorkspaceStatus(message: che.workspace.event.WorkspaceStatusEvent): Promise<void> {
if (message.error) {
this.runtimeIsAccessible.reject(new Error(`Failed to run the workspace: "${message.error}"`));
return;
}
if (message.status === 'RUNNING') {
try {
await this.checkWorkspaceRuntime();
this.runtimeIsAccessible.resolve();
} catch (e) {
this.runtimeIsAccessible.reject(e);
}
return;
}
if (message.status === 'STOPPED') {
if (message.prevStatus === 'STARTING') {
this.loader.error('Workspace stopped.');
this.runtimeIsAccessible.reject('Workspace stopped.');
}
if (message.prevStatus === 'STOPPING') {
try {
await this.startWorkspace();
} catch (e) {
this.runtimeIsAccessible.reject(e);
}
}
}
}
private async connectMasterApi(): Promise<CheJsonRpcMasterApi> {
const entryPoint = this.websocketBaseURL() + WEBSOCKET_CONTEXT;
const master = new CheJsonRpcMasterApi(new WebsocketClient(), entryPoint, this);
await master.connect();
return master;
}
/**
* Subscribes to the workspace events.
*/
subscribeWorkspaceEvents(masterApi: CheJsonRpcMasterApi): Promise<any> {
return new Promise((resolve, reject) => {
masterApi.subscribeEnvironmentOutput(this.workspace.id,
(message: any) => this.onEnvironmentOutput(message.text));
masterApi.subscribeInstallerOutput(this.workspace.id,
(message: any) => this.onEnvironmentOutput(message.text));
masterApi.subscribeWorkspaceStatus(this.workspace.id,
(message: WorkspaceStatusChangedEvent) => {
if (message.error) {
reject(new Error(`Failed to run the workspace: "${message.error}"`));
} else if (message.status === 'RUNNING') {
this.checkWorkspaceRuntime().then(resolve, reject);
} else if (message.status === 'STOPPED') {
if (message.prevStatus === 'STARTING') {
this.loader.error('Workspace stopped.');
this.loader.hideLoader();
this.loader.showReload();
}
if (this.startAfterStopping) {
this.startWorkspace().catch((error: any) => reject(error));
}
}
});
});
private subscribeWorkspaceEvents(masterApi: CheJsonRpcMasterApi): void {
masterApi.subscribeEnvironmentOutput(
this.workspace.id,
(message: che.workspace.event.RuntimeLogEvent) => this.onLogOutput(message)
);
masterApi.subscribeInstallerOutput(
this.workspace.id,
(message: che.workspace.event.InstallerLogEvent) => this.onLogOutput(message)
);
masterApi.subscribeWorkspaceStatus(
this.workspace.id,
(message: che.workspace.event.WorkspaceStatusEvent) => this.onWorkspaceStatus(message)
);
}
checkWorkspaceRuntime(): Promise<any> {
return new Promise((resolve, reject) => {
this.getWorkspace(this.workspace.id).then(workspace => {
if (workspace.status === 'RUNNING') {
if (workspace.runtime) {
resolve();
} else {
reject(new Error('You do not have permissions to access workspace runtime, in this case IDE cannot be loaded.'));
}
}
});
});
private async checkWorkspaceRuntime(): Promise<void> {
const workspace = await this.getWorkspace(this.workspace.id);
if (workspace.status !== 'RUNNING') {
throw new Error('Workspace is NOT RUNNING yet.');
}
if (!workspace.runtime) {
throw new Error('You do not have permissions to access workspace runtime, in this case IDE cannot be loaded.');
}
}
/**
* Opens IDE for the workspace.
*/
openIDE(): void {
this.getWorkspace(this.workspace.id).then(workspace => {
const machines = workspace.runtime.machines || [];
for (const machineName of Object.keys(machines)) {
const servers = machines[machineName].servers || [];
for (const serverId of Object.keys(servers)) {
const attributes = servers[serverId].attributes;
if (attributes['type'] === 'ide') {
this.openURL(servers[serverId].url + this.getQueryString());
return;
}
async openIDE(): Promise<void> {
const workspace = await this.getWorkspace(this.workspace.id);
const machines = workspace.runtime.machines || [];
for (const machineName of Object.keys(machines)) {
const servers = machines[machineName].servers || [];
for (const serverId of Object.keys(servers)) {
const attributes = servers[serverId].attributes;
if (attributes['type'] === 'ide') {
this.openURL(servers[serverId].url + this.getQueryString());
return;
}
}
this.openURL(workspace.links.ide + this.getQueryString());
});
}
this.openURL(workspace.links.ide + this.getQueryString());
}
/**
@ -278,7 +295,6 @@ export class WorkspaceLoader {
xhr.setRequestHeader('Authorization', 'Bearer ' + this.keycloak.token);
resolve(xhr);
}).error(() => {
console.log('Failed to refresh token');
window.sessionStorage.setItem('oidcIdeRedirectUrl', location.href);
this.keycloak.login();
reject(new Error('Failed to refresh token'));

File diff suppressed because it is too large Load Diff

View File

@ -10,48 +10,60 @@
* Red Hat, Inc. - initial API and implementation
*******************************************************************************/
const path = require('path');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.css$/,
use: [{
loader: "style-loader" // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}]
}
]
},
devServer: {
contentBase: './dist',
port: 3000,
index: 'index.html',
historyApiFallback: true,
proxy: {
'/api/websocket': {
target: 'http://localhost:8080',
ws: true,
changeOrigin: true
module.exports = env => {
const proxyTarget = env && env.target ? env.target : 'http://localhost:8080';
return merge(
common,
{
mode: 'development',
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.css$/,
use: [{
loader: "style-loader" // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}]
}
]
},
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
devServer: {
contentBase: path.resolve(__dirname, './target/dist'),
host: 'localhost',
port: 3000,
index: 'index.html',
historyApiFallback: true,
proxy: {
'/api/websocket': {
target: proxyTarget,
ws: true,
changeOrigin: true
},
'/api': {
target: proxyTarget,
changeOrigin: true,
headers: {
origin: proxyTarget
}
},
}
},
plugins: [
new HtmlWebpackPlugin({
inject: false,
title: "Che Workspace Loader",
template: "src/index.html",
urlPrefix: "/"
})
]
}
},
plugins: [
new HtmlWebpackPlugin({
inject: false,
title: "Che Workspace Loader",
template: "src/index.html",
urlPrefix: "/"
})
]
});
)
};