Skip to content

Commit c8ce7d9

Browse files
authored
Start workspace from default devfile on private repository SSH url (#1286)
Remove the Devfile resolve from a privatre repositry via an SSH url is not supported warning and start workspace from the default devfile in this case.
1 parent e3d2d8d commit c8ce7d9

File tree

4 files changed

+23
-223
lines changed

4 files changed

+23
-223
lines changed

packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/__tests__/index.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ describe('Creating steps, applying a devfile', () => {
238238
expect(prepareDevfile).toHaveBeenCalledWith(
239239
expect.objectContaining({
240240
attributes: {
241+
'controller.devfile.io/bootstrap-devworkspace': true,
241242
defaultDevfile: true,
242243
},
243244
}),

packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/index.tsx

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import { ProgressStepTitle } from '@/components/WorkspaceProgress/StepTitle';
3131
import { TimeLimit } from '@/components/WorkspaceProgress/TimeLimit';
3232
import { lazyInject } from '@/inversify.config';
3333
import devfileApi from '@/services/devfileApi';
34-
import { FactoryLocationAdapter } from '@/services/factory-location-adapter';
3534
import {
3635
buildFactoryParams,
3736
FactoryParams,
@@ -181,14 +180,12 @@ class CreatingStepApplyDevfile extends ProgressStep<Props, State> {
181180

182181
// when using the default devfile instead of a user devfile
183182
if (factoryResolver === undefined && isEqual(devfile, defaultDevfile)) {
184-
if (FactoryLocationAdapter.isSshLocation(factoryParams.sourceUrl)) {
185-
if (!devfile.attributes) {
186-
devfile.attributes = {};
187-
}
188-
189-
devfile.attributes['controller.devfile.io/bootstrap-devworkspace'] = true;
183+
if (!devfile.attributes) {
184+
devfile.attributes = {};
190185
}
191186

187+
devfile.attributes['controller.devfile.io/bootstrap-devworkspace'] = true;
188+
192189
if (devfile.projects === undefined) {
193190
devfile.projects = [];
194191
}
@@ -203,13 +200,11 @@ class CreatingStepApplyDevfile extends ProgressStep<Props, State> {
203200
}
204201
}
205202
} else if (factoryResolver?.source === 'repo') {
206-
if (FactoryLocationAdapter.isSshLocation(factoryParams.sourceUrl)) {
207-
if (!devfile.attributes) {
208-
devfile.attributes = {};
209-
}
210-
211-
devfile.attributes['controller.devfile.io/bootstrap-devworkspace'] = true;
203+
if (!devfile.attributes) {
204+
devfile.attributes = {};
212205
}
206+
207+
devfile.attributes['controller.devfile.io/bootstrap-devworkspace'] = true;
213208
}
214209

215210
if (remotes) {

packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Fetch/Devfile/__tests__/index.spec.tsx

Lines changed: 10 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
/* eslint-disable @typescript-eslint/no-non-null-assertion */
1414

1515
import { FACTORY_LINK_ATTR } from '@eclipse-che/common';
16-
import { AlertVariant } from '@patternfly/react-core';
1716
import { cleanup, screen, waitFor } from '@testing-library/react';
1817
import userEvent, { UserEvent } from '@testing-library/user-event';
1918
import React from 'react';
@@ -346,7 +345,7 @@ describe('Creating steps, fetching a devfile', () => {
346345
});
347346
});
348347

349-
describe('unsupported git provider error', () => {
348+
describe('unsupported git provider', () => {
350349
let emptyStore: Store;
351350
const rejectReason = 'Failed to fetch devfile';
352351

@@ -355,110 +354,13 @@ describe('Creating steps, fetching a devfile', () => {
355354
mockRequestFactoryResolver.mockRejectedValueOnce(rejectReason);
356355
});
357356

358-
test('alert title', async () => {
357+
test('should continue with the default devfile', async () => {
359358
renderComponent(emptyStore, searchParams);
360359

361360
await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS);
362361

363-
const expectAlertItem = expect.objectContaining({
364-
title: 'Warning',
365-
actionCallbacks: [
366-
expect.objectContaining({
367-
title: 'Continue with default devfile',
368-
callback: expect.any(Function),
369-
}),
370-
expect.objectContaining({
371-
title: 'Reload',
372-
callback: expect.any(Function),
373-
}),
374-
],
375-
});
376-
await waitFor(() => expect(mockOnError).toHaveBeenCalledWith(expectAlertItem));
377-
378-
expect(mockOnNextStep).not.toHaveBeenCalled();
379-
});
380-
381-
test('action "Continue with default devfile"', async () => {
382-
// this deferred object will help run the callback at the right time
383-
const deferred = getDefer();
384-
385-
const actionTitle = 'Continue with default devfile';
386-
mockOnError.mockImplementationOnce((alertItem: AlertItem) => {
387-
const action = alertItem.actionCallbacks?.find(_action =>
388-
_action.title.startsWith(actionTitle),
389-
);
390-
expect(action).toBeDefined();
391-
392-
if (action) {
393-
deferred.promise.then(action.callback);
394-
} else {
395-
throw new Error('Action not found');
396-
}
397-
});
398-
399-
renderComponent(emptyStore, searchParams);
400-
await jest.runAllTimersAsync();
401-
402-
await waitFor(() => expect(mockOnError).toHaveBeenCalled());
403-
expect(mockOnRestart).not.toHaveBeenCalled();
404-
expect(mockOnNextStep).not.toHaveBeenCalled();
405-
406-
mockOnError.mockClear();
407-
408-
/* test the action */
409-
await jest.runOnlyPendingTimersAsync();
410-
411-
// resolve deferred to trigger the callback
412-
deferred.resolve();
413-
414-
await waitFor(() => expect(mockOnNextStep).toHaveBeenCalled());
415-
expect(mockOnRestart).not.toHaveBeenCalled();
416-
expect(mockOnError).not.toHaveBeenCalled();
417-
});
418-
419-
test('action "Reload"', async () => {
420-
// this deferred object will help run the callback at the right time
421-
const deferred = getDefer();
422-
423-
const actionTitle = 'Reload';
424-
mockOnError.mockImplementationOnce(async (alertItem: AlertItem) => {
425-
const action = alertItem.actionCallbacks?.find(_action =>
426-
_action.title.startsWith(actionTitle),
427-
);
428-
expect(action).toBeDefined();
429-
430-
if (action) {
431-
deferred.promise.then(action.callback);
432-
} else {
433-
throw new Error('Action not found');
434-
}
435-
});
436-
437-
renderComponent(emptyStore, searchParams);
438-
await jest.runAllTimersAsync();
439-
440-
await waitFor(() => expect(mockOnError).toHaveBeenCalled());
441-
expect(mockOnRestart).not.toHaveBeenCalled();
442-
expect(mockOnNextStep).not.toHaveBeenCalled();
443-
444-
// first call resolves with error
445-
expect(mockRequestFactoryResolver).toHaveBeenCalledTimes(1);
446-
447-
mockOnError.mockClear();
448-
449-
/* test the action */
450-
451-
await jest.runAllTimersAsync();
452-
453-
// resolve deferred to trigger the callback
454-
deferred.resolve();
455-
456-
await waitFor(() => expect(mockOnRestart).toHaveBeenCalled());
457-
expect(mockOnNextStep).not.toHaveBeenCalled();
458-
expect(mockOnError).not.toHaveBeenCalled();
459-
460-
// should request the factory resolver for the second time
461-
await waitFor(() => expect(mockRequestFactoryResolver).toHaveBeenCalledTimes(2));
362+
await waitFor(() => expect(mockOnError).not.toHaveBeenCalled());
363+
expect(mockOnNextStep).toHaveBeenCalled();
462364
});
463365
});
464366

@@ -695,31 +597,6 @@ describe('Creating steps, fetching a devfile', () => {
695597
const protocol = 'http://';
696598
const factoryUrl = 'git@github.com:user/repository-name.git';
697599
const emptyStore = new MockStoreBuilder().build();
698-
const sshPrivateRepoAllertItem = expect.objectContaining({
699-
title: 'Warning',
700-
variant: AlertVariant.warning,
701-
children: (
702-
<ExpandableWarning
703-
textBefore="Devfile resolve from a privatre repositry via an SSH url is not supported."
704-
errorMessage="Could not reach devfile"
705-
textAfter="Apply a Personal Access Token to fetch the devfile.yaml content."
706-
/>
707-
),
708-
actionCallbacks: [
709-
expect.objectContaining({
710-
title: 'Continue with default devfile',
711-
callback: expect.any(Function),
712-
}),
713-
expect.objectContaining({
714-
title: 'Reload',
715-
callback: expect.any(Function),
716-
}),
717-
expect.objectContaining({
718-
title: 'Open Documentation page',
719-
callback: expect.any(Function),
720-
}),
721-
],
722-
});
723600

724601
let spyWindowLocation: jest.SpyInstance;
725602
let location: Location;
@@ -771,32 +648,32 @@ describe('Creating steps, fetching a devfile', () => {
771648
expect(mockOnError).not.toHaveBeenCalled();
772649
});
773650

774-
it('should show warning on SSH url', async () => {
651+
it('should use default devfile on private SSH url', async () => {
775652
searchParams = new URLSearchParams({
776653
[FACTORY_URL_ATTR]: 'git@github.com:user/repository.git',
777654
});
778655

779656
renderComponent(emptyStore, searchParams, location);
780657

781658
await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS);
782-
await waitFor(() => expect(mockOnNextStep).not.toHaveBeenCalled);
659+
await waitFor(() => expect(mockOnNextStep).toHaveBeenCalled());
783660

784661
expect(mockOpenOAuthPage).not.toHaveBeenCalled();
785-
expect(mockOnError).toHaveBeenCalledWith(sshPrivateRepoAllertItem);
662+
expect(mockOnError).not.toHaveBeenCalled();
786663
});
787664

788-
it('should show warning on bitbucket-server SSH url', async () => {
665+
it('should use default devfile on bitbucket-server SSH url', async () => {
789666
searchParams = new URLSearchParams({
790667
[FACTORY_URL_ATTR]: 'ssh://git@bitbucket-server.com/~user/repository.git',
791668
});
792669

793670
renderComponent(emptyStore, searchParams, location);
794671

795672
await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS);
796-
await waitFor(() => expect(mockOnNextStep).not.toHaveBeenCalled);
673+
await waitFor(() => expect(mockOnNextStep).toHaveBeenCalled);
797674

798675
expect(mockOpenOAuthPage).not.toHaveBeenCalled();
799-
expect(mockOnError).toHaveBeenCalledWith(sshPrivateRepoAllertItem);
676+
expect(mockOnError).not.toHaveBeenCalled();
800677
});
801678
});
802679
});

packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Fetch/Devfile/index.tsx

Lines changed: 4 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,6 @@ export class ApplyingDevfileError extends Error {
4646
}
4747
}
4848

49-
export class UnsupportedGitProviderError extends Error {
50-
constructor(message: string) {
51-
super(message);
52-
this.name = 'UnsupportedGitProviderError';
53-
}
54-
}
55-
56-
export class SSHPrivateRepositoryUrlError extends Error {
57-
constructor(message: string) {
58-
super(message);
59-
this.name = 'UnsupportedGitProviderError';
60-
}
61-
}
62-
6349
const RELOADS_LIMIT = 2;
6450
type ReloadsInfo = {
6551
[url: string]: number;
@@ -77,7 +63,6 @@ export type State = ProgressStepState & {
7763

7864
class CreatingStepFetchDevfile extends ProgressStep<Props, State> {
7965
protected readonly name = 'Inspecting repo';
80-
private readonly sshPattern = new RegExp('(git@|(ssh|git)://).*');
8166

8267
constructor(props: Props) {
8368
super(props);
@@ -188,10 +173,6 @@ class CreatingStepFetchDevfile extends ProgressStep<Props, State> {
188173
this.clearStepError();
189174
}
190175

191-
protected handleOpenDocumentationPage(): void {
192-
window.open(this.props.branding.docs.startWorkspaceFromGit, '_blank');
193-
}
194-
195176
protected handleTimeout(): void {
196177
const timeoutError = new Error(
197178
`Devfile hasn't been resolved in the last ${TIMEOUT_TO_RESOLVE_SEC} seconds.`,
@@ -231,14 +212,12 @@ class CreatingStepFetchDevfile extends ProgressStep<Props, State> {
231212
}
232213
if (
233214
errorMessage === 'Failed to fetch devfile' ||
215+
errorMessage ===
216+
'Cannot build factory with any of the provided parameters. Please check parameters correctness, and resend query.' ||
234217
errorMessage.startsWith('Could not reach devfile')
235218
) {
236-
// check if the source url is an SSH url
237-
if (this.sshPattern.test(sourceUrl)) {
238-
throw new SSHPrivateRepositoryUrlError(errorMessage);
239-
} else {
240-
throw new UnsupportedGitProviderError(errorMessage);
241-
}
219+
this.setState({ useDefaultDevfile: true });
220+
return true;
242221
}
243222
throw e;
244223
}
@@ -359,58 +338,6 @@ class CreatingStepFetchDevfile extends ProgressStep<Props, State> {
359338
],
360339
};
361340
}
362-
if (error instanceof UnsupportedGitProviderError) {
363-
return {
364-
key,
365-
title: 'Warning',
366-
variant: AlertVariant.warning,
367-
children: (
368-
<ExpandableWarning
369-
textBefore="Could not find any devfile in the Git repository"
370-
errorMessage={helpers.errors.getMessage(error)}
371-
textAfter="The Git provider is not supported."
372-
/>
373-
),
374-
actionCallbacks: [
375-
{
376-
title: 'Continue with default devfile',
377-
callback: () => this.handleDefaultDevfile(key),
378-
},
379-
{
380-
title: 'Reload',
381-
callback: () => this.handleRestart(key),
382-
},
383-
],
384-
};
385-
}
386-
if (error instanceof SSHPrivateRepositoryUrlError) {
387-
return {
388-
key,
389-
title: 'Warning',
390-
variant: AlertVariant.warning,
391-
children: (
392-
<ExpandableWarning
393-
textBefore="Devfile resolve from a privatre repositry via an SSH url is not supported."
394-
errorMessage={helpers.errors.getMessage(error)}
395-
textAfter="Apply a Personal Access Token to fetch the devfile.yaml content."
396-
/>
397-
),
398-
actionCallbacks: [
399-
{
400-
title: 'Continue with default devfile',
401-
callback: () => this.handleDefaultDevfile(key),
402-
},
403-
{
404-
title: 'Reload',
405-
callback: () => this.handleRestart(key),
406-
},
407-
{
408-
title: 'Open Documentation page',
409-
callback: () => this.handleOpenDocumentationPage(),
410-
},
411-
],
412-
};
413-
}
414341
return {
415342
key,
416343
title: 'Failed to create the workspace',

0 commit comments

Comments
 (0)