Skip to content

Commit

Permalink
Use vscode-file-downloader
Browse files Browse the repository at this point in the history
  • Loading branch information
ross-p-smith committed Oct 17, 2023
1 parent 42823f3 commit 5b7afad
Show file tree
Hide file tree
Showing 13 changed files with 5,919 additions and 3,780 deletions.
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"dbaeumer.vscode-eslint",
"amodio.tsl-problem-matcher",
"ms-vscode.azure-account",
"humao.rest-client"
"humao.rest-client",
"mindaro-dev.file-downloader"
]
}
}
Expand Down
9,225 changes: 5,467 additions & 3,758 deletions package-lock.json

Large diffs are not rendered by default.

41 changes: 38 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
"onCommand:azureApiManagement.setupWorkingFolder",
"onCommand:azureApiManagement.extractService",
"onCommand:azureApiManagement.extractApi",
"onCommand:azureApiManagement.apiops.exportService",
"onCommand:azureApiManagement.apiops.exportApi",
"onCommand:azureApiManagement.apiops.importService",
"onCommand:azureApiManagement.importFunctionApp",
"onCommand:azureApiManagement.importFunctionAppToApi",
"onCommand:azureApiManagement.importWebApp",
Expand Down Expand Up @@ -322,6 +325,21 @@
"title": "%azureApiManagement.extractApi%",
"category": "Azure API Management"
},
{
"command": "azureApiManagement.apiops.exportService",
"title": "%azureApiManagement.apiops.exportService%",
"category": "Azure API Management"
},
{
"command": "azureApiManagement.apiops.exportApi",
"title": "%azureApiManagement.apiops.exportApi%",
"category": "Azure API Management"
},
{
"command": "azureApiManagement.apiops.importService",
"title": "%azureApiManagement.apiops.importService%",
"category": "Azure API Management"
},
{
"command": "azureApiManagement.importFunctionApp",
"title": "%azureApiManagement.importFunctionApp%",
Expand Down Expand Up @@ -544,15 +562,25 @@
"group": "2@1"
},
{
"command": "azureApiManagement.Refresh",
"command": "azureApiManagement.apiops.exportService",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementService",
"group": "3@1"
},
{
"command": "azureApiManagement.deleteService",
"command": "azureApiManagement.apiops.importService",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementService",
"group": "3@1"
},
{
"command": "azureApiManagement.Refresh",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementService",
"group": "4@1"
},
{
"command": "azureApiManagement.deleteService",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementService",
"group": "5@1"
},
{
"command": "azureApiManagement.importOpenApiByFile",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApis",
Expand Down Expand Up @@ -613,6 +641,11 @@
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApi",
"group": "4@1"
},
{
"command": "azureApiManagement.apiops.exportApi",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApi",
"group": "4@2"
},
{
"command": "azureApiManagement.revisions",
"when": "view == azureApiManagementExplorer && viewItem == azureApiManagementApi",
Expand Down Expand Up @@ -850,6 +883,7 @@
"@azure/arm-apimanagement": "^6.0.0",
"@azure/arm-appservice": "^7.0.0",
"@azure/arm-resources": "^4.0.0",
"@microsoft/vscode-file-downloader-api": "^1.0.1",
"@types/xml": "^1.0.4",
"adal-node": "^0.2.2",
"await-notify": "1.0.1",
Expand All @@ -872,7 +906,8 @@
},
"extensionDependencies": [
"ms-vscode.azure-account",
"humao.rest-client"
"humao.rest-client",
"mindaro-dev.file-downloader"
],
"overrides": {
"fsevents": "~2.3.2"
Expand Down
3 changes: 3 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"azureApiManagement.advancedPolicyAuthoringExperience": "(Experimental Feature) Enables advanced policy authoring experience.",
"azureApiManagement.extractService": "Extract Service",
"azureApiManagement.extractApi": "Extract API",
"azureApiManagement.apiops.exportService": "Export Service (APIOps)",
"azureApiManagement.apiops.exportApi": "Export API (APIOps)",
"azureApiManagement.apiops.importService": "Import Service (APIOps)",
"azureApiManagement.importFunctionApp": "Import from Azure Functions",
"azureApiManagement.importFunctionAppToApi": "Import Azure Functions",
"azureApiManagement.addApiFilter": "Filter APIs",
Expand Down
120 changes: 120 additions & 0 deletions src/commands/exportService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fse from 'fs-extra';
import * as yaml from 'js-yaml';
import * as path from 'path';
import { ProgressLocation, Uri, window } from "vscode";
import { IActionContext } from 'vscode-azureextensionui';
import * as Constants from '../constants';
import { ApiTreeItem } from '../explorer/ApiTreeItem';
import { ServiceTreeItem } from '../explorer/ServiceTreeItem';
import { ext } from '../extensionVariables';
import { localize } from '../localize';
import { azUtils } from '../utils/azUtils';
import { cpUtils } from '../utils/cpUtils';
import { askFolder } from '../utils/vscodeUtils';
import ApiOpsTooling from '../utils/ApiOpsTooling';
import ExtensionHelper from '../utils/extensionUtil';

export async function exportService(context: IActionContext, node?: ServiceTreeItem): Promise<void> {
if (!node) {
node = <ServiceTreeItem>await ext.tree.showTreeItemPicker(ServiceTreeItem.contextValue, context);
}

await extract(node);
}

export async function exportAPI(context: IActionContext, node?: ApiTreeItem): Promise<void> {
if (!node) {
node = <ApiTreeItem>await ext.tree.showTreeItemPicker(ApiTreeItem.contextValue, context);
}

const apiName = node.apiContract.name === undefined ? "" : node.apiContract.name;

await extract(node, apiName);
}

async function extract(node: ApiTreeItem | ServiceTreeItem, apiName?: string): Promise<void> {

const uri = await askFolder("Export");
const sourceApimName = node.root.serviceName;
const resourceGroup = node.root.resourceGroupName;
const subscriptionId = node.root.subscriptionId;
const templatesFolder = await createTemplatesFolder(uri, sourceApimName);
const noticeContent = await generateExtractConfig(templatesFolder, sourceApimName, apiName);

window.withProgress(
{
location: ProgressLocation.Notification,
title: localize("Extracting", noticeContent),
cancellable: false
},
async () => {
await azUtils.checkAzInstalled();
await runExtractor(templatesFolder, sourceApimName, resourceGroup, subscriptionId);
}
).then(
() => {
window.showInformationMessage(localize("Exported", `Export completed!`));
});
}

async function createTemplatesFolder(uri: Uri, sourceApimName: string): Promise<string> {
const templatesFolder = path.join(uri.fsPath, sourceApimName);
if (!fse.existsSync(templatesFolder)) {
await fse.mkdir(templatesFolder);
} else {
await fse.emptyDir(templatesFolder);
}
return templatesFolder;
}

async function runExtractor(filePath: string, apimName: string, resourceGroupName: string, subscriptionId: string): Promise<void> {
ext.outputChannel.show();

// Check our APIOps tooling has been downloaded
const downloader = new ApiOpsTooling(ext.context, new ExtensionHelper());
await downloader.downloadExternalBinary(Constants.extractorBinaryName);

await azUtils.setSubscription(subscriptionId, ext.outputChannel);

const extractionConfigurationFilePath = path.join(filePath, "configuration.extractor.yaml");

// It's not on the PATH so you need './'
await cpUtils.executeCommand(
ext.outputChannel,
await downloader.getDownloadStoragePath(),
`./${Constants.extractorBinaryName}`,
`AZURE_SUBSCRIPTION_ID=${subscriptionId}`,
`API_MANAGEMENT_SERVICE_OUTPUT_FOLDER_PATH=${filePath}`,
`API_MANAGEMENT_SERVICE_NAME=${apimName}`,
`AZURE_RESOURCE_GROUP_NAME=${resourceGroupName}`,
`CONFIGURATION_YAML_PATH=${extractionConfigurationFilePath}`
);
}

// The extractor has the ability to extract a single API or all APIs in a service. We will generate an
// empty file in the case of all APIs and a file with the name of the API in the case of a single API.
// This file will be set using CONFIGURATION_YAML_PATH
async function generateExtractConfig(templatesFolder: string, sourceApimName: string, apiName?: string): Promise<string> {
const extractionConfigurationFilePath = path.join(templatesFolder, "configuration.extractor.yaml");
let extractionConfiguration = {};
let noticeContent = "";

if (apiName) {
extractionConfiguration = {
apiNames: [apiName]
};
noticeContent = localize("Export", `Exporting API '${apiName}' to '${templatesFolder}'`);
} else {
noticeContent = localize("Export", `Exporting service '${sourceApimName}' to '${templatesFolder}'`);
}

const yamlStr = yaml.dump(extractionConfiguration);
await fse.writeFile(extractionConfigurationFilePath, yamlStr);

return noticeContent;
}
5 changes: 3 additions & 2 deletions src/commands/setupWorkingFolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { ext } from '../extensionVariables';
import { localize } from '../localize';
import { cpUtils } from '../utils/cpUtils';
import { dotnetUtils } from '../utils/dotnetUtils';
import { checkCsharpExtensionInstalled } from '../utils/extensionUtil';
import { getDefaultWorkspacePath } from '../utils/fsUtil';
import ExtensionHelper from '../utils/extensionUtil';

export async function setupWorkingFolder(this: IActionContext): Promise<void> {
ext.outputChannel.appendLine(localize("folderInitialized", "Initialization started..."));
Expand All @@ -20,7 +20,8 @@ export async function setupWorkingFolder(this: IActionContext): Promise<void> {
await dotnetUtils.validateDotnetInstalled(this);

// check vscode csharp extension is installed.
checkCsharpExtensionInstalled(this);
const extensionHelper = new ExtensionHelper();
extensionHelper.checkCsharpExtensionInstalled(this);

const workingFolderPath = getDefaultWorkspacePath();

Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,7 @@ export const emptyPolicyXml =
</policies>`;

export const sessionFolderKey = "currentSessionWorkingFolder";

export const apiOpsToolingLocation = "https://api.github.com/repos/azure/apiops/releases/latest";
export const extractorBinaryName = "extractor.linux-x64.exe";
export const publisherBinaryName = "publisher.linux-x64.exe";
12 changes: 11 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { createService } from './commands/createService';
import { debugPolicy } from './commands/debugPolicies/debugPolicy';
import { deleteNode } from './commands/deleteNode';
import { copyDockerRunCommand, generateKubernetesDeployment } from './commands/deployGateway';
import { exportAPI, exportService } from './commands/exportService';
import { extractAPI, extractService } from './commands/extract';
import { generateFunctions } from './commands/generateFunctions';
import { generateNewGatewayToken } from './commands/generateNewGatewayToken';
Expand Down Expand Up @@ -78,6 +79,8 @@ import { ServiceTreeItem } from './explorer/ServiceTreeItem';
import { SubscriptionTreeItem } from './explorer/SubscriptionTreeItem';
import { ext } from './extensionVariables';
import { localize } from './localize';
import ApiOpsTooling from './utils/ApiOpsTooling';
import ExtensionHelper from './utils/extensionUtil';

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
Expand Down Expand Up @@ -109,7 +112,11 @@ export async function activateInternal(context: vscode.ExtensionContext) {
vscode.window.registerUriHandler(handler)
);

activate(context); // activeta debug context
activate(context); // activate debug context

// Download Tooling in the background when the extension is activated.
// const apiOpsTooling = new ApiOpsTooling(ext.context, new ExtensionHelper());
// await apiOpsTooling.downloadExternalBinaries();
});
}

Expand All @@ -133,6 +140,8 @@ function registerCommands(tree: AzExtTreeDataProvider): void {
registerCommand('azureApiManagement.addApiToProduct', async (context: IActionContext, node?: ProductApisTreeItem) => { await addApiToProduct(context, node); });
registerCommand('azureApiManagement.removeApiFromGateway', async (context: IActionContext, node?: AzureTreeItem) => await deleteNode(context, GatewayApiTreeItem.contextValue, node));
registerCommand('azureApiManagement.addApiToGateway', async (context: IActionContext, node?: GatewayApisTreeItem) => { await addApiToGateway(context, node); });
registerCommand('azureApiManagement.apiops.exportService', async (context: IActionContext, node: ServiceTreeItem) => await exportService(context, node));
registerCommand('azureApiManagement.apiops.exportApi', async (context: IActionContext, node: ApiTreeItem) => await exportAPI(context, node));
registerCommand('azureApiManagement.extractService', async (context: IActionContext, node: ServiceTreeItem) => await extractService(context, node));
registerCommand('azureApiManagement.extractApi', async (context: IActionContext, node: ApiTreeItem) => await extractAPI(context, node));
registerCommand('azureApiManagement.importFunctionApp', async (context: IActionContext, node: ApisTreeItem) => await importFunctionApp(context, node));
Expand Down Expand Up @@ -324,6 +333,7 @@ function registerEditors(context: vscode.ExtensionContext) : void {
}, doubleClickDebounceDelay);
}


// this method is called when your extension is deactivated
// tslint:disable:typedef
// tslint:disable-next-line:no-empty
Expand Down
79 changes: 79 additions & 0 deletions src/utils/ApiOpsTooling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as path from 'path';
import * as vscode from 'vscode';
import fetch from 'node-fetch';
import * as projectConstants from '../constants';
import { localize } from '../localize';
import ExtensionHelper from './extensionUtil';
import FileDownloader, { FileDownloadSettings } from './IFileDownloader';

export default class ApiOpsTooling {
constructor(private readonly context: vscode.ExtensionContext, private readonly extensionHelper: ExtensionHelper) {}

// These binaries are used for API Ops, we download them to the global storage path in the
// background as they are around 70Mb each.
public async downloadExternalBinaries(): Promise<void>{

await this.downloadExternalBinary(projectConstants.extractorBinaryName);
await this.downloadExternalBinary(projectConstants.publisherBinaryName);
}

// Get the URI of the binary, if it doesn't exist, download it.
public async downloadExternalBinary(fileName: string): Promise<void> {
await this.downloadGitHubBinaryIfNotExists(
await this.getDownloadLink(fileName),
fileName);
}

public async getDownloadStoragePath(): Promise<string> {
return path.join(this.context.globalStorageUri.fsPath, `file-downloader-downloads`);
}

// Gets the download link for the latest release of the API Ops tooling.
public async getDownloadLink(fileName: string): Promise<string> {
const response = await fetch(projectConstants.apiOpsToolingLocation);
const data = await response.json();

for (const asset of data.assets) {
if (asset.name === fileName) {
return asset.url;
}
}

vscode.window.showInformationMessage(localize("APIOps", "'{0}' not found in latest release.", fileName));
return "";
}

public async downloadGitHubBinaryIfNotExists(url: string, fileName: string): Promise<void> {

// Maybe GitHub is is down, don't stop the extension from working.
if (url === "") {
return;
}

const fileDownloader: FileDownloader = await this.extensionHelper.getFileDownloaderApi();

const exists = await fileDownloader.tryGetItem(fileName, this.context);
if (!exists) {
try {
const settings: FileDownloadSettings = {
makeExecutable: true,
// eslint-disable-next-line @typescript-eslint/naming-convention
headers: {"Accept": `application/octet-stream`, "Content-Type": `application/octet-stream`}
};
const uri: vscode.Uri = await fileDownloader.downloadFile(
vscode.Uri.parse(url),
fileName,
this.context,
/* cancellationToken */ undefined,
/* progressCallback */ undefined,
settings);

if (uri.path) {
vscode.window.showInformationMessage(localize("APIOps", "'{0}' downloaded", fileName));
}
} catch (error) {
vscode.window.showErrorMessage(localize("APIOps", `Error downloading {0}: ${String(error)}}`, url));
}
}
}
}
Loading

0 comments on commit 5b7afad

Please sign in to comment.