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

feat: use cli ls extension [IDE-76] #551

Merged
merged 26 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [2.20.0]
- If $/snyk.hasAuthenticated transmits an API URL, this is saved in the settings.
- Added CLI release channel.
- Added option to change base URL to download CLI.
- Run Snyk language Server from the CLI extension.

## [2.19.1]
- Adjust OSS panel font size.
Expand Down
20 changes: 16 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,17 +303,29 @@
"scope": "machine",
"markdownDescription": "Snyk will download, install and update dependencies for you. If this option is disabled, make sure valid paths to the dependencies are provided."
},
"snyk.advanced.cliPath": {
"snyk.advanced.cliBaseDownloadUrl": {
"order": 2,
"type": "string",
"scope": "machine",
"markdownDescription": "Sets path to Snyk CLI extension dependency."
"default": "https://downloads.snyk.io",
"markdownDescription": "Base URL to download the CLI."
},
"snyk.advanced.languageServerPath": {
"snyk.advanced.cliReleaseChannel": {
"order": 3,
"type": "string",
"default": "stable",
"enum": [
"stable",
"rc",
"preview"
],
"markdownDescription": "CLI release channel."
},
"snyk.advanced.cliPath": {
"order": 4,
"type": "string",
"scope": "machine",
"markdownDescription": "Sets path to Snyk Language Server (requires restart)."
"markdownDescription": "Sets path to Snyk CLI extension dependency."
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/snyk/base/modules/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IWorkspaceTrust } from '../../common/configuration/trustedFolders';
import { IContextService } from '../../common/services/contextService';
import { DownloadService } from '../../common/services/downloadService';
import { IOpenerService } from '../../common/services/openerService';
import { IViewManagerService } from '../../common/services/viewManagerService';
import { ExtensionContext } from '../../common/vscode/extensionContext';
Expand Down Expand Up @@ -28,5 +29,7 @@ export interface ISnykLib {
export interface IExtension extends IBaseSnykModule, ISnykLib {
context: ExtensionContext | undefined;
activate(context: VSCodeExtensionContext): void;
stopLanguageServer(): Promise<void>;
restartLanguageServer(): Promise<void>;
initDependencyDownload(): DownloadService;
}
66 changes: 49 additions & 17 deletions src/snyk/cli/cliExecutable.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,69 @@
import * as fs from 'fs/promises';
import path from 'path';
import { Platform } from '../common/platform';
import { Checksum } from './checksum';
import fs from 'fs/promises';
import { CliSupportedPlatform } from './supportedPlatforms';
import { Checksum } from './checksum';
import { Platform } from '../common/platform';

// TODO: This file is to be removed in VS Code + Language Server feature cleanup. We need to ensure all users have migrated to use CLI path that's set by the language server.
export class CliExecutable {
// If values updated, `.vscodeignore` to be changed.
public static filenameSuffixes: Record<CliSupportedPlatform, string> = {
linux: 'snyk-linux',
win32: 'snyk-win.exe',
darwin: 'snyk-macos',
linux_alpine: 'snyk-alpine',

Check warning on line 10 in src/snyk/cli/cliExecutable.ts

View workflow job for this annotation

GitHub Actions / Build and Test (ubuntu-latest)

Identifier 'linux_alpine' is not in camel case

Check warning on line 10 in src/snyk/cli/cliExecutable.ts

View workflow job for this annotation

GitHub Actions / Build and Test (macos-latest)

Identifier 'linux_alpine' is not in camel case

Check warning on line 10 in src/snyk/cli/cliExecutable.ts

View workflow job for this annotation

GitHub Actions / Build and Test (windows-latest)

Identifier 'linux_alpine' is not in camel case
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
macos: 'snyk-macos',
macos_arm64: 'snyk-macos-arm64',

Check warning on line 12 in src/snyk/cli/cliExecutable.ts

View workflow job for this annotation

GitHub Actions / Build and Test (ubuntu-latest)

Identifier 'macos_arm64' is not in camel case

Check warning on line 12 in src/snyk/cli/cliExecutable.ts

View workflow job for this annotation

GitHub Actions / Build and Test (macos-latest)

Identifier 'macos_arm64' is not in camel case

Check warning on line 12 in src/snyk/cli/cliExecutable.ts

View workflow job for this annotation

GitHub Actions / Build and Test (windows-latest)

Identifier 'macos_arm64' is not in camel case
windows: 'snyk-win.exe',
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
};

constructor(public readonly version: string, public readonly checksum: Checksum) {}

static getFilename(platform: CliSupportedPlatform): string {
return this.filenameSuffixes[platform];
}

static getPath(extensionDir: string, customPath?: string): string {
static async getPath(extensionDir: string, customPath?: string): Promise<string> {
if (customPath) {
return customPath;
}

const platform = Platform.getCurrent();
const fileName = CliExecutable.getFilename(platform as CliSupportedPlatform);
const platform = await this.getCurrentWithArch();
const fileName = this.getFileName(platform);
return path.join(extensionDir, fileName);
}

static exists(extensionDir: string, customPath?: string): Promise<boolean> {
static getFileName(platform: CliSupportedPlatform): string {
return this.filenameSuffixes[platform];
}

static async getCurrentWithArch(): Promise<CliSupportedPlatform> {
let platform = '';
const osName = Platform.getCurrent().toString().toLowerCase();
const archName = Platform.getArch().toLowerCase();
if (osName === 'linux') {
if (await this.isAlpine()) {
platform = 'linux_alpine';
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
} else {
platform = 'linux';
}
} else if (osName === 'darwin') {
if (archName === 'arm64') {
platform = 'macos_arm64';
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
} else {
platform = 'macos';
}
} else if (osName.includes('win')) {
platform = 'windows';
}
if (!platform) {
throw new Error(`${osName} is unsupported.`);
}

return platform as CliSupportedPlatform;
}

static async exists(extensionDir: string, customPath?: string): Promise<boolean> {
return fs
.access(await CliExecutable.getPath(extensionDir, customPath))
.then(() => true)
.catch(() => false);
}

static isAlpine(): Promise<boolean> {
return fs
.access(CliExecutable.getPath(extensionDir, customPath))
.access('/etc/alpine-release')
.then(() => true)
.catch(() => false);
}
Expand Down
84 changes: 84 additions & 0 deletions src/snyk/cli/staticCliApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import axios, { CancelTokenSource } from 'axios';
import { IConfiguration } from '../common/configuration/configuration';
import { PROTOCOL_VERSION } from '../common/constants/languageServer';
import { DownloadAxiosResponse } from '../common/download/downloader';
import { ILog } from '../common/logger/interfaces';
import { getAxiosConfig } from '../common/proxy';
import { IVSCodeWorkspace } from '../common/vscode/workspace';
import { CliExecutable } from './cliExecutable';
import { CliSupportedPlatform } from './supportedPlatforms';

export interface IStaticCliApi {
getLatestCliVersion(releaseChannel: string): Promise<string>;
downloadBinary(platform: CliSupportedPlatform): Promise<[Promise<DownloadAxiosResponse>, CancelTokenSource]>;
getSha256Checksum(version: string, platform: CliSupportedPlatform): Promise<string>;
}

export class StaticCliApi implements IStaticCliApi {
constructor(
private readonly workspace: IVSCodeWorkspace,
private readonly configuration: IConfiguration,
private readonly logger: ILog,
) {}

getLatestVersionDownloadUrl(releaseChannel: string): string {
const downloadUrl = `${this.configuration.getCliBaseDownloadUrl()}/cli/${releaseChannel}/ls-protocol-version-${PROTOCOL_VERSION}`;
return downloadUrl;
}

getDownloadUrl(version: string, platform: CliSupportedPlatform): string {
if (!version.startsWith('v')) {
version = `v${version}`;
}
const downloadUrl = `${this.configuration.getCliBaseDownloadUrl()}/cli/${version}/${this.getFileName(platform)}`;
return downloadUrl;
}

getSha256DownloadUrl(version: string, platform: CliSupportedPlatform): string {
const downloadUrl = `${this.getDownloadUrl(version, platform)}.sha256`;
return downloadUrl;
}

getFileName(platform: CliSupportedPlatform): string {
return CliExecutable.getFileName(platform);
}

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;
}

async downloadBinary(platform: CliSupportedPlatform): Promise<[Promise<DownloadAxiosResponse>, CancelTokenSource]> {
const axiosCancelToken = axios.CancelToken.source();
const latestCliVersion = await this.getLatestCliVersion(this.configuration.getCliReleaseChannel());

const downloadUrl = this.getDownloadUrl(latestCliVersion, platform);

const response = axios.get(downloadUrl, {
responseType: 'stream',
cancelToken: axiosCancelToken.token,
...(await getAxiosConfig(this.workspace, this.configuration, this.logger)),
});

return [response as Promise<DownloadAxiosResponse>, axiosCancelToken];
}

async getSha256Checksum(version: string, platform: CliSupportedPlatform): Promise<string> {
const fileName = this.getFileName(platform);
const { data } = await axios.get<string>(
`${this.getSha256DownloadUrl(version, platform)}`,
await getAxiosConfig(this.workspace, this.configuration, this.logger),
);

const checksum = data.replace(fileName, '').replace('\n', '').trim();

if (!checksum) return Promise.reject(new Error('Checksum not found'));

return checksum;
}
}
2 changes: 1 addition & 1 deletion src/snyk/cli/supportedPlatforms.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
const SupportedCliPlatformsList = ['linux', 'win32', 'darwin'] as const;
const SupportedCliPlatformsList = ['linux', 'linux_alpine', 'windows', 'macos', 'macos_arm64'] as const;
export type CliSupportedPlatform = typeof SupportedCliPlatformsList[number];
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
76 changes: 60 additions & 16 deletions src/snyk/common/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
ADVANCED_AUTHENTICATION_METHOD,
ADVANCED_AUTOMATIC_DEPENDENCY_MANAGEMENT,
ADVANCED_AUTOSCAN_OSS_SETTING,
ADVANCED_CLI_BASE_DOWNLOAD_URL,
ADVANCED_CLI_PATH,
ADVANCED_CLI_RELEASE_CHANNEL,
ADVANCED_CUSTOM_ENDPOINT,
ADVANCED_CUSTOM_LS_PATH,
ADVANCED_ORGANIZATION,
CODE_QUALITY_ENABLED_SETTING,
CODE_SECURITY_ENABLED_SETTING,
Expand All @@ -30,6 +31,8 @@ import {
} from '../constants/settings';
import SecretStorageAdapter from '../vscode/secretStorage';
import { IVSCodeWorkspace } from '../vscode/workspace';
import { CliExecutable } from '../../cli/cliExecutable';
import { extensionContext } from '../vscode/extensionContext';

const NEWISSUES = 'Net new issues';

Expand Down Expand Up @@ -85,6 +88,9 @@ export interface IConfiguration {

setCliPath(cliPath: string): Promise<void>;

setCliReleaseChannel(releaseChannel: string): Promise<void>;
setCliBaseDownloadUrl(baseDownloadUrl: string): Promise<void>;

clearToken(): Promise<void>;

snykCodeUrl: string;
Expand Down Expand Up @@ -113,8 +119,9 @@ export interface IConfiguration {

isAutomaticDependencyManagementEnabled(): boolean;

getCliPath(): string | undefined;

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

isFedramp: boolean;
Expand All @@ -125,8 +132,6 @@ export interface IConfiguration {

scanningMode: string | undefined;

getSnykLanguageServerPath(): string | undefined;

getTrustedFolders(): string[];

setTrustedFolders(trustedFolders: string[]): Promise<void>;
Expand All @@ -144,13 +149,49 @@ export interface IConfiguration {

export class Configuration implements IConfiguration {
// These attributes are used in tests
private readonly defaultSnykCodeBaseURL = 'https://deeproxy.snyk.io';
private readonly defaultAuthHost = 'https://app.snyk.io';
private readonly defaultApiEndpoint = 'https://api.snyk.io';
private readonly defaultCliBaseDownloadUrl = 'https://downloads.snyk.io';
private readonly defaultCliReleaseChannel = 'stable';

private featureFlag: { [key: string]: boolean } = {};

constructor(private processEnv: NodeJS.ProcessEnv = process.env, private workspace: IVSCodeWorkspace) {}
async setCliReleaseChannel(releaseChannel: string): Promise<void> {
if (!releaseChannel) return;
return this.workspace.updateConfiguration(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_CLI_RELEASE_CHANNEL),
releaseChannel,
true,
);
}
async setCliBaseDownloadUrl(baseDownloadUrl: string): Promise<void> {
if (!baseDownloadUrl) return;
return this.workspace.updateConfiguration(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_CLI_BASE_DOWNLOAD_URL),
baseDownloadUrl,
true,
);
}

getCliReleaseChannel(): string {
return (
this.workspace.getConfiguration<string>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_CLI_RELEASE_CHANNEL),
) ?? this.defaultCliReleaseChannel
);
}
getCliBaseDownloadUrl(): string {
return (
this.workspace.getConfiguration<string>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_CLI_BASE_DOWNLOAD_URL),
) ?? this.defaultCliBaseDownloadUrl
);
}

getOssQuickFixCodeActionsEnabled(): boolean {
return this.getPreviewFeatures().ossQuickfixes ?? false;
Expand Down Expand Up @@ -241,13 +282,6 @@ export class Configuration implements IConfiguration {
return `${authUrl.toString()}manage/snyk-code?from=vscode`;
}

getSnykLanguageServerPath(): string | undefined {
return this.workspace.getConfiguration<string>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_CUSTOM_LS_PATH),
);
}

getDeltaFindingsEnabled(): boolean {
const selectionValue = this.workspace.getConfiguration<string>(
CONFIGURATION_IDENTIFIER,
Expand Down Expand Up @@ -287,7 +321,9 @@ export class Configuration implements IConfiguration {
}

async setCliPath(cliPath: string | undefined): Promise<void> {
if (!cliPath) return;
if (!cliPath && extensionContext) {
cliPath = await CliExecutable.getPath(extensionContext.extensionPath);
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
}
return this.workspace.updateConfiguration(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_CLI_PATH),
Expand Down Expand Up @@ -489,8 +525,16 @@ export class Configuration implements IConfiguration {
);
}

getCliPath(): string | undefined {
return this.workspace.getConfiguration<string>(CONFIGURATION_IDENTIFIER, this.getConfigName(ADVANCED_CLI_PATH));
async getCliPath(): Promise<string | undefined> {
let cliPath = this.workspace.getConfiguration<string>(
CONFIGURATION_IDENTIFIER,
this.getConfigName(ADVANCED_CLI_PATH),
);
if (!cliPath && extensionContext) {
cliPath = await CliExecutable.getPath(extensionContext.extensionPath);
ShawkyZ marked this conversation as resolved.
Show resolved Hide resolved
await this.setCliPath(cliPath);
}
return cliPath;
}

getTrustedFolders(): string[] {
Expand Down
5 changes: 2 additions & 3 deletions src/snyk/common/constants/globalState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export const MEMENTO_ANONYMOUS_ID = 'snyk.anonymousId';
export const MEMENTO_LS_LAST_UPDATE_DATE = 'snyk.lsLastUpdateDate';
export const MEMENTO_LS_PROTOCOL_VERSION = 'snyk.lsProtocolVersion';
export const MEMENTO_LS_CHECKSUM = 'snyk.lsChecksum';
export const MEMENTO_CLI_VERSION = 'snyk.cliVersion';
export const MEMENTO_CLI_CHECKSUM = 'snyk.cliChecksum';
3 changes: 2 additions & 1 deletion src/snyk/common/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export const ADVANCED_CUSTOM_ENDPOINT = `${CONFIGURATION_IDENTIFIER}.advanced.cu
export const ADVANCED_ORGANIZATION = `${CONFIGURATION_IDENTIFIER}.advanced.organization`;
export const ADVANCED_AUTOMATIC_DEPENDENCY_MANAGEMENT = `${CONFIGURATION_IDENTIFIER}.advanced.automaticDependencyManagement`;
export const ADVANCED_CLI_PATH = `${CONFIGURATION_IDENTIFIER}.advanced.cliPath`;
export const ADVANCED_CUSTOM_LS_PATH = `${CONFIGURATION_IDENTIFIER}.advanced.languageServerPath`;
export const ADVANCED_CLI_BASE_DOWNLOAD_URL = `${CONFIGURATION_IDENTIFIER}.advanced.cliBaseDownloadUrl`;
export const ADVANCED_CLI_RELEASE_CHANNEL = `${CONFIGURATION_IDENTIFIER}.advanced.cliReleaseChannel`;
export const ADVANCED_AUTHENTICATION_METHOD = `${CONFIGURATION_IDENTIFIER}.advanced.authenticationMethod`;

export const ISSUE_VIEW_OPTIONS_SETTING = `${CONFIGURATION_IDENTIFIER}.issueViewOptions`;
Expand Down
Loading
Loading