Skip to content

Commit 1628575

Browse files
authored
aws field in debug config is passed to SAM as environment vars (#1145)
The `aws` field in our `direct-invoke` launch configs now pass AWS credentials and regions to the SAM debug session. This includes passing active session tokens. These credentials and region info will be available to the debugged app, allowing the use of AWS SDK calls.
1 parent e614e97 commit 1628575

14 files changed

+305
-75
lines changed

package.nls.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
"AWS.configuration.description.awssam.debug.runtime": "The Lambda Function's runtime",
6969
"AWS.configuration.description.awssam.debug.timeout": "The amount of time (in seconds) that Lambda allows a function to run before stopping it.",
7070
"AWS.configuration.description.awssam.debug.aws": "AWS connection details",
71-
"AWS.configuration.description.awssam.debug.credentials": "The AWS credential id to use during the invocation.",
72-
"AWS.configuration.description.awssam.debug.region": "The AWS region to use during the invocation.",
71+
"AWS.configuration.description.awssam.debug.credentials": "The AWS credential type and ID to use during the invocation. Example: credential profile \"default\" would be entered as `profile:default`.",
72+
"AWS.configuration.description.awssam.debug.region": "AWS region to use during the invocation.",
7373
"AWS.configuration.description.awssam.debug.event": "The payload to pass to the lambda invocation.\n Must specify one of 'json' or 'path'.",
7474
"AWS.configuration.description.awssam.debug.event.json": "A JSON document to use as the event payload",
7575
"AWS.configuration.description.awssam.debug.event.path": "The path to a file to use as the event payload",

src/credentials/credentialsStore.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
*/
55

66
import * as AWS from 'aws-sdk'
7+
import { getLogger } from '../shared/logger/logger'
78
import { CredentialsProvider } from './providers/credentialsProvider'
89
import { asString, CredentialsProviderId } from './providers/credentialsProviderId'
10+
import { CredentialsProviderManager } from './providers/credentialsProviderManager'
911

1012
export interface CachedCredentials {
1113
credentials: AWS.Credentials
@@ -31,20 +33,21 @@ export class CredentialsStore {
3133

3234
/**
3335
* If credentials are not stored, the credentialsProvider is used to produce credentials (which are then stored).
36+
* If the credentials exist but are outdated, the credentials will be invalidated and updated.
37+
* Either way, the credentials tied to the credentialsProviderId will then be returned.
3438
*/
35-
public async getOrCreateCredentials(
39+
public async upsertCredentials(
3640
credentialsProviderId: CredentialsProviderId,
3741
credentialsProvider: CredentialsProvider
3842
): Promise<CachedCredentials> {
3943
let credentials = await this.getCredentials(credentialsProviderId)
4044

4145
if (!credentials) {
42-
credentials = {
43-
credentials: await credentialsProvider.getCredentials(),
44-
credentialsHashCode: credentialsProvider.getHashCode(),
45-
}
46-
47-
this.credentialsCache[asString(credentialsProviderId)] = credentials
46+
credentials = await this.setCredentials(credentialsProviderId, credentialsProvider)
47+
} else if (credentialsProvider.getHashCode() !== credentials.credentialsHashCode) {
48+
getLogger().verbose(`Using updated credentials: ${asString(credentialsProviderId)}`)
49+
this.invalidateCredentials(credentialsProviderId)
50+
credentials = await this.setCredentials(credentialsProviderId, credentialsProvider)
4851
}
4952

5053
return credentials
@@ -57,4 +60,32 @@ export class CredentialsStore {
5760
// tslint:disable-next-line:no-dynamic-delete
5861
delete this.credentialsCache[asString(credentialsProviderId)]
5962
}
63+
64+
private async setCredentials(
65+
credentialsProviderId: CredentialsProviderId,
66+
credentialsProvider: CredentialsProvider
67+
): Promise<CachedCredentials> {
68+
const credentials = {
69+
credentials: await credentialsProvider.getCredentials(),
70+
credentialsHashCode: credentialsProvider.getHashCode(),
71+
}
72+
73+
this.credentialsCache[asString(credentialsProviderId)] = credentials
74+
75+
return credentials
76+
}
77+
}
78+
79+
export async function getCredentialsFromStore(
80+
credentialsProviderId: CredentialsProviderId,
81+
credentialsStore: CredentialsStore
82+
): Promise<AWS.Credentials> {
83+
const provider = await CredentialsProviderManager.getInstance().getCredentialsProvider(credentialsProviderId)
84+
if (!provider) {
85+
credentialsStore.invalidateCredentials(credentialsProviderId)
86+
throw new Error(`Could not find Credentials Provider for ${asString(credentialsProviderId)}`)
87+
}
88+
89+
const cachedCredentials = await credentialsStore.upsertCredentials(credentialsProviderId, provider)
90+
return cachedCredentials.credentials
6091
}

src/credentials/credentialsUtilities.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import * as nls from 'vscode-nls'
7+
const localize = nls.loadMessageBundle()
8+
9+
import * as vscode from 'vscode'
610
import { Credentials } from 'aws-sdk'
11+
import { credentialHelpUrl } from '../shared/constants'
12+
import { CredentialsProviderId, asString } from './providers/credentialsProviderId'
713

814
export function asEnvironmentVariables(credentials: Credentials): NodeJS.ProcessEnv {
915
const environmentVariables: NodeJS.ProcessEnv = {}
@@ -17,3 +23,26 @@ export function asEnvironmentVariables(credentials: Credentials): NodeJS.Process
1723

1824
return environmentVariables
1925
}
26+
27+
export function notifyUserInvalidCredentials(credentialProviderId: CredentialsProviderId): void {
28+
const getHelp = localize('AWS.message.credentials.invalid.help', 'Get Help...')
29+
const viewLogs = localize('AWS.message.credentials.invalid.logs', 'View Logs...')
30+
31+
vscode.window
32+
.showErrorMessage(
33+
localize(
34+
'AWS.message.credentials.invalid',
35+
'Invalid Credentials {0}, see logs for more information.',
36+
asString(credentialProviderId)
37+
),
38+
getHelp,
39+
viewLogs
40+
)
41+
.then((selection: string | undefined) => {
42+
if (selection === getHelp) {
43+
vscode.env.openExternal(vscode.Uri.parse(credentialHelpUrl))
44+
} else if (selection === viewLogs) {
45+
vscode.commands.executeCommand('aws.viewLogs')
46+
}
47+
})
48+
}

src/credentials/loginManager.ts

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,19 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import * as nls from 'vscode-nls'
7-
const localize = nls.loadMessageBundle()
8-
9-
import * as vscode from 'vscode'
106
import { AwsContext } from '../shared/awsContext'
11-
import { credentialHelpUrl } from '../shared/constants'
127
import { getAccountId } from '../shared/credentials/accountId'
138
import { getLogger } from '../shared/logger'
149
import { recordAwsSetCredentials, Result } from '../shared/telemetry/telemetry'
1510
import { CredentialsStore } from './credentialsStore'
16-
import { CredentialsProvider } from './providers/credentialsProvider'
11+
import { notifyUserInvalidCredentials } from './credentialsUtilities'
1712
import { asString, CredentialsProviderId } from './providers/credentialsProviderId'
1813
import { CredentialsProviderManager } from './providers/credentialsProviderManager'
1914

2015
export class LoginManager {
2116
private readonly defaultCredentialsRegion = 'us-east-1'
22-
private readonly credentialsStore: CredentialsStore = new CredentialsStore()
2317

24-
public constructor(private readonly awsContext: AwsContext) {}
18+
public constructor(private readonly awsContext: AwsContext, private readonly store: CredentialsStore) {}
2519

2620
/**
2721
* Establishes a Credentials for the Toolkit to use. Essentially the Toolkit becomes "logged in".
@@ -37,9 +31,8 @@ export class LoginManager {
3731
throw new Error(`Could not find Credentials Provider for ${asString(credentialsProviderId)}`)
3832
}
3933

40-
await this.updateCredentialsStore(credentialsProviderId, provider)
34+
const storedCredentials = await this.store.upsertCredentials(credentialsProviderId, provider)
4135

42-
const storedCredentials = await this.credentialsStore.getCredentials(credentialsProviderId)
4336
if (!storedCredentials) {
4437
throw new Error(`No credentials found for id ${asString(credentialsProviderId)}`)
4538
}
@@ -64,11 +57,11 @@ export class LoginManager {
6457
)}. Toolkit will now disconnect from AWS. %O`,
6558
err as Error
6659
)
67-
this.credentialsStore.invalidateCredentials(credentialsProviderId)
60+
this.store.invalidateCredentials(credentialsProviderId)
6861

6962
await this.logout()
7063

71-
this.notifyUserInvalidCredentials(credentialsProviderId)
64+
notifyUserInvalidCredentials(credentialsProviderId)
7265
} finally {
7366
recordAwsSetCredentials({ result: loginResult })
7467
}
@@ -80,45 +73,4 @@ export class LoginManager {
8073
public async logout(): Promise<void> {
8174
await this.awsContext.setCredentials(undefined)
8275
}
83-
84-
/**
85-
* Updates the CredentialsStore if the credentials are considered different
86-
*/
87-
private async updateCredentialsStore(
88-
credentialsProviderId: CredentialsProviderId,
89-
provider: CredentialsProvider
90-
): Promise<void> {
91-
const storedCredentials = await this.credentialsStore.getCredentials(credentialsProviderId)
92-
if (provider.getHashCode() !== storedCredentials?.credentialsHashCode) {
93-
getLogger().verbose(
94-
`Credentials for ${asString(credentialsProviderId)} have changed, using updated credentials.`
95-
)
96-
this.credentialsStore.invalidateCredentials(credentialsProviderId)
97-
}
98-
99-
await this.credentialsStore.getOrCreateCredentials(credentialsProviderId, provider)
100-
}
101-
102-
private notifyUserInvalidCredentials(credentialProviderId: CredentialsProviderId) {
103-
const getHelp = localize('AWS.message.credentials.invalid.help', 'Get Help...')
104-
const viewLogs = localize('AWS.message.credentials.invalid.logs', 'View Logs...')
105-
106-
vscode.window
107-
.showErrorMessage(
108-
localize(
109-
'AWS.message.credentials.invalid',
110-
'Invalid Credentials {0}, see logs for more information.',
111-
asString(credentialProviderId)
112-
),
113-
getHelp,
114-
viewLogs
115-
)
116-
.then((selection: string | undefined) => {
117-
if (selection === getHelp) {
118-
vscode.env.openExternal(vscode.Uri.parse(credentialHelpUrl))
119-
} else if (selection === viewLogs) {
120-
vscode.commands.executeCommand('aws.viewLogs')
121-
}
122-
})
123-
}
12476
}

src/extension.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { ExtensionDisposableFiles } from './shared/utilities/disposableFiles'
5555
import { getChannelLogger } from './shared/utilities/vsCodeUtils'
5656
import { ExtContext } from './shared/extensions'
5757
import { activate as activateStepFunctions } from './stepFunctions/activation'
58+
import { CredentialsStore } from './credentials/credentialsStore'
5859

5960
let localize: nls.LocalizeFunc
6061

@@ -78,7 +79,8 @@ export async function activate(context: vscode.ExtensionContext) {
7879
const awsContext = new DefaultAwsContext(context)
7980
const awsContextTrees = new AwsContextTreeCollection()
8081
const regionProvider = new DefaultRegionProvider(endpointsProvider)
81-
const loginManager = new LoginManager(awsContext)
82+
const credentialsStore = new CredentialsStore()
83+
const loginManager = new LoginManager(awsContext, credentialsStore)
8284

8385
const toolkitEnvDetails = getToolkitEnvironmentDetails()
8486
getLogger().info(toolkitEnvDetails)
@@ -114,6 +116,7 @@ export async function activate(context: vscode.ExtensionContext) {
114116
outputChannel: toolkitOutputChannel,
115117
telemetryService: ext.telemetry,
116118
chanLogger: getChannelLogger(toolkitOutputChannel),
119+
credentialsStore,
117120
}
118121

119122
context.subscriptions.push(

src/shared/extensions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { RegionProvider } from './regions/regionProvider'
99
import { SettingsConfiguration } from './settingsConfiguration'
1010
import { TelemetryService } from './telemetry/telemetryService'
1111
import { ChannelLogger } from './utilities/vsCodeUtils'
12+
import { CredentialsStore } from '../credentials/credentialsStore'
1213

1314
export const VSCODE_EXTENSION_ID = {
1415
awstoolkit: 'amazonwebservices.aws-toolkit-vscode',
@@ -27,4 +28,5 @@ export interface ExtContext {
2728
outputChannel: vscode.OutputChannel
2829
telemetryService: TelemetryService
2930
chanLogger: ChannelLogger
31+
credentialsStore: CredentialsStore
3032
}

src/shared/sam/cli/samCliLocalInvoke.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class DefaultSamLocalInvokeCommand implements SamLocalInvokeCommand {
4747
]
4848
) {}
4949

50-
public async invoke({ options = {}, ...params }: SamLocalInvokeCommandArgs): Promise<void> {
50+
public async invoke({ options, ...params }: SamLocalInvokeCommandArgs): Promise<void> {
5151
this.channelLogger.info(
5252
'AWS.running.command',
5353
'Running command: {0}',
@@ -155,6 +155,10 @@ export interface SamCliLocalInvokeInvocationArguments {
155155
* Location of the file containing the environment variables to invoke the Lambda Function against.
156156
*/
157157
environmentVariablePath: string
158+
/**
159+
* Environment variables set when invoking the SAM process (NOT passed to the Lambda).
160+
*/
161+
environmentVariables?: NodeJS.ProcessEnv
158162
/**
159163
* When specified, starts the Lambda function container in debug mode and exposes this port on the local host.
160164
*/
@@ -212,6 +216,9 @@ export class SamCliLocalInvokeInvocation {
212216
invokeArgs.push(...(this.args.extraArgs ?? []))
213217

214218
await this.args.invoker.invoke({
219+
options: {
220+
env: this.args.environmentVariables,
221+
},
215222
command: 'sam',
216223
args: invokeArgs,
217224
isDebug: !!this.args.debugPort,

src/shared/sam/debugger/awsSamDebugger.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ import {
3939
} from './awsSamDebugConfigurationValidator'
4040
import { makeConfig } from '../localLambdaRunner'
4141
import { SamLocalInvokeCommand } from '../cli/samCliLocalInvoke'
42+
import { getCredentialsFromStore } from '../../../credentials/credentialsStore'
43+
import { fromString } from '../../../credentials/providers/credentialsProviderId'
44+
import { notifyUserInvalidCredentials } from '../../../credentials/credentialsUtilities'
45+
import { Credentials } from 'aws-sdk/lib/credentials'
4246

4347
const localize = nls.loadMessageBundle()
4448

@@ -111,6 +115,11 @@ export interface SamLaunchRequestArgs extends AwsSamDebuggerConfiguration {
111115
debuggerPath?: string
112116
debugPort?: number
113117

118+
/**
119+
* Credentials to add as env vars if available
120+
*/
121+
awsCredentials?: Credentials
122+
114123
//
115124
// Invocation properties (for "execute" phase, after "config" phase).
116125
// Non-serializable...
@@ -311,6 +320,19 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
311320
// XXX: don't know what URI to choose...
312321
vscode.Uri.parse(templateInvoke.samTemplatePath!!)
313322

323+
let awsCredentials: Credentials | undefined
324+
325+
if (config.aws?.credentials) {
326+
const credentialsProviderId = fromString(config.aws.credentials)
327+
try {
328+
awsCredentials = await getCredentialsFromStore(credentialsProviderId, this.ctx.credentialsStore)
329+
} catch (err) {
330+
getLogger().error(err as Error)
331+
notifyUserInvalidCredentials(credentialsProviderId)
332+
return undefined
333+
}
334+
}
335+
314336
let launchConfig: SamLaunchRequestArgs = {
315337
...config,
316338
request: 'attach',
@@ -330,6 +352,7 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
330352
timeoutSec: lambdaTimout,
331353
environmentVariables: { ...config.lambda?.environmentVariables },
332354
},
355+
awsCredentials: awsCredentials,
333356
}
334357

335358
//

src/shared/sam/debugger/typescriptSamDebug.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function makeTypescriptConfig(config: SamLaunchRequestArgs): Promis
6262
// Always generate a temporary template.yaml, don't use workspace one directly.
6363
config.samTemplatePath = await makeInputTemplate(config)
6464

65-
// Make a python launch-config from the generic config.
65+
// Make a node launch-config from the generic config.
6666
const nodejsLaunchConfig: NodejsDebugConfiguration = {
6767
...config, // Compose.
6868
type: 'node',

src/shared/sam/localLambdaRunner.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { DefaultValidatingSamCliProcessInvoker } from './cli/defaultValidatingSa
2626
import { SamCliBuildInvocation, SamCliBuildInvocationArguments } from './cli/samCliBuild'
2727
import { SamCliLocalInvokeInvocation, SamCliLocalInvokeInvocationArguments } from './cli/samCliLocalInvoke'
2828
import { SamLaunchRequestArgs } from './debugger/awsSamDebugger'
29+
import { asEnvironmentVariables } from '../../credentials/credentialsUtilities'
2930

3031
export interface LambdaLocalInvokeParams {
3132
/** URI of the current editor document. */
@@ -136,13 +137,17 @@ export async function invokeLambdaFunction(
136137

137138
ctx.chanLogger.info('AWS.output.building.sam.application', 'Building SAM Application...')
138139
const samBuildOutputFolder = path.join(config.baseBuildDir!, 'output')
140+
const envVars = {
141+
...(config.awsCredentials ? asEnvironmentVariables(config.awsCredentials) : {}),
142+
...(config.aws?.region ? { AWS_DEFAULT_REGION: config.aws.region } : {}),
143+
}
139144
const samCliArgs: SamCliBuildInvocationArguments = {
140145
buildDir: samBuildOutputFolder,
141146
baseDir: config.codeRoot,
142147
templatePath: config.samTemplatePath!,
143148
invoker: processInvoker,
144149
manifestPath: config.manifestPath,
145-
environmentVariables: {},
150+
environmentVariables: envVars,
146151
useContainer: config.sam?.containerBuild || false,
147152
extraArgs: config.sam?.buildArguments,
148153
}
@@ -174,6 +179,7 @@ export async function invokeLambdaFunction(
174179
templatePath: config.samTemplatePath,
175180
eventPath: config.eventPayloadFile,
176181
environmentVariablePath: config.envFile,
182+
environmentVariables: envVars,
177183
invoker: config.samLocalInvokeCommand!,
178184
dockerNetwork: config.sam?.dockerNetwork,
179185
debugPort: !config.noDebug ? config.debugPort?.toString() : undefined,

0 commit comments

Comments
 (0)