Skip to content

Commit 353cf45

Browse files
authored
fix(sam): "Invalid (or missing) template file" #3946
2 parents 80817a2 + 9511474 commit 353cf45

11 files changed

+84
-72
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+
}
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": "`AWS: Add SAM Debug Configuration` command only works the first time it is invoked."
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/activation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export async function activate(ctx: ExtContext): Promise<void> {
7272
Commands.register('aws.addSamDebugConfig', async () => {
7373
if (!didActivateCodeLensProviders) {
7474
await activateSlowCodeLensesOnce()
75-
await samDebugConfigCmd()
7675
}
76+
await samDebugConfigCmd()
7777
})
7878

7979
ctx.extensionContext.subscriptions.push(

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
})

0 commit comments

Comments
 (0)