diff --git a/package-lock.json b/package-lock.json index 9c20c27..301f926 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@types/cytoscape": "^3.19.9", "@vscode/l10n": "^0.0.13", + "compare-versions": "^6.1.0", "cross-fetch": "^3.1.5", "outscale-api": "^0.11.0", "rxjs": "^7.5.7", @@ -1396,10 +1397,9 @@ "dev": true }, "node_modules/compare-versions": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", - "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", - "dev": true + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz", + "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==" }, "node_modules/concat-map": { "version": "0.0.1", @@ -2285,54 +2285,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "node_modules/function-bind": { "version": "1.1.2", @@ -3407,6 +3361,12 @@ "selenium-webdriver": "^4.6.1" } }, + "node_modules/monaco-page-objects/node_modules/compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", + "dev": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5102,6 +5062,12 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/vscode-extension-tester/node_modules/compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", + "dev": true + }, "node_modules/web-tree-sitter": { "version": "0.20.7", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.7.tgz", @@ -6329,10 +6295,9 @@ "dev": true }, "compare-versions": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", - "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", - "dev": true + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz", + "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==" }, "concat-map": { "version": "0.0.1", @@ -6999,44 +6964,8 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true }, "function-bind": { "version": "1.1.2", @@ -7842,6 +7771,14 @@ "compare-versions": "^5.0.1", "fs-extra": "^10.1.0", "ts-essentials": "^9.3.0" + }, + "dependencies": { + "compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", + "dev": true + } } }, "ms": { @@ -9124,6 +9061,12 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true + }, + "compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", + "dev": true } } }, @@ -9291,4 +9234,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 5c9dccb..cd15fc0 100644 --- a/package.json +++ b/package.json @@ -413,6 +413,7 @@ "dependencies": { "@types/cytoscape": "^3.19.9", "@vscode/l10n": "^0.0.13", + "compare-versions": "^6.1.0", "cross-fetch": "^3.1.5", "outscale-api": "^0.11.0", "rxjs": "^7.5.7", diff --git a/src/components/osc_cost.ts b/src/components/osc_cost.ts index 9ce4856..20a5949 100644 --- a/src/components/osc_cost.ts +++ b/src/components/osc_cost.ts @@ -1,43 +1,26 @@ import * as vscode from 'vscode'; import { getConfigurationParameter, OSC_COST_PARAMETER } from "../configuration/utils"; import { pathExists } from "../config_file/utils"; -import { ACCESSKEY_FOLDER_NAME } from "../flat/folders/simple/node.folder.accesskey"; -import { APIACCESSRULES_FOLDER_NAME } from "../flat/folders/simple/node.folder.apiaccessrule"; -import { CA_FOLDER_NAME } from "../flat/folders/simple/node.folder.ca"; -import { CLIENTGATEWAYS_FOLDER_NAME } from "../flat/folders/simple/node.folder.clientgateway"; -import { DHCPOPTIONS_FOLDER_NAME } from "../flat/folders/simple/node.folder.dhcpoption"; -import { DIRECTLINKS_FOLDER_NAME } from "../flat/folders/simple/node.folder.directlink"; -import { DIRECTLINKINTERFACES_FOLDER_NAME } from "../flat/folders/simple/node.folder.directlinkinterface"; -import { IMAGES_FOLDER_NAME } from "../flat/folders/simple/node.folder.image"; -import { KEYPAIRS_FOLDER_NAME } from "../flat/folders/simple/node.folder.keypair"; import { LOADBALANCER_FOLDER_NAME } from "../flat/folders/simple/node.folder.loadbalancer"; import { NATSERVICES_FOLDER_NAME } from "../flat/folders/simple/node.folder.natservice"; -import { NETACCESSPOINTS_FOLDER_NAME } from "../flat/folders/simple/node.folder.netaccesspoint"; -import { NETPEERINGS_FOLDER_NAME } from "../flat/folders/simple/node.folder.netpeering"; import { SNAPSHOTS_FOLDER_NAME } from "../flat/folders/simple/node.folder.snapshot"; -import { SUBNETS_FOLDER_NAME } from "../flat/folders/simple/node.folder.subnet"; import { VPNCONNECTIONS_FOLDER_NAME } from "../flat/folders/simple/node.folder.vpnconnection"; import { FLEXIBLEGPUS_FOLDER_NAME } from "../flat/folders/specific/node.folder.flexiblegpu"; -import { INTERNETSERVICES_FOLDER_NAME } from "../flat/folders/specific/node.folder.internetservice"; -import { NET_FOLDER_NAME } from "../flat/folders/specific/node.folder.net"; -import { NICS_FOLDER_NAME } from "../flat/folders/specific/node.folder.nic"; import { PUBLICIP_FOLDER_NAME } from "../flat/folders/specific/node.folder.publicip"; -import { ROUTETABLES_FOLDER_NAME } from "../flat/folders/specific/node.folder.routetable"; -import { SECURITYGROUPS_FOLDER_NAME } from "../flat/folders/specific/node.folder.securitygroup"; -import { VIRTUALGATEWAYS_FOLDER_NAME } from "../flat/folders/specific/node.folder.virtualgateway"; import { VM_FOLDER_NAME } from "../flat/folders/specific/node.folder.vm"; import { VOLUME_FOLDER_NAME } from "../flat/folders/specific/node.folder.volume"; import { Profile, ResourceNodeType } from "../flat/node"; import { OutputChannel } from "../logs/output_channel"; import { shell } from "./shell"; +import { satisfies } from 'compare-versions'; export type ResourceCost = number; export type ResourcesTypeCost = { globalPrice: number, values: Map }; -const DEFAULT_OPTIONS_OSC_COST = new Map([ - ['v0.1.0', '--format json'], - ['v0.2.0', '--format json --skip-resource Oos'], - ]); +const DEFAULT_OPTIONS_OSC_COST: [string, string][] = [ + ['=v0.2.0', '--format json --skip-resource Oos'], // Skipe OOS to reduce the latency +]; export class AccountCost { accountCost: number; @@ -100,7 +83,7 @@ function formatPrice(price: number, currency: string): string { return "~" + price.toFixed(2) + currency; } -export function getCurrency(region: string): string { +function getCurrency(region: string): string { switch (region) { case "eu-west-2": case "cloudgouv-eu-west-1": @@ -130,30 +113,10 @@ function folderNameToOscCostResourceType(folderName: string): string | undefined case FLEXIBLEGPUS_FOLDER_NAME: return "FlexibleGpu"; case VPNCONNECTIONS_FOLDER_NAME: - return "FlexibleGpu"; + return "Vpn"; case NATSERVICES_FOLDER_NAME: return "NatServices"; - case ACCESSKEY_FOLDER_NAME: - case CLIENTGATEWAYS_FOLDER_NAME: - case IMAGES_FOLDER_NAME: - case INTERNETSERVICES_FOLDER_NAME: - case KEYPAIRS_FOLDER_NAME: - case NET_FOLDER_NAME: - case NICS_FOLDER_NAME: - case ROUTETABLES_FOLDER_NAME: - case SECURITYGROUPS_FOLDER_NAME: - case SUBNETS_FOLDER_NAME: - case VIRTUALGATEWAYS_FOLDER_NAME: - case DHCPOPTIONS_FOLDER_NAME: - case DIRECTLINKS_FOLDER_NAME: - case DIRECTLINKINTERFACES_FOLDER_NAME: - case NETPEERINGS_FOLDER_NAME: - case APIACCESSRULES_FOLDER_NAME: - case NETACCESSPOINTS_FOLDER_NAME: - case CA_FOLDER_NAME: - return undefined; default: - OutputChannel.getInstance().appendLine(`The folder '${folderName}' is not handle for osc-cost conversion. Report it to the developpers`); return undefined; } } @@ -173,30 +136,10 @@ function resourceNodeTypeToOscCostResourceType(resourceNodeType: ResourceNodeTyp case 'FlexibleGpu': return "FlexibleGpu"; case 'VpnConnection': - return "FlexibleGpu"; + return "Vpn"; case 'NatService': return "NatServices"; - case 'AccessKey': - case 'ClientGateway': - case 'omis': - case 'InternetService': - case 'keypairs': - case 'vpc': - case 'Nic': - case 'routetables': - case 'securitygroups': - case 'Subnet': - case 'VirtualGateway': - case 'DhcpOption': - case 'DirectLink': - case 'DirectLinkInterface': - case 'NetPeering': - case 'ApiAccessRule': - case 'NetAccessPoint': - case 'Ca': - return undefined; default: - OutputChannel.getInstance().appendLine(`The resourceNodeType '${resourceNodeType}' is not handle for osc-cost conversion. Report it to the developpers`); return undefined; } } @@ -205,7 +148,6 @@ function resourceNodeTypeToOscCostResourceType(resourceNodeType: ResourceNodeTyp function jsonToAccountCost(oscCostOutput: string): AccountCost | undefined { const accountCost = new AccountCost(); for (const jsonString of oscCostOutput.split('\n')) { - OutputChannel.getInstance().appendLine(`The jsonString is ${jsonString}`); let json; try { json = JSON.parse(jsonString); @@ -276,12 +218,26 @@ export async function fetchAccountCost(profile: Profile): Promise { + const res = await shell.exec(`${oscCostPath} --version`); + if (typeof res === "undefined") { + return res; + } + // version is like "osc-cost X.Y.Z" + return res.split(" ")[1].trim(); + +} + +function getDefaultOptions(version: string): string | undefined { + const options = DEFAULT_OPTIONS_OSC_COST.filter((val) => { + return satisfies(version, val[0]); + }); + + if (options.length > 1) { + OutputChannel.getInstance().appendLine("Got multiple default options possible, rejecting all of them"); + return undefined; + } + + if (options.length === 0) { + OutputChannel.getInstance().appendLine("Got none default option"); + return undefined; + } + + return options[0][1]; +} diff --git a/src/components/shell.ts b/src/components/shell.ts index 99559c6..1b6027f 100644 --- a/src/components/shell.ts +++ b/src/components/shell.ts @@ -1,131 +1,28 @@ 'use strict'; -import * as vscode from 'vscode'; import * as shelljs from 'shelljs'; -import { ChildProcess } from 'child_process'; -export enum Platform { - // eslint-disable-next-line @typescript-eslint/naming-convention - Windows, - // eslint-disable-next-line @typescript-eslint/naming-convention - MacOS, - // eslint-disable-next-line @typescript-eslint/naming-convention - Linux, - // eslint-disable-next-line @typescript-eslint/naming-convention - Unsupported, // shouldn't happen! -} +import util = require('util'); +import _exec = require('child_process'); +const innerExec = util.promisify(_exec.exec); + export interface Shell { - isWindows(): boolean; - isUnix(): boolean; - platform(): Platform; - execOpts(): any; - exec(cmd: string, stdin?: string): Promise; + exec(cmd: string): Promise; which(bin: string): string | null; } export const shell: Shell = { - isWindows: isWindows, - isUnix: isUnix, - platform: platform, - execOpts: execOpts, exec: exec, which: which, }; -const WINDOWS = 'win32'; - -export interface ShellResult { - readonly code: number; - readonly stdout: string; - readonly stderr: string; -} - -export type ShellHandler = (code: number, stdout: string, stderr: string) => void; - -function isWindows(): boolean { - return (process.platform === WINDOWS); -} - -function isUnix(): boolean { - return !isWindows(); -} - -function platform(): Platform { - switch (process.platform) { - case 'win32': return Platform.Windows; - case 'darwin': return Platform.MacOS; - case 'linux': return Platform.Linux; - default: return Platform.Unsupported; - } +async function exec(cmd: string): Promise { + const { stdout } = await innerExec(cmd); + return stdout; } -function concatIfSafe(homeDrive: string | undefined, homePath: string | undefined): string | undefined { - if (homeDrive && homePath) { - const safe = !homePath.toLowerCase().startsWith('\\windows\\system32'); - if (safe) { - return homeDrive.concat(homePath); - } - } - - return undefined; -} - -function home(): string { - return process.env['HOME'] || - concatIfSafe(process.env['HOMEDRIVE'], process.env['HOMEPATH']) || - process.env['USERPROFILE'] || - ''; -} - -function execOpts(): any { - let env = process.env; - if (isWindows()) { - // eslint-disable-next-line @typescript-eslint/naming-convention - env = Object.assign({}, env, { HOME: home() }); - } - env = shellEnvironment(env); - - const opts = { - cwd: typeof vscode.workspace.workspaceFolders === 'undefined' ? undefined : vscode.workspace.workspaceFolders[0], - env: env, - async: true - }; - return opts; -} - -async function exec(cmd: string, stdin?: string): Promise { - try { - return await execCore(cmd, execOpts(), null, stdin); - } catch (ex) { - vscode.window.showErrorMessage(`${ex}`); - return undefined; - } -} - -function execCore(cmd: string, opts: any, callback?: ((proc: ChildProcess) => void) | null, stdin?: string): Promise { - return new Promise((resolve) => { - const proc = shelljs.exec(cmd, opts, (code, stdout, stderr) => resolve({ code: code, stdout: stdout, stderr: stderr })); - if (stdin && proc.stdin !== null) { - proc.stdin.end(stdin); - } - if (callback) { - callback(proc); - } - }); -} function which(bin: string): string | null { return shelljs.which(bin); -} - -export function shellEnvironment(baseEnvironment: any): any { - const env = Object.assign({}, baseEnvironment); - return env; -} - -const SAFE_CHARS_REGEX = /^[-,._+:@%/\w]*$/; - -export function isSafe(s: string): boolean { - return SAFE_CHARS_REGEX.test(s); -} +} \ No newline at end of file diff --git a/src/explorer.ts b/src/explorer.ts index 749e4c4..6b304a8 100644 --- a/src/explorer.ts +++ b/src/explorer.ts @@ -11,13 +11,12 @@ export class OscExplorer implements vscode.TreeDataProvider { private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; - refresh(): void { this._onDidChangeTreeData.fire(); } getTreeItem(element: ExplorerNode): Thenable { - return element.getTreeItem(); + return Promise.resolve(element.getTreeItem()); } async getChildren(element?: ExplorerNode): Promise { @@ -26,11 +25,25 @@ export class OscExplorer implements vscode.TreeDataProvider { } else { const toExplorerNode = async (profileName: string, definition: any): Promise => { const profile = jsonToProfile(profileName, definition); + const profileObj = new ProfileNode(profile); + if (isOscCostEnabled()) { - profile.oscCost = await this.retrieveAccountCost(profile); - OutputChannel.getInstance().appendLine(`Retrieve the cost for ${profile.name}: ${profile.oscCost?.accountCost}`); + // Do not wait for completion but only fire a refresh on the node only if the data is available + this.retrieveAccountCost(profile).then( + (res: AccountCost | undefined) => { + if (typeof res === 'undefined') { + vscode.window.showErrorMessage(vscode.l10n.t(`Retrieve the cost for ${profile.name} fails: undefined`)); + return; + } + profileObj.profile.oscCost = res; + this._onDidChangeTreeData.fire(profileObj); + OutputChannel.getInstance().appendLine(`Retrieve the cost for ${profile.name}: ${profile.oscCost?.accountCost}`); + }, + (reason: any) => { + vscode.window.showErrorMessage(vscode.l10n.t(`Retrieval the cost for ${profile.name} fails: ${reason}`)); + }); } - return Promise.resolve(new ProfileNode(profile)); + return Promise.resolve(profileObj); }; const oscConfigObject = readConfigFile(); @@ -39,7 +52,6 @@ export class OscExplorer implements vscode.TreeDataProvider { return Promise.resolve([]); } const explorerNodes = await Promise.all(Object.keys(oscConfigObject).map(async (dep) => await toExplorerNode(dep, oscConfigObject[dep]))); - return Promise.resolve(explorerNodes); }