+
+
+ New template with visual builder
+
+
+
+ Current workspace template
+
+
+
+
+ See more application example...
+
diff --git a/packages/core/resources/walkthrough/appBuilder/AppPickerResource/API.png b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/API.png
new file mode 100644
index 00000000000..3cda59fbf48
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/API.png differ
diff --git a/packages/core/resources/walkthrough/appBuilder/AppPickerResource/AppComposer.png b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/AppComposer.png
new file mode 100644
index 00000000000..51ceb392f05
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/AppComposer.png differ
diff --git a/packages/core/resources/walkthrough/appBuilder/AppPickerResource/CustomTemplate.png b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/CustomTemplate.png
new file mode 100644
index 00000000000..6ebe6fe27a6
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/CustomTemplate.png differ
diff --git a/packages/core/resources/walkthrough/appBuilder/AppPickerResource/S3.png b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/S3.png
new file mode 100644
index 00000000000..7ea7eebd23b
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/AppPickerResource/S3.png differ
diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoop.md b/packages/core/resources/walkthrough/appBuilder/InnerLoop.md
new file mode 100644
index 00000000000..97f54e9dbca
--- /dev/null
+++ b/packages/core/resources/walkthrough/appBuilder/InnerLoop.md
@@ -0,0 +1,12 @@
+
Build your code
+Compile your code and install dependencies with SAM CLI so you can invoke it locally.
+
+
Select function to invoke
+Find the function you want to invoke in Application Builder and use the icon to open the invoke and debug view.
+
+
Invoke your function
+Configure a payload to use for invoking your function.
+
+
View your execution results
+The VS Code panel will display the results of your invocation.
+
diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-1.jpg b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-1.jpg
new file mode 100644
index 00000000000..b1236191118
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-1.jpg differ
diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-2.jpg b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-2.jpg
new file mode 100644
index 00000000000..d7c0295e5ce
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-2.jpg differ
diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-3.jpg b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-3.jpg
new file mode 100644
index 00000000000..471cf590830
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-3.jpg differ
diff --git a/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-4.jpg b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-4.jpg
new file mode 100644
index 00000000000..c0df6db61b0
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/InnerLoopResource/walkthrough-local-4.jpg differ
diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoop.md b/packages/core/resources/walkthrough/appBuilder/RemoteLoop.md
new file mode 100644
index 00000000000..f811771127b
--- /dev/null
+++ b/packages/core/resources/walkthrough/appBuilder/RemoteLoop.md
@@ -0,0 +1,12 @@
+
Deploy your application
+Use SAM CLI to deploy your application template to the cloud.
+
+
Select deployed function to invoke
+Find the function you want to invoke in Application Builder and use the icon to open the remote invocation view.
+
+
Invoke your function with a payload
+Configure a payload to use for invoking your function.
+
+
View your execution result in the output panel
+The VS Code panel will display the results of your invocation.
+
diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-1.jpg b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-1.jpg
new file mode 100644
index 00000000000..c036fc93eaf
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-1.jpg differ
diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-2.jpg b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-2.jpg
new file mode 100644
index 00000000000..e542edc38f4
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-2.jpg differ
diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-3.jpg b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-3.jpg
new file mode 100644
index 00000000000..891cfdc4aa0
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-3.jpg differ
diff --git a/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-4.jpg b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-4.jpg
new file mode 100644
index 00000000000..738a7d0b91f
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/RemoteLoopResource/walkthrough-remote-4.jpg differ
diff --git a/packages/core/resources/walkthrough/appBuilder/install.png b/packages/core/resources/walkthrough/appBuilder/install.png
new file mode 100644
index 00000000000..cdbae8b83a6
Binary files /dev/null and b/packages/core/resources/walkthrough/appBuilder/install.png differ
diff --git a/packages/core/resources/walkthrough/setup-connect.md b/packages/core/resources/walkthrough/setup-connect.md
index 44a1584838a..e78f612f4f7 100644
--- a/packages/core/resources/walkthrough/setup-connect.md
+++ b/packages/core/resources/walkthrough/setup-connect.md
@@ -12,7 +12,7 @@ Choose the most appropriate method based on your requirements.
## Connect to AWS through the Toolkit for VS Code
-1. [Click here](command:aws.login) to open the configuration wizard to connect to AWS.
+1. [Click here](command:aws.toolkit.login) to open the configuration wizard to connect to AWS.
> This command can also be accessed through the [Command Palette](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/toolkit-navigation.html#command-locations) by choosing **AWS: >Connect to AWS**\.
>
diff --git a/packages/core/resources/walkthrough/setup-region.md b/packages/core/resources/walkthrough/setup-region.md
index 292eb1ad220..50a9101e6bb 100644
--- a/packages/core/resources/walkthrough/setup-region.md
+++ b/packages/core/resources/walkthrough/setup-region.md
@@ -4,7 +4,7 @@ When you set up your credentials, the AWS Toolkit for Visual Studio Code automat
## Add a Region to the AWS Explorer
-1. [Click here](command:aws.showRegion) to select a Region to add or remove.
+1. [Click here](command:aws.toolkit.showRegion) to select a Region to add or remove.
> This command can also be accessed through the [Command Palette](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/toolkit-navigation.html#command-locations) by choosing **AWS: Show or Hide Regions**\.
>
diff --git a/packages/core/src/applicationcomposer/commands/openTemplateInComposer.ts b/packages/core/src/applicationcomposer/commands/openTemplateInComposer.ts
index ed50e4ee2da..351673754c2 100644
--- a/packages/core/src/applicationcomposer/commands/openTemplateInComposer.ts
+++ b/packages/core/src/applicationcomposer/commands/openTemplateInComposer.ts
@@ -8,11 +8,13 @@ import { ApplicationComposerManager } from '../webviewManager'
import vscode from 'vscode'
import { telemetry } from '../../shared/telemetry/telemetry'
import { ToolkitError } from '../../shared/errors'
+import { isTreeNode, TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
+import { SamAppLocation } from '../../awsService/appBuilder/explorer/samProject'
import { getAmazonqApi } from '../../amazonq/extApi'
export const openTemplateInComposerCommand = Commands.declare(
'aws.openInApplicationComposer',
- (manager: ApplicationComposerManager) => async (arg?: vscode.TextEditor | vscode.Uri) => {
+ (manager: ApplicationComposerManager) => async (arg?: vscode.TextEditor | vscode.Uri | TreeNode) => {
let result: vscode.WebviewPanel | undefined
await telemetry.appcomposer_openTemplate.run(async (span) => {
const amazonqApi = await getAmazonqApi()
@@ -26,8 +28,14 @@ export const openTemplateInComposerCommand = Commands.declare(
span.record({
hasChatAuth,
})
- arg ??= vscode.window.activeTextEditor
- const input = arg instanceof vscode.Uri ? arg : arg?.document
+ let input = undefined
+ if (arg instanceof vscode.Uri) {
+ input = arg
+ } else if (isTreeNode(arg)) {
+ input = ((arg as TreeNode).resource as SamAppLocation).samTemplateUri
+ } else {
+ input = vscode.window.activeTextEditor?.document
+ }
if (!input) {
throw new ToolkitError('No active text editor or document found')
diff --git a/packages/core/src/auth/utils.ts b/packages/core/src/auth/utils.ts
index 9b7eaca7592..0cd33ef6360 100644
--- a/packages/core/src/auth/utils.ts
+++ b/packages/core/src/auth/utils.ts
@@ -57,6 +57,8 @@ import { SharedCredentialsProviderFactory } from './providers/sharedCredentialsP
import { Ec2CredentialsProvider } from './providers/ec2CredentialsProvider'
import { EcsCredentialsProvider } from './providers/ecsCredentialsProvider'
import { EnvVarsCredentialsProvider } from './providers/envVarsCredentialsProvider'
+import { showMessageWithUrl } from '../shared/utilities/messages'
+import { credentialHelpUrl } from '../shared/constants'
// iam-only excludes Builder ID and IAM Identity Center from the list of valid connections
// TODO: Understand if "iam" should include these from the list at all
@@ -106,6 +108,46 @@ export async function promptAndUseConnection(...[auth, type]: Parameters {
return telemetry.function_call.run(
async () => {
diff --git a/packages/core/src/awsService/apigateway/activation.ts b/packages/core/src/awsService/apigateway/activation.ts
index 2bf2f44ea4c..78add0d3e67 100644
--- a/packages/core/src/awsService/apigateway/activation.ts
+++ b/packages/core/src/awsService/apigateway/activation.ts
@@ -9,6 +9,8 @@ import { invokeRemoteRestApi } from './vue/invokeRemoteRestApi'
import { copyUrlCommand } from './commands/copyUrl'
import { ExtContext } from '../../shared/extensions'
import { Commands } from '../../shared/vscode/commands2'
+import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
+import { getSourceNode } from '../../shared/utilities/treeNodeUtils'
/**
* Activate API Gateway functionality for the extension.
@@ -20,14 +22,16 @@ export async function activate(activateArguments: {
const extensionContext = activateArguments.extContext.extensionContext
const regionProvider = activateArguments.extContext.regionProvider
extensionContext.subscriptions.push(
- Commands.register('aws.apig.copyUrl', async (node: RestApiNode) => await copyUrlCommand(node, regionProvider)),
- Commands.register(
- 'aws.apig.invokeRemoteRestApi',
- async (node: RestApiNode) =>
- await invokeRemoteRestApi(activateArguments.extContext, {
- apiNode: node,
- outputChannel: activateArguments.outputChannel,
- })
- )
+ Commands.register('aws.apig.copyUrl', async (node: RestApiNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await copyUrlCommand(sourceNode, regionProvider)
+ }),
+ Commands.register('aws.apig.invokeRemoteRestApi', async (node: RestApiNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await invokeRemoteRestApi(activateArguments.extContext, {
+ apiNode: sourceNode,
+ outputChannel: activateArguments.outputChannel,
+ })
+ })
)
}
diff --git a/packages/core/src/awsService/appBuilder/activation.ts b/packages/core/src/awsService/appBuilder/activation.ts
new file mode 100644
index 00000000000..9f30282573c
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/activation.ts
@@ -0,0 +1,205 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import globals from '../../shared/extensionGlobals'
+import { ExtContext } from '../../shared/extensions'
+import { Commands, VsCodeCommandArg } from '../../shared/vscode/commands2'
+import { ToolView } from '../../awsexplorer/toolView'
+import { telemetry } from '../../shared/telemetry/telemetry'
+import { activateViewsShared, registerToolView } from '../../awsexplorer/activationShared'
+import { setContext } from '../../shared/vscode/setContext'
+import { fs } from '../../shared/fs/fs'
+import { AppBuilderRootNode } from './explorer/nodes/rootNode'
+import { initWalkthroughProjectCommand, walkthroughContextString, getOrInstallCliWrapper } from './walkthrough'
+import { getLogger } from '../../shared/logger'
+import path from 'path'
+import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
+import { runBuild } from '../../shared/sam/build'
+import { runOpenHandler, runOpenTemplate } from './utils'
+import { ResourceNode } from './explorer/nodes/resourceNode'
+import { getSyncWizard, runSync } from '../../shared/sam/sync'
+import { getDeployWizard, runDeploy } from '../../shared/sam/deploy'
+import { DeployTypeWizard } from './wizards/deployTypeWizard'
+
+export const templateToOpenAppComposer = 'aws.toolkit.appComposer.templateToOpenOnStart'
+
+/**
+ * Activates the AWS Explorer UI and related functionality.
+ *
+ * IMPORTANT: Views that should work in all vscode environments (node or web)
+ * should be setup in {@link activateViewsShared}.
+ */
+export async function activate(context: ExtContext): Promise {
+ // recover context variables from global state when activate
+ const walkthroughSelected = globals.globalState.get(walkthroughContextString)
+ if (walkthroughSelected !== undefined) {
+ await setContext(walkthroughContextString, walkthroughSelected)
+ }
+
+ await registerAppBuilderCommands(context)
+
+ const appBuilderNode: ToolView[] = [
+ {
+ nodes: [AppBuilderRootNode.instance],
+ view: 'aws.appBuilder',
+ refreshCommands: [AppBuilderRootNode.instance.refreshAppBuilderExplorer],
+ },
+ {
+ nodes: [AppBuilderRootNode.instance],
+ view: 'aws.appBuilderForFileExplorer',
+ refreshCommands: [AppBuilderRootNode.instance.refreshAppBuilderForFileExplorer],
+ },
+ ]
+
+ const watcher = vscode.workspace.createFileSystemWatcher('**/{template.yaml,template.yml,samconfig.toml}')
+ watcher.onDidChange(async (uri) => runRefreshAppBuilder(uri, 'changed'))
+ watcher.onDidCreate(async (uri) => runRefreshAppBuilder(uri, 'created'))
+ watcher.onDidDelete(async (uri) => runRefreshAppBuilder(uri, 'deleted'))
+
+ for (const viewNode of appBuilderNode) {
+ registerToolView(viewNode, context.extensionContext)
+ }
+
+ await openApplicationComposerAfterReload()
+}
+
+async function runRefreshAppBuilder(uri: vscode.Uri, event: string) {
+ getLogger().debug(`${uri.fsPath} ${event}, refreshing appBuilder`)
+ await vscode.commands.executeCommand('aws.appBuilderForFileExplorer.refresh')
+ await vscode.commands.executeCommand('aws.appBuilder.refresh')
+}
+
+/**
+ * To support open template in AppComposer after extension reload.
+ * This typically happens when user create project from walkthrough
+ * and added a new folder to an empty workspace.
+ *
+ * Checkes templateToOpenAppComposer in global and opens template
+ * Directly return if templateToOpenAppComposer is undefined
+ */
+export async function openApplicationComposerAfterReload(): Promise {
+ const templatesToOpen = globals.globalState.get<[string]>(templateToOpenAppComposer)
+ // undefined
+ if (!templatesToOpen) {
+ return
+ }
+
+ for (const template of templatesToOpen) {
+ const templateUri = vscode.Uri.file(template)
+ const templateFolder = vscode.Uri.file(path.dirname(template))
+ const basename = path.basename(template)
+ // ignore templates that doesn't belong to current workspace, ignore if not template
+ if (
+ !vscode.workspace.getWorkspaceFolder(templateFolder) ||
+ (basename !== 'template.yaml' && basename !== 'template.yml')
+ ) {
+ continue
+ }
+
+ await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup')
+ await vscode.commands.executeCommand('aws.openInApplicationComposer', templateUri)
+
+ if (await fs.exists(vscode.Uri.joinPath(templateFolder, 'README.md'))) {
+ await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup')
+ await vscode.commands.executeCommand(
+ 'markdown.showPreview',
+ vscode.Uri.joinPath(templateFolder, 'README.md')
+ )
+ }
+ }
+ // set to undefined
+ await globals.globalState.update(templateToOpenAppComposer, undefined)
+}
+
+async function setWalkthrough(walkthroughSelected: string = 'S3'): Promise {
+ await setContext(walkthroughContextString, walkthroughSelected)
+ await globals.globalState.update(walkthroughContextString, walkthroughSelected)
+}
+
+/**
+ *
+ * @param context VScode Context
+ */
+async function registerAppBuilderCommands(context: ExtContext): Promise {
+ const source = 'AppBuilderWalkthrough'
+ context.extensionContext.subscriptions.push(
+ Commands.register('aws.toolkit.installSAMCLI', async () => {
+ await getOrInstallCliWrapper('sam-cli', source)
+ }),
+ Commands.register('aws.toolkit.installAWSCLI', async () => {
+ await getOrInstallCliWrapper('aws-cli', source)
+ }),
+ Commands.register('aws.toolkit.installDocker', async () => {
+ await getOrInstallCliWrapper('docker', source)
+ }),
+ Commands.register('aws.toolkit.lambda.setWalkthroughToAPI', async () => {
+ await setWalkthrough('API')
+ }),
+ Commands.register('aws.toolkit.lambda.setWalkthroughToS3', async () => {
+ await setWalkthrough('S3')
+ }),
+ Commands.register('aws.toolkit.lambda.setWalkthroughToVisual', async () => {
+ await setWalkthrough('Visual')
+ }),
+ Commands.register('aws.toolkit.lambda.setWalkthroughToCustomTemplate', async () => {
+ await setWalkthrough('CustomTemplate')
+ }),
+ Commands.register('aws.toolkit.lambda.initializeWalkthroughProject', async (): Promise => {
+ await telemetry.appBuilder_selectWalkthroughTemplate.run(async () => await initWalkthroughProjectCommand())
+ await globals.globalState.update('aws.toolkit.lambda.walkthroughCompleted', true)
+ }),
+ Commands.register('aws.toolkit.lambda.walkthrough.credential', async (): Promise => {
+ await vscode.commands.executeCommand('aws.toolkit.auth.manageConnections', source)
+ }),
+ Commands.register(
+ { id: `aws.toolkit.lambda.openWalkthrough`, compositeKey: { 1: 'source' } },
+ async (_: VsCodeCommandArg, source?: string) => {
+ telemetry.appBuilder_startWalkthrough.emit({ source: source })
+ await vscode.commands.executeCommand(
+ 'workbench.action.openWalkthrough',
+ 'amazonwebservices.aws-toolkit-vscode#aws.toolkit.lambda.walkthrough'
+ )
+ }
+ ),
+ Commands.register(
+ {
+ id: 'aws.appBuilder.build',
+ autoconnect: false,
+ },
+ async (arg?: TreeNode | undefined) => await telemetry.sam_build.run(async () => await runBuild(arg))
+ ),
+ Commands.register({ id: 'aws.appBuilder.openTemplate', autoconnect: false }, async (arg: TreeNode) =>
+ telemetry.appBuilder_openTemplate.run(async (span) => {
+ if (arg) {
+ span.record({ source: 'AppBuilderOpenTemplate' })
+ } else {
+ span.record({ source: 'commandPalette' })
+ }
+ await runOpenTemplate(arg)
+ })
+ ),
+ Commands.register({ id: 'aws.appBuilder.openHandler', autoconnect: false }, async (arg: ResourceNode) =>
+ telemetry.lambda_goToHandler.run(async (span) => {
+ span.record({ source: 'AppBuilderOpenHandler' })
+ await runOpenHandler(arg)
+ })
+ ),
+ Commands.register({ id: 'aws.appBuilder.deploy', autoconnect: true }, async (arg) => {
+ const wizard = new DeployTypeWizard(
+ await getSyncWizard('infra', arg, undefined, false),
+ await getDeployWizard(arg, false)
+ )
+ const choices = await wizard.run()
+ if (choices) {
+ if (choices.choice === 'deploy' && choices.deployParam) {
+ await runDeploy(arg, choices.deployParam)
+ } else if (choices.choice === 'sync' && choices.syncParam) {
+ await runSync('infra', arg, undefined, choices.syncParam)
+ }
+ }
+ })
+ )
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts b/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts
new file mode 100644
index 00000000000..cb179d94f0d
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/detectSamProjects.ts
@@ -0,0 +1,61 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { SamAppLocation } from './samProject'
+import { getLogger } from '../../../shared/logger/logger'
+import { getProjectRootUri } from '../../../shared/sam/utils'
+
+export async function detectSamProjects(): Promise {
+ const workspaceFolders = vscode.workspace.workspaceFolders
+
+ if (!workspaceFolders) {
+ return []
+ }
+
+ const results = new Map()
+ const projects = (await Promise.all(workspaceFolders.map(detectSamProjectsFromWorkspaceFolder))).reduce(
+ (a, b) => a.concat(b),
+ []
+ )
+
+ projects.forEach((p) => results.set(p.samTemplateUri.toString(), p))
+
+ return Array.from(results.values())
+}
+
+async function detectSamProjectsFromWorkspaceFolder(
+ workspaceFolder: vscode.WorkspaceFolder
+): Promise {
+ const result: SamAppLocation[] = []
+ const samTemplateFiles = await getFiles(workspaceFolder, '**/template.{yml,yaml}', '**/.aws-sam/**')
+ for (const samTemplateFile of samTemplateFiles) {
+ const project = {
+ samTemplateUri: samTemplateFile,
+ workspaceFolder: workspaceFolder,
+ projectRoot: getProjectRootUri(samTemplateFile),
+ }
+ result.push(project)
+ }
+ return result
+}
+
+export async function getFiles(
+ workspaceFolder: vscode.WorkspaceFolder,
+ pattern: string,
+ buildArtifactFolderPattern?: string
+): Promise {
+ try {
+ const globPattern = new vscode.RelativePattern(workspaceFolder, pattern)
+ const excludePattern = buildArtifactFolderPattern
+ ? new vscode.RelativePattern(workspaceFolder, buildArtifactFolderPattern)
+ : undefined
+
+ return await vscode.workspace.findFiles(globPattern, excludePattern)
+ } catch (error) {
+ getLogger().error(`Failed to get files with pattern ${pattern}:`, error)
+ return []
+ }
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts
new file mode 100644
index 00000000000..497e5aa22ad
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/nodes/appNode.ts
@@ -0,0 +1,104 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as nls from 'vscode-nls'
+const localize = nls.loadMessageBundle()
+
+import * as vscode from 'vscode'
+import { getLogger } from '../../../../shared/logger'
+import { ResourceTreeEntity, SamAppLocation, getApp, getStackName } from '../samProject'
+import { ResourceNode, generateResourceNodes } from './resourceNode'
+import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider'
+import { createPlaceholderItem } from '../../../../shared/treeview/utils'
+import { getIcon } from '../../../../shared/icons'
+import { getSamCliContext } from '../../../../shared/sam/cli/samCliContext'
+import { SamCliListResourcesParameters } from '../../../../shared/sam/cli/samCliListResources'
+import { getDeployedResources, StackResource } from '../../../../lambda/commands/listSamResources'
+import * as path from 'path'
+import fs from '../../../../shared/fs/fs'
+import { generateStackNode } from './deployedStack'
+
+export class AppNode implements TreeNode {
+ public readonly id = this.location.samTemplateUri.toString()
+ public readonly resource = this.location
+ public readonly label = path.join(
+ this.location.workspaceFolder.name,
+ path.relative(this.location.workspaceFolder.uri.fsPath, path.dirname(this.location.samTemplateUri.fsPath))
+ )
+ private stackName: string = ''
+ public constructor(private readonly location: SamAppLocation) {}
+
+ public async getChildren(): Promise<(ResourceNode | TreeNode)[]> {
+ const resources = []
+ try {
+ const successfulApp = await getApp(this.location)
+ const templateResources: ResourceTreeEntity[] = successfulApp.resourceTree
+ const { stackName, region } = await getStackName(this.location.projectRoot)
+ this.stackName = stackName
+
+ const listStackResourcesArguments: SamCliListResourcesParameters = {
+ stackName: this.stackName,
+ templateFile: this.location.samTemplateUri.fsPath,
+ region: region,
+ projectRoot: this.location.projectRoot,
+ }
+
+ const deployedResources: StackResource[] | undefined = this.stackName
+ ? await getDeployedResources({
+ listResourcesParams: listStackResourcesArguments,
+ invoker: getSamCliContext().invoker,
+ })
+ : undefined
+ // Skip generating stack node if stack does not exist in region or other errors
+ if (deployedResources && deployedResources.length > 0) {
+ resources.push(...(await generateStackNode(this.stackName, region)))
+ }
+ resources.push(
+ ...generateResourceNodes(this.location, templateResources, this.stackName, region, deployedResources)
+ )
+
+ // indicate that App exists, but it is empty
+ if (resources.length === 0) {
+ if (await fs.exists(this.location.samTemplateUri)) {
+ return [
+ createPlaceholderItem(
+ localize(
+ 'AWS.appBuilder.explorerNode.app.noResource',
+ '[No resource found in IaC template]'
+ )
+ ),
+ ]
+ }
+ return [
+ createPlaceholderItem(
+ localize('AWS.appBuilder.explorerNode.app.noTemplate', '[No IaC templates found in Workspaces]')
+ ),
+ ]
+ }
+ return resources
+ } catch (error) {
+ getLogger().error(`Could not load the construct tree located at '${this.id}': %O`, error as Error)
+ return [
+ createPlaceholderItem(
+ localize(
+ 'AWS.appBuilder.explorerNode.app.noResourceTree',
+ '[Unable to load Resource tree for this App. Update IaC template]'
+ )
+ ),
+ ]
+ }
+ }
+
+ public getTreeItem() {
+ const item = new vscode.TreeItem(this.label, vscode.TreeItemCollapsibleState.Collapsed)
+
+ item.contextValue = 'awsAppBuilderAppNode'
+ item.iconPath = getIcon('vscode-folder')
+ item.resourceUri = this.location.samTemplateUri
+ item.tooltip = this.location.samTemplateUri.path
+
+ return item
+ }
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts
new file mode 100644
index 00000000000..70ac56bb1f6
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedNode.ts
@@ -0,0 +1,183 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { getIcon } from '../../../../shared/icons'
+import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider'
+import { createPlaceholderItem } from '../../../../shared/treeview/utils'
+import * as nls from 'vscode-nls'
+
+import { getLogger } from '../../../../shared/logger/logger'
+import { FunctionConfiguration, LambdaClient, GetFunctionCommand } from '@aws-sdk/client-lambda'
+import { DefaultLambdaClient } from '../../../../shared/clients/lambdaClient'
+import globals from '../../../../shared/extensionGlobals'
+import { defaultPartition } from '../../../../shared/regions/regionProvider'
+import { Lambda, APIGateway } from 'aws-sdk'
+import { LambdaNode } from '../../../../lambda/explorer/lambdaNodes'
+import { LambdaFunctionNode } from '../../../../lambda/explorer/lambdaFunctionNode'
+import { DefaultS3Client, DefaultBucket } from '../../../../shared/clients/s3Client'
+import { S3Node } from '../../../../awsService/s3/explorer/s3Nodes'
+import { S3BucketNode } from '../../../../awsService/s3/explorer/s3BucketNode'
+import { ApiGatewayNode } from '../../../../awsService/apigateway/explorer/apiGatewayNodes'
+import { RestApiNode } from '../../../../awsService/apigateway/explorer/apiNodes'
+import {
+ SERVERLESS_FUNCTION_TYPE,
+ SERVERLESS_API_TYPE,
+ s3BucketType,
+} from '../../../../shared/cloudformation/cloudformation'
+import { ToolkitError } from '../../../../shared'
+import { getIAMConnection } from '../../../../auth/utils'
+
+const localize = nls.loadMessageBundle()
+export interface DeployedResource {
+ stackName: string
+ regionCode: string
+ explorerNode: any
+ arn: string
+ contextValue: string
+}
+
+export const DeployedResourceContextValues: Record = {
+ [SERVERLESS_FUNCTION_TYPE]: 'awsRegionFunctionNodeDownloadable',
+ [SERVERLESS_API_TYPE]: 'awsApiGatewayNode',
+ [s3BucketType]: 'awsS3BucketNode',
+}
+
+export class DeployedResourceNode implements TreeNode {
+ public readonly id: string
+ public readonly contextValue: string
+
+ public constructor(public readonly resource: DeployedResource) {
+ if (this.resource.arn) {
+ this.id = this.resource.arn
+ this.contextValue = this.resource.contextValue
+ } else {
+ getLogger().warn('Cannot create DeployedResourceNode, the ARN does not exist.')
+ this.id = ''
+ this.contextValue = ''
+ }
+ }
+
+ public async getChildren(): Promise {
+ return []
+ }
+
+ public getTreeItem() {
+ const item = new vscode.TreeItem(this.id)
+
+ item.contextValue = this.contextValue
+ item.iconPath = getIcon('vscode-cloud')
+ item.collapsibleState = vscode.TreeItemCollapsibleState.None
+ item.tooltip = this.resource.arn
+ return item
+ }
+}
+
+export async function generateDeployedNode(
+ deployedResource: any,
+ regionCode: string,
+ stackName: string,
+ resourceTreeEntity: any
+): Promise {
+ let newDeployedResource: any
+ const partitionId = globals.regionProvider.getPartitionId(regionCode) ?? defaultPartition
+ try {
+ switch (resourceTreeEntity.Type) {
+ case SERVERLESS_FUNCTION_TYPE: {
+ const defaultClient = new DefaultLambdaClient(regionCode)
+ const lambdaNode = new LambdaNode(regionCode, defaultClient)
+ let configuration: Lambda.FunctionConfiguration
+ let v3configuration
+ let logGroupName
+ try {
+ configuration = (await defaultClient.getFunction(deployedResource.PhysicalResourceId))
+ .Configuration as Lambda.FunctionConfiguration
+ newDeployedResource = new LambdaFunctionNode(lambdaNode, regionCode, configuration)
+ } catch (error: any) {
+ getLogger().error('Error getting Lambda configuration')
+ throw ToolkitError.chain(error, 'Error getting Lambda configuration', {
+ code: 'lambdaClientError',
+ })
+ }
+ const connection = await getIAMConnection({ prompt: false })
+ if (!connection || connection.type !== 'iam') {
+ return [
+ createPlaceholderItem(
+ localize(
+ 'AWS.appBuilder.explorerNode.unavailableDeployedResource',
+ '[Failed to retrive deployed resource.]'
+ )
+ ),
+ ]
+ }
+ const cred = await connection.getCredentials()
+ const v3Client = new LambdaClient({ region: regionCode, credentials: cred })
+
+ const v3command = new GetFunctionCommand({ FunctionName: deployedResource.PhysicalResourceId })
+ try {
+ v3configuration = (await v3Client.send(v3command)).Configuration as FunctionConfiguration
+ logGroupName = v3configuration.LoggingConfig?.LogGroup
+ } catch {
+ getLogger().error('Error getting Lambda V3 configuration')
+ }
+ newDeployedResource.configuration = {
+ ...newDeployedResource.configuration,
+ logGroupName: logGroupName,
+ } as any
+ break
+ }
+ case s3BucketType: {
+ const s3Client = new DefaultS3Client(regionCode)
+ const s3Node = new S3Node(s3Client)
+ const s3Bucket = new DefaultBucket({
+ partitionId: partitionId,
+ region: regionCode,
+ name: deployedResource.PhysicalResourceId,
+ })
+ newDeployedResource = new S3BucketNode(s3Bucket, s3Node, s3Client)
+ break
+ }
+ case SERVERLESS_API_TYPE: {
+ const apiParentNode = new ApiGatewayNode(partitionId, regionCode)
+ const apiNodes = await apiParentNode.getChildren()
+ const apiNode = apiNodes.find((node) => node.id === deployedResource.PhysicalResourceId)
+ newDeployedResource = new RestApiNode(
+ apiParentNode,
+ partitionId,
+ regionCode,
+ apiNode as APIGateway.RestApi
+ )
+ break
+ }
+ default:
+ newDeployedResource = new DeployedResourceNode(deployedResource)
+ getLogger().info('Details are missing or are incomplete for:', deployedResource)
+ return [
+ createPlaceholderItem(
+ localize('AWS.appBuilder.explorerNode.noApps', '[This resource is not yet supported.]')
+ ),
+ ]
+ }
+ } catch (error: any) {
+ void vscode.window.showErrorMessage(error.messages)
+ return [
+ createPlaceholderItem(
+ localize(
+ 'AWS.appBuilder.explorerNode.unavailableDeployedResource',
+ '[Failed to retrive deployed resource.]'
+ )
+ ),
+ ]
+ }
+ newDeployedResource.contextValue = DeployedResourceContextValues[resourceTreeEntity.Type]
+ const finalDeployedResource = {
+ stackName,
+ regionCode,
+ explorerNode: newDeployedResource,
+ arn: newDeployedResource.arn,
+ contextValue: newDeployedResource.contextValue,
+ }
+ return [new DeployedResourceNode(finalDeployedResource)]
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts
new file mode 100644
index 00000000000..76c8f5ea76b
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/nodes/deployedStack.ts
@@ -0,0 +1,66 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import * as vscode from 'vscode'
+import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider'
+import { getIcon } from '../../../../shared/icons'
+import { CloudFormationClient, DescribeStacksCommand } from '@aws-sdk/client-cloudformation'
+import { ToolkitError } from '../../../../shared'
+import { getIAMConnection } from '../../../../auth/utils'
+
+export class StackNameNode implements TreeNode {
+ public readonly id = this.stackName
+ public readonly resource = this
+ public arn: string | undefined
+ public readonly link = `command:aws.explorer.cloudformation.showStack?${encodeURIComponent(JSON.stringify({ stackName: this.stackName, region: this.regionCode }))}`
+
+ public constructor(
+ public stackName: string,
+ public regionCode: string
+ ) {
+ this.stackName = stackName
+ this.regionCode = regionCode
+ }
+
+ public async getChildren(): Promise {
+ // This stack name node is a leaf node that does not have any children.
+ return []
+ }
+ public get value(): string {
+ return `Stack: ${this.stackName} (${this.regionCode})`
+ }
+
+ public getTreeItem() {
+ const item = new vscode.TreeItem(this.value)
+
+ item.contextValue = 'awsAppBuilderStackNode'
+ item.iconPath = getIcon('vscode-cloud')
+ return item
+ }
+}
+
+export async function generateStackNode(stackName?: string, regionCode?: string): Promise {
+ const connection = await getIAMConnection({ prompt: false })
+ if (!connection || connection.type !== 'iam') {
+ return []
+ }
+ const cred = await connection.getCredentials()
+ const client = new CloudFormationClient({ region: regionCode, credentials: cred })
+ try {
+ const command = new DescribeStacksCommand({ StackName: stackName })
+ const response = await client.send(command)
+ if (response.Stacks && response.Stacks[0]) {
+ const stackArn = response.Stacks[0].StackId
+ if (stackName === undefined || regionCode === undefined) {
+ return []
+ }
+ const stackNode = new StackNameNode(stackName || '', regionCode || '')
+ stackNode.arn = stackArn
+ return [stackNode]
+ }
+ } catch (error) {
+ throw new ToolkitError(`Failed to generate stack node ${stackName} in region ${regionCode}: ${error}`)
+ }
+ return []
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts
new file mode 100644
index 00000000000..481ecdf7009
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/nodes/propertyNode.ts
@@ -0,0 +1,46 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { getIcon } from '../../../../shared/icons'
+import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider'
+
+export class PropertyNode implements TreeNode {
+ public readonly id = this.key
+ public readonly resource = this.value
+
+ public constructor(
+ private readonly key: string,
+ private readonly value: unknown
+ ) {}
+
+ public async getChildren(): Promise {
+ if (this.value instanceof Array || this.value instanceof Object) {
+ return generatePropertyNodes(this.value)
+ } else {
+ return []
+ }
+ }
+
+ public getTreeItem() {
+ const item = new vscode.TreeItem(`${this.key}: ${this.value}`)
+
+ item.contextValue = 'awsAppBuilderPropertyNode'
+ item.iconPath = getIcon('vscode-gear')
+
+ if (this.value instanceof Array || this.value instanceof Object) {
+ item.label = this.key
+ item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed
+ }
+
+ return item
+ }
+}
+
+export function generatePropertyNodes(properties: { [key: string]: any }): TreeNode[] {
+ return Object.entries(properties)
+ .filter(([key, _]) => key !== 'Id' && key !== 'Type' && key !== 'Events')
+ .map(([key, value]) => new PropertyNode(key, value))
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts
new file mode 100644
index 00000000000..bda7b69ac4f
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/nodes/resourceNode.ts
@@ -0,0 +1,147 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { IconPath, getIcon } from '../../../../shared/icons'
+import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider'
+import { ResourceTreeEntity, SamAppLocation } from '../samProject'
+import {
+ SERVERLESS_FUNCTION_TYPE,
+ s3BucketType,
+ appRunnerType,
+ ecrRepositoryType,
+} from '../../../../shared/cloudformation/cloudformation'
+import { generatePropertyNodes } from './propertyNode'
+import { generateDeployedNode } from './deployedNode'
+import { StackResource } from '../../../../lambda/commands/listSamResources'
+import { DeployedResourceNode } from './deployedNode'
+
+enum ResourceTypeId {
+ Function = 'function',
+ Api = 'api',
+ Other = '',
+}
+
+export class ResourceNode implements TreeNode {
+ public readonly id = this.resourceTreeEntity.Id
+ private readonly type = this.resourceTreeEntity.Type
+ public readonly resourceLogicalId = this.deployedResource?.LogicalResourceId
+
+ public constructor(
+ private readonly location: SamAppLocation,
+ private readonly resourceTreeEntity: ResourceTreeEntity,
+ private readonly stackName?: string,
+ private readonly region?: string,
+ private readonly deployedResource?: StackResource,
+ // TODO: cleanup or rename functionArn parameter as type can be differ from Lambda; value never set in generateResourceNodes()
+ private readonly functionArn?: string
+ ) {}
+
+ public get resource() {
+ return {
+ resource: this.resourceTreeEntity,
+ location: this.location.samTemplateUri,
+ workspaceFolder: this.location.workspaceFolder,
+ region: this.region,
+ stackName: this.stackName,
+ deployedResource: this.deployedResource,
+ functionArn: this.functionArn,
+ }
+ }
+
+ public async getChildren() {
+ let deployedNodes: DeployedResourceNode[] = []
+ let propertyNodes: TreeNode[] = []
+
+ if (this.deployedResource && this.region && this.stackName) {
+ deployedNodes = await generateDeployedNode(
+ this.deployedResource,
+ this.region,
+ this.stackName,
+ this.resourceTreeEntity
+ )
+ }
+ if (this.resourceTreeEntity.Type === SERVERLESS_FUNCTION_TYPE) {
+ propertyNodes = generatePropertyNodes(this.resourceTreeEntity)
+ }
+
+ return [...propertyNodes, ...deployedNodes]
+ }
+
+ public getTreeItem(): vscode.TreeItem {
+ // Determine the initial TreeItem collapsible state based on the type
+ const collapsibleState = this.deployedResource
+ ? vscode.TreeItemCollapsibleState.Collapsed
+ : vscode.TreeItemCollapsibleState.None
+
+ // Create the TreeItem with the determined collapsible state
+ const item = new vscode.TreeItem(this.resourceTreeEntity.Id, collapsibleState)
+
+ // Set the tooltip to the URI of the SAM template
+ item.tooltip = this.location.samTemplateUri.toString()
+
+ item.iconPath = this.getIconPath()
+
+ // Set the resource URI to the SAM template URI
+ item.resourceUri = this.location.samTemplateUri
+
+ // Define the context value for the item
+ item.contextValue = `awsAppBuilderResourceNode.${this.getResourceId()}`
+
+ return item
+ }
+
+ // Additional resources and corresponding icons will be added in the future.
+ // When adding support for new resources, ensure that each new resource
+ // has an appropriate mapping in place.
+ private getIconPath(): IconPath | undefined {
+ switch (this.type) {
+ case SERVERLESS_FUNCTION_TYPE:
+ return getIcon('aws-lambda-function')
+ case s3BucketType:
+ return getIcon('aws-s3-bucket')
+ case appRunnerType:
+ return getIcon('aws-apprunner-service')
+ case ecrRepositoryType:
+ return getIcon('aws-ecr-registry')
+ default:
+ return getIcon('vscode-info')
+ }
+ }
+
+ private getResourceId(): ResourceTypeId {
+ switch (this.type) {
+ case SERVERLESS_FUNCTION_TYPE:
+ return ResourceTypeId.Function
+ case 'Api':
+ return ResourceTypeId.Api
+ default:
+ return ResourceTypeId.Other
+ }
+ }
+}
+
+export function generateResourceNodes(
+ app: SamAppLocation,
+ resources: NonNullable,
+ stackName?: string,
+ region?: string,
+ deployedResources?: StackResource[]
+): ResourceNode[] {
+ if (!deployedResources) {
+ return resources.map((resource) => new ResourceNode(app, resource, stackName, region))
+ }
+
+ return resources.map((resource) => {
+ if (resource.Type) {
+ const deployedResource = deployedResources.find(
+ (deployedResource) => resource.Id === deployedResource.LogicalResourceId
+ )
+ return new ResourceNode(app, resource, stackName, region, deployedResource)
+ } else {
+ return new ResourceNode(app, resource, stackName, region)
+ }
+ })
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/rootNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/rootNode.ts
new file mode 100644
index 00000000000..ce0406fd874
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/nodes/rootNode.ts
@@ -0,0 +1,110 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { debugNewSamAppDocUrl } from '../../../../shared/constants'
+import { telemetry } from '../../../../shared/telemetry/telemetry'
+import { ResourceTreeDataProvider, TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider'
+import { createPlaceholderItem } from '../../../../shared/treeview/utils'
+import { localize, openUrl } from '../../../../shared/utilities/vsCodeUtils'
+import { Commands } from '../../../../shared/vscode/commands2'
+import { AppNode } from './appNode'
+import { detectSamProjects } from '../detectSamProjects'
+import globals from '../../../../shared/extensionGlobals'
+import { WalkthroughNode } from './walkthroughNode'
+
+export async function getAppNodes(): Promise {
+ // no active workspace, show buttons in welcomeview
+ if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {
+ return []
+ }
+
+ const appsFound = await detectSamProjects()
+
+ if (appsFound.length === 0) {
+ return [
+ createPlaceholderItem(
+ localize('AWS.appBuilder.explorerNode.noApps', '[No IaC templates found in Workspaces]')
+ ),
+ ]
+ }
+
+ const nodesToReturn: TreeNode[] = appsFound
+ .map((appLocation) => new AppNode(appLocation))
+ .sort((a, b) => a.label.localeCompare(b.label) ?? 0)
+
+ return nodesToReturn
+}
+
+export class AppBuilderRootNode implements TreeNode {
+ public readonly id = 'appBuilder'
+ public readonly resource = this
+ private readonly onDidChangeChildrenEmitter = new vscode.EventEmitter()
+ public readonly onDidChangeChildren = this.onDidChangeChildrenEmitter.event
+ private readonly _refreshAppBuilderExplorer
+ private readonly _refreshAppBuilderForFileExplorer
+
+ constructor() {
+ Commands.register('aws.appBuilder.viewDocs', () => {
+ void openUrl(debugNewSamAppDocUrl.toolkit)
+ telemetry.aws_help.emit({ name: 'appBuilder' })
+ })
+ this._refreshAppBuilderExplorer = (provider?: ResourceTreeDataProvider) =>
+ Commands.register('aws.appBuilder.refresh', () => {
+ this.refresh()
+ if (provider) {
+ provider.refresh()
+ }
+ })
+
+ this._refreshAppBuilderForFileExplorer = (provider?: ResourceTreeDataProvider) =>
+ Commands.register('aws.appBuilderForFileExplorer.refresh', () => {
+ this.refresh()
+ if (provider) {
+ provider.refresh()
+ }
+ })
+ }
+
+ public get refreshAppBuilderExplorer() {
+ return this._refreshAppBuilderExplorer
+ }
+
+ public get refreshAppBuilderForFileExplorer() {
+ return this._refreshAppBuilderForFileExplorer
+ }
+
+ public async getChildren() {
+ const nodesToReturn = await getAppNodes()
+ if (nodesToReturn.length === 0) {
+ return []
+ }
+
+ const walkthroughCompleted = globals.globalState.get('aws.toolkit.lambda.walkthroughCompleted')
+ // show walkthrough node if walkthrough not completed yet
+ if (!walkthroughCompleted) {
+ nodesToReturn.unshift(new WalkthroughNode())
+ }
+ return nodesToReturn
+ }
+
+ public refresh(): void {
+ this.onDidChangeChildrenEmitter.fire()
+ }
+
+ public getTreeItem() {
+ const item = new vscode.TreeItem('APPLICATION BUILDER')
+ item.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed
+ item.contextValue = 'awsAppBuilderRootNode'
+
+ return item
+ }
+
+ static #instance: AppBuilderRootNode
+
+ static get instance(): AppBuilderRootNode {
+ return (this.#instance ??= new AppBuilderRootNode())
+ }
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/nodes/walkthroughNode.ts b/packages/core/src/awsService/appBuilder/explorer/nodes/walkthroughNode.ts
new file mode 100644
index 00000000000..8f3432075df
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/nodes/walkthroughNode.ts
@@ -0,0 +1,37 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { TreeNode } from '../../../../shared/treeview/resourceTreeDataProvider'
+import { localize } from '../../../../shared/utilities/vsCodeUtils'
+
+/**
+ * Create Open Walkthrough Node in App builder sidebar
+ *
+ */
+export class WalkthroughNode implements TreeNode {
+ public readonly id = 'walkthrough'
+ public readonly resource: WalkthroughNode = this
+
+ // Constructor left empty intentionally for future extensibility
+ public constructor() {}
+
+ /**
+ * Generates the TreeItem for the Walkthrough Node.
+ * This item will appear in the sidebar with a label and command to open the walkthrough.
+ */
+ public getTreeItem() {
+ const itemLabel = localize('AWS.appBuilder.openWalkthroughTitle', 'Walkthrough of Application Builder')
+
+ const item = new vscode.TreeItem(itemLabel)
+ item.contextValue = 'awsWalkthroughNode'
+ item.command = {
+ title: localize('AWS.appBuilder.openWalkthroughTitle', 'Walkthrough of Application Builder'),
+ command: 'aws.toolkit.lambda.openWalkthrough',
+ }
+
+ return item
+ }
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/openTemplate.ts b/packages/core/src/awsService/appBuilder/explorer/openTemplate.ts
new file mode 100644
index 00000000000..686340719e3
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/openTemplate.ts
@@ -0,0 +1,20 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { CloudFormationTemplateRegistry } from '../../../shared/fs/templateRegistry'
+import { createTemplatePrompter, TemplateItem } from '../../../shared/sam/sync'
+import { createExitPrompter } from '../../../shared/ui/common/exitPrompter'
+import { Wizard } from '../../../shared/wizards/wizard'
+
+export interface OpenTemplateParams {
+ readonly template: TemplateItem
+}
+
+export class OpenTemplateWizard extends Wizard {
+ public constructor(state: Partial, registry: CloudFormationTemplateRegistry) {
+ super({ initState: state, exitPrompterProvider: createExitPrompter })
+ this.form.template.bindPrompter(() => createTemplatePrompter(registry))
+ }
+}
diff --git a/packages/core/src/awsService/appBuilder/explorer/samProject.ts b/packages/core/src/awsService/appBuilder/explorer/samProject.ts
new file mode 100644
index 00000000000..fdb4b8e2117
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/explorer/samProject.ts
@@ -0,0 +1,103 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import * as CloudFormation from '../../../shared/cloudformation/cloudformation'
+import { SamConfig, SamConfigErrorCode } from '../../../shared/sam/config'
+import { getLogger } from '../../../shared/logger/logger'
+import { ToolkitError } from '../../../shared/errors'
+import { showViewLogsMessage } from '../../../shared/utilities/messages'
+
+export interface SamApp {
+ location: SamAppLocation
+ resourceTree: ResourceTreeEntity[]
+}
+
+export interface SamAppLocation {
+ samTemplateUri: vscode.Uri
+ workspaceFolder: vscode.WorkspaceFolder
+ projectRoot: vscode.Uri
+}
+
+export interface ResourceTreeEntity {
+ Id: string
+ Type: string
+ Runtime?: string
+ CodeUri?: string
+ Handler?: string
+ Events?: ResourceTreeEntity[]
+ Path?: string
+ Method?: string
+}
+
+export async function getStackName(projectRoot: vscode.Uri): Promise {
+ try {
+ const samConfig = await SamConfig.fromProjectRoot(projectRoot)
+ const stackName = await samConfig.getCommandParam('global', 'stack_name')
+ const region = await samConfig.getCommandParam('global', 'region')
+
+ return { stackName, region }
+ } catch (error: any) {
+ switch (error.code) {
+ case SamConfigErrorCode.samNoConfigFound:
+ getLogger().info('No stack name or region information available in samconfig.toml', error)
+ break
+ case SamConfigErrorCode.samConfigParseError:
+ getLogger().error(`Error getting stack name or region information: ${error.message}`, error)
+ void showViewLogsMessage('Encountered an issue reading samconfig.toml')
+ break
+ default:
+ getLogger().warn(`Error getting stack name or region information: ${error.message}`, error)
+ }
+ return {}
+ }
+}
+
+export async function getApp(location: SamAppLocation): Promise {
+ const samTemplate = await CloudFormation.tryLoad(location.samTemplateUri)
+ if (!samTemplate.template) {
+ throw new ToolkitError(`Template at ${location.samTemplateUri.fsPath} is not valid`)
+ }
+ const templateResources = getResourceEntity(samTemplate.template)
+
+ const resourceTree = [...templateResources]
+
+ return { location, resourceTree }
+}
+
+function getResourceEntity(template: any): ResourceTreeEntity[] {
+ const resourceTree: ResourceTreeEntity[] = []
+
+ for (const [logicalId, resource] of Object.entries(template?.Resources ?? []) as [string, any][]) {
+ const resourceEntity: ResourceTreeEntity = {
+ Id: logicalId,
+ Type: resource.Type,
+ Runtime: resource.Properties?.Runtime ?? template?.Globals?.Function?.Runtime,
+ Handler: resource.Properties?.Handler ?? template?.Globals?.Function?.Handler,
+ Events: resource.Properties?.Events ? getEvents(resource.Properties.Events) : undefined,
+ CodeUri: resource.Properties?.CodeUri ?? template?.Globals?.Function?.CodeUri,
+ }
+ resourceTree.push(resourceEntity)
+ }
+
+ return resourceTree
+}
+
+function getEvents(events: Record): ResourceTreeEntity[] {
+ const eventResources: ResourceTreeEntity[] = []
+
+ for (const [eventsLogicalId, event] of Object.entries(events)) {
+ const eventProperties = event.Properties
+ const eventResource: ResourceTreeEntity = {
+ Id: eventsLogicalId,
+ Type: event.Type,
+ Path: eventProperties.Path,
+ Method: eventProperties.Method,
+ }
+ eventResources.push(eventResource)
+ }
+
+ return eventResources
+}
diff --git a/packages/core/src/awsService/appBuilder/utils.ts b/packages/core/src/awsService/appBuilder/utils.ts
new file mode 100644
index 00000000000..de3dee8770d
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/utils.ts
@@ -0,0 +1,198 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
+import * as nls from 'vscode-nls'
+import { ResourceNode } from './explorer/nodes/resourceNode'
+import type { SamAppLocation } from './explorer/samProject'
+import { ToolkitError } from '../../shared/errors'
+import globals from '../../shared/extensionGlobals'
+import { OpenTemplateParams, OpenTemplateWizard } from './explorer/openTemplate'
+import { DataQuickPickItem, createQuickPick } from '../../shared/ui/pickerPrompter'
+import { createCommonButtons } from '../../shared/ui/buttons'
+import { samDeployUrl } from '../../shared/constants'
+import path from 'path'
+import fs from '../../shared/fs/fs'
+import { getLogger } from '../../shared/logger/logger'
+import { RuntimeFamily, getFamily } from '../../lambda/models/samLambdaRuntime'
+import { showMessage } from '../../shared/utilities/messages'
+const localize = nls.loadMessageBundle()
+
+export async function runOpenTemplate(arg?: TreeNode) {
+ const templateUri = arg ? (arg.resource as SamAppLocation).samTemplateUri : await promptUserForTemplate()
+ if (!templateUri || !(await fs.exists(templateUri))) {
+ throw new ToolkitError('No template provided', { code: 'NoTemplateProvided' })
+ }
+ const document = await vscode.workspace.openTextDocument(templateUri)
+ await vscode.window.showTextDocument(document)
+}
+
+/**
+ * Find and open the lambda handler with given ResoruceNode
+ * If not found, a NoHandlerFound error will be raised
+ * @param arg ResourceNode
+ */
+export async function runOpenHandler(arg: ResourceNode): Promise {
+ const folderUri = path.dirname(arg.resource.location.fsPath)
+ if (!arg.resource.resource.CodeUri) {
+ throw new ToolkitError('No CodeUri provided in template, cannot open handler', { code: 'NoCodeUriProvided' })
+ }
+
+ if (!arg.resource.resource.Handler) {
+ throw new ToolkitError('No Handler provided in template, cannot open handler', { code: 'NoHandlerProvided' })
+ }
+
+ if (!arg.resource.resource.Runtime) {
+ throw new ToolkitError('No Runtime provided in template, cannot open handler', { code: 'NoRuntimeProvided' })
+ }
+
+ const handlerFile = await getLambdaHandlerFile(
+ vscode.Uri.file(folderUri),
+ arg.resource.resource.CodeUri,
+ arg.resource.resource.Handler,
+ arg.resource.resource.Runtime
+ )
+ if (!handlerFile) {
+ throw new ToolkitError(`No handler file found with name "${arg.resource.resource.Handler}"`, {
+ code: 'NoHandlerFound',
+ })
+ }
+ await vscode.workspace.openTextDocument(handlerFile).then(async (doc) => await vscode.window.showTextDocument(doc))
+}
+
+// create a set to store all supported runtime in the following function
+const supportedRuntimeForHandler = new Set([
+ RuntimeFamily.Ruby,
+ RuntimeFamily.Python,
+ RuntimeFamily.NodeJS,
+ RuntimeFamily.DotNet,
+ RuntimeFamily.Java,
+])
+
+/**
+ * Get the actual Lambda handler file, in vscode.Uri format, from the template
+ * file and handler name. If not found, return undefined.
+ *
+ * @param folderUri The root folder for sam project
+ * @param codeUri codeUri prop in sam template
+ * @param handler handler prop in sam template
+ * @param runtime runtime prop in sam template
+ * @returns
+ */
+export async function getLambdaHandlerFile(
+ folderUri: vscode.Uri,
+ codeUri: string,
+ handler: string,
+ runtime: string
+): Promise {
+ const family = getFamily(runtime)
+ if (!supportedRuntimeForHandler.has(family)) {
+ throw new ToolkitError(`Runtime ${runtime} is not supported for open handler button`, {
+ code: 'RuntimeNotSupported',
+ })
+ }
+
+ const handlerParts = handler.split('.')
+ // sample: app.lambda_handler -> app.rb
+ if (family === RuntimeFamily.Ruby) {
+ // Ruby supports namespace/class handlers as well, but the path is
+ // guaranteed to be slash-delimited so we can assume the first part is
+ // the path
+ return vscode.Uri.joinPath(folderUri, codeUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.rb')
+ }
+
+ // sample:app.lambda_handler -> app.py
+ if (family === RuntimeFamily.Python) {
+ // Otherwise (currently Node.js and Python) handle dot-delimited paths
+ return vscode.Uri.joinPath(folderUri, codeUri, handlerParts.slice(0, handlerParts.length - 1).join('/') + '.py')
+ }
+
+ // sample: app.handler -> app.mjs/app.js
+ // More likely to be mjs if NODEJS version>=18, now searching for both
+ if (family === RuntimeFamily.NodeJS) {
+ const handlerName = handlerParts.slice(0, handlerParts.length - 1).join('/')
+ const handlerPath = path.dirname(handlerName)
+ const handlerFile = path.basename(handlerName)
+ const pattern = new vscode.RelativePattern(
+ vscode.Uri.joinPath(folderUri, codeUri, handlerPath),
+ `${handlerFile}.{js,mjs}`
+ )
+ return searchHandlerFile(folderUri, pattern)
+ }
+ // search directly under Code uri for Dotnet and java
+ // sample: ImageResize::ImageResize.Function::FunctionHandler -> Function.cs
+ if (family === RuntimeFamily.DotNet) {
+ const handlerName = path.basename(handler.split('::')[1].replaceAll('.', '/'))
+ const pattern = new vscode.RelativePattern(vscode.Uri.joinPath(folderUri, codeUri), `${handlerName}.cs`)
+ return searchHandlerFile(folderUri, pattern)
+ }
+
+ // sample: resizer.App::handleRequest -> App.java
+ if (family === RuntimeFamily.Java) {
+ const handlerName = handler.split('::')[0].replaceAll('.', '/')
+ const pattern = new vscode.RelativePattern(vscode.Uri.joinPath(folderUri, codeUri), `**/${handlerName}.java`)
+ return searchHandlerFile(folderUri, pattern)
+ }
+}
+
+/**
+ Searches for a handler file in the given pattern and returns the first match.
+ If no match is found, returns undefined.
+*/
+export async function searchHandlerFile(
+ folderUri: vscode.Uri,
+ pattern: vscode.RelativePattern
+): Promise {
+ const handlerFile = await vscode.workspace.findFiles(pattern, new vscode.RelativePattern(folderUri, '.aws-sam'))
+ if (handlerFile.length === 0) {
+ return undefined
+ }
+ if (handlerFile.length > 1) {
+ getLogger().warn(`Multiple handler files found with name "${path.basename(handlerFile[0].fsPath)}"`)
+ void showMessage('warn', `Multiple handler files found with name "${path.basename(handlerFile[0].fsPath)}"`)
+ }
+ if (await fs.exists(handlerFile[0])) {
+ return handlerFile[0]
+ }
+ return undefined
+}
+
+async function promptUserForTemplate() {
+ const registry = await globals.templateRegistry
+ const openTemplateParams: Partial = {}
+
+ const param = await new OpenTemplateWizard(openTemplateParams, registry).run()
+ return param?.template.uri
+}
+
+export async function deployTypePrompt() {
+ const items: DataQuickPickItem[] = [
+ {
+ label: 'Sync',
+ data: 'sync',
+ detail: 'Speed up your development and testing experience in the AWS Cloud. With the --watch parameter, sync will build, deploy and watch for local changes',
+ description: 'Development environments',
+ },
+ {
+ label: 'Deploy',
+ data: 'deploy',
+ detail: 'Deploys your template through CloudFormation',
+ description: 'Production environments',
+ },
+ ]
+
+ const selected = await createQuickPick(items, {
+ title: localize('AWS.appBuilder.deployType.title', 'Select deployment command'),
+ placeholder: 'Press enter to proceed with highlighted option',
+ buttons: createCommonButtons(samDeployUrl),
+ }).prompt()
+
+ if (!selected) {
+ getLogger().info('Operation cancelled.')
+ return
+ }
+ return selected
+}
diff --git a/packages/core/src/awsService/appBuilder/walkthrough.ts b/packages/core/src/awsService/appBuilder/walkthrough.ts
new file mode 100644
index 00000000000..04f43d61878
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/walkthrough.ts
@@ -0,0 +1,349 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as semver from 'semver'
+import * as vscode from 'vscode'
+import globals from '../../shared/extensionGlobals'
+import { getLogger } from '../../shared/logger'
+
+import { Wizard } from '../../shared/wizards/wizard'
+import { createQuickPick } from '../../shared/ui/pickerPrompter'
+import { createCommonButtons } from '../../shared/ui/buttons'
+import * as nls from 'vscode-nls'
+import { ToolkitError } from '../../shared/errors'
+import { createSingleFileDialog } from '../../shared/ui/common/openDialog'
+import { fs } from '../../shared/fs/fs'
+import path from 'path'
+import { telemetry } from '../../shared/telemetry'
+
+import { minSamCliVersionForAppBuilderSupport } from '../../shared/sam/cli/samCliValidator'
+import { SamCliInfoInvocation } from '../../shared/sam/cli/samCliInfo'
+import { openUrl } from '../../shared/utilities/vsCodeUtils'
+import { getOrInstallCli, awsClis, AwsClis } from '../../shared/utilities/cliUtils'
+import { getPattern } from '../../shared/utilities/downloadPatterns'
+import { addFolderToWorkspace } from '../../shared/utilities/workspaceUtils'
+
+const localize = nls.loadMessageBundle()
+const serverlessLandUrl = 'https://serverlessland.com/'
+export const walkthroughContextString = 'aws.toolkit.lambda.walkthroughSelected'
+export const templateToOpenAppComposer = 'aws.toolkit.appComposer.templateToOpenOnStart'
+const defaultTemplateName = 'template.yaml'
+const serverlessLandOwner = 'aws-samples'
+const serverlessLandRepo = 'serverless-patterns'
+
+type WalkthroughOptions = 'CustomTemplate' | 'Visual' | 'S3' | 'API'
+type TutorialRuntimeOptions = 'python' | 'node' | 'java' | 'dotnet' | 'skipped'
+
+interface IServerlessLandProject {
+ asset: string
+ handler?: string
+}
+
+export const appMap = new Map([
+ ['APIdotnet', { asset: 'apigw-rest-api-lambda-dotnet.zip', handler: 'src/HelloWorld/Function.cs' }],
+ ['APInode', { asset: 'apigw-rest-api-lambda-node.zip', handler: 'hello_world/app.mjs' }],
+ ['APIpython', { asset: 'apigw-rest-api-lambda-python.zip', handler: 'hello_world/app.py' }],
+ [
+ 'APIjava',
+ {
+ asset: 'apigw-rest-api-lambda-java.zip',
+ handler: 'HelloWorldFunction/src/main/java/helloworld/App.java',
+ },
+ ],
+ ['S3dotnet', { asset: 's3-lambda-resizing-dotnet.zip', handler: 'ImageResize/Function.cs' }],
+ ['S3node', { asset: 's3-lambda-resizing-node.zip', handler: 'src/app.js' }],
+ ['S3python', { asset: 's3-lambda-resizing-python.zip', handler: 'src/app.py' }],
+ [
+ 'S3java',
+ {
+ asset: 's3-lambda-resizing-java.zip',
+ handler: 'ResizerFunction/src/main/java/resizer/App.java',
+ },
+ ],
+])
+
+export class RuntimeLocationWizard extends Wizard<{
+ runtime: TutorialRuntimeOptions
+ dir: string
+ realDir: vscode.Uri
+}> {
+ public constructor(skipRuntime: boolean, labelValue: string, existingTemplates?: vscode.Uri[]) {
+ super()
+ const form = this.form
+
+ // step1: choose runtime
+ const items: { label: string; data: TutorialRuntimeOptions }[] = [
+ { label: 'Python', data: 'python' },
+ { label: 'Node JS', data: 'node' },
+ { label: 'Java', data: 'java' },
+ { label: 'Dot Net', data: 'dotnet' },
+ ]
+ form.runtime.bindPrompter(
+ () => {
+ return createQuickPick(items, {
+ title: localize('AWS.toolkit.lambda.walkthroughSelectRuntime', 'Select a runtime'),
+ buttons: createCommonButtons(serverlessLandUrl),
+ })
+ },
+ { showWhen: () => !skipRuntime }
+ )
+
+ // step2: choose location for project
+ const wsFolders = vscode.workspace.workspaceFolders
+ const items2 = [
+ {
+ label: localize('AWS.toolkit.lambda.walkthroughOpenExplorer', 'Open file explorer'),
+ data: 'file-selector',
+ },
+ ]
+
+ // if at least one open workspace, add all opened workspace as options
+ if (wsFolders && labelValue !== 'Open existing Project') {
+ for (const wsFolder of wsFolders) {
+ items2.push({ label: wsFolder.uri.fsPath, data: wsFolder.uri.fsPath })
+ }
+ }
+
+ if (wsFolders && existingTemplates && labelValue === 'Open existing Project') {
+ existingTemplates.map((file) => {
+ items2.push({ label: file.fsPath, data: path.dirname(file.fsPath) })
+ })
+ }
+
+ form.dir.bindPrompter(() => {
+ return createQuickPick(items2, {
+ title:
+ labelValue === 'Open existing Project'
+ ? localize('AWS.toolkit.lambda.walkthroughOpenExistProject', 'Select an existing template file')
+ : localize('AWS.toolkit.lambda.walkthroughProjectLocation', 'Select a location for project'),
+ buttons: createCommonButtons(labelValue === 'Open existing Project' ? undefined : serverlessLandUrl),
+ })
+ })
+
+ const options: vscode.OpenDialogOptions = {
+ canSelectMany: false,
+ openLabel: labelValue,
+ canSelectFiles: false,
+ canSelectFolders: true,
+ }
+ if (wsFolders) {
+ options.defaultUri = wsFolders[0]?.uri
+ }
+
+ form.realDir.bindPrompter((state) => createSingleFileDialog(options), {
+ showWhen: (state) => state.dir !== undefined && state.dir === 'file-selector',
+ setDefault: (state) => (state.dir ? vscode.Uri.file(state.dir) : undefined),
+ })
+ }
+}
+
+export async function getTutorial(
+ runtime: TutorialRuntimeOptions,
+ project: WalkthroughOptions,
+ outputDir: vscode.Uri,
+ source?: string
+): Promise {
+ const appSelected = appMap.get(project + runtime)
+ telemetry.record({ action: project + runtime, source: source ?? 'AppBuilderWalkthrough' })
+ if (!appSelected) {
+ throw new ToolkitError(`Tried to get template '${project}+${runtime}', but it hasn't been registered.`)
+ }
+
+ try {
+ await getPattern(serverlessLandOwner, serverlessLandRepo, appSelected.asset, outputDir, true)
+ } catch (error) {
+ throw new ToolkitError(`Error occurred while fetching the pattern from serverlessland: ${error}`)
+ }
+}
+
+/**
+ * Takes projectUri and runtime then generate matching project
+ * @param walkthroughSelected the selected walkthrough
+ * @param projectUri The choosen project uri to generate proejct
+ * @param runtime The runtime choosen
+ */
+export async function genWalkthroughProject(
+ walkthroughSelected: WalkthroughOptions,
+ projectUri: vscode.Uri,
+ runtime: TutorialRuntimeOptions | undefined
+): Promise {
+ // create project here
+ // TODO update with file fetching from serverless land
+ if (walkthroughSelected === 'CustomTemplate') {
+ // customer already have a project, no need to generate
+ return
+ }
+
+ // check if template.yaml already exists
+ const templateUri = vscode.Uri.joinPath(projectUri, defaultTemplateName)
+ if (await fs.exists(templateUri)) {
+ // ask if want to overwrite
+ const choice = await vscode.window.showInformationMessage(
+ localize(
+ 'AWS.toolkit.lambda.walkthroughCreateProjectPrompt',
+ '{0} already exist in the selected directory, overwrite?',
+ defaultTemplateName
+ ),
+ 'Yes',
+ 'No'
+ )
+ if (choice !== 'Yes') {
+ throw new ToolkitError(`${defaultTemplateName} already exist`)
+ }
+ }
+
+ // if Yes, or template not found, continue to generate
+ if (walkthroughSelected === 'Visual') {
+ // create an empty template.yaml, open it in appcomposer later
+ await fs.writeFile(templateUri, Buffer.from(''))
+ return
+ }
+ // start fetching project
+ if (runtime && runtime !== 'skipped') {
+ await getTutorial(runtime, walkthroughSelected, projectUri, 'AppBuilderWalkthrough')
+ }
+}
+
+/**
+ * check if the selected project Uri exist in current workspace. If not, add Project folder to Workspace
+ * @param projectUri uri for the selected project
+ */
+export async function openProjectInWorkspace(projectUri: vscode.Uri): Promise {
+ let templateUri: vscode.Uri | undefined = vscode.Uri.joinPath(projectUri, defaultTemplateName)
+ if (!(await fs.exists(templateUri))) {
+ // no template.yaml, trying yml
+ templateUri = vscode.Uri.joinPath(projectUri, 'template.yml')
+ if (!(await fs.exists(templateUri))) {
+ templateUri = undefined
+ }
+ }
+
+ // Open template file, and after update workspace folder
+ if (templateUri) {
+ await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup')
+ await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(templateUri))
+ // set global key to template to be opened, appComposer will open them upon reload
+ await globals.globalState.update(templateToOpenAppComposer, [templateUri.fsPath])
+ }
+
+ // if extension is reloaded here, this function will never return (killed)
+ await addFolderToWorkspace({ uri: projectUri, name: path.basename(projectUri.fsPath) }, true)
+
+ // Open template file
+ if (templateUri) {
+ // extension not reloaded, set to false
+ await globals.globalState.update(templateToOpenAppComposer, undefined)
+ await vscode.commands.executeCommand('aws.openInApplicationComposer', templateUri)
+ }
+
+ // Open Readme if exist
+ if (await fs.exists(vscode.Uri.joinPath(projectUri, 'README.md'))) {
+ await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup')
+ await vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.joinPath(projectUri, 'README.md'))
+ }
+}
+
+/**
+ * Used in Toolkit Appbuilder Walkthrough.
+ * 1: Customer select a template
+ * 2: Create project / Or don't create if customer choose use my own template
+ * 3: Add project to workspace, Open template.yaml, open template.yaml in AppComposer
+ */
+export async function initWalkthroughProjectCommand() {
+ const walkthroughSelected = globals.globalState.get(walkthroughContextString)
+ let runtimeSelected: TutorialRuntimeOptions | undefined = undefined
+ try {
+ if (!walkthroughSelected || !(typeof walkthroughSelected === 'string')) {
+ getLogger().info('exit on no walkthrough selected')
+ void vscode.window.showErrorMessage(
+ localize('AWS.toolkit.lambda.walkthroughNotSelected', 'Please select a template first')
+ )
+ return
+ }
+ let labelValue = 'Create Project'
+ if (walkthroughSelected === 'CustomTemplate') {
+ labelValue = 'Open existing Project'
+ }
+ // if these two, skip runtime choice
+ const skipRuntimeChoice = walkthroughSelected === 'Visual' || walkthroughSelected === 'CustomTemplate'
+ const templates: vscode.Uri[] =
+ walkthroughSelected === 'CustomTemplate'
+ ? await vscode.workspace.findFiles('**/{template.yaml,template.yml}', '**/.aws-sam/*')
+ : []
+ const result = await new RuntimeLocationWizard(skipRuntimeChoice, labelValue, templates).run()
+ if (!result) {
+ getLogger().info('User canceled the runtime selection process via quickpick')
+ return
+ }
+
+ if (!result.realDir || !fs.exists(result.realDir)) {
+ // exit for non-vaild uri
+ getLogger().info('exit on customer fileselector cancellation')
+ return
+ }
+
+ runtimeSelected = result.runtime
+
+ // generate project
+ await genWalkthroughProject(walkthroughSelected, result.realDir, runtimeSelected)
+ // open a workspace if no workspace yet
+ await openProjectInWorkspace(result.realDir)
+ } finally {
+ telemetry.record({ action: `${walkthroughSelected}:${runtimeSelected}`, source: 'AppBuilderWalkthrough' })
+ }
+}
+
+export async function getOrUpdateOrInstallSAMCli(source: string) {
+ try {
+ // find sam
+ const samPath = await getOrInstallCli('sam-cli', true, true)
+ // check version
+ const samCliVersion = (await new SamCliInfoInvocation(samPath).execute()).version
+
+ if (semver.lt(samCliVersion, minSamCliVersionForAppBuilderSupport)) {
+ // sam found but version too low
+ const updateInstruction = localize(
+ 'AWS.toolkit.updateSAMInstruction',
+ 'View AWS SAM CLI update instructions'
+ )
+ const selection = await vscode.window.showInformationMessage(
+ localize(
+ 'AWS.toolkit.samOutdatedPrompt',
+ 'AWS SAM CLI version {0} or greater is required ({1} currently installed).',
+ minSamCliVersionForAppBuilderSupport,
+ samCliVersion
+ ),
+ updateInstruction
+ )
+ if (selection === updateInstruction) {
+ void openUrl(vscode.Uri.parse(awsClis['sam-cli'].manualInstallLink))
+ }
+ }
+ } catch (err) {
+ throw ToolkitError.chain(err, 'Failed to install or detect SAM')
+ } finally {
+ telemetry.record({ source: source, toolId: 'sam-cli' })
+ }
+}
+
+/**
+ * wraps getOrinstallCli and send telemetry
+ * @param toolId to install/check
+ * @param source to be added in telemetry
+ */
+export async function getOrInstallCliWrapper(toolId: AwsClis, source: string) {
+ await telemetry.appBuilder_installTool.run(async (span) => {
+ span.record({ source: source, toolId: toolId })
+ if (toolId === 'sam-cli') {
+ await getOrUpdateOrInstallSAMCli(source)
+ return
+ }
+ try {
+ await getOrInstallCli(toolId, true, true)
+ } finally {
+ telemetry.record({ source: source, toolId: toolId })
+ }
+ })
+}
diff --git a/packages/core/src/awsService/appBuilder/wizards/deployTypeWizard.ts b/packages/core/src/awsService/appBuilder/wizards/deployTypeWizard.ts
new file mode 100644
index 00000000000..fbaec4657ca
--- /dev/null
+++ b/packages/core/src/awsService/appBuilder/wizards/deployTypeWizard.ts
@@ -0,0 +1,54 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { samDeployUrl } from '../../../shared/constants'
+import { createCommonButtons } from '../../../shared/ui/buttons'
+import { DataQuickPickItem, createQuickPick } from '../../../shared/ui/pickerPrompter'
+import * as nls from 'vscode-nls'
+import { Wizard } from '../../../shared/wizards/wizard'
+import { DeployParams, DeployWizard } from '../../../shared/sam/deploy'
+import { SyncParams, SyncWizard } from '../../../shared/sam/sync'
+import { WizardPrompter } from '../../../shared/ui/wizardPrompter'
+import { createExitPrompter } from '../../../shared/ui/common/exitPrompter'
+const localize = nls.loadMessageBundle()
+
+export class DeployTypeWizard extends Wizard<{
+ choice: string
+ syncParam: SyncParams
+ deployParam: DeployParams
+}> {
+ public constructor(syncWizard: SyncWizard, deployWizard: DeployWizard) {
+ super({ exitPrompterProvider: createExitPrompter })
+ const form = this.form
+
+ const items: DataQuickPickItem[] = [
+ {
+ label: 'Sync',
+ data: 'sync',
+ detail: 'Speed up your development and testing experience in the AWS Cloud. With the --watch parameter, sync will build, deploy and watch for local changes',
+ description: 'Development environments',
+ },
+ {
+ label: 'Deploy',
+ data: 'deploy',
+ detail: 'Deploys your template through CloudFormation',
+ description: 'Production environments',
+ },
+ ]
+ form.choice.bindPrompter(() => {
+ return createQuickPick(items, {
+ title: localize('AWS.appBuilder.deployType.title', 'Select deployment command'),
+ placeholder: 'Press enter to proceed with highlighted option',
+ buttons: createCommonButtons(samDeployUrl),
+ })
+ })
+ form.deployParam.bindPrompter((state) => new WizardPrompter(deployWizard), {
+ showWhen: (state) => state.choice === 'deploy',
+ })
+ form.syncParam.bindPrompter((state) => new WizardPrompter(syncWizard), {
+ showWhen: (state) => state.choice === 'sync',
+ })
+ }
+}
diff --git a/packages/core/src/awsService/cloudWatchLogs/activation.ts b/packages/core/src/awsService/cloudWatchLogs/activation.ts
index a186a8ba983..5c3f4ac91c8 100644
--- a/packages/core/src/awsService/cloudWatchLogs/activation.ts
+++ b/packages/core/src/awsService/cloudWatchLogs/activation.ts
@@ -19,6 +19,10 @@ import { searchLogGroup } from './commands/searchLogGroup'
import { changeLogSearchParams } from './changeLogSearch'
import { CloudWatchLogsNode } from './explorer/cloudWatchLogsNode'
import { loadAndOpenInitialLogStreamFile, LogStreamCodeLensProvider } from './document/logStreamsCodeLensProvider'
+import { DeployedResourceNode } from '../appBuilder/explorer/nodes/deployedNode'
+import { isTreeNode } from '../../shared/treeview/resourceTreeDataProvider'
+import { getLogger } from '../../shared/logger/logger'
+import { ToolkitError } from '../../shared'
export async function activate(context: vscode.ExtensionContext, configuration: Settings): Promise {
const registry = LogDataRegistry.instance
@@ -89,6 +93,26 @@ export async function activate(context: vscode.ExtensionContext, configuration:
Commands.register('aws.cwl.changeFilterPattern', async () => changeLogSearchParams(registry, 'filterPattern')),
- Commands.register('aws.cwl.changeTimeFilter', async () => changeLogSearchParams(registry, 'timeFilter'))
+ Commands.register('aws.cwl.changeTimeFilter', async () => changeLogSearchParams(registry, 'timeFilter')),
+
+ Commands.register('aws.appBuilder.searchLogs', async (node: DeployedResourceNode) => {
+ try {
+ const logGroupInfo = isTreeNode(node)
+ ? {
+ regionName: node.resource.regionCode,
+ groupName: getFunctionLogGroupName(node.resource.explorerNode.configuration),
+ }
+ : undefined
+ const source: string = logGroupInfo ? 'AppBuilderSearchLogs' : 'CommandPaletteSearchLogs'
+ await searchLogGroup(registry, source, logGroupInfo)
+ } catch (err) {
+ getLogger().error('Failed to search logs: %s', err)
+ throw ToolkitError.chain(err, 'Failed to search logs')
+ }
+ })
)
}
+function getFunctionLogGroupName(configuration: any) {
+ const logGroupPrefix = '/aws/lambda/'
+ return configuration.logGroupName || logGroupPrefix + configuration.FunctionName
+}
diff --git a/packages/core/src/awsService/s3/activation.ts b/packages/core/src/awsService/s3/activation.ts
index fcaee4f3905..2b8a164800a 100644
--- a/packages/core/src/awsService/s3/activation.ts
+++ b/packages/core/src/awsService/s3/activation.ts
@@ -25,6 +25,8 @@ import { Commands } from '../../shared/vscode/commands2'
import * as nls from 'vscode-nls'
import { DefaultS3Client } from '../../shared/clients/s3Client'
+import { TreeNode } from '../../shared/treeview/resourceTreeDataProvider'
+import { getSourceNode } from '../../shared/utilities/treeNodeUtils'
const localize = nls.loadMessageBundle()
/**
@@ -58,7 +60,7 @@ export async function activate(ctx: ExtContext): Promise {
}),
Commands.register(
{ id: 'aws.s3.uploadFile', autoconnect: true },
- async (node?: S3BucketNode | S3FolderNode) => {
+ async (node?: S3BucketNode | S3FolderNode | TreeNode) => {
if (!node) {
const awsContext = ctx.awsContext
const regionCode = awsContext.getCredentialDefaultRegion()
@@ -66,7 +68,8 @@ export async function activate(ctx: ExtContext): Promise {
const document = vscode.window.activeTextEditor?.document.uri
await uploadFileCommand(s3Client, document)
} else {
- await uploadFileCommand(node.s3, node)
+ const sourceNode = getSourceNode(node)
+ await uploadFileCommand(sourceNode.s3, sourceNode)
}
}
),
@@ -76,11 +79,13 @@ export async function activate(ctx: ExtContext): Promise {
Commands.register('aws.s3.createBucket', async (node: S3Node) => {
await createBucketCommand(node)
}),
- Commands.register('aws.s3.createFolder', async (node: S3BucketNode | S3FolderNode) => {
- await createFolderCommand(node)
+ Commands.register('aws.s3.createFolder', async (node: S3BucketNode | S3FolderNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await createFolderCommand(sourceNode)
}),
- Commands.register('aws.s3.deleteBucket', async (node: S3BucketNode) => {
- await deleteBucketCommand(node)
+ Commands.register('aws.s3.deleteBucket', async (node: S3BucketNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await deleteBucketCommand(sourceNode)
}),
Commands.register('aws.s3.deleteFile', async (node: S3FileNode) => {
await deleteFileCommand(node)
diff --git a/packages/core/src/awsexplorer/activation.ts b/packages/core/src/awsexplorer/activation.ts
index a93baef6034..5ea7295bf98 100644
--- a/packages/core/src/awsexplorer/activation.ts
+++ b/packages/core/src/awsexplorer/activation.ts
@@ -32,6 +32,10 @@ import { activateViewsShared, registerToolView } from './activationShared'
import { isExtensionInstalled } from '../shared/utilities'
import { CommonAuthViewProvider } from '../login/webview'
import { setContext } from '../shared'
+import { TreeNode } from '../shared/treeview/resourceTreeDataProvider'
+import { getSourceNode } from '../shared/utilities/treeNodeUtils'
+import { openAwsCFNConsoleCommand, openAwsConsoleCommand } from '../shared/awsConsole'
+import { StackNameNode } from '../awsService/appBuilder/explorer/nodes/deployedStack'
/**
* Activates the AWS Explorer UI and related functionality.
@@ -121,6 +125,7 @@ export async function activate(args: {
refreshCommands: [refreshAmazonQ, refreshAmazonQRootNode],
})
}
+
const viewNodes: ToolView[] = [
...amazonQViewNode,
...codecatalystViewNode,
@@ -196,8 +201,21 @@ async function registerAwsExplorerCommands(
isPreviewAndRender: true,
})
),
- Commands.register('aws.copyArn', async (node: AWSResourceNode) => await copyTextCommand(node, 'ARN')),
- Commands.register('aws.copyName', async (node: AWSResourceNode) => await copyTextCommand(node, 'name')),
+ Commands.register('aws.copyArn', async (node: AWSResourceNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await copyTextCommand(sourceNode, 'ARN')
+ }),
+ Commands.register('aws.copyName', async (node: AWSResourceNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await copyTextCommand(sourceNode, 'name')
+ }),
+ Commands.register('aws.openAwsConsole', async (node: AWSResourceNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await openAwsConsoleCommand(sourceNode)
+ }),
+ Commands.register('aws.openAwsCFNConsole', async (node: StackNameNode) => {
+ await openAwsCFNConsoleCommand(node)
+ }),
Commands.register('aws.refreshAwsExplorerNode', async (element: AWSTreeNodeBase | undefined) => {
awsExplorer.refresh(element)
}),
diff --git a/packages/core/src/extensionNode.ts b/packages/core/src/extensionNode.ts
index 98f222578c8..42aab7bcf09 100644
--- a/packages/core/src/extensionNode.ts
+++ b/packages/core/src/extensionNode.ts
@@ -7,6 +7,7 @@ import * as vscode from 'vscode'
import * as nls from 'vscode-nls'
import * as codecatalyst from './codecatalyst/activation'
+import { activate as activateAppBuilder } from './awsService/appBuilder/activation'
import { activate as activateAwsExplorer } from './awsexplorer/activation'
import { activate as activateCloudWatchLogs } from './awsService/cloudWatchLogs/activation'
import { activate as activateSchemas } from './eventSchemas/activation'
@@ -203,6 +204,8 @@ export async function activate(context: vscode.ExtensionContext) {
await activateRedshift(extContext)
+ await activateAppBuilder(extContext)
+
await activateIamPolicyChecks(extContext)
context.subscriptions.push(
diff --git a/packages/core/src/lambda/activation.ts b/packages/core/src/lambda/activation.ts
index 911f3f9be44..4a21b2e9611 100644
--- a/packages/core/src/lambda/activation.ts
+++ b/packages/core/src/lambda/activation.ts
@@ -11,25 +11,36 @@ import { downloadLambdaCommand } from './commands/downloadLambda'
import { tryRemoveFolder } from '../shared/filesystemUtilities'
import { ExtContext } from '../shared/extensions'
import { invokeRemoteLambda } from './vue/remoteInvoke/invokeLambda'
-import { registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend'
+import { registerSamDebugInvokeVueCommand, registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend'
import { Commands } from '../shared/vscode/commands2'
import { DefaultLambdaClient } from '../shared/clients/lambdaClient'
import { copyLambdaUrl } from './commands/copyLambdaUrl'
+import { ResourceNode } from '../awsService/appBuilder/explorer/nodes/resourceNode'
+import { isTreeNode, TreeNode } from '../shared/treeview/resourceTreeDataProvider'
+import { getSourceNode } from '../shared/utilities/treeNodeUtils'
/**
* Activates Lambda components.
*/
export async function activate(context: ExtContext): Promise {
context.extensionContext.subscriptions.push(
- Commands.register('aws.deleteLambda', async (node: LambdaFunctionNode) => {
- await deleteLambda(node.configuration, new DefaultLambdaClient(node.regionCode))
- await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', node.parent)
+ Commands.register('aws.deleteLambda', async (node: LambdaFunctionNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await deleteLambda(sourceNode.configuration, new DefaultLambdaClient(sourceNode.regionCode))
+ await vscode.commands.executeCommand('aws.refreshAwsExplorerNode', sourceNode.parent)
+ }),
+ Commands.register('aws.invokeLambda', async (node: LambdaFunctionNode | TreeNode) => {
+ let source: string = 'AwsExplorerRemoteInvoke'
+ if (isTreeNode(node)) {
+ node = getSourceNode(node)
+ source = 'AppBuilderRemoteInvoke'
+ }
+ await invokeRemoteLambda(context, {
+ outputChannel: context.outputChannel,
+ functionNode: node,
+ source: source,
+ })
}),
- Commands.register(
- 'aws.invokeLambda',
- async (node: LambdaFunctionNode) =>
- await invokeRemoteLambda(context, { outputChannel: context.outputChannel, functionNode: node })
- ),
// Capture debug finished events, and delete the temporary directory if it exists
vscode.debug.onDidTerminateDebugSession(async (session) => {
if (
@@ -39,7 +50,10 @@ export async function activate(context: ExtContext): Promise {
await tryRemoveFolder(session.configuration.baseBuildDir)
}
}),
- Commands.register('aws.downloadLambda', async (node: LambdaFunctionNode) => await downloadLambdaCommand(node)),
+ Commands.register('aws.downloadLambda', async (node: LambdaFunctionNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await downloadLambdaCommand(sourceNode)
+ }),
Commands.register({ id: 'aws.uploadLambda', autoconnect: true }, async (arg?: unknown) => {
if (arg instanceof LambdaFunctionNode) {
await uploadLambdaCommand({
@@ -53,10 +67,15 @@ export async function activate(context: ExtContext): Promise {
await uploadLambdaCommand()
}
}),
- Commands.register(
- 'aws.copyLambdaUrl',
- async (node: LambdaFunctionNode) => await copyLambdaUrl(node, new DefaultLambdaClient(node.regionCode))
- ),
- registerSamInvokeVueCommand(context)
+ Commands.register('aws.copyLambdaUrl', async (node: LambdaFunctionNode | TreeNode) => {
+ const sourceNode = getSourceNode(node)
+ await copyLambdaUrl(sourceNode, new DefaultLambdaClient(sourceNode.regionCode))
+ }),
+
+ registerSamInvokeVueCommand(context),
+
+ Commands.register('aws.launchDebugConfigForm', async (node: ResourceNode) =>
+ registerSamDebugInvokeVueCommand(context, { resource: node })
+ )
)
}
diff --git a/packages/core/src/lambda/commands/deploySamApplication.ts b/packages/core/src/lambda/commands/deploySamApplication.ts
deleted file mode 100644
index 4231b2b614e..00000000000
--- a/packages/core/src/lambda/commands/deploySamApplication.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-/*!
- * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-import * as path from 'path'
-import * as vscode from 'vscode'
-import * as nls from 'vscode-nls'
-
-import { asEnvironmentVariables } from '../../auth/credentials/utils'
-import { AwsContext } from '../../shared/awsContext'
-import globals from '../../shared/extensionGlobals'
-
-import { makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities'
-import { checklogs } from '../../shared/localizedText'
-import { getLogger } from '../../shared/logger'
-import { SamCliBuildInvocation } from '../../shared/sam/cli/samCliBuild'
-import { SamCliSettings } from '../../shared/sam/cli/samCliSettings'
-import { getSamCliContext, SamCliContext, getSamCliVersion } from '../../shared/sam/cli/samCliContext'
-import { runSamCliDeploy } from '../../shared/sam/cli/samCliDeploy'
-import { SamCliProcessInvoker } from '../../shared/sam/cli/samCliInvokerUtils'
-import { runSamCliPackage } from '../../shared/sam/cli/samCliPackage'
-import { throwAndNotifyIfInvalid } from '../../shared/sam/cli/samCliValidationUtils'
-import { Result } from '../../shared/telemetry/telemetry'
-import { addCodiconToString } from '../../shared/utilities/textUtilities'
-import { SamDeployWizardResponse } from '../wizards/samDeployWizard'
-import { telemetry } from '../../shared/telemetry/telemetry'
-
-const localize = nls.loadMessageBundle()
-
-interface DeploySamApplicationParameters {
- sourceTemplatePath: string
- deployRootFolder: string
- environmentVariables: NodeJS.ProcessEnv
- region: string
- packageBucketName: string
- ecrRepo?: string
- destinationStackName: string
- parameterOverrides: Map
-}
-
-export interface WindowFunctions {
- showInformationMessage: typeof vscode.window.showInformationMessage
- showErrorMessage: typeof vscode.window.showErrorMessage
- setStatusBarMessage(text: string, hideWhenDone: Thenable): vscode.Disposable
-}
-
-export async function deploySamApplication(
- {
- samCliContext = getSamCliContext(),
- samDeployWizard,
- }: {
- samCliContext?: SamCliContext
- samDeployWizard: () => Promise
- },
- {
- awsContext,
- settings,
- window = getDefaultWindowFunctions(),
- refreshFn = () => {
- // no need to await, doesn't need to block further execution (true -> no telemetry)
- void vscode.commands.executeCommand('aws.refreshAwsExplorer', true)
- },
- }: {
- awsContext: Pick
- settings: SamCliSettings
- window?: WindowFunctions
- refreshFn?: () => void
- }
-): Promise {
- let deployResult: Result = 'Succeeded'
- let samVersion: string | undefined
- let deployFolder: string | undefined
- try {
- const credentials = await awsContext.getCredentials()
- if (!credentials) {
- throw new Error('No AWS profile selected')
- }
-
- throwAndNotifyIfInvalid(await samCliContext.validator.detectValidSamCli())
-
- const deployWizardResponse = await samDeployWizard()
-
- if (!deployWizardResponse) {
- return
- }
-
- deployFolder = await makeTemporaryToolkitFolder('samDeploy')
- samVersion = await getSamCliVersion(samCliContext)
-
- const deployParameters: DeploySamApplicationParameters = {
- deployRootFolder: deployFolder,
- destinationStackName: deployWizardResponse.stackName,
- packageBucketName: deployWizardResponse.s3Bucket,
- ecrRepo: deployWizardResponse.ecrRepo?.repositoryUri,
- parameterOverrides: deployWizardResponse.parameterOverrides,
- environmentVariables: asEnvironmentVariables(credentials),
- region: deployWizardResponse.region,
- sourceTemplatePath: deployWizardResponse.template.fsPath,
- }
-
- const deployApplicationPromise = deploy({
- deployParameters,
- invoker: samCliContext.invoker,
- window,
- })
-
- window.setStatusBarMessage(
- addCodiconToString(
- 'cloud-upload',
- localize(
- 'AWS.samcli.deploy.statusbar.message',
- 'Deploying SAM Application to {0}...',
- deployWizardResponse.stackName
- )
- ),
- deployApplicationPromise
- )
-
- await deployApplicationPromise
- refreshFn()
-
- // successful deploy: retain S3 bucket for quick future access
- const profile = awsContext.getCredentialProfileName()
- if (profile) {
- await settings.updateSavedBuckets(profile, deployWizardResponse.region, deployWizardResponse.s3Bucket)
- } else {
- getLogger().warn('Profile not provided; cannot write recent buckets.')
- }
- } catch (err) {
- deployResult = 'Failed'
- outputDeployError(err as Error)
- void vscode.window.showErrorMessage(
- localize('AWS.samcli.deploy.workflow.error', 'Failed to deploy SAM application.')
- )
- } finally {
- await tryRemoveFolder(deployFolder)
- telemetry.sam_deploy.emit({ result: deployResult, version: samVersion })
- }
-}
-
-function getBuildRootFolder(deployRootFolder: string): string {
- return path.join(deployRootFolder, 'build')
-}
-
-function getBuildTemplatePath(deployRootFolder: string): string {
- // Assumption: sam build will always produce a template.yaml file.
- // If that is not the case, revisit this logic.
- return path.join(getBuildRootFolder(deployRootFolder), 'template.yaml')
-}
-
-function getPackageTemplatePath(deployRootFolder: string): string {
- return path.join(deployRootFolder, 'template.yaml')
-}
-
-async function buildOperation(params: {
- deployParameters: DeploySamApplicationParameters
- invoker: SamCliProcessInvoker
-}): Promise {
- try {
- getLogger().info(localize('AWS.samcli.deploy.workflow.init', 'Building SAM Application...'))
-
- const buildDestination = getBuildRootFolder(params.deployParameters.deployRootFolder)
-
- const build = new SamCliBuildInvocation({
- buildDir: buildDestination,
- baseDir: undefined,
- templatePath: params.deployParameters.sourceTemplatePath,
- invoker: params.invoker,
- })
-
- await build.execute()
-
- return true
- } catch (err) {
- getLogger().warn(
- localize(
- 'AWS.samcli.build.failedBuild',
- '"sam build" failed: {0}',
- params.deployParameters.sourceTemplatePath
- )
- )
- return false
- }
-}
-
-async function packageOperation(
- params: {
- deployParameters: DeploySamApplicationParameters
- invoker: SamCliProcessInvoker
- },
- buildSuccessful: boolean
-): Promise {
- if (!buildSuccessful) {
- void vscode.window.showInformationMessage(
- localize(
- 'AWS.samcli.deploy.workflow.packaging.noBuild',
- 'Attempting to package source template directory directly since "sam build" failed'
- )
- )
- }
-
- getLogger().info(
- localize(
- 'AWS.samcli.deploy.workflow.packaging',
- 'Packaging SAM Application to S3 Bucket: {0}',
- params.deployParameters.packageBucketName
- )
- )
-
- // HACK: Attempt to package the initial template if the build fails.
- const buildTemplatePath = buildSuccessful
- ? getBuildTemplatePath(params.deployParameters.deployRootFolder)
- : params.deployParameters.sourceTemplatePath
- const packageTemplatePath = getPackageTemplatePath(params.deployParameters.deployRootFolder)
-
- await runSamCliPackage(
- {
- sourceTemplateFile: buildTemplatePath,
- destinationTemplateFile: packageTemplatePath,
- environmentVariables: params.deployParameters.environmentVariables,
- region: params.deployParameters.region,
- s3Bucket: params.deployParameters.packageBucketName,
- ecrRepo: params.deployParameters.ecrRepo,
- },
- params.invoker
- )
-}
-
-async function deployOperation(params: {
- deployParameters: DeploySamApplicationParameters
- invoker: SamCliProcessInvoker
-}): Promise {
- try {
- getLogger().info(
- localize(
- 'AWS.samcli.deploy.workflow.stackName.initiated',
- 'Deploying SAM Application to CloudFormation Stack: {0}',
- params.deployParameters.destinationStackName
- )
- )
-
- const packageTemplatePath = getPackageTemplatePath(params.deployParameters.deployRootFolder)
-
- await runSamCliDeploy(
- {
- parameterOverrides: params.deployParameters.parameterOverrides,
- environmentVariables: params.deployParameters.environmentVariables,
- templateFile: packageTemplatePath,
- region: params.deployParameters.region,
- stackName: params.deployParameters.destinationStackName,
- s3Bucket: params.deployParameters.packageBucketName,
- ecrRepo: params.deployParameters.ecrRepo,
- },
- params.invoker
- )
- } catch (err) {
- // Handle sam deploy Errors to supplement the error message prior to writing it out
- const error = err as Error
-
- getLogger().error(error)
-
- const errorMessage = enhanceAwsCloudFormationInstructions(String(err), params.deployParameters)
- globals.outputChannel.appendLine(errorMessage)
-
- throw new Error('Deploy failed')
- }
-}
-
-async function deploy(params: {
- deployParameters: DeploySamApplicationParameters
- invoker: SamCliProcessInvoker
- window: WindowFunctions
-}): Promise {
- globals.outputChannel.show(true)
- getLogger().info(localize('AWS.samcli.deploy.workflow.start', 'Starting SAM Application deployment...'))
-
- const buildSuccessful = await buildOperation(params)
- await packageOperation(params, buildSuccessful)
- await deployOperation(params)
-
- getLogger().info(
- localize(
- 'AWS.samcli.deploy.workflow.success',
- 'Deployed SAM Application to CloudFormation Stack: {0}',
- params.deployParameters.destinationStackName
- )
- )
-
- void params.window.showInformationMessage(
- localize('AWS.samcli.deploy.workflow.success.general', 'SAM Application deployment succeeded.')
- )
-}
-
-function enhanceAwsCloudFormationInstructions(
- message: string,
- deployParameters: DeploySamApplicationParameters
-): string {
- // detect error message from https://github.com/aws/aws-cli/blob/4ff0cbacbac69a21d4dd701921fe0759cf7852ed/awscli/customizations/cloudformation/exceptions.py#L42
- // and append region to assist in troubleshooting the error
- // (command uses CLI configured value--users that don't know this and omit region won't see error)
- if (
- message.includes(
- `aws cloudformation describe-stack-events --stack-name ${deployParameters.destinationStackName}`
- )
- ) {
- message += ` --region ${deployParameters.region}`
- }
-
- return message
-}
-
-function outputDeployError(error: Error) {
- getLogger().error(error)
-
- globals.outputChannel.show(true)
- getLogger().error('AWS.samcli.deploy.general.error', 'Error deploying a SAM Application. {0}', checklogs())
-}
-
-function getDefaultWindowFunctions(): WindowFunctions {
- return {
- setStatusBarMessage: vscode.window.setStatusBarMessage,
- showErrorMessage: vscode.window.showErrorMessage,
- showInformationMessage: vscode.window.showInformationMessage,
- }
-}
diff --git a/packages/core/src/lambda/commands/listSamResources.ts b/packages/core/src/lambda/commands/listSamResources.ts
new file mode 100644
index 00000000000..5a0d1678c9b
--- /dev/null
+++ b/packages/core/src/lambda/commands/listSamResources.ts
@@ -0,0 +1,43 @@
+/*!
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as vscode from 'vscode'
+import { getLogger } from '../../shared/logger'
+import { runSamCliListResource } from '../../shared/sam/cli/samCliListResources'
+
+export interface StackResource {
+ LogicalResourceId: string
+ PhysicalResourceId: string
+}
+
+/*
+This function return exclusively the deployed resources
+Newly added but yet-to-be deployed resources are not included in this result
+*/
+export async function getDeployedResources(params: any) {
+ try {
+ const samCliListResourceOutput = await runSamCliListResource(params.listResourcesParams, params.invoker).then(
+ (output) => parseSamListResourceOutput(output)
+ )
+ // Filter out resources that are not deployed
+ return samCliListResourceOutput.filter((resource) => resource.PhysicalResourceId !== '-')
+ } catch (err) {
+ const error = err as Error
+ getLogger().error(error)
+ }
+}
+
+function parseSamListResourceOutput(output: any): StackResource[] {
+ try {
+ if ((Array.isArray(output) && output.length === 0) || '[]' === output) {
+ // Handle if the output is instance or stringify version of an empty array to avoid parsing error
+ return []
+ }
+ return JSON.parse(output) as StackResource[]
+ } catch (error: any) {
+ void vscode.window.showErrorMessage(`Failed to parse SAM CLI output: ${error.message}`)
+ return []
+ }
+}
diff --git a/packages/core/src/lambda/models/samLambdaRuntime.ts b/packages/core/src/lambda/models/samLambdaRuntime.ts
index 754d910a24e..5b97ef06e2a 100644
--- a/packages/core/src/lambda/models/samLambdaRuntime.ts
+++ b/packages/core/src/lambda/models/samLambdaRuntime.ts
@@ -22,6 +22,7 @@ export enum RuntimeFamily {
DotNet,
Go,
Java,
+ Ruby,
}
export type RuntimePackageType = 'Image' | 'Zip'
@@ -57,8 +58,15 @@ export const pythonRuntimes: ImmutableSet = ImmutableSet([
'python3.7',
])
export const goRuntimes: ImmutableSet = ImmutableSet(['go1.x'])
-export const javaRuntimes: ImmutableSet = ImmutableSet(['java17', 'java11', 'java8', 'java8.al2'])
-export const dotNetRuntimes: ImmutableSet = ImmutableSet(['dotnet6'])
+export const javaRuntimes: ImmutableSet = ImmutableSet([
+ 'java17',
+ 'java11',
+ 'java8',
+ 'java8.al2',
+ 'java21',
+])
+export const dotNetRuntimes: ImmutableSet = ImmutableSet(['dotnet6', 'dotnet8'])
+export const rubyRuntimes: ImmutableSet = ImmutableSet(['ruby3.2', 'ruby3.3'])
/**
* Deprecated runtimes can be found at https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html
@@ -77,6 +85,8 @@ export const deprecatedRuntimes: ImmutableSet = ImmutableSet([
'nodejs8.10',
'nodejs10.x',
'nodejs12.x',
+ 'ruby2.5',
+ 'ruby2.7',
])
const defaultRuntimes = ImmutableMap([
[RuntimeFamily.NodeJS, 'nodejs20.x'],
@@ -84,6 +94,7 @@ const defaultRuntimes = ImmutableMap([
[RuntimeFamily.DotNet, 'dotnet6'],
[RuntimeFamily.Go, 'go1.x'],
[RuntimeFamily.Java, 'java17'],
+ [RuntimeFamily.Ruby, 'ruby3.3'],
])
export const samZipLambdaRuntimes: ImmutableSet = ImmutableSet.union([
@@ -157,6 +168,8 @@ export function getFamily(runtime: string): RuntimeFamily {
return RuntimeFamily.Go
} else if (javaRuntimes.has(runtime)) {
return RuntimeFamily.Java
+ } else if (rubyRuntimes.has(runtime)) {
+ return RuntimeFamily.Ruby
}
return RuntimeFamily.Unknown
}
@@ -206,6 +219,10 @@ export function getRuntimeFamily(langId: string): RuntimeFamily {
return RuntimeFamily.Python
case 'go':
return RuntimeFamily.Go
+ case 'java':
+ return RuntimeFamily.Java
+ case 'ruby':
+ return RuntimeFamily.Ruby
default:
return RuntimeFamily.Unknown
}
diff --git a/packages/core/src/lambda/vue/configEditor/samInvoke.css b/packages/core/src/lambda/vue/configEditor/samInvoke.css
index d248e071a90..9ca2c8ef452 100644
--- a/packages/core/src/lambda/vue/configEditor/samInvoke.css
+++ b/packages/core/src/lambda/vue/configEditor/samInvoke.css
@@ -1,7 +1,3 @@
-form {
- padding: 15px;
-}
-
.section-header {
margin: 0px;
margin-bottom: 10px;
@@ -10,7 +6,9 @@ form {
}
textarea {
- max-width: 100%;
+ color: var(--vscode-settings-textInputForeground);
+ background: var(--vscode-settings-textInputBackground);
+ border: 1px solid var(--vscode-settings-textInputBorder);
}
.config-item {
@@ -47,7 +45,133 @@ textarea {
margin-bottom: 16px;
}
+.header-buttons {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
#target-type-selector {
margin-bottom: 15px;
margin-left: 8px;
}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 150px 1fr;
+ margin-bottom: 10px;
+}
+
+.form-control {
+ min-width: 170%; /* Set a minimum width */
+ width: 100%; /* Allow the width to adjust based on content */
+ display: inline-block;
+ flex-grow: 1;
+ margin-right: 0.5rem;
+}
+
+.payload-options-button {
+ display: grid;
+ align-items: center;
+ border: none;
+ padding: 5px 10px;
+ cursor: pointer;
+ font-size: 0.9em;
+ margin-bottom: 10px;
+}
+
+.payload-options-buttons {
+ display: flex;
+ align-items: center;
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.Icontainer {
+ margin-inline: auto;
+ margin-top: 5rem;
+}
+
+.container {
+ width: 574px;
+ height: 824px;
+ top: 18px;
+ gap: 20px;
+ margin: auto;
+ left: 688px;
+ background-color: var(--vscode-editor-background);
+}
+
+.container em {
+ display: block;
+ text-align: justify;
+}
+
+.button-theme-primary {
+ color: var(--vscode-button-foreground);
+ background: var(--vscode-button-background);
+ border: 1px solid var(--vscode-button-border);
+ padding: 8px 12px;
+}
+.button-theme-primary:hover:not(:disabled) {
+ background: var(--vscode-button-hoverBackground);
+ cursor: pointer;
+}
+.button-theme-secondary {
+ color: var(--vscode-button-secondaryForeground);
+ background: var(--vscode-button-secondaryBackground);
+ border: 1px solid var(--vscode-button-border);
+ padding: 8px 12px;
+}
+.button-theme-secondary:hover:not(:disabled) {
+ background: var(--vscode-button-secondaryHoverBackground);
+ cursor: pointer;
+}
+
+.formfield {
+ display: flex;
+ align-items: center;
+ margin-bottom: 0.5rem;
+}
+
+.payload-options-buttons {
+ display: flex;
+ align-items: center;
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.radio-selector {
+ width: 15px;
+ height: 15px;
+ border-radius: 50%;
+}
+
+.label-selector {
+ padding-left: 7px;
+ font-weight: 500;
+ font-size: 13px;
+ line-height: 15.51px;
+ text-align: center;
+}
+
+.form-row-select {
+ width: 387px;
+ height: 28px;
+ border: 1px;
+ border-radius: 5px;
+ gap: 4px;
+ padding: 2px 8px;
+}
+
+.form-row-event-select {
+ width: 244px;
+ height: 28px;
+ margin-bottom: 15px;
+ margin-left: 8px;
+}
+
+.runtime-description {
+ font-size: 12px;
+ margin-top: 5px;
+}
diff --git a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts
index 7ae941d72e1..9e3eed9980b 100644
--- a/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts
+++ b/packages/core/src/lambda/vue/configEditor/samInvokeBackend.ts
@@ -36,9 +36,22 @@ import { VueWebview } from '../../../webviews/main'
import { Commands } from '../../../shared/vscode/commands2'
import { telemetry } from '../../../shared/telemetry/telemetry'
import { fs } from '../../../shared'
+import { ToolkitError } from '../../../shared'
+import { ResourceNode } from '../../../awsService/appBuilder/explorer/nodes/resourceNode'
const localize = nls.loadMessageBundle()
+export interface ResourceData {
+ logicalId: string
+ region: string
+ arn: string
+ location: string
+ handler: string
+ runtime: string
+ stackName: string
+ source: string
+}
+
export type AwsSamDebuggerConfigurationLoose = AwsSamDebuggerConfiguration & {
invokeTarget: Omit<
AwsSamDebuggerConfiguration['invokeTarget'],
@@ -55,7 +68,7 @@ interface SampleQuickPickItem extends vscode.QuickPickItem {
filename: string
}
-interface LaunchConfigPickItem extends vscode.QuickPickItem {
+export interface LaunchConfigPickItem extends vscode.QuickPickItem {
index: number
config?: AwsSamDebuggerConfiguration
}
@@ -66,7 +79,8 @@ export class SamInvokeWebview extends VueWebview {
public constructor(
private readonly extContext: ExtContext, // TODO(sijaden): get rid of `ExtContext`
- private readonly config?: AwsSamDebuggerConfiguration
+ private readonly config?: AwsSamDebuggerConfiguration,
+ private readonly data?: ResourceData
) {
super(SamInvokeWebview.sourcePath)
}
@@ -79,11 +93,11 @@ export class SamInvokeWebview extends VueWebview {
return this.config
}
- /**
- * Open a quick pick containing the names of launch configs in the `launch.json` array.
- * Filter out non-supported launch configs.
- */
- public async loadSamLaunchConfig(): Promise {
+ public getResourceData() {
+ return this.data
+ }
+
+ public async getSamLaunchConfigs(): Promise {
// TODO: Find a better way to infer this. Might need another arg from the frontend (depends on the context in which the launch config is made?)
const workspaceFolder = vscode.workspace.workspaceFolders?.length
? vscode.workspace.workspaceFolders[0]
@@ -94,7 +108,17 @@ export class SamInvokeWebview extends VueWebview {
}
const uri = workspaceFolder.uri
const launchConfig = new LaunchConfiguration(uri)
- const pickerItems = await getLaunchConfigQuickPickItems(launchConfig, uri)
+ const pickerItems = await this.getLaunchConfigQuickPickItems(launchConfig, uri)
+ return pickerItems
+ }
+
+ /**
+ * Open a quick pick containing the names of launch configs in the `launch.json` array.
+ * Filter out non-supported launch configs.
+ */
+ public async loadSamLaunchConfig(): Promise {
+ const pickerItems: LaunchConfigPickItem[] = (await this.getSamLaunchConfigs()) || []
+
if (pickerItems.length === 0) {
pickerItems.push({
index: -1,
@@ -151,9 +175,14 @@ export class SamInvokeWebview extends VueWebview {
return sample
} catch (err) {
getLogger().error('Error getting manifest data..: %O', err as Error)
+ throw ToolkitError.chain(err, 'getting manifest data')
}
}
+ protected getTemplateRegistry() {
+ return globals.templateRegistry
+ }
+
/**
* Get all templates in the registry.
* Call back into the webview with the registry contents.
@@ -161,7 +190,7 @@ export class SamInvokeWebview extends VueWebview {
public async getTemplate() {
const items: (vscode.QuickPickItem & { templatePath: string })[] = []
const noTemplate = 'NOTEMPLATEFOUND'
- for (const template of (await globals.templateRegistry).items) {
+ for (const template of (await this.getTemplateRegistry()).items) {
const resources = template.item.Resources
if (resources) {
for (const resource of Object.keys(resources)) {
@@ -213,6 +242,41 @@ export class SamInvokeWebview extends VueWebview {
}
}
+ // This method serves as a wrapper around the backend function `openLaunchJsonFile`.
+ // The frontend cannot directly import and invoke backend functions like `openLaunchJsonFile`
+ // because doing so would break the webview environment by introducing server-side logic
+ // into client-side code. Instead, this method acts as an interface or bridge, allowing
+ // the frontend to request the backend to open the launch configuration file without
+ // directly coupling the frontend to backend-specific implementations.
+ public async openLaunchConfig() {
+ await openLaunchJsonFile()
+ }
+
+ public async promptFile() {
+ const fileLocations = await vscode.window.showOpenDialog({
+ openLabel: 'Open',
+ })
+
+ if (!fileLocations || fileLocations.length === 0) {
+ return undefined
+ }
+
+ try {
+ const fileContent = await fs.readFileBytes(fileLocations[0].fsPath)
+ return {
+ sample: fileContent,
+ selectedFilePath: fileLocations[0].fsPath,
+ selectedFile: this.getFileName(fileLocations[0].fsPath),
+ }
+ } catch (e) {
+ getLogger().error('readFileSync: Failed to read file at path %O', fileLocations[0].fsPath, e)
+ throw ToolkitError.chain(e, 'Failed to read selected file')
+ }
+ }
+
+ public getFileName(filePath: string): string {
+ return path.basename(filePath)
+ }
/**
* Open a quick pick containing the names of launch configs in the `launch.json` array, plus a "Create New Entry" entry.
* On selecting a name, overwrite the existing entry in the `launch.json` array and resave the file.
@@ -220,7 +284,7 @@ export class SamInvokeWebview extends VueWebview {
* @param config Config to save
*/
public async saveLaunchConfig(config: AwsSamDebuggerConfiguration): Promise {
- const uri = await getUriFromLaunchConfig(config)
+ const uri = await this.getUriFromLaunchConfig(config)
if (!uri) {
// TODO Localize
void vscode.window.showErrorMessage(
@@ -228,13 +292,14 @@ export class SamInvokeWebview extends VueWebview {
)
return
}
+
const launchConfig = new LaunchConfiguration(uri)
- const launchConfigItems = await getLaunchConfigQuickPickItems(launchConfig, uri)
+ const launchConfigItems = await this.getLaunchConfigQuickPickItems(launchConfig, uri)
const pickerItems = [
{
label: addCodiconToString(
'add',
- localize('AWS.command.addSamDebugConfiguration', 'Add Debug Configuration')
+ localize('AWS.command.addSamDebugConfiguration', 'Add Local Invoke and Debug Configuration')
),
index: -1,
alwaysShow: true,
@@ -267,7 +332,7 @@ export class SamInvokeWebview extends VueWebview {
const response = await input.promptUser({ inputBox: ib })
if (response) {
await launchConfig.addDebugConfiguration(finalizeConfig(config, response))
- await openLaunchJsonFile()
+ await this.openLaunchConfig()
}
} else {
// use existing label
@@ -275,7 +340,7 @@ export class SamInvokeWebview extends VueWebview {
finalizeConfig(config, pickerResponse.label),
pickerResponse.index
)
- await openLaunchJsonFile()
+ await this.openLaunchConfig()
}
}
@@ -284,12 +349,12 @@ export class SamInvokeWebview extends VueWebview {
* TODO: Post validation failures back to webview?
* @param config Config to invoke
*/
- public async invokeLaunchConfig(config: AwsSamDebuggerConfiguration): Promise {
+ public async invokeLaunchConfig(config: AwsSamDebuggerConfiguration, source?: string): Promise {
const finalConfig = finalizeConfig(
resolveWorkspaceFolderVariable(undefined, config),
'Editor-Created Debug Config'
)
- const targetUri = await getUriFromLaunchConfig(finalConfig)
+ const targetUri = await this.getUriFromLaunchConfig(finalConfig)
const folder = targetUri ? vscode.workspace.getWorkspaceFolder(targetUri) : undefined
// Cloud9 currently can't resolve the `aws-sam` debug config provider.
@@ -298,12 +363,65 @@ export class SamInvokeWebview extends VueWebview {
// (Cloud9 also doesn't currently have variable resolution support anyways)
if (isCloud9()) {
const provider = new SamDebugConfigProvider(this.extContext)
- await provider.resolveDebugConfiguration(folder, finalConfig)
+ await provider.resolveDebugConfiguration(folder, finalConfig, undefined, source)
} else {
// startDebugging on VS Code goes through the whole resolution chain
await vscode.debug.startDebugging(folder, finalConfig)
}
}
+ public async getLaunchConfigQuickPickItems(
+ launchConfig: LaunchConfiguration,
+ uri: vscode.Uri
+ ): Promise {
+ const existingConfigs = launchConfig.getDebugConfigurations()
+ const samValidator = new DefaultAwsSamDebugConfigurationValidator(vscode.workspace.getWorkspaceFolder(uri))
+ const registry = await globals.templateRegistry
+ const mapped = existingConfigs.map((val, index) => {
+ return {
+ config: val as AwsSamDebuggerConfiguration,
+ index: index,
+ label: val.name,
+ }
+ })
+ // XXX: can't use filter() with async predicate.
+ const filtered: LaunchConfigPickItem[] = []
+ for (const c of mapped) {
+ const valid = await samValidator.validate(c.config, registry, true)
+ if (valid?.isValid) {
+ filtered.push(c)
+ }
+ }
+ return filtered
+ }
+
+ public async getUriFromLaunchConfig(config: AwsSamDebuggerConfiguration): Promise {
+ let targetPath: string
+ if (isTemplateTargetProperties(config.invokeTarget)) {
+ targetPath = config.invokeTarget.templatePath
+ } else if (isCodeTargetProperties(config.invokeTarget)) {
+ targetPath = config.invokeTarget.projectRoot
+ } else {
+ // error
+ return undefined
+ }
+ if (path.isAbsolute(targetPath)) {
+ return vscode.Uri.file(targetPath)
+ }
+ // TODO: rework this logic (and config variables in general)
+ // we have too many places where we try to resolve these paths when it realistically can be
+ // in a single place. Much less bug-prone when it's centralized.
+ // the following line is a quick-fix for a very narrow edge-case
+ targetPath = targetPath.replace('${workspaceFolder}/', '')
+ const workspaceFolders = vscode.workspace.workspaceFolders || []
+ for (const workspaceFolder of workspaceFolders) {
+ const absolutePath = tryGetAbsolutePath(workspaceFolder, targetPath)
+ if (await fs.exists(absolutePath)) {
+ return vscode.Uri.file(absolutePath)
+ }
+ }
+
+ return undefined
+ }
}
const WebviewPanel = VueWebview.compilePanel(SamInvokeWebview)
@@ -313,7 +431,7 @@ export function registerSamInvokeVueCommand(context: ExtContext): vscode.Disposa
const webview = new WebviewPanel(context.extensionContext, context, launchConfig)
await telemetry.sam_openConfigUi.run(async (span) => {
await webview.show({
- title: localize('AWS.command.launchConfigForm.title', 'Edit SAM Debug Configuration'),
+ title: localize('AWS.command.launchConfigForm.title', 'Local Invoke and Debug Configuration'),
// TODO: make this only open `Beside` when executed via CodeLens
viewColumn: vscode.ViewColumn.Beside,
})
@@ -321,58 +439,28 @@ export function registerSamInvokeVueCommand(context: ExtContext): vscode.Disposa
})
}
-async function getUriFromLaunchConfig(config: AwsSamDebuggerConfiguration): Promise {
- let targetPath: string
- if (isTemplateTargetProperties(config.invokeTarget)) {
- targetPath = config.invokeTarget.templatePath
- } else if (isCodeTargetProperties(config.invokeTarget)) {
- targetPath = config.invokeTarget.projectRoot
- } else {
- // error
- return undefined
- }
- if (path.isAbsolute(targetPath)) {
- return vscode.Uri.file(targetPath)
- }
- // TODO: rework this logic (and config variables in general)
- // we have too many places where we try to resolve these paths when it realistically can be
- // in a single place. Much less bug-prone when it's centralized.
- // the following line is a quick-fix for a very narrow edge-case
- targetPath = targetPath.replace('${workspaceFolder}/', '')
- const workspaceFolders = vscode.workspace.workspaceFolders || []
- for (const workspaceFolder of workspaceFolders) {
- const absolutePath = tryGetAbsolutePath(workspaceFolder, targetPath)
- if (await fs.exists(absolutePath)) {
- return vscode.Uri.file(absolutePath)
- }
- }
-
- return undefined
-}
-
-async function getLaunchConfigQuickPickItems(
- launchConfig: LaunchConfiguration,
- uri: vscode.Uri
-): Promise {
- const existingConfigs = launchConfig.getDebugConfigurations()
- const samValidator = new DefaultAwsSamDebugConfigurationValidator(vscode.workspace.getWorkspaceFolder(uri))
- const registry = await globals.templateRegistry
- const mapped = existingConfigs.map((val, index) => {
- return {
- config: val as AwsSamDebuggerConfiguration,
- index: index,
- label: val.name,
- }
+export async function registerSamDebugInvokeVueCommand(context: ExtContext, params: { resource: ResourceNode }) {
+ const launchConfig: AwsSamDebuggerConfiguration | undefined = undefined
+ const resource = params?.resource.resource
+ const source = 'AppBuilderLocalInvoke'
+ const webview = new WebviewPanel(context.extensionContext, context, launchConfig, {
+ logicalId: resource.resource.Id ?? '',
+ region: resource.region ?? '',
+ location: resource.location.fsPath,
+ handler: resource.resource.Handler!,
+ runtime: resource.resource.Runtime!,
+ arn: resource.functionArn ?? '',
+ stackName: resource.stackName ?? '',
+ source: source,
+ })
+ await telemetry.sam_openConfigUi.run(async (span) => {
+ telemetry.record({ source: 'AppBuilderDebugger' }),
+ await webview.show({
+ title: localize('AWS.command.launchConfigForm.title', 'Local Invoke and Debug Configuration'),
+ // TODO: make this only open `Beside` when executed via CodeLens
+ viewColumn: vscode.ViewColumn.Beside,
+ })
})
- // XXX: can't use filter() with async predicate.
- const filtered: LaunchConfigPickItem[] = []
- for (const c of mapped) {
- const valid = await samValidator.validate(c.config, registry, true)
- if (valid?.isValid) {
- filtered.push(c)
- }
- }
- return filtered
}
export function finalizeConfig(config: AwsSamDebuggerConfiguration, name: string): AwsSamDebuggerConfiguration {
diff --git a/packages/core/src/lambda/vue/configEditor/samInvokeComponent.vue b/packages/core/src/lambda/vue/configEditor/samInvokeComponent.vue
index 8d958ff8206..468d7393ac6 100644
--- a/packages/core/src/lambda/vue/configEditor/samInvokeComponent.vue
+++ b/packages/core/src/lambda/vue/configEditor/samInvokeComponent.vue
@@ -4,277 +4,388 @@
-