From 525181b88a5bad13a5c0efa963de1ee0d45f60bc Mon Sep 17 00:00:00 2001 From: vicheey Date: Mon, 16 Dec 2024 11:25:21 -0800 Subject: [PATCH] fix(lambda): template parameter prompter not available from all entrypoints #6240 ## Problem SAM CLI guided deploy support template parameter override for both sync and deploy command. However, the AppBuilder wizard UI only support this feature for SAM deploy trigger from SAM template context menu or AppBuilder project node menu button. ## Solution - Implement nested wizard for template parameter for both sync and deploy action for all entry points. - Refactor DeployWizard class for consistency with SyncWizard class - Add unit test for validating correct backward flow and state restoration for both wizard. --- .../wizards/templateParametersWizard.ts | 62 +++ packages/core/src/shared/sam/deploy.ts | 304 ++++-------- packages/core/src/shared/sam/sync.ts | 160 ++++-- packages/core/src/shared/ui/wizardPrompter.ts | 4 + .../wizards/deployTypeWizard.test.ts | 10 +- .../core/src/test/shared/sam/deploy.test.ts | 341 +++++++++++-- .../core/src/test/shared/sam/sync.test.ts | 454 ++++++++++++++---- .../src/test/shared/wizards/prompterTester.ts | 43 ++ ...-bbd8c658-8ffa-4a1d-b9f1-9c81c3122b75.json | 4 + 9 files changed, 987 insertions(+), 395 deletions(-) create mode 100644 packages/core/src/awsService/appBuilder/wizards/templateParametersWizard.ts create mode 100644 packages/toolkit/.changes/next-release/Bug Fix-bbd8c658-8ffa-4a1d-b9f1-9c81c3122b75.json diff --git a/packages/core/src/awsService/appBuilder/wizards/templateParametersWizard.ts b/packages/core/src/awsService/appBuilder/wizards/templateParametersWizard.ts new file mode 100644 index 00000000000..75c80c4eda9 --- /dev/null +++ b/packages/core/src/awsService/appBuilder/wizards/templateParametersWizard.ts @@ -0,0 +1,62 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as vscode from 'vscode' +import { Wizard } from '../../../shared/wizards/wizard' +import { createExitPrompter } from '../../../shared/ui/common/exitPrompter' +import * as CloudFormation from '../../../shared/cloudformation/cloudformation' +import { createInputBox } from '../../../shared/ui/inputPrompter' +import { createCommonButtons } from '../../../shared/ui/buttons' +import { getRecentResponse, updateRecentResponse } from '../../../shared/sam/utils' +import { getParameters } from '../../../lambda/config/parameterUtils' + +export interface TemplateParametersForm { + [key: string]: any +} + +export class TemplateParametersWizard extends Wizard { + template: vscode.Uri + preloadedTemplate: CloudFormation.Template | undefined + samTemplateParameters: Map | undefined + samCommandUrl: vscode.Uri + commandMementoRootKey: string + + public constructor(template: vscode.Uri, samCommandUrl: vscode.Uri, commandMementoRootKey: string) { + super({ exitPrompterProvider: createExitPrompter }) + this.template = template + this.samCommandUrl = samCommandUrl + this.commandMementoRootKey = commandMementoRootKey + } + + public override async init(): Promise { + this.samTemplateParameters = await getParameters(this.template) + this.preloadedTemplate = await CloudFormation.load(this.template.fsPath) + const samTemplateNames = new Set(this.samTemplateParameters?.keys() ?? []) + + samTemplateNames.forEach((name) => { + if (this.preloadedTemplate) { + const defaultValue = this.preloadedTemplate.Parameters + ? (this.preloadedTemplate.Parameters[name]?.Default as string) + : undefined + this.form[name].bindPrompter(() => + this.createParamPromptProvider(name, defaultValue).transform(async (item) => { + await updateRecentResponse(this.commandMementoRootKey, this.template.fsPath, name, item) + return item + }) + ) + } + }) + + return this + } + + createParamPromptProvider(name: string, defaultValue: string | undefined) { + return createInputBox({ + title: `Specify SAM Template parameter value for ${name}`, + buttons: createCommonButtons(this.samCommandUrl), + value: getRecentResponse(this.commandMementoRootKey, this.template.fsPath, name) ?? defaultValue, + }) + } +} diff --git a/packages/core/src/shared/sam/deploy.ts b/packages/core/src/shared/sam/deploy.ts index 6353c46a0e5..a88e071f617 100644 --- a/packages/core/src/shared/sam/deploy.ts +++ b/packages/core/src/shared/sam/deploy.ts @@ -4,41 +4,38 @@ */ import * as vscode from 'vscode' +import { AWSTreeNodeBase } from '../treeview/nodes/awsTreeNodeBase' +import { TreeNode, isTreeNode } from '../treeview/resourceTreeDataProvider' import { ToolkitError, globals } from '../../shared' -import * as CloudFormation from '../../shared/cloudformation/cloudformation' -import { getParameters } from '../../lambda/config/parameterUtils' import { DefaultCloudFormationClient } from '../clients/cloudFormationClient' import { DefaultS3Client } from '../clients/s3Client' import { samDeployUrl } from '../constants' import { getSpawnEnv } from '../env/resolveEnv' import { CloudFormationTemplateRegistry } from '../fs/templateRegistry' import { telemetry } from '../telemetry' -import { createCommonButtons } from '../ui/buttons' import { createExitPrompter } from '../ui/common/exitPrompter' import { createRegionPrompter } from '../ui/common/region' -import { createInputBox } from '../ui/inputPrompter' import { ChildProcess } from '../utilities/processUtils' import { CancellationError } from '../utilities/timeoutUtils' -import { Wizard } from '../wizards/wizard' import { addTelemetryEnvVar } from './cli/samCliInvokerUtils' import { validateSamDeployConfig, SamConfig, writeSamconfigGlobal } from './config' import { BucketSource, createBucketSourcePrompter, createBucketNamePrompter } from '../ui/sam/bucketPrompter' import { createStackPrompter } from '../ui/sam/stackPrompter' import { TemplateItem, createTemplatePrompter } from '../ui/sam/templatePrompter' import { createDeployParamsSourcePrompter, ParamsSource } from '../ui/sam/paramsSourcePrompter' -import { - getErrorCode, - getProjectRoot, - getSamCliPathAndVersion, - getSource, - getRecentResponse, - updateRecentResponse, -} from './utils' +import { getErrorCode, getProjectRoot, getSamCliPathAndVersion, getSource, updateRecentResponse } from './utils' import { runInTerminal } from './processTerminal' +import { + TemplateParametersForm, + TemplateParametersWizard, +} from '../../awsService/appBuilder/wizards/templateParametersWizard' +import { getParameters } from '../../lambda/config/parameterUtils' +import { CompositeWizard } from '../wizards/compositeWizard' export interface DeployParams { readonly paramsSource: ParamsSource readonly template: TemplateItem + readonly templateParameters: any readonly region: string readonly stackName: string readonly bucketSource: BucketSource @@ -48,230 +45,120 @@ export interface DeployParams { [key: string]: any } -const deployMementoRootKey = 'samcli.deploy.params' - -function getRecentDeployParams(identifier: string, key: string): string | undefined { - return getRecentResponse(deployMementoRootKey, identifier, key) +export enum SamDeployEntryPoints { + SamTemplateFile, + RegionNodeContextMenu, + AppBuilderNodeButton, + CommandPalette, } -function createParamPromptProvider(name: string, defaultValue: string | undefined, templateFsPath: string = 'default') { - return createInputBox({ - title: `Specify SAM parameter value for ${name}`, - buttons: createCommonButtons(samDeployUrl), - value: getRecentDeployParams(templateFsPath, name) ?? defaultValue, - }) +function getDeployEntryPoint(arg: vscode.Uri | AWSTreeNodeBase | TreeNode | undefined) { + if (arg instanceof vscode.Uri) { + return SamDeployEntryPoints.SamTemplateFile + } else if (arg instanceof AWSTreeNodeBase) { + return SamDeployEntryPoints.RegionNodeContextMenu + } else if (isTreeNode(arg)) { + return SamDeployEntryPoints.AppBuilderNodeButton + } else { + return SamDeployEntryPoints.CommandPalette + } } +const deployMementoRootKey = 'samcli.deploy.params' type DeployResult = { isSuccess: boolean } -export class DeployWizard extends Wizard { +export class DeployWizard extends CompositeWizard { registry: CloudFormationTemplateRegistry state: Partial arg: any - samTemplateParameters: Map | undefined - preloadedTemplate: CloudFormation.Template | undefined public constructor( state: Partial, registry: CloudFormationTemplateRegistry, arg?: any, - samTemplateParameters?: Map, - preloadedTemplate?: CloudFormation.Template, shouldPromptExit: boolean = true ) { super({ initState: state, exitPrompterProvider: shouldPromptExit ? createExitPrompter : undefined }) this.registry = registry this.state = state this.arg = arg - this.samTemplateParameters = samTemplateParameters - this.preloadedTemplate = preloadedTemplate - if (this.arg && this.arg.path) { - // "Deploy" command was invoked on a template.yaml file. - const templateUri = this.arg as vscode.Uri - const templateItem = { uri: templateUri, data: {} } as TemplateItem - const projectRootFolder = getProjectRoot(templateItem) - - this.addParameterPromptersIfApplicable(templateUri) + } - this.form.template.setDefault(templateItem) - this.form.projectRoot.setDefault(() => projectRootFolder) - this.form.paramsSource.bindPrompter(async () => - createDeployParamsSourcePrompter(await validateSamDeployConfig(projectRootFolder)) - ) + public override async init(): Promise { + this.form.template.bindPrompter(() => createTemplatePrompter(this.registry, deployMementoRootKey, samDeployUrl)) - this.form.region.bindPrompter(() => createRegionPrompter().transform((r) => r.id), { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - }) - this.form.stackName.bindPrompter( - ({ region }) => - createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl), - { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - } - ) - this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(samDeployUrl), { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - }) - this.form.bucketName.bindPrompter( - ({ region }) => - createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey, samDeployUrl), - { - showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided, - } - ) - } else if (this.arg && this.arg.regionCode) { - // "Deploy" command was invoked on a regionNode. - this.form.template.bindPrompter(() => - createTemplatePrompter(this.registry, deployMementoRootKey, samDeployUrl) - ) - this.form.projectRoot.setDefault(({ template }) => getProjectRoot(template)) - this.form.paramsSource.bindPrompter(async ({ projectRoot }) => { - const existValidSamConfig: boolean | undefined = await validateSamDeployConfig(projectRoot) - return createDeployParamsSourcePrompter(existValidSamConfig) - }) - this.form.region.setDefault(() => this.arg.regionCode) - this.form.stackName.bindPrompter( - ({ region }) => - createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl), - { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - } - ) - this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(samDeployUrl), { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - }) - this.form.bucketName.bindPrompter( - ({ region }) => - createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey, samDeployUrl), - { - showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided, - } - ) - } else if (this.arg && this.arg.getTreeItem().resourceUri) { - // "Deploy" command was invoked on a TreeNode on the AppBuilder. - const templateUri = this.arg.getTreeItem().resourceUri as vscode.Uri - const templateItem = { uri: templateUri, data: {} } as TemplateItem - const projectRootFolder = getProjectRoot(templateItem) + this.form.templateParameters.bindPrompter( + async ({ template }) => + this.createWizardPrompter( + TemplateParametersWizard, + template!.uri, + samDeployUrl, + deployMementoRootKey + ), + { + showWhen: async ({ template }) => { + const samTemplateParameters = await getParameters(template!.uri) + return !!samTemplateParameters && samTemplateParameters.size > 0 + }, + } + ) - this.addParameterPromptersIfApplicable(templateUri) - this.form.template.setDefault(templateItem) - this.form.paramsSource.bindPrompter(async () => - createDeployParamsSourcePrompter(await validateSamDeployConfig(projectRootFolder)) - ) + this.form.projectRoot.setDefault(({ template }) => getProjectRoot(template)) - this.form.region.bindPrompter(() => createRegionPrompter().transform((r) => r.id), { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - }) - this.form.stackName.bindPrompter( - ({ region }) => - createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl), - { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - } - ) - this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(samDeployUrl), { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - }) - this.form.bucketName.bindPrompter( - ({ region }) => - createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey, samDeployUrl), - { - showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided, - } - ) - this.form.projectRoot.setDefault(() => getProjectRoot(templateItem)) - } else { - // "Deploy" command was invoked on the command palette. - this.form.template.bindPrompter(() => - createTemplatePrompter(this.registry, deployMementoRootKey, samDeployUrl) - ) - this.form.projectRoot.setDefault(({ template }) => getProjectRoot(template)) - this.form.paramsSource.bindPrompter(async ({ projectRoot }) => { - const existValidSamConfig: boolean | undefined = await validateSamDeployConfig(projectRoot) - return createDeployParamsSourcePrompter(existValidSamConfig) - }) - this.form.region.bindPrompter(() => createRegionPrompter().transform((r) => r.id), { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - }) - this.form.stackName.bindPrompter( - ({ region }) => - createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl), - { - showWhen: ({ paramsSource }) => - paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - } - ) - this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(samDeployUrl), { + this.form.paramsSource.bindPrompter(async ({ projectRoot }) => + createDeployParamsSourcePrompter(await validateSamDeployConfig(projectRoot)) + ) + this.form.region.bindPrompter(() => createRegionPrompter().transform((r) => r.id), { + showWhen: ({ paramsSource }) => + paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, + }) + this.form.stackName.bindPrompter( + ({ region }) => + createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl), + { showWhen: ({ paramsSource }) => paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, - }) - this.form.bucketName.bindPrompter( - ({ region }) => - createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey, samDeployUrl), - { - showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided, - } - ) - } - - return this - } - - /** - * Parse the template for parameters and add prompters for them if applicable. - * @param templateUri the uri of the template - */ - addParameterPromptersIfApplicable(templateUri: vscode.Uri) { - if (!this.samTemplateParameters || this.samTemplateParameters.size === 0) { - return - } - const parameterNames = new Set(this.samTemplateParameters.keys()) - parameterNames.forEach((name) => { - if (this.preloadedTemplate) { - const defaultValue = this.preloadedTemplate.Parameters - ? (this.preloadedTemplate.Parameters[name]?.Default as string) - : undefined - this.form[name].bindPrompter(() => createParamPromptProvider(name, defaultValue, templateUri.fsPath)) } + ) + this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(samDeployUrl), { + showWhen: ({ paramsSource }) => + paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, }) + this.form.bucketName.bindPrompter( + ({ region }) => createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey, samDeployUrl), + { + showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided, + } + ) + + return this } } export async function getDeployWizard(arg?: any, shouldPromptExit?: boolean): Promise { - let samTemplateParameters = new Map() - let preloadedTemplate: CloudFormation.Template | undefined - if (arg && arg.path) { - // "Deploy" command was invoked on a template.yaml file. - const templateUri = arg as vscode.Uri - samTemplateParameters = await getParameters(templateUri) - preloadedTemplate = await CloudFormation.load(templateUri.fsPath) - } else if (arg && arg.regionCode) { - // region node, do nothing - } else if (arg && arg.getTreeItem().resourceUri) { - const templateUri = arg.getTreeItem().resourceUri as vscode.Uri - samTemplateParameters = await getParameters(templateUri) - preloadedTemplate = await CloudFormation.load(templateUri.fsPath) + let initState: Partial + let templateUri: vscode.Uri + const entryPoint = getDeployEntryPoint(arg) + + switch (entryPoint) { + case SamDeployEntryPoints.SamTemplateFile: + initState = { template: { uri: arg as vscode.Uri, data: {} } as TemplateItem } + break + case SamDeployEntryPoints.RegionNodeContextMenu: + initState = { region: arg.regionCode } + break + case SamDeployEntryPoints.AppBuilderNodeButton: + templateUri = arg.getTreeItem().resourceUri as vscode.Uri + initState = { template: { uri: templateUri, data: {} } as TemplateItem } + break + case SamDeployEntryPoints.CommandPalette: + default: + initState = {} + break } - const deployParams: Partial = {} - const wizard = new DeployWizard( - deployParams, - await globals.templateRegistry, - arg, - samTemplateParameters, - preloadedTemplate, - shouldPromptExit - ) + const wizard = new DeployWizard(initState, await globals.templateRegistry, arg, shouldPromptExit) return wizard } @@ -308,14 +195,13 @@ export async function runDeploy(arg: any, wizardParams?: DeployParams): Promise< deployFlags.push('--save-params') } - const samTemplateParameters = await getParameters(params.template.uri) - if (samTemplateParameters.size > 0) { - const parameterNames = new Set(samTemplateParameters.keys()) + if (!!params.templateParameters && Object.entries(params.templateParameters).length > 0) { + const templateParameters = new Map(Object.entries(params.templateParameters)) const paramsToSet: string[] = [] - for (const name of parameterNames) { - if (params[name]) { - await updateRecentResponse(deployMementoRootKey, params.template.uri.fsPath, name, params[name]) - paramsToSet.push(`ParameterKey=${name},ParameterValue=${params[name]}`) + for (const [key, value] of templateParameters.entries()) { + if (value) { + await updateRecentResponse(deployMementoRootKey, params.template.uri.fsPath, key, value) + paramsToSet.push(`ParameterKey=${key},ParameterValue=${value}`) } } paramsToSet.length > 0 && deployFlags.push('--parameter-overrides', paramsToSet.join(' ')) diff --git a/packages/core/src/shared/sam/sync.ts b/packages/core/src/shared/sam/sync.ts index 9f4bdc6ab07..14e40840fa8 100644 --- a/packages/core/src/shared/sam/sync.ts +++ b/packages/core/src/shared/sam/sync.ts @@ -9,7 +9,6 @@ import * as vscode from 'vscode' import * as path from 'path' import * as localizedText from '../localizedText' import { DefaultS3Client } from '../clients/s3Client' -import { Wizard } from '../wizards/wizard' import { DataQuickPickItem, createMultiPick, createQuickPick } from '../ui/pickerPrompter' import { DefaultCloudFormationClient } from '../clients/cloudFormationClient' import * as CloudFormation from '../cloudformation/cloudformation' @@ -28,14 +27,14 @@ import { createExitPrompter } from '../ui/common/exitPrompter' import { getConfigFileUri, SamConfig, validateSamSyncConfig, writeSamconfigGlobal } from './config' import { cast, Optional } from '../utilities/typeConstructors' import { pushIf, toRecord } from '../utilities/collectionUtils' -import { getOverriddenParameters } from '../../lambda/config/parameterUtils' +import { getParameters } from '../../lambda/config/parameterUtils' import { addTelemetryEnvVar } from './cli/samCliInvokerUtils' import { samSyncParamUrl, samSyncUrl, samUpgradeUrl } from '../constants' import { openUrl } from '../utilities/vsCodeUtils' import { showOnce } from '../utilities/messages' import { IamConnection } from '../../auth/connection' import { CloudFormationTemplateRegistry } from '../fs/templateRegistry' -import { TreeNode } from '../treeview/resourceTreeDataProvider' +import { isTreeNode, TreeNode } from '../treeview/resourceTreeDataProvider' import { getSpawnEnv } from '../env/resolveEnv' import { getProjectRoot, @@ -52,6 +51,11 @@ import { ParamsSource, createSyncParamsSourcePrompter } from '../ui/sam/paramsSo import { createEcrPrompter } from '../ui/sam/ecrPrompter' import { BucketSource, createBucketNamePrompter, createBucketSourcePrompter } from '../ui/sam/bucketPrompter' import { runInTerminal } from './processTerminal' +import { + TemplateParametersForm, + TemplateParametersWizard, +} from '../../awsService/appBuilder/wizards/templateParametersWizard' +import { CompositeWizard } from '../wizards/compositeWizard' export interface SyncParams { readonly paramsSource: ParamsSource @@ -59,6 +63,7 @@ export interface SyncParams { readonly deployType: 'infra' | 'code' readonly projectRoot: vscode.Uri readonly template: TemplateItem + readonly templateParameters: any readonly stackName: string readonly bucketSource: BucketSource readonly bucketName: string @@ -147,7 +152,30 @@ export const syncFlagItems: DataQuickPickItem[] = [ }, ] -export class SyncWizard extends Wizard { +export enum SamSyncEntryPoints { + SamTemplateFile, + SamConfigFile, + RegionNodeContextMenu, + AppBuilderNodeButton, + CommandPalette, +} + +function getSyncEntryPoint(arg: vscode.Uri | AWSTreeNodeBase | TreeNode | undefined) { + if (arg instanceof vscode.Uri) { + if (arg.path.endsWith('samconfig.toml')) { + return SamSyncEntryPoints.SamConfigFile + } + return SamSyncEntryPoints.SamTemplateFile + } else if (arg instanceof AWSTreeNodeBase) { + return SamSyncEntryPoints.RegionNodeContextMenu + } else if (isTreeNode(arg)) { + return SamSyncEntryPoints.AppBuilderNodeButton + } else { + return SamSyncEntryPoints.CommandPalette + } +} + +export class SyncWizard extends CompositeWizard { registry: CloudFormationTemplateRegistry public constructor( state: Pick & Partial, @@ -156,17 +184,38 @@ export class SyncWizard extends Wizard { ) { super({ initState: state, exitPrompterProvider: shouldPromptExit ? createExitPrompter : undefined }) this.registry = registry + } + + public override async init(): Promise { this.form.template.bindPrompter(() => createTemplatePrompter(this.registry, syncMementoRootKey, samSyncUrl)) + this.form.templateParameters.bindPrompter( + async ({ template }) => + this.createWizardPrompter( + TemplateParametersWizard, + template!.uri, + samSyncUrl, + syncMementoRootKey + ), + { + showWhen: async ({ template }) => { + const samTemplateParameters = await getParameters(template!.uri) + return !!samTemplateParameters && samTemplateParameters.size > 0 + }, + } + ) + this.form.projectRoot.setDefault(({ template }) => getProjectRoot(template)) this.form.paramsSource.bindPrompter(async ({ projectRoot }) => { const existValidSamConfig: boolean | undefined = await validateSamSyncConfig(projectRoot) return createSyncParamsSourcePrompter(existValidSamConfig) }) + this.form.region.bindPrompter(() => createRegionPrompter().transform((r) => r.id), { showWhen: ({ paramsSource }) => paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, }) + this.form.stackName.bindPrompter( ({ region }) => createStackPrompter(new DefaultCloudFormationClient(region!), syncMementoRootKey, samSyncUrl), @@ -210,6 +259,7 @@ export class SyncWizard extends Wizard { paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave, } ) + return this } } @@ -296,30 +346,22 @@ export async function saveAndBindArgs(args: SyncParams): Promise<{ readonly boun return { boundArgs } } -async function loadLegacyParameterOverrides(template: TemplateItem) { - try { - const params = await getOverriddenParameters(template.uri) - if (!params) { - return - } - - return [...params.entries()].map(([k, v]) => `${k}=${v}`) - } catch (err) { - getLogger().warn(`sam: unable to load legacy parameter overrides: %s`, err) - } -} - export async function runSamSync(args: SyncParams) { telemetry.record({ lambdaPackageType: args.ecrRepoUri !== undefined ? 'Image' : 'Zip' }) const { path: samCliPath, parsedVersion } = await getSamCliPathAndVersion() const { boundArgs } = await saveAndBindArgs(args) - const overrides = await loadLegacyParameterOverrides(args.template) - if (overrides !== undefined) { - // Leaving this out of the definitions file as this is _very_ niche and specific to the - // implementation. Plus we would have to redefine `sam_sync` to add it. - telemetry.record({ isUsingTemplatesJson: true } as any) - boundArgs.push('--parameter-overrides', ...overrides) + + if (!!args.templateParameters && Object.entries(args.templateParameters).length > 0) { + const templateParameters = new Map(Object.entries(args.templateParameters)) + const paramsToSet: string[] = [] + for (const [key, value] of templateParameters.entries()) { + if (value) { + await updateRecentResponse(syncMementoRootKey, args.template.uri.fsPath, key, value) + paramsToSet.push(`ParameterKey=${key},ParameterValue=${value}`) + } + } + paramsToSet.length > 0 && boundArgs.push('--parameter-overrides', paramsToSet.join(' ')) } // '--no-watch' was not added until https://github.com/aws/aws-sam-cli/releases/tag/v1.77.0 @@ -431,21 +473,30 @@ export async function prepareSyncParams( ): Promise> { // Skip creating dependency layers by default for backwards compat const baseParams: Partial = { skipDependencyLayer: true } + const entryPoint = getSyncEntryPoint(arg) - if (arg instanceof AWSTreeNodeBase) { - // "Deploy" command was invoked on a regionNode. - return { ...baseParams, region: arg.regionCode } - } else if (arg instanceof vscode.Uri) { - if (arg.path.endsWith('samconfig.toml')) { - // "Deploy" command was invoked on a samconfig.toml file. - // TODO: add step to verify samconfig content to skip param source prompter - const config = await SamConfig.fromConfigFileUri(arg) + switch (entryPoint) { + case SamSyncEntryPoints.SamTemplateFile: { + const entryPointArg = arg as vscode.Uri + const template = { + uri: entryPointArg, + data: await CloudFormation.load(entryPointArg.fsPath, validate), + } + + return { + ...baseParams, + template: template, + projectRoot: getProjectRootUri(template.uri), + } + } + case SamSyncEntryPoints.SamConfigFile: { + const config = await SamConfig.fromConfigFileUri(arg as vscode.Uri) const params = getSyncParamsFromConfig(config) const projectRoot = vscode.Uri.joinPath(config.location, '..') const templateUri = params.templatePath ? vscode.Uri.file(path.resolve(projectRoot.fsPath, params.templatePath)) : undefined - const template = templateUri + const samConfigFileTemplate = templateUri ? { uri: templateUri, data: await CloudFormation.load(templateUri.fsPath), @@ -454,29 +505,38 @@ export async function prepareSyncParams( // Always use the dependency layer if the user specified to do so const skipDependencyLayer = !config.getCommandParam('sync', 'dependency_layer') - return { ...baseParams, ...params, template, projectRoot, skipDependencyLayer } as SyncParams + return { + ...baseParams, + ...params, + template: samConfigFileTemplate, + projectRoot, + skipDependencyLayer, + } as SyncParams } - - // "Deploy" command was invoked on a template.yaml file. - const template = { - uri: arg, - data: await CloudFormation.load(arg.fsPath, validate), + case SamSyncEntryPoints.RegionNodeContextMenu: { + const entryPointArg = arg as AWSTreeNodeBase + return { ...baseParams, region: entryPointArg.regionCode } } - - return { ...baseParams, template, projectRoot: getProjectRootUri(template.uri) } - } else if (arg && arg.getTreeItem()) { - // "Deploy" command was invoked on a TreeNode on the AppBuilder. - const templateUri = (arg.getTreeItem() as vscode.TreeItem).resourceUri - if (templateUri) { - const template = { - uri: templateUri, - data: await CloudFormation.load(templateUri.fsPath, validate), + case SamSyncEntryPoints.AppBuilderNodeButton: { + const entryPointArg = arg as TreeNode + const templateUri = (entryPointArg.getTreeItem() as vscode.TreeItem).resourceUri + if (templateUri) { + const template = { + uri: templateUri, + data: await CloudFormation.load(templateUri.fsPath, validate), + } + return { + ...baseParams, + template, + projectRoot: getProjectRootUri(templateUri), + } } - return { ...baseParams, template, projectRoot: getProjectRootUri(template.uri) } + return baseParams } + case SamSyncEntryPoints.CommandPalette: + default: + return baseParams } - - return baseParams } export type SamSyncResult = { diff --git a/packages/core/src/shared/ui/wizardPrompter.ts b/packages/core/src/shared/ui/wizardPrompter.ts index 64668b7340e..c9173e738ba 100644 --- a/packages/core/src/shared/ui/wizardPrompter.ts +++ b/packages/core/src/shared/ui/wizardPrompter.ts @@ -18,6 +18,9 @@ import { Prompter, PromptResult } from './prompter' * - {@link SingleNestedWizard} * - {@link DoubleNestedWizard} */ + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const WIZARD_PROMPTER = 'WIZARD_PROMPTER' export class WizardPrompter extends Prompter { public get recentItem(): any { return undefined @@ -56,6 +59,7 @@ export class WizardPrompter extends Prompter { } } + // eslint-disable-next-line @typescript-eslint/naming-convention protected async promptUser(): Promise> { this.response = await this.wizard.run() diff --git a/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts b/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts index a75c7c8b9a2..8b26cedb1ef 100644 --- a/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts +++ b/packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts @@ -63,10 +63,10 @@ describe('DeployTypeWizard', function () { assert.strictEqual(picker.items.length, 2) picker.acceptItem(picker.items[1]) }) - .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) - .handleInputBox('Specify SAM parameter value for DestinationBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { inputBox.acceptValue('my-destination-bucket-name') }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { @@ -98,6 +98,12 @@ describe('DeployTypeWizard', function () { assert.strictEqual(picker.items.length, 2) picker.acceptItem(picker.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (quickPick) => { // Need time to check samconfig.toml file and generate options await quickPick.untilReady() diff --git a/packages/core/src/test/shared/sam/deploy.test.ts b/packages/core/src/test/shared/sam/deploy.test.ts index 5e1d22297a7..c38c6eb8731 100644 --- a/packages/core/src/test/shared/sam/deploy.test.ts +++ b/packages/core/src/test/shared/sam/deploy.test.ts @@ -15,7 +15,7 @@ import assert from 'assert' import { getTestWindow } from '../vscode/window' import { DefaultCloudFormationClient } from '../../../shared/clients/cloudFormationClient' import { intoCollection } from '../../../shared/utilities/collectionUtils' -import { PrompterTester } from '../wizards/prompterTester' +import { clickBackButton, createPromptHandler, PrompterTester } from '../wizards/prompterTester' import { RegionNode } from '../../../awsexplorer/regionNode' import { createTestRegionProvider } from '../regions/testUtil' import { DefaultS3Client } from '../../../shared/clients/s3Client' @@ -33,6 +33,7 @@ import { CancellationError } from '../../../shared/utilities/timeoutUtils' import { TemplateItem } from '../../../shared/ui/sam/templatePrompter' import { ParamsSource } from '../../../shared/ui/sam/paramsSourcePrompter' import { BucketSource } from '../../../shared/ui/sam/bucketPrompter' +import { TestInputBox, TestQuickPick } from '../vscode/quickInput' describe('SAM DeployWizard', async function () { let sandbox: sinon.SinonSandbox @@ -89,10 +90,10 @@ describe('SAM DeployWizard', async function () { await testFolder.write('samconfig.toml', samconfigInvalidData) const prompterTester = PrompterTester.init() - .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) - .handleInputBox('Specify SAM parameter value for DestinationBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { inputBox.acceptValue('my-destination-bucket-name') }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { @@ -129,8 +130,8 @@ describe('SAM DeployWizard', async function () { const parameters = await (await getDeployWizard(templateFile)).run() assert(parameters) - assert.strictEqual(parameters.SourceBucketName, 'my-source-bucket-name') - assert.strictEqual(parameters.DestinationBucketName, 'my-destination-bucket-name') + assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) @@ -160,10 +161,10 @@ describe('SAM DeployWizard', async function () { await testFolder.write('samconfig.toml', samconfigCompleteData) const prompterTester = PrompterTester.init() - .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) - .handleInputBox('Specify SAM parameter value for DestinationBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { inputBox.acceptValue('my-destination-bucket-name') }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { @@ -181,8 +182,8 @@ describe('SAM DeployWizard', async function () { const parameters = await (await getDeployWizard(templateFile)).run() assert(parameters) - assert.strictEqual(parameters.SourceBucketName, 'my-source-bucket-name') - assert.strictEqual(parameters.DestinationBucketName, 'my-destination-bucket-name') + assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) @@ -232,6 +233,12 @@ describe('SAM DeployWizard', async function () { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { // Need time to check samconfig.toml file and generate options await quickPick.untilReady() @@ -269,11 +276,13 @@ describe('SAM DeployWizard', async function () { const parameters = await (await getDeployWizard(regionNode)).run() assert(parameters) - // assert.strictEqual(parameters.SourceBucketName, 'my-source-bucket-name') - // assert.strictEqual(parameters.DestinationBucketName, 'my-destination-bucket-name') + // assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + // assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.paramsSource, 1) assert.strictEqual(parameters.region, 'us-west-2') assert.strictEqual(parameters.stackName, 'stack1') @@ -306,6 +315,12 @@ describe('SAM DeployWizard', async function () { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { // Need time to check samconfig.toml file and generate options await quickPick.untilReady() @@ -324,6 +339,8 @@ describe('SAM DeployWizard', async function () { assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.paramsSource, 2) assert.strictEqual(parameters.region, 'us-west-2') assert(!parameters.stackName) @@ -361,10 +378,10 @@ describe('SAM DeployWizard', async function () { */ const prompterTester = PrompterTester.init() - .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) - .handleInputBox('Specify SAM parameter value for DestinationBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { inputBox.acceptValue('my-destination-bucket-name') }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { @@ -400,8 +417,8 @@ describe('SAM DeployWizard', async function () { const parameters = await (await getDeployWizard(appNode)).run() assert(parameters) - assert.strictEqual(parameters.SourceBucketName, 'my-source-bucket-name') - assert.strictEqual(parameters.DestinationBucketName, 'my-destination-bucket-name') + assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) @@ -432,10 +449,10 @@ describe('SAM DeployWizard', async function () { await testFolder.write('samconfig.toml', samconfigCompleteData) const prompterTester = PrompterTester.init() - .handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { inputBox.acceptValue('my-source-bucket-name') }) - .handleInputBox('Specify SAM parameter value for DestinationBucketName', (inputBox) => { + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { inputBox.acceptValue('my-destination-bucket-name') }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { @@ -453,8 +470,8 @@ describe('SAM DeployWizard', async function () { const parameters = await (await getDeployWizard(appNode)).run() assert(parameters) - assert.strictEqual(parameters.SourceBucketName, 'my-source-bucket-name') - assert.strictEqual(parameters.DestinationBucketName, 'my-destination-bucket-name') + assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) @@ -495,6 +512,12 @@ describe('SAM DeployWizard', async function () { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { // Need time to check samconfig.toml file and generate options await quickPick.untilReady() @@ -539,6 +562,8 @@ describe('SAM DeployWizard', async function () { assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.paramsSource, 1) assert.strictEqual(parameters.region, 'us-west-2') assert.strictEqual(parameters.stackName, 'stack3') @@ -547,6 +572,240 @@ describe('SAM DeployWizard', async function () { prompterTester.assertCallAll() }) + it('happy path without/invalid samconfig.toml with backward click', async () => { + /** + * Selection: + * - SourceBucketName : [Skip?] undefined + * - DestinationBucketName : [Skip?] undefined + * + * - template : [Select] template/yaml set + * - projectRoot : [Skip] automatically set + * - paramsSource : [Select] 2. ('Specify required parameters') + * - region : [Skip] automatically set from region node 'us-west-2' + * - stackName : [Select] 3. 'stack3' + * - bucketSource : [Select] 2. ('Specify an S3 bucket') + * - bucketName : [Select] 3. 'stack-3-bucket' + */ + + // Create a second project folder to simulate multiple project in 1 workspace + const testFolder2 = await TestFolder.create() + const templateFile2 = vscode.Uri.file(await testFolder2.write('template.yaml', validTemplateData)) + await (await globals.templateRegistry).addItem(templateFile2) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 2) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleInputBox( + 'Specify SAM Template parameter value for SourceBucketName', + (() => { + return createPromptHandler({ + default: async (inputBox: TestInputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }, + numbered: [ + { + order: [2], + handler: clickBackButton, + }, + { + order: [1], + handler: async (inputBox: TestInputBox) => { + inputBox.acceptValue('my-source-bucket-name-not-final') + }, + }, + ], + }) + })() + ) + .handleInputBox( + 'Specify SAM Template parameter value for DestinationBucketName', + (() => { + return createPromptHandler({ + default: async (inputBox: TestInputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }, + numbered: [ + { + order: [2], + handler: clickBackButton, + }, + ], + }) + })() + ) + .handleQuickPick( + 'Specify parameter source for deploy', + (() => { + return createPromptHandler({ + default: async (quickPick: TestQuickPick) => { + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 2) + assert.strictEqual(quickPick.items[1].label, 'Specify required parameters') + quickPick.acceptItem(quickPick.items[1]) + }, + numbered: [ + { + order: [1], + handler: async (quickPick: TestQuickPick) => { + await quickPick.untilReady() + quickPick.acceptItem(quickPick.items[0]) + }, + }, + { + order: [2], + handler: clickBackButton, + }, + ], + }) + })() + ) + .handleQuickPick( + 'Select a region', + (() => { + return createPromptHandler({ + default: async (quickPick: TestQuickPick) => { + await quickPick.untilReady() + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }, + numbered: [ + { + order: [2], + handler: clickBackButton, + }, + ], + }) + })() + ) + .handleQuickPick( + 'Select a CloudFormation Stack', + (() => { + return createPromptHandler({ + default: async (quickPick: TestQuickPick) => { + await quickPick.untilReady() + assert.strictEqual(quickPick.items[2].label, 'stack3') + quickPick.acceptItem(quickPick.items[2]) + }, + numbered: [ + { + order: [1], + handler: async (quickPick: TestQuickPick) => { + await quickPick.untilReady() + assert.strictEqual(quickPick.items[0].label, 'stack1') + quickPick.acceptItem(quickPick.items[0]) + }, + }, + { + order: [2], + handler: clickBackButton, + }, + ], + }) + })() + ) + .handleQuickPick( + 'Specify S3 bucket for deployment artifacts', + (() => { + return createPromptHandler({ + default: async (quickPick: TestQuickPick) => { + await quickPick.untilReady() + assert.strictEqual(quickPick.items[1].label, 'Specify an S3 bucket') + quickPick.acceptItem(quickPick.items[1]) + }, + numbered: [ + { + order: [2], + handler: clickBackButton, + }, + ], + }) + })() + ) + .handleQuickPick( + 'Select an S3 Bucket', + (() => { + return createPromptHandler({ + default: async (picker: TestQuickPick) => { + await picker.untilReady() + assert.strictEqual(picker.items[2].label, 'stack-3-bucket') + picker.acceptItem(picker.items[2]) + }, + numbered: [ + { + order: [1], + handler: clickBackButton, + }, + ], + }) + })() + ) + .build() + + const parameters = await (await getDeployWizard()).run() + assert(parameters) + + const expectedCallOrder = [ + 'Select a SAM/CloudFormation Template', + 'Specify SAM Template parameter value for SourceBucketName', + 'Specify SAM Template parameter value for DestinationBucketName', + 'Specify parameter source for deploy', + 'Select a region', + 'Select a CloudFormation Stack', + 'Specify S3 bucket for deployment artifacts', + 'Select an S3 Bucket', + 'Specify S3 bucket for deployment artifacts', + 'Select a CloudFormation Stack', + 'Select a region', + 'Specify parameter source for deploy', + 'Specify SAM Template parameter value for DestinationBucketName', + 'Specify SAM Template parameter value for SourceBucketName', + 'Select a SAM/CloudFormation Template', + 'Specify SAM Template parameter value for SourceBucketName', + 'Specify SAM Template parameter value for DestinationBucketName', + 'Specify parameter source for deploy', + 'Select a region', + 'Select a CloudFormation Stack', + 'Specify S3 bucket for deployment artifacts', + 'Select an S3 Bucket', + ] + + assert.deepStrictEqual( + { + ...parameters, + template: { uri: { fsPath: parameters.template.uri.fsPath } }, + projectRoot: { fsPath: parameters.projectRoot.fsPath }, + }, + { + template: { + uri: { + fsPath: templateFile.fsPath, + }, + }, + projectRoot: { + fsPath: projectRoot.fsPath, + }, + templateParameters: { + SourceBucketName: 'my-source-bucket-name', + DestinationBucketName: 'my-destination-bucket-name', + }, + paramsSource: 1, + region: 'us-west-2', + stackName: 'stack3', + bucketSource: 1, + bucketName: 'stack-3-bucket', + } + ) + + expectedCallOrder.forEach((title, index) => { + prompterTester.assertCallOrder(title, index + 1) + }) + }) + it('happy path with samconfig.toml', async () => { /** * Selection: @@ -579,6 +838,12 @@ describe('SAM DeployWizard', async function () { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for deploy', async (quickPick) => { // Need time to check samconfig.toml file and generate options await quickPick.untilReady() @@ -593,6 +858,8 @@ describe('SAM DeployWizard', async function () { assert(parameters) assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.templateParameters.SourceBucketName, 'my-source-bucket-name') + assert.strictEqual(parameters.templateParameters.DestinationBucketName, 'my-destination-bucket-name') assert.strictEqual(parameters.paramsSource, 2) assert(!parameters.region) assert(!parameters.stackName) @@ -663,8 +930,10 @@ describe('SAM Deploy', () => { // Mock result from DeployWizard; the Wizard is already tested separately mockDeployParams = { paramsSource: ParamsSource.SamConfig, - SourceBucketName: 'my-source-bucket-name', - DestinationBucketName: 'my-destination-bucket-name', + templateParameters: { + SourceBucketName: 'my-source-bucket-name', + DestinationBucketName: 'my-destination-bucket-name', + }, region: undefined, stackName: undefined, bucketName: undefined, @@ -710,8 +979,8 @@ describe('SAM Deploy', () => { '--config-file', `${samconfigFile}`, '--parameter-overrides', - `ParameterKey=SourceBucketName,ParameterValue=${mockDeployParams.SourceBucketName} ` + - `ParameterKey=DestinationBucketName,ParameterValue=${mockDeployParams.DestinationBucketName}`, + `ParameterKey=SourceBucketName,ParameterValue=${mockDeployParams.templateParameters.SourceBucketName} ` + + `ParameterKey=DestinationBucketName,ParameterValue=${mockDeployParams.templateParameters.DestinationBucketName}`, ], { spawnOptions: { @@ -739,8 +1008,10 @@ describe('SAM Deploy', () => { // Mock result from DeployWizard; the Wizard is already tested separately mockDeployParams = { paramsSource: ParamsSource.SamConfig, - SourceBucketName: 'my-source-bucket-name', - DestinationBucketName: 'my-destination-bucket-name', + templateParameters: { + SourceBucketName: 'my-source-bucket-name', + DestinationBucketName: 'my-destination-bucket-name', + }, // Simulate entry from region node when region ('us-east-1') differ from 'us-west-2' in samconfig.toml region: 'us-east-1', stackName: undefined, @@ -786,8 +1057,8 @@ describe('SAM Deploy', () => { '--config-file', `${samconfigFile}`, '--parameter-overrides', - `ParameterKey=SourceBucketName,ParameterValue=${mockDeployParams.SourceBucketName} ` + - `ParameterKey=DestinationBucketName,ParameterValue=${mockDeployParams.DestinationBucketName}`, + `ParameterKey=SourceBucketName,ParameterValue=${mockDeployParams.templateParameters.SourceBucketName} ` + + `ParameterKey=DestinationBucketName,ParameterValue=${mockDeployParams.templateParameters.DestinationBucketName}`, ], { spawnOptions: { @@ -815,8 +1086,10 @@ describe('SAM Deploy', () => { // Mock result from DeployWizard; the Wizard is already tested separately mockDeployParams = { paramsSource: ParamsSource.SpecifyAndSave, - SourceBucketName: 'my-source-bucket-name', - DestinationBucketName: 'my-destination-bucket-name', + templateParameters: { + SourceBucketName: 'my-source-bucket-name', + DestinationBucketName: 'my-destination-bucket-name', + }, region: 'us-east-1', stackName: 'stack1', bucketName: undefined, @@ -864,8 +1137,8 @@ describe('SAM Deploy', () => { 'CAPABILITY_NAMED_IAM', '--save-params', '--parameter-overrides', - `ParameterKey=SourceBucketName,ParameterValue=${mockDeployParams.SourceBucketName} ` + - `ParameterKey=DestinationBucketName,ParameterValue=${mockDeployParams.DestinationBucketName}`, + `ParameterKey=SourceBucketName,ParameterValue=${mockDeployParams.templateParameters.SourceBucketName} ` + + `ParameterKey=DestinationBucketName,ParameterValue=${mockDeployParams.templateParameters.DestinationBucketName}`, ], { spawnOptions: { @@ -899,8 +1172,10 @@ describe('SAM Deploy', () => { beforeEach(async () => { mockDeployParams = { paramsSource: ParamsSource.SpecifyAndSave, - SourceBucketName: 'my-source-bucket-name', - DestinationBucketName: 'my-destination-bucket-name', + templateParameters: { + SourceBucketName: 'my-source-bucket-name', + DestinationBucketName: 'my-destination-bucket-name', + }, region: 'us-east-1', stackName: 'stack1', bucketName: undefined, diff --git a/packages/core/src/test/shared/sam/sync.test.ts b/packages/core/src/test/shared/sam/sync.test.ts index 960019dd60e..627cee964f6 100644 --- a/packages/core/src/test/shared/sam/sync.test.ts +++ b/packages/core/src/test/shared/sam/sync.test.ts @@ -61,12 +61,13 @@ import { CloudFormationTemplateRegistry } from '../../../shared/fs/templateRegis import { samconfigCompleteData, samconfigInvalidData, validTemplateData } from '../../shared/sam/samTestUtils' import { assertTelemetry, assertTelemetryCurried } from '../../testUtil' -import { PrompterTester } from '../wizards/prompterTester' +import { clickBackButton, createPromptHandler, PrompterTester } from '../wizards/prompterTester' import { createTestRegionProvider } from '../regions/testUtil' import { ToolkitPromptSettings } from '../../../shared/settings' import { DefaultEcrClient } from '../../../shared/clients/ecrClient' import assert from 'assert' import { BucketSource } from '../../../shared/ui/sam/bucketPrompter' +import { TestInputBox, TestQuickPick } from '../vscode/quickInput' describe('SAM SyncWizard', async function () { const createTester = async (params?: Partial) => @@ -168,6 +169,10 @@ describe('SAM SyncWizard', async () => { * Selection: * - template : [Skip] automatically set * - projectRoot : [Skip] automatically set + * + * - SourceBucketName : [Select] prefill value + * - DestinationBucketName : [Select] prefill value + * * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') * - region : [Select] 'us-west-2' * - stackName : [Select] 1. 'stack1' @@ -180,6 +185,12 @@ describe('SAM SyncWizard', async () => { await testFolder.write('samconfig.toml', samconfigInvalidData) const prompterTester = PrompterTester.init() + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -248,6 +259,10 @@ describe('SAM SyncWizard', async () => { * Selection: * - template : [Skip] automatically set * - projectRoot : [Skip] automatically set + * + * - SourceBucketName : [Select] prefill value + * - DestinationBucketName : [Select] prefill value + * * - paramsSource : [Select] 3. ('Use default values from samconfig') * - region : [Skip] null; will use 'us-west-2' from samconfig * - stackName : [Skip] null; will use 'project-1' from samconfig @@ -260,6 +275,12 @@ describe('SAM SyncWizard', async () => { await testFolder.write('samconfig.toml', samconfigCompleteData) const prompterTester = PrompterTester.init() + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (quickPick) => { // Need time to check samconfig.toml file and generate options await quickPick.untilReady() @@ -305,6 +326,10 @@ describe('SAM SyncWizard', async () => { * Selection: * - template : [Skip] automatically set * - projectRoot : [Skip] automatically set + * + * - SourceBucketName : [Select] prefill value + * - DestinationBucketName : [Select] prefill value + * * - paramsSource : [Select] 2. ('Specify required parameters') * - region : [Select] 'us-west-2' * - stackName : [Select] 2. 'stack2' @@ -314,6 +339,12 @@ describe('SAM SyncWizard', async () => { */ const prompterTester = PrompterTester.init() + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -387,6 +418,10 @@ describe('SAM SyncWizard', async () => { * Selection: * - template : [Skip] automatically set * - projectRoot : [Skip] automatically set + * + * - SourceBucketName : [Select] prefill value + * - DestinationBucketName : [Select] prefill value + * * - paramsSource : [Select] 3. ('Use default values from samconfig') * - region : [Skip] null; will use value from samconfig file * - stackName : [Skip] null; will use value from samconfig file @@ -399,6 +434,12 @@ describe('SAM SyncWizard', async () => { await testFolder.write('samconfig.toml', samconfigCompleteData) const prompterTester = PrompterTester.init() + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -445,6 +486,10 @@ describe('SAM SyncWizard', async () => { * Selection: * - template : [Select] template/yaml set * - projectRoot : [Skip] automatically set + * + * - SourceBucketName : [Select] prefill value + * - DestinationBucketName : [Select] prefill value + * * - paramsSource : [Select] 2. ('Specify required parameters') * - region : [Skip] automatically set from region node 'us-west-2' * - stackName : [Select] 2. 'stack2' @@ -460,6 +505,12 @@ describe('SAM SyncWizard', async () => { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -547,6 +598,12 @@ describe('SAM SyncWizard', async () => { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -597,6 +654,12 @@ describe('SAM SyncWizard', async () => { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -628,7 +691,12 @@ describe('SAM SyncWizard', async () => { const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', '')) /** * Selection: + * - template : [Skip] automatically set * - projectRoot : [Skip] automatically set + * + * - SourceBucketName : [Select] prefill value + * - DestinationBucketName : [Select] prefill value + * * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') * - region : [Select] 'us-west-2' * - stackName : [Select] 2. 'stack2' @@ -644,6 +712,12 @@ describe('SAM SyncWizard', async () => { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -710,142 +784,282 @@ describe('SAM SyncWizard', async () => { assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container","--watch"]') prompterTester.assertCallAll() }) - }) - describe('entry: command palette', () => { - it('happy path with invalid samconfig.toml', async () => { - /** - * Selection: - * - template : [Select] template/yaml set - * - projectRoot : [Skip] automatically set - * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') - * - region : [Select] 'us-west-2' - * - stackName : [Select] 3. 'stack3' - * - bucketName : [select] 3. stack-3-bucket - * - syncFlags : [Select] all - */ + it('happy path with empty samconfig.toml with backward click', async () => { + // generate samconfig.toml in temporary test folder + const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', '')) - const prompterTester = PrompterTester.init() + const prompterTester = PrompterTester.init({ handlerTimeout: 300000 }) .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { // Need sometime to wait for the template to search for template file + // 1st await quickPick.untilReady() - assert.strictEqual(quickPick.items.length, 1) - assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) - .handleQuickPick('Specify parameter source for sync', async (picker) => { - // Need time to check samconfig.toml file and generate options - await picker.untilReady() - - assert.strictEqual(picker.items.length, 2) - assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') - assert.strictEqual(picker.items[1].label, 'Specify required parameters') - picker.acceptItem(picker.items[0]) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', async (inputBox) => { + // 2nd + // 6th + await inputBox.untilReady() + inputBox.acceptValue('my-source-bucket-name') }) + .handleInputBox( + 'Specify SAM Template parameter value for DestinationBucketName', + (() => { + return createPromptHandler({ + // 3rd + // 7th + // 9th + default: async (input: TestInputBox) => { + await input.untilReady() + input.acceptValue('my-destination-bucket-name') + }, + numbered: [ + { + // 5th + order: [2], + handler: clickBackButton, + }, + ], + }) + })() + ) + .handleQuickPick( + 'Specify parameter source for sync', + (() => { + return createPromptHandler({ + // 10th + default: async (picker: TestQuickPick) => { + await picker.untilReady() + picker.acceptItem(picker.items[1]) + }, + numbered: [ + { + // 4th + // 8th + order: [1, 2], + handler: clickBackButton, + }, + ], + }) + })() + ) .handleQuickPick('Select a region', (quickPick) => { + // 11th const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] quickPick.acceptItem(select || quickPick.items[0]) }) - .handleQuickPick('Select a CloudFormation Stack', async (picker) => { - await picker.untilReady() - assert.strictEqual(picker.items.length, 3) - assert.strictEqual(picker.items[0].label, 'stack1') - assert.strictEqual(picker.items[1].label, 'stack2') - assert.strictEqual(picker.items[2].label, 'stack3') - picker.acceptItem(picker.items[2]) + .handleQuickPick('Select a CloudFormation Stack', async (quickPick) => { + await quickPick.untilReady() + assert.strictEqual(quickPick.items[1].label, 'stack2') + quickPick.acceptItem(quickPick.items[1]) }) .handleQuickPick('Specify S3 bucket for deployment artifacts', async (picker) => { await picker.untilReady() - assert.strictEqual(picker.items.length, 2) - assert.deepStrictEqual(picker.items[0], { - label: 'Create a SAM CLI managed S3 bucket', - data: BucketSource.SamCliManaged, - }) - assert.deepStrictEqual(picker.items[1], { - label: 'Specify an S3 bucket', - data: BucketSource.UserProvided, - }) - picker.acceptItem(picker.items[0]) + picker.acceptItem(picker.items[1]) + }) + .handleQuickPick('Select an S3 Bucket', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items[1].label, 'stack-2-bucket') + picker.acceptItem(picker.items[1]) }) .handleQuickPick('Specify parameters for sync', async (picker) => { await picker.untilReady() - assert.strictEqual(picker.items.length, 9) const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] - picker.acceptItems(dependencyLayer, useContainer) + const watch = picker.items.filter((item) => item.label === 'Watch')[0] + picker.acceptItems(dependencyLayer, useContainer, watch) }) .build() - const parameters = await (await getSyncWizard('infra', undefined, false, false)).run() - + const parameters = await (await getSyncWizard('infra', samconfigFile, false, false)).run() assert(parameters) - assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) - assert.strictEqual(parameters.paramsSource, ParamsSource.SpecifyAndSave) + assert.strictEqual(parameters.paramsSource, ParamsSource.Specify) assert.strictEqual(parameters.region, 'us-west-2') - assert.strictEqual(parameters.stackName, 'stack3') - assert.strictEqual(parameters.bucketSource, BucketSource.SamCliManaged) - assert(!parameters.bucketName) + assert.strictEqual(parameters.stackName, 'stack2') + assert.strictEqual(parameters.bucketSource, BucketSource.UserProvided) + assert.strictEqual(parameters.bucketName, 'stack-2-bucket') assert.strictEqual(parameters.deployType, 'infra') assert.strictEqual(parameters.skipDependencyLayer, true) - assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container"]') + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container","--watch"]') prompterTester.assertCallAll() + const expectedCallOrder = [ + 'Select a SAM/CloudFormation Template', + 'Specify SAM Template parameter value for SourceBucketName', + 'Specify SAM Template parameter value for DestinationBucketName', + 'Specify parameter source for sync', + 'Specify SAM Template parameter value for DestinationBucketName', + 'Specify SAM Template parameter value for SourceBucketName', + 'Specify SAM Template parameter value for DestinationBucketName', + 'Specify parameter source for sync', + 'Specify SAM Template parameter value for DestinationBucketName', + 'Specify parameter source for sync', + 'Select a region', + 'Select a CloudFormation Stack', + 'Specify S3 bucket for deployment artifacts', + 'Select an S3 Bucket', + 'Specify parameters for sync', + ] + expectedCallOrder.forEach((title, index) => { + prompterTester.assertCallOrder(title, index + 1) + }) }) - it('happy path with valid samconfig.toml', async () => { - /** - * Selection: - * - template : [Select] template.yaml - * - projectRoot : [Skip] automatically set - * - paramsSource : [Select] 3. ('Use default values from samconfig') - * - region : [Skip] automatically set from region node 'us-west-2' - * - stackName : [Skip] null; will use value from samconfig file - * - bucketName : [Skip] automatically set for bucketSource option 1 - * - syncFlags : [Skip] null; will use flags from samconfig - */ - - // generate samconfig.toml in temporary test folder - await testFolder.write('samconfig.toml', samconfigCompleteData) - - const prompterTester = PrompterTester.init() - .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { - // Need sometime to wait for the template to search for template file - await quickPick.untilReady() - assert.strictEqual(quickPick.items.length, 1) - assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) - quickPick.acceptItem(quickPick.items[0]) - }) - .handleQuickPick('Specify parameter source for sync', async (picker) => { - // Need time to check samconfig.toml file and generate options - await picker.untilReady() - - assert.strictEqual(picker.items.length, 3) - assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') - assert.strictEqual(picker.items[1].label, 'Specify required parameters') - assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') - picker.acceptItem(picker.items[2]) - }) - .build() - - const parameters = await (await getSyncWizard('infra', undefined, false, false)).run() - - assert(parameters) + describe('entry: command palette', () => { + it('happy path with invalid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template/yaml set + * - projectRoot : [Skip] automatically set + * + * - SourceBucketName : [Select] prefill value + * - DestinationBucketName : [Select] prefill value + * + * - paramsSource : [Select] 1. ('Specify required parameters and save as defaults') + * - region : [Select] 'us-west-2' + * - stackName : [Select] 3. 'stack3' + * - bucketName : [select] 3. stack-3-bucket + * - syncFlags : [Select] all + */ + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) + .handleQuickPick('Specify parameter source for sync', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 2) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + picker.acceptItem(picker.items[0]) + }) + .handleQuickPick('Select a region', (quickPick) => { + const select = quickPick.items.filter((i) => i.detail === 'us-west-2')[0] + quickPick.acceptItem(select || quickPick.items[0]) + }) + .handleQuickPick('Select a CloudFormation Stack', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'stack1') + assert.strictEqual(picker.items[1].label, 'stack2') + assert.strictEqual(picker.items[2].label, 'stack3') + picker.acceptItem(picker.items[2]) + }) + .handleQuickPick('Specify S3 bucket for deployment artifacts', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 2) + assert.deepStrictEqual(picker.items[0], { + label: 'Create a SAM CLI managed S3 bucket', + data: BucketSource.SamCliManaged, + }) + assert.deepStrictEqual(picker.items[1], { + label: 'Specify an S3 bucket', + data: BucketSource.UserProvided, + }) + picker.acceptItem(picker.items[0]) + }) + .handleQuickPick('Specify parameters for sync', async (picker) => { + await picker.untilReady() + assert.strictEqual(picker.items.length, 9) + const dependencyLayer = picker.items.filter((item) => item.label === 'Dependency layer')[0] + const useContainer = picker.items.filter((item) => item.label === 'Use container')[0] + picker.acceptItems(dependencyLayer, useContainer) + }) + .build() + + const parameters = await (await getSyncWizard('infra', undefined, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SpecifyAndSave) + assert.strictEqual(parameters.region, 'us-west-2') + assert.strictEqual(parameters.stackName, 'stack3') + assert.strictEqual(parameters.bucketSource, BucketSource.SamCliManaged) + assert(!parameters.bucketName) + assert.strictEqual(parameters.deployType, 'infra') + assert.strictEqual(parameters.skipDependencyLayer, true) + assert.strictEqual(parameters.syncFlags, '["--dependency-layer","--use-container"]') + prompterTester.assertCallAll() + }) - assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) - assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) - assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) - assert.strictEqual(parameters.deployType, 'infra') - assert(!parameters.region) - assert(!parameters.stackName) - assert(!parameters.bucketSource) - assert(!parameters.syncFlags) - assert.strictEqual(parameters.skipDependencyLayer, true) - prompterTester.assertCallAll() + it('happy path with valid samconfig.toml', async () => { + /** + * Selection: + * - template : [Select] template.yaml + * - projectRoot : [Skip] automatically set + * + * - SourceBucketName : [Select] prefill value + * - DestinationBucketName : [Select] prefill value + * + * - paramsSource : [Select] 3. ('Use default values from samconfig') + * - region : [Skip] automatically set from region node 'us-west-2' + * - stackName : [Skip] null; will use value from samconfig file + * - bucketName : [Skip] automatically set for bucketSource option 1 + * - syncFlags : [Skip] null; will use flags from samconfig + */ + + // generate samconfig.toml in temporary test folder + await testFolder.write('samconfig.toml', samconfigCompleteData) + + const prompterTester = PrompterTester.init() + .handleQuickPick('Select a SAM/CloudFormation Template', async (quickPick) => { + // Need sometime to wait for the template to search for template file + await quickPick.untilReady() + assert.strictEqual(quickPick.items.length, 1) + assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) + quickPick.acceptItem(quickPick.items[0]) + }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) + .handleQuickPick('Specify parameter source for sync', async (picker) => { + // Need time to check samconfig.toml file and generate options + await picker.untilReady() + + assert.strictEqual(picker.items.length, 3) + assert.strictEqual(picker.items[0].label, 'Specify required parameters and save as defaults') + assert.strictEqual(picker.items[1].label, 'Specify required parameters') + assert.strictEqual(picker.items[2].label, 'Use default values from samconfig') + picker.acceptItem(picker.items[2]) + }) + .build() + + const parameters = await (await getSyncWizard('infra', undefined, false, false)).run() + + assert(parameters) + + assert.strictEqual(parameters.template.uri.fsPath, templateFile.fsPath) + assert.strictEqual(parameters.projectRoot.fsPath, projectRoot.fsPath) + assert.strictEqual(parameters.paramsSource, ParamsSource.SamConfig) + assert.strictEqual(parameters.deployType, 'infra') + assert(!parameters.region) + assert(!parameters.stackName) + assert(!parameters.bucketSource) + assert(!parameters.syncFlags) + assert.strictEqual(parameters.skipDependencyLayer, true) + prompterTester.assertCallAll() + }) }) }) }) - describe('SAM runSync', () => { let sandbox: sinon.SinonSandbox let testFolder: TestFolder @@ -948,6 +1162,12 @@ describe('SAM runSync', () => { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -1004,6 +1224,8 @@ describe('SAM runSync', () => { 'us-west-2', '--no-dependency-layer', '--save-params', + '--parameter-overrides', + 'ParameterKey=SourceBucketName,ParameterValue=my-source-bucket-name ParameterKey=DestinationBucketName,ParameterValue=my-destination-bucket-name', '--dependency-layer', '--use-container', ], @@ -1038,6 +1260,12 @@ describe('SAM runSync', () => { assert.strictEqual(quickPick.items[0].label, templateFile.fsPath) quickPick.acceptItem(quickPick.items[0]) }) + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -1087,6 +1315,8 @@ describe('SAM runSync', () => { 'us-west-2', '--no-dependency-layer', '--save-params', + '--parameter-overrides', + 'ParameterKey=SourceBucketName,ParameterValue=my-source-bucket-name ParameterKey=DestinationBucketName,ParameterValue=my-destination-bucket-name', '--dependency-layer', '--use-container', '--watch', @@ -1117,6 +1347,12 @@ describe('SAM runSync', () => { it('[entry: template file] specify flag should instantiate correct process in terminal', async () => { const prompterTester = PrompterTester.init() + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -1170,6 +1406,8 @@ describe('SAM runSync', () => { '--region', 'us-west-2', '--no-dependency-layer', + '--parameter-overrides', + 'ParameterKey=SourceBucketName,ParameterValue=my-source-bucket-name ParameterKey=DestinationBucketName,ParameterValue=my-destination-bucket-name', '--dependency-layer', '--use-container', ], @@ -1207,6 +1445,12 @@ describe('SAM runSync', () => { const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', samconfigCompleteData)) const prompterTester = PrompterTester.init() + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() @@ -1228,6 +1472,8 @@ describe('SAM runSync', () => { '--no-dependency-layer', '--config-file', `${samconfigFile.fsPath}`, + '--parameter-overrides', + 'ParameterKey=SourceBucketName,ParameterValue=my-source-bucket-name ParameterKey=DestinationBucketName,ParameterValue=my-destination-bucket-name', ], { spawnOptions: { @@ -1309,6 +1555,12 @@ describe('SAM runSync', () => { getTestWindow().onDidShowMessage((m) => m.items.find((i) => i.title === 'OK')?.select()) const prompterTester = PrompterTester.init() + .handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => { + inputBox.acceptValue('my-source-bucket-name') + }) + .handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => { + inputBox.acceptValue('my-destination-bucket-name') + }) .handleQuickPick('Specify parameter source for sync', async (picker) => { // Need time to check samconfig.toml file and generate options await picker.untilReady() diff --git a/packages/core/src/test/shared/wizards/prompterTester.ts b/packages/core/src/test/shared/wizards/prompterTester.ts index 04169830648..0fbe93f084a 100644 --- a/packages/core/src/test/shared/wizards/prompterTester.ts +++ b/packages/core/src/test/shared/wizards/prompterTester.ts @@ -4,6 +4,7 @@ */ import assert from 'assert' +import * as vscode from 'vscode' import { TestInputBox, TestQuickPick } from '../vscode/quickInput' import { getTestWindow, TestWindow } from '../vscode/window' import { waitUntil } from '../../../shared/utilities/timeoutUtils' @@ -132,3 +133,45 @@ export class PrompterTester { throw assert.fail(`Unexpected prompter titled: "${input.title}"`) } } + +export function createPromptHandler(config: { + default: (input: T) => Promise + numbered?: Array<{ + order: number[] + handler: (input: T) => Promise + }> +}) { + const generator = (function* () { + let currentIteration = 0 + const handlersMap = new Map() + + // Setup handlers map + config.numbered?.forEach((item) => { + item.order.forEach((orderNum) => { + handlersMap.set(orderNum, item.handler) + }) + }) + + while (true) { + currentIteration++ + const handler = handlersMap.get(currentIteration) + + if (handler) { + yield handler + } else { + yield config.default + } + } + })() + + // Return a function that advances the generator and executes the handler + return (picker: T) => { + const next = generator.next().value + return next(picker) + } +} + +export async function clickBackButton(input: TestQuickPick | TestInputBox) { + await input.untilReady() + input.pressButton(vscode.QuickInputButtons.Back) +} diff --git a/packages/toolkit/.changes/next-release/Bug Fix-bbd8c658-8ffa-4a1d-b9f1-9c81c3122b75.json b/packages/toolkit/.changes/next-release/Bug Fix-bbd8c658-8ffa-4a1d-b9f1-9c81c3122b75.json new file mode 100644 index 00000000000..d906da4d593 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-bbd8c658-8ffa-4a1d-b9f1-9c81c3122b75.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "AppBuilder : Support template parameters override for SAM deploy and sync for all entry points" +}