diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/DeploymentConfigurationListStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/DeploymentConfigurationListStep.ts index 5d06da034..9ea002b06 100644 --- a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/DeploymentConfigurationListStep.ts +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/DeploymentConfigurationListStep.ts @@ -13,6 +13,7 @@ import { ContainerAppVerifyStep } from "./azureResources/ContainerAppVerifyStep" import { ContainerRegistryVerifyStep } from "./azureResources/ContainerRegistryVerifyStep"; import { ResourceGroupVerifyStep } from "./azureResources/ResourceGroupVerifyStep"; import { DockerfileValidateStep } from "./filePaths/DockerfileValidateStep"; +import { EnvUseRemoteConfigurationPromptStep } from "./filePaths/EnvUseRemoteConfigurationPromptStep"; import { EnvValidateStep } from "./filePaths/EnvValidateStep"; import { SrcValidateStep } from "./filePaths/SrcValidateStep"; @@ -52,6 +53,9 @@ export class DeploymentConfigurationListStep extends AzureWizardPromptStep; public shouldExecute(context: WorkspaceDeploymentConfigurationContext): boolean { - return !!context.deploymentConfigurationSettings?.[this.deploymentSettingsKey] && !context?.[this.contextKey]; + return !!context.deploymentConfigurationSettings?.[this.deploymentSettingsKey]; } public createSuccessOutput(context: WorkspaceDeploymentConfigurationContext): ExecuteActivityOutput { diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/azureResources/ContainerAppVerifyStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/azureResources/ContainerAppVerifyStep.ts index 21bfc13c7..e06c60078 100644 --- a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/azureResources/ContainerAppVerifyStep.ts +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/azureResources/ContainerAppVerifyStep.ts @@ -18,6 +18,14 @@ export class ContainerAppVerifyStep extends AzureResourceVerifyStepBase { protected contextKey = 'containerApp' as const; protected async verifyResource(context: WorkspaceDeploymentConfigurationContext): Promise { + await ContainerAppVerifyStep.verifyContainerApp(context); + } + + static async verifyContainerApp(context: WorkspaceDeploymentConfigurationContext): Promise { + if (context.containerApp) { + return; + } + const client: ContainerAppsAPIClient = await createContainerAppsAPIClient(context); const containerApp: ContainerApp = await client.containerApps.get(nonNullValueAndProp(context.resourceGroup, 'name'), nonNullValueAndProp(context.deploymentConfigurationSettings, 'containerApp')); context.containerApp = ContainerAppItem.CreateContainerAppModel(containerApp); diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/azureResources/ResourceGroupVerifyStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/azureResources/ResourceGroupVerifyStep.ts index b03bdea24..ed2ecaf98 100644 --- a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/azureResources/ResourceGroupVerifyStep.ts +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/azureResources/ResourceGroupVerifyStep.ts @@ -16,6 +16,14 @@ export class ResourceGroupVerifyStep extends AzureResourceVerifyStepBase { protected contextKey = 'resourceGroup' as const; protected async verifyResource(context: WorkspaceDeploymentConfigurationContext): Promise { + await ResourceGroupVerifyStep.verifyResourceGroup(context); + } + + static async verifyResourceGroup(context: WorkspaceDeploymentConfigurationContext): Promise { + if (context.resourceGroup) { + return; + } + const resourceGroups: ResourceGroup[] = await ResourceGroupListStep.getResourceGroups(context); context.resourceGroup = resourceGroups.find(rg => rg.name === context.deploymentConfigurationSettings?.resourceGroup); } diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvUseRemoteConfigurationPromptStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvUseRemoteConfigurationPromptStep.ts new file mode 100644 index 000000000..b4d671545 --- /dev/null +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvUseRemoteConfigurationPromptStep.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type EnvironmentVar } from "@azure/arm-appcontainers"; +import { activitySuccessContext, activitySuccessIcon, AzureWizardPromptStep, createUniversallyUniqueContextValue, GenericTreeItem, nonNullProp, type IAzureQuickPickItem } from "@microsoft/vscode-azext-utils"; +import * as deepEqual from "deep-eql"; +import * as path from "path"; +import { ext } from "../../../../../extensionVariables"; +import { localize } from "../../../../../utils/localize"; +import { EnvFileListStep } from "../../../../image/imageSource/EnvFileListStep"; +import { type WorkspaceDeploymentConfigurationContext } from "../WorkspaceDeploymentConfigurationContext"; +import { ContainerAppVerifyStep } from "../azureResources/ContainerAppVerifyStep"; +import { ResourceGroupVerifyStep } from "../azureResources/ResourceGroupVerifyStep"; + +export const useRemoteConfigurationKey: string = 'useRemoteConfiguration'; +export const useRemoteConfigurationLabel: string = localize('useRemoteConfiguration', 'Remote env configuration'); +export const useRemoteConfigurationOutputMessage: string = localize('usingRemoteConfiguration', 'Using the existing remote env configuration.'); + +export class EnvUseRemoteConfigurationPromptStep extends AzureWizardPromptStep { + private shouldPromptEnvVars?: boolean; + + public async configureBeforePrompt(context: T): Promise { + const envPath: string | undefined = context.deploymentConfigurationSettings?.envPath; + if (!envPath || envPath === useRemoteConfigurationKey) { + this.shouldPromptEnvVars = false; + return; + } + + // Verify the resource group and container app ahead of time so we can inspect the current environment variables + try { + await ResourceGroupVerifyStep.verifyResourceGroup(context); + await ContainerAppVerifyStep.verifyContainerApp(context); + } catch { + this.shouldPromptEnvVars = false; + return; + } + + const rootPath: string = nonNullProp(context, 'rootFolder').uri.fsPath; + const fullPath: string = path.join(rootPath, envPath); + const configVars: EnvironmentVar[] = await EnvFileListStep.parseEnvironmentVariablesFromEnvPath(fullPath); + const currentVars: EnvironmentVar[] = context.containerApp?.template?.containers?.[0]?.env ?? []; + this.shouldPromptEnvVars = !deepEqual(configVars, currentVars); + } + + public async prompt(context: T): Promise { + const useEnvFile: string = localize('useEnvFile', 'Env file'); + const useExistingConfig: string = localize('useExistingConfig', 'Existing configuration'); + + const picks: IAzureQuickPickItem[] = [ + { label: useEnvFile, data: useEnvFile, description: localize('local', 'Local') }, + { label: useExistingConfig, data: useExistingConfig, description: localize('remote', 'Remote') }, + ]; + + const result: string = (await context.ui.showQuickPick(picks, { + placeHolder: localize('selectSourcePrompt', 'Detected conflicts between local and remote environment variables. Select source.'), + suppressPersistence: true, + })).data; + + if (result === useEnvFile) { + // Do nothing, later steps will verify the file path + } else if (result === useExistingConfig) { + context.envPath = ''; + context.activityChildren?.push( + new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['envUseExistingConfigurationPromptStepItem', activitySuccessContext]), + label: useRemoteConfigurationLabel, + iconPath: activitySuccessIcon, + }) + ); + ext.outputChannel.appendLog(useRemoteConfigurationOutputMessage); + } + } + + public shouldPrompt(): boolean { + return !!this.shouldPromptEnvVars; + } +} diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvValidateStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvValidateStep.ts index e56d5783d..3ae9c3633 100644 --- a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvValidateStep.ts +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/EnvValidateStep.ts @@ -3,17 +3,82 @@ * Licensed under the MIT License. See License.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { activityFailContext, activityFailIcon, activitySuccessContext, activitySuccessIcon, AzExtFsExtra, AzureWizardExecuteStep, createUniversallyUniqueContextValue, GenericTreeItem, nonNullProp, nonNullValueAndProp, type ExecuteActivityOutput } from "@microsoft/vscode-azext-utils"; +import * as path from "path"; +import { type Progress } from "vscode"; +import { localize } from "../../../../../utils/localize"; import { type WorkspaceDeploymentConfigurationContext } from "../WorkspaceDeploymentConfigurationContext"; -import { FilePathsVerifyStep } from "./FilePathsVerifyStep"; +import { useRemoteConfigurationKey, useRemoteConfigurationLabel, useRemoteConfigurationOutputMessage } from "./EnvUseRemoteConfigurationPromptStep"; +import { verifyingFilePaths } from "./FilePathsVerifyStep"; -export class EnvValidateStep extends FilePathsVerifyStep { - priority: number = 120; +export class EnvValidateStep extends AzureWizardExecuteStep { + public priority: number = 120; + private configEnvPath: string; - deploymentSettingskey = 'envPath' as const; - contextKey = 'envPath' as const; - fileType = 'environment variables'; + public async execute(context: T, progress: Progress<{ message?: string; increment?: number; }>): Promise { + this.options.continueOnFail = true; + progress.report({ message: verifyingFilePaths }); - public shouldExecute(context: WorkspaceDeploymentConfigurationContext): boolean { - return !context.envPath; + this.configEnvPath = nonNullValueAndProp(context.deploymentConfigurationSettings, 'envPath'); + if (this.configEnvPath === useRemoteConfigurationKey) { + context.envPath = ''; + return; + } + + const rootPath: string = nonNullProp(context, 'rootFolder').uri.fsPath; + if (!context.envPath && this.configEnvPath) { + const fullPath: string = path.join(rootPath, this.configEnvPath); + if (await this.verifyFilePath(fullPath)) { + context.envPath = fullPath; + } + } + } + + public shouldExecute(context: T): boolean { + return context.envPath === undefined; + } + + public async verifyFilePath(path: string): Promise { + if (await AzExtFsExtra.pathExists(path)) { + return true; + } else { + throw new Error(localize('fileNotFound', 'File not found: {0}', path)); + } + } + + public createSuccessOutput(context: T): ExecuteActivityOutput { + if (context.envPath === undefined) { + return {}; + } + + let label: string; + let message: string; + if (context.envPath === '') { + label = useRemoteConfigurationLabel; + message = useRemoteConfigurationOutputMessage; + } else { + label = localize('envPathLabel', 'Env path'); + message = localize('envPathSuccessMessage', 'Successfully verified {0} path "{1}".', '.env', context.envPath); + } + + return { + item: new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['envValidateStepSuccessItem', activitySuccessContext]), + label, + iconPath: activitySuccessIcon + }), + message, + }; + } + + public createFailOutput(): ExecuteActivityOutput { + return { + item: new GenericTreeItem(undefined, { + contextValue: createUniversallyUniqueContextValue(['envValidateStepFailItem', activityFailContext]), + label: localize('envPathLabel', 'Env path'), + iconPath: activityFailIcon + }), + message: localize('envPathFailMessage', 'Failed to verify {0} path "{1}".', '.env', this.configEnvPath), + }; } } diff --git a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/FilePathsVerifyStep.ts b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/FilePathsVerifyStep.ts index 909138bcb..fd6f244f7 100644 --- a/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/FilePathsVerifyStep.ts +++ b/src/commands/deployWorkspaceProject/deploymentConfiguration/workspace/filePaths/FilePathsVerifyStep.ts @@ -10,6 +10,8 @@ import { localize } from "../../../../../utils/localize"; import { type DeploymentConfigurationSettings } from "../../../settings/DeployWorkspaceProjectSettingsV2"; import { type WorkspaceDeploymentConfigurationContext } from "../WorkspaceDeploymentConfigurationContext"; +export const verifyingFilePaths: string = localize('verifyingFilePaths', `Verifying file paths...`); + export abstract class FilePathsVerifyStep extends AzureWizardExecuteStep { abstract deploymentSettingskey: keyof DeploymentConfigurationSettings; abstract contextKey: keyof Pick; @@ -23,7 +25,7 @@ export abstract class FilePathsVerifyStep extends AzureWizardExecuteStep): Promise { this.options.continueOnFail = true; - progress.report({ message: localize('verifyingFilePaths', `Verifying file paths...`) }); + progress.report({ message: verifyingFilePaths }); const rootPath: string = nonNullProp(context, 'rootFolder').uri.fsPath; diff --git a/src/commands/deployWorkspaceProject/internal/DeployWorkspaceProjectSaveSettingsStep.ts b/src/commands/deployWorkspaceProject/internal/DeployWorkspaceProjectSaveSettingsStep.ts index 5488c666b..68a3e786b 100644 --- a/src/commands/deployWorkspaceProject/internal/DeployWorkspaceProjectSaveSettingsStep.ts +++ b/src/commands/deployWorkspaceProject/internal/DeployWorkspaceProjectSaveSettingsStep.ts @@ -8,6 +8,7 @@ import * as path from "path"; import { type Progress, type WorkspaceFolder } from "vscode"; import { relativeSettingsFilePath } from "../../../constants"; import { localize } from "../../../utils/localize"; +import { useRemoteConfigurationKey } from "../deploymentConfiguration/workspace/filePaths/EnvUseRemoteConfigurationPromptStep"; import { type DeploymentConfigurationSettings } from "../settings/DeployWorkspaceProjectSettingsV2"; import { dwpSettingUtilsV2 } from "../settings/dwpSettingUtilsV2"; import { type DeployWorkspaceProjectInternalContext } from "./DeployWorkspaceProjectInternalContext"; @@ -30,7 +31,7 @@ export class DeployWorkspaceProjectSaveSettingsStep extends AzureWizardExecuteSt type: 'AcrDockerBuildRequest', dockerfilePath: path.relative(rootFolder.uri.fsPath, nonNullProp(context, 'dockerfilePath')), srcPath: path.relative(rootFolder.uri.fsPath, context.srcPath || rootFolder.uri.fsPath) || ".", - envPath: context.envPath ? path.relative(rootFolder.uri.fsPath, context.envPath) : "", + envPath: this.getEnvPath(rootFolder, context.envPath), resourceGroup: context.resourceGroup?.name, containerApp: context.containerApp?.name, containerRegistry: context.registry?.name, @@ -49,6 +50,16 @@ export class DeployWorkspaceProjectSaveSettingsStep extends AzureWizardExecuteSt return !!context.shouldSaveDeploySettings; } + private getEnvPath(rootFolder: WorkspaceFolder, envPath: string | undefined): string { + if (envPath === undefined) { + return ''; + } else if (envPath === '') { + return useRemoteConfigurationKey; + } else { + return path.relative(rootFolder.uri.fsPath, envPath); + } + } + public createSuccessOutput(context: DeployWorkspaceProjectInternalContext): ExecuteActivityOutput { context.telemetry.properties.didSaveSettings = 'true'; diff --git a/src/commands/deployWorkspaceProject/internal/ShouldSaveDeploySettingsPromptStep.ts b/src/commands/deployWorkspaceProject/internal/ShouldSaveDeploySettingsPromptStep.ts index 19d79b44c..d81c5b713 100644 --- a/src/commands/deployWorkspaceProject/internal/ShouldSaveDeploySettingsPromptStep.ts +++ b/src/commands/deployWorkspaceProject/internal/ShouldSaveDeploySettingsPromptStep.ts @@ -7,6 +7,7 @@ import { AzureWizardPromptStep, nonNullProp } from "@microsoft/vscode-azext-util import * as path from "path"; import { type WorkspaceFolder } from "vscode"; import { localize } from "../../../utils/localize"; +import { useRemoteConfigurationKey } from "../deploymentConfiguration/workspace/filePaths/EnvUseRemoteConfigurationPromptStep"; import { type DeploymentConfigurationSettings } from "../settings/DeployWorkspaceProjectSettingsV2"; import { dwpSettingUtilsV2 } from "../settings/dwpSettingUtilsV2"; import { type DeployWorkspaceProjectInternalContext } from "./DeployWorkspaceProjectInternalContext"; @@ -20,6 +21,15 @@ export class ShouldSaveDeploySettingsPromptStep extends AzureWizardPromptStep extends AzureWizardPr super(); } - public async prompt(context: T): Promise { - const existingData = context.template?.containers?.[context.containersIdx ?? 0].env ?? context.containerApp?.template?.containers?.[context.containersIdx ?? 0].env; - context.envPath ??= await this.promptForEnvPath(context, !!existingData /** showHasExistingData */); - - if (!context.envPath && existingData) { - context.environmentVariables = existingData; - this._setEnvironmentVariableOption = SetEnvironmentVariableOption.UseExisting; - } else { - context.environmentVariables = await this.parseEnvironmentVariablesFromEnvPath(context.envPath); + public async configureBeforePrompt(context: T): Promise { + if (context.environmentVariables?.length === 0) { + context.telemetry.properties.environmentVariableFileCount = '0'; + this._setEnvironmentVariableOption = SetEnvironmentVariableOption.NoDotEnv; } if (this._setEnvironmentVariableOption) { @@ -53,10 +48,17 @@ export class EnvFileListStep extends AzureWizardPr } } - public async configureBeforePrompt(context: T): Promise { - if (context.environmentVariables?.length === 0) { - context.telemetry.properties.environmentVariableFileCount = '0'; - this._setEnvironmentVariableOption = SetEnvironmentVariableOption.NoDotEnv; + public async prompt(context: T): Promise { + const existingData = context.template?.containers?.[context.containersIdx ?? 0].env ?? context.containerApp?.template?.containers?.[context.containersIdx ?? 0].env; + context.envPath ??= await this.promptForEnvPath(context, !!existingData /** showHasExistingData */); + + if (context.envPath) { + context.environmentVariables = await this.parseEnvironmentVariablesFromEnvPath(context.envPath); + } else if (existingData) { + context.environmentVariables = existingData; + this._setEnvironmentVariableOption = SetEnvironmentVariableOption.UseExisting; + } else { + this._setEnvironmentVariableOption = SetEnvironmentVariableOption.SkipForNow; } if (this._setEnvironmentVariableOption) { @@ -83,10 +85,16 @@ export class EnvFileListStep extends AzureWizardPr } this._setEnvironmentVariableOption = SetEnvironmentVariableOption.ProvideFile; + return await EnvFileListStep.parseEnvironmentVariablesFromEnvPath(envPath); + } + + public static async parseEnvironmentVariablesFromEnvPath(envPath: string | undefined): Promise { + if (!envPath || !await AzExtFsExtra.pathExists(envPath)) { + return []; + } const data: string = await AzExtFsExtra.readFile(envPath); const envData: DotenvParseOutput = parse(data); - return Object.keys(envData).map(name => { return { name, value: envData[name] } }); }