Skip to content

Commit 3c7f54b

Browse files
committed
refactor(uploadLambda): fix bugs, future-proof
- Fix bugs: 1. Cloud9 findFile() branch wasn't tested. 2. Cloud9 findFile() doesn't find `.application.json` at more than one level of nesting. - Add `techdebt.test.ts` as a mechanism to enforce removal of temporary code once our dependencies or other factors make that possible.
1 parent 867cf8c commit 3c7f54b

File tree

7 files changed

+232
-156
lines changed

7 files changed

+232
-156
lines changed

src/lambda/commands/uploadLambda.ts

Lines changed: 88 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import * as AdmZip from 'adm-zip'
1212
import * as fs from 'fs-extra'
1313
import * as path from 'path'
1414
import { showConfirmationMessage, showViewLogsMessage } from '../../shared/utilities/messages'
15-
import { fileExists, findFile, makeTemporaryToolkitFolder, tryRemoveFolder } from '../../shared/filesystemUtilities'
15+
import {
16+
fileExists,
17+
cloud9Findfile,
18+
makeTemporaryToolkitFolder,
19+
tryRemoveFolder,
20+
} from '../../shared/filesystemUtilities'
1621
import * as localizedText from '../../shared/localizedText'
1722
import { getLogger } from '../../shared/logger'
1823
import { SamCliBuildInvocation } from '../../shared/sam/cli/samCliBuild'
@@ -35,6 +40,41 @@ import { toArrayAsync } from '../../shared/utilities/collectionUtils'
3540
import { fromExtensionManifest } from '../../shared/settings'
3641
import { createRegionPrompter } from '../../shared/ui/common/region'
3742

43+
interface SavedLambdas {
44+
[profile: string]: { [region: string]: string }
45+
}
46+
47+
class LambdaSettings extends fromExtensionManifest('aws.lambda', { recentlyUploaded: Object }) {
48+
static #instance: LambdaSettings
49+
50+
public getRecentLambdas(): SavedLambdas | undefined {
51+
try {
52+
return this.get('recentlyUploaded')
53+
} catch (error) {
54+
this.delete('recentlyUploaded')
55+
}
56+
}
57+
58+
/**
59+
* Adds a new "recently used Lambda" to user settings for the given profile
60+
* and region (limit of one item per profile+region).
61+
*/
62+
public setRecentLambda(profile: string, region: string, lambdaName: string): Promise<boolean> {
63+
const oldLambdas = this.getRecentLambdas()
64+
return this.update('recentlyUploaded', {
65+
...oldLambdas,
66+
[profile]: {
67+
...(oldLambdas?.[profile] ?? {}),
68+
[region]: lambdaName,
69+
},
70+
})
71+
}
72+
73+
public static get instance() {
74+
return (this.#instance ??= new this())
75+
}
76+
}
77+
3878
interface LambdaFunction {
3979
readonly name: string
4080
readonly region: string
@@ -84,7 +124,7 @@ export async function uploadLambdaCommand(lambdaArg?: LambdaFunction, path?: vsc
84124
if (result === 'Succeeded') {
85125
const profile = globals.awsContext.getCredentialProfileName()
86126
if (profile && lambda) {
87-
updateSavedLambda(profile, lambda.region, lambda.name)
127+
LambdaSettings.instance.setRecentLambda(profile, lambda.region, lambda.name)
88128
}
89129
}
90130
}
@@ -442,27 +482,34 @@ async function uploadZipBuffer(
442482
)
443483
}
444484

445-
export async function findApplicationJsonFile(startPath: vscode.Uri): Promise<vscode.Uri | undefined> {
446-
const isTemplateFile = /template.(yaml|yml|json)$/.test(startPath.fsPath)
447-
const parentDir = isTemplateFile ? vscode.Uri.joinPath(startPath, '..') : startPath
448-
449-
if (isCloud9()) {
450-
const found = await findFile(parentDir.fsPath, '.application.json')
451-
if (!found) {
452-
getLogger().debug(
453-
'lambda: .application.json file not found in parent directory or sub directories of %s',
454-
parentDir.fsPath
455-
)
456-
}
457-
return found ? vscode.Uri.file(found) : undefined
458-
} else {
459-
const found = await vscode.workspace.findFiles(
460-
new vscode.RelativePattern(parentDir.path, '**/*.application.json'),
461-
undefined,
462-
1
485+
export async function findApplicationJsonFile(
486+
startPath: vscode.Uri,
487+
cloud9 = isCloud9()
488+
): Promise<vscode.Uri | undefined> {
489+
if (!(await fileExists(startPath.fsPath))) {
490+
getLogger().error(
491+
'findApplicationJsonFile() invalid path (not accessible or does not exist): "%s"',
492+
startPath.fsPath
463493
)
464-
return found[0]
494+
return undefined
465495
}
496+
const isdir = fs.statSync(startPath.fsPath).isDirectory()
497+
const parentDir = isdir ? startPath.fsPath : path.dirname(startPath.fsPath)
498+
const found = cloud9
499+
? await cloud9Findfile(parentDir, '.application.json')
500+
: await vscode.workspace.findFiles(
501+
new vscode.RelativePattern(parentDir, '**/.application.json'),
502+
// exclude:
503+
// - null = NO excludes apply
504+
// - undefined = default excludes apply (e.g. the `files.exclude` setting but not `search.exclude`).
505+
// eslint-disable-next-line no-null/no-null
506+
null,
507+
1
508+
)
509+
if (!found || found.length === 0) {
510+
getLogger().debug('uploadLambda: .application.json not found in: "%s"', parentDir)
511+
}
512+
return found[0]
466513
}
467514

468515
export function getFunctionNames(file: vscode.Uri, region: string): string[] | undefined {
@@ -489,45 +536,56 @@ export function getFunctionNames(file: vscode.Uri, region: string): string[] | u
489536

490537
async function listAllLambdaNames(region: string, path?: vscode.Uri) {
491538
const lambdaFunctionNames: DataQuickPickItem<string>[] = []
539+
540+
// Get Lambda functions from .application.json #2588
492541
if (path) {
493-
const namesFromAppFile = await getLambdaNamesFromApplicationFile(region, path)
494-
if (namesFromAppFile) {
542+
const appFile = await findApplicationJsonFile(path)
543+
const namesFromAppFile = appFile ? getFunctionNames(appFile, region) : undefined
544+
if (!appFile) {
545+
getLogger().debug('lambda: .application.json not found')
546+
} else if (!namesFromAppFile) {
547+
getLogger().debug('lambda: no functions in .application.json for region: %s', region)
548+
} else {
495549
lambdaFunctionNames.push(
496550
...namesFromAppFile.map(n => {
497551
return {
498552
label: n,
499-
description: localize('AWS.lambda.upload.fromC9AppFile', 'from appliication.json'),
553+
description: localize('AWS.lambda.upload.fromAppJson', 'from .application.json'),
500554
data: n,
501555
}
502556
})
503557
)
504558
}
505559
}
560+
561+
// Get Lambda functions from user AWS account.
506562
const lambdaClient = globals.toolkitClientBuilder.createLambdaClient(region)
507563
try {
508564
const foundLambdas = await toArrayAsync(listLambdaFunctions(lambdaClient))
509565
for (const l of foundLambdas) {
510566
lambdaFunctionNames.push({ label: l.FunctionName!, data: l.FunctionName })
511567
}
512568
} catch (error) {
513-
getLogger().error('upload lambda: Error listing lambdas: %s', (error as Error).message)
569+
getLogger().error('lambda: failed to list Lambda functions: %s', (error as Error).message)
514570
}
515-
const previousLambdas = getSavedLambdas()
571+
572+
// Get "recently used" Lambda functions.
573+
const recent = LambdaSettings.instance.getRecentLambdas()
516574
const profile = globals.awsContext.getCredentialProfileName()
517-
if (previousLambdas && profile && previousLambdas[profile] && previousLambdas[profile][region]) {
575+
if (profile && recent?.[profile]?.[region]) {
518576
let isInList = false
519577
for (const l of lambdaFunctionNames) {
520-
if (l.label === previousLambdas[profile][region]) {
578+
if (l.label === recent[profile][region]) {
521579
l.description = localizedText.recentlyUsed
522580
l.recentlyUsed = true
523581
isInList = true
524582
}
525583
}
526584
if (!isInList) {
527585
lambdaFunctionNames.splice(0, 0, {
528-
label: previousLambdas[profile][region],
586+
label: recent[profile][region],
529587
recentlyUsed: true,
530-
data: previousLambdas[profile][region],
588+
data: recent[profile][region],
531589
description: localizedText.recentlyUsed,
532590
})
533591
}
@@ -558,39 +616,3 @@ function createFunctionNamePrompter(region: string, path?: vscode.Uri) {
558616
})
559617
return prompter
560618
}
561-
562-
interface SavedLambdas {
563-
[profile: string]: { [region: string]: string }
564-
}
565-
566-
function getSavedLambdas(): SavedLambdas | undefined {
567-
const settings = new LambdaSettings()
568-
try {
569-
return settings.get('recentlyUploaded')
570-
} catch (error) {
571-
getLogger().error('settings: Error retreiving saved lambdas from settings: %s', (error as Error).message)
572-
}
573-
}
574-
575-
async function updateSavedLambda(profile: string, region: string, lambdaName: string): Promise<boolean> {
576-
const settings = new LambdaSettings()
577-
const oldLambdas = getSavedLambdas()
578-
return settings.update('recentlyUploaded', {
579-
...oldLambdas,
580-
[profile]: {
581-
...(oldLambdas?.[profile] ?? {}),
582-
[region]: lambdaName,
583-
},
584-
})
585-
}
586-
587-
async function getLambdaNamesFromApplicationFile(region: string, path: vscode.Uri) {
588-
const appFile = await findApplicationJsonFile(path)
589-
if (appFile) {
590-
return getFunctionNames(appFile, region)
591-
} else {
592-
getLogger().debug('lambda: No .application.json files found')
593-
}
594-
}
595-
596-
class LambdaSettings extends fromExtensionManifest('aws.lambda', { recentlyUploaded: Object }) {}

src/shared/filesystemUtilities.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,14 @@ export function downloadsDir(): string {
2929
}
3030
}
3131

32-
export async function fileExists(filePath: string): Promise<boolean> {
32+
/**
33+
* Checks if file or directory `p` exists.
34+
*
35+
* TODO: optionally check read/write permissions and return a granular status.
36+
*/
37+
export async function fileExists(p: string): Promise<boolean> {
3338
try {
34-
await access(filePath)
39+
await access(p)
3540
} catch (err) {
3641
return false
3742
}
@@ -164,24 +169,29 @@ export async function hasFileWithSuffix(dir: string, suffix: string, exclude?: v
164169
}
165170

166171
/**
167-
* Searches directory and all sub-directories for a filename.
172+
* TEMPORARY SHIM for vscode.workspace.findFiles() on Cloud9.
173+
*
168174
* @param dir Directory to search
169175
* @param fileName Name of file to locate
170-
* @returns The absolute path if found.
176+
* @returns List of one or zero Uris (for compat with vscode.workspace.findFiles())
171177
*/
172-
export async function findFile(dir: string, fileName: string): Promise<string | undefined> {
178+
export async function cloud9Findfile(dir: string, fileName: string): Promise<vscode.Uri[]> {
173179
const files = await readdir(dir)
174-
const subDirs = []
180+
const subDirs: vscode.Uri[] = []
175181
for (const file of files) {
176182
const filePath = path.join(dir, file)
177183
if (filePath === path.join(dir, fileName)) {
178-
return filePath
184+
return [vscode.Uri.file(filePath)]
179185
}
180186
if ((await stat(filePath)).isDirectory()) {
181-
subDirs.push(filePath)
187+
subDirs.push(vscode.Uri.file(filePath))
182188
}
183189
}
184-
for (const dir of subDirs) {
185-
findFile(dir, fileName)
190+
for (const d of subDirs) {
191+
const found = await cloud9Findfile(d.fsPath, fileName)
192+
if (found.length > 0) {
193+
return found
194+
}
186195
}
196+
return []
187197
}

src/shared/vscode/env.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,12 @@ export { extensionVersion }
7979
* by the `engines` field in `package.json`
8080
*/
8181
export function isMinimumVersion(): boolean {
82-
return vscode.version.startsWith(packageJson.engines.vscode.replace(/\^\~/, ''))
82+
return vscode.version.startsWith(getMinVscodeVersion())
83+
}
84+
85+
/**
86+
* Returns the minimum vscode "engine" version declared in `package.json`.
87+
*/
88+
export function getMinVscodeVersion(): string {
89+
return packageJson.engines.vscode.replace(/[^~]/, '')
8390
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*!
2+
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import * as vscode from 'vscode'
6+
import * as assert from 'assert'
7+
import * as fs from 'fs-extra'
8+
import * as path from 'path'
9+
import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities'
10+
import { findApplicationJsonFile, getFunctionNames } from '../../../lambda/commands/uploadLambda'
11+
import { assertEqualPaths, toFile } from '../../testUtil'
12+
13+
describe('uploadLambda', async function () {
14+
let tempFolder: string
15+
let folderUri: vscode.Uri
16+
const dotApplicationJsonData = `{
17+
"DeploymentMethod": "lambda",
18+
"Functions": {
19+
"sampleFunction": {
20+
"PhysicalId": {
21+
"us-west-2": "sampleFunction-w2",
22+
"us-east-1": "sampleFunction-e1"
23+
}
24+
},
25+
"differentFunction": {
26+
"PhysicalId": {
27+
"us-east-1": "differentFunction-e1"
28+
}
29+
}
30+
}
31+
}`
32+
beforeEach(async function () {
33+
tempFolder = await makeTemporaryToolkitFolder()
34+
folderUri = vscode.Uri.file(tempFolder)
35+
})
36+
afterEach(async function () {
37+
await fs.remove(tempFolder)
38+
})
39+
40+
it('finds application.json file from dir path - flat', async function () {
41+
toFile('top secret data', path.join(tempFolder, '.application.json'))
42+
assertEqualPaths(
43+
(await findApplicationJsonFile(folderUri))?.fsPath ?? '',
44+
path.join(tempFolder, '.application.json')
45+
)
46+
// Also test Cloud9 temporary workaround.
47+
assertEqualPaths(
48+
(await findApplicationJsonFile(folderUri, true))?.fsPath ?? '',
49+
path.join(tempFolder, '.application.json')
50+
)
51+
})
52+
53+
it('finds application.json file from dir path - nested', async function () {
54+
const subfolder = path.join(tempFolder, 'one', 'two')
55+
const appjsonPath = path.join(subfolder, '.application.json')
56+
toFile('top secret data', appjsonPath)
57+
58+
assertEqualPaths((await findApplicationJsonFile(folderUri))?.fsPath ?? '', appjsonPath)
59+
// Also test Cloud9 temporary workaround.
60+
assertEqualPaths((await findApplicationJsonFile(folderUri, true))?.fsPath ?? '', appjsonPath)
61+
})
62+
63+
it('finds application.json file from template file path', async function () {
64+
const templateUri = vscode.Uri.file(path.join(tempFolder, 'template.yaml'))
65+
const appjsonPath = path.join(tempFolder, '.application.json')
66+
toFile('SAM stuff...', templateUri.fsPath)
67+
toFile('top secret data', appjsonPath)
68+
69+
assertEqualPaths((await findApplicationJsonFile(templateUri))?.fsPath ?? '', appjsonPath)
70+
// Also test Cloud9 temporary workaround.
71+
assertEqualPaths((await findApplicationJsonFile(templateUri, true))?.fsPath ?? '', appjsonPath)
72+
})
73+
74+
it('lists functions from .application.json', async function () {
75+
const filePath = path.join(tempFolder, '.application.json')
76+
toFile(dotApplicationJsonData, filePath)
77+
const foundFunctions1 = getFunctionNames(vscode.Uri.file(filePath), 'us-west-2')
78+
const foundFunctions2 = getFunctionNames(vscode.Uri.file(filePath), 'us-east-1')
79+
assert.deepStrictEqual(foundFunctions1, ['sampleFunction-w2'])
80+
assert.deepStrictEqual(foundFunctions2, ['sampleFunction-e1', 'differentFunction-e1'])
81+
})
82+
83+
it('invalid .application.json', async function () {
84+
const filePath = path.join(tempFolder, '.application.json')
85+
const invalidJson = '{ "DeploymentMethod": "lambda", "Functions": { ?? } }'
86+
toFile(invalidJson, filePath)
87+
assert.deepStrictEqual(getFunctionNames(vscode.Uri.file(filePath), 'us-west-2'), undefined)
88+
assert.deepStrictEqual(getFunctionNames(vscode.Uri.file(filePath), 'us-east-1'), undefined)
89+
})
90+
})

0 commit comments

Comments
 (0)