Skip to content

Commit

Permalink
refactor(lambda): decompose methods for SAM sync/deploy/build (#6014)
Browse files Browse the repository at this point in the history
## Problem

The `sync.ts` are huge consisting of many helper functions that being
reused for deploy and build.

## Solution
- split shared methods into separate files for easier testing and
maintenance
- split sync.test.ts file into smaller file
- consolidate `paramsSourcePrompter` for both sync and deploy to avoid
duplicate code
- update PrompterTester helper class to clean up VS Code UI element for
early exit from consumer callback
- rename paramsSource prompter title 

---

License: I confirm that my contribution is made under the terms of the
Apache 2.0 license.

---------

Co-authored-by: Roger Zhang <[email protected]>
  • Loading branch information
vicheey and roger-zhangg authored Nov 20, 2024
1 parent 6fb2ecb commit 85a46ce
Show file tree
Hide file tree
Showing 25 changed files with 2,215 additions and 1,381 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
*/

import { CloudFormationTemplateRegistry } from '../../../shared/fs/templateRegistry'
import { createTemplatePrompter, TemplateItem } from '../../../shared/sam/sync'
import { syncMementoRootKey } from '../../../shared/sam/sync'

import { createExitPrompter } from '../../../shared/ui/common/exitPrompter'
import { createTemplatePrompter, TemplateItem } from '../../../shared/ui/sam/templatePrompter'
import { Wizard } from '../../../shared/wizards/wizard'

export interface OpenTemplateParams {
Expand All @@ -15,6 +17,6 @@ export interface OpenTemplateParams {
export class OpenTemplateWizard extends Wizard<OpenTemplateParams> {
public constructor(state: Partial<OpenTemplateParams>, registry: CloudFormationTemplateRegistry) {
super({ initState: state, exitPrompterProvider: createExitPrompter })
this.form.template.bindPrompter(() => createTemplatePrompter(registry))
this.form.template.bindPrompter(() => createTemplatePrompter(registry, syncMementoRootKey))
}
}
11 changes: 7 additions & 4 deletions packages/core/src/shared/sam/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import * as vscode from 'vscode'
import { TemplateItem, createTemplatePrompter } from './sync'
import { TemplateItem, createTemplatePrompter } from '../ui/sam/templatePrompter'
import { ChildProcess } from '../utilities/processUtils'
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
import { Wizard } from '../wizards/wizard'
Expand All @@ -19,10 +19,11 @@ import globals from '../extensionGlobals'
import { TreeNode } from '../treeview/resourceTreeDataProvider'
import { telemetry } from '../telemetry/telemetry'
import { getSpawnEnv } from '../env/resolveEnv'
import { getErrorCode, getProjectRoot, getSamCliPathAndVersion, isDotnetRuntime } from './utils'
import { getErrorCode, getProjectRoot, getSamCliPathAndVersion, isDotnetRuntime, updateRecentResponse } from './utils'
import { getConfigFileUri, validateSamBuildConfig } from './config'
import { runInTerminal } from './processTerminal'

const buildMementoRootKey = 'samcli.build.params'
export interface BuildParams {
readonly template: TemplateItem
readonly projectRoot: vscode.Uri
Expand Down Expand Up @@ -58,7 +59,7 @@ export function createParamsSourcePrompter(existValidSamconfig: boolean) {
)

return createQuickPick(items, {
title: 'Specify parameters for build',
title: 'Specify parameter source for build',
placeholder: 'Select configuration options for sam build',
buttons: createCommonButtons(samBuildUrl),
})
Expand Down Expand Up @@ -128,7 +129,7 @@ export class BuildWizard extends Wizard<BuildParams> {
this.arg = arg
if (this.arg === undefined) {
// "Build" command was invoked on the command palette.
this.form.template.bindPrompter(() => createTemplatePrompter(this.registry))
this.form.template.bindPrompter(() => createTemplatePrompter(this.registry, buildMementoRootKey))
this.form.projectRoot.setDefault(({ template }) => getProjectRoot(template))
this.form.paramsSource.bindPrompter(async ({ projectRoot }) => {
const existValidSamConfig: boolean | undefined = await validateSamBuildConfig(projectRoot)
Expand Down Expand Up @@ -216,6 +217,8 @@ export async function runBuild(arg?: TreeNode): Promise<SamBuildResult> {
const templatePath = params.template.uri.fsPath
buildFlags.push('--template', `${templatePath}`)

await updateRecentResponse(buildMementoRootKey, 'global', 'templatePath', templatePath)

try {
const { path: samCliPath } = await getSamCliPathAndVersion()

Expand Down
162 changes: 62 additions & 100 deletions packages/core/src/shared/sam/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import * as vscode from 'vscode'
import { ToolkitError, getLogger, globals } from '../../shared'
import { ToolkitError, globals } from '../../shared'
import * as CloudFormation from '../../shared/cloudformation/cloudformation'
import { getParameters } from '../../lambda/config/parameterUtils'
import { DefaultCloudFormationClient } from '../clients/cloudFormationClient'
Expand All @@ -17,14 +17,23 @@ import { createCommonButtons } from '../ui/buttons'
import { createExitPrompter } from '../ui/common/exitPrompter'
import { createRegionPrompter } from '../ui/common/region'
import { createInputBox } from '../ui/inputPrompter'
import { DataQuickPickItem, createQuickPick } from '../ui/pickerPrompter'
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 { TemplateItem, createStackPrompter, createBucketPrompter, createTemplatePrompter } from './sync'
import { getErrorCode, getProjectRoot, getSamCliPathAndVersion, getSource } from './utils'
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 { runInTerminal } from './processTerminal'

export interface DeployParams {
Expand All @@ -39,89 +48,24 @@ export interface DeployParams {
[key: string]: any
}

const mementoRootKey = 'samcli.deploy.params'
export function getRecentParams(identifier: string, key: string): string | undefined {
const root = globals.context.workspaceState.get(mementoRootKey, {} as Record<string, Record<string, string>>)
const deployMementoRootKey = 'samcli.deploy.params'

return root[identifier]?.[key]
}

export async function updateRecentParams(identifier: string, key: string, value: string | undefined) {
try {
const root = globals.context.workspaceState.get(mementoRootKey, {} as Record<string, Record<string, string>>)
await globals.context.workspaceState.update(mementoRootKey, {
...root,
[identifier]: { ...root[identifier], [key]: value },
})
} catch (err) {
getLogger().warn(`sam: unable to save response at key "${key}": %s`, err)
}
function getRecentDeployParams(identifier: string, key: string): string | undefined {
return getRecentResponse(deployMementoRootKey, identifier, key)
}

function createParamPromptProvider(name: string, defaultValue: string | undefined, templateFsPath: string = 'default') {
return createInputBox({
title: `Specify SAM parameter value for ${name}`,
buttons: createCommonButtons(samDeployUrl),
value: getRecentParams(templateFsPath, name) ?? defaultValue,
value: getRecentDeployParams(templateFsPath, name) ?? defaultValue,
})
}
function bucketSourcePrompter() {
const items: DataQuickPickItem<BucketSource>[] = [
{
label: 'Create a SAM CLI managed S3 bucket',
data: BucketSource.SamCliManaged,
},
{
label: 'Specify an S3 bucket',
data: BucketSource.UserProvided,
},
]

return createQuickPick(items, {
title: 'Specify S3 bucket for deployment artifacts',
placeholder: 'Press enter to proceed with highlighted option',
buttons: createCommonButtons(samDeployUrl),
})
}
function paramsSourcePrompter(existValidSamconfig: boolean | undefined) {
const items: DataQuickPickItem<ParamsSource>[] = [
{
label: 'Specify required parameters and save as defaults',
data: ParamsSource.SpecifyAndSave,
},
{
label: 'Specify required parameters',
data: ParamsSource.Specify,
},
]

if (existValidSamconfig) {
items.push({
label: 'Use default values from samconfig',
data: ParamsSource.SamConfig,
})
}

return createQuickPick(items, {
title: 'Specify parameters for deploy',
placeholder: 'Press enter to proceed with highlighted option',
buttons: createCommonButtons(samDeployUrl),
})
}
type DeployResult = {
isSuccess: boolean
}

export enum BucketSource {
SamCliManaged,
UserProvided,
}
export enum ParamsSource {
SpecifyAndSave,
Specify,
SamConfig,
}

export class DeployWizard extends Wizard<DeployParams> {
registry: CloudFormationTemplateRegistry
state: Partial<DeployParams>
Expand Down Expand Up @@ -153,50 +97,58 @@ export class DeployWizard extends Wizard<DeployParams> {
this.form.template.setDefault(templateItem)
this.form.projectRoot.setDefault(() => projectRootFolder)
this.form.paramsSource.bindPrompter(async () =>
paramsSourcePrompter(await validateSamDeployConfig(projectRootFolder))
createDeployParamsSourcePrompter(await validateSamDeployConfig(projectRootFolder))
)

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!)),
({ region }) =>
createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl),
{
showWhen: ({ paramsSource }) =>
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
}
)
this.form.bucketSource.bindPrompter(() => bucketSourcePrompter(), {
this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(), {
showWhen: ({ paramsSource }) =>
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
})
this.form.bucketName.bindPrompter(({ region }) => createBucketPrompter(new DefaultS3Client(region!)), {
showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided,
})
this.form.bucketName.bindPrompter(
({ region }) => createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey),
{
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))
this.form.template.bindPrompter(() => createTemplatePrompter(this.registry, deployMementoRootKey))
this.form.projectRoot.setDefault(({ template }) => getProjectRoot(template))
this.form.paramsSource.bindPrompter(async ({ projectRoot }) => {
const existValidSamConfig: boolean | undefined = await validateSamDeployConfig(projectRoot)
return paramsSourcePrompter(existValidSamConfig)
return createDeployParamsSourcePrompter(existValidSamConfig)
})
this.form.region.setDefault(() => this.arg.regionCode)
this.form.stackName.bindPrompter(
({ region }) => createStackPrompter(new DefaultCloudFormationClient(region!)),
({ region }) =>
createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl),
{
showWhen: ({ paramsSource }) =>
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
}
)
this.form.bucketSource.bindPrompter(() => bucketSourcePrompter(), {
this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(), {
showWhen: ({ paramsSource }) =>
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
})
this.form.bucketName.bindPrompter(({ region }) => createBucketPrompter(new DefaultS3Client(region!)), {
showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided,
})
this.form.bucketName.bindPrompter(
({ region }) => createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey),
{
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
Expand All @@ -206,54 +158,62 @@ export class DeployWizard extends Wizard<DeployParams> {
this.addParameterPromptersIfApplicable(templateUri)
this.form.template.setDefault(templateItem)
this.form.paramsSource.bindPrompter(async () =>
paramsSourcePrompter(await validateSamDeployConfig(projectRootFolder))
createDeployParamsSourcePrompter(await validateSamDeployConfig(projectRootFolder))
)

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!)),
({ region }) =>
createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl),
{
showWhen: ({ paramsSource }) =>
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
}
)
this.form.bucketSource.bindPrompter(() => bucketSourcePrompter(), {
this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(), {
showWhen: ({ paramsSource }) =>
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
})
this.form.bucketName.bindPrompter(({ region }) => createBucketPrompter(new DefaultS3Client(region!)), {
showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided,
})
this.form.bucketName.bindPrompter(
({ region }) => createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey),
{
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))
this.form.template.bindPrompter(() => createTemplatePrompter(this.registry, deployMementoRootKey))
this.form.projectRoot.setDefault(({ template }) => getProjectRoot(template))
this.form.paramsSource.bindPrompter(async ({ projectRoot }) => {
const existValidSamConfig: boolean | undefined = await validateSamDeployConfig(projectRoot)
return paramsSourcePrompter(existValidSamConfig)
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!)),
({ region }) =>
createStackPrompter(new DefaultCloudFormationClient(region!), deployMementoRootKey, samDeployUrl),
{
showWhen: ({ paramsSource }) =>
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
}
)
this.form.bucketSource.bindPrompter(() => bucketSourcePrompter(), {
this.form.bucketSource.bindPrompter(() => createBucketSourcePrompter(), {
showWhen: ({ paramsSource }) =>
paramsSource === ParamsSource.Specify || paramsSource === ParamsSource.SpecifyAndSave,
})
this.form.bucketName.bindPrompter(({ region }) => createBucketPrompter(new DefaultS3Client(region!)), {
showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided,
})
this.form.bucketName.bindPrompter(
({ region }) => createBucketNamePrompter(new DefaultS3Client(region!), deployMementoRootKey),
{
showWhen: ({ bucketSource }) => bucketSource === BucketSource.UserProvided,
}
)
}

return this
Expand Down Expand Up @@ -346,13 +306,15 @@ export async function runDeploy(arg: any, wizardParams?: DeployParams): Promise<
const paramsToSet: string[] = []
for (const name of parameterNames) {
if (params[name]) {
await updateRecentParams(params.template.uri.fsPath, name, params[name])
await updateRecentResponse(deployMementoRootKey, params.template.uri.fsPath, name, params[name])
paramsToSet.push(`ParameterKey=${name},ParameterValue=${params[name]}`)
}
}
paramsToSet.length > 0 && deployFlags.push('--parameter-overrides', paramsToSet.join(' '))
}

await updateRecentResponse(deployMementoRootKey, 'global', 'templatePath', params.template.uri.fsPath)

try {
const { path: samCliPath } = await getSamCliPathAndVersion()

Expand Down
Loading

0 comments on commit 85a46ce

Please sign in to comment.