Skip to content

Commit 616835a

Browse files
committed
fix(sam): "Invalid (or missing) template file"
Problem: SAM debug may fail to find template.yaml even if it is explicitly given by `invokeTarget.templatePath` in the vscode launch config. #2614 Solution: - Always call addItem() in validate(), instead of depending on the workspace-wide "scan". - Log a message when process() fails to process the file contents.
1 parent 80817a2 commit 616835a

File tree

9 files changed

+79
-71
lines changed

9 files changed

+79
-71
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "SAM debugging: \"Invalid (or missing) template file\" may occur even when a valid template.yaml is specified by `invokeTarget.templatePath` in the launch config. #2614"
4+
}

src/lambda/vue/configEditor/samInvokeBackend.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -351,24 +351,22 @@ async function getLaunchConfigQuickPickItems(
351351
const existingConfigs = launchConfig.getDebugConfigurations()
352352
const samValidator = new DefaultAwsSamDebugConfigurationValidator(vscode.workspace.getWorkspaceFolder(uri))
353353
const registry = await globals.templateRegistry
354-
return existingConfigs
355-
.map((val, index) => {
356-
return {
357-
config: val,
358-
index,
359-
}
360-
})
361-
.filter(o => {
362-
const res = samValidator.validate(o.config as any as AwsSamDebuggerConfiguration, registry, true)
363-
return res?.isValid
364-
})
365-
.map(val => {
366-
return {
367-
index: val.index,
368-
label: val.config.name,
369-
config: val.config as AwsSamDebuggerConfiguration,
370-
}
371-
})
354+
const mapped = existingConfigs.map((val, index) => {
355+
return {
356+
config: val as AwsSamDebuggerConfiguration,
357+
index: index,
358+
label: val.name,
359+
}
360+
})
361+
// XXX: can't use filter() with async predicate.
362+
const filtered: LaunchConfigPickItem[] = []
363+
for (const c of mapped) {
364+
const valid = await samValidator.validate(c.config, registry, true)
365+
if (valid?.isValid) {
366+
filtered.push(c)
367+
}
368+
}
369+
return filtered
372370
}
373371

374372
export function finalizeConfig(config: AwsSamDebuggerConfiguration, name: string): AwsSamDebuggerConfiguration {

src/shared/debug/launchConfiguration.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,14 @@ export class LaunchConfiguration {
7474
isAwsSamDebugConfiguration(o)
7575
) as AwsSamDebuggerConfiguration[]
7676
const registry = await globals.templateRegistry
77-
return configs.filter(o => this.samValidator.validate(o, registry)?.isValid)
77+
// XXX: can't use filter() with async predicate.
78+
const validConfigs: AwsSamDebuggerConfiguration[] = []
79+
for (const c of configs) {
80+
if ((await this.samValidator.validate(c, registry))?.isValid) {
81+
validConfigs.push(c)
82+
}
83+
}
84+
return validConfigs
7885
}
7986

8087
/**

src/shared/fs/watchedFiles.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,31 +172,44 @@ export abstract class WatchedFiles<T> implements vscode.Disposable {
172172
}
173173

174174
/**
175-
* Adds an item to registry. Wipes any existing item in its place with new copy of the data
176-
* @param uri vscode.Uri containing the item to load in
175+
* Adds or updates an item in the registry, and returns the result.
176+
*
177+
* If the item matches an "exclude" rule, it is not added nor does it update/replace any existing item.
178+
*
179+
* @param uri vscode.Uri containing the item to register.
180+
* @param quiet On failure, log a message instead of throwing an exception.
181+
* @param contents Optional data to associate with the item, for logical (non-filesystem) URIs.
182+
*
183+
* @returns Item, or undefined if (1) processing fails or (2) the name matches an "exclude" rule.
177184
*/
178-
public async addItem(uri: vscode.Uri, quiet?: boolean, contents?: string): Promise<void> {
185+
public async addItem(uri: vscode.Uri, quiet?: boolean, contents?: string): Promise<WatchedItem<T> | undefined> {
179186
const excluded = this.excludedFilePatterns.find(pattern => uri.fsPath.match(pattern))
180187
if (excluded) {
181-
getLogger().verbose(`${this.name}: excluding path (matches "${excluded}"): ${uri.fsPath}`)
182-
return
188+
getLogger().verbose(`${this.name}: excluded (matches "${excluded}"): ${uri.fsPath}`)
189+
return undefined
183190
}
184191
this.assertAbsolute(uri)
185192
const pathAsString = normalizeVSCodeUri(uri)
186193
try {
187194
const item = await this.process(uri, contents)
188195
if (item) {
189196
this.registryData.set(pathAsString, item)
197+
return {
198+
path: pathAsString,
199+
item: item,
200+
}
190201
} else {
202+
getLogger().info(`${this.name}: failed to process: ${uri}`)
191203
// if value isn't valid for type, remove from registry
192204
this.registryData.delete(pathAsString)
193205
}
194206
} catch (e) {
195207
if (!quiet) {
196208
throw e
197209
}
198-
getLogger().verbose(`${this.name}: failed to load(): ${uri}: ${(e as Error).message}`)
210+
getLogger().info(`${this.name}: failed to process: ${uri}: ${(e as Error).message}`)
199211
}
212+
return undefined
200213
}
201214

202215
/**

src/shared/sam/debugger/awsSamDebugConfigurationValidator.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface ValidationResult {
2727
}
2828

2929
export interface AwsSamDebugConfigurationValidator {
30-
validate(config: AwsSamDebuggerConfiguration, registry: CloudFormationTemplateRegistry): ValidationResult
30+
validate(config: AwsSamDebuggerConfiguration, registry: CloudFormationTemplateRegistry): Promise<ValidationResult>
3131
}
3232

3333
export class DefaultAwsSamDebugConfigurationValidator implements AwsSamDebugConfigurationValidator {
@@ -36,11 +36,11 @@ export class DefaultAwsSamDebugConfigurationValidator implements AwsSamDebugConf
3636
/**
3737
* Validates debug configuration properties.
3838
*/
39-
public validate(
39+
public async validate(
4040
config: AwsSamDebuggerConfiguration,
4141
registry: CloudFormationTemplateRegistry,
4242
resolveVars?: boolean
43-
): ValidationResult {
43+
): Promise<ValidationResult> {
4444
let rv: ValidationResult = { isValid: false, message: undefined }
4545
if (resolveVars) {
4646
config = doTraverseAndReplace(config, this.workspaceFolder?.uri.fsPath ?? '')
@@ -67,12 +67,15 @@ export class DefaultAwsSamDebugConfigurationValidator implements AwsSamDebugConf
6767
config.invokeTarget.target === TEMPLATE_TARGET_TYPE ||
6868
config.invokeTarget.target === API_TARGET_TYPE
6969
) {
70-
let cfnTemplate
70+
let cfnTemplate: CloudFormation.Template | undefined
7171
if (config.invokeTarget.templatePath) {
7272
const fullpath = tryGetAbsolutePath(this.workspaceFolder, config.invokeTarget.templatePath)
7373
// Normalize to absolute path for use in the runner.
7474
config.invokeTarget.templatePath = fullpath
75-
cfnTemplate = registry.getItem(fullpath)?.item
75+
// Forcefully add to the registry in case the registry scan somehow missed the file. #2614
76+
// If the user (launch config) gave an explicit path we should always "find" it.
77+
const uri = vscode.Uri.file(fullpath)
78+
cfnTemplate = (await registry.addItem(uri, true))?.item
7679
}
7780
rv = this.validateTemplateConfig(config, config.invokeTarget.templatePath, cfnTemplate)
7881
} else if (config.invokeTarget.target === CODE_TARGET_TYPE) {

src/shared/sam/debugger/awsSamDebugger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
439439
})
440440
} else {
441441
const registry = await globals.templateRegistry
442-
const rv = configValidator.validate(config, registry)
442+
const rv = await configValidator.validate(config, registry)
443443
if (!rv.isValid) {
444444
throw new ToolkitError(`Invalid launch configuration: ${rv.message}`, { code: 'BadLaunchConfig' })
445445
} else if (rv.message) {

src/test/shared/debug/launchConfiguration.test.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,7 @@ describe('LaunchConfiguration', function () {
112112
mockConfigSource = mock()
113113
mockSamValidator = mock()
114114
when(mockConfigSource.getDebugConfigurations()).thenReturn(debugConfigurations)
115-
when(mockSamValidator.validate(deepEqual(samDebugConfiguration), registry)).thenReturn(
116-
await (async () => {
117-
return { isValid: true }
118-
})()
119-
)
115+
when(mockSamValidator.validate(deepEqual(samDebugConfiguration), registry)).thenResolve({ isValid: true })
120116
})
121117

122118
afterEach(async function () {

src/test/shared/sam/debugger/awsSamDebugConfigurationValidator.test.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -110,92 +110,92 @@ describe('DefaultAwsSamDebugConfigurationValidator', function () {
110110
validator = new DefaultAwsSamDebugConfigurationValidator(instance(mockFolder))
111111
})
112112

113-
it('returns invalid when resolving debug configurations with an invalid request type', function () {
113+
it('returns invalid when resolving debug configurations with an invalid request type', async () => {
114114
templateConfig.request = 'not-direct-invoke'
115115

116-
const result = validator.validate(templateConfig, mockRegistry)
116+
const result = await validator.validate(templateConfig, mockRegistry)
117117
assert.strictEqual(result.isValid, false)
118118
})
119119

120-
it('returns invalid when resolving debug configurations with an invalid target type', function () {
120+
it('returns invalid when resolving debug configurations with an invalid target type', async () => {
121121
templateConfig.invokeTarget.target = 'not-valid' as any
122122

123-
const result = validator.validate(templateConfig as any, mockRegistry)
123+
const result = await validator.validate(templateConfig as any, mockRegistry)
124124
assert.strictEqual(result.isValid, false)
125125
})
126126

127-
it("returns invalid when resolving template debug configurations with a template that isn't in the registry", () => {
127+
it("returns invalid when resolving template debug configurations with a template that isn't in the registry", async () => {
128128
const mockEmptyRegistry: CloudFormationTemplateRegistry = mock()
129129
when(mockEmptyRegistry.getItem('/')).thenReturn(undefined)
130130

131131
validator = new DefaultAwsSamDebugConfigurationValidator(instance(mockFolder))
132132

133-
const result = validator.validate(templateConfig, mockRegistry)
133+
const result = await validator.validate(templateConfig, mockRegistry)
134134
assert.strictEqual(result.isValid, false)
135135
})
136136

137-
it("returns invalid when resolving template debug configurations with a template that doesn't have the set resource", () => {
137+
it("returns invalid when resolving template debug configurations with a template that doesn't have the set resource", async () => {
138138
const target = templateConfig.invokeTarget as TemplateTargetProperties
139139
target.logicalId = 'wrong'
140140

141-
const result = validator.validate(templateConfig, mockRegistry)
141+
const result = await validator.validate(templateConfig, mockRegistry)
142142
assert.strictEqual(result.isValid, false)
143143
})
144144

145-
it("returns invalid when resolving template debug configurations with a template that isn't serverless", () => {
145+
it("returns invalid when resolving template debug configurations with a template that isn't serverless", async () => {
146146
const target = templateConfig.invokeTarget as TemplateTargetProperties
147147
target.logicalId = 'OtherResource'
148148

149-
const result = validator.validate(templateConfig, mockRegistry)
149+
const result = await validator.validate(templateConfig, mockRegistry)
150150
assert.strictEqual(result.isValid, false)
151151
})
152152

153-
it('returns undefined when resolving template debug configurations with a resource that has an invalid runtime in template', function () {
153+
it('returns undefined when resolving template debug configurations with a resource that has an invalid runtime in template', async () => {
154154
const properties = templateData.item.Resources?.TestResource?.Properties as CloudFormation.ZipResourceProperties
155155
properties.Runtime = 'invalid'
156156

157-
const result = validator.validate(templateConfig, mockRegistry)
157+
const result = await validator.validate(templateConfig, mockRegistry)
158158
assert.strictEqual(result.isValid, false)
159159
})
160160

161-
it("API config returns invalid when resolving with a template that isn't serverless", () => {
161+
it("API config returns invalid when resolving with a template that isn't serverless", async () => {
162162
const target = templateConfig.invokeTarget as TemplateTargetProperties
163163
target.logicalId = 'OtherResource'
164164

165-
const result = validator.validate(apiConfig, mockRegistry)
165+
const result = await validator.validate(apiConfig, mockRegistry)
166166
assert.strictEqual(result.isValid, false)
167167
})
168168

169-
it('API config is invalid when it does not have an API field', function () {
169+
it('API config is invalid when it does not have an API field', async () => {
170170
const config = createApiConfig()
171171
config.api = undefined
172172

173-
const result = validator.validate(config, mockRegistry)
173+
const result = await validator.validate(config, mockRegistry)
174174
assert.strictEqual(result.isValid, false)
175175
})
176176

177-
it("API config is invalid when its path does not start with a '/'", () => {
177+
it("API config is invalid when its path does not start with a '/'", async () => {
178178
const config = createApiConfig()
179179

180180
config.api!.path = 'noleadingslash'
181181

182-
const result = validator.validate(config, mockRegistry)
182+
const result = await validator.validate(config, mockRegistry)
183183
assert.strictEqual(result.isValid, false)
184184
})
185185

186-
it('returns invalid when resolving code debug configurations with invalid runtimes', function () {
186+
it('returns invalid when resolving code debug configurations with invalid runtimes', async () => {
187187
codeConfig.lambda = { runtime: 'asd' }
188188

189-
const result = validator.validate(codeConfig, mockRegistry)
189+
const result = await validator.validate(codeConfig, mockRegistry)
190190
assert.strictEqual(result.isValid, false)
191191
})
192192

193-
it('returns invalid when Image app does not declare runtime', function () {
193+
it('returns invalid when Image app does not declare runtime', async () => {
194194
const lambda = imageTemplateConfig.lambda
195195

196196
delete lambda?.runtime
197197

198-
const result = validator.validate(templateConfig, mockRegistry)
198+
const result = await validator.validate(templateConfig, mockRegistry)
199199
assert.strictEqual(result.isValid, false)
200200
})
201201
})

src/test/shared/sam/debugger/samDebugConfigProvider.test.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,6 @@ describe('SamDebugConfigurationProvider', async function () {
462462
makeSampleSamTemplateYaml(true, { resourceName, runtime: 'moreLikeRanOutOfTime' }),
463463
tempFile.fsPath
464464
)
465-
await (await globals.templateRegistry).addItem(tempFile)
466465
await assert.rejects(() =>
467466
debugConfigProvider.makeConfig(undefined, {
468467
type: AWS_SAM_DEBUG_TYPE,
@@ -853,7 +852,6 @@ describe('SamDebugConfigurationProvider', async function () {
853852
},
854853
}
855854
const templatePath = vscode.Uri.file(path.join(appDir, 'template.yaml'))
856-
await (await globals.templateRegistry).addItem(templatePath)
857855
const actual = (await debugConfigProvider.makeConfig(folder, input))!
858856

859857
const expected: SamLaunchRequestArgs = {
@@ -980,7 +978,6 @@ describe('SamDebugConfigurationProvider', async function () {
980978
},
981979
}
982980
const templatePath = vscode.Uri.file(path.join(appDir, 'template.yaml'))
983-
await (await globals.templateRegistry).addItem(templatePath)
984981
const actual = (await debugConfigProvider.makeConfig(folder, input))!
985982

986983
const expected: SamLaunchRequestArgs = {
@@ -1119,7 +1116,6 @@ describe('SamDebugConfigurationProvider', async function () {
11191116
},
11201117
}
11211118
const templatePath = vscode.Uri.file(path.join(appDir, 'template.yaml'))
1122-
await (await globals.templateRegistry).addItem(templatePath)
11231119
const actual = (await debugConfigProvider.makeConfig(folder, input))!
11241120

11251121
const expected: SamLaunchRequestArgs = {
@@ -1415,7 +1411,6 @@ describe('SamDebugConfigurationProvider', async function () {
14151411
},
14161412
}
14171413
const templatePath = vscode.Uri.file(path.join(appDir, 'template.yaml'))
1418-
await (await globals.templateRegistry).addItem(templatePath)
14191414
const actual = (await debugConfigProvider.makeConfig(folder, input))! as SamLaunchRequestArgs
14201415
const expectedCodeRoot = (actual.baseBuildDir ?? 'fail') + '/input'
14211416
const expected: SamLaunchRequestArgs = {
@@ -1514,7 +1509,6 @@ describe('SamDebugConfigurationProvider', async function () {
15141509
},
15151510
}
15161511
const templatePath = vscode.Uri.file(path.join(appDir, 'template.yaml'))
1517-
await (await globals.templateRegistry).addItem(templatePath)
15181512
const actual = (await debugConfigProvider.makeConfig(folder, input))! as SamLaunchRequestArgs
15191513
const expectedCodeRoot = (actual.baseBuildDir ?? 'fail') + '/input'
15201514
const expected: SamLaunchRequestArgs = {
@@ -1778,7 +1772,6 @@ describe('SamDebugConfigurationProvider', async function () {
17781772
},
17791773
}
17801774
const templatePath = vscode.Uri.file(path.join(appDir, 'template.yaml'))
1781-
await (await globals.templateRegistry).addItem(templatePath)
17821775
const actual = (await debugConfigProvider.makeConfig(folder, input))! as SamLaunchRequestArgs
17831776
const codeRoot = `${appDir}/src/HelloWorld`
17841777
const expectedCodeRoot = (actual.baseBuildDir ?? 'fail') + '/input'
@@ -1933,7 +1926,6 @@ describe('SamDebugConfigurationProvider', async function () {
19331926
},
19341927
}
19351928
const templatePath = vscode.Uri.file(path.join(appDir, 'template.yaml'))
1936-
await (await globals.templateRegistry).addItem(templatePath)
19371929
const actual = (await debugConfigProvider.makeConfig(folder, input))! as SamLaunchRequestArgs
19381930
const codeRoot = `${appDir}/src/HelloWorld`
19391931
const expectedCodeRoot = (actual.baseBuildDir ?? 'fail') + '/input'
@@ -2242,7 +2234,6 @@ describe('SamDebugConfigurationProvider', async function () {
22422234
},
22432235
}
22442236
const templatePath = vscode.Uri.file(path.join(appDir, 'python3.7-plain-sam-app/template.yaml'))
2245-
await (await globals.templateRegistry).addItem(templatePath)
22462237

22472238
// Invoke with noDebug=false (the default).
22482239
const actual = (await debugConfigProvider.makeConfig(folder, input))!
@@ -2368,7 +2359,6 @@ describe('SamDebugConfigurationProvider', async function () {
23682359
},
23692360
}
23702361
const templatePath = vscode.Uri.file(path.join(appDir, 'python3.7-plain-sam-app/template.yaml'))
2371-
await (await globals.templateRegistry).addItem(templatePath)
23722362

23732363
// Invoke with noDebug=false (the default).
23742364
const actual = (await debugConfigProvider.makeConfig(folder, input))!
@@ -2460,7 +2450,6 @@ describe('SamDebugConfigurationProvider', async function () {
24602450
},
24612451
}
24622452
const templatePath = vscode.Uri.file(path.join(appDir, 'python3.7-image-sam-app/template.yaml'))
2463-
await (await globals.templateRegistry).addItem(templatePath)
24642453

24652454
// Invoke with noDebug=false (the default).
24662455
const actual = (await debugConfigProvider.makeConfig(folder, input))!
@@ -2739,7 +2728,6 @@ describe('SamDebugConfigurationProvider', async function () {
27392728
useIkpdb: true,
27402729
}
27412730
const templatePath = vscode.Uri.file(path.join(appDir, 'python3.7-plain-sam-app/template.yaml'))
2742-
await (await globals.templateRegistry).addItem(templatePath)
27432731

27442732
// Invoke with noDebug=false (the default).
27452733
const actual = (await debugConfigProvider.makeConfig(folder, input))!
@@ -2885,7 +2873,6 @@ describe('SamDebugConfigurationProvider', async function () {
28852873
}),
28862874
tempFile.fsPath
28872875
)
2888-
await (await globals.templateRegistry).addItem(tempFile)
28892876
const actual = (await debugConfigProviderMockCredentials.makeConfig(folder, input))!
28902877
const tempDir = path.dirname(actual.codeRoot)
28912878

0 commit comments

Comments
 (0)