@@ -17,8 +17,10 @@ import { AWS_SCHEME } from './constants'
17
17
import { writeFile } from 'fs-extra'
18
18
import { SystemUtilities } from './systemUtilities'
19
19
import { normalizeVSCodeUri } from './utilities/vsCodeUtils'
20
+ import { telemetry } from './telemetry/telemetry'
20
21
21
- const goformationManifestUrl = 'https://api.github.com/repos/awslabs/goformation/releases/latest'
22
+ // Note: this file is currently 12+ MB. When requesting it, specify compression/gzip.
23
+ export const samAndCfnSchemaUrl = 'https://raw.githubusercontent.com/aws/serverless-application-model/main/samtranslator/schema/schema.json'
22
24
const devfileManifestUrl = 'https://api.github.com/repos/devfile/api/releases/latest'
23
25
const schemaPrefix = `${ AWS_SCHEME } ://`
24
26
@@ -140,41 +142,41 @@ export class SchemaService {
140
142
* @param extensionContext VSCode extension context
141
143
*/
142
144
export async function getDefaultSchemas ( extensionContext : vscode . ExtensionContext ) : Promise < Schemas | undefined > {
143
- const cfnSchemaUri = vscode . Uri . joinPath ( extensionContext . globalStorageUri , 'cloudformation.schema.json' )
144
- const samSchemaUri = vscode . Uri . joinPath ( extensionContext . globalStorageUri , 'sam.schema.json' )
145
145
const devfileSchemaUri = vscode . Uri . joinPath ( extensionContext . globalStorageUri , 'devfile.schema.json' )
146
-
147
- const goformationSchemaVersion = await getPropertyFromJsonUrl ( goformationManifestUrl , 'tag_name' )
148
146
const devfileSchemaVersion = await getPropertyFromJsonUrl ( devfileManifestUrl , 'tag_name' )
149
147
148
+ // Sam schema is a superset of Cfn schema, so we can use it for both
149
+ const samAndCfnSchemaDestinationUri = vscode . Uri . joinPath ( extensionContext . globalStorageUri , 'sam.schema.json' )
150
+ const samAndCfnCacheKey = 'samAndCfnSchemaVersion'
151
+
150
152
const schemas : Schemas = { }
151
153
152
154
try {
153
- await updateSchemaFromRemote ( {
154
- destination : cfnSchemaUri ,
155
- version : goformationSchemaVersion ,
156
- url : `https://raw.githubusercontent.com/awslabs/goformation/ ${ goformationSchemaVersion } /schema/cloudformation.schema.json` ,
157
- cacheKey : 'cfnSchemaVersion' ,
155
+ await updateSchemaFromRemoteETag ( {
156
+ destination : samAndCfnSchemaDestinationUri ,
157
+ eTag : undefined ,
158
+ url : samAndCfnSchemaUrl ,
159
+ cacheKey : samAndCfnCacheKey ,
158
160
extensionContext,
159
161
title : schemaPrefix + 'cloudformation.schema.json' ,
160
162
} )
161
- schemas [ 'cfn' ] = cfnSchemaUri
163
+ schemas [ 'cfn' ] = samAndCfnSchemaDestinationUri
162
164
} catch ( e ) {
163
- getLogger ( ) . verbose ( 'Could not download cfn schema: %s' , ( e as Error ) . message )
165
+ getLogger ( ) . verbose ( 'Could not download sam/ cfn schema: %s' , ( e as Error ) . message )
164
166
}
165
167
166
168
try {
167
- await updateSchemaFromRemote ( {
168
- destination : samSchemaUri ,
169
- version : goformationSchemaVersion ,
170
- url : `https://raw.githubusercontent.com/awslabs/goformation/ ${ goformationSchemaVersion } /schema/sam.schema.json` ,
171
- cacheKey : 'samSchemaVersion' ,
169
+ await updateSchemaFromRemoteETag ( {
170
+ destination : samAndCfnSchemaDestinationUri ,
171
+ eTag : undefined ,
172
+ url : samAndCfnSchemaUrl ,
173
+ cacheKey : samAndCfnCacheKey ,
172
174
extensionContext,
173
175
title : schemaPrefix + 'sam.schema.json' ,
174
176
} )
175
- schemas [ 'sam' ] = samSchemaUri
177
+ schemas [ 'sam' ] = samAndCfnSchemaDestinationUri
176
178
} catch ( e ) {
177
- getLogger ( ) . verbose ( 'Could not download sam schema: %s' , ( e as Error ) . message )
179
+ getLogger ( ) . verbose ( 'Could not download sam/cfn schema: %s' , ( e as Error ) . message )
178
180
}
179
181
180
182
try {
@@ -231,13 +233,7 @@ export async function updateSchemaFromRemote(params: {
231
233
throw new Error ( `failed to resolve schema: ${ params . destination } ` )
232
234
}
233
235
234
- const parsedFile = { ...JSON . parse ( content ) , title : params . title }
235
- const dir = vscode . Uri . joinPath ( params . destination , '..' )
236
- await SystemUtilities . createDirectory ( dir )
237
- await writeFile ( params . destination . fsPath , JSON . stringify ( parsedFile ) )
238
- await params . extensionContext . globalState . update ( params . cacheKey , params . version ) . then ( undefined , err => {
239
- getLogger ( ) . warn ( `schemas: failed to update cache key for "${ params . title } ": ${ err ?. message } ` )
240
- } )
236
+ await doCacheContent ( content , params )
241
237
} catch ( err ) {
242
238
if ( cachedContent ) {
243
239
getLogger ( ) . warn (
@@ -251,6 +247,93 @@ export async function updateSchemaFromRemote(params: {
251
247
}
252
248
}
253
249
250
+ /**
251
+ * Fetches url content and caches locally. Uses E-Tag to determine if cached
252
+ * content can be used.
253
+ * @param params.destination Path to local file
254
+ * @param params.eTag E-Tag to send with fetch request. If this matches the url's it means we can use our cache.
255
+ * @param params.url Url to fetch from
256
+ * @param params.cacheKey Cache key to check version against
257
+ * @param params.extensionContext VSCode extension context
258
+ */
259
+ export async function updateSchemaFromRemoteETag ( params : {
260
+ destination : vscode . Uri
261
+ eTag ?: string
262
+ url : string
263
+ cacheKey : string
264
+ extensionContext : vscode . ExtensionContext
265
+ title : string
266
+ } ) : Promise < void > {
267
+ const cachedETag = params . extensionContext . globalState . get < string > ( params . cacheKey )
268
+
269
+ // Check that the cached file actually can be fetched. Else we might
270
+ // never update the cache.
271
+ const fileFetcher = new FileResourceFetcher ( params . destination . fsPath )
272
+ const cachedContent = await fileFetcher . get ( )
273
+
274
+ // Determine if existing cached content is sufficient
275
+ const needsUpdate = cachedETag === undefined || cachedETag !== params . eTag
276
+ const hasCachedContent = cachedContent !== undefined
277
+ if ( hasCachedContent && ! needsUpdate ) {
278
+ return
279
+ }
280
+
281
+ try {
282
+ // Only use our cached E-Tag if we have it + cached content
283
+ const eTagToRequest = ! ! cachedETag && cachedContent !== undefined ? cachedETag : params . eTag
284
+
285
+ const httpFetcher = new HttpResourceFetcher ( params . url , { showUrl : true } )
286
+ const response = await httpFetcher . getNewETagContent ( eTagToRequest )
287
+ const { content, eTag : latestETag } = response
288
+ if ( content === undefined ) {
289
+ // Our cached content is the latest
290
+ telemetry . toolkit_getExternalResource . emit ( {
291
+ url : params . url ,
292
+ passive : true ,
293
+ result : 'Cancelled' ,
294
+ reason : 'Cache hit' ,
295
+ } )
296
+ return
297
+ }
298
+ telemetry . toolkit_getExternalResource . emit ( { url : params . url , passive : true , result : 'Succeeded' } )
299
+ await doCacheContent ( content , { ...params , version : latestETag } )
300
+ } catch ( err ) {
301
+ if ( cachedContent ) {
302
+ getLogger ( ) . warn (
303
+ `schemas: Using cached schema instead since failed to fetch the latest version for "${ params . title } ": %s` ,
304
+ err
305
+ )
306
+ } else {
307
+ throw err
308
+ }
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Cache content to our extension context
314
+ * @param content
315
+ * @param params.version An identifier for a version of the resource. Can include an E-Tag value
316
+ */
317
+ async function doCacheContent (
318
+ content : string ,
319
+ params : {
320
+ destination : vscode . Uri
321
+ version ?: string
322
+ url : string
323
+ cacheKey : string
324
+ extensionContext : vscode . ExtensionContext
325
+ title : string
326
+ }
327
+ ) : Promise < void > {
328
+ const parsedFile = { ...JSON . parse ( content ) , title : params . title }
329
+ const dir = vscode . Uri . joinPath ( params . destination , '..' )
330
+ await SystemUtilities . createDirectory ( dir )
331
+ await writeFile ( params . destination . fsPath , JSON . stringify ( parsedFile ) )
332
+ await params . extensionContext . globalState . update ( params . cacheKey , params . version ) . then ( undefined , err => {
333
+ getLogger ( ) . warn ( `schemas: failed to update cache key for "${ params . title } ": ${ err ?. message } ` )
334
+ } )
335
+ }
336
+
254
337
/**
255
338
* Adds custom tags to the YAML extension's settings in order to hide error
256
339
* notifications for SAM/CFN intrinsic functions if a user has the YAML extension.
0 commit comments