From 77ce544c43ad400d26508d18e28b3aabb8c95107 Mon Sep 17 00:00:00 2001 From: Matthew Fisher <40250218+MicroFish91@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:55:19 -0800 Subject: [PATCH] Improve output and activity logs for `deployContainerApp` (#806) --- .../deployContainerApp/deployContainerApp.ts | 23 +++++++++- .../image/deployImageApi/deployImage.ts | 19 ++------ .../imageSource/ContainerAppUpdateStep.ts | 16 ++++++- .../image/imageSource/EnvFileListStep.ts | 19 ++++++-- .../image/imageSource/ImageSourceContext.ts | 1 + .../image/imageSource/ImageSourceListStep.ts | 24 ++++++++-- .../ContainerRegistryImageConfigureStep.ts | 46 +++++++++++++------ .../RegistryImageInputStep.ts | 2 +- .../containerRegistry/acr/AcrListStep.ts | 2 +- .../acr/AcrRepositoriesListStep.ts | 2 +- .../DockerHubContainerRepositoryListStep.ts | 2 +- .../dockerHub/DockerHubNamespaceInputStep.ts | 2 +- .../getLatestContainerImage.ts | 5 +- src/commands/ingress/IngressPromptStep.ts | 14 +----- .../disableIngress/DisableIngressStep.ts | 28 ++++++++++- .../enableIngress/EnableIngressStep.ts | 20 +++++--- ...stryCredentialsAddConfigurationListStep.ts | 13 ++++-- src/tree/ContainerAppItem.ts | 2 + 18 files changed, 171 insertions(+), 69 deletions(-) diff --git a/src/commands/deployContainerApp/deployContainerApp.ts b/src/commands/deployContainerApp/deployContainerApp.ts index 307bd3467..a6acaaf1a 100644 --- a/src/commands/deployContainerApp/deployContainerApp.ts +++ b/src/commands/deployContainerApp/deployContainerApp.ts @@ -5,8 +5,9 @@ import { type ResourceGroup } from "@azure/arm-resources"; import { LocationListStep, ResourceGroupListStep } from "@microsoft/vscode-azext-azureutils"; -import { AzureWizard, createSubscriptionContext, nonNullProp, nonNullValue, type IActionContext, type ISubscriptionActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; +import { activityInfoIcon, activitySuccessContext, AzureWizard, createSubscriptionContext, createUniversallyUniqueContextValue, GenericTreeItem, nonNullProp, nonNullValue, type IActionContext, type ISubscriptionActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; import { ImageSource } from "../../constants"; +import { ext } from "../../extensionVariables"; import { type ContainerAppItem } from "../../tree/ContainerAppItem"; import { createActivityContext } from "../../utils/activityUtils"; import { isAzdExtensionInstalled } from "../../utils/azdUtils"; @@ -50,6 +51,26 @@ export async function deployContainerApp(context: IActionContext, node?: Contain localize('containerAppResourceGroup', 'Expected to find the container app\'s resource group.'), ); + // Log resource group + wizardContext.activityChildren?.push( + new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['useExistingResourceGroupInfoItem', activitySuccessContext]), + label: localize('useResourceGroup', 'Using resource group "{0}"', wizardContext.resourceGroup.name), + iconPath: activityInfoIcon + }) + ); + ext.outputChannel.appendLog(localize('usingResourceGroup', 'Using resource group "{0}".', wizardContext.resourceGroup.name)); + + // Log container app + wizardContext.activityChildren?.push( + new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['useExistingContainerAppInfoItem', activitySuccessContext]), + label: localize('useContainerApp', 'Using container app "{0}"', wizardContext.containerApp?.name), + iconPath: activityInfoIcon + }) + ); + ext.outputChannel.appendLog(localize('usingContainerApp', 'Using container app "{0}".', wizardContext.containerApp?.name)); + await LocationListStep.setLocation(wizardContext, item.containerApp.location); const wizard: AzureWizard = new AzureWizard(wizardContext, { diff --git a/src/commands/image/deployImageApi/deployImage.ts b/src/commands/image/deployImageApi/deployImage.ts index 35b138483..ceb03288f 100644 --- a/src/commands/image/deployImageApi/deployImage.ts +++ b/src/commands/image/deployImageApi/deployImage.ts @@ -3,16 +3,14 @@ * Licensed under the MIT License. See License.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizard, createSubscriptionContext, type AzureWizardPromptStep, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; +import { AzureWizard, createSubscriptionContext, type IActionContext, type ISubscriptionContext } from "@microsoft/vscode-azext-utils"; import { type ContainerAppItem } from "../../../tree/ContainerAppItem"; import { createActivityContext } from "../../../utils/activityUtils"; import { getManagedEnvironmentFromContainerApp } from "../../../utils/getResourceUtils"; import { getVerifyProvidersStep } from "../../../utils/getVerifyProvidersStep"; -import { getImageNameWithoutTag } from "../../../utils/imageNameUtils"; import { localize } from "../../../utils/localize"; import { ContainerAppOverwriteConfirmStep } from "../../ContainerAppOverwriteConfirmStep"; import { showContainerAppNotification } from "../../createContainerApp/showContainerAppNotification"; -import { IngressPromptStep } from "../../ingress/IngressPromptStep"; import { ContainerAppUpdateStep } from "../imageSource/ContainerAppUpdateStep"; import { ImageSourceListStep } from "../imageSource/ImageSourceListStep"; import { type ContainerRegistryImageSourceContext } from "../imageSource/containerRegistry/ContainerRegistryImageSourceContext"; @@ -33,19 +31,12 @@ export async function deployImage(context: IActionContext & Partial[] = [ - new ImageSourceListStep(), - ]; - - // If more than the image tag changed, prompt for ingress again - if (getImageNameWithoutTag(wizardContext.containerApp?.template?.containers?.[0].image ?? '') !== getImageNameWithoutTag(wizardContext.image ?? '')) { - promptSteps.push(new IngressPromptStep()); - } - promptSteps.push(new ContainerAppOverwriteConfirmStep()); - const wizard: AzureWizard = new AzureWizard(wizardContext, { title: localize('deploy', 'Deploy image to container app "{0}"', containerApp.name), - promptSteps, + promptSteps: [ + new ImageSourceListStep(), + new ContainerAppOverwriteConfirmStep(), + ], executeSteps: [ getVerifyProvidersStep(), new ContainerAppUpdateStep(), diff --git a/src/commands/image/imageSource/ContainerAppUpdateStep.ts b/src/commands/image/imageSource/ContainerAppUpdateStep.ts index 365f14e7a..274da923a 100644 --- a/src/commands/image/imageSource/ContainerAppUpdateStep.ts +++ b/src/commands/image/imageSource/ContainerAppUpdateStep.ts @@ -4,14 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { type Ingress } from "@azure/arm-appcontainers"; -import { AzureWizardExecuteStep, GenericParentTreeItem, GenericTreeItem, activityFailContext, activityFailIcon, activitySuccessContext, activitySuccessIcon, createUniversallyUniqueContextValue, nonNullProp, type ExecuteActivityOutput } from "@microsoft/vscode-azext-utils"; +import { activityFailContext, activityFailIcon, activitySuccessContext, activitySuccessIcon, AzureWizardExecuteStep, createUniversallyUniqueContextValue, GenericParentTreeItem, GenericTreeItem, nonNullProp, type ExecuteActivityOutput } from "@microsoft/vscode-azext-utils"; import * as retry from "p-retry"; import { type Progress } from "vscode"; import { ext } from "../../../extensionVariables"; import { getContainerEnvelopeWithSecrets, type ContainerAppModel } from "../../../tree/ContainerAppItem"; import { localize } from "../../../utils/localize"; import { type IngressContext } from "../../ingress/IngressContext"; -import { enabledIngressDefaults } from "../../ingress/enableIngress/EnableIngressStep"; +import { DisableIngressStep } from "../../ingress/disableIngress/DisableIngressStep"; +import { enabledIngressDefaults, EnableIngressStep } from "../../ingress/enableIngress/EnableIngressStep"; import { RegistryCredentialType } from "../../registryCredentials/RegistryCredentialsAddConfigurationListStep"; import { updateContainerApp } from "../../updateContainerApp"; import { type ImageSourceContext } from "./ImageSourceContext"; @@ -41,6 +42,17 @@ export class ContainerAppUpdateStep extends AzureWizardPr const logMessage: string = localize('skippedEnvVarsMessage', 'Skipped environment variable configuration for the container app' + - (setEnvironmentVariableOption === SetEnvironmentVariableOption.NoDotEnv ? ' because no .env files were detected. ' : '. ') + (setEnvironmentVariableOption === SetEnvironmentVariableOption.NoDotEnv ? ' because no .env files were detected.' : '.') ); ext.outputChannel.appendLog(logMessage); - } else { + } else if (setEnvironmentVariableOption === SetEnvironmentVariableOption.ProvideFile) { context.activityChildren?.push( new GenericTreeItem(undefined, { - contextValue: createUniversallyUniqueContextValue(['envFileListStepSuccessItem', setEnvironmentVariableOption, activitySuccessContext]), - label: localize('saveEnvVarsLabel', 'Save environment variable configuration'), + contextValue: createUniversallyUniqueContextValue(['environmentVariablesListStepSuccessItem', activitySuccessContext]), + label: localize('saveEnvVarsFileLabel', 'Save environment variables using provided .env file'), + iconPath: activitySuccessIcon + }) + ); + ext.outputChannel.appendLog(localize('savedEnvVarsFileMessage', 'Saved environment variables using provided .env file "{0}".', context.envPath)); + } else if (setEnvironmentVariableOption === SetEnvironmentVariableOption.UseExisting) { + context.activityChildren?.push( + new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['environmentVariablesListStepSuccessItem', activitySuccessContext]), + label: localize('useExistingEnvVarsLabel', 'Use existing environment variable configuration'), iconPath: activitySuccessIcon }) ); - ext.outputChannel.appendLog(localize('savedEnvVarsMessage', 'Saved environment variable configuration.')); + ext.outputChannel.appendLog(localize('useExistingEnvVarsMessage', 'Used existing environment variable configuration.')); } } } diff --git a/src/commands/image/imageSource/ImageSourceContext.ts b/src/commands/image/imageSource/ImageSourceContext.ts index eae8d7e00..8af0e2967 100644 --- a/src/commands/image/imageSource/ImageSourceContext.ts +++ b/src/commands/image/imageSource/ImageSourceContext.ts @@ -16,6 +16,7 @@ export interface ImageSourceBaseContext extends RegistryCredentialsContext, ICon imageSource?: ImageSource; showQuickStartImage?: boolean; + containersIdx?: number; image?: string; envPath?: string; diff --git a/src/commands/image/imageSource/ImageSourceListStep.ts b/src/commands/image/imageSource/ImageSourceListStep.ts index 70f2b2dd7..8f75d2173 100644 --- a/src/commands/image/imageSource/ImageSourceListStep.ts +++ b/src/commands/image/imageSource/ImageSourceListStep.ts @@ -77,13 +77,29 @@ export class ImageSourceListStep extends AzureWizardPromptStep extends AzureWizardActivityOutputExecuteStep { - public priority: number = 570; - public stepName: string = 'containerRegistryImageConfigureStep'; - protected getSuccessString = (context: T) => localize('successOutput', 'Successfully set container image to "{0}".', context.image); - protected getFailString = (context: T) => localize('failOutput', 'Failed to set container image to "{0}".', context.image); - protected getTreeItemLabel = (context: T) => localize('treeItemLabel', 'Set container image to "{0}"', context.image); - - public async execute(context: T): Promise { - context.image = `${getLoginServer(context)}/${context.repositoryName}:${context.tag}`; +export class ContainerRegistryImageConfigureStep extends AzureWizardPromptStep { + public async configureBeforePrompt(context: T): Promise { + context.image ||= `${getLoginServer(context)}/${context.repositoryName}:${context.tag}`; const { registryName, registryDomain } = parseImageName(context.image); context.telemetry.properties.registryName = registryName; context.telemetry.properties.registryDomain = registryDomain ?? 'other'; + + // Output logs + context.activityChildren?.push( + new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['containerRegistryImageConfigureStepItem', activitySuccessContext]), + label: localize('configureTargetImageLabel', 'Configure target image "{0}"', context.image), + iconPath: activitySuccessIcon + }) + ); + ext.outputChannel.appendLog(localize('configureTargetImageMessage', 'Configured target image "{0}".', context.image)); + } + + public async prompt(): Promise { + // Don't prompt, just need to use the subwizard + } + + public shouldPrompt(): boolean { + return false; } - public shouldExecute(context: T): boolean { - return !context.image; + public async getSubWizard(context: T): Promise | undefined> { + // If more than the image tag changed, prompt for ingress again + if (getImageNameWithoutTag(context.containerApp?.template?.containers?.[context.containersIdx ?? 0].image ?? '') !== getImageNameWithoutTag(context.image ?? '')) { + return { + promptSteps: [new IngressPromptStep()], + }; + } + return undefined; } } diff --git a/src/commands/image/imageSource/containerRegistry/RegistryImageInputStep.ts b/src/commands/image/imageSource/containerRegistry/RegistryImageInputStep.ts index 8c5160954..7ef204139 100644 --- a/src/commands/image/imageSource/containerRegistry/RegistryImageInputStep.ts +++ b/src/commands/image/imageSource/containerRegistry/RegistryImageInputStep.ts @@ -18,7 +18,7 @@ export class RegistryImageInputStep extends AzureWizardPromptStep[]> { const picks: IAzureQuickPickItem[] = []; const registryDomain = getRegistryDomainFromContext(context); diff --git a/src/tree/ContainerAppItem.ts b/src/tree/ContainerAppItem.ts index 37c052e75..96b71da9d 100644 --- a/src/tree/ContainerAppItem.ts +++ b/src/tree/ContainerAppItem.ts @@ -212,6 +212,8 @@ export async function getContainerEnvelopeWithSecrets(context: IActionContext, s const concreteContainerAppEnvelope = >containerAppEnvelope; const webClient: ContainerAppsAPIClient = await createContainerAppsAPIClient([context, createSubscriptionContext(subscription)]); + // Todo: If a container app fails to provision, this list command may error with code 400 + // We need to determine the best way to break the user out of this state once they get locked into it concreteContainerAppEnvelope.configuration.secrets = ((await webClient.containerApps.listSecrets(containerApp.resourceGroup, containerApp.name)).value); concreteContainerAppEnvelope.configuration.registries ||= [];