Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle nonexisting LS version #557

Merged
merged 21 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()}`);
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading