From b1218bd06b6abc3514d3b6271fe2f8215099543d Mon Sep 17 00:00:00 2001 From: Abdelrahman Shawki Hassan Date: Wed, 13 Nov 2024 09:33:00 +0100 Subject: [PATCH] fix: handle nonexisting LS version (#557) * fix: set cli channel preview --------- Co-authored-by: Bastian Doetsch --- src/snyk/cli/cliExecutable.ts | 25 ++++++++----- src/snyk/cli/staticCliApi.ts | 23 +++++++----- src/snyk/common/commands/commandController.ts | 5 +-- .../common/configuration/configuration.ts | 35 +++++++++++++++---- src/snyk/common/constants/errors.ts | 3 ++ src/snyk/common/download/downloader.ts | 18 +++++++--- src/snyk/common/services/downloadService.ts | 12 +++++-- .../common/services/notificationService.ts | 22 ++++++++++-- src/snyk/common/services/openerService.ts | 1 - src/snyk/extension.ts | 6 ++-- src/test/unit/common/configuration.test.ts | 28 +++++++++++++++ .../common/services/downloadService.test.ts | 8 ++--- src/test/unit/download/downloader.test.ts | 2 +- 13 files changed, 144 insertions(+), 44 deletions(-) create mode 100644 src/snyk/common/constants/errors.ts diff --git a/src/snyk/cli/cliExecutable.ts b/src/snyk/cli/cliExecutable.ts index 3bfe289b5..b87d4e78c 100644 --- a/src/snyk/cli/cliExecutable.ts +++ b/src/snyk/cli/cliExecutable.ts @@ -7,12 +7,17 @@ import { Platform } from '../common/platform'; export class CliExecutable { public static filenameSuffixes: Record = { linux: 'snyk-linux', + // eslint-disable-next-line camelcase linux_arm64: 'snyk-linux-arm64', + // eslint-disable-next-line camelcase linux_alpine: 'snyk-alpine', + // eslint-disable-next-line camelcase linux_alpine_arm64: 'snyk-alpine-arm64', macos: 'snyk-macos', + // eslint-disable-next-line camelcase macos_arm64: 'snyk-macos-arm64', windows: 'snyk-win.exe', + // eslint-disable-next-line camelcase windows_arm64: 'snyk-win.exe', }; constructor(public readonly version: string, public readonly checksum: Checksum) {} @@ -22,8 +27,8 @@ export class CliExecutable { return customPath; } - const platform = await this.getCurrentWithArch(); - const fileName = this.getFileName(platform); + const platform = await CliExecutable.getCurrentWithArch(); + const fileName = CliExecutable.getFileName(platform); return path.join(extensionDir, fileName); } @@ -34,7 +39,7 @@ export class CliExecutable { static async getCurrentWithArch(): Promise { const osName = Platform.getCurrent().toString().toLowerCase(); const archSuffix = Platform.getArch().toLowerCase(); - const platform = await this.getPlatformName(osName); + const platform = await CliExecutable.getPlatformName(osName); let cliName = platform; if (archSuffix === 'arm64') { @@ -46,7 +51,7 @@ export class CliExecutable { static async getPlatformName(osName: string): Promise { let platform = ''; if (osName === 'linux') { - if (await this.isAlpine()) { + if (await CliExecutable.isAlpine()) { platform = 'linux_alpine'; } else { platform = 'linux'; @@ -69,10 +74,12 @@ export class CliExecutable { .catch(() => false); } - static isAlpine(): Promise { - return fs - .access('/etc/alpine-release') - .then(() => true) - .catch(() => false); + static async isAlpine(): Promise { + try { + await fs.access('/etc/alpine-release'); + return true; + } catch (e) { + return false; + } } } diff --git a/src/snyk/cli/staticCliApi.ts b/src/snyk/cli/staticCliApi.ts index 1fcb26847..9646b7246 100644 --- a/src/snyk/cli/staticCliApi.ts +++ b/src/snyk/cli/staticCliApi.ts @@ -7,6 +7,7 @@ import { getAxiosConfig } from '../common/proxy'; import { IVSCodeWorkspace } from '../common/vscode/workspace'; import { CliExecutable } from './cliExecutable'; import { CliSupportedPlatform } from './supportedPlatforms'; +import { ERRORS } from '../common/constants/errors'; export interface IStaticCliApi { getLatestCliVersion(releaseChannel: string): Promise; @@ -44,18 +45,24 @@ export class StaticCliApi implements IStaticCliApi { } async getLatestCliVersion(releaseChannel: string): Promise { - let { data } = await axios.get( - this.getLatestVersionDownloadUrl(releaseChannel), - await getAxiosConfig(this.workspace, this.configuration, this.logger), - ); - data = data.replace('\n', ''); - if (data == '') return Promise.reject(new Error('CLI Version not found')); - return data; + try { + let { data } = await axios.get( + this.getLatestVersionDownloadUrl(releaseChannel), + await getAxiosConfig(this.workspace, this.configuration, this.logger), + ); + data = data.replace('\n', ''); + return data; + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.logger.error(e); + throw Error(ERRORS.DOWNLOAD_FAILED); + } } async downloadBinary(platform: CliSupportedPlatform): Promise<[Promise, CancelTokenSource]> { const axiosCancelToken = axios.CancelToken.source(); - const latestCliVersion = await this.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const cliReleaseChannel = await this.configuration.getCliReleaseChannel(); + const latestCliVersion = await this.getLatestCliVersion(cliReleaseChannel); const downloadUrl = this.getDownloadUrl(latestCliVersion, platform); diff --git a/src/snyk/common/commands/commandController.ts b/src/snyk/common/commands/commandController.ts index bcc550b93..596976f03 100644 --- a/src/snyk/common/commands/commandController.ts +++ b/src/snyk/common/commands/commandController.ts @@ -14,7 +14,7 @@ import { SNYK_TRUST_WORKSPACE_FOLDERS_COMMAND, VSCODE_GO_TO_SETTINGS_COMMAND, } from '../constants/commands'; -import { COMMAND_DEBOUNCE_INTERVAL, SNYK_NAME_EXTENSION, SNYK_PUBLISHER } from '../constants/general'; +import { COMMAND_DEBOUNCE_INTERVAL } from '../constants/general'; import { ErrorHandler } from '../error/errorHandler'; import { ILanguageServer } from '../languageServer/languageServer'; import { CodeIssueData, IacIssueData } from '../languageServer/types'; @@ -79,12 +79,13 @@ export class CommandController { ErrorHandler.handle(e, this.logger); } } + async setBaseBranch(folderPath: string): Promise { await this.folderConfigs.setBranch(this.window, this.configuration, folderPath); } openSettings(): void { - void this.commands.executeCommand(VSCODE_GO_TO_SETTINGS_COMMAND, `@ext:${SNYK_PUBLISHER}.${SNYK_NAME_EXTENSION}`); + void this.commands.executeCommand(VSCODE_GO_TO_SETTINGS_COMMAND, `@ext:${this.configuration.getExtensionId()}`); } async createDCIgnore(custom = false, uriAdapter: IUriAdapter, path?: string): Promise { diff --git a/src/snyk/common/configuration/configuration.ts b/src/snyk/common/configuration/configuration.ts index 3df7b4ad6..51065a7f3 100644 --- a/src/snyk/common/configuration/configuration.ts +++ b/src/snyk/common/configuration/configuration.ts @@ -77,6 +77,8 @@ export interface IConfiguration { authHost: string; + getExtensionId(): string; + setExtensionId(extensionId: string): void; getFeatureFlag(flagName: string): boolean; setFeatureFlag(flagName: string, value: boolean): void; @@ -121,7 +123,7 @@ export interface IConfiguration { isAutomaticDependencyManagementEnabled(): boolean; getCliPath(): Promise; - getCliReleaseChannel(): string; + getCliReleaseChannel(): Promise; getCliBaseDownloadUrl(): string; getInsecure(): boolean; @@ -157,8 +159,18 @@ export class Configuration implements IConfiguration { private readonly defaultCliReleaseChannel = 'stable'; private featureFlag: { [key: string]: boolean } = {}; + private extensionId: string; constructor(private processEnv: NodeJS.ProcessEnv = process.env, private workspace: IVSCodeWorkspace) {} + + getExtensionId(): string { + return this.extensionId; + } + + setExtensionId(extensionId: string): void { + this.extensionId = extensionId; + } + async setCliReleaseChannel(releaseChannel: string): Promise { if (!releaseChannel) return; return this.workspace.updateConfiguration( @@ -168,6 +180,7 @@ export class Configuration implements IConfiguration { true, ); } + async setCliBaseDownloadUrl(baseDownloadUrl: string): Promise { if (!baseDownloadUrl) return; return this.workspace.updateConfiguration( @@ -178,14 +191,22 @@ export class Configuration implements IConfiguration { ); } - getCliReleaseChannel(): string { - return ( - this.workspace.getConfiguration( - CONFIGURATION_IDENTIFIER, - this.getConfigName(ADVANCED_CLI_RELEASE_CHANNEL), - ) ?? this.defaultCliReleaseChannel + async getCliReleaseChannel(): Promise { + let releaseChannel = this.workspace.getConfiguration( + CONFIGURATION_IDENTIFIER, + this.getConfigName(ADVANCED_CLI_RELEASE_CHANNEL), ); + const extensionId = this.getExtensionId(); + // If Extension is preview and has default value of release Channel we override it to preview. + if (extensionId && extensionId.includes('preview') && releaseChannel === this.defaultCliReleaseChannel) { + await this.setCliReleaseChannel('preview'); + releaseChannel = 'preview'; + } else if (!releaseChannel) { + releaseChannel = this.defaultCliReleaseChannel; + } + return releaseChannel; } + getCliBaseDownloadUrl(): string { return ( this.workspace.getConfiguration( diff --git a/src/snyk/common/constants/errors.ts b/src/snyk/common/constants/errors.ts new file mode 100644 index 000000000..32f9f6f20 --- /dev/null +++ b/src/snyk/common/constants/errors.ts @@ -0,0 +1,3 @@ +export const ERRORS = { + DOWNLOAD_FAILED: `Unable to download the Snyk CLI. This could be caused by connectivity issues or the CLI not being available on the selected release channel.`, +}; diff --git a/src/snyk/common/download/downloader.ts b/src/snyk/common/download/downloader.ts index 9a59abfa8..8ab54f425 100644 --- a/src/snyk/common/download/downloader.ts +++ b/src/snyk/common/download/downloader.ts @@ -13,6 +13,7 @@ import { CancellationToken } from '../vscode/types'; import { IVSCodeWindow } from '../vscode/window'; import { CliSupportedPlatform } from '../../cli/supportedPlatforms'; import { ExtensionContext } from '../vscode/extensionContext'; +import { ERRORS } from '../constants/errors'; export type DownloadAxiosResponse = { data: stream.Readable; headers: { [header: string]: unknown } }; @@ -28,11 +29,17 @@ export class Downloader { * Downloads CLI. Existing executable is deleted. */ async download(): Promise { - const platform = await CliExecutable.getCurrentWithArch(); - if (platform === null) { - return Promise.reject(!messages.notSupported); + try { + const platform = await CliExecutable.getCurrentWithArch(); + if (platform === null) { + return Promise.reject(!messages.notSupported); + } + return await this.getCliExecutable(platform); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.logger.error(e); + throw new Error(ERRORS.DOWNLOAD_FAILED); } - return await this.getCliExecutable(platform); } private async getCliExecutable(platform: CliSupportedPlatform): Promise { @@ -43,7 +50,8 @@ export class Downloader { if (await this.binaryExists(cliPath)) { await this.deleteFileAtPath(cliPath); } - const cliVersion = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const cliReleaseChannel = await this.configuration.getCliReleaseChannel(); + const cliVersion = await this.cliApi.getLatestCliVersion(cliReleaseChannel); const sha256 = await this.cliApi.getSha256Checksum(cliVersion, platform); const checksum = await this.downloadCli(cliPath, platform, sha256); diff --git a/src/snyk/common/services/downloadService.ts b/src/snyk/common/services/downloadService.ts index 5834cae53..bb7814002 100644 --- a/src/snyk/common/services/downloadService.ts +++ b/src/snyk/common/services/downloadService.ts @@ -61,7 +61,11 @@ export class DownloadService { async update(): Promise { const platform = await CliExecutable.getCurrentWithArch(); - const version = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const cliReleaseChannel = await this.configuration.getCliReleaseChannel(); + const version = await this.cliApi.getLatestCliVersion(cliReleaseChannel); + if (!version) { + return false; + } const cliInstalled = await this.isCliInstalled(); const cliVersionHasUpdated = this.hasCliVersionUpdated(version); const needsUpdate = cliVersionHasUpdated || this.hasLspVersionUpdated(); @@ -95,7 +99,11 @@ export class DownloadService { } private async isCliUpdateAvailable(platform: CliSupportedPlatform): Promise { - const version = await this.cliApi.getLatestCliVersion(this.configuration.getCliReleaseChannel()); + const cliReleaseChannel = await this.configuration.getCliReleaseChannel(); + const version = await this.cliApi.getLatestCliVersion(cliReleaseChannel); + if (!version) { + return false; + } const latestChecksum = await this.cliApi.getSha256Checksum(version, platform); const path = await CliExecutable.getPath( this.extensionContext.extensionPath, diff --git a/src/snyk/common/services/notificationService.ts b/src/snyk/common/services/notificationService.ts index d8be2d8f7..6052db94c 100644 --- a/src/snyk/common/services/notificationService.ts +++ b/src/snyk/common/services/notificationService.ts @@ -1,6 +1,6 @@ import { snykMessages } from '../../base/messages/snykMessages'; import { IConfiguration } from '../configuration/configuration'; -import { VSCODE_VIEW_CONTAINER_COMMAND } from '../constants/commands'; +import { SNYK_OPEN_BROWSER_COMMAND, VSCODE_VIEW_CONTAINER_COMMAND } from '../constants/commands'; import { ErrorHandler } from '../error/errorHandler'; import { ILog } from '../logger/interfaces'; import { errorsLogs } from '../messages/errors'; @@ -9,7 +9,10 @@ import { IVSCodeWindow } from '../vscode/window'; export interface INotificationService { init(): Promise; + showErrorNotification(message: string): Promise; + + showErrorNotificationWithLinkAction(message: string, actionText: string, actionLink: string): Promise; } export class NotificationService implements INotificationService { @@ -44,6 +47,21 @@ export class NotificationService implements INotificationService { } async showErrorNotification(message: string): Promise { - await this.window.showErrorMessage(message); + await this.showErrorNotificationWithLinkAction( + message, + 'Show Documentation', + 'https://docs.snyk.io/scm-ide-and-ci-cd-integrations/snyk-ide-plugins-and-extensions/visual-studio-code-extension/troubleshooting-for-visual-studio-code-extension', + ); + } + + async showErrorNotificationWithLinkAction(message: string, actionText: string, actionLink: string): Promise { + await this.window + .showErrorMessage(message, actionText) + .then(async selectedAction => { + if (selectedAction == actionText) { + await this.commands.executeCommand(SNYK_OPEN_BROWSER_COMMAND, actionLink); + } + }) + .catch(err => ErrorHandler.handle(err, this.logger, 'error occurred during error handling')); } } diff --git a/src/snyk/common/services/openerService.ts b/src/snyk/common/services/openerService.ts index 14a4f00dd..5882f0b02 100644 --- a/src/snyk/common/services/openerService.ts +++ b/src/snyk/common/services/openerService.ts @@ -6,7 +6,6 @@ export interface IOpenerService { openBrowserUrl(url: string): Promise; } -// TODO: use Language Server to open browser urls export class OpenerService { async openBrowserUrl(url: string): Promise { try { diff --git a/src/snyk/extension.ts b/src/snyk/extension.ts index e54d7654c..cb455a315 100644 --- a/src/snyk/extension.ts +++ b/src/snyk/extension.ts @@ -155,7 +155,7 @@ class SnykExtension extends SnykLib implements IExtension { this.user = await User.getAnonymous(this.context, Logger); SecretStorageAdapter.init(vscodeContext); - + configuration.setExtensionId(vscodeContext.extension.id); this.configurationWatcher = new ConfigurationWatcher(Logger); this.notificationService = new NotificationService(vsCodeWindow, vsCodeCommands, configuration, Logger); @@ -474,8 +474,8 @@ class SnykExtension extends SnykLib implements IExtension { public initDependencyDownload(): DownloadService { this.downloadService.downloadOrUpdate().catch(err => { void ErrorHandler.handleGlobal(err, Logger, this.contextService, this.loadingBadge); + void this.notificationService.showErrorNotification((err as Error).message); }); - return this.downloadService; } @@ -514,7 +514,7 @@ class SnykExtension extends SnykLib implements IExtension { ), vscode.commands.registerCommand(SNYK_SHOW_ERROR_FROM_CONTEXT_COMMAND, () => { const err = this.contextService.viewContext[SNYK_CONTEXT.ERROR] as Error; - void vscode.window.showErrorMessage(err.message); + void this.notificationService.showErrorNotification(err.message); }), ); } diff --git a/src/test/unit/common/configuration.test.ts b/src/test/unit/common/configuration.test.ts index bfbe7c7b7..96f6c6871 100644 --- a/src/test/unit/common/configuration.test.ts +++ b/src/test/unit/common/configuration.test.ts @@ -5,6 +5,7 @@ import sinon from 'sinon'; import { Configuration, PreviewFeatures } from '../../../snyk/common/configuration/configuration'; import { ADVANCED_CLI_PATH, + ADVANCED_CLI_RELEASE_CHANNEL, ADVANCED_CUSTOM_ENDPOINT, ADVANCED_CUSTOM_LS_PATH, FEATURES_PREVIEW_SETTING, @@ -169,5 +170,32 @@ suite('Configuration', () => { const cliPath = await configuration.getCliPath(); strictEqual(cliPath, '/path/to/cli'); }); + + test('CLI Release Channel: Return preview if extension is preview and release channel is default', async () => { + const workspace = stubWorkspaceConfiguration(ADVANCED_CLI_RELEASE_CHANNEL, 'stable'); + + const configuration = new Configuration({}, workspace); + configuration.setExtensionId('snyk-vulnerability-scanner-preview'); + const cliReleaseChannel = await configuration.getCliReleaseChannel(); + strictEqual(cliReleaseChannel, 'preview'); + }); + + test('CLI Release Channel: Return current release channel without change if extension is not preview', async () => { + const workspace = stubWorkspaceConfiguration(ADVANCED_CLI_RELEASE_CHANNEL, 'stable'); + + const configuration = new Configuration({}, workspace); + configuration.setExtensionId('snyk-vulnerability-scanner'); + const cliReleaseChannel = await configuration.getCliReleaseChannel(); + strictEqual(cliReleaseChannel, 'stable'); + }); + + test('CLI Release Channel: Return current version if release channel not stable and extension is preview', async () => { + const workspace = stubWorkspaceConfiguration(ADVANCED_CLI_RELEASE_CHANNEL, 'v1.1294.0'); + + const configuration = new Configuration({}, workspace); + configuration.setExtensionId('snyk-vulnerability-scanner-preview'); + const cliReleaseChannel = await configuration.getCliReleaseChannel(); + strictEqual(cliReleaseChannel, 'v1.1294.0'); + }); }); }); diff --git a/src/test/unit/common/services/downloadService.test.ts b/src/test/unit/common/services/downloadService.test.ts index 4d7526ec1..d39a96306 100644 --- a/src/test/unit/common/services/downloadService.test.ts +++ b/src/test/unit/common/services/downloadService.test.ts @@ -47,7 +47,7 @@ suite('DownloadService', () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliReleaseChannel: () => 'stable', + getCliReleaseChannel: () => Promise.resolve('stable'), getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; @@ -61,7 +61,7 @@ suite('DownloadService', () => { test('Tries to download CLI if not installed', async () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliReleaseChannel: () => 'stable', + getCliReleaseChannel: () => Promise.resolve('stable'), getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; const service = new DownloadService(context, configuration, cliApi, windowMock, logger, downloader); @@ -76,7 +76,7 @@ suite('DownloadService', () => { test('Tries to update CLI if installed', async () => { configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliReleaseChannel: () => 'stable', + getCliReleaseChannel: () => Promise.resolve('stable'), getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; const service = new DownloadService(context, configuration, cliApi, windowMock, logger, downloader); @@ -93,7 +93,7 @@ suite('DownloadService', () => { test("Doesn't download CLI if automatic dependency management disabled", async () => { configuration = { isAutomaticDependencyManagementEnabled: () => false, - getCliReleaseChannel: () => 'stable', + getCliReleaseChannel: () => Promise.resolve('stable'), getCliPath: () => Promise.resolve('path/to/cli'), } as IConfiguration; const service = new DownloadService(context, configuration, cliApi, windowMock, logger, downloader); diff --git a/src/test/unit/download/downloader.test.ts b/src/test/unit/download/downloader.test.ts index 4674b31e2..358c8fc94 100644 --- a/src/test/unit/download/downloader.test.ts +++ b/src/test/unit/download/downloader.test.ts @@ -25,7 +25,7 @@ suite('CLI Downloader (CLI)', () => { logger = new LoggerMock(); configuration = { isAutomaticDependencyManagementEnabled: () => true, - getCliReleaseChannel: () => 'stable', + getCliReleaseChannel: () => Promise.resolve('stable'), getCliPath(): Promise { return Promise.resolve('abc/d'); },