diff --git a/package-lock.json b/package-lock.json index 404e11bfb..55b196aea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,9 +21,9 @@ "@azure/storage-blob": "^12.4.1", "@fluentui/react-components": "^9.56.2", "@fluentui/react-icons": "^2.0.265", - "@microsoft/vscode-azext-azureutils": "^3.3.3", + "@microsoft/vscode-azext-azureutils": "^3.5.0", "@microsoft/vscode-azext-github": "^1.0.4", - "@microsoft/vscode-azext-utils": "^3.3.3", + "@microsoft/vscode-azext-utils": "^3.4.0", "@microsoft/vscode-azureresources-api": "^2.0.2", "@vscode/codicons": "0.0.38", "buffer": "^6.0.3", @@ -2514,7 +2514,9 @@ } }, "node_modules/@microsoft/vscode-azext-azureutils": { - "version": "3.3.3", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-3.5.0.tgz", + "integrity": "sha512-vmZtSbKgdyx/9XSek5PDYar/UAF8s5CaUiGYPw7K7bMKb6W8ogz0iN9B/4g3o3ILFe2sTGj1yyv1fC8magPz5A==", "license": "MIT", "dependencies": { "@azure/arm-authorization": "^9.0.0", @@ -2528,7 +2530,7 @@ "@azure/core-client": "^1.6.0", "@azure/core-rest-pipeline": "^1.9.0", "@azure/logger": "^1.0.4", - "@microsoft/vscode-azext-utils": "^3.1.1", + "@microsoft/vscode-azext-utils": "^3.4.0", "semver": "^7.3.7", "uuid": "^9.0.0" }, @@ -2702,12 +2704,12 @@ } }, "node_modules/@microsoft/vscode-azext-utils": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-3.3.3.tgz", - "integrity": "sha512-rltLtVeUTUNHEeGzyw7A0GoRhHNBRWRpB6N2LEETBUXn5J06EqgXg/K6JxO2NCooCAi+eI+g1uSUCn2AM4DsTQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-3.4.0.tgz", + "integrity": "sha512-N7XwdasZycNbsdPm0EBi/KVE/AK3eL3U8/qPQk/2MGKGGM23O3/T7NmUfnwyIXe0vRjH2O6jzhlsXFq0cDftvQ==", "license": "MIT", "dependencies": { - "@microsoft/vscode-azureresources-api": "^2.3.1", + "@microsoft/vscode-azureresources-api": "^2.6.0", "@vscode/extension-telemetry": "^0.9.6", "dayjs": "^1.11.2", "escape-string-regexp": "^2.0.0", @@ -2740,7 +2742,9 @@ } }, "node_modules/@microsoft/vscode-azureresources-api": { - "version": "2.3.1", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azureresources-api/-/vscode-azureresources-api-2.6.0.tgz", + "integrity": "sha512-pvNRLt0/xi7BMP53WQM/vaYf77wRJxAIeqLbVHucpKqV3BLt5hlTjBevQNlo0RyxuAECIE09xO1Gt24pVmsJ3Q==", "license": "MIT", "peerDependencies": { "@azure/ms-rest-azure-env": "^2.0.0" diff --git a/package.json b/package.json index f78b24fca..cfbd2bb10 100644 --- a/package.json +++ b/package.json @@ -862,9 +862,9 @@ "@azure/storage-blob": "^12.4.1", "@fluentui/react-components": "^9.56.2", "@fluentui/react-icons": "^2.0.265", - "@microsoft/vscode-azext-azureutils": "^3.3.3", + "@microsoft/vscode-azext-azureutils": "^3.5.0", "@microsoft/vscode-azext-github": "^1.0.4", - "@microsoft/vscode-azext-utils": "^3.3.3", + "@microsoft/vscode-azext-utils": "^3.4.0", "@microsoft/vscode-azureresources-api": "^2.0.2", "@vscode/codicons": "0.0.38", "buffer": "^6.0.3", diff --git a/src/commands/ManagedEnvironmentContext.ts b/src/commands/ManagedEnvironmentContext.ts index 7214cb7c5..bc935bac8 100644 --- a/src/commands/ManagedEnvironmentContext.ts +++ b/src/commands/ManagedEnvironmentContext.ts @@ -11,3 +11,8 @@ export interface ManagedEnvironmentRequiredContext extends ISubscriptionActionCo subscription: AzureSubscription; managedEnvironment: ManagedEnvironment; } + +export interface ManagedEnvironmentContext extends ISubscriptionActionContext { + subscription: AzureSubscription; + managedEnvironment?: ManagedEnvironment; +} diff --git a/src/commands/StartingResourcesLogStep.ts b/src/commands/StartingResourcesLogStep.ts index a8df4cb9d..6d95276cf 100644 --- a/src/commands/StartingResourcesLogStep.ts +++ b/src/commands/StartingResourcesLogStep.ts @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { type ContainerApp, type ManagedEnvironment } from "@azure/arm-appcontainers"; +import { type Registry } from "@azure/arm-containerregistry"; import { type ResourceGroup } from "@azure/arm-resources"; import { LocationListStep, type ILocationWizardContext } from "@microsoft/vscode-azext-azureutils"; -import { ActivityChildItem, ActivityChildType, activityInfoIcon, AzureWizardPromptStep, createContextValue, type ExecuteActivityContext, type IActionContext } from "@microsoft/vscode-azext-utils"; +import { ActivityChildItem, ActivityChildType, activityInfoIcon, AzureWizardPromptStep, createContextValue, prependOrInsertAfterLastInfoChild, type ActivityInfoChild, type ExecuteActivityContext, type IActionContext } from "@microsoft/vscode-azext-utils"; import { activityInfoContext } from "../constants"; import { ext } from "../extensionVariables"; -import { prependOrInsertAfterLastInfoChild } from "../utils/activityUtils"; import { localize } from "../utils/localize"; type StartingResourcesLogContext = IActionContext & Partial & ILocationWizardContext & { resourceGroup?: ResourceGroup, managedEnvironment?: ManagedEnvironment, + registry?: Registry; containerApp?: ContainerApp }; @@ -26,7 +27,6 @@ const startingResourcesContext: string = 'startingResourcesLogStepItem'; */ export class StartingResourcesLogStep extends AzureWizardPromptStep { public hideStepCount: boolean = true; - protected hasLogged: boolean = false; /** * Implement if you require additional context loading before resource logging @@ -34,9 +34,6 @@ export class StartingResourcesLogStep ext protected configureStartingResources?(context: T): void | Promise; public async configureBeforePrompt(context: T): Promise { - if (this.hasLogged) { - return; - } await this.configureStartingResources?.(context); await this.logStartingResources(context); } @@ -50,30 +47,52 @@ export class StartingResourcesLogStep ext } protected async logStartingResources(context: T): Promise { + // Resource group if (context.resourceGroup) { prependOrInsertAfterLastInfoChild(context, new ActivityChildItem({ contextValue: createContextValue([startingResourcesContext, activityInfoContext]), label: localize('useResourceGroup', 'Use resource group "{0}"', context.resourceGroup.name), activityType: ActivityChildType.Info, - iconPath: activityInfoIcon - }) + iconPath: activityInfoIcon, + stepId: this.id, + }) as ActivityInfoChild, ); ext.outputChannel.appendLog(localize('usingResourceGroup', 'Using resource group "{0}".', context.resourceGroup.name)); } + context.telemetry.properties.existingResourceGroup = String(!!context.resourceGroup); + // Managed environment if (context.managedEnvironment) { prependOrInsertAfterLastInfoChild(context, new ActivityChildItem({ label: localize('useManagedEnvironment', 'Use managed environment "{0}"', context.managedEnvironment.name), contextValue: createContextValue([startingResourcesContext, activityInfoContext]), activityType: ActivityChildType.Info, - iconPath: activityInfoIcon - }), + iconPath: activityInfoIcon, + stepId: this.id, + }) as ActivityInfoChild, ); ext.outputChannel.appendLog(localize('usingManagedEnvironment', 'Using managed environment "{0}".', context.managedEnvironment.name)); } + context.telemetry.properties.existingEnvironment = String(!!context.managedEnvironment); + + // Container registry + if (context.registry) { + prependOrInsertAfterLastInfoChild(context, + new ActivityChildItem({ + label: localize('useAcr', 'Use container registry "{0}"', context.registry.name), + contextValue: createContextValue([startingResourcesContext, activityInfoContext]), + activityType: ActivityChildType.Info, + iconPath: activityInfoIcon, + stepId: this.id, + }) as ActivityInfoChild, + ); + ext.outputChannel.appendLog(localize('usingAcr', 'Using Azure Container Registry "{0}".', context.registry.name)); + } + context.telemetry.properties.existingRegistry = String(!!context.registry); + // Container app if (context.containerApp) { prependOrInsertAfterLastInfoChild(context, new ActivityChildItem({ @@ -81,16 +100,18 @@ export class StartingResourcesLogStep ext contextValue: createContextValue([startingResourcesContext, activityInfoContext]), activityType: ActivityChildType.Info, iconPath: activityInfoIcon, - }), + stepId: this.id, + }) as ActivityInfoChild, ); ext.outputChannel.appendLog(localize('usingContainerApp', 'Using container app "{0}".', context.containerApp.name)); } + context.telemetry.properties.existingContainerApp = String(!!context.containerApp); + // Location if (LocationListStep.hasLocation(context)) { const location: string = (await LocationListStep.getLocation(context)).name; ext.outputChannel.appendLog(localize('usingLocation', 'Using location: "{0}".', location)); } - - this.hasLogged = true; + context.telemetry.properties.existingLocation = String(!!LocationListStep.hasLocation(context)); } } diff --git a/src/commands/api/CHANGELOG.md b/src/commands/api/CHANGELOG.md index 190c5d386..8187c0088 100644 --- a/src/commands/api/CHANGELOG.md +++ b/src/commands/api/CHANGELOG.md @@ -1,20 +1,21 @@ # Change Log -### 0.0.3 +## 1.0.0 +### Changed +* [[961](https://github.com/microsoft/vscode-azurecontainerapps/pull/961)] Add a new resource location parameter to the `deployWorkspaceProjectApi` definition. If no location is provided, try to infer location via other provided resources. +* [[961](https://github.com/microsoft/vscode-azurecontainerapps/pull/961)] Suppress registry prompting by default and remove associated flag (`suppressRegistryPrompt`) +## 0.0.3 ### Added * [[817]](https://github.com/microsoft/vscode-azurecontainerapps/pull/817) Added an API entry-point and compat wrapper for existing `deployImageApi` command ### Changed * [[816]](https://github.com/microsoft/vscode-azurecontainerapps/pull/816) Added backward compatibility to ensure existing functionality remains unaffected by new managed identity features. -### 0.0.2 - +## 0.0.2 ### Changed * [[615]](https://github.com/microsoft/vscode-azurecontainerapps/pull/615) Removed ability to set option `ignoreExistingDeploySettings`. This will now happen automatically by default. ## 0.0.1 -* Initial release - ### Added * [[578]](https://github.com/microsoft/vscode-azurecontainerapps/pull/578) Added an API entry-point to the `deployWorkspaceProject` command diff --git a/src/commands/api/deployWorkspaceProjectApi.ts b/src/commands/api/deployWorkspaceProjectApi.ts index 6e4f49171..0538cbbd6 100644 --- a/src/commands/api/deployWorkspaceProjectApi.ts +++ b/src/commands/api/deployWorkspaceProjectApi.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { type ResourceGroup } from "@azure/arm-resources"; -import { ResourceGroupListStep, parseAzureResourceGroupId } from "@microsoft/vscode-azext-azureutils"; -import { callWithTelemetryAndErrorHandling, createSubscriptionContext, subscriptionExperience, type IActionContext, type ISubscriptionActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; +import { LocationListStep, ResourceGroupListStep, parseAzureResourceGroupId } from "@microsoft/vscode-azext-azureutils"; +import { callWithTelemetryAndErrorHandling, createSubscriptionContext, nonNullValueAndProp, subscriptionExperience, type IActionContext, type ISubscriptionActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; import { Uri, type WorkspaceFolder } from "vscode"; import { ext } from "../../extensionVariables"; @@ -22,10 +22,10 @@ export async function deployWorkspaceProjectApi(deployWorkspaceProjectOptions: a return await callWithTelemetryAndErrorHandling('containerApps.api.deployWorkspaceProject', async (context: IActionContext): Promise => { const { resourceGroupId, + location, rootPath, dockerfilePath, srcPath, - suppressRegistryPrompt, suppressConfirmation, suppressContainerAppCreation, shouldSaveDeploySettings @@ -52,10 +52,15 @@ export async function deployWorkspaceProjectApi(deployWorkspaceProjectOptions: a shouldSaveDeploySettings: !!shouldSaveDeploySettings, }); + if (location || deployWorkspaceProjectInternalContext.resourceGroup) { + const autoSelectLocation = location ?? nonNullValueAndProp(deployWorkspaceProjectInternalContext.resourceGroup, 'location'); + await LocationListStep.setAutoSelectLocation(deployWorkspaceProjectInternalContext, autoSelectLocation); + } + const deployWorkspaceProjectContext: DeployWorkspaceProjectContext = await deployWorkspaceProjectInternal(deployWorkspaceProjectInternalContext, { + advancedCreate: false, suppressActivity: true, suppressConfirmation, - suppressRegistryPrompt: suppressRegistryPrompt ?? true, suppressContainerAppCreation, suppressProgress: true, suppressWizardTitle: true, diff --git a/src/commands/api/getAzureContainerAppsApiProvider.ts b/src/commands/api/getAzureContainerAppsApiProvider.ts index 7e127d804..055cd5cd1 100644 --- a/src/commands/api/getAzureContainerAppsApiProvider.ts +++ b/src/commands/api/getAzureContainerAppsApiProvider.ts @@ -10,9 +10,7 @@ import type * as api from "./vscode-azurecontainerapps.api"; export function getAzureContainerAppsApiProvider(): apiUtils.AzureExtensionApiProvider { return createApiProvider([{ - // Todo: Change this to 0.0.3 later. 0.0.3 is backwards compatible anyway so this change should be fine either way. - // For some reason it's causing a block on Function side, so just keep it at 0.0.1 until we figure out why - apiVersion: '0.0.1', + apiVersion: '1.0.0', deployImage: deployImageApi, deployWorkspaceProject: deployWorkspaceProjectApi, }]); diff --git a/src/commands/api/vscode-azurecontainerapps.api.d.ts b/src/commands/api/vscode-azurecontainerapps.api.d.ts index bcfb48cd2..28b950f3b 100644 --- a/src/commands/api/vscode-azurecontainerapps.api.d.ts +++ b/src/commands/api/vscode-azurecontainerapps.api.d.ts @@ -22,6 +22,7 @@ export interface DeployWorkspaceProjectOptionsContract { // Existing resources subscriptionId?: string; resourceGroupId?: string; + location?: string; // Workspace deployment paths (absolute fs path) rootPath?: string; @@ -29,7 +30,6 @@ export interface DeployWorkspaceProjectOptionsContract { dockerfilePath?: string; // Options - suppressRegistryPrompt?: boolean; suppressConfirmation?: boolean; // Suppress any [resource] confirmation prompts suppressContainerAppCreation?: boolean; shouldSaveDeploySettings?: boolean; diff --git a/src/commands/createContainerApp/ContainerAppCreateStep.ts b/src/commands/createContainerApp/ContainerAppCreateStep.ts index eaea62d8d..b61e3a366 100644 --- a/src/commands/createContainerApp/ContainerAppCreateStep.ts +++ b/src/commands/createContainerApp/ContainerAppCreateStep.ts @@ -7,7 +7,7 @@ import { KnownActiveRevisionsMode, type ContainerAppsAPIClient, type Ingress } f import { LocationListStep } from "@microsoft/vscode-azext-azureutils"; import { AzureWizardExecuteStepWithActivityOutput, nonNullProp, nonNullValueAndProp, type AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; import { type Progress } from "vscode"; -import { containerAppsWebProvider, ImageSource } from "../../constants"; +import { ImageSource } from "../../constants"; import { ContainerAppItem } from "../../tree/ContainerAppItem"; import { createContainerAppsAPIClient } from "../../utils/azureClients"; import { localize } from "../../utils/localize"; @@ -37,7 +37,7 @@ export class ContainerAppCreateStep extends } : undefined; context.containerApp = ContainerAppItem.CreateContainerAppModel(await appClient.containerApps.beginCreateOrUpdateAndWait(resourceGroupName, containerAppName, { - location: (await LocationListStep.getLocation(context, containerAppsWebProvider)).name, + location: (await LocationListStep.getLocation(context)).name, managedEnvironmentId: context.managedEnvironment?.id, configuration: { ingress, diff --git a/src/commands/createContainerApp/ContainerAppListStep.ts b/src/commands/createContainerApp/ContainerAppListStep.ts new file mode 100644 index 000000000..af0ff68cd --- /dev/null +++ b/src/commands/createContainerApp/ContainerAppListStep.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerApp, type ContainerAppsAPIClient } from "@azure/arm-appcontainers"; +import { LocationListStep, parseAzureResourceId, ResourceGroupListStep, uiUtils } from "@microsoft/vscode-azext-azureutils"; +import { AzureWizardPromptStep, nonNullProp, type AzureWizardExecuteStep, type IAzureQuickPickItem, type IWizardOptions } from "@microsoft/vscode-azext-utils"; +import { containerAppProvider, containerAppResourceType } from "../../constants"; +import { ContainerAppItem } from "../../tree/ContainerAppItem"; +import { createContainerAppsAPIClient } from "../../utils/azureClients"; +import { localize } from "../../utils/localize"; +import { ContainerAppUpdateStep } from "../image/imageSource/ContainerAppUpdateStep"; +import { type ContainerAppCreateContext } from "./ContainerAppCreateContext"; +import { ContainerAppCreateStep } from "./ContainerAppCreateStep"; +import { ContainerAppNameStep } from "./ContainerAppNameStep"; + +export type ContainerAppListStepOptions = { + /** + * For existing container apps, automatically add subwizard steps to update + */ + updateIfExists?: boolean; + /** + * Provide a custom strategy for updating the list of container app picks. + * Can be used to inject custom sorting, grouping, filtering, etc. + */ + pickUpdateStrategy?: ContainerAppPickUpdateStrategy; +}; + +export interface ContainerAppPickUpdateStrategy { + updatePicks(context: Partial, picks: IAzureQuickPickItem[]): void | Promise; +} + +export class ContainerAppListStep extends AzureWizardPromptStep { + constructor(readonly options: ContainerAppListStepOptions = {}) { + super(); + } + + public async prompt(context: T): Promise { + const containerAppPicks: IAzureQuickPickItem[] = await this.getPicks(context); + const picks: IAzureQuickPickItem[] = await this.options.pickUpdateStrategy?.updatePicks(context, containerAppPicks) ?? containerAppPicks; + + picks.unshift({ + label: localize('newContainerApp', '$(plus) Create new container app'), + data: undefined, + }); + + const containerApp: ContainerApp | undefined = (await context.ui.showQuickPick(picks, { + placeHolder: localize('selectContainerApp', 'Select a container app'), + suppressPersistence: true, + })).data; + + if (containerApp) { + context.containerApp = ContainerAppItem.CreateContainerAppModel(containerApp); + } + } + + public shouldPrompt(context: T): boolean { + return !context.containerApp && !context.newContainerAppName; + } + + private async getPicks(context: T): Promise[]> { + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient(context); + + let containerApps: ContainerApp[] = []; + if (context.resourceGroup) { + containerApps = await uiUtils.listAllIterator(client.containerApps.listByResourceGroup(nonNullProp(context.resourceGroup, 'name'))); + } else if (context.newResourceGroupName) { + containerApps = []; + } else { + containerApps = await uiUtils.listAllIterator(client.containerApps.listBySubscription()); + } + + if (context.managedEnvironment) { + containerApps = containerApps.filter(ca => ca.managedEnvironmentId === context.managedEnvironment.id); + } + + return containerApps.map(ca => { + return { + label: nonNullProp(ca, 'name'), + description: parseAzureResourceId(nonNullProp(ca, 'id')).resourceGroup, + data: ca, + }; + }); + } + + public async getSubWizard(context: T): Promise | undefined> { + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; + + // Create + if (!context.containerApp) { + promptSteps.push(new ContainerAppNameStep()); + + LocationListStep.addProviderForFiltering(context, containerAppProvider, containerAppResourceType); + + if (!context.resourceGroup) { + promptSteps.push(new ResourceGroupListStep()); + } + + LocationListStep.addStep(context, promptSteps); + + executeSteps.push(new ContainerAppCreateStep()); + } + + // Update + if (context.containerApp && this.options.updateIfExists) { + executeSteps.push(new ContainerAppUpdateStep()); + } + + return { promptSteps, executeSteps }; + } +} diff --git a/src/commands/createContainerApp/ContainerAppNameStep.ts b/src/commands/createContainerApp/ContainerAppNameStep.ts index 1045f0244..2f5a97633 100644 --- a/src/commands/createContainerApp/ContainerAppNameStep.ts +++ b/src/commands/createContainerApp/ContainerAppNameStep.ts @@ -42,7 +42,12 @@ export class ContainerAppNameStep extends AzureWizardPromptStep { - const resourceGroupName: string = getResourceGroupFromId(nonNullValueAndProp(context.managedEnvironment, 'id')); + if (!context.managedEnvironment) { + // If there's no managed environment, a new one will be created and the container app name will be unique + return undefined; + } + + const resourceGroupName: string = context.resourceGroup?.name ?? getResourceGroupFromId(nonNullValueAndProp(context.managedEnvironment, 'id')); if (!await ContainerAppNameStep.isNameAvailable(context, resourceGroupName, name)) { return localize('containerAppExists', 'The container app "{0}" already exists in resource group "{1}".', name, resourceGroupName); } diff --git a/src/commands/createManagedEnvironment/LogAnalyticsListStep.ts b/src/commands/createManagedEnvironment/LogAnalyticsListStep.ts index 117a4ae37..1828a5ad0 100644 --- a/src/commands/createManagedEnvironment/LogAnalyticsListStep.ts +++ b/src/commands/createManagedEnvironment/LogAnalyticsListStep.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type Workspace } from '@azure/arm-operationalinsights'; +import { type OperationalInsightsManagementClient, type Workspace } from '@azure/arm-operationalinsights'; import { uiUtils } from '@microsoft/vscode-azext-azureutils'; -import { AzureWizardPromptStep, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; +import { AzureWizardPromptStep, type IAzureQuickPickItem, type ISubscriptionActionContext } from '@microsoft/vscode-azext-utils'; import { createOperationalInsightsManagementClient } from '../../utils/azureClients'; import { localize } from '../../utils/localize'; import { nonNullProp } from '../../utils/nonNull'; @@ -30,11 +30,14 @@ export class LogAnalyticsListStep extends AzureWizardPromptStep { return { label: nonNullProp(ws, 'name'), data: ws } })); } + + static async getLogAnalyticsWorkspaces(context: ISubscriptionActionContext): Promise { + const client: OperationalInsightsManagementClient = await createOperationalInsightsManagementClient(context); + return await uiUtils.listAllIterator(client.workspaces.list()); + } } diff --git a/src/commands/createManagedEnvironment/ManagedEnvironmentCreateStep.ts b/src/commands/createManagedEnvironment/ManagedEnvironmentCreateStep.ts index 2ba83c548..96e9ff02a 100644 --- a/src/commands/createManagedEnvironment/ManagedEnvironmentCreateStep.ts +++ b/src/commands/createManagedEnvironment/ManagedEnvironmentCreateStep.ts @@ -7,7 +7,7 @@ import { type ContainerAppsAPIClient } from "@azure/arm-appcontainers"; import { getResourceGroupFromId, LocationListStep } from "@microsoft/vscode-azext-azureutils"; import { AzureWizardExecuteStepWithActivityOutput } from "@microsoft/vscode-azext-utils"; import { type Progress } from "vscode"; -import { managedEnvironmentsAppProvider } from "../../constants"; +import { managedEnvironmentResourceType } from "../../constants"; import { createContainerAppsAPIClient, createOperationalInsightsManagementClient } from '../../utils/azureClients'; import { localize } from "../../utils/localize"; import { nonNullProp, nonNullValueAndProp } from "../../utils/nonNull"; @@ -58,7 +58,7 @@ export class ManagedEnvironmentCreateStep; + +export interface ManagedEnvironmentPickUpdateStrategy { + updatePicks(context: ManagedEnvironmentCreateContext | Partial, picks: ManagedEnvironmentPick[]): ManagedEnvironmentPick[] | Promise; +} + +export class ManagedEnvironmentListStep extends AzureWizardPromptStep { + constructor(readonly options: ManagedEnvironmentListStepOptions = {}) { + super(); + } + + public async prompt(context: T): Promise { + const environmentPicks: ManagedEnvironmentPick[] = await this.getPicks(context); + const picks: IAzureQuickPickItem[] = await this.options.pickUpdateStrategy?.updatePicks(context, environmentPicks) ?? environmentPicks; + + picks.unshift({ + label: localize('newManagedEnvironment', '$(plus) Create new container apps environment'), + data: undefined, + }); + + context.managedEnvironment = (await context.ui.showQuickPick(picks, { + placeHolder: localize('selectManagedEnvironment', 'Select a container apps environment'), + enableGrouping: true, + suppressPersistence: true, + })).data; + } + + public shouldPrompt(context: T): boolean { + return !context.managedEnvironment && !context.newManagedEnvironmentName; + } + + private async getPicks(context: T): Promise { + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient(context); + + let managedEnvironments: ManagedEnvironment[]; + if (context.resourceGroup) { + managedEnvironments = await uiUtils.listAllIterator(client.managedEnvironments.listByResourceGroup(nonNullProp(context.resourceGroup, 'name'))); + } else if (context.newResourceGroupName) { + managedEnvironments = []; + } else { + managedEnvironments = await uiUtils.listAllIterator(client.managedEnvironments.listBySubscription()); + } + + return managedEnvironments.map(env => { + return { + label: nonNullProp(env, 'name'), + data: env, + }; + }); + } + + public async getSubWizard(context: T): Promise | undefined> { + if (context.managedEnvironment) { + await ManagedEnvironmentListStep.populateContextWithRelatedResources(context, context.managedEnvironment); + return undefined; + } + + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; + + promptSteps.push(new ManagedEnvironmentNameStep()); + + LocationListStep.addProviderForFiltering(context, managedEnvironmentProvider, managedEnvironmentResourceType); + LocationListStep.addProviderForFiltering(context, logAnalyticsProvider, logAnalyticsResourceType); + + if (!context.resourceGroup) { + promptSteps.push(new ResourceGroupListStep()); + } + + LocationListStep.addStep(context, promptSteps); + + executeSteps.push( + new LogAnalyticsCreateStep(), + new ManagedEnvironmentCreateStep(), + ); + + return { promptSteps, executeSteps }; + } + + static async getManagedEnvironmentsBySubscription(context: ISubscriptionActionContext): Promise { + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient(context); + return await uiUtils.listAllIterator(client.managedEnvironments.listBySubscription()); + } + + static async populateContextWithRelatedResources(context: ManagedEnvironmentContext & { resourceGroup?: ResourceGroup; logAnalyticsWorkspace?: Workspace }, managedEnvironment: ManagedEnvironment): Promise { + if (!context.resourceGroup) { + const resourceGroups: ResourceGroup[] = await ResourceGroupListStep.getResourceGroups(context); + context.resourceGroup = resourceGroups.find(rg => rg.name === getResourceGroupFromId(nonNullProp(managedEnvironment, 'id'))); + } + if (!context.logAnalyticsWorkspace) { + const workspaces: Workspace[] = await LogAnalyticsListStep.getLogAnalyticsWorkspaces(context); + context.logAnalyticsWorkspace = workspaces.find(w => w.customerId && w.customerId === managedEnvironment?.appLogsConfiguration?.logAnalyticsConfiguration?.customerId); + } + await LocationListStep.setAutoSelectLocation(context, managedEnvironment.location); + context.managedEnvironment = managedEnvironment; + } +} diff --git a/src/commands/createManagedEnvironment/ManagedEnvironmentNameStep.ts b/src/commands/createManagedEnvironment/ManagedEnvironmentNameStep.ts index 1b212f7b3..a3d651c32 100644 --- a/src/commands/createManagedEnvironment/ManagedEnvironmentNameStep.ts +++ b/src/commands/createManagedEnvironment/ManagedEnvironmentNameStep.ts @@ -14,6 +14,7 @@ export class ManagedEnvironmentNameStep extends AzureWizardPromptStep this.validateNameAvailable(context, name) })).trim(); diff --git a/src/commands/createManagedEnvironment/createManagedEnvironment.ts b/src/commands/createManagedEnvironment/createManagedEnvironment.ts index ec0dc03c7..9be08b652 100644 --- a/src/commands/createManagedEnvironment/createManagedEnvironment.ts +++ b/src/commands/createManagedEnvironment/createManagedEnvironment.ts @@ -7,7 +7,7 @@ import { type ManagedEnvironment } from "@azure/arm-appcontainers"; import { LocationListStep, ResourceGroupCreateStep } from "@microsoft/vscode-azext-azureutils"; import { AzureWizard, createSubscriptionContext, nonNullProp, subscriptionExperience, type AzureWizardExecuteStep, type AzureWizardPromptStep, type IActionContext } from "@microsoft/vscode-azext-utils"; import { type AzureSubscription } from "@microsoft/vscode-azureresources-api"; -import { appProvider, managedEnvironmentsId } from "../../constants"; +import { logAnalyticsProvider, logAnalyticsResourceType, managedEnvironmentProvider, managedEnvironmentResourceType } from "../../constants"; import { ext } from "../../extensionVariables"; import { createActivityContext } from "../../utils/activityUtils"; import { getVerifyProvidersStep } from "../../utils/getVerifyProvidersStep"; @@ -38,7 +38,9 @@ export async function createManagedEnvironment(context: IActionContext, node?: { new LogAnalyticsCreateStep(), new ManagedEnvironmentCreateStep() ); - LocationListStep.addProviderForFiltering(wizardContext, appProvider, managedEnvironmentsId); + + LocationListStep.addProviderForFiltering(wizardContext, managedEnvironmentProvider, managedEnvironmentResourceType); + LocationListStep.addProviderForFiltering(wizardContext, logAnalyticsProvider, logAnalyticsResourceType); LocationListStep.addStep(wizardContext, promptSteps); const wizard: AzureWizard = new AzureWizard(wizardContext, { diff --git a/src/commands/deployWorkspaceProject/deployWorkspaceProject.ts b/src/commands/deployWorkspaceProject/deployWorkspaceProject.ts index 4acf92baf..6a7b96926 100644 --- a/src/commands/deployWorkspaceProject/deployWorkspaceProject.ts +++ b/src/commands/deployWorkspaceProject/deployWorkspaceProject.ts @@ -11,7 +11,6 @@ import { ext } from "../../extensionVariables"; import { type SetTelemetryProps } from "../../telemetry/SetTelemetryProps"; import { type DeployWorkspaceProjectNotificationTelemetryProps as NotificationTelemetryProps } from "../../telemetry/deployWorkspaceProjectTelemetryProps"; import { ContainerAppItem, isIngressEnabled, type ContainerAppModel } from "../../tree/ContainerAppItem"; -import { ManagedEnvironmentItem } from "../../tree/ManagedEnvironmentItem"; import { localize } from "../../utils/localize"; import { type IContainerAppContext } from "../IContainerAppContext"; import { type DeployWorkspaceProjectResults } from "../api/vscode-azurecontainerapps.api"; @@ -19,6 +18,7 @@ import { browseContainerApp } from "../browseContainerApp"; import { type DeployWorkspaceProjectContext } from "./DeployWorkspaceProjectContext"; import { type DeploymentConfiguration } from "./deploymentConfiguration/DeploymentConfiguration"; import { getTreeItemDeploymentConfiguration } from "./deploymentConfiguration/getTreeItemDeploymentConfiguration"; +import { DeploymentMode } from "./deploymentConfiguration/workspace/DeploymentModeListStep"; import { getWorkspaceDeploymentConfiguration } from "./deploymentConfiguration/workspace/getWorkspaceDeploymentConfiguration"; import { formatSectionHeader } from "./formatSectionHeader"; import { getDeployWorkspaceProjectResults } from "./getDeployWorkspaceProjectResults"; @@ -28,9 +28,9 @@ import { convertV1ToV2SettingsSchema } from "./settings/convertSettings/convertV export const deployWorkspaceProjectCommandName: string = localize('deployWorkspaceProject', 'Deploy Project from Workspace...'); -export async function deployWorkspaceProject(context: IActionContext & Partial, item?: ContainerAppItem | ManagedEnvironmentItem): Promise { +export async function deployWorkspaceProject(context: IActionContext & Partial, item?: ContainerAppItem): Promise { // If an incompatible tree item is passed, treat it as if no item was passed - if (item && !ContainerAppItem.isContainerAppItem(item) && !ManagedEnvironmentItem.isManagedEnvironmentItem(item)) { + if (item && !ContainerAppItem.isContainerAppItem(item)) { item = undefined; } @@ -55,12 +55,14 @@ export async function deployWorkspaceProject(context: IActionContext & Partial { +export async function getTreeItemDeploymentConfiguration(context: IContainerAppContext, item: ContainerAppItem): Promise { const wizardContext: TreeItemDeploymentConfigurationContext = context; const wizard: AzureWizard = new AzureWizard(wizardContext, { @@ -23,13 +26,19 @@ export async function getTreeItemDeploymentConfiguration(context: IContainerAppC await wizard.execute(); return { + ...await getContainerAppsResources(context, item), + deploymentMode: DeploymentMode.Advanced, rootFolder: wizardContext.rootFolder, - managedEnvironment: ManagedEnvironmentItem.isManagedEnvironmentItem(item) ? (item as ManagedEnvironmentItem).managedEnvironment : undefined, - containerApp: ContainerAppItem.isContainerAppItem(item) ? (item as ContainerAppItem).containerApp : undefined, registry: wizardContext.registry, - - // If it's a container app item, safe to assume it's a re-deployment, so don't re-prompt to save - // If it's anything else, it's a first-time deployment, so it makes sense to ask to save - shouldSaveDeploySettings: ContainerAppItem.isContainerAppItem(item) ? false : undefined + shouldSaveDeploySettings: false, }; } + +async function getContainerAppsResources(context: IContainerAppContext, item: ContainerAppItem): Promise { + const containerAppsResources: ContainerAppsResources = await getResourcesFromContainerAppHelper(context, item.containerApp); + + const workspaces: Workspace[] = await LogAnalyticsListStep.getLogAnalyticsWorkspaces(context); + const logAnalyticsWorkspace: Workspace | undefined = workspaces.find(w => w.customerId && w.customerId === containerAppsResources.managedEnvironment?.appLogsConfiguration?.logAnalyticsConfiguration?.customerId); + + return Object.assign(containerAppsResources, { logAnalyticsWorkspace }); +} diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/DeploymentModeListStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/DeploymentModeListStep.ts new file mode 100644 index 000000000..acf8c623f --- /dev/null +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/DeploymentModeListStep.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; +import { localize } from "../../../../utils/localize"; +import { type WorkspaceDeploymentConfigurationContext } from "./WorkspaceDeploymentConfigurationContext"; + +export enum DeploymentMode { + Basic = 'basic', + Advanced = 'advanced', +} + +export class DeploymentModeListStep extends AzureWizardPromptStep { + public async prompt(context: WorkspaceDeploymentConfigurationContext): Promise { + const picks: IAzureQuickPickItem[] = [ + { + label: localize('basic', 'Basic'), + detail: localize('basicDetails', 'Deploy a basic app with minimal setup. Automatically creates all required resources.'), + data: DeploymentMode.Basic, + }, + { + label: localize('advanced', 'Advanced'), + detail: localize('advancedDetails', 'Deploy an advanced app with greater customizations. Use for existing container apps.'), + data: DeploymentMode.Advanced, + }, + ]; + + const pick = await context.ui.showQuickPick(picks, { + placeHolder: localize('selectWorkspaceProjectDeploymentMode', 'Select the workspace project deployment mode'), + suppressPersistence: true, + }); + + context.deploymentMode = pick.data; + context.telemetry.properties.deploymentMode = pick.data; + } + + public shouldPrompt(context: WorkspaceDeploymentConfigurationContext): boolean { + return !context.deploymentConfigurationSettings && !context.deploymentMode; + } +} diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvValidateStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvValidateStep.ts index 3333be3fa..cdc527726 100644 --- a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvValidateStep.ts +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvValidateStep.ts @@ -74,8 +74,6 @@ export class EnvValidateStep }; } - // Todo: Verify if we need progress output - public createFailOutput(): ExecuteActivityOutput { return { item: new ActivityChildItem({ diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/FilePathsVerifyStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/FilePathsVerifyStep.ts index f5a376045..d15212ded 100644 --- a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/FilePathsVerifyStep.ts +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/FilePathsVerifyStep.ts @@ -64,8 +64,6 @@ export abstract class FilePathsVerifyStep extends AzureWizardExecuteStep { @@ -23,6 +24,7 @@ export async function getWorkspaceDeploymentConfiguration(context: IContainerApp promptSteps: [ new RootFolderStep(), new DeploymentConfigurationListStep(), + new DeploymentModeListStep(), ], }); @@ -39,6 +41,7 @@ export async function getWorkspaceDeploymentConfiguration(context: IContainerApp await wizard.execute(); return { + deploymentMode: wizardContext.deploymentMode, configurationIdx: wizardContext.configurationIdx, rootFolder: wizardContext.rootFolder, dockerfilePath: wizardContext.dockerfilePath, diff --git a/src/commands/deployWorkspaceProject/internal/ManagedEnvironmentSortByLocalSettingsStrategy.ts b/src/commands/deployWorkspaceProject/internal/ManagedEnvironmentSortByLocalSettingsStrategy.ts new file mode 100644 index 000000000..237d27434 --- /dev/null +++ b/src/commands/deployWorkspaceProject/internal/ManagedEnvironmentSortByLocalSettingsStrategy.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type ContainerApp, type ContainerAppsAPIClient } from "@azure/arm-appcontainers"; +import { parseAzureResourceId } from "@microsoft/vscode-azext-azureutils"; +import { nonNullProp } from "@microsoft/vscode-azext-utils"; +import { createContainerAppsAPIClient } from "../../../utils/azureClients"; +import { localize } from "../../../utils/localize"; +import { type ManagedEnvironmentPick, type ManagedEnvironmentPickUpdateStrategy } from "../../createManagedEnvironment/ManagedEnvironmentListStep"; +import { type DeploymentConfigurationSettings } from "../settings/DeployWorkspaceProjectSettingsV2"; +import { dwpSettingUtilsV2 } from "../settings/dwpSettingUtilsV2"; +import { type DeployWorkspaceProjectInternalContext } from "./DeployWorkspaceProjectInternalContext"; + +/** + * Sort and recommend managed environments that are used in local settings deployment configurations. + */ +export class ManagedEnvironmentSortByLocalSettingsStrategy implements ManagedEnvironmentPickUpdateStrategy { + async updatePicks(context: T, picks: ManagedEnvironmentPick[]): Promise { + const deploymentConfigurations: DeploymentConfigurationSettings[] | undefined = await dwpSettingUtilsV2.getWorkspaceDeploymentConfigurations(nonNullProp(context, 'rootFolder')); + if (!deploymentConfigurations?.length) { + return picks; + } + + const asyncTasks: Promise[] = []; + const recommendedEnvironmentIds: Set = new Set(); + + const client = await createContainerAppsAPIClient(context); + for (const config of deploymentConfigurations) { + if (!config.resourceGroup || !config.containerApp) { + continue; + } + asyncTasks.push( + (async () => { + const id: string | undefined = await getManagedEnvironmentId(client, nonNullProp(config, 'resourceGroup'), nonNullProp(config, 'containerApp')); + if (id) { + recommendedEnvironmentIds.add(id); + } + })(), + ); + } + + await Promise.allSettled(asyncTasks); + + const recommendedPicks: ManagedEnvironmentPick[] = []; + const otherPicks: ManagedEnvironmentPick[] = []; + for (const p of picks) { + const id: string = nonNullProp(p.data, 'id'); + p.description = parseAzureResourceId(id).resourceGroup; + + if (recommendedEnvironmentIds.has(id)) { + p.group = localize('localSettings', 'Local Settings'); + recommendedPicks.push(p); + } else { + p.group = localize('other', 'Other'); + otherPicks.push(p); + } + } + + context.telemetry.properties.recommendedEnvCount = String(recommendedPicks.length); + + return [...recommendedPicks, ...otherPicks]; + } +} + +async function getManagedEnvironmentId(client: ContainerAppsAPIClient, resourceGroupName: string, containerAppName: string): Promise { + try { + const containerApp: ContainerApp = await client.containerApps.get(resourceGroupName, containerAppName); + return containerApp.managedEnvironmentId; + } + catch (these_hands) { + return undefined; + } +} diff --git a/src/commands/deployWorkspaceProject/internal/SharedResourcesNameStep.ts b/src/commands/deployWorkspaceProject/internal/SharedResourcesNameStep.ts index c8d41c828..5a24f1e9e 100644 --- a/src/commands/deployWorkspaceProject/internal/SharedResourcesNameStep.ts +++ b/src/commands/deployWorkspaceProject/internal/SharedResourcesNameStep.ts @@ -13,7 +13,7 @@ import { RegistryNameStep } from "../../image/imageSource/containerRegistry/acr/ import { type DeployWorkspaceProjectInternalContext } from "./DeployWorkspaceProjectInternalContext"; import { sanitizeResourceName } from "./sanitizeResourceName"; -/** Names any app environment shared resources: `resource group`, `managed environment`, `container registry` */ +/** Names any app environment shared resources: `resource group`, `managed environment`, `log analytics workspace`, `container registry` */ export class SharedResourcesNameStep extends AzureWizardPromptStep { public async configureBeforePrompt(context: DeployWorkspaceProjectInternalContext): Promise { if ((context.resourceGroup || context.managedEnvironment) && !context.registry) { @@ -24,7 +24,7 @@ export class SharedResourcesNameStep extends AzureWizardPromptStep { const resourceName: string = (await context.ui.showInputBox({ - prompt: localize('sharedNamePrompt', 'Enter a name for the container app environment'), + prompt: localize('sharedNamePrompt', 'Enter a name for new shared environment resources'), value: sanitizeResourceName(context.rootFolder?.name ?? ''), validateInput: (name: string) => this.validateInput(context, name), asyncValidationTask: (name: string) => this.validateNameAvailability(context, name) diff --git a/src/commands/deployWorkspaceProject/internal/deployWorkspaceProjectInternal.ts b/src/commands/deployWorkspaceProject/internal/deployWorkspaceProjectInternal.ts index a6b1677fe..a2e894545 100644 --- a/src/commands/deployWorkspaceProject/internal/deployWorkspaceProjectInternal.ts +++ b/src/commands/deployWorkspaceProject/internal/deployWorkspaceProjectInternal.ts @@ -4,41 +4,49 @@ *--------------------------------------------------------------------------------------------*/ import { KnownActiveRevisionsMode } from "@azure/arm-appcontainers"; -import { LocationListStep, ResourceGroupCreateStep } from "@microsoft/vscode-azext-azureutils"; -import { ActivityChildItem, ActivityChildType, AzureWizard, activityInfoContext, activityInfoIcon, nonNullValueAndProp, type AzureWizardExecuteStep, type AzureWizardPromptStep, type ExecuteActivityContext } from "@microsoft/vscode-azext-utils"; +import { LocationListStep, ResourceGroupCreateStep, ResourceGroupListStep } from "@microsoft/vscode-azext-azureutils"; +import { AzureWizard, type AzureWizardExecuteStep, type AzureWizardPromptStep, type ExecuteActivityContext } from "@microsoft/vscode-azext-utils"; import { ProgressLocation, window } from "vscode"; -import { appProvider, managedEnvironmentsId } from "../../../constants"; +import { containerAppProvider, containerAppResourceType, logAnalyticsProvider, logAnalyticsResourceType, managedEnvironmentProvider, managedEnvironmentResourceType, registryProvider, registryResourceType } from "../../../constants"; import { ext } from "../../../extensionVariables"; -import { createActivityContext, prependOrInsertAfterLastInfoChild } from "../../../utils/activityUtils"; +import { createActivityContext } from "../../../utils/activityUtils"; import { getVerifyProvidersStep } from "../../../utils/getVerifyProvidersStep"; import { localize } from "../../../utils/localize"; import { CommandAttributes } from "../../CommandAttributes"; import { ContainerAppCreateStep } from "../../createContainerApp/ContainerAppCreateStep"; +import { ContainerAppListStep } from "../../createContainerApp/ContainerAppListStep"; import { LogAnalyticsCreateStep } from "../../createManagedEnvironment/LogAnalyticsCreateStep"; import { ManagedEnvironmentCreateStep } from "../../createManagedEnvironment/ManagedEnvironmentCreateStep"; +import { ManagedEnvironmentListStep } from "../../createManagedEnvironment/ManagedEnvironmentListStep"; import { editContainerCommandName } from "../../editContainer/editContainer"; +import { RootFolderStep } from "../../image/imageSource/buildImageInAzure/RootFolderStep"; import { ContainerAppUpdateStep } from "../../image/imageSource/ContainerAppUpdateStep"; +import { AcrDefaultSortStrategy } from "../../image/imageSource/containerRegistry/acr/AcrDefaultSortStrategy"; +import { AcrListStep } from "../../image/imageSource/containerRegistry/acr/AcrListStep"; +import { RegistryCreateStep } from "../../image/imageSource/containerRegistry/acr/createAcr/RegistryCreateStep"; import { ImageSourceListStep } from "../../image/imageSource/ImageSourceListStep"; import { IngressPromptStep } from "../../ingress/IngressPromptStep"; +import { StartingResourcesLogStep } from "../../StartingResourcesLogStep"; import { deployWorkspaceProjectCommandName } from "../deployWorkspaceProject"; import { formatSectionHeader } from "../formatSectionHeader"; import { AppResourcesNameStep } from "./AppResourcesNameStep"; import { DeployWorkspaceProjectConfirmStep } from "./DeployWorkspaceProjectConfirmStep"; import { type DeployWorkspaceProjectInternalContext } from "./DeployWorkspaceProjectInternalContext"; import { DeployWorkspaceProjectSaveSettingsStep } from "./DeployWorkspaceProjectSaveSettingsStep"; +import { getStartingConfiguration } from "./getStartingConfiguration/getStartingConfiguration"; +import { ManagedEnvironmentSortByLocalSettingsStrategy } from "./ManagedEnvironmentSortByLocalSettingsStrategy"; import { SharedResourcesNameStep } from "./SharedResourcesNameStep"; import { ShouldSaveDeploySettingsPromptStep } from "./ShouldSaveDeploySettingsPromptStep"; -import { getStartingConfiguration } from "./startingConfiguration/getStartingConfiguration"; export interface DeployWorkspaceProjectInternalOptions { /** - * Suppress showing the wizard execution through the activity log + * Set to offer advanced creation prompts */ - suppressActivity?: boolean; + advancedCreate?: boolean; /** - * Suppress registry selection prompt + * Suppress showing the wizard execution through the activity log */ - suppressRegistryPrompt?: boolean; + suppressActivity?: boolean; /** * Suppress any [resource] confirmation prompts */ @@ -82,7 +90,7 @@ export async function deployWorkspaceProjectInternal( undefined : localize('loadingWorkspaceTitle', 'Loading workspace project starting configurations...') }, async () => { - startingConfiguration = await getStartingConfiguration({ ...context }, options); + startingConfiguration = await getStartingConfiguration(context, options); }); if (startingConfiguration?.containerApp?.revisionsMode === KnownActiveRevisionsMode.Multiple) { @@ -100,137 +108,75 @@ export async function deployWorkspaceProjectInternal( }; const promptSteps: AzureWizardPromptStep[] = [ - new DeployWorkspaceProjectConfirmStep(!!options.suppressConfirmation), - new SharedResourcesNameStep(), - new AppResourcesNameStep(!!options.suppressContainerAppCreation) + new RootFolderStep(), ]; + const executeSteps: AzureWizardExecuteStep[] = []; - const executeSteps: AzureWizardExecuteStep[] = [ - new DeployWorkspaceProjectSaveSettingsStep() - ]; + LocationListStep.addProviderForFiltering(context, managedEnvironmentProvider, managedEnvironmentResourceType); + LocationListStep.addProviderForFiltering(context, logAnalyticsProvider, logAnalyticsResourceType); + LocationListStep.addProviderForFiltering(context, registryProvider, registryResourceType); + LocationListStep.addProviderForFiltering(context, containerAppProvider, containerAppResourceType); - // Resource group - if (wizardContext.resourceGroup) { - wizardContext.telemetry.properties.existingResourceGroup = 'true'; - - const resourceGroupName: string = nonNullValueAndProp(wizardContext.resourceGroup, 'name'); + if (!options.advancedCreate) { + // Basic + if (!wizardContext.resourceGroup) { + executeSteps.push(new ResourceGroupCreateStep()); + } + if (!wizardContext.managedEnvironment) { + executeSteps.push( + new LogAnalyticsCreateStep(), + new ManagedEnvironmentCreateStep(), + ); + } + if (!wizardContext.registry) { + executeSteps.push(new RegistryCreateStep()); + } + if (!wizardContext.containerApp && !options.suppressContainerAppCreation) { + executeSteps.push(new ContainerAppCreateStep()); + } - prependOrInsertAfterLastInfoChild(wizardContext, - new ActivityChildItem({ - label: localize('useResourceGroup', 'Use resource group "{0}"', resourceGroupName), - activityType: ActivityChildType.Info, - contextValue: activityInfoContext, - iconPath: activityInfoIcon - }) + promptSteps.push( + new SharedResourcesNameStep(), + new AppResourcesNameStep(!!options.suppressContainerAppCreation), ); - - await LocationListStep.setLocation(wizardContext, wizardContext.resourceGroup.location); - ext.outputChannel.appendLog(localize('usingResourceGroup', 'Using resource group "{0}".', resourceGroupName)); } else { - wizardContext.telemetry.properties.existingResourceGroup = 'false'; - executeSteps.push(new ResourceGroupCreateStep()); - } - - // Managed environment - if (wizardContext.managedEnvironment) { - wizardContext.telemetry.properties.existingEnvironment = 'true'; - - const managedEnvironmentName: string = nonNullValueAndProp(wizardContext.managedEnvironment, 'name'); - - prependOrInsertAfterLastInfoChild(wizardContext, - new ActivityChildItem({ - label: localize('useManagedEnvironment', 'Use container apps environment "{0}"', managedEnvironmentName), - activityType: ActivityChildType.Info, - contextValue: activityInfoContext, - iconPath: activityInfoIcon - }) - ); - - if (!LocationListStep.hasLocation(wizardContext)) { - await LocationListStep.setLocation(wizardContext, wizardContext.managedEnvironment.location); + // Advanced + if (!wizardContext.managedEnvironment) { + promptSteps.push(new ManagedEnvironmentListStep({ + pickUpdateStrategy: new ManagedEnvironmentSortByLocalSettingsStrategy(), + })); + } + if (!wizardContext.resourceGroup) { + promptSteps.push(new ResourceGroupListStep()); + } + if (!wizardContext.registry) { + promptSteps.push(new AcrListStep({ + pickUpdateStrategy: new AcrDefaultSortStrategy(), + })); + } + if (!wizardContext.containerApp && !options.suppressContainerAppCreation) { + promptSteps.push(new ContainerAppListStep({ updateIfExists: true })); } - - ext.outputChannel.appendLog(localize('usingManagedEnvironment', 'Using container apps environment "{0}".', managedEnvironmentName)); - } else { - wizardContext.telemetry.properties.existingEnvironment = 'false'; - - executeSteps.push( - new LogAnalyticsCreateStep(), - new ManagedEnvironmentCreateStep() - ); } - // Azure Container Registry - if (wizardContext.registry) { - wizardContext.telemetry.properties.existingRegistry = 'true'; - - const registryName: string = nonNullValueAndProp(wizardContext.registry, 'name'); - - prependOrInsertAfterLastInfoChild(wizardContext, - new ActivityChildItem({ - label: localize('useAcr', 'Use container registry "{0}"', registryName), - activityType: ActivityChildType.Info, - contextValue: activityInfoContext, - iconPath: activityInfoIcon - }) - ); - - ext.outputChannel.appendLog(localize('usingAcr', 'Using Azure Container Registry "{0}".', registryName)); - } else { - // ImageSourceListStep can already handle this creation logic - wizardContext.telemetry.properties.existingRegistry = 'false'; - } + LocationListStep.addStep(wizardContext, promptSteps); - // Container app if (wizardContext.containerApp) { - wizardContext.telemetry.properties.existingContainerApp = 'true'; - - const containerAppName: string = nonNullValueAndProp(wizardContext.containerApp, 'name'); - executeSteps.push(new ContainerAppUpdateStep()); - - prependOrInsertAfterLastInfoChild(wizardContext, - new ActivityChildItem({ - label: localize('useContainerApp', 'Use container app "{0}"', containerAppName), - activityType: ActivityChildType.Info, - contextValue: activityInfoContext, - iconPath: activityInfoIcon - }) - ); - - ext.outputChannel.appendLog(localize('usingContainerApp', 'Using container app "{0}".', containerAppName)); - - if (!LocationListStep.hasLocation(wizardContext)) { - await LocationListStep.setLocation(wizardContext, wizardContext.containerApp.location); - } - } else { - wizardContext.telemetry.properties.existingContainerApp = 'false'; - - if (!options.suppressContainerAppCreation) { - executeSteps.push(new ContainerAppCreateStep()); - } } promptSteps.push( new ImageSourceListStep(), new IngressPromptStep(), + new DeployWorkspaceProjectConfirmStep(!!options.suppressConfirmation), + new StartingResourcesLogStep(), + new ShouldSaveDeploySettingsPromptStep(), ); - // Verify providers - executeSteps.push(getVerifyProvidersStep()); - - // Location - if (LocationListStep.hasLocation(wizardContext)) { - wizardContext.telemetry.properties.existingLocation = 'true'; - ext.outputChannel.appendLog(localize('useLocation', 'Using location "{0}".', (await LocationListStep.getLocation(wizardContext)).name)); - } else { - wizardContext.telemetry.properties.existingLocation = 'false'; - LocationListStep.addProviderForFiltering(wizardContext, appProvider, managedEnvironmentsId); - LocationListStep.addStep(wizardContext, promptSteps); - } - - // Save deploy settings - promptSteps.push(new ShouldSaveDeploySettingsPromptStep()); + executeSteps.push( + getVerifyProvidersStep(), + new DeployWorkspaceProjectSaveSettingsStep() + ); const wizard: AzureWizard = new AzureWizard(wizardContext, { title: options.suppressWizardTitle ? @@ -238,7 +184,7 @@ export async function deployWorkspaceProjectInternal( localize('deployWorkspaceProjectTitle', 'Deploy workspace project to a container app'), promptSteps, executeSteps, - showLoadingPrompt: true + showLoadingPrompt: true, }); await wizard.prompt(); @@ -254,7 +200,6 @@ export async function deployWorkspaceProjectInternal( await wizard.execute(); wizardContext.telemetry.properties.revisionMode = wizardContext.containerApp?.revisionsMode; - ext.outputChannel.appendLog( formatSectionHeader(localize('finishCommandExecution', 'Finished deploying workspace project')) ); diff --git a/src/commands/deployWorkspaceProject/internal/startingConfiguration/containerAppsResourceHelpers.ts b/src/commands/deployWorkspaceProject/internal/getStartingConfiguration/containerAppResourceHelpers.ts similarity index 98% rename from src/commands/deployWorkspaceProject/internal/startingConfiguration/containerAppsResourceHelpers.ts rename to src/commands/deployWorkspaceProject/internal/getStartingConfiguration/containerAppResourceHelpers.ts index aa00381b7..06df01aae 100644 --- a/src/commands/deployWorkspaceProject/internal/startingConfiguration/containerAppsResourceHelpers.ts +++ b/src/commands/deployWorkspaceProject/internal/getStartingConfiguration/containerAppResourceHelpers.ts @@ -10,7 +10,7 @@ import { nonNullProp, type ISubscriptionActionContext } from "@microsoft/vscode- import { type ContainerAppModel } from "../../../../tree/ContainerAppItem"; import { createContainerAppsAPIClient } from "../../../../utils/azureClients"; -interface ContainerAppsResources { +export interface ContainerAppsResources { resourceGroup?: ResourceGroup; managedEnvironment?: ManagedEnvironment; containerApp?: ContainerAppModel; diff --git a/src/commands/deployWorkspaceProject/internal/startingConfiguration/getStartingConfiguration.ts b/src/commands/deployWorkspaceProject/internal/getStartingConfiguration/getStartingConfiguration.ts similarity index 57% rename from src/commands/deployWorkspaceProject/internal/startingConfiguration/getStartingConfiguration.ts rename to src/commands/deployWorkspaceProject/internal/getStartingConfiguration/getStartingConfiguration.ts index 0a0a9725c..53b603785 100644 --- a/src/commands/deployWorkspaceProject/internal/startingConfiguration/getStartingConfiguration.ts +++ b/src/commands/deployWorkspaceProject/internal/getStartingConfiguration/getStartingConfiguration.ts @@ -4,50 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import { KnownSkuName } from "@azure/arm-containerregistry"; -import { AzureWizard, type AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; import { ImageSource } from "../../../../constants"; +import { ManagedEnvironmentListStep } from "../../../createManagedEnvironment/ManagedEnvironmentListStep"; import { EnvFileListStep } from "../../../image/imageSource/EnvFileListStep"; -import { DockerfileItemStep } from "../../../image/imageSource/buildImageInAzure/DockerfileItemStep"; import { AcrBuildSupportedOS } from "../../../image/imageSource/buildImageInAzure/OSPickStep"; -import { RootFolderStep } from "../../../image/imageSource/buildImageInAzure/RootFolderStep"; import { type DeployWorkspaceProjectInternalContext } from "../DeployWorkspaceProjectInternalContext"; import { type DeployWorkspaceProjectInternalOptions } from "../deployWorkspaceProjectInternal"; -import { DwpAcrListStep } from "./DwpAcrListStep"; -import { DwpManagedEnvironmentListStep } from "./DwpManagedEnvironmentListStep"; -import { getResourcesFromContainerAppHelper, getResourcesFromManagedEnvironmentHelper } from "./containerAppsResourceHelpers"; +import { getResourcesFromContainerAppHelper } from "./containerAppResourceHelpers"; export async function getStartingConfiguration(context: DeployWorkspaceProjectInternalContext, options: DeployWorkspaceProjectInternalOptions): Promise> { await tryAddMissingAzureResourcesToContext(context); - const promptSteps: AzureWizardPromptStep[] = [ - new RootFolderStep(), - new DockerfileItemStep(), - new DwpManagedEnvironmentListStep(), - ]; - - if (!options.suppressRegistryPrompt) { - promptSteps.push(new DwpAcrListStep()); - } - - const wizard: AzureWizard = new AzureWizard(context, { - promptSteps, - }); - - await wizard.prompt(); - await wizard.execute(); - return { - rootFolder: context.rootFolder, - dockerfilePath: context.dockerfilePath, resourceGroup: context.resourceGroup, + logAnalyticsWorkspace: context.logAnalyticsWorkspace, managedEnvironment: context.managedEnvironment, containerApp: context.containerApp, - registry: context.registry, - newRegistrySku: KnownSkuName.Basic, suppressEnableAdminUserPrompt: options.suppressConfirmation, imageSource: ImageSource.RemoteAcrBuild, os: AcrBuildSupportedOS.Linux, - envPath: context.envPath, + newRegistrySku: !options.advancedCreate ? KnownSkuName.Basic : undefined, environmentVariables: context.envPath ? undefined /** No need to set anything if there's an envPath, the step will handle parsing the data for us */ : @@ -56,14 +32,13 @@ export async function getStartingConfiguration(context: DeployWorkspaceProjectIn } async function tryAddMissingAzureResourcesToContext(context: DeployWorkspaceProjectInternalContext): Promise { - if (!context.containerApp && !context.managedEnvironment) { - return; - } else if (context.containerApp) { + if (context.containerApp && (!context.resourceGroup || !context.managedEnvironment)) { const resources = await getResourcesFromContainerAppHelper(context, context.containerApp); context.resourceGroup ??= resources.resourceGroup; context.managedEnvironment ??= resources.managedEnvironment; - } else if (context.managedEnvironment) { - const resources = await getResourcesFromManagedEnvironmentHelper(context, context.managedEnvironment); - context.resourceGroup ??= resources.resourceGroup; + } + + if (context.managedEnvironment) { + await ManagedEnvironmentListStep.populateContextWithRelatedResources(context, context.managedEnvironment); } } diff --git a/src/commands/deployWorkspaceProject/internal/startingConfiguration/DwpAcrListStep.ts b/src/commands/deployWorkspaceProject/internal/startingConfiguration/DwpAcrListStep.ts deleted file mode 100644 index 6de84a85f..000000000 --- a/src/commands/deployWorkspaceProject/internal/startingConfiguration/DwpAcrListStep.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type Registry } from "@azure/arm-containerregistry"; -import { AzureWizardPromptStep, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; -import { localize } from "../../../../utils/localize"; -import { acrCreatePick, AcrListStep } from "../../../image/imageSource/containerRegistry/acr/AcrListStep"; -import { type DeployWorkspaceProjectInternalContext } from "../DeployWorkspaceProjectInternalContext"; - -export class DwpAcrListStep extends AzureWizardPromptStep { - public async prompt(context: T): Promise { - const picks: IAzureQuickPickItem[] = await this.getPicks(context); - if (picks.length < 2) { - // No container registries to choose from - return; - } - - const placeHolder: string = localize('selectContainerRegistry', 'Select an Azure Container Registry to store your image'); - const pick: IAzureQuickPickItem = await context.ui.showQuickPick(picks, { placeHolder, enableGrouping: true, suppressPersistence: true }); - - context.registry = pick.data; - } - - public shouldPrompt(context: T): boolean { - return !context.registry; - } - - public async getPicks(context: T): Promise[]> { - return [ - acrCreatePick, - ...await AcrListStep.getSortedAndRecommendedPicks(context), - ]; - } -} diff --git a/src/commands/deployWorkspaceProject/internal/startingConfiguration/DwpManagedEnvironmentListStep.ts b/src/commands/deployWorkspaceProject/internal/startingConfiguration/DwpManagedEnvironmentListStep.ts deleted file mode 100644 index 510f37d64..000000000 --- a/src/commands/deployWorkspaceProject/internal/startingConfiguration/DwpManagedEnvironmentListStep.ts +++ /dev/null @@ -1,111 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { type ContainerAppsAPIClient, type ManagedEnvironment } from "@azure/arm-appcontainers"; -import { type ResourceGroup } from "@azure/arm-resources"; -import { ResourceGroupListStep, getResourceGroupFromId, uiUtils } from "@microsoft/vscode-azext-azureutils"; -import { AzureWizardPromptStep, nonNullProp, nonNullValueAndProp, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; -import { createContainerAppsAPIClient } from "../../../../utils/azureClients"; -import { localize } from "../../../../utils/localize"; -import { hasMatchingPickDescription, recommendedPickDescription } from "../../../../utils/pickUtils"; -import { type DeploymentConfigurationSettings } from "../../settings/DeployWorkspaceProjectSettingsV2"; -import { dwpSettingUtilsV2 } from "../../settings/dwpSettingUtilsV2"; -import { type DeployWorkspaceProjectInternalContext } from "../DeployWorkspaceProjectInternalContext"; - -export class DwpManagedEnvironmentListStep extends AzureWizardPromptStep { - public async prompt(context: DeployWorkspaceProjectInternalContext): Promise { - const placeHolder: string = localize('selectManagedEnvironment', 'Select a container apps environment'); - const picks: IAzureQuickPickItem[] = await this.getPicks(context); - - if (!picks.length) { - // No managed environments to choose from - return; - } - - await this.setRecommendedPicks(context, picks); - const pick = await context.ui.showQuickPick(picks, { placeHolder, suppressPersistence: true }); - context.telemetry.properties.usedRecommendedEnv = hasMatchingPickDescription(pick, recommendedPickDescription) ? 'true' : 'false'; - context.telemetry.properties.recommendedEnvCount = - String(picks.reduce((count, pick) => count + (hasMatchingPickDescription(pick, recommendedPickDescription) ? 1 : 0), 0)); - - const managedEnvironment: ManagedEnvironment | undefined = pick.data; - if (!managedEnvironment) { - // User is choosing to create a new managed environment - return; - } - - await this.setContextWithManagedEnvironmentResources(context, managedEnvironment); - } - - public shouldPrompt(context: DeployWorkspaceProjectInternalContext): boolean { - return !context.managedEnvironment; - } - - private async getPicks(context: DeployWorkspaceProjectInternalContext): Promise[]> { - const client: ContainerAppsAPIClient = await createContainerAppsAPIClient(context); - const managedEnvironments: ManagedEnvironment[] = await uiUtils.listAllIterator( - context.resourceGroup ? - client.managedEnvironments.listByResourceGroup(nonNullValueAndProp(context.resourceGroup, 'name')) : - client.managedEnvironments.listBySubscription() - ); - - if (!managedEnvironments.length) { - return []; - } - - return [ - { - label: localize('newManagedEnvironment', '$(plus) Create new container apps environment'), - data: undefined - }, - ...managedEnvironments.map(env => { - return { - label: nonNullProp(env, 'name'), - data: env - }; - }) - ]; - } - - private async setContextWithManagedEnvironmentResources(context: DeployWorkspaceProjectInternalContext, managedEnvironment: ManagedEnvironment): Promise { - const resourceGroups: ResourceGroup[] = await ResourceGroupListStep.getResourceGroups(context); - context.resourceGroup = resourceGroups.find(rg => rg.name === getResourceGroupFromId(nonNullProp(managedEnvironment, 'id'))); - context.managedEnvironment = managedEnvironment; - } - - private async setRecommendedPicks(context: DeployWorkspaceProjectInternalContext, picks: IAzureQuickPickItem[]): Promise { - const deploymentConfigurations: DeploymentConfigurationSettings[] | undefined = await dwpSettingUtilsV2.getWorkspaceDeploymentConfigurations(nonNullProp(context, 'rootFolder')); - if (!deploymentConfigurations?.length) { - return; - } - - const client = await createContainerAppsAPIClient(context); - for (const config of deploymentConfigurations) { - try { - if (config.resourceGroup && config.containerApp) { - const containerApp = await client.containerApps.get(config.resourceGroup, config.containerApp); - const recommendedPick = picks.find(p => p.data?.id === containerApp.managedEnvironmentId); - if (recommendedPick) { - recommendedPick.description = recommendedPickDescription; - } - } - } - catch (these_hands) { - // ignore the error and continue - } - } - - // sort the picks by recommendation - picks.sort((a, b) => { - if (hasMatchingPickDescription(a, recommendedPickDescription)) { - return -1; - } else if (hasMatchingPickDescription(b, recommendedPickDescription)) { - return 1; - } else { - return 0; - } - }); - } -} diff --git a/src/commands/image/imageSource/EnvFileListStep.ts b/src/commands/image/imageSource/EnvFileListStep.ts index b0c673f5e..5d776230a 100644 --- a/src/commands/image/imageSource/EnvFileListStep.ts +++ b/src/commands/image/imageSource/EnvFileListStep.ts @@ -4,14 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { type EnvironmentVar } from "@azure/arm-appcontainers"; -import { ActivityChildItem, ActivityChildType, AzExtFsExtra, AzureWizardPromptStep, activityInfoContext, activityInfoIcon, activitySuccessContext, activitySuccessIcon, createContextValue, type ConfirmationViewProperty } from "@microsoft/vscode-azext-utils"; +import { ActivityChildItem, ActivityChildType, AzExtFsExtra, AzureWizardPromptStep, activityInfoContext, activityInfoIcon, activitySuccessContext, activitySuccessIcon, createContextValue, prependOrInsertAfterLastInfoChild, type ActivityInfoChild, type ConfirmationViewProperty } from "@microsoft/vscode-azext-utils"; import { parse, type DotenvParseOutput } from "dotenv"; import { RelativePattern, workspace, type Uri, type WorkspaceFolder } from "vscode"; import { ImageSource, envFileGlobPattern } from "../../../constants"; import { ext } from "../../../extensionVariables"; import { type EnvironmentVariableTelemetryProps as TelemetryProps } from "../../../telemetry/ImageSourceTelemetryProps"; import { type SetTelemetryProps } from "../../../telemetry/SetTelemetryProps"; -import { prependOrInsertAfterLastInfoChild } from "../../../utils/activityUtils"; import { localize } from "../../../utils/localize"; import { selectWorkspaceFile } from "../../../utils/workspaceUtils"; import { type EnvironmentVariablesContext } from "../../environmentVariables/EnvironmentVariablesContext"; @@ -162,7 +161,7 @@ export class EnvFileListStep extends AzureWizardPr activityType: ActivityChildType.Info, iconPath: activityInfoIcon, stepId: this.id - }) + }) as ActivityInfoChild, ); ext.outputChannel.appendLog(localize('useExistingEnvVarsMessage', 'Used existing environment variable configuration.')); } diff --git a/src/commands/image/imageSource/ImageSourceListStep.ts b/src/commands/image/imageSource/ImageSourceListStep.ts index 8f75d2173..2c0a6bf59 100644 --- a/src/commands/image/imageSource/ImageSourceListStep.ts +++ b/src/commands/image/imageSource/ImageSourceListStep.ts @@ -22,7 +22,6 @@ import { TarFileStep } from "./buildImageInAzure/TarFileStep"; import { UploadSourceCodeStep } from "./buildImageInAzure/UploadSourceCodeStep"; import { ContainerRegistryImageConfigureStep } from "./containerRegistry/ContainerRegistryImageConfigureStep"; import { ContainerRegistryListStep } from "./containerRegistry/ContainerRegistryListStep"; -import { AcrListStep } from "./containerRegistry/acr/AcrListStep"; interface ImageSourceListStepOptions { suppressEnvPrompt?: boolean; @@ -86,11 +85,10 @@ export class ImageSourceListStep extends AzureWizardPromptStep[] = []; switch (context.registryDomain) { case acrDomain: - promptSteps.push(new AcrListStep({ suppressCreate: true }), new AcrRepositoriesListStep(), new AcrTagListStep()); + promptSteps.push(new AcrListStep({ suppressCreatePick: true }), new AcrRepositoriesListStep(), new AcrTagListStep()); break; case dockerHubDomain: promptSteps.push(new DockerHubNamespaceInputStep(), new DockerHubContainerRepositoryListStep(), new DockerHubContainerTagListStep()); diff --git a/src/commands/image/imageSource/containerRegistry/acr/AcrDefaultSortStrategy.ts b/src/commands/image/imageSource/containerRegistry/acr/AcrDefaultSortStrategy.ts new file mode 100644 index 000000000..cce076524 --- /dev/null +++ b/src/commands/image/imageSource/containerRegistry/acr/AcrDefaultSortStrategy.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KnownActiveRevisionsMode } from "@azure/arm-appcontainers"; +import { type Registry } from "@azure/arm-containerregistry"; +import { parseAzureResourceId } from "@microsoft/vscode-azext-azureutils"; +import { nonNullProp, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; +import { acrDomain } from "../../../../../constants"; +import { parseImageName } from "../../../../../utils/imageNameUtils"; +import { localize } from "../../../../../utils/localize"; +import { currentlyDeployedPickDescription, hasMatchingPickDescription } from "../../../../../utils/pickUtils"; +import { type ContainerRegistryImageSourceContext } from "../ContainerRegistryImageSourceContext"; +import { getLatestContainerAppImage } from "../getLatestContainerImage"; +import { type AcrPickItem, type AcrPickUpdateStrategy } from "./AcrListStep"; + +/** + * This strategy organizes Azure Container Registry (ACR) picks for the user by grouping registries by resource group, + * prioritizing registries in the currently selected resource group, and highlighting the registry currently deployed to the container app if it exists. + * Registries in the same resource group are shown at the top, followed by others, and the currently deployed registry (if any) is moved to the front of the list. + */ +export class AcrDefaultSortStrategy implements AcrPickUpdateStrategy { + updatePicks(context: ContainerRegistryImageSourceContext, picks: AcrPickItem[]): AcrPickItem[] { + const registriesByGroup: Record = {}; + for (const p of picks) { + const registry: Registry | undefined = p.data; + if (!registry.id) { + continue; + } + + const { resourceGroup } = parseAzureResourceId(registry.id); + const registriesGroup: Registry[] = registriesByGroup[resourceGroup] ?? []; + registriesGroup.push(registry); + registriesByGroup[resourceGroup] = registriesGroup; + } + + // Sort resource groups alphabetically; if matches selected resource group, prioritize to the top + const sortedResourceGroups: string[] = Object.keys(registriesByGroup).sort((a, b) => { + const lowA: string = a.toLocaleLowerCase(); + const lowB: string = b.toLocaleLowerCase(); + + switch (true) { + // If the user already picked a resource group, sort those to the top + case a === context.resourceGroup?.name: + return -1; + case b === context.resourceGroup?.name: + return 1; + + // Everything below handles normal alphabetical sorting + case lowA < lowB: + return -1; + case lowB < lowA: + return 1; + default: + return 0; + } + }); + + // Check for a currently deployed registry + let currentRegistry: string | undefined; + let hasCurrentRegistry: boolean = false; + if (context.containerApp) { + const { registryDomain, registryName } = parseImageName(getLatestContainerAppImage(context.containerApp, context.containersIdx ?? 0)); + if (context.containerApp.revisionsMode === KnownActiveRevisionsMode.Single && registryDomain === acrDomain) { + currentRegistry = registryName; + + const crIndex: number = picks.findIndex((p) => !!currentRegistry && p.data.loginServer === currentRegistry); + hasCurrentRegistry = crIndex !== -1; + } + } + + context.telemetry.properties.sameRgAcrCount = '0'; + + let hasSameRgRegistry: boolean = false; + const reOrderedPicks: IAzureQuickPickItem[] = []; + for (const [i, rg] of sortedResourceGroups.entries()) { + const registriesGroup: Registry[] = registriesByGroup[rg]; + + // Same resource group would be sorted to the top of the list... + let maybeSameRg: string | undefined; + if (i === 0 && !hasCurrentRegistry && rg === context.resourceGroup?.name) { + maybeSameRg = localize('sameRg', 'Within Same Resource Group'); + context.telemetry.properties.sameRgAcrCount = String(registriesGroup.length); + hasSameRgRegistry = true; + } + + // ...any "Other" resource groups would come after + let maybeOtherRg: string | undefined; + if (i > 0 && hasSameRgRegistry) { + maybeOtherRg = localize('other', 'Other'); + } + + const groupedRegistries: IAzureQuickPickItem[] = registriesGroup.map(r => { + const maybeDeployed: string = r.loginServer === currentRegistry ? ` ${currentlyDeployedPickDescription}` : ''; + return { + label: nonNullProp(r, 'name'), + group: maybeSameRg || maybeOtherRg, + description: maybeDeployed || rg, + data: r, + }; + }); + + reOrderedPicks.push(...groupedRegistries); + } + + // If a currently deployed registry exists, bring it to the front of the list + if (hasCurrentRegistry) { + const cdIdx: number = reOrderedPicks.findIndex(p => hasMatchingPickDescription(p, currentlyDeployedPickDescription)); + if (cdIdx !== -1) { + const currentlyDeployedPick: IAzureQuickPickItem | undefined = reOrderedPicks.splice(cdIdx, 1)[0]; + reOrderedPicks.unshift(currentlyDeployedPick); + } + } + + return reOrderedPicks; + } +} diff --git a/src/commands/image/imageSource/containerRegistry/acr/AcrListStep.ts b/src/commands/image/imageSource/containerRegistry/acr/AcrListStep.ts index 634cd3432..1bb092f46 100644 --- a/src/commands/image/imageSource/containerRegistry/acr/AcrListStep.ts +++ b/src/commands/image/imageSource/containerRegistry/acr/AcrListStep.ts @@ -3,44 +3,47 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { KnownActiveRevisionsMode } from "@azure/arm-appcontainers"; import { type ContainerRegistryManagementClient, type Registry } from "@azure/arm-containerregistry"; -import { type ResourceGroup } from "@azure/arm-resources"; -import { LocationListStep, ResourceGroupListStep, getResourceGroupFromId, parseAzureResourceId, uiUtils } from "@microsoft/vscode-azext-azureutils"; +import { LocationListStep, ResourceGroupListStep, uiUtils } from "@microsoft/vscode-azext-azureutils"; import { AzureWizardPromptStep, nonNullProp, type AzureWizardExecuteStep, type ConfirmationViewProperty, type IAzureQuickPickItem, type ISubscriptionActionContext, type IWizardOptions } from "@microsoft/vscode-azext-utils"; -import { acrDomain, noMatchingResources, noMatchingResourcesQp } from "../../../../../constants"; +import { noMatchingResources, noMatchingResourcesQp, registryProvider, registryResourceType } from "../../../../../constants"; import { createContainerRegistryManagementClient } from "../../../../../utils/azureClients"; -import { parseImageName } from "../../../../../utils/imageNameUtils"; import { localize } from "../../../../../utils/localize"; -import { currentlyDeployedPickDescription, hasMatchingPickDescription } from "../../../../../utils/pickUtils"; -import { type ContainerAppCreateBaseContext } from "../../../../createContainerApp/ContainerAppCreateContext"; -import { type ManagedEnvironmentCreateContext } from "../../../../createManagedEnvironment/ManagedEnvironmentCreateContext"; import { type ContainerRegistryImageSourceContext } from "../ContainerRegistryImageSourceContext"; -import { getLatestContainerAppImage } from "../getLatestContainerImage"; -import { type CreateAcrContext } from "./createAcr/CreateAcrContext"; +import { AcrDefaultSortStrategy } from "./AcrDefaultSortStrategy"; import { RegistryCreateStep } from "./createAcr/RegistryCreateStep"; import { RegistryNameStep } from "./createAcr/RegistryNameStep"; import { SkuListStep } from "./createAcr/SkuListStep"; export interface AcrListStepOptions { - suppressCreate?: boolean; + suppressCreatePick?: boolean; + pickUpdateStrategy?: AcrPickUpdateStrategy; } -export const acrCreatePick = { +export type AcrPickItem = IAzureQuickPickItem; + +export interface AcrPickUpdateStrategy { + /** + * Provide a custom strategy for updating the list of container registry picks. + * Can be used to inject custom sorting, grouping, filtering, etc. + */ + updatePicks(context: ContainerRegistryImageSourceContext, picks: AcrPickItem[]): AcrPickItem[] | Promise; +} + +const acrCreatePick = { label: localize('newContainerRegistry', '$(plus) Create new container registry'), - data: undefined + data: undefined, }; export class AcrListStep extends AzureWizardPromptStep { - constructor(private readonly options?: AcrListStepOptions) { + constructor(readonly options: AcrListStepOptions = {}) { super(); + this.options.pickUpdateStrategy ??= new AcrDefaultSortStrategy(); } private pickLabel: string; public async prompt(context: T): Promise { - const placeHolder: string = localize('selectRegistry', 'Select an Azure Container Registry'); - let pick: IAzureQuickPickItem; let result: Registry | typeof noMatchingResources | undefined; do { @@ -51,7 +54,11 @@ export class AcrListStep extends break; } - pick = await context.ui.showQuickPick(picks, { placeHolder, enableGrouping: true, suppressPersistence: true }); + pick = await context.ui.showQuickPick(picks, { + placeHolder: localize('selectRegistry', 'Select a container registry'), + enableGrouping: true, + suppressPersistence: true, + }); result = pick.data; } while (result === noMatchingResources); @@ -71,147 +78,49 @@ export class AcrListStep extends } } - public async getSubWizard(context: T): Promise | undefined> { - const promptSteps: AzureWizardPromptStep[] = []; - const executeSteps: AzureWizardExecuteStep[] = []; - - if (!context.registry) { - promptSteps.push( - new RegistryNameStep(), - new SkuListStep() - ); - executeSteps.push(new RegistryCreateStep()); - - await tryConfigureResourceGroupForRegistry(context, promptSteps); - - if (!LocationListStep.hasLocation(context) && context.resourceGroup) { - await LocationListStep.setLocation(context, context.resourceGroup.location); - } else { - LocationListStep.addStep(context, promptSteps); - } - } - - return { - promptSteps, - executeSteps - }; - } + private async getPicks(context: T): Promise[]> { + const registryPicks: AcrPickItem[] = (await AcrListStep.getRegistries(context)).map(r => { + return { + label: nonNullProp(r, 'name'), + data: r, + }; + }); - public async getPicks(context: T): Promise[]> { - const picks: IAzureQuickPickItem[] = []; - if (!this.options?.suppressCreate) { - picks.push(acrCreatePick); + const picks: IAzureQuickPickItem[] = await this.options.pickUpdateStrategy?.updatePicks(context, registryPicks) ?? registryPicks; + if (!this.options?.suppressCreatePick) { + picks.unshift(acrCreatePick); } - - const registryPicks: IAzureQuickPickItem[] = await AcrListStep.getSortedAndRecommendedPicks(context); - if (!picks.length && !registryPicks.length) { + if (!picks.length) { return [noMatchingResourcesQp]; } - return picks.concat(registryPicks); + return picks; } - /** - * Returns a list of registries sorted by resource group, with matching resource group sorted to the top of the list. - * If a currently deployed registry exists, that will be sorted to the top and marked instead. - */ - public static async getSortedAndRecommendedPicks(context: ISubscriptionActionContext & Partial): Promise[]> { - const registries: Registry[] = await AcrListStep.getRegistries(context); - context.telemetry.properties.acrCount = String(registries.length); - if (!registries.length) { - return []; + public async getSubWizard(context: T): Promise | undefined> { + if (context.registry) { + return undefined; } - const registriesByGroup: Record = {}; - for (const r of registries) { - if (!r.id) { - continue; - } - - const { resourceGroup } = parseAzureResourceId(r.id); - const registriesGroup: Registry[] = registriesByGroup[resourceGroup] ?? []; - registriesGroup.push(r); - registriesByGroup[resourceGroup] = registriesGroup; - } + const promptSteps: AzureWizardPromptStep[] = []; + const executeSteps: AzureWizardExecuteStep[] = []; - // Sort resource groups alphabetically; if matches selected resource group, prioritize to the top - const sortedResourceGroups: string[] = Object.keys(registriesByGroup).sort((a, b) => { - const lowA: string = a.toLocaleLowerCase(); - const lowB: string = b.toLocaleLowerCase(); - - switch (true) { - // If the user already picked a resource group, sort those to the top - case a === context.resourceGroup?.name: - return -1; - case b === context.resourceGroup?.name: - return 1; - - // Everything below handles normal alphabetical sorting - case lowA < lowB: - return -1; - case lowB < lowA: - return 1; - default: - return 0; - } - }); + LocationListStep.addProviderForFiltering(context, registryProvider, registryResourceType); - // Check for a currently deployed registry - let currentRegistry: string | undefined; - let hasCurrentRegistry: boolean = false; - if (context.containerApp) { - const { registryDomain, registryName } = parseImageName(getLatestContainerAppImage(context.containerApp, context.containersIdx ?? 0)); - if (context.containerApp.revisionsMode === KnownActiveRevisionsMode.Single && registryDomain === acrDomain) { - currentRegistry = registryName; + promptSteps.push( + new RegistryNameStep(), + new SkuListStep(), + ); - const crIndex: number = registries.findIndex((r) => !!currentRegistry && r.loginServer === currentRegistry); - hasCurrentRegistry = crIndex !== -1; - } + if (!context.resourceGroup) { + promptSteps.push(new ResourceGroupListStep()); } - const picks: IAzureQuickPickItem[] = []; - let hasSameRgRegistry: boolean = false; - context.telemetry.properties.sameRgAcrCount = '0'; - for (const [i, rg] of sortedResourceGroups.entries()) { - const registriesGroup: Registry[] = registriesByGroup[rg]; - - // Same resource group would be sorted to the top of the list... - let maybeSameRg: string | undefined; - if (i === 0 && !hasCurrentRegistry && rg === context.resourceGroup?.name) { - maybeSameRg = localize('sameRg', 'Within Same Resource Group'); - context.telemetry.properties.sameRgAcrCount = String(registriesGroup.length); - hasSameRgRegistry = true; - } - - // ...any "Other" resource groups would come after - let maybeOtherRg: string | undefined; - if (i > 0 && hasSameRgRegistry) { - maybeOtherRg = localize('other', 'Other'); - } - - const groupedRegistries: IAzureQuickPickItem[] = registriesGroup.map(r => { - const maybeDeployed: string = r.loginServer === currentRegistry ? ` ${currentlyDeployedPickDescription}` : ''; - return { - label: nonNullProp(r, 'name'), - group: maybeSameRg || maybeOtherRg, - description: maybeDeployed || rg, - data: r, - }; - }); - - picks.push(...groupedRegistries); - } + LocationListStep.addStep(context, promptSteps); - // If a currently deployed registry exists, bring it to the front of the list - if (hasCurrentRegistry) { - const cdIdx: number = picks.findIndex(p => hasMatchingPickDescription(p, currentlyDeployedPickDescription)); - if (cdIdx !== -1) { - const currentlyDeployedPick: IAzureQuickPickItem | undefined = picks.splice(cdIdx, 1)[0]; - picks.unshift(currentlyDeployedPick); - } - } + executeSteps.push(new RegistryCreateStep()); - return picks; + return { promptSteps, executeSteps }; } public static async getRegistries(context: ISubscriptionActionContext): Promise { @@ -219,24 +128,3 @@ export class AcrListStep extends return await uiUtils.listAllIterator(client.registries.list()); } } - -async function tryConfigureResourceGroupForRegistry( - context: ContainerRegistryImageSourceContext, - promptSteps: AzureWizardPromptStep[], -): Promise { - // No need to pollute the base context with all the potential pre-create typings as they are not otherwise used - const resourceCreationContext = context as Partial & Partial & CreateAcrContext; - if (resourceCreationContext.resourceGroup || resourceCreationContext.newResourceGroupName) { - return; - } - - // Try to check for an existing container app or managed environment resource group - const resourceGroupName: string | undefined = resourceCreationContext.containerApp?.resourceGroup || - (resourceCreationContext.managedEnvironment?.id ? getResourceGroupFromId(resourceCreationContext.managedEnvironment.id) : undefined); - const resourceGroups: ResourceGroup[] = await ResourceGroupListStep.getResourceGroups(resourceCreationContext); - - resourceCreationContext.resourceGroup = resourceGroups.find(rg => resourceGroupName && rg.name === resourceGroupName); - if (!resourceCreationContext.resourceGroup) { - promptSteps.push(new ResourceGroupListStep()); - } -} diff --git a/src/commands/image/imageSource/containerRegistry/acr/createAcr/RegistryCreateStep.ts b/src/commands/image/imageSource/containerRegistry/acr/createAcr/RegistryCreateStep.ts index e70046220..97ff1e849 100644 --- a/src/commands/image/imageSource/containerRegistry/acr/createAcr/RegistryCreateStep.ts +++ b/src/commands/image/imageSource/containerRegistry/acr/createAcr/RegistryCreateStep.ts @@ -5,13 +5,16 @@ import { type ContainerRegistryManagementClient } from "@azure/arm-containerregistry"; import { LocationListStep } from "@microsoft/vscode-azext-azureutils"; -import { AzureWizardExecuteStepWithActivityOutput, nonNullProp, nonNullValueAndProp } from "@microsoft/vscode-azext-utils"; +import { AzureWizardExecuteStepWithActivityOutput, nonNullProp, nonNullValueAndProp, type ISubscriptionActionContext } from "@microsoft/vscode-azext-utils"; import { type Progress } from "vscode"; import { createContainerRegistryManagementClient } from "../../../../../../utils/azureClients"; import { localize } from "../../../../../../utils/localize"; import { type CreateAcrContext } from "./CreateAcrContext"; -export class RegistryCreateStep extends AzureWizardExecuteStepWithActivityOutput { +// Made the base context partial here to help improve type compatability with some other command entrypoints +type RegistryCreateContext = Partial & ISubscriptionActionContext; + +export class RegistryCreateStep extends AzureWizardExecuteStepWithActivityOutput { public priority: number = 350; public stepName: string = 'registryCreateStep'; protected getOutputLogSuccess = (context: T) => localize('createRegistrySuccess', 'Created container registry "{0}".', context.newRegistryName); diff --git a/src/commands/image/imageSource/containerRegistry/acr/createAcr/RegistryNameStep.ts b/src/commands/image/imageSource/containerRegistry/acr/createAcr/RegistryNameStep.ts index fcedce591..dfbac98af 100644 --- a/src/commands/image/imageSource/containerRegistry/acr/createAcr/RegistryNameStep.ts +++ b/src/commands/image/imageSource/containerRegistry/acr/createAcr/RegistryNameStep.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { type ContainerRegistryManagementClient, type RegistryNameStatus } from "@azure/arm-containerregistry"; -import { AzureWizardPromptStep, randomUtils, type ISubscriptionActionContext } from "@microsoft/vscode-azext-utils"; +import { AzureWizardPromptStep, nonNullProp, randomUtils, type ISubscriptionActionContext } from "@microsoft/vscode-azext-utils"; import { createContainerRegistryManagementClient } from "../../../../../../utils/azureClients"; import { localize } from "../../../../../../utils/localize"; import { type CreateAcrContext } from "./CreateAcrContext"; @@ -13,6 +13,9 @@ export class RegistryNameStep extends AzureWizardPromptStep { public async prompt(context: CreateAcrContext): Promise { context.newRegistryName = await context.ui.showInputBox({ prompt: localize('registryName', 'Enter a name for the new registry'), + value: context.resourceGroup?.name || context.newResourceGroupName ? + await RegistryNameStep.generateRelatedName(context, context.resourceGroup?.name ?? nonNullProp(context, 'newResourceGroupName')) : + undefined, validateInput: RegistryNameStep.validateInput, asyncValidationTask: (value: string): Promise => this.validateNameAvalability(context, value) }); diff --git a/src/commands/registryCredentials/RegistryCredentialsAddConfigurationListStep.ts b/src/commands/registryCredentials/RegistryCredentialsAddConfigurationListStep.ts index ef9f1ebba..9a1d7e303 100644 --- a/src/commands/registryCredentials/RegistryCredentialsAddConfigurationListStep.ts +++ b/src/commands/registryCredentials/RegistryCredentialsAddConfigurationListStep.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ActivityChildItem, ActivityChildType, activityInfoIcon, AzureWizardPromptStep, createContextValue, nonNullProp, type AzureWizardExecuteStep, type ConfirmationViewProperty, type IAzureQuickPickItem, type IWizardOptions } from "@microsoft/vscode-azext-utils"; +import { ActivityChildItem, ActivityChildType, activityInfoIcon, AzureWizardPromptStep, createContextValue, nonNullProp, prependOrInsertAfterLastInfoChild, type ActivityInfoChild, type AzureWizardExecuteStep, type ConfirmationViewProperty, type IAzureQuickPickItem, type IWizardOptions } from "@microsoft/vscode-azext-utils"; import { acrDomain, activityInfoContext, type SupportedRegistries } from "../../constants"; import { ext } from "../../extensionVariables"; -import { prependOrInsertAfterLastInfoChild } from "../../utils/activityUtils"; import { getRegistryDomainFromContext } from "../../utils/imageNameUtils"; import { localize } from "../../utils/localize"; import { AcrEnableAdminUserConfirmStep } from "./dockerLogin/AcrEnableAdminUserConfirmStep"; @@ -56,9 +55,17 @@ export class RegistryCredentialsAddConfigurationListStep extends AzureWizardProm } public async prompt(context: RegistryCredentialsContext): Promise { - const placeHolder: string = context.registry || context.newRegistryName ? - localize('selectCredentialTypeNamed', 'Select a connection method for registry "{0}"', context.registry?.name || context.newRegistryName) : - localize('selectCredentialType', 'Select a registry connection method'); + let placeHolder: string; + switch (true) { + case !!context.registry: + placeHolder = localize('selectCredentialTypeExistingRegistry', 'Select a connection method for registry "{0}"', context.registry?.name); + break; + case !!context.newRegistryName: + placeHolder = localize('selectCredentialTypeNewRegistry', 'Select a connection method for new registry "{0}"', context.newRegistryName); + break; + default: + placeHolder = localize('selectCredentialTypeGeneric', 'Select a registry connection method'); + } const pick = (await context.ui.showQuickPick(this.getPicks(context), { placeHolder, @@ -111,8 +118,8 @@ export class RegistryCredentialsAddConfigurationListStep extends AzureWizardProm contextValue: createContextValue(['registryCredentialsAddConfigurationListStepItem', activityInfoContext]), activityType: ActivityChildType.Info, iconPath: activityInfoIcon, - stepId: this.id - }) + stepId: this.id, + }) as ActivityInfoChild, ); ext.outputChannel.appendLog(localize('usingRegistryCredentials', 'Using existing registry credentials.')); } diff --git a/src/constants.ts b/src/constants.ts index 5da4186a5..fdd2e2985 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,19 +7,17 @@ import { type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; import { type QuickPickItem } from "vscode"; import { localize } from "./utils/localize"; -export const managedEnvironmentsId = 'managedEnvironments'; -export const containerAppsId = 'containerApps'; -export const appProvider: string = 'Microsoft.App'; -export const webProvider: string = 'Microsoft.Web'; export const registryProvider: string = 'Microsoft.ContainerRegistry'; -export const operationalInsightsProvider: string = 'Microsoft.OperationalInsights'; +export const registryResourceType: string = 'registries'; -export const containerAppsWebProvider: string = `${webProvider}/${containerAppsId}`; -export const managedEnvironmentsAppProvider: string = `${appProvider}/${managedEnvironmentsId}`; +export const logAnalyticsProvider: string = 'Microsoft.OperationalInsights'; +export const logAnalyticsResourceType: string = 'workspaces'; -export const rootFilter = { - type: managedEnvironmentsAppProvider -} +export const managedEnvironmentProvider: string = 'Microsoft.App'; +export const managedEnvironmentResourceType: string = 'managedEnvironments'; + +export const containerAppProvider: string = 'Microsoft.App'; +export const containerAppResourceType: string = 'containerApps'; export namespace IngressConstants { export const external: string = localize('external', 'External'); diff --git a/src/utils/activityUtils.ts b/src/utils/activityUtils.ts index ba53f93e8..cef45c033 100644 --- a/src/utils/activityUtils.ts +++ b/src/utils/activityUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ActivityChildType, type ActivityChildItemBase, type ExecuteActivityContext } from "@microsoft/vscode-azext-utils"; +import { type ExecuteActivityContext } from "@microsoft/vscode-azext-utils"; import { type AzureResourcesExtensionApiWithActivity } from "@microsoft/vscode-azext-utils/activity"; import { ext } from "../extensionVariables"; import { settingUtils } from "./settingUtils"; @@ -15,24 +15,3 @@ export async function createActivityContext(options?: { withChildren?: boolean } activityChildren: options?.withChildren ? [] : undefined, }; } - -/** - * Adds a new activity child after the last info child in the `activityChildren` array. - * If no info child already exists, the new child is prepended to the front of the array. - * (This utility function is useful for keeping the info children grouped at the front of the list) - */ -export function prependOrInsertAfterLastInfoChild(context: Partial, infoChild: ActivityChildItemBase): void { - if (!context.activityChildren) { - return; - } - - const idx: number = context.activityChildren - .map(child => child.activityType) - .lastIndexOf(ActivityChildType.Info); - - if (idx === -1) { - context.activityChildren.unshift(infoChild); - } else { - context.activityChildren.splice(idx + 1, 0, infoChild); - } -} diff --git a/src/utils/getVerifyProvidersStep.ts b/src/utils/getVerifyProvidersStep.ts index 00dee3493..671e75be1 100644 --- a/src/utils/getVerifyProvidersStep.ts +++ b/src/utils/getVerifyProvidersStep.ts @@ -5,16 +5,15 @@ import { VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils"; import { type ISubscriptionActionContext } from "@microsoft/vscode-azext-utils"; -import { appProvider, operationalInsightsProvider, registryProvider, webProvider } from "../constants"; +import { logAnalyticsProvider, managedEnvironmentProvider, registryProvider } from "../constants"; /** * Use to obtain a `VerifyProvidersStep` that registers all known container app providers to the user's subscription */ export function getVerifyProvidersStep(): VerifyProvidersStep { return new VerifyProvidersStep([ - appProvider, - operationalInsightsProvider, - webProvider, + managedEnvironmentProvider, + logAnalyticsProvider, registryProvider ]); }