Skip to content

Commit

Permalink
feat: handle gracefully when an invalid devfile is found in a git rep…
Browse files Browse the repository at this point in the history
…ository

Signed-off-by: Oleksii Orel <[email protected]>
  • Loading branch information
olexii4 committed Oct 14, 2022
1 parent 003c5ea commit e653195
Show file tree
Hide file tree
Showing 17 changed files with 954 additions and 36 deletions.
2 changes: 1 addition & 1 deletion packages/dashboard-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"process": "^0.11.10",
"qs": "^6.9.4",
"react": "^16.14.0",
"react-copy-to-clipboard": "^5.0.2",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^16.12.0",
"react-helmet": "^6.1.0",
"react-pluralize": "^1.6.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { isEqual } from 'lodash';
import { AlertVariant } from '@patternfly/react-core';
import { helpers } from '@eclipse-che/common';
import common, { helpers } from '@eclipse-che/common';
import { AppState } from '../../../../../../store';
import * as WorkspacesStore from '../../../../../../store/Workspaces';
import { DisposableCollection } from '../../../../../../services/helpers/disposable';
Expand All @@ -36,6 +36,10 @@ import { FactoryParams } from '../../../types';
import buildFactoryParams from '../../../buildFactoryParams';
import { AbstractLoaderStep, LoaderStepProps, LoaderStepState } from '../../../../AbstractStep';
import { AlertItem } from '../../../../../../services/helpers/types';
import { selectDefaultDevfile } from '../../../../../../store/DevfileRegistries/selectors';
import { getProjectName } from '../../../../../../services/helpers/getProjectName';
import CreateWorkspaceErrorItems from '../../../../../../pages/Loader/Factory/CreateWorkspaceErrorItems';
import { isEqual } from 'lodash';

export type Props = MappedProps &
LoaderStepProps & {
Expand All @@ -48,6 +52,13 @@ export type State = LoaderStepState & {
shouldCreate: boolean; // should the loader create a workspace
};

export class CreateWorkspaceError extends Error {
constructor(message: string) {
super(message);
this.name = 'CreateWorkspaceError';
}
}

class StepApplyDevfile extends AbstractLoaderStep<Props, State> {
protected readonly toDispose = new DisposableCollection();

Expand Down Expand Up @@ -126,12 +137,45 @@ class StepApplyDevfile extends AbstractLoaderStep<Props, State> {
this.props.onRestart();
}

private updateCurrentDevfile(devfile: devfileApi.Devfile): void {
const { factoryResolver, allWorkspaces, defaultDevfile } = this.props;
const { factoryParams } = this.state;
const { factoryId, policiesCreate, storageType } = factoryParams;
// check if it is a using default devfile flow
if (factoryResolver === undefined && isEqual(devfile, defaultDevfile)) {
if (devfile.projects === undefined) {
devfile.projects = [];
}
if (devfile.projects.length === 0) {
// adds a default project from the source URL
const sourceUrl = new URL(factoryParams.sourceUrl);
const name = getProjectName(factoryParams.sourceUrl);
const origin = sourceUrl.pathname.endsWith('.git')
? `${sourceUrl.origin}${sourceUrl.pathname}`
: `${sourceUrl.origin}${sourceUrl.pathname}.git`;
devfile.projects[0] = { git: { remotes: { origin } }, name };
// change default name
devfile.metadata.name = name;
devfile.metadata.generateName = name;
}
}
// test the devfile name to decide if we need to append a suffix to is
const nameConflict = allWorkspaces.some(w => devfile.metadata.name === w.name);
const appendSuffix = policiesCreate === 'perclick' || nameConflict;

const updatedDevfile = prepareDevfile(devfile, factoryId, storageType, appendSuffix);

this.setState({
devfile: updatedDevfile,
newWorkspaceName: updatedDevfile.metadata.name,
});
}

protected async runStep(): Promise<boolean> {
await delay(MIN_STEP_DURATION_MS);

const { factoryResolverConverted } = this.props;
const { shouldCreate, factoryParams, devfile } = this.state;
const { factoryId, policiesCreate, storageType } = factoryParams;
const { factoryResolverConverted, factoryResolver, defaultDevfile } = this.props;
const { shouldCreate, devfile } = this.state;

const workspace = this.findTargetWorkspace(this.props, this.state);
if (workspace !== undefined) {
Expand All @@ -150,25 +194,28 @@ class StepApplyDevfile extends AbstractLoaderStep<Props, State> {
}

if (devfile === undefined) {
if (factoryResolver === undefined) {
const _devfile = defaultDevfile;
if (_devfile === undefined) {
throw new Error('Failed to resolve the default devfile.');
}
this.updateCurrentDevfile(_devfile);
return false;
}
const _devfile = factoryResolverConverted?.devfileV2;
if (_devfile === undefined) {
throw new Error('Failed to resolve the devfile.');
}

// test the devfile name to decide if we need to append a suffix to is
const nameConflict = this.props.allWorkspaces.some(w => _devfile.metadata.name === w.name);
const appendSuffix = policiesCreate === 'perclick' || nameConflict;

const updatedDevfile = prepareDevfile(_devfile, factoryId, storageType, appendSuffix);

this.setState({
devfile: updatedDevfile,
newWorkspaceName: updatedDevfile.metadata.name,
});
this.updateCurrentDevfile(_devfile);
return false;
}

await this.createWorkspaceFromDevfile(devfile);
try {
await this.createWorkspaceFromDevfile(devfile);
} catch (e) {
const errorMessage = common.helpers.errors.getMessage(e);
throw new CreateWorkspaceError(errorMessage);
}

// wait for the workspace creation to complete
try {
Expand Down Expand Up @@ -206,7 +253,38 @@ class StepApplyDevfile extends AbstractLoaderStep<Props, State> {
);
}

private handleCreateWorkspaceError(): void {
const { defaultDevfile } = this.props;
const { devfile } = this.state;
const _devfile = defaultDevfile;
if (_devfile && devfile) {
_devfile.projects = devfile.projects;
_devfile.metadata.name = devfile.metadata.name;
_devfile.metadata.generateName = devfile.metadata.generateName;
this.updateCurrentDevfile(_devfile);
}
this.clearStepError();
}

private getAlertItem(error: unknown): AlertItem | undefined {
if (error instanceof CreateWorkspaceError) {
return {
key: 'factory-loader-create-workspace-error',
title: 'Warning',
variant: AlertVariant.warning,
children: <CreateWorkspaceErrorItems error={error} />,
actionCallbacks: [
{
title: 'Continue with the default devfile',
callback: () => this.handleCreateWorkspaceError(),
},
{
title: 'Reload',
callback: () => this.clearStepError(),
},
],
};
}
if (!error) {
return;
}
Expand Down Expand Up @@ -249,6 +327,7 @@ const mapStateToProps = (state: AppState) => ({
defaultNamespace: selectDefaultNamespace(state),
factoryResolver: selectFactoryResolver(state),
factoryResolverConverted: selectFactoryResolverConverted(state),
defaultDevfile: selectDefaultDevfile(state),
});

const connector = connect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,17 @@ class StepCheckExistingWorkspaces extends AbstractLoaderStep<Props, State> {
let newWorkspaceName: string;
if (prevLoaderStep.id === LoadingStep.CREATE_WORKSPACE__FETCH_DEVFILE) {
if (
factoryResolver?.location !== factoryParams.sourceUrl ||
factoryResolverConverted?.devfileV2 === undefined
factoryResolver?.location === factoryParams.sourceUrl &&
factoryResolverConverted?.devfileV2 !== undefined
) {
const devfile = factoryResolverConverted.devfileV2;
newWorkspaceName = devfile.metadata.name;
} else {
if (factoryResolver === undefined) {
return true;
}
throw new Error('Failed to resolve the devfile.');
}
const devfile = factoryResolverConverted.devfileV2;
newWorkspaceName = devfile.metadata.name;
} else {
const resources = devWorkspaceResources[factoryParams.sourceUrl]?.resources;
if (resources === undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { isEqual } from 'lodash';
import { helpers } from '@eclipse-che/common';
import common, { helpers } from '@eclipse-che/common';
import { AlertVariant } from '@patternfly/react-core';
import { AppState } from '../../../../../../store';
import * as FactoryResolverStore from '../../../../../../store/FactoryResolver';
Expand All @@ -35,7 +35,14 @@ import { MIN_STEP_DURATION_MS, TIMEOUT_TO_RESOLVE_SEC } from '../../../../const'
import buildFactoryParams from '../../../buildFactoryParams';
import { AbstractLoaderStep, LoaderStepProps, LoaderStepState } from '../../../../AbstractStep';
import { AlertItem } from '../../../../../../services/helpers/types';
import OAuthService, { isOAuthResponse } from '../../../../../../services/oauth';
import ApplyingDevfileErrorItems from '../../../../../../pages/Loader/Factory/ApplyingDevfileErrorItems';

export class ApplyingDevfileError extends Error {
constructor(message) {
super(message);
this.name = 'ApplyingDevfileError';
}
}

const RELOADS_LIMIT = 2;
type ReloadsInfo = {
Expand All @@ -49,6 +56,7 @@ export type Props = MappedProps &
export type State = LoaderStepState & {
factoryParams: FactoryParams;
shouldResolve: boolean;
useDefaultDevfile: boolean;
};

class StepFetchDevfile extends AbstractLoaderStep<Props, State> {
Expand All @@ -60,6 +68,7 @@ class StepFetchDevfile extends AbstractLoaderStep<Props, State> {
this.state = {
factoryParams: buildFactoryParams(props.searchParams),
shouldResolve: true,
useDefaultDevfile: false,
};
}

Expand Down Expand Up @@ -106,9 +115,9 @@ class StepFetchDevfile extends AbstractLoaderStep<Props, State> {

private init() {
const { factoryResolver } = this.props;
const { factoryParams } = this.state;
const { factoryParams, useDefaultDevfile } = this.state;
const { sourceUrl } = factoryParams;
if (sourceUrl && sourceUrl === factoryResolver?.location) {
if (sourceUrl && (useDefaultDevfile || sourceUrl === factoryResolver?.location)) {
// prevent a resource being fetched one more time
this.setState({
shouldResolve: false,
Expand All @@ -129,7 +138,7 @@ class StepFetchDevfile extends AbstractLoaderStep<Props, State> {
protected async runStep(): Promise<boolean> {
await delay(MIN_STEP_DURATION_MS);

const { factoryParams, shouldResolve } = this.state;
const { factoryParams, shouldResolve, useDefaultDevfile } = this.state;
const { currentStepIndex, factoryResolver, factoryResolverConverted, loaderSteps } = this.props;
const { sourceUrl } = factoryParams;

Expand All @@ -151,15 +160,30 @@ class StepFetchDevfile extends AbstractLoaderStep<Props, State> {
}

if (shouldResolve === false) {
if (useDefaultDevfile) {
// go to the next step
return true;
}

if (this.state.lastError instanceof Error) {
throw this.state.lastError;
}
throw new Error('Failed to resolve the devfile.');
}

// start resolving the devfile
const resolveDone = await this.resolveDevfile(sourceUrl);
if (resolveDone === false) {
let resolveDone = false;
try {
// start resolving the devfile
resolveDone = await this.resolveDevfile(sourceUrl);
} catch (e) {
const errorMessage = common.helpers.errors.getMessage(e);
// check if it is a scheme validation error
if (errorMessage.includes('schema validation failed')) {
throw new ApplyingDevfileError(errorMessage);
}
throw e;
}
if (!resolveDone) {
return false;
}

Expand All @@ -176,6 +200,13 @@ class StepFetchDevfile extends AbstractLoaderStep<Props, State> {
}
}

private handleDevfileError(): void {
this.setState({
useDefaultDevfile: true,
});
this.clearStepError();
}

/**
* Resolves promise with `true` if devfile resolved successfully. Resolves promise with `false` if the devfile needs to be resolved one more time after authentication.
*/
Expand Down Expand Up @@ -261,6 +292,24 @@ class StepFetchDevfile extends AbstractLoaderStep<Props, State> {
}

private getAlertItem(error: unknown): AlertItem | undefined {
if (error instanceof ApplyingDevfileError) {
return {
key: 'factory-loader-devfile-error',
title: 'Warning',
variant: AlertVariant.warning,
children: <ApplyingDevfileErrorItems error={error} />,
actionCallbacks: [
{
title: 'Continue with the default devfile',
callback: () => this.handleDevfileError(),
},
{
title: 'Reload',
callback: () => this.clearStepError(),
},
],
};
}
if (!error) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ import { AppState } from '../../../store';
import * as DevfileRegistriesStore from '../../../store/DevfileRegistries';
import { SampleCard } from './SampleCard';
import { AlertItem } from '../../../services/helpers/types';
import { selectMetadataFiltered } from '../../../store/DevfileRegistries/selectors';
import {
EMPTY_WORKSPACE_TAG,
selectMetadataFiltered,
} from '../../../store/DevfileRegistries/selectors';
import { selectWorkspacesSettings } from '../../../store/Workspaces/Settings/selectors';
import * as FactoryResolverStore from '../../../store/FactoryResolver';
import { isDevworkspacesEnabled } from '../../../services/helpers/devworkspace';
Expand Down Expand Up @@ -60,7 +63,6 @@ type State = {
};

export const VISIBLE_TAGS = ['Community', 'Tech-Preview'];
export const EMPTY_WORKSPACE_TAG = 'Empty';

const EXCLUDED_TARGET_EDITOR_NAMES = ['dirigible', 'jupyter', 'eclipseide', 'code-server'];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2018-2021 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

.error {
border-top: 2px solid;
border-color: #c9190b;
background-color: #faeae8;
}

.error .textMessage {
font-size: small;
overflow: hidden;
text-overflow: ellipsis;
}

.error button {
font-size: small;
}
Loading

0 comments on commit e653195

Please sign in to comment.