Skip to content

Commit

Permalink
fix: handle nonexisting LS version (#557)
Browse files Browse the repository at this point in the history
* fix: set cli channel preview

---------

Co-authored-by: Bastian Doetsch <[email protected]>
  • Loading branch information
ShawkyZ and bastiandoetsch authored Nov 13, 2024
1 parent 3a41a25 commit b1218bd
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 44 deletions.
25 changes: 16 additions & 9 deletions src/snyk/cli/cliExecutable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import { Platform } from '../common/platform';
export class CliExecutable {
public static filenameSuffixes: Record<CliSupportedPlatform, string> = {
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) {}
Expand All @@ -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);
}

Expand All @@ -34,7 +39,7 @@ export class CliExecutable {
static async getCurrentWithArch(): Promise<CliSupportedPlatform> {
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') {
Expand All @@ -46,7 +51,7 @@ export class CliExecutable {
static async getPlatformName(osName: string): Promise<string> {
let platform = '';
if (osName === 'linux') {
if (await this.isAlpine()) {
if (await CliExecutable.isAlpine()) {
platform = 'linux_alpine';
} else {
platform = 'linux';
Expand All @@ -69,10 +74,12 @@ export class CliExecutable {
.catch(() => false);
}

static isAlpine(): Promise<boolean> {
return fs
.access('/etc/alpine-release')
.then(() => true)
.catch(() => false);
static async isAlpine(): Promise<boolean> {
try {
await fs.access('/etc/alpine-release');
return true;
} catch (e) {
return false;
}
}
}
23 changes: 15 additions & 8 deletions src/snyk/cli/staticCliApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
Expand Down Expand Up @@ -44,18 +45,24 @@ export class StaticCliApi implements IStaticCliApi {
}

async getLatestCliVersion(releaseChannel: string): Promise<string> {
let { data } = await axios.get<string>(
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<string>(
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<DownloadAxiosResponse>, 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);

Expand Down
5 changes: 3 additions & 2 deletions src/snyk/common/commands/commandController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,12 +79,13 @@ export class CommandController {
ErrorHandler.handle(e, this.logger);
}
}

async setBaseBranch(folderPath: string): Promise<void> {
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<void> {
Expand Down
35 changes: 28 additions & 7 deletions src/snyk/common/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -121,7 +123,7 @@ export interface IConfiguration {
isAutomaticDependencyManagementEnabled(): boolean;

getCliPath(): Promise<string | undefined>;
getCliReleaseChannel(): string;
getCliReleaseChannel(): Promise<string>;
getCliBaseDownloadUrl(): string;
getInsecure(): boolean;

Expand Down Expand Up @@ -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<void> {
if (!releaseChannel) return;
return this.workspace.updateConfiguration(
Expand All @@ -168,6 +180,7 @@ export class Configuration implements IConfiguration {
true,
);
}

async setCliBaseDownloadUrl(baseDownloadUrl: string): Promise<void> {
if (!baseDownloadUrl) return;
return this.workspace.updateConfiguration(
Expand All @@ -178,14 +191,22 @@ export class Configuration implements IConfiguration {
);
}

getCliReleaseChannel(): string {
return (
this.workspace.getConfiguration<string>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_CLI_RELEASE_CHANNEL),
) ?? this.defaultCliReleaseChannel
async getCliReleaseChannel(): Promise<string> {
let releaseChannel = this.workspace.getConfiguration<string>(
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<string>(
Expand Down
3 changes: 3 additions & 0 deletions src/snyk/common/constants/errors.ts
Original file line number Diff line number Diff line change
@@ -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.`,
};
18 changes: 13 additions & 5 deletions src/snyk/common/download/downloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } };

Expand All @@ -28,11 +29,17 @@ export class Downloader {
* Downloads CLI. Existing executable is deleted.
*/
async download(): Promise<CliExecutable | null> {
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<CliExecutable | null> {
Expand All @@ -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);

Expand Down
12 changes: 10 additions & 2 deletions src/snyk/common/services/downloadService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ export class DownloadService {

async update(): Promise<boolean> {
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();
Expand Down Expand Up @@ -95,7 +99,11 @@ export class DownloadService {
}

private async isCliUpdateAvailable(platform: CliSupportedPlatform): Promise<boolean> {
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,
Expand Down
22 changes: 20 additions & 2 deletions src/snyk/common/services/notificationService.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -9,7 +9,10 @@ import { IVSCodeWindow } from '../vscode/window';

export interface INotificationService {
init(): Promise<void>;

showErrorNotification(message: string): Promise<void>;

showErrorNotificationWithLinkAction(message: string, actionText: string, actionLink: string): Promise<void>;
}

export class NotificationService implements INotificationService {
Expand Down Expand Up @@ -44,6 +47,21 @@ export class NotificationService implements INotificationService {
}

async showErrorNotification(message: string): Promise<void> {
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<void> {
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'));
}
}
1 change: 0 additions & 1 deletion src/snyk/common/services/openerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export interface IOpenerService {
openBrowserUrl(url: string): Promise<void>;
}

// TODO: use Language Server to open browser urls
export class OpenerService {
async openBrowserUrl(url: string): Promise<void> {
try {
Expand Down
6 changes: 3 additions & 3 deletions src/snyk/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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);
}),
);
}
Expand Down
28 changes: 28 additions & 0 deletions src/test/unit/common/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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');
});
});
});
Loading

0 comments on commit b1218bd

Please sign in to comment.