|
3 | 3 | * Licensed under the MIT License. See License.txt in the project root for license information. |
4 | 4 | *--------------------------------------------------------------------------------------------*/ |
5 | 5 |
|
| 6 | +import { type Site, type StringDictionary } from '@azure/arm-appservice'; |
6 | 7 | import { type ServiceClient } from '@azure/core-client'; |
7 | 8 | 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'; |
8 | 11 | 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'; |
11 | 14 | import { hiddenStacksSetting, noRuntimeStacksAvailableLabel } from '../../../constants'; |
12 | 15 | import { previewDescription } from '../../../constants-nls'; |
| 16 | +import { funcVersionLink } from '../../../FuncVersion'; |
13 | 17 | import { localize } from '../../../localize'; |
| 18 | +import { isResolvedFunctionApp } from '../../../tree/ResolvedFunctionAppResource'; |
14 | 19 | import { requestUtils } from '../../../utils/requestUtils'; |
15 | 20 | import { getWorkspaceSetting } from '../../../vsCodeConfig/settings'; |
16 | 21 | import { type FullFunctionAppStack, type IFunctionAppWizardContext } from '../IFunctionAppWizardContext'; |
@@ -149,7 +154,7 @@ function getPriority(ss: FunctionAppRuntimes): number { |
149 | 154 | } |
150 | 155 |
|
151 | 156 | 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[]> { |
153 | 158 | if (!context._stacks) { |
154 | 159 | let stacksArmResponse: StacksArmResponse; |
155 | 160 | try { |
@@ -178,16 +183,16 @@ async function getStacks(context: IFunctionAppWizardContext & { _stacks?: Functi |
178 | 183 | return context._stacks; |
179 | 184 | } |
180 | 185 |
|
181 | | -async function getFlexStacks(context: IFunctionAppWizardContext & { _stacks?: FunctionAppStack[] }): Promise<FunctionAppStack[]> { |
| 186 | +async function getFlexStacks(context: ISubscriptionActionContext & { _stacks?: FunctionAppStack[] }, location?: string): Promise<FunctionAppStack[]> { |
182 | 187 | const client: ServiceClient = await createGenericClient(context, context); |
183 | | - const location = await LocationListStep.getLocation(context); |
| 188 | + location = location ?? (await LocationListStep.getLocation(context)).name; |
184 | 189 | const flexFunctionAppStacks: FunctionAppStack[] = []; |
185 | 190 | const stacks = ['dotnet', 'java', 'node', 'powershell', 'python']; |
186 | 191 | if (!context._stacks) { |
187 | 192 | const getFlexStack = async (stack: string) => { |
188 | 193 | const result: AzExtPipelineResponse = await client.sendRequest(createPipelineRequest({ |
189 | 194 | method: 'GET', |
190 | | - url: requestUtils.createRequestUrl(`providers/Microsoft.Web/locations/${location.name}/functionAppStacks`, { |
| 195 | + url: requestUtils.createRequestUrl(`providers/Microsoft.Web/locations/${location}/functionAppStacks`, { |
191 | 196 | 'api-version': '2023-12-01', |
192 | 197 | stack, |
193 | 198 | removeDeprecatedStacks: String(!getWorkspaceSetting<boolean>('showDeprecatedStacks')) |
@@ -273,3 +278,147 @@ export function shouldShowEolWarning(minorVersion?: AppStackMinorVersion<Functio |
273 | 278 | } |
274 | 279 | return false |
275 | 280 | } |
| 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 | +} |
0 commit comments