Skip to content

Commit 588d0e6

Browse files
committed
feat(cloudformation): enforce s3 upload for large templates and artifacts
1 parent 4d40aab commit 588d0e6

3 files changed

Lines changed: 118 additions & 20 deletions

File tree

packages/core/src/awsService/cloudformation/commands/cfnCommands.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,15 @@ type UserInputtedTemplateParameters = {
402402
s3Key?: string
403403
}
404404

405+
async function promptForS3Key(templateUri: string): Promise<string | undefined> {
406+
const fileName = templateUri.split('/').pop()
407+
const timestamp = Date.now()
408+
const defaultKey = fileName
409+
? `${fileName.split('.')[0]}-${timestamp}.${fileName.split('.').pop()}`
410+
: `template-${timestamp}.yaml`
411+
return getS3Key(defaultKey)
412+
}
413+
405414
async function changeSetSteps(
406415
client: LanguageClient,
407416
documentManager: DocumentManager,
@@ -432,31 +441,38 @@ async function changeSetSteps(
432441
// Ask user if they want to upload to S3
433442
let s3Bucket: string | undefined
434443
let s3Key: string | undefined
435-
const uploadChoice = await shouldUploadToS3()
436-
if (uploadChoice === undefined) {
437-
return // User chose to configure settings, exit command
438-
}
439-
if (uploadChoice) {
440-
s3Bucket = await getS3Bucket()
444+
const templateExceedsSizeLimit = documentManager.requiresS3Upload(templateUri)
445+
446+
if (templateExceedsSizeLimit || hasArtifacts) {
447+
const reason = templateExceedsSizeLimit
448+
? 'S3 bucket is required because template exceeds the 51,200 byte CloudFormation limit'
449+
: 'S3 bucket is required because template contains artifacts that need to be uploaded to S3'
450+
s3Bucket = await getS3Bucket(reason)
441451
if (!s3Bucket) {
442452
return
443453
}
444454

445-
const fileName = templateUri.split('/').pop()
446-
const timestamp = Date.now()
447-
const fileNameWithTimestamp = fileName
448-
? `${fileName.split('.')[0]}-${timestamp}.${fileName.split('.').pop()}`
449-
: `template-${timestamp}.yaml`
450-
s3Key = await getS3Key(fileNameWithTimestamp)
451-
if (!s3Key) {
452-
return
455+
if (templateExceedsSizeLimit) {
456+
s3Key = await promptForS3Key(templateUri)
457+
if (!s3Key) {
458+
return
459+
}
453460
}
454-
} else if (hasArtifacts) {
455-
s3Bucket = await getS3Bucket(
456-
'S3 bucket is required because template contains artifacts that need to be uploaded to S3'
457-
)
458-
if (!s3Bucket) {
459-
return
461+
} else {
462+
const uploadChoice = await shouldUploadToS3()
463+
if (uploadChoice === undefined) {
464+
return // User chose to configure settings, exit command
465+
}
466+
if (uploadChoice) {
467+
s3Bucket = await getS3Bucket()
468+
if (!s3Bucket) {
469+
return
470+
}
471+
472+
s3Key = await promptForS3Key(templateUri)
473+
if (!s3Key) {
474+
return
475+
}
460476
}
461477
}
462478

packages/core/src/awsService/cloudformation/documents/documentManager.ts

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

66
import { NotificationType } from 'vscode-languageserver-protocol'
77
import { LanguageClient } from 'vscode-languageclient/node'
8+
import { getLogger } from '../../../shared/logger/logger'
89

910
export type DocumentMetadata = {
1011
uri: string
@@ -15,13 +16,15 @@ export type DocumentMetadata = {
1516
languageId: string
1617
version: number
1718
lineCount: number
19+
sizeBytes: number
1820
}
1921

2022
const DocumentsMetadataNotification = new NotificationType<DocumentMetadata[]>('aws/documents/metadata')
2123

2224
type DocumentsChangeListener = (documents: DocumentMetadata[]) => void
2325

2426
export class DocumentManager {
27+
private static readonly cfnTemplateBodyMaxBytes = 51_200
2528
private documents: DocumentMetadata[] = []
2629
private readonly listeners: DocumentsChangeListener[] = []
2730

@@ -41,4 +44,15 @@ export class DocumentManager {
4144
get() {
4245
return [...this.documents]
4346
}
47+
48+
requiresS3Upload(uri: string): boolean {
49+
const doc = this.documents.find((d) => d.uri === uri)
50+
if (!doc) {
51+
getLogger('awsCfnLsp').warn(
52+
`Document metadata not found for URI: ${uri}. Assuming no s3 upload required may lead to deployment failure.`
53+
)
54+
return false
55+
}
56+
return doc.sizeBytes > DocumentManager.cfnTemplateBodyMaxBytes
57+
}
4458
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import assert from 'assert'
7+
import * as sinon from 'sinon'
8+
import { DocumentManager, DocumentMetadata } from '../../../../awsService/cloudformation/documents/documentManager'
9+
import { LanguageClient } from 'vscode-languageclient/node'
10+
11+
function createMetadata(overrides: Partial<DocumentMetadata> = {}): DocumentMetadata {
12+
return {
13+
uri: 'file:///template.yaml',
14+
fileName: 'template.yaml',
15+
ext: 'yaml',
16+
type: 'yaml',
17+
cfnType: 'template',
18+
languageId: 'yaml',
19+
version: 1,
20+
lineCount: 10,
21+
sizeBytes: 100,
22+
...overrides,
23+
}
24+
}
25+
26+
describe('DocumentManager', function () {
27+
let sandbox: sinon.SinonSandbox
28+
let documentManager: DocumentManager
29+
let notificationCallback: (docs: DocumentMetadata[]) => void
30+
31+
beforeEach(function () {
32+
sandbox = sinon.createSandbox()
33+
const fakeClient = {
34+
onNotification: (_type: unknown, callback: (docs: DocumentMetadata[]) => void) => {
35+
notificationCallback = callback
36+
},
37+
} as unknown as LanguageClient
38+
documentManager = new DocumentManager(fakeClient)
39+
})
40+
41+
afterEach(function () {
42+
sandbox.restore()
43+
})
44+
45+
describe('requiresS3Upload', function () {
46+
it('returns false when document is not found', function () {
47+
notificationCallback([createMetadata()])
48+
49+
assert.strictEqual(documentManager.requiresS3Upload('file:///nonexistent.yaml'), false)
50+
})
51+
52+
it('returns false when template size is at the limit', function () {
53+
notificationCallback([createMetadata({ sizeBytes: 51_200 })])
54+
55+
assert.strictEqual(documentManager.requiresS3Upload('file:///template.yaml'), false)
56+
})
57+
58+
it('returns true when template size exceeds the limit', function () {
59+
notificationCallback([createMetadata({ sizeBytes: 51_201 })])
60+
61+
assert.strictEqual(documentManager.requiresS3Upload('file:///template.yaml'), true)
62+
})
63+
64+
it('returns false when no documents have been received', function () {
65+
assert.strictEqual(documentManager.requiresS3Upload('file:///template.yaml'), false)
66+
})
67+
})
68+
})

0 commit comments

Comments
 (0)