| |
| |
| |
| |
|
|
| import * as semver from 'semver' |
| import * as vscode from 'vscode' |
| import * as nls from 'vscode-nls' |
| import { |
| getCodeRoot, |
| getHandlerName, |
| getTemplateResource, |
| NodejsDebugConfiguration, |
| PythonDebugConfiguration, |
| GoDebugConfiguration, |
| getTemplate, |
| getArchitecture, |
| isImageLambdaConfig, |
| } from '../../../lambda/local/debugConfiguration' |
| import { |
| Architecture, |
| getDefaultRuntime, |
| getFamily, |
| getRuntimeFamily, |
| goRuntimes, |
| RuntimeFamily, |
| } from '../../../lambda/models/samLambdaRuntime' |
| import { Timeout } from '../../utilities/timeoutUtils' |
| import * as csharpDebug from './csharpSamDebug' |
| import * as javaDebug from './javaSamDebug' |
| import * as pythonDebug from './pythonSamDebug' |
| import * as tsDebug from './typescriptSamDebug' |
| import * as goDebug from './goSamDebug' |
| import { ExtContext } from '../../extensions' |
| import { isInDirectory, makeTemporaryToolkitFolder } from '../../filesystemUtilities' |
| import { getLogger } from '../../logger/logger' |
| import { getStartPort } from '../../utilities/debuggerUtils' |
| import * as pathutil from '../../utilities/pathUtils' |
| import { tryGetAbsolutePath } from '../../utilities/workspaceUtils' |
| import { |
| AwsSamDebuggerConfiguration, |
| AWS_SAM_DEBUG_TYPE, |
| createApiAwsSamDebugConfig, |
| createTemplateAwsSamDebugConfig, |
| } from './awsSamDebugConfiguration' |
| import { TemplateTargetProperties } from './awsSamDebugConfiguration.gen' |
| import { |
| AwsSamDebugConfigurationValidator, |
| DefaultAwsSamDebugConfigurationValidator, |
| } from './awsSamDebugConfigurationValidator' |
| import { getInputTemplatePath, makeInputTemplate, makeJsonFiles } from '../localLambdaRunner' |
| import { SamLocalInvokeCommand } from '../cli/samCliLocalInvoke' |
| import { getCredentialsFromStore } from '../../../auth/credentials/store' |
| import { fromString } from '../../../auth/providers/credentials' |
| import { Credentials } from '@aws-sdk/types' |
| import * as CloudFormation from '../../cloudformation/cloudformation' |
| import { getSamCliContext, getSamCliVersion } from '../cli/samCliContext' |
| import { minSamCliVersionForImageSupport, minSamCliVersionForGoSupport } from '../cli/samCliValidator' |
| import { getIdeProperties } from '../../extensionUtilities' |
| import { resolve } from 'path' |
| import globals from '../../extensionGlobals' |
| import { telemetry, Runtime as TelemetryRuntime } from '../../telemetry/telemetry' |
| import { Runtime } from '@aws-sdk/client-lambda' |
| import { ErrorInformation, isUserCancelledError, ToolkitError } from '../../errors' |
| import { openLaunchJsonFile } from './commands/addSamDebugConfiguration' |
| import { Logging } from '../../logger/commands' |
| import { credentialHelpUrl, samTroubleshootingUrl } from '../../constants' |
| import { Auth } from '../../../auth/auth' |
| import { openUrl } from '../../utilities/vsCodeUtils' |
| import fs from '../../fs/fs' |
|
|
| const localize = nls.loadMessageBundle() |
|
|
| interface NotificationButton<T = unknown> { |
| readonly label: string |
| readonly onClick: () => Promise<T> | T |
| } |
|
|
| class SamLaunchRequestError extends ToolkitError.named('SamLaunchRequestError') { |
| private readonly buttons: NotificationButton[] |
|
|
| public constructor(message: string, info?: ErrorInformation & { readonly extraButtons?: NotificationButton[] }) { |
| super(message, info) |
| this.buttons = info?.extraButtons ?? [ |
| { |
| label: localize('AWS.generic.message.troubleshooting', 'Troubleshooting'), |
| onClick: () => openUrl(samTroubleshootingUrl), |
| }, |
| { |
| label: localize('AWS.generic.message.openConfig', 'Open Launch Config'), |
| onClick: openLaunchJsonFile, |
| }, |
| ] |
| } |
|
|
| public async showNotification(): Promise<void> { |
| if (isUserCancelledError(this)) { |
| getLogger().verbose(`SAM run/debug: user cancelled`) |
| return |
| } |
|
|
| const logId = getLogger().error(this.trace) |
|
|
| const viewLogsButton = { |
| label: localize('AWS.generic.message.viewLogs', 'View Logs...'), |
| onClick: () => Logging.instance.viewLogsAtMessage.execute(logId), |
| } |
|
|
| const buttonsWithLogs = [viewLogsButton, ...this.buttons] |
|
|
| await vscode.window.showErrorMessage(this.message, ...buttonsWithLogs.map((b) => b.label)).then((resp) => { |
| return buttonsWithLogs.find(({ label }) => label === resp)?.onClick() |
| }) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export interface SamLaunchRequestArgs extends AwsSamDebuggerConfiguration { |
| |
| readonly request: 'attach' | 'launch' | 'direct-invoke' |
|
|
| |
| runtime: Runtime |
| runtimeFamily: RuntimeFamily |
| |
| handlerName: string |
| workspaceFolder: vscode.WorkspaceFolder |
|
|
| |
| |
| |
| |
| |
| |
| codeRoot: string |
|
|
| |
| baseBuildDir?: string |
|
|
| |
| mountWith?: 'read' | 'write' |
|
|
| |
| |
| |
| |
| documentUri: vscode.Uri |
|
|
| |
| |
| |
| |
| |
| |
| templatePath: string |
|
|
| |
| |
| |
| |
| |
| eventPayloadFile?: string |
|
|
| |
| |
| |
| |
| |
| |
| envFile?: string |
|
|
| |
| |
| |
| |
| noDebug?: boolean |
| |
| debuggerPath?: string |
| debugArgs?: string[] |
| |
| containerEnvVars?: { [k: string]: string } |
| |
| |
| |
| |
| containerEnvFile?: string |
| debugPort?: number |
| |
| apiPort?: number |
|
|
| |
| |
| |
| awsCredentials?: Credentials |
|
|
| |
| |
| |
| parameterOverrides?: string[] |
|
|
| |
| |
| |
| |
| samLocalInvokeCommand?: SamLocalInvokeCommand |
| onWillAttachDebugger?(debugPort: number, timeout: Timeout): Promise<void> |
|
|
| |
| |
| |
| architecture?: Architecture |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider { |
| public constructor(readonly ctx: ExtContext) {} |
|
|
| |
| |
| |
| |
| public async provideDebugConfigurations( |
| folder: vscode.WorkspaceFolder | undefined, |
| token?: vscode.CancellationToken |
| ): Promise<AwsSamDebuggerConfiguration[] | undefined> { |
| if (token?.isCancellationRequested) { |
| return undefined |
| } |
|
|
| const configs: AwsSamDebuggerConfiguration[] = [] |
| if (folder) { |
| const folderPath = folder.uri.fsPath |
| const templates = (await globals.templateRegistry).items |
|
|
| for (const templateDatum of templates) { |
| if (isInDirectory(folderPath, templateDatum.path)) { |
| if (!templateDatum.item.Resources) { |
| getLogger().error(`provideDebugConfigurations: invalid template: ${templateDatum.path}`) |
| continue |
| } |
| for (const resourceKey of Object.keys(templateDatum.item.Resources)) { |
| const resource = templateDatum.item.Resources[resourceKey] |
| if (resource) { |
| |
| const runtimeName = CloudFormation.isZipLambdaResource(resource?.Properties) |
| ? (CloudFormation.getStringForProperty( |
| resource?.Properties, |
| 'Runtime', |
| templateDatum.item |
| ) ?? '') |
| : '' |
| configs.push( |
| createTemplateAwsSamDebugConfig( |
| folder, |
| runtimeName, |
| false, |
| resourceKey, |
| templateDatum.path |
| ) |
| ) |
| const events = resource?.Properties?.Events |
| if (events) { |
| |
| for (const key in events) { |
| const value = events[key] |
| if (value.Type === 'Api') { |
| const properties = value.Properties as CloudFormation.ApiEventProperties |
| configs.push( |
| createApiAwsSamDebugConfig( |
| folder, |
| runtimeName, |
| resourceKey, |
| templateDatum.path, |
| { |
| path: properties?.Path, |
| httpMethod: properties?.Method, |
| } |
| ) |
| ) |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| getLogger().verbose(`provideDebugConfigurations: debugconfigs: %O`, configs) |
| } |
|
|
| return configs |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| public async resolveDebugConfiguration( |
| folder: vscode.WorkspaceFolder | undefined, |
| config: AwsSamDebuggerConfiguration, |
| token?: vscode.CancellationToken, |
| source?: string |
| ): Promise<AwsSamDebuggerConfiguration | undefined> { |
| return config |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public async resolveDebugConfigurationWithSubstitutedVariables( |
| folder: vscode.WorkspaceFolder | undefined, |
| config: AwsSamDebuggerConfiguration, |
| token?: vscode.CancellationToken, |
| source?: string |
| ): Promise<undefined> { |
| await this.makeAndInvokeConfig(folder, config, token, source) |
| |
| return undefined |
| } |
|
|
| private async makeAndInvokeConfig( |
| folder: vscode.WorkspaceFolder | undefined, |
| config: AwsSamDebuggerConfiguration, |
| token?: vscode.CancellationToken, |
| source?: string |
| ): Promise<void> { |
| try { |
| if (config.invokeTarget.target === 'api') { |
| await telemetry.apigateway_invokeLocal.run(async (span) => { |
| const resolved = await this.makeConfig(folder, config, token) |
| span.record({ httpMethod: resolved.api?.httpMethod }) |
|
|
| return this.invokeConfig(resolved) |
| }) |
| } else { |
| await telemetry.lambda_invokeLocal.run(async () => { |
| telemetry.record({ source: source }) |
| const resolved = await this.makeConfig(folder, config, token) |
|
|
| return this.invokeConfig(resolved) |
| }) |
| } |
| } catch (err) { |
| if (err instanceof SamLaunchRequestError) { |
| void err.showNotification() |
| } else if (err instanceof ToolkitError) { |
| void new SamLaunchRequestError(err.message, { ...err }).showNotification() |
| } else { |
| void SamLaunchRequestError.chain(err, 'Failed to run launch configuration').showNotification() |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| public async makeConfig( |
| folder: vscode.WorkspaceFolder | undefined, |
| config: AwsSamDebuggerConfiguration, |
| token?: vscode.CancellationToken |
| ): Promise<SamLaunchRequestArgs> { |
| if (token?.isCancellationRequested) { |
| throw new ToolkitError('Cancellation requested', { cancelled: true }) |
| } |
|
|
| folder = |
| folder ?? (vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders[0] : undefined) |
| if (!folder) { |
| const message = localize( |
| 'AWS.sam.debugger.noWorkspace', |
| 'Choose a workspace, then try again', |
| getIdeProperties().company |
| ) |
|
|
| throw new SamLaunchRequestError(message, { code: 'NoWorkspaceFolder', extraButtons: [] }) |
| } |
|
|
| |
| |
| const hasLaunchJson = !!config.request |
| const configValidator: AwsSamDebugConfigurationValidator = new DefaultAwsSamDebugConfigurationValidator(folder) |
|
|
| if (!hasLaunchJson) { |
| const message = localize( |
| 'AWS.sam.debugger.noLaunchJson', |
| 'To debug a Lambda locally, create a launch.json from the Run panel, then select a configuration.' |
| ) |
|
|
| throw new SamLaunchRequestError(message, { |
| code: 'NoLaunchConfig', |
| extraButtons: [ |
| { |
| label: localize('AWS.gotoRunPanel', 'Run panel'), |
| onClick: () => vscode.commands.executeCommand('workbench.view.debug'), |
| }, |
| ], |
| }) |
| } else { |
| const registry = await globals.templateRegistry |
| const rv = await configValidator.validate(config, registry) |
| if (!rv.isValid) { |
| throw new ToolkitError(`Invalid launch configuration: ${rv.message}`, { code: 'BadLaunchConfig' }) |
| } else if (rv.message) { |
| void vscode.window.showInformationMessage(rv.message) |
| } |
| getLogger().verbose(`SAM debug: config %s:`, config.name) |
| } |
|
|
| const editor = vscode.window.activeTextEditor |
| const templateInvoke = config.invokeTarget as TemplateTargetProperties |
| const template = await getTemplate(folder, config) |
| const templateResource = await getTemplateResource(folder, config) |
| const codeRoot = await getCodeRoot(folder, config) |
| const architecture = getArchitecture(template, templateResource, config.invokeTarget) |
| |
| |
| |
| const handlerName = await getHandlerName(folder, config) |
|
|
| config.baseBuildDir = resolve(folder.uri.fsPath, config.sam?.buildDir ?? (await makeTemporaryToolkitFolder())) |
| await fs.mkdir(config.baseBuildDir) |
|
|
| if (templateInvoke?.templatePath) { |
| |
| |
| templateInvoke.templatePath = pathutil.normalize(tryGetAbsolutePath(folder, templateInvoke.templatePath)) |
| } else if (config.invokeTarget.target === 'code') { |
| const codeConfig = config as SamLaunchRequestArgs & { invokeTarget: { target: 'code' } } |
| |
| |
| codeConfig.invokeTarget.projectRoot = pathutil.normalize( |
| resolve(folder.uri.fsPath, config.invokeTarget.projectRoot) |
| ) |
| templateInvoke.templatePath = getInputTemplatePath(codeConfig) |
| } |
|
|
| const isZip = CloudFormation.isZipLambdaResource(templateResource?.Properties) |
| const runtime: Runtime | undefined = |
| (config.lambda?.runtime as Runtime) ?? |
| (template && isZip |
| ? CloudFormation.getStringForProperty(templateResource?.Properties, 'Runtime', template) |
| : undefined) ?? |
| getDefaultRuntime(getRuntimeFamily(editor?.document?.languageId ?? 'unknown')) |
|
|
| const lambdaMemory = |
| (template |
| ? CloudFormation.getNumberForProperty(templateResource?.Properties, 'MemorySize', template) |
| : undefined) ?? config.lambda?.memoryMb |
| const lambdaTimeout = |
| (template |
| ? CloudFormation.getNumberForProperty(templateResource?.Properties, 'Timeout', template) |
| : undefined) ?? config.lambda?.timeoutSec |
|
|
| |
| if (!isZip) { |
| const samCliVersion = await getSamCliVersion(this.ctx.samCliContext()) |
| if (semver.lt(samCliVersion, minSamCliVersionForImageSupport)) { |
| const message = localize( |
| 'AWS.output.sam.no.image.support', |
| 'Support for Image-based Lambdas requires a minimum SAM CLI version of 1.13.0.' |
| ) |
|
|
| throw new SamLaunchRequestError(message, { code: 'UnsupportedSamVersion', details: { samCliVersion } }) |
| } |
| } |
|
|
| if (!runtime) { |
| const message = localize( |
| 'AWS.sam.debugger.failedLaunch.missingRuntime', |
| 'Toolkit could not infer a runtime for config: {0}. Add a "lambda.runtime" field to your launch configuration.', |
| config.name |
| ) |
|
|
| throw new SamLaunchRequestError(message, { code: 'MissingRuntime' }) |
| } |
|
|
| |
| |
| if (goRuntimes.includes(runtime) && !config.noDebug) { |
| const samCliVersion = await getSamCliVersion(this.ctx.samCliContext()) |
| if (semver.lt(samCliVersion, minSamCliVersionForGoSupport)) { |
| void vscode.window.showWarningMessage( |
| localize( |
| 'AWS.output.sam.local.no.go.support', |
| 'Debugging go1.x lambdas requires a minimum SAM CLI version of {0}. Function will run locally without debug.', |
| minSamCliVersionForGoSupport |
| ) |
| ) |
| config.noDebug = true |
| } |
| } |
|
|
| const runtimeFamily = getFamily(runtime) |
| |
| const region = config.aws?.region ?? this.ctx.awsContext.getCredentialDefaultRegion() |
| const documentUri = |
| vscode.window.activeTextEditor?.document.uri ?? |
| |
| vscode.Uri.parse(templateInvoke.templatePath!) |
|
|
| let awsCredentials = await this.ctx.awsContext.getCredentials() |
| if (!awsCredentials && !config.aws?.credentials) { |
| getLogger().warn('SAM debug: missing AWS credentials (Toolkit is not connected)') |
| } else if (config.aws?.credentials) { |
| |
| |
| let fromStore: Credentials | undefined |
| try { |
| const credentialsId = fromString(config.aws.credentials) |
| fromStore = await getCredentialsFromStore(credentialsId, this.ctx.credentialsStore) |
| } catch { |
| getLogger().error(`SAM debug: fromString('${config.aws.credentials}') failed`) |
| } |
| if (fromStore) { |
| awsCredentials = fromStore |
| } else { |
| const credentialsId = config.aws.credentials |
| const getHelp = localize('AWS.generic.message.getHelp', 'Get Help...') |
|
|
| throw new SamLaunchRequestError(`Invalid credentials found in launch configuration: ${credentialsId}`, { |
| code: 'InvalidCredentials', |
| extraButtons: [ |
| { |
| label: getHelp, |
| onClick: () => openUrl(vscode.Uri.parse(credentialHelpUrl)), |
| }, |
| ], |
| }) |
| } |
| } |
|
|
| if (config.api) { |
| config.api.headers = { |
| 'content-type': 'application/json', |
| ...(config.api.headers ? config.api.headers : {}), |
| } |
| } |
|
|
| let parameterOverrideArr: string[] | undefined |
| const params = config.sam?.template?.parameters |
| if (params) { |
| parameterOverrideArr = [] |
| for (const key of Object.keys(params)) { |
| parameterOverrideArr.push(`${key}=${params[key].toString()}`) |
| } |
| } |
|
|
| |
| |
| const apiPort = config.invokeTarget.target === 'api' ? await getStartPort() : undefined |
| const debugPort = config.noDebug ? undefined : await getStartPort(apiPort ? apiPort + 1 : undefined) |
| let launchConfig: SamLaunchRequestArgs = { |
| ...config, |
| request: 'attach', |
| codeRoot: codeRoot ?? '', |
| workspaceFolder: folder, |
| runtime: runtime as Runtime, |
| runtimeFamily: runtimeFamily, |
| handlerName: handlerName, |
| documentUri: documentUri, |
| templatePath: pathutil.normalize(templateInvoke?.templatePath), |
| eventPayloadFile: '', |
| envFile: '', |
| apiPort: apiPort, |
| debugPort: debugPort, |
| invokeTarget: { |
| ...config.invokeTarget, |
| }, |
| lambda: { |
| ...config.lambda, |
| memoryMb: lambdaMemory, |
| timeoutSec: lambdaTimeout, |
| environmentVariables: { ...config.lambda?.environmentVariables }, |
| }, |
| region: region, |
| awsCredentials: awsCredentials, |
| parameterOverrides: parameterOverrideArr, |
| architecture: architecture, |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| switch (launchConfig.runtimeFamily) { |
| case RuntimeFamily.NodeJS: { |
| |
| launchConfig = await tsDebug.makeTypescriptConfig(launchConfig) |
| break |
| } |
| case RuntimeFamily.Python: { |
| |
| launchConfig = await pythonDebug.makePythonDebugConfig(launchConfig) |
| break |
| } |
| case RuntimeFamily.DotNet: { |
| |
| launchConfig = await csharpDebug.makeCsharpConfig(launchConfig) |
| break |
| } |
| case RuntimeFamily.Go: { |
| launchConfig = await goDebug.makeGoConfig(launchConfig) |
| break |
| } |
| case RuntimeFamily.Java: { |
| |
| launchConfig = await javaDebug.makeJavaConfig(launchConfig) |
| break |
| } |
| default: { |
| const message = localize( |
| 'AWS.sam.debugger.invalidRuntime', |
| 'Unknown or unsupported runtime: {0}', |
| runtime |
| ) |
|
|
| throw new ToolkitError(message, { code: 'UnsupportedRuntime' }) |
| } |
| } |
|
|
| |
| if (launchConfig.invokeTarget.target === 'code') { |
| const codeConfig = launchConfig as SamLaunchRequestArgs & { invokeTarget: { target: 'code' } } |
| await makeInputTemplate(codeConfig) |
| } |
|
|
| await makeJsonFiles(launchConfig) |
|
|
| |
| |
| |
| launchConfig.type = AWS_SAM_DEBUG_TYPE |
|
|
| if (launchConfig.request !== 'attach' && launchConfig.request !== 'launch') { |
| |
| |
| |
| throw Error( |
| `resolveDebugConfiguration: launchConfig was not correctly resolved before return: ${JSON.stringify( |
| launchConfig |
| )}` |
| ) |
| } |
|
|
| return launchConfig |
| } |
|
|
| |
| |
| |
| public async invokeConfig(config: SamLaunchRequestArgs): Promise<SamLaunchRequestArgs> { |
| telemetry.record({ |
| debug: !config.noDebug, |
| runtime: config.runtime as TelemetryRuntime, |
| lambdaArchitecture: config.architecture, |
| lambdaPackageType: (await isImageLambdaConfig(config)) ? 'Image' : 'Zip', |
| version: await getSamCliVersion(getSamCliContext()), |
| }) |
|
|
| await Auth.instance.tryAutoConnect() |
| switch (config.runtimeFamily) { |
| case RuntimeFamily.NodeJS: { |
| config.type = 'node' |
| const c = await tsDebug.invokeTypescriptLambda(this.ctx, config as NodejsDebugConfiguration) |
| return c |
| } |
| case RuntimeFamily.Python: { |
| config.type = 'python' |
| return await pythonDebug.invokePythonLambda(this.ctx, config as PythonDebugConfiguration) |
| } |
| case RuntimeFamily.DotNet: { |
| config.type = 'coreclr' |
| return await csharpDebug.invokeCsharpLambda(this.ctx, config) |
| } |
| case RuntimeFamily.Go: { |
| config.type = 'go' |
| return await goDebug.invokeGoLambda(this.ctx, config as GoDebugConfiguration) |
| } |
| case RuntimeFamily.Java: { |
| config.type = 'java' |
| return await javaDebug.invokeJavaLambda(this.ctx, config) |
| } |
| default: { |
| throw new Error(`unknown runtimeFamily: ${config.runtimeFamily}`) |
| } |
| } |
| } |
| } |
|
|