Skip to content

Commit 525181b

Browse files
authored
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.
1 parent a4c3443 commit 525181b

File tree

9 files changed

+987
-395
lines changed

9 files changed

+987
-395
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import { Wizard } from '../../../shared/wizards/wizard'
8+
import { createExitPrompter } from '../../../shared/ui/common/exitPrompter'
9+
import * as CloudFormation from '../../../shared/cloudformation/cloudformation'
10+
import { createInputBox } from '../../../shared/ui/inputPrompter'
11+
import { createCommonButtons } from '../../../shared/ui/buttons'
12+
import { getRecentResponse, updateRecentResponse } from '../../../shared/sam/utils'
13+
import { getParameters } from '../../../lambda/config/parameterUtils'
14+
15+
export interface TemplateParametersForm {
16+
[key: string]: any
17+
}
18+
19+
export class TemplateParametersWizard extends Wizard<TemplateParametersForm> {
20+
template: vscode.Uri
21+
preloadedTemplate: CloudFormation.Template | undefined
22+
samTemplateParameters: Map<string, { required: boolean }> | undefined
23+
samCommandUrl: vscode.Uri
24+
commandMementoRootKey: string
25+
26+
public constructor(template: vscode.Uri, samCommandUrl: vscode.Uri, commandMementoRootKey: string) {
27+
super({ exitPrompterProvider: createExitPrompter })
28+
this.template = template
29+
this.samCommandUrl = samCommandUrl
30+
this.commandMementoRootKey = commandMementoRootKey
31+
}
32+
33+
public override async init(): Promise<this> {
34+
this.samTemplateParameters = await getParameters(this.template)
35+
this.preloadedTemplate = await CloudFormation.load(this.template.fsPath)
36+
const samTemplateNames = new Set<string>(this.samTemplateParameters?.keys() ?? [])
37+
38+
samTemplateNames.forEach((name) => {
39+
if (this.preloadedTemplate) {
40+
const defaultValue = this.preloadedTemplate.Parameters
41+
? (this.preloadedTemplate.Parameters[name]?.Default as string)
42+
: undefined
43+
this.form[name].bindPrompter(() =>
44+
this.createParamPromptProvider(name, defaultValue).transform(async (item) => {
45+
await updateRecentResponse(this.commandMementoRootKey, this.template.fsPath, name, item)
46+
return item
47+
})
48+
)
49+
}
50+
})
51+
52+
return this
53+
}
54+
55+
createParamPromptProvider(name: string, defaultValue: string | undefined) {
56+
return createInputBox({
57+
title: `Specify SAM Template parameter value for ${name}`,
58+
buttons: createCommonButtons(this.samCommandUrl),
59+
value: getRecentResponse(this.commandMementoRootKey, this.template.fsPath, name) ?? defaultValue,
60+
})
61+
}
62+
}

packages/core/src/shared/sam/deploy.ts

Lines changed: 95 additions & 209 deletions
Large diffs are not rendered by default.

packages/core/src/shared/sam/sync.ts

Lines changed: 110 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import * as vscode from 'vscode'
99
import * as path from 'path'
1010
import * as localizedText from '../localizedText'
1111
import { DefaultS3Client } from '../clients/s3Client'
12-
import { Wizard } from '../wizards/wizard'
1312
import { DataQuickPickItem, createMultiPick, createQuickPick } from '../ui/pickerPrompter'
1413
import { DefaultCloudFormationClient } from '../clients/cloudFormationClient'
1514
import * as CloudFormation from '../cloudformation/cloudformation'
@@ -28,14 +27,14 @@ import { createExitPrompter } from '../ui/common/exitPrompter'
2827
import { getConfigFileUri, SamConfig, validateSamSyncConfig, writeSamconfigGlobal } from './config'
2928
import { cast, Optional } from '../utilities/typeConstructors'
3029
import { pushIf, toRecord } from '../utilities/collectionUtils'
31-
import { getOverriddenParameters } from '../../lambda/config/parameterUtils'
30+
import { getParameters } from '../../lambda/config/parameterUtils'
3231
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
3332
import { samSyncParamUrl, samSyncUrl, samUpgradeUrl } from '../constants'
3433
import { openUrl } from '../utilities/vsCodeUtils'
3534
import { showOnce } from '../utilities/messages'
3635
import { IamConnection } from '../../auth/connection'
3736
import { CloudFormationTemplateRegistry } from '../fs/templateRegistry'
38-
import { TreeNode } from '../treeview/resourceTreeDataProvider'
37+
import { isTreeNode, TreeNode } from '../treeview/resourceTreeDataProvider'
3938
import { getSpawnEnv } from '../env/resolveEnv'
4039
import {
4140
getProjectRoot,
@@ -52,13 +51,19 @@ import { ParamsSource, createSyncParamsSourcePrompter } from '../ui/sam/paramsSo
5251
import { createEcrPrompter } from '../ui/sam/ecrPrompter'
5352
import { BucketSource, createBucketNamePrompter, createBucketSourcePrompter } from '../ui/sam/bucketPrompter'
5453
import { runInTerminal } from './processTerminal'
54+
import {
55+
TemplateParametersForm,
56+
TemplateParametersWizard,
57+
} from '../../awsService/appBuilder/wizards/templateParametersWizard'
58+
import { CompositeWizard } from '../wizards/compositeWizard'
5559

5660
export interface SyncParams {
5761
readonly paramsSource: ParamsSource
5862
readonly region: string
5963
readonly deployType: 'infra' | 'code'
6064
readonly projectRoot: vscode.Uri
6165
readonly template: TemplateItem
66+
readonly templateParameters: any
6267
readonly stackName: string
6368
readonly bucketSource: BucketSource
6469
readonly bucketName: string
@@ -147,7 +152,30 @@ export const syncFlagItems: DataQuickPickItem<string>[] = [
147152
},
148153
]
149154

150-
export class SyncWizard extends Wizard<SyncParams> {
155+
export enum SamSyncEntryPoints {
156+
SamTemplateFile,
157+
SamConfigFile,
158+
RegionNodeContextMenu,
159+
AppBuilderNodeButton,
160+
CommandPalette,
161+
}
162+
163+
function getSyncEntryPoint(arg: vscode.Uri | AWSTreeNodeBase | TreeNode | undefined) {
164+
if (arg instanceof vscode.Uri) {
165+
if (arg.path.endsWith('samconfig.toml')) {
166+
return SamSyncEntryPoints.SamConfigFile
167+
}
168+
return SamSyncEntryPoints.SamTemplateFile
169+
} else if (arg instanceof AWSTreeNodeBase) {
170+
return SamSyncEntryPoints.RegionNodeContextMenu
171+
} else if (isTreeNode(arg)) {
172+
return SamSyncEntryPoints.AppBuilderNodeButton
173+
} else {
174+
return SamSyncEntryPoints.CommandPalette
175+
}
176+
}
177+
178+
export class SyncWizard extends CompositeWizard<SyncParams> {
151179
registry: CloudFormationTemplateRegistry
152180
public constructor(
153181
state: Pick<SyncParams, 'deployType'> & Partial<SyncParams>,
@@ -156,17 +184,38 @@ export class SyncWizard extends Wizard<SyncParams> {
156184
) {
157185
super({ initState: state, exitPrompterProvider: shouldPromptExit ? createExitPrompter : undefined })
158186
this.registry = registry
187+
}
188+
189+
public override async init(): Promise<this> {
159190
this.form.template.bindPrompter(() => createTemplatePrompter(this.registry, syncMementoRootKey, samSyncUrl))
191+
this.form.templateParameters.bindPrompter(
192+
async ({ template }) =>
193+
this.createWizardPrompter<TemplateParametersWizard, TemplateParametersForm>(
194+
TemplateParametersWizard,
195+
template!.uri,
196+
samSyncUrl,
197+
syncMementoRootKey
198+
),
199+
{
200+
showWhen: async ({ template }) => {
201+
const samTemplateParameters = await getParameters(template!.uri)
202+
return !!samTemplateParameters && samTemplateParameters.size > 0
203+
},
204+
}
205+
)
206+
160207
this.form.projectRoot.setDefault(({ template }) => getProjectRoot(template))
161208

162209
this.form.paramsSource.bindPrompter(async ({ projectRoot }) => {
163210
const existValidSamConfig: boolean | undefined = await validateSamSyncConfig(projectRoot)
164211
return createSyncParamsSourcePrompter(existValidSamConfig)
165212
})
213+
166214
this.form.region.bindPrompter(() => createRegionPrompter().transform((r) => r.id), {
167215
showWhen: ({ paramsSource }) =>
168216
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
169217
})
218+
170219
this.form.stackName.bindPrompter(
171220
({ region }) =>
172221
createStackPrompter(new DefaultCloudFormationClient(region!), syncMementoRootKey, samSyncUrl),
@@ -210,6 +259,7 @@ export class SyncWizard extends Wizard<SyncParams> {
210259
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
211260
}
212261
)
262+
return this
213263
}
214264
}
215265

@@ -296,30 +346,22 @@ export async function saveAndBindArgs(args: SyncParams): Promise<{ readonly boun
296346
return { boundArgs }
297347
}
298348

299-
async function loadLegacyParameterOverrides(template: TemplateItem) {
300-
try {
301-
const params = await getOverriddenParameters(template.uri)
302-
if (!params) {
303-
return
304-
}
305-
306-
return [...params.entries()].map(([k, v]) => `${k}=${v}`)
307-
} catch (err) {
308-
getLogger().warn(`sam: unable to load legacy parameter overrides: %s`, err)
309-
}
310-
}
311-
312349
export async function runSamSync(args: SyncParams) {
313350
telemetry.record({ lambdaPackageType: args.ecrRepoUri !== undefined ? 'Image' : 'Zip' })
314351

315352
const { path: samCliPath, parsedVersion } = await getSamCliPathAndVersion()
316353
const { boundArgs } = await saveAndBindArgs(args)
317-
const overrides = await loadLegacyParameterOverrides(args.template)
318-
if (overrides !== undefined) {
319-
// Leaving this out of the definitions file as this is _very_ niche and specific to the
320-
// implementation. Plus we would have to redefine `sam_sync` to add it.
321-
telemetry.record({ isUsingTemplatesJson: true } as any)
322-
boundArgs.push('--parameter-overrides', ...overrides)
354+
355+
if (!!args.templateParameters && Object.entries(args.templateParameters).length > 0) {
356+
const templateParameters = new Map<string, string>(Object.entries(args.templateParameters))
357+
const paramsToSet: string[] = []
358+
for (const [key, value] of templateParameters.entries()) {
359+
if (value) {
360+
await updateRecentResponse(syncMementoRootKey, args.template.uri.fsPath, key, value)
361+
paramsToSet.push(`ParameterKey=${key},ParameterValue=${value}`)
362+
}
363+
}
364+
paramsToSet.length > 0 && boundArgs.push('--parameter-overrides', paramsToSet.join(' '))
323365
}
324366

325367
// '--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(
431473
): Promise<Partial<SyncParams>> {
432474
// Skip creating dependency layers by default for backwards compat
433475
const baseParams: Partial<SyncParams> = { skipDependencyLayer: true }
476+
const entryPoint = getSyncEntryPoint(arg)
434477

435-
if (arg instanceof AWSTreeNodeBase) {
436-
// "Deploy" command was invoked on a regionNode.
437-
return { ...baseParams, region: arg.regionCode }
438-
} else if (arg instanceof vscode.Uri) {
439-
if (arg.path.endsWith('samconfig.toml')) {
440-
// "Deploy" command was invoked on a samconfig.toml file.
441-
// TODO: add step to verify samconfig content to skip param source prompter
442-
const config = await SamConfig.fromConfigFileUri(arg)
478+
switch (entryPoint) {
479+
case SamSyncEntryPoints.SamTemplateFile: {
480+
const entryPointArg = arg as vscode.Uri
481+
const template = {
482+
uri: entryPointArg,
483+
data: await CloudFormation.load(entryPointArg.fsPath, validate),
484+
}
485+
486+
return {
487+
...baseParams,
488+
template: template,
489+
projectRoot: getProjectRootUri(template.uri),
490+
}
491+
}
492+
case SamSyncEntryPoints.SamConfigFile: {
493+
const config = await SamConfig.fromConfigFileUri(arg as vscode.Uri)
443494
const params = getSyncParamsFromConfig(config)
444495
const projectRoot = vscode.Uri.joinPath(config.location, '..')
445496
const templateUri = params.templatePath
446497
? vscode.Uri.file(path.resolve(projectRoot.fsPath, params.templatePath))
447498
: undefined
448-
const template = templateUri
499+
const samConfigFileTemplate = templateUri
449500
? {
450501
uri: templateUri,
451502
data: await CloudFormation.load(templateUri.fsPath),
@@ -454,29 +505,38 @@ export async function prepareSyncParams(
454505
// Always use the dependency layer if the user specified to do so
455506
const skipDependencyLayer = !config.getCommandParam('sync', 'dependency_layer')
456507

457-
return { ...baseParams, ...params, template, projectRoot, skipDependencyLayer } as SyncParams
508+
return {
509+
...baseParams,
510+
...params,
511+
template: samConfigFileTemplate,
512+
projectRoot,
513+
skipDependencyLayer,
514+
} as SyncParams
458515
}
459-
460-
// "Deploy" command was invoked on a template.yaml file.
461-
const template = {
462-
uri: arg,
463-
data: await CloudFormation.load(arg.fsPath, validate),
516+
case SamSyncEntryPoints.RegionNodeContextMenu: {
517+
const entryPointArg = arg as AWSTreeNodeBase
518+
return { ...baseParams, region: entryPointArg.regionCode }
464519
}
465-
466-
return { ...baseParams, template, projectRoot: getProjectRootUri(template.uri) }
467-
} else if (arg && arg.getTreeItem()) {
468-
// "Deploy" command was invoked on a TreeNode on the AppBuilder.
469-
const templateUri = (arg.getTreeItem() as vscode.TreeItem).resourceUri
470-
if (templateUri) {
471-
const template = {
472-
uri: templateUri,
473-
data: await CloudFormation.load(templateUri.fsPath, validate),
520+
case SamSyncEntryPoints.AppBuilderNodeButton: {
521+
const entryPointArg = arg as TreeNode
522+
const templateUri = (entryPointArg.getTreeItem() as vscode.TreeItem).resourceUri
523+
if (templateUri) {
524+
const template = {
525+
uri: templateUri,
526+
data: await CloudFormation.load(templateUri.fsPath, validate),
527+
}
528+
return {
529+
...baseParams,
530+
template,
531+
projectRoot: getProjectRootUri(templateUri),
532+
}
474533
}
475-
return { ...baseParams, template, projectRoot: getProjectRootUri(template.uri) }
534+
return baseParams
476535
}
536+
case SamSyncEntryPoints.CommandPalette:
537+
default:
538+
return baseParams
477539
}
478-
479-
return baseParams
480540
}
481541

482542
export type SamSyncResult = {

packages/core/src/shared/ui/wizardPrompter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { Prompter, PromptResult } from './prompter'
1818
* - {@link SingleNestedWizard}
1919
* - {@link DoubleNestedWizard}
2020
*/
21+
22+
// eslint-disable-next-line @typescript-eslint/naming-convention
23+
export const WIZARD_PROMPTER = 'WIZARD_PROMPTER'
2124
export class WizardPrompter<T> extends Prompter<T> {
2225
public get recentItem(): any {
2326
return undefined
@@ -56,6 +59,7 @@ export class WizardPrompter<T> extends Prompter<T> {
5659
}
5760
}
5861

62+
// eslint-disable-next-line @typescript-eslint/naming-convention
5963
protected async promptUser(): Promise<PromptResult<T>> {
6064
this.response = await this.wizard.run()
6165

packages/core/src/test/awsService/appBuilder/wizards/deployTypeWizard.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,10 @@ describe('DeployTypeWizard', function () {
6363
assert.strictEqual(picker.items.length, 2)
6464
picker.acceptItem(picker.items[1])
6565
})
66-
.handleInputBox('Specify SAM parameter value for SourceBucketName', (inputBox) => {
66+
.handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => {
6767
inputBox.acceptValue('my-source-bucket-name')
6868
})
69-
.handleInputBox('Specify SAM parameter value for DestinationBucketName', (inputBox) => {
69+
.handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => {
7070
inputBox.acceptValue('my-destination-bucket-name')
7171
})
7272
.handleQuickPick('Specify parameter source for deploy', async (quickPick) => {
@@ -98,6 +98,12 @@ describe('DeployTypeWizard', function () {
9898
assert.strictEqual(picker.items.length, 2)
9999
picker.acceptItem(picker.items[0])
100100
})
101+
.handleInputBox('Specify SAM Template parameter value for SourceBucketName', (inputBox) => {
102+
inputBox.acceptValue('my-source-bucket-name')
103+
})
104+
.handleInputBox('Specify SAM Template parameter value for DestinationBucketName', (inputBox) => {
105+
inputBox.acceptValue('my-destination-bucket-name')
106+
})
101107
.handleQuickPick('Specify parameter source for sync', async (quickPick) => {
102108
// Need time to check samconfig.toml file and generate options
103109
await quickPick.untilReady()

0 commit comments

Comments
 (0)