From 5eb4973ced93e2718286cf5a196129dc3fd2a3b5 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 15 May 2018 09:03:26 -0700 Subject: [PATCH] Fix getting templates when offline and clean up getting templates (#373) --- src/constants.ts | 2 + src/extension.ts | 11 +- src/templates/TemplateData.ts | 285 +++++++++++----------- src/utils/getCliFeedJson.ts | 19 +- test/createFunction/FunctionTesterBase.ts | 16 +- test/templateData.test.ts | 16 +- 6 files changed, 169 insertions(+), 180 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 01d0d70ac..e9b16b233 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,6 +11,8 @@ export const projectRuntimeSetting: string = 'projectRuntime'; export const templateFilterSetting: string = 'templateFilter'; export const deploySubpathSetting: string = 'deploySubpath'; export const templateVersionSetting: string = 'templateVersion'; +export const v1ReleaseVersion: string = '1.0.12'; // known stable version +export const betaReleaseVersion: string = '2.0.1-beta.25'; // known stable version export enum ProjectLanguage { Bash = 'Bash', diff --git a/src/extension.ts b/src/extension.ts index ff6a2c86a..ce421d1be 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -36,14 +36,13 @@ import { restartFunctionApp } from './commands/restartFunctionApp'; import { startFunctionApp } from './commands/startFunctionApp'; import { stopFunctionApp } from './commands/stopFunctionApp'; import { ext } from './extensionVariables'; -import { getTemplateDataFromBackup, tryGetLatestTemplateData, tryGetTemplateDataFromCache } from './templates/TemplateData'; +import { getTemplateData } from './templates/TemplateData'; import { FunctionAppProvider } from './tree/FunctionAppProvider'; import { FunctionAppTreeItem } from './tree/FunctionAppTreeItem'; import { FunctionTreeItem } from './tree/FunctionTreeItem'; import { ProxyTreeItem } from './tree/ProxyTreeItem'; import { dotnetUtils } from './utils/dotnetUtils'; import { functionRuntimeUtils } from './utils/functionRuntimeUtils'; -import { cliFeedJsonResponse, getCliFeedJson } from './utils/getCliFeedJson'; export function activate(context: vscode.ExtensionContext): void { let reporter: TelemetryReporter | undefined; @@ -83,7 +82,7 @@ export function activate(context: vscode.ExtensionContext): void { await validateFunctionProjects(this, ui, outputChannel, event.added); }); - const templateDataTask: Promise = getTemplateData(reporter, context); + const templateDataTask: Promise = getTemplateDataTask(context); actionHandler.registerCommand('azureFunctions.selectSubscriptions', () => vscode.commands.executeCommand('azure-account.selectSubscriptions')); actionHandler.registerCommand('azureFunctions.refresh', async (node?: IAzureNode) => await tree.refresh(node)); @@ -125,10 +124,8 @@ export function activate(context: vscode.ExtensionContext): void { }); } -async function getTemplateData(reporter: TelemetryReporter | undefined, context: vscode.ExtensionContext): Promise { - const cliFeedJson: cliFeedJsonResponse = await getCliFeedJson(); - // tslint:disable-next-line:strict-boolean-expressions - ext.templateData = await tryGetTemplateDataFromCache(reporter, context.globalState, cliFeedJson) || await tryGetLatestTemplateData(reporter, cliFeedJson, context.globalState) || await getTemplateDataFromBackup(reporter, cliFeedJson); +async function getTemplateDataTask(context: vscode.ExtensionContext): Promise { + ext.templateData = await getTemplateData(context.globalState); } // tslint:disable-next-line:no-empty diff --git a/src/templates/TemplateData.ts b/src/templates/TemplateData.ts index 60651888f..5a1edb21f 100644 --- a/src/templates/TemplateData.ts +++ b/src/templates/TemplateData.ts @@ -10,23 +10,23 @@ import * as path from 'path'; // tslint:disable-next-line:no-require-imports import request = require('request-promise'); import * as vscode from 'vscode'; -import { callWithTelemetryAndErrorHandling, IActionContext } from 'vscode-azureextensionui'; -import TelemetryReporter from 'vscode-extension-telemetry'; -import { ScriptProjectCreatorBase } from '../commands/createNewProject/ScriptProjectCreatorBase'; -import { ProjectLanguage, ProjectRuntime, TemplateFilter, templateVersionSetting } from '../constants'; +import { callWithTelemetryAndErrorHandling, IActionContext, parseError } from 'vscode-azureextensionui'; +import { betaReleaseVersion, ProjectLanguage, ProjectRuntime, TemplateFilter, templateVersionSetting, v1ReleaseVersion } from '../constants'; import { ext } from '../extensionVariables'; import { localize } from '../localize'; import { getFuncExtensionSetting, updateGlobalSetting } from '../ProjectSettings'; -import { cliFeedJsonResponse } from '../utils/getCliFeedJson'; +import { cliFeedJsonResponse, tryGetCliFeedJson } from '../utils/getCliFeedJson'; import { Config } from './Config'; import { ConfigBinding } from './ConfigBinding'; import { ConfigSetting } from './ConfigSetting'; import { Resources } from './Resources'; import { Template, TemplateCategory } from './Template'; +export type parsedTemplates = [Template[], Config]; const templatesKey: string = 'FunctionTemplates'; const configKey: string = 'FunctionTemplateConfig'; const resourcesKey: string = 'FunctionTemplateResources'; +const templateVersionKey: string = 'templateVersion'; const tempPath: string = path.join(os.tmpdir(), 'vscode-azurefunctions-templates'); const verifiedTemplates: string[] = [ @@ -58,23 +58,30 @@ const verifiedJavaTemplates: string[] = [ * We cache the template data retrieved from the portal so that the user can create functions offline. */ export class TemplateData { - private readonly _templatesMap: { [runtime: string]: Template[] } = {}; - private readonly _configMap: { [runtime: string]: Config } = {}; - constructor(templatesMap: { [runtime: string]: Template[] }, configMap: { [runtime: string]: Config }) { + private readonly _templatesMap: { [runtime: string]: Template[] | undefined } = {}; + private readonly _configMap: { [runtime: string]: Config | undefined } = {}; + // if there are no templates, then there is likely no internet or a problem with the clifeed url + private readonly _noInternetErrMsg: string = localize('retryInternet', 'There was an error in retrieving the templates. Recheck your internet connection and try again.'); + constructor(templatesMap: { [runtime: string]: Template[] | undefined }, configMap: { [runtime: string]: Config | undefined }) { this._templatesMap = templatesMap; this._configMap = configMap; } public async getTemplates(language: string, runtime: string = ProjectRuntime.one, templateFilter?: string): Promise { + const templates: Template[] | undefined = this._templatesMap[runtime]; + if (!templates) { + throw new Error(this._noInternetErrMsg); + } + if (language === ProjectLanguage.Java) { // Currently we leverage JS templates to get the function metadata of Java Functions. // Will refactor the code here when templates HTTP API is ready. // See issue here: https://github.com/Microsoft/vscode-azurefunctions/issues/84 - const javaTemplates: Template[] = this._templatesMap[runtime].filter((t: Template) => t.language === ProjectLanguage.JavaScript); + const javaTemplates: Template[] = templates.filter((t: Template) => t.language === ProjectLanguage.JavaScript); return javaTemplates.filter((t: Template) => verifiedJavaTemplates.find((vt: string) => vt === removeLanguageFromId(t.id))); } else if (language === ProjectLanguage.CSharp) { // https://github.com/Microsoft/vscode-azurefunctions/issues/179 - return this._templatesMap[runtime].filter((t: Template) => verifiedCSharpTemplates.some((id: string) => id === t.id)); + return templates.filter((t: Template) => verifiedCSharpTemplates.some((id: string) => id === t.id)); } else { switch (language) { case ProjectLanguage.CSharpScript: @@ -85,24 +92,28 @@ export class TemplateData { default: } - let templates: Template[] = this._templatesMap[runtime].filter((t: Template) => t.language.toLowerCase() === language.toLowerCase()); + let filterTemplates: Template[] = templates.filter((t: Template) => t.language.toLowerCase() === language.toLowerCase()); switch (templateFilter) { case TemplateFilter.All: break; case TemplateFilter.Core: - templates = templates.filter((t: Template) => t.isCategory(TemplateCategory.Core)); + filterTemplates = filterTemplates.filter((t: Template) => t.isCategory(TemplateCategory.Core)); break; case TemplateFilter.Verified: default: - templates = templates.filter((t: Template) => verifiedTemplates.find((vt: string) => vt === t.id)); + filterTemplates = filterTemplates.filter((t: Template) => verifiedTemplates.find((vt: string) => vt === t.id)); } - return templates; + return filterTemplates; } } public async getSetting(runtime: ProjectRuntime, bindingType: string, settingName: string): Promise { - const binding: ConfigBinding | undefined = this._configMap[runtime].bindings.find((b: ConfigBinding) => b.bindingType === bindingType); + const config: Config | undefined = this._configMap[runtime]; + if (!config) { + throw new Error(this._noInternetErrMsg); + } + const binding: ConfigBinding | undefined = config.bindings.find((b: ConfigBinding) => b.bindingType === bindingType); if (binding) { return binding.settings.find((bs: ConfigSetting) => bs.name === settingName); } else { @@ -111,146 +122,113 @@ export class TemplateData { } } -function verifyTemplatesByRuntime(templatesMap: { [runtime: string]: Template[] }, runtime: ProjectRuntime): void { +function verifyTemplatesByRuntime(templates: Template[], runtime: ProjectRuntime): void { if (runtime === ProjectRuntime.one) { for (const verifiedTemplateId of verifiedTemplates) { - if (!templatesMap[ScriptProjectCreatorBase.defaultRuntime].some((t: Template) => t.id === verifiedTemplateId)) { + if (!templates.some((t: Template) => t.id === verifiedTemplateId)) { throw new Error(localize('failedToFindJavaScriptTemplate', 'Failed to find verified template with id "{0}".', verifiedTemplateId)); } } for (const verifiedTemplateId of verifiedCSharpTemplates) { - if (!templatesMap[ProjectRuntime.one].some((t: Template) => t.id === verifiedTemplateId)) { + if (!templates.some((t: Template) => t.id === verifiedTemplateId)) { throw new Error(localize('failedToFindCSharpTemplate', 'Failed to find verified template with id "{0}".', verifiedTemplateId)); } } } else if (runtime === ProjectRuntime.beta) { for (const verifiedTemplateId of verifiedCSharpTemplates) { - if (!templatesMap[ProjectRuntime.beta].some((t: Template) => t.id === verifiedTemplateId)) { + if (!templates.some((t: Template) => t.id === verifiedTemplateId)) { throw new Error(localize('failedToFindCSharpTemplate', 'Failed to find verified template with id "{0}".', verifiedTemplateId)); } } } } -export async function tryGetTemplateDataFromCache(reporter: TelemetryReporter | undefined, globalState: vscode.Memento, cliFeedJson: cliFeedJsonResponse): Promise { - try { - return await callWithTelemetryAndErrorHandling('azureFunctions.tryGetTemplateDataFromCache', reporter, undefined, async function (this: IActionContext): Promise { +export async function getTemplateData(globalState?: vscode.Memento): Promise { + const templatesMap: { [runtime: string]: Template[] | undefined } = {}; + const configMap: { [runtime: string]: Config | undefined } = {}; + const cliFeedJson: cliFeedJsonResponse | undefined = await tryGetCliFeedJson(); + for (const key of Object.keys(ProjectRuntime)) { + await callWithTelemetryAndErrorHandling('azureFunctions.getTemplateData', ext.reporter, undefined, async function (this: IActionContext): Promise { this.suppressErrorDisplay = true; this.properties.isActivationEvent = 'true'; - const templatesMap: { [runtime: string]: Template[] } = {}; - const configMap: { [runtime: string]: Config } = {}; - for (const key of Object.keys(ProjectRuntime)) { - // called within loop in case setting has changed between runtimes - const userTemplateVersion: string | undefined = getFuncExtensionSetting(templateVersionSetting); - const runtime: ProjectRuntime = ProjectRuntime[key]; - const feedRuntime: string = getFeedRuntime(runtime); - const releaseVersion: string = userTemplateVersion ? userTemplateVersion : cliFeedJson.tags[feedRuntime].release; - const cachedRelease: string | undefined = globalState.get(`${runtime}-release`); - if (!cachedRelease || releaseVersion !== cachedRelease) { - // templates are not up-to-date and need to be downloaded/extracted - return undefined; - } + const runtime: ProjectRuntime = ProjectRuntime[key]; + this.properties.runtime = runtime; + const templateVersion: string | undefined = await tryGetTemplateVersionSetting(this, cliFeedJson, runtime); + let parsedTemplatesByRuntime: parsedTemplates | undefined; - const cachedResources: object | undefined = globalState.get(getRuntimeKey(resourcesKey, runtime)); - const cachedTemplates: object[] | undefined = globalState.get(getRuntimeKey(templatesKey, runtime)); - const cachedConfig: object | undefined = globalState.get(getRuntimeKey(configKey, runtime)); + // 1. Use the cached templates if they match templateVersion + if (globalState && globalState.get(`${templateVersionKey}-${runtime}`) === templateVersion) { + parsedTemplatesByRuntime = await tryGetParsedTemplateDataFromCache(this, runtime, globalState); + this.properties.templateSource = 'matchingCache'; + } - if (cachedResources && cachedTemplates && cachedConfig) { - [templatesMap[runtime], configMap[runtime]] = parseTemplates(cachedResources, cachedTemplates, cachedConfig); - verifyTemplatesByRuntime(templatesMap, runtime); - } else { - return undefined; - } + // 2. Download templates from the cli-feed if the cache doesn't match templateVersion + if (!parsedTemplatesByRuntime && cliFeedJson && templateVersion) { + parsedTemplatesByRuntime = await tryGetParsedTemplateDataFromCliFeed(this, cliFeedJson, templateVersion, runtime, globalState); + this.properties.templateSource = 'cliFeed'; } - return new TemplateData(templatesMap, configMap); + // 3. Use the cached templates, even if they don't match templateVersion + if (!parsedTemplatesByRuntime && globalState) { + parsedTemplatesByRuntime = await tryGetParsedTemplateDataFromCache(this, runtime, globalState); + this.properties.templateSource = 'mismatchCache'; + } + + // 4. Download templates from the cli-feed using the backupVersion + if (!parsedTemplatesByRuntime && cliFeedJson) { + const backupVersion: string = runtime === ProjectRuntime.one ? v1ReleaseVersion : betaReleaseVersion; + parsedTemplatesByRuntime = await tryGetParsedTemplateDataFromCliFeed(this, cliFeedJson, backupVersion, runtime, globalState); + this.properties.templateSource = 'backupCliFeed'; + } + + if (parsedTemplatesByRuntime) { + [templatesMap[runtime], configMap[runtime]] = parsedTemplatesByRuntime; + } else { + // Failed to get templates for this runtime + this.properties.templateSource = 'None'; + } }); - } catch (error) { - return undefined; } + return new TemplateData(templatesMap, configMap); } -export async function tryGetLatestTemplateData(reporter: TelemetryReporter | undefined, cliFeedJson: cliFeedJsonResponse, globalState?: vscode.Memento): Promise { +async function tryGetParsedTemplateDataFromCache(context: IActionContext, runtime: ProjectRuntime, globalState: vscode.Memento): Promise { try { - return await callWithTelemetryAndErrorHandling('azureFunctions.tryGetLatestTemplateData', reporter, undefined, async function (this: IActionContext): Promise { - this.suppressErrorDisplay = true; - this.properties.isActivationEvent = 'true'; - const templatesMap: { [runtime: string]: Template[] } = {}; - const configMap: { [runtime: string]: Config } = {}; - for (const key of Object.keys(ProjectRuntime)) { - // called within loop in case setting has changed between runtimes - const userTemplateVersion: string | undefined = getFuncExtensionSetting(templateVersionSetting); - const runtime: ProjectRuntime = ProjectRuntime[key]; - const feedRuntime: string = getFeedRuntime(runtime); - const releaseVersion: string = userTemplateVersion ? userTemplateVersion : cliFeedJson.tags[feedRuntime].release; - await downloadAndExtractTemplates(cliFeedJson, releaseVersion); - - // only Resources.json has a capital letter - const rawResources: object = await fse.readJSON(path.join(tempPath, 'resources', 'Resources.json')); - const rawTemplates: object[] = await fse.readJSON(path.join(tempPath, 'templates', 'templates.json')); - const rawConfig: object = await fse.readJSON(path.join(tempPath, 'bindings', 'bindings.json')); - - [templatesMap[runtime], configMap[runtime]] = parseTemplates(rawResources, rawTemplates, rawConfig); - verifyTemplatesByRuntime(templatesMap, runtime); - if (globalState) { - globalState.update(`${runtime}-release`, releaseVersion); - globalState.update(getRuntimeKey(templatesKey, runtime), rawTemplates); - globalState.update(getRuntimeKey(configKey, runtime), rawConfig); - globalState.update(getRuntimeKey(resourcesKey, runtime), rawResources); - } - } - return new TemplateData(templatesMap, configMap); - }); - } catch (error) { - return undefined; - } finally { - if (await fse.pathExists(tempPath)) { - await fse.remove(tempPath); + const cachedResources: object | undefined = globalState.get(getRuntimeKey(resourcesKey, runtime)); + const cachedTemplates: object[] | undefined = globalState.get(getRuntimeKey(templatesKey, runtime)); + const cachedConfig: object | undefined = globalState.get(getRuntimeKey(configKey, runtime)); + if (cachedResources && cachedTemplates && cachedConfig) { + return parseTemplates(cachedResources, cachedTemplates, cachedConfig); } + } catch (error) { + context.properties.cacheError = parseError(error).message; } + return undefined; } -export async function getTemplateDataFromBackup(reporter: TelemetryReporter | undefined, cliFeedJson: cliFeedJsonResponse, globalState?: vscode.Memento): Promise { - const v1ReleaseVersion: string = '1.0.12'; // known stable version - const betaReleaseVersion: string = '2.0.1-beta.25'; // known stable version - +async function tryGetParsedTemplateDataFromCliFeed(context: IActionContext, cliFeedJson: cliFeedJsonResponse, templateVersion: string, runtime: ProjectRuntime, globalState?: vscode.Memento): Promise { try { - return await callWithTelemetryAndErrorHandling('azureFunctions.getTemplateDataFromBackup', reporter, undefined, async function (this: IActionContext): Promise { - this.suppressErrorDisplay = true; - this.properties.isActivationEvent = 'true'; - const templatesMap: { [runtime: string]: Template[] } = {}; - const configMap: { [runtime: string]: Config } = {}; - - for (const key of Object.keys(ProjectRuntime)) { - const runtime: ProjectRuntime = ProjectRuntime[key]; - const releaseVersion: string = runtime === ProjectRuntime.one ? v1ReleaseVersion : betaReleaseVersion; - if (globalState && globalState.get(`${runtime}-backup`) === releaseVersion) { - const cachedResources: object | undefined = globalState.get(getRuntimeKey(`${resourcesKey}-backup`, runtime)); - const cachedTemplates: object[] | undefined = globalState.get(getRuntimeKey(`${templatesKey}-backup`, runtime)); - const cachedConfig: object | undefined = globalState.get(getRuntimeKey(`${configKey}-backup`, runtime)); - if (cachedResources && cachedTemplates && cachedConfig) { - [templatesMap[runtime], configMap[runtime]] = parseTemplates(cachedResources, cachedTemplates, cachedConfig); - } - } else { - await downloadAndExtractTemplates(cliFeedJson, releaseVersion); - // only Resources.json has a capital letter - const rawResources: object = await fse.readJSON(path.join(tempPath, 'resources', 'Resources.json')); - const rawTemplates: object[] = await fse.readJSON(path.join(tempPath, 'templates', 'templates.json')); - const rawConfig: object = await fse.readJSON(path.join(tempPath, 'bindings', 'bindings.json')); - - [templatesMap[runtime], configMap[runtime]] = parseTemplates(rawResources, rawTemplates, rawConfig); - verifyTemplatesByRuntime(templatesMap, runtime); - if (globalState) { - globalState.update(`${runtime}-backup`, releaseVersion); - globalState.update(getRuntimeKey(`${templatesKey}-backup`, runtime), rawTemplates); - globalState.update(getRuntimeKey(`${configKey}-backup`, runtime), rawConfig); - globalState.update(getRuntimeKey(`${resourcesKey}-backup`, runtime), rawResources); - } - } - } - return new TemplateData(templatesMap, configMap); - }); + context.properties.templateVersion = templateVersion; + await downloadAndExtractTemplates(cliFeedJson.releases[templateVersion].templateApiZip, templateVersion); + // only Resources.json has a capital letter + const rawResources: object = await fse.readJSON(path.join(tempPath, 'resources', 'Resources.json')); + const rawTemplates: object[] = await fse.readJSON(path.join(tempPath, 'templates', 'templates.json')); + const rawConfig: object = await fse.readJSON(path.join(tempPath, 'bindings', 'bindings.json')); + + const parsedTemplatesByRuntime: parsedTemplates = parseTemplates(rawResources, rawTemplates, rawConfig); + verifyTemplatesByRuntime(parsedTemplatesByRuntime[0], runtime); + if (globalState) { + globalState.update(`${templateVersionKey}-${runtime}`, templateVersion); + globalState.update(getRuntimeKey(templatesKey, runtime), rawTemplates); + globalState.update(getRuntimeKey(configKey, runtime), rawConfig); + globalState.update(getRuntimeKey(resourcesKey, runtime), rawResources); + } + return parsedTemplatesByRuntime; + + } catch (error) { + context.properties.cliFeedError = parseError(error).message; + return undefined; } finally { if (await fse.pathExists(tempPath)) { await fse.remove(tempPath); @@ -262,7 +240,7 @@ function getRuntimeKey(baseKey: string, runtime: ProjectRuntime): string { return runtime === ProjectRuntime.one ? baseKey : `${baseKey}.${runtime}`; } -function parseTemplates(rawResources: object, rawTemplates: object[], rawConfig: object): [Template[], Config] { +function parseTemplates(rawResources: object, rawTemplates: object[], rawConfig: object): parsedTemplates { const resources: Resources = new Resources(rawResources); const templates: Template[] = []; for (const rawTemplate of rawTemplates) { @@ -280,23 +258,8 @@ export function removeLanguageFromId(id: string): string { } // tslint:disable-next-line:no-unsafe-any -async function downloadAndExtractTemplates(cliFeedJson: cliFeedJsonResponse, release: string): Promise<{}> { - const zipFile: string = 'templates.zip'; - // tslint:disable-next-line:strict-boolean-expressions - if (!cliFeedJson.releases[release]) { - const invalidVersion: string = `v${release} is not a valid release version. Pick a valid version.`; - const releaseQuickPicks: vscode.QuickPickItem[] = []; - for (const rel of Object.keys(cliFeedJson.releases)) { - releaseQuickPicks.push({ - label: rel, - description: '' - }); - } - const input: vscode.QuickPickItem | undefined = await ext.ui.showQuickPick(releaseQuickPicks, { placeHolder: invalidVersion }); - release = input.label; - await updateGlobalSetting(templateVersionSetting, release); - } - const templateUrl: string = cliFeedJson.releases[release].templateApiZip; +async function downloadAndExtractTemplates(templateUrl: string, release: string): Promise<{}> { + const zipFile: string = `templates-${release}.zip`; return new Promise(async (resolve: () => void, reject: (e: Error) => void): Promise => { const templateOptions: request.OptionsWithUri = { method: 'GET', @@ -310,14 +273,14 @@ async function downloadAndExtractTemplates(cliFeedJson: cliFeedJsonResponse, rel reject(err); } }).pipe(fse.createWriteStream(path.join(tempPath, zipFile)).on('finish', () => { - ext.outputChannel.appendLine(`Downloading v${release} templates zip file. . .`); + ext.outputChannel.appendLine(localize('downloadTemplates', 'Downloading "v{0}" templates zip file. . .', release)); // tslint:disable-next-line:no-unsafe-any extract(path.join(tempPath, zipFile), { dir: tempPath }, (err: Error) => { // tslint:disable-next-line:strict-boolean-expressions if (err) { reject(err); } - ext.outputChannel.appendLine('Template files extracted.'); + ext.outputChannel.appendLine(localize('templatesExtracted', 'Template files extracted.')); resolve(); }); @@ -335,3 +298,43 @@ function getFeedRuntime(runtime: ProjectRuntime): string { throw new RangeError(); } } + +async function tryGetTemplateVersionSetting(context: IActionContext, cliFeedJson: cliFeedJsonResponse | undefined, runtime: ProjectRuntime): Promise { + const feedRuntime: string = getFeedRuntime(runtime); + const userTemplateVersion: string | undefined = getFuncExtensionSetting(templateVersionSetting); + if (userTemplateVersion) { + context.properties.userTemplateVersion = userTemplateVersion; + } + let templateVersion: string; + if (cliFeedJson) { + templateVersion = userTemplateVersion ? userTemplateVersion : cliFeedJson.tags[feedRuntime].release; + // tslint:disable-next-line:strict-boolean-expressions + if (!cliFeedJson.releases[templateVersion]) { + const invalidVersion: string = localize('invalidTemplateVersion', 'Failed to retrieve Azure Functions templates for version "{0}".', templateVersion); + const selectVersion: vscode.MessageItem = { title: localize('selectVersion', 'Select version') }; + const useLatest: vscode.MessageItem = { title: localize('useLatest', 'Use latest') }; + const warningInput: vscode.MessageItem = await ext.ui.showWarningMessage(invalidVersion, selectVersion, useLatest); + if (warningInput === selectVersion) { + const releaseQuickPicks: vscode.QuickPickItem[] = []; + for (const rel of Object.keys(cliFeedJson.releases)) { + releaseQuickPicks.push({ + label: rel, + description: '' + }); + } + const input: vscode.QuickPickItem | undefined = await ext.ui.showQuickPick(releaseQuickPicks, { placeHolder: invalidVersion }); + templateVersion = input.label; + await updateGlobalSetting(templateVersionSetting, input.label); + } else { + templateVersion = cliFeedJson.tags[feedRuntime].release; + // reset user setting so that it always gets latest + await updateGlobalSetting(templateVersionSetting, ''); + + } + } + } else { + return undefined; + } + + return templateVersion; +} diff --git a/src/utils/getCliFeedJson.ts b/src/utils/getCliFeedJson.ts index b9371ca8a..7a2da0732 100644 --- a/src/utils/getCliFeedJson.ts +++ b/src/utils/getCliFeedJson.ts @@ -5,6 +5,8 @@ // tslint:disable-next-line:no-require-imports import request = require('request-promise'); +import { callWithTelemetryAndErrorHandling, IActionContext } from 'vscode-azureextensionui'; +import { ext } from '../extensionVariables'; const funcCliFeedUrl: string = 'https://aka.ms/V00v5v'; @@ -23,10 +25,15 @@ export type cliFeedJsonResponse = { } }; -export async function getCliFeedJson(): Promise { - const funcJsonOptions: request.OptionsWithUri = { - method: 'GET', - uri: funcCliFeedUrl - }; - return JSON.parse(await >request(funcJsonOptions).promise()); +export async function tryGetCliFeedJson(): Promise { + // tslint:disable-next-line:no-unsafe-any + return await callWithTelemetryAndErrorHandling('azureFunctions.tryGetCliFeedJson', ext.reporter, undefined, async function (this: IActionContext): Promise { + this.properties.isActivationEvent = 'true'; + this.suppressErrorDisplay = true; + const funcJsonOptions: request.OptionsWithUri = { + method: 'GET', + uri: funcCliFeedUrl + }; + return JSON.parse(await >request(funcJsonOptions).promise()); + }); } diff --git a/test/createFunction/FunctionTesterBase.ts b/test/createFunction/FunctionTesterBase.ts index b29a67f73..364824305 100644 --- a/test/createFunction/FunctionTesterBase.ts +++ b/test/createFunction/FunctionTesterBase.ts @@ -14,23 +14,19 @@ import { createFunction } from '../../src/commands/createFunction/createFunction import { ProjectLanguage, projectLanguageSetting, ProjectRuntime, projectRuntimeSetting, TemplateFilter, templateFilterSetting } from '../../src/constants'; import { ext } from '../../src/extensionVariables'; import { getGlobalFuncExtensionSetting, updateGlobalSetting } from '../../src/ProjectSettings'; -import { getTemplateDataFromBackup, TemplateData, tryGetLatestTemplateData } from '../../src/templates/TemplateData'; +import { getTemplateData, TemplateData } from '../../src/templates/TemplateData'; import * as fsUtil from '../../src/utils/fs'; -import { cliFeedJsonResponse, getCliFeedJson } from '../../src/utils/getCliFeedJson'; let backupTemplateData: TemplateData; let funcPortalTemplateData: TemplateData | undefined; -let funcStagingPortalTemplateData: TemplateData | undefined; // tslint:disable-next-line:no-function-expression suiteSetup(async function (this: IHookCallbackContext): Promise { this.timeout(30 * 1000); // Ensure template data is initialized before any 'Create Function' test is run - const cliFeedJson: cliFeedJsonResponse = await getCliFeedJson(); - backupTemplateData = await getTemplateDataFromBackup(undefined, cliFeedJson); - funcPortalTemplateData = await tryGetLatestTemplateData(undefined, cliFeedJson, undefined); + backupTemplateData = (await getTemplateData(undefined)); + funcPortalTemplateData = (await getTemplateData(undefined)); // https://github.com/Microsoft/vscode-azurefunctions/issues/334 - funcStagingPortalTemplateData = await tryGetLatestTemplateData(undefined, cliFeedJson, undefined); }); export abstract class FunctionTesterBase implements vscode.Disposable { @@ -86,12 +82,6 @@ export abstract class FunctionTesterBase implements vscode.Disposable { assert.fail('Failed to find templates from functions portal.'); } - if (funcStagingPortalTemplateData) { - await this.testCreateFunctionInternal(funcStagingPortalTemplateData, this.funcStagingPortalTestFolder, templateName, inputs.slice()); - } else { - assert.fail('Failed to find templates from functions staging portal.'); - } - await this.testCreateFunctionInternal(backupTemplateData, this.backupTestFolder, templateName, inputs.slice()); } diff --git a/test/templateData.test.ts b/test/templateData.test.ts index c05bdc81b..47cf6d95f 100644 --- a/test/templateData.test.ts +++ b/test/templateData.test.ts @@ -9,21 +9,17 @@ import { JavaProjectCreator } from '../src/commands/createNewProject/JavaProject import { JavaScriptProjectCreator } from '../src/commands/createNewProject/JavaScriptProjectCreator'; import { ProjectLanguage, ProjectRuntime, TemplateFilter } from '../src/constants'; import { Template } from '../src/templates/Template'; -import { getTemplateDataFromBackup, TemplateData, tryGetLatestTemplateData } from '../src/templates/TemplateData'; -import { cliFeedJsonResponse, getCliFeedJson } from '../src/utils/getCliFeedJson'; +import { getTemplateData, TemplateData } from '../src/templates/TemplateData'; let backupTemplateData: TemplateData; let funcPortalTemplateData: TemplateData | undefined; -let funcStagingPortalTemplateData: TemplateData | undefined; // tslint:disable-next-line:no-function-expression suiteSetup(async function (this: IHookCallbackContext): Promise { this.timeout(30 * 1000); - const cliFeedJson: cliFeedJsonResponse = await getCliFeedJson(); - backupTemplateData = await getTemplateDataFromBackup(undefined, cliFeedJson); - funcPortalTemplateData = await tryGetLatestTemplateData(undefined, cliFeedJson, undefined); + backupTemplateData = (await getTemplateData(undefined)); + funcPortalTemplateData = (await getTemplateData(undefined)); // https://github.com/Microsoft/vscode-azurefunctions/issues/334 - funcStagingPortalTemplateData = await tryGetLatestTemplateData(undefined, cliFeedJson, undefined); }); suite('Template Data Tests', async () => { @@ -34,12 +30,6 @@ suite('Template Data Tests', async () => { assert.fail('Failed to find templates from functions portal.'); } - if (funcStagingPortalTemplateData) { - await validateTemplateData(funcStagingPortalTemplateData); - } else { - assert.fail('Failed to find templates from functions staging portal.'); - } - await validateTemplateData(backupTemplateData); }); });