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" +}