Skip to content

Commit dfbf2ab

Browse files
feat(lambda): Copy Url #3036
* chore(#2572): Initial copy lambda URL boilerplate This commit creates the boilerplate code which sets up the vscode command to copy the lambda function url. Signed-off-by: Nikolas Komonen <[email protected]> * chore: Logic to get lambda func url This commit implements the API logic to retrieve the Lambda Function URL. Signed-off-by: Nikolas Komonen <[email protected]> * feat(#2572): Copy Lambda Func URL in context menu This is the final commit that enables the user to right click a Lambda function and copy its URL if it exists. Additionally, this adds in a new test suite for the package.json command setup. This ensures all the aspects of the command registration are configured. Signed-off-by: Nikolas Komonen <[email protected]> * Add changelog Signed-off-by: Nikolas Komonen <[email protected]> * simplify object argument properties This commit changes the argument property to specify only the information it needs using 'Pick'. This helps to simplify the unit tests which this commit also changes. Signed-off-by: Nikolas Komonen <[email protected]> * throw error on cancelled quick pick When given the option to quick pick a lambda func url, the user can cancel it. This commit will now throw an error if the user cancels the quick pick, instead of quietly ignoring it. Signed-off-by: Nikolas Komonen <[email protected]> * Fix PR comments This commit: - Reduces the thoroughness of the extension manifest tests. It was a bit too specific. - Refactors the no lambda function URL message and splits the documentation URL in to the constants file. - Fixes the type casting of a Lambda mock to be more accurate, though not perfect due to limitations. Signed-off-by: Nikolas Komonen <[email protected]> * Add quickpick test This refactors the quick pick functionality so it can be tested. And also adds in the test. Signed-off-by: Nikolas Komonen <[email protected]> * chore: Remove unnecessary test The lambda client test is not necessary currently due to not having a defined way to test SDKs. This commit removes the test that tests the SDK and slightly refactors some code with the removal of this. Also this removes the parameter for passing in a SDK since it is no longer needed. Signed-off-by: Nikolas Komonen <[email protected]> Signed-off-by: Nikolas Komonen <[email protected]>
1 parent 9ad44f4 commit dfbf2ab

File tree

8 files changed

+220
-0
lines changed

8 files changed

+220
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Copy Lambda Function URL in AWS Explorer"
4+
}

package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,10 @@
840840
"command": "aws.invokeLambda",
841841
"when": "false"
842842
},
843+
{
844+
"command": "aws.copyLambdaUrl",
845+
"when": "false"
846+
},
843847
{
844848
"command": "aws.viewSchemaItem",
845849
"when": "false"
@@ -1478,6 +1482,11 @@
14781482
"when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/",
14791483
"group": "4@1"
14801484
},
1485+
{
1486+
"command": "aws.copyLambdaUrl",
1487+
"when": "view == aws.explorer && viewItem =~ /^(awsRegionFunctionNode|awsRegionFunctionNodeDownloadable)$/",
1488+
"group": "2@0"
1489+
},
14811490
{
14821491
"command": "aws.deleteCloudFormation",
14831492
"when": "view == aws.explorer && viewItem == awsCloudFormationNode",
@@ -2454,6 +2463,16 @@
24542463
}
24552464
}
24562465
},
2466+
{
2467+
"command": "aws.copyLambdaUrl",
2468+
"title": "%AWS.generic.copyUrl%",
2469+
"category": "%AWS.title%",
2470+
"cloud9": {
2471+
"cn": {
2472+
"category": "%AWS.title.cn%"
2473+
}
2474+
}
2475+
},
24572476
{
24582477
"command": "aws.deploySamApplication",
24592478
"title": "%AWS.command.deploySamApplication%",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@
194194
"AWS.generic.create": "Create...",
195195
"AWS.generic.save": "Save",
196196
"AWS.generic.close": "Close",
197+
"AWS.generic.copyUrl": "Copy URL",
197198
"AWS.generic.promptDelete": "Delete...",
198199
"AWS.generic.promptUpdate": "Update...",
199200
"AWS.generic.preview": "Preview",

src/lambda/activation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { invokeRemoteLambda } from './vue/remoteInvoke/invokeLambda'
1414
import { registerSamInvokeVueCommand } from './vue/configEditor/samInvokeBackend'
1515
import { Commands } from '../shared/vscode/commands2'
1616
import { DefaultLambdaClient } from '../shared/clients/lambdaClient'
17+
import { copyLambdaUrl } from './commands/copyLambdaUrl'
1718

1819
/**
1920
* Activates Lambda components.
@@ -52,6 +53,10 @@ export async function activate(context: ExtContext): Promise<void> {
5253
await uploadLambdaCommand()
5354
}
5455
}),
56+
Commands.register(
57+
'aws.copyLambdaUrl',
58+
async (node: LambdaFunctionNode) => await copyLambdaUrl(node, new DefaultLambdaClient(node.regionCode))
59+
),
5560
registerSamInvokeVueCommand(context)
5661
)
5762
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*!
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { DefaultLambdaClient, LambdaClient } from '../../shared/clients/lambdaClient'
7+
import { LambdaFunctionNode } from '../explorer/lambdaFunctionNode'
8+
import globals from '../../shared/extensionGlobals'
9+
import { copyToClipboard } from '../../shared/utilities/messages'
10+
import { addCodiconToString } from '../../shared/utilities/textUtilities'
11+
import { createQuickPick, QuickPickPrompter } from '../../shared/ui/pickerPrompter'
12+
import { isValidResponse } from '../../shared/wizards/wizard'
13+
import { FunctionUrlConfigList } from 'aws-sdk/clients/lambda'
14+
import { CancellationError } from '../../shared/utilities/timeoutUtils'
15+
import { lambdaFunctionUrlConfigUrl } from '../../shared/constants'
16+
17+
export const noLambdaFuncMessage = `No URL for Lambda function. [How to create URL.](${lambdaFunctionUrlConfigUrl})`
18+
19+
export async function copyLambdaUrl(
20+
node: Pick<LambdaFunctionNode, 'name' | 'regionCode'>,
21+
client: LambdaClient = new DefaultLambdaClient(node.regionCode),
22+
quickPickUrl = _quickPickUrl
23+
): Promise<void> {
24+
const configs = await client.getFunctionUrlConfigs(node.name)
25+
26+
if (configs.length == 0) {
27+
globals.window.showWarningMessage(noLambdaFuncMessage)
28+
globals.window.setStatusBarMessage(addCodiconToString('circle-slash', 'No URL for Lambda function.'), 5000)
29+
} else {
30+
let url: string | undefined = undefined
31+
if (configs.length > 1) {
32+
url = await quickPickUrl(configs)
33+
} else {
34+
url = configs[0].FunctionUrl
35+
}
36+
37+
if (url) {
38+
copyToClipboard(url, 'URL')
39+
}
40+
}
41+
}
42+
43+
async function _quickPickUrl(configList: FunctionUrlConfigList): Promise<string | undefined> {
44+
const res = await createLambdaFuncUrlPrompter(configList).prompt()
45+
if (!isValidResponse(res)) {
46+
throw new CancellationError('user')
47+
}
48+
return res
49+
}
50+
51+
export function createLambdaFuncUrlPrompter(configList: FunctionUrlConfigList): QuickPickPrompter<string> {
52+
const items = configList.map(c => ({
53+
label: c.FunctionArn,
54+
data: c.FunctionUrl,
55+
}))
56+
return createQuickPick(items, { title: 'Select function to copy url from.' })
57+
}

src/shared/clients/lambdaClient.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { Lambda } from 'aws-sdk'
77
import { _Blob } from 'aws-sdk/clients/lambda'
8+
import { ToolkitError } from '../errors'
89
import globals from '../extensionGlobals'
910
import { getLogger } from '../logger'
1011
import { ClassToInterfaceType } from '../utilities/tsUtils'
@@ -74,6 +75,24 @@ export class DefaultLambdaClient {
7475
}
7576
}
7677

78+
public async getFunctionUrlConfigs(name: string): Promise<Lambda.FunctionUrlConfigList> {
79+
getLogger().debug(`GetFunctionUrlConfig called for function: ${name}`)
80+
const client = await this.createSdkClient()
81+
82+
try {
83+
const request = client.listFunctionUrlConfigs({ FunctionName: name })
84+
const response = await request.promise()
85+
// prune `Code` from logs so we don't reveal a signed link to customer resources.
86+
getLogger().debug('GetFunctionUrlConfig returned response (code section pruned): %O', {
87+
...response,
88+
Code: 'Pruned',
89+
})
90+
return response.FunctionUrlConfigs
91+
} catch (e) {
92+
throw ToolkitError.chain(e, 'Failed to get Lambda function URLs')
93+
}
94+
}
95+
7796
public async updateFunctionCode(name: string, zipFile: Buffer): Promise<Lambda.FunctionConfiguration> {
7897
getLogger().debug(`updateFunctionCode called for function: ${name}`)
7998
const client = await this.createSdkClient()

src/shared/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const ssoCredentialsHelpUrl: string =
3535

3636
export const supportedLambdaRuntimesUrl: string =
3737
'https://docs.aws.amazon.com/lambda/latest/dg/runtime-support-policy.html'
38+
export const createUrlForLambdaFunctionUrl = 'https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html'
3839
// URLs for samInitWizard
3940
export const samInitDocUrl: string = isCloud9()
4041
? 'https://docs.aws.amazon.com/cloud9/latest/user-guide/serverless-apps-toolkit.html#sam-create'
@@ -46,6 +47,7 @@ export const launchConfigDocUrl: string = isCloud9()
4647
export const samDeployDocUrl: string = isCloud9()
4748
? 'https://docs.aws.amazon.com/cloud9/latest/user-guide/serverless-apps-toolkit.html#deploy-serverless-app'
4849
: 'https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/serverless-apps.html#serverless-apps-deploy'
50+
export const lambdaFunctionUrlConfigUrl: string = 'https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html'
4951

5052
// URLs for CDK
5153
export const cdkProvideFeedbackUrl: string = `${githubUrl}/issues/new/choose`
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*!
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as assert from 'assert'
7+
import { createStubInstance, SinonStubbedInstance, stub, SinonStub, spy, SinonSpy } from 'sinon'
8+
import { copyLambdaUrl, createLambdaFuncUrlPrompter, noLambdaFuncMessage } from '../../../lambda/commands/copyLambdaUrl'
9+
import { LambdaFunctionNode } from '../../../lambda/explorer/lambdaFunctionNode'
10+
import { DefaultLambdaClient, LambdaClient } from '../../../shared/clients/lambdaClient'
11+
import globals from '../../../shared/extensionGlobals'
12+
import { addCodiconToString } from '../../../shared/utilities/textUtilities'
13+
import { env } from 'vscode'
14+
import { FunctionUrlConfig } from 'aws-sdk/clients/lambda'
15+
import { createQuickPickTester } from '../../shared/ui/testUtils'
16+
17+
/**
18+
* Builds an instance of {@link FunctionUrlConfig} without the
19+
* need to define all values.
20+
*
21+
* @param options key + value of {@link FunctionUrlConfig}
22+
*/
23+
export function buildFunctionUrlConfig(options: Partial<FunctionUrlConfig>): FunctionUrlConfig {
24+
return {
25+
AuthType: options.AuthType ?? '',
26+
CreationTime: options.CreationTime ?? '',
27+
FunctionArn: options.FunctionArn ?? '',
28+
FunctionUrl: options.FunctionUrl ?? '',
29+
LastModifiedTime: options.LastModifiedTime ?? '',
30+
}
31+
}
32+
33+
describe('copy lambda function URL to clipboard', async () => {
34+
let client: SinonStubbedInstance<LambdaClient>
35+
let node: Pick<LambdaFunctionNode, 'name' | 'regionCode'>
36+
let quickPickUrlFunc: SinonStub
37+
38+
const nodeName = 'my-node-name'
39+
40+
beforeEach(async () => {
41+
client = createStubInstance(DefaultLambdaClient)
42+
quickPickUrlFunc = stub()
43+
44+
node = { name: nodeName, regionCode: 'myRegion' }
45+
})
46+
47+
it('Single URL exists', async () => {
48+
const urlConfig = buildFunctionUrlConfig({ FunctionUrl: 'url1', FunctionArn: 'arn1' })
49+
client.getFunctionUrlConfigs.resolves([urlConfig])
50+
51+
await copyLambdaUrl(node, client)
52+
53+
assert.strictEqual(await env.clipboard.readText(), urlConfig.FunctionUrl)
54+
})
55+
56+
it('Multiple URLs exists', async () => {
57+
const expectedConfig = buildFunctionUrlConfig({ FunctionUrl: 'url1', FunctionArn: 'arn1' })
58+
const urlConfigs = [expectedConfig, buildFunctionUrlConfig({ FunctionUrl: 'url2', FunctionArn: 'arn2' })]
59+
client.getFunctionUrlConfigs.resolves(urlConfigs)
60+
quickPickUrlFunc.resolves(expectedConfig.FunctionUrl)
61+
62+
await copyLambdaUrl(node, client, quickPickUrlFunc)
63+
64+
assert.deepStrictEqual(quickPickUrlFunc.args, [[urlConfigs]])
65+
assert.strictEqual(await env.clipboard.readText(), expectedConfig.FunctionUrl)
66+
})
67+
68+
describe("URL doesn't exist", async () => {
69+
let spiedInformationMessage: SinonSpy
70+
let spiedStatusBarMessage: SinonSpy
71+
72+
before(async () => {
73+
await env.clipboard.writeText('') // clear clipboard
74+
spiedInformationMessage = spy(globals.window, 'showWarningMessage')
75+
spiedStatusBarMessage = spy(globals.window, 'setStatusBarMessage')
76+
})
77+
78+
afterEach(async () => {
79+
spiedInformationMessage.resetHistory()
80+
spiedStatusBarMessage.resetHistory()
81+
})
82+
83+
it(`URL doesn't exist`, async () => {
84+
client.getFunctionUrlConfigs.resolves([])
85+
86+
await copyLambdaUrl(node, client)
87+
88+
assert.strictEqual(await env.clipboard.readText(), '')
89+
assert.deepStrictEqual(spiedInformationMessage.args, [[noLambdaFuncMessage]])
90+
assert.deepStrictEqual(spiedStatusBarMessage.args, [
91+
[addCodiconToString('circle-slash', 'No URL for Lambda function.'), 5000],
92+
])
93+
})
94+
})
95+
})
96+
97+
describe('lambda func url prompter', async () => {
98+
it('prompts for lambda function ARN', async () => {
99+
const configList: FunctionUrlConfig[] = [
100+
<FunctionUrlConfig>{ FunctionUrl: 'url1', FunctionArn: 'arn1' },
101+
<FunctionUrlConfig>{ FunctionUrl: 'url2', FunctionArn: 'arn2' },
102+
]
103+
const prompter = createLambdaFuncUrlPrompter(configList)
104+
const tester = createQuickPickTester(prompter)
105+
tester.assertItems(
106+
configList.map(c => {
107+
return { label: c.FunctionArn, data: c.FunctionUrl } // order matters
108+
})
109+
)
110+
tester.acceptItem(configList[1].FunctionArn)
111+
await tester.result(configList[1].FunctionUrl)
112+
})
113+
})

0 commit comments

Comments
 (0)