Skip to content

Commit 58236e2

Browse files
authored
Add EOL warning when updating/deploying to Function apps running retired stack versions (#4428)
* add eol * added more things * more changes * more changes * changes * more changes * change * requested changes * requested changes
1 parent c74a93b commit 58236e2

File tree

11 files changed

+319
-152
lines changed

11 files changed

+319
-152
lines changed

package-lock.json

Lines changed: 99 additions & 124 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1371,7 +1371,7 @@
13711371
"@azure/core-client": "^1.7.3",
13721372
"@azure/core-rest-pipeline": "^1.11.0",
13731373
"@azure/storage-blob": "^12.5.0",
1374-
"@microsoft/vscode-azext-azureappservice": "^3.3.1",
1374+
"@microsoft/vscode-azext-azureappservice": "^3.5.3",
13751375
"@microsoft/vscode-azext-azureappsettings": "^0.2.2",
13761376
"@microsoft/vscode-azext-azureutils": "^3.1.6",
13771377
"@microsoft/vscode-azext-utils": "^2.6.3",

src/commands/appSettings/downloadAppSettings.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@
66
import { type StringDictionary } from "@azure/arm-appservice";
77
import { confirmOverwriteSettings } from "@microsoft/vscode-azext-azureappservice";
88
import { AppSettingsTreeItem, type IAppSettingsClient } from "@microsoft/vscode-azext-azureappsettings";
9-
import { AzExtFsExtra, type IActionContext } from "@microsoft/vscode-azext-utils";
9+
import { AzExtFsExtra, nonNullValue, type IActionContext, type ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
1010
import * as vscode from 'vscode';
1111
import { functionFilter, localSettingsFileName } from "../../constants";
1212
import { viewOutput } from "../../constants-nls";
1313
import { ext } from "../../extensionVariables";
1414
import { getLocalSettingsJson, type ILocalSettingsJson } from "../../funcConfig/local.settings";
1515
import { localize } from "../../localize";
1616
import type * as api from '../../vscode-azurefunctions.api';
17+
import { showEolWarningIfNecessary } from "../createFunctionApp/stacks/getStackPicks";
1718
import { decryptLocalSettings } from "./localSettings/decryptLocalSettings";
1819
import { encryptLocalSettings } from "./localSettings/encryptLocalSettings";
1920
import { getLocalSettingsFile } from "./localSettings/getLocalSettingsFile";
2021

21-
export async function downloadAppSettings(context: IActionContext, node?: AppSettingsTreeItem): Promise<void> {
22+
export async function downloadAppSettings(context: ISubscriptionActionContext, node?: AppSettingsTreeItem): Promise<void> {
2223
if (!node) {
2324
node = await ext.rgApi.pickAppResource<AppSettingsTreeItem>(context, {
2425
filter: functionFilter,
2526
expectedChildContextValue: new RegExp(AppSettingsTreeItem.contextValue)
2627
});
2728
}
2829

30+
const parent = node.parent;
2931
const client: IAppSettingsClient = await node.clientProvider.createClient(context);
32+
await showEolWarningIfNecessary(context, nonNullValue(parent), client);
3033
await node.runWithTemporaryDescription(context, localize('downloading', 'Downloading...'), async () => {
3134
await downloadAppSettingsInternal(context, client);
3235
});

src/commands/appSettings/uploadAppSettings.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
import { type StringDictionary } from "@azure/arm-appservice";
77
import { confirmOverwriteSettings } from "@microsoft/vscode-azext-azureappservice";
88
import { AppSettingsTreeItem, type IAppSettingsClient } from "@microsoft/vscode-azext-azureappsettings";
9-
import { AzExtFsExtra, type IActionContext } from "@microsoft/vscode-azext-utils";
9+
import { AzExtFsExtra, nonNullValue, type IActionContext, type ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
1010
import * as vscode from 'vscode';
1111
import { ConnectionKey, functionFilter, localEventHubsEmulatorConnectionRegExp, localSettingsFileName, localStorageEmulatorConnectionString } from "../../constants";
1212
import { viewOutput } from "../../constants-nls";
1313
import { ext } from "../../extensionVariables";
1414
import { type ILocalSettingsJson } from "../../funcConfig/local.settings";
1515
import { localize } from "../../localize";
1616
import type * as api from '../../vscode-azurefunctions.api';
17+
import { showEolWarningIfNecessary } from "../createFunctionApp/stacks/getStackPicks";
1718
import { decryptLocalSettings } from "./localSettings/decryptLocalSettings";
1819
import { encryptLocalSettings } from "./localSettings/encryptLocalSettings";
1920
import { getLocalSettingsFile } from "./localSettings/getLocalSettingsFile";
2021

21-
export async function uploadAppSettings(context: IActionContext, node?: AppSettingsTreeItem, _nodes?: [], workspaceFolder?: vscode.WorkspaceFolder, exclude?: (RegExp | string)[]): Promise<void> {
22+
export async function uploadAppSettings(context: ISubscriptionActionContext, node?: AppSettingsTreeItem, _nodes?: [], workspaceFolder?: vscode.WorkspaceFolder, exclude?: (RegExp | string)[]): Promise<void> {
2223
context.telemetry.eventVersion = 2;
2324
if (!node) {
2425
node = await ext.rgApi.pickAppResource<AppSettingsTreeItem>(context, {
@@ -27,7 +28,9 @@ export async function uploadAppSettings(context: IActionContext, node?: AppSetti
2728
});
2829
}
2930

31+
const parent = node.parent;
3032
const client: IAppSettingsClient = await node.clientProvider.createClient(context);
33+
await showEolWarningIfNecessary(context, nonNullValue(parent), client);
3134
await node.runWithTemporaryDescription(context, localize('uploading', 'Uploading...'), async () => {
3235
await uploadAppSettingsInternal(context, client, workspaceFolder, exclude);
3336
});

src/commands/createFunctionApp/stacks/getStackPicks.ts

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,19 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { type Site, type StringDictionary } from '@azure/arm-appservice';
67
import { type ServiceClient } from '@azure/core-client';
78
import { createPipelineRequest } from '@azure/core-rest-pipeline';
9+
import { type SiteClient } from '@microsoft/vscode-azext-azureappservice';
10+
import { type IAppSettingsClient } from '@microsoft/vscode-azext-azureappsettings';
811
import { createGenericClient, LocationListStep, type AzExtPipelineResponse } from '@microsoft/vscode-azext-azureutils';
9-
import { maskUserInfo, openUrl, parseError, type AgentQuickPickItem, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
10-
import { funcVersionLink } from '../../../FuncVersion';
12+
import { maskUserInfo, nonNullValue, openUrl, parseError, type AgentQuickPickItem, type AzExtParentTreeItem, type IAzureQuickPickItem, type ISubscriptionActionContext } from '@microsoft/vscode-azext-utils';
13+
import { type MessageItem } from 'vscode';
1114
import { hiddenStacksSetting, noRuntimeStacksAvailableLabel } from '../../../constants';
1215
import { previewDescription } from '../../../constants-nls';
16+
import { funcVersionLink } from '../../../FuncVersion';
1317
import { localize } from '../../../localize';
18+
import { isResolvedFunctionApp } from '../../../tree/ResolvedFunctionAppResource';
1419
import { requestUtils } from '../../../utils/requestUtils';
1520
import { getWorkspaceSetting } from '../../../vsCodeConfig/settings';
1621
import { type FullFunctionAppStack, type IFunctionAppWizardContext } from '../IFunctionAppWizardContext';
@@ -149,7 +154,7 @@ function getPriority(ss: FunctionAppRuntimes): number {
149154
}
150155

151156
type StacksArmResponse = { value: { properties: FunctionAppStack }[] };
152-
async function getStacks(context: IFunctionAppWizardContext & { _stacks?: FunctionAppStack[] }): Promise<FunctionAppStack[]> {
157+
async function getStacks(context: ISubscriptionActionContext & { _stacks?: FunctionAppStack[] }): Promise<FunctionAppStack[]> {
153158
if (!context._stacks) {
154159
let stacksArmResponse: StacksArmResponse;
155160
try {
@@ -178,16 +183,16 @@ async function getStacks(context: IFunctionAppWizardContext & { _stacks?: Functi
178183
return context._stacks;
179184
}
180185

181-
async function getFlexStacks(context: IFunctionAppWizardContext & { _stacks?: FunctionAppStack[] }): Promise<FunctionAppStack[]> {
186+
async function getFlexStacks(context: ISubscriptionActionContext & { _stacks?: FunctionAppStack[] }, location?: string): Promise<FunctionAppStack[]> {
182187
const client: ServiceClient = await createGenericClient(context, context);
183-
const location = await LocationListStep.getLocation(context);
188+
location = location ?? (await LocationListStep.getLocation(context)).name;
184189
const flexFunctionAppStacks: FunctionAppStack[] = [];
185190
const stacks = ['dotnet', 'java', 'node', 'powershell', 'python'];
186191
if (!context._stacks) {
187192
const getFlexStack = async (stack: string) => {
188193
const result: AzExtPipelineResponse = await client.sendRequest(createPipelineRequest({
189194
method: 'GET',
190-
url: requestUtils.createRequestUrl(`providers/Microsoft.Web/locations/${location.name}/functionAppStacks`, {
195+
url: requestUtils.createRequestUrl(`providers/Microsoft.Web/locations/${location}/functionAppStacks`, {
191196
'api-version': '2023-12-01',
192197
stack,
193198
removeDeprecatedStacks: String(!getWorkspaceSetting<boolean>('showDeprecatedStacks'))
@@ -273,3 +278,147 @@ export function shouldShowEolWarning(minorVersion?: AppStackMinorVersion<Functio
273278
}
274279
return false
275280
}
281+
282+
export interface eolWarningOptions {
283+
site: Site;
284+
isLinux?: boolean;
285+
isFlex?: boolean;
286+
client?: SiteClient | IAppSettingsClient;
287+
location?: string;
288+
version?: string;
289+
runtime?: string
290+
}
291+
/**
292+
* This function checks the end of life date for stack and returns a message if the stack is end of life or will be end of life in 6 months.
293+
*/
294+
export async function getEolWarningMessages(context: ISubscriptionActionContext, options: eolWarningOptions): Promise<string> {
295+
let isEOL = false;
296+
let willBeEOL = false;
297+
let version: string | undefined;
298+
let displayInfo: {
299+
endOfLife: Date | undefined;
300+
displayVersion: string | undefined;
301+
} = { endOfLife: undefined, displayVersion: undefined };
302+
303+
if (options.isFlex) {
304+
const runtime = options.site.functionAppConfig?.runtime?.name;
305+
version = options.site.functionAppConfig?.runtime?.version;
306+
displayInfo = (await getEOLDate(context, {
307+
site: options.site,
308+
version: nonNullValue(version),
309+
runtime: nonNullValue(runtime),
310+
isFlex: true,
311+
location: options.site.location
312+
})
313+
);
314+
} else if (options.isLinux) {
315+
const linuxFxVersion = options.site.siteConfig?.linuxFxVersion;
316+
displayInfo = await getEOLLinuxFxVersion(context, nonNullValue(linuxFxVersion));
317+
} else if (options.site.siteConfig) {
318+
if (options.site.siteConfig.netFrameworkVersion && !options.site.siteConfig.powerShellVersion && !options.site.siteConfig.javaVersion) {
319+
displayInfo = (await getEOLDate(context, {
320+
site: options.site,
321+
version: options.site.siteConfig.netFrameworkVersion,
322+
runtime: 'dotnet'
323+
}));
324+
} else if (options.site.siteConfig.javaVersion) {
325+
displayInfo = (await getEOLDate(context, {
326+
site: options.site,
327+
version: options.site.siteConfig.javaVersion,
328+
runtime: 'java'
329+
}));
330+
} else if (options.site.siteConfig.powerShellVersion) {
331+
displayInfo = (await getEOLDate(context, {
332+
site: options.site,
333+
version: options.site.siteConfig.powerShellVersion,
334+
runtime: 'powershell'
335+
}));
336+
}
337+
338+
// In order to get the node version, we need to check the app settings
339+
let appSettings: StringDictionary | undefined;
340+
if (options.client) {
341+
appSettings = await options.client.listApplicationSettings();
342+
}
343+
if (appSettings && appSettings.properties && appSettings.properties['WEBSITE_NODE_DEFAULT_VERSION']) {
344+
displayInfo = (await getEOLDate(context, {
345+
site: options.site,
346+
version: appSettings.properties['WEBSITE_NODE_DEFAULT_VERSION'],
347+
runtime: 'node'
348+
}));
349+
}
350+
}
351+
352+
if (displayInfo.endOfLife) {
353+
const sixMonthsFromNow = new Date(new Date().setMonth(new Date().getMonth() + 6));
354+
isEOL = displayInfo.endOfLife <= new Date();
355+
willBeEOL = displayInfo.endOfLife <= sixMonthsFromNow;
356+
if (isEOL) {
357+
return localize('eolWarning', 'Upgrade to the latest available version as version {0} has reached end-of-life on {1} and is no longer supported.', displayInfo.displayVersion, displayInfo.endOfLife.toLocaleDateString());
358+
} else if (willBeEOL) {
359+
return localize('willBeEolWarning', 'Upgrade to the latest available version as version {0} will reach end-of-life on {1} and will no longer be supported.', displayInfo.displayVersion, displayInfo.endOfLife.toLocaleDateString());
360+
}
361+
}
362+
363+
return '';
364+
}
365+
366+
export async function showEolWarningIfNecessary(context: ISubscriptionActionContext, parent: AzExtParentTreeItem, client?: IAppSettingsClient) {
367+
if (isResolvedFunctionApp(parent)) {
368+
client = client ?? await parent.site.createClient(context);
369+
const eolWarningMessage = await getEolWarningMessages(context, {
370+
site: parent.site.rawSite,
371+
isLinux: client.isLinux,
372+
isFlex: parent.isFlex,
373+
client
374+
});
375+
const continueOn: MessageItem = { title: localize('continueOn', 'Continue') };
376+
await context.ui.showWarningMessage(eolWarningMessage, { modal: true }, continueOn);
377+
}
378+
}
379+
380+
async function getEOLDate(context: ISubscriptionActionContext, options: eolWarningOptions): Promise<{ endOfLife: Date | undefined, displayVersion: string }> {
381+
const stacks = options.isFlex ?
382+
(await getFlexStacks(context, options.location)).filter(s => options.runtime === s.value) :
383+
(await getStacks(context)).filter(s => options.runtime === s.value);
384+
const versionFilteredStacks = stacks[0].majorVersions.filter(mv => mv.minorVersions.some(minor => options.isFlex ? minor.stackSettings.linuxRuntimeSettings?.runtimeVersion : minor.stackSettings.windowsRuntimeSettings?.runtimeVersion === options.version));
385+
const filteredStack = versionFilteredStacks[0].minorVersions[0];
386+
const displayVersion = filteredStack?.displayText;
387+
const endOfLifeDate = options.isFlex ?
388+
filteredStack?.stackSettings.linuxRuntimeSettings?.endOfLifeDate :
389+
filteredStack?.stackSettings.windowsRuntimeSettings?.endOfLifeDate;
390+
if (endOfLifeDate) {
391+
const endOfLife = new Date(endOfLifeDate)
392+
return {
393+
endOfLife,
394+
displayVersion
395+
}
396+
}
397+
return {
398+
endOfLife: undefined,
399+
displayVersion
400+
}
401+
}
402+
403+
async function getEOLLinuxFxVersion(context: ISubscriptionActionContext, linuxFxVersion: string): Promise<{ endOfLife: Date | undefined, displayVersion: string }> {
404+
const stacks = (await getStacks(context)).filter(s =>
405+
s.majorVersions.some(mv =>
406+
mv.minorVersions.some(minor => minor.stackSettings.linuxRuntimeSettings?.runtimeVersion === linuxFxVersion)
407+
)
408+
);
409+
const versionFilteredStacks = stacks[0].majorVersions.filter(mv => mv.minorVersions.some(minor => minor.stackSettings.linuxRuntimeSettings?.runtimeVersion === linuxFxVersion));
410+
const filteredStack = versionFilteredStacks[0].minorVersions[0];
411+
const displayVersion = filteredStack?.displayText;
412+
const endOfLifeDate = filteredStack?.stackSettings.linuxRuntimeSettings?.endOfLifeDate;
413+
if (endOfLifeDate) {
414+
const endOfLife = new Date(endOfLifeDate)
415+
return {
416+
endOfLife,
417+
displayVersion
418+
}
419+
}
420+
return {
421+
endOfLife: undefined,
422+
displayVersion
423+
}
424+
}

src/commands/deploy/deploy.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55

66
import { type Site, type SiteConfigResource } from '@azure/arm-appservice';
77
import { getDeployFsPath, getDeployNode, deploy as innerDeploy, showDeployConfirmation, type IDeployContext, type IDeployPaths } from '@microsoft/vscode-azext-azureappservice';
8-
import { DialogResponses, type ExecuteActivityContext, type IActionContext } from '@microsoft/vscode-azext-utils';
8+
import { DialogResponses, type ExecuteActivityContext, type IActionContext, type ISubscriptionActionContext } from '@microsoft/vscode-azext-utils';
99
import { type AzureSubscription } from '@microsoft/vscode-azureresources-api';
1010
import type * as vscode from 'vscode';
11-
import { CodeAction, ConnectionType, DurableBackend, ProjectLanguage, ScmType, deploySubpathSetting, hostFileName, remoteBuildSetting, type DurableBackendValues } from '../../constants';
11+
import { CodeAction, ConnectionType, deploySubpathSetting, DurableBackend, hostFileName, ProjectLanguage, remoteBuildSetting, ScmType, type DurableBackendValues } from '../../constants';
1212
import { ext } from '../../extensionVariables';
1313
import { addLocalFuncTelemetry } from '../../funcCoreTools/getLocalFuncCoreToolsVersion';
1414
import { localize } from '../../localize';
@@ -25,6 +25,7 @@ import { verifyInitForVSCode } from '../../vsCodeConfig/verifyInitForVSCode';
2525
import { type ISetConnectionSettingContext } from '../appSettings/connectionSettings/ISetConnectionSettingContext';
2626
import { validateEventHubsConnection } from '../appSettings/connectionSettings/eventHubs/validateEventHubsConnection';
2727
import { validateSqlDbConnection } from '../appSettings/connectionSettings/sqlDatabase/validateSqlDbConnection';
28+
import { getEolWarningMessages } from '../createFunctionApp/stacks/getStackPicks';
2829
import { tryGetFunctionProjectRoot } from '../createNewProject/verifyIsProject';
2930
import { getOrCreateFunctionApp } from './getOrCreateFunctionApp';
3031
import { notifyDeployComplete } from './notifyDeployComplete';
@@ -141,9 +142,20 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
141142
await validateSqlDbConnection(context, context.projectPath);
142143
}
143144

144-
if (getWorkspaceSetting<boolean>('showDeployConfirmation', context.workspaceFolder.uri.fsPath) && !context.isNewApp && isZipDeploy) {
145+
const subContext = {
146+
...context
147+
} as unknown as ISubscriptionActionContext;
148+
149+
const eolWarningMessage = await getEolWarningMessages(subContext, {
150+
site: node.site.rawSite,
151+
isLinux: client.isLinux,
152+
isFlex: isFlexConsumption,
153+
client
154+
});
155+
156+
if ((getWorkspaceSetting<boolean>('showDeployConfirmation', context.workspaceFolder.uri.fsPath) && !context.isNewApp && isZipDeploy) || eolWarningMessage) {
145157
const deployCommandId = 'azureFunctions.deploy';
146-
await showDeployConfirmation(context, node.site, deployCommandId);
158+
await showDeployConfirmation(context, node.site, deployCommandId, [eolWarningMessage]);
147159
}
148160

149161
await runPreDeployTask(context, context.effectiveDeployFsPath, siteConfig.scmType);

src/commands/deploy/notifyDeployComplete.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ export async function notifyDeployComplete(context: IActionContext, node: SlotTr
4343
} else if (result === streamLogs) {
4444
await startStreamingLogs(postDeployContext, node);
4545
} else if (result === uploadSettings) {
46-
await uploadAppSettings(postDeployContext, node.appSettingsTreeItem, undefined, workspaceFolder);
46+
const subContext = {
47+
...postDeployContext,
48+
...node.site.subscription
49+
}
50+
await uploadAppSettings(subContext, node.appSettingsTreeItem, undefined, workspaceFolder);
4751
}
4852
});
4953
});

src/commands/editAppSetting.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { AppSettingTreeItem } from '@microsoft/vscode-azext-azureappsettings';
7-
import { type IActionContext } from '@microsoft/vscode-azext-utils';
7+
import { nonNullValue } from '@microsoft/vscode-azext-utils';
88
import { functionFilter } from '../constants';
99
import { ext } from '../extensionVariables';
10+
import { type IFunctionAppWizardContext } from './createFunctionApp/IFunctionAppWizardContext';
11+
import { showEolWarningIfNecessary } from './createFunctionApp/stacks/getStackPicks';
1012

11-
export async function editAppSetting(context: IActionContext, node?: AppSettingTreeItem): Promise<void> {
13+
export async function editAppSetting(context: IFunctionAppWizardContext, node?: AppSettingTreeItem): Promise<void> {
1214
if (!node) {
1315
node = await ext.rgApi.pickAppResource<AppSettingTreeItem>(context, {
1416
filter: functionFilter,
1517
expectedChildContextValue: new RegExp(AppSettingTreeItem.contextValue)
1618
});
1719
}
18-
20+
const parent = node.parent.parent;
21+
await showEolWarningIfNecessary(context, nonNullValue(parent))
1922
await node.edit(context);
2023
}

0 commit comments

Comments
 (0)