Skip to content

Commit a192f04

Browse files
authored
New serverless application schemas support (#897)
This integrates Schema Code Download with new serverless application model (SAM) templates that expect the schema - essentially making dynamic changes This introduces support for 2 new EventBridge SAM templates (one static, one dynamic). The static one can be considered as "hello world" template - showing how to react to EC2 instance state change notifications with a minimal EventBridge template and Lambda as a Target. The dynamic one is interesting. The Lambda functions in these templates expect to find code that is generated by the EventBridge Schema service, so the flow is to run the standard SAM application creation flow, then download the code as a post-creation action. These templates can also work from SAM CLI interactive mode where users are prompted for a schema selection.
1 parent 8f35824 commit a192f04

25 files changed

+2105
-142
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Breaking Change",
3+
"description": "Minimum version of SAM CLI has been adjusted from 0.32.0 to 0.38.0 to accommodate new SAM application support for EventBridge Schemas"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Added the ability to create new Serverless Applications with EventBridge Schemas support."
4+
}

package.nls.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@
199199
"AWS.samcli.configured.location": "Configured SAM CLI Location: {0}",
200200
"AWS.samcli.error.notFound": "Unable to find the SAM CLI, which is required to create new Serverless Applications and debug them locally. If you have already installed the SAM CLI, update your User Settings by locating it.",
201201
"AWS.samcli.error.notFound.brief": "Could not get SAM CLI location",
202+
"AWS.samcli.error.invalid_schema_support_version": "Installed SAM executable does not support templates that require Event Schema selection. Required minimum version {0}, but found {1}",
202203
"AWS.samcli.local.invoke.ended": "Local invoke of SAM Application has ended.",
203204
"AWS.samcli.local.invoke.error": "Error encountered running local SAM Application",
204205
"AWS.samcli.local.invoke.port.not.open": "The debug port doesn't appear to be open. The debugger might not succeed when attaching to your SAM Application.",
@@ -224,7 +225,21 @@
224225
"AWS.initWizard.name.browse.openLabel": "Open",
225226
"AWS.samcli.initWizard.name.error.empty": "Application name cannot be empty",
226227
"AWS.samcli.initWizard.name.error.pathSep": "The path separator ({0}) is not allowed in application names",
228+
"AWS.samcli.initWizard.schemas.aws_credentials_missing": "You need to be connected to AWS to select {0}.",
227229
"AWS.samcli.initWizard.runtime.prompt": "Select a SAM Application Runtime",
230+
"AWS.samcli.initWizard.template.prompt": "Select a SAM Application Template",
231+
"AWS.samcli.initWizard.template.helloWorld.name": "AWS SAM Hello World",
232+
"AWS.samcli.initWizard.template.eventBridge_helloWorld.name": "AWS SAM EventBridge Hello World",
233+
"AWS.samcli.initWizard.template.eventBridge_starterApp.name": "AWS SAM EventBridge App from Scratch",
234+
"AWS.samcli.initWizard.template.helloWorld.description": "A basic SAM app",
235+
"AWS.samcli.initWizard.template.eventBridge_helloWorld.description": "A Hello World app for Amazon EventBridge that invokes a Lambda for every EC2 instance state change in your account",
236+
"AWS.samcli.initWizard.template.eventBridge_starterApp.description": "A starter app for Amazon EventBridge that invokes a Lambda based on a dynamic event trigger for an EventBridge Schema of your choice",
237+
"AWS.samcli.initWizard.schemas.region.prompt": "Select an EventBridge Schemas Region",
238+
"AWS.samcli.initWizard.schemas.registry.prompt": "Select a Registry",
239+
"AWS.samcli.initWizard.schemas.registry.failed_to_load_resources": "Error loading registries.",
240+
"AWS.samcli.initWizard.schemas.schema.prompt": "Select a Schema",
241+
"AWS.samcli.initWizard.schemas.notFound": "No schemas found in registry {0}.",
242+
"AWS.samcli.initWizard.schemas.failed_to_load_resources": "Error loading schemas in registry {0}.",
228243
"AWS.samcli.initWizard.source.error.notFound": "Project created successfully, but main source code file not found: {0}",
229244
"AWS.samcli.initWizard.source.error.notInWorkspace": "Could not open file '{0}'. If this file exists on disk, try adding it to your workspace.",
230245
"AWS.wizard.selectedPreviously": "Selected Previously",

src/eventSchemas/commands/downloadSchemaItemCode.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function getCoreFileName(schemaName: string, fileExtension: string) {
100100
return parsedName[parsedName.length - 1].concat(fileExtension)
101101
}
102102

103-
function createSchemaCodeDownloaderObject(client: SchemaClient): SchemaCodeDownloader {
103+
export function createSchemaCodeDownloaderObject(client: SchemaClient): SchemaCodeDownloader {
104104
const downloader = new CodeDownloader(client)
105105
const generator = new CodeGenerator(client)
106106
const poller = new CodeGenerationStatusPoller(client)
@@ -115,7 +115,7 @@ export interface SchemaCodeDownloadRequestDetails {
115115
language: string
116116
schemaVersion: string
117117
destinationDirectory: vscode.Uri
118-
schemaCoreCodeFileName: string
118+
schemaCoreCodeFileName?: string
119119
}
120120

121121
export class SchemaCodeDownloader {
@@ -342,16 +342,18 @@ export class CodeExtractor {
342342
})
343343
}
344344

345-
public getCoreCodeFilePath(codeZipFile: string, coreFileName: string): string | undefined {
346-
const zip = new admZip(codeZipFile)
347-
const zipEntries = zip.getEntries()
348-
349-
for (const zipEntry of zipEntries) {
350-
if (zipEntry.isDirectory) {
351-
// Ignore directories
352-
} else {
353-
if (zipEntry.name === coreFileName) {
354-
return zipEntry.entryName
345+
public getCoreCodeFilePath(codeZipFile: string, coreFileName: string | undefined): string | undefined {
346+
if (coreFileName) {
347+
const zip = new admZip(codeZipFile)
348+
const zipEntries = zip.getEntries()
349+
350+
for (const zipEntry of zipEntries) {
351+
if (zipEntry.isDirectory) {
352+
// Ignore directories
353+
} else {
354+
if (zipEntry.name === coreFileName) {
355+
return zipEntry.entryName
356+
}
355357
}
356358
}
357359
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*!
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
// TODO: This is fragile. Very fragile. But it is necessary to get Schemas service launched, and we've evaluated all other tradeoffs
7+
// This will be done on the server-side as soon as we can, but for now the client needs to do this
8+
export class SchemaCodeGenUtils {
9+
private readonly SCHEMA_PACKAGE_PREFIX = 'schema'
10+
private readonly AWS = 'aws'
11+
private readonly PARTNER = 'partner'
12+
private readonly AWS_PARTNER_PREFIX = `${this.AWS}.${this.PARTNER}-` // dash suffix because of 3p partner registry name format
13+
private readonly AWS_EVENTS_PREFIX = `${this.AWS}.` // . suffix because of 1p event registry schema format
14+
15+
public buildSchemaPackageName(schemaName: string): string {
16+
const builder = new CodeGenPackageBuilder()
17+
builder.append(this.SCHEMA_PACKAGE_PREFIX)
18+
this.buildPackageName(builder, schemaName)
19+
20+
return builder.build()
21+
}
22+
23+
private buildPackageName(builder: CodeGenPackageBuilder, schemaName: string): void {
24+
// do not modify the order of conditional checks
25+
if (this.isAwsPartnerEvent(schemaName)) {
26+
this.buildPartnerEventPackageName(builder, schemaName)
27+
} else if (this.isAwsEvent(schemaName)) {
28+
this.buildAwsEventPackageName(builder, schemaName)
29+
} else {
30+
this.buildCustomPackageName(builder, schemaName)
31+
}
32+
}
33+
34+
private isAwsPartnerEvent(schemaName: string): boolean {
35+
return schemaName.startsWith(this.AWS_PARTNER_PREFIX)
36+
}
37+
38+
private buildPartnerEventPackageName(builder: CodeGenPackageBuilder, schemaName: string): void {
39+
const partnerSchemaString = schemaName.substring(this.AWS_PARTNER_PREFIX.length)
40+
41+
builder
42+
.append(this.AWS)
43+
.append(this.PARTNER)
44+
.append(partnerSchemaString)
45+
}
46+
47+
private isAwsEvent(name: string): boolean {
48+
return name.startsWith(this.AWS_EVENTS_PREFIX)
49+
}
50+
51+
private buildAwsEventPackageName(builder: CodeGenPackageBuilder, schemaName: string): void {
52+
const awsEventSchemaParts = schemaName.split('.')
53+
for (const part of awsEventSchemaParts) {
54+
builder.append(part)
55+
}
56+
}
57+
58+
private buildCustomPackageName(builder: CodeGenPackageBuilder, schemaName: string): void {
59+
builder.append(schemaName)
60+
}
61+
}
62+
63+
class CodeGenPackageBuilder {
64+
private builder = ''
65+
public build(): string {
66+
return this.builder
67+
}
68+
69+
public append(segment: String): CodeGenPackageBuilder {
70+
if (this.builder.length > 0) {
71+
this.builder = this.builder.concat(IdentifierFormatter.PACKAGE_SEPARATOR)
72+
}
73+
this.builder = this.builder.concat(IdentifierFormatter.toValidIdentifier(segment.toLowerCase()))
74+
75+
return this
76+
}
77+
}
78+
79+
export namespace IdentifierFormatter {
80+
export const PACKAGE_SEPARATOR = '.'
81+
const POTENTIAL_PACKAGE_SEPARATOR = '@'
82+
const NOT_VALID_IDENTIFIER_REGEX = new RegExp(`[^a-zA-Z0-9_${POTENTIAL_PACKAGE_SEPARATOR}]`, 'g')
83+
const POTENTIAL_PACKAGE_SEPARATOR_REGEX = new RegExp(POTENTIAL_PACKAGE_SEPARATOR, 'g')
84+
const UNDERSCORE = '_'
85+
86+
export function toValidIdentifier(name: string): string {
87+
return name
88+
.replace(NOT_VALID_IDENTIFIER_REGEX, UNDERSCORE)
89+
.replace(POTENTIAL_PACKAGE_SEPARATOR_REGEX, PACKAGE_SEPARATOR)
90+
}
91+
}

src/eventSchemas/models/schemaCodeLangs.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import { Runtime } from 'aws-sdk/clients/lambda'
67
import { Set } from 'immutable'
78

89
export const JAVA = 'Java 8+'
@@ -44,3 +45,15 @@ export function getLanguageDetails(
4445
throw new Error(`Language ${language} is not supported as Schema Code Language`)
4546
}
4647
}
48+
49+
export function supportsEventBridgeTemplates(runtime: Runtime): boolean {
50+
return runtime === 'python3.7' || runtime === 'python3.6' || runtime === 'python3.8'
51+
}
52+
53+
export function getApiValueForSchemasDownload(runtime: Runtime): string {
54+
if (supportsEventBridgeTemplates(runtime)) {
55+
return 'Python36'
56+
}
57+
58+
throw new Error(`Runtime ${runtime} is not supported by eventBridge application`)
59+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*!
2+
* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { Credentials, Schemas } from 'aws-sdk'
7+
import { SchemaClient } from '../../shared/clients/schemaClient'
8+
import { getLogger, Logger } from '../../shared/logger'
9+
import { toArrayAsync } from '../../shared/utilities/collectionUtils'
10+
11+
export class Cache {
12+
public constructor(public readonly credentialsRegionDataList: credentialsRegionDataListMap[]) {}
13+
}
14+
15+
export interface credentialsRegionDataListMap {
16+
credentials: Credentials
17+
regionDataList: regionRegistryMap[]
18+
}
19+
20+
export interface regionRegistryMap {
21+
region: string
22+
registryNames: string[]
23+
registrySchemasMapList: registrySchemasMap[]
24+
}
25+
26+
export interface registrySchemasMap {
27+
registryName: string
28+
schemaList: Schemas.SchemaSummary[]
29+
}
30+
31+
/**
32+
* Responsible for retaining registry && schema list per region for Create-New-SAM-Application wizard
33+
*/
34+
export class SchemasDataProvider {
35+
private static INSTANCE: SchemasDataProvider | undefined
36+
private readonly logger: Logger = getLogger()
37+
38+
public constructor(private readonly cache: Cache) {}
39+
40+
public async getRegistries(region: string, client: SchemaClient, credentials: Credentials) {
41+
const cachedRegion = this.cache.credentialsRegionDataList
42+
.filter(x => x.credentials === credentials)
43+
.shift()
44+
?.regionDataList.filter(x => x.region === region)
45+
.shift()
46+
47+
try {
48+
// if region is not cached, make api query and retain results
49+
if (!cachedRegion || cachedRegion.registryNames.length === 0) {
50+
const registrySummary = await toArrayAsync(client.listRegistries())
51+
const registryNames = registrySummary.map(x => x.RegistryName!)
52+
this.pushRegionDataIntoCache(region, registryNames, [], credentials)
53+
54+
return registryNames
55+
}
56+
} catch (err) {
57+
const error = err as Error
58+
this.logger.error('Error retrieving registries', error)
59+
60+
return undefined
61+
}
62+
63+
return cachedRegion!.registryNames
64+
}
65+
66+
public async getSchemas(region: string, registryName: string, client: SchemaClient, credentials: Credentials) {
67+
const registrySchemasMapList = this.cache.credentialsRegionDataList
68+
.filter(x => x.credentials === credentials)
69+
.shift()
70+
?.regionDataList.filter(x => x.region === region)
71+
.shift()?.registrySchemasMapList
72+
let schemas = registrySchemasMapList?.filter(x => x.registryName === registryName).shift()?.schemaList
73+
try {
74+
// if no schemas found, make api query and retain results given that registryName && region already cached
75+
if (!schemas || schemas.length === 0) {
76+
schemas = await toArrayAsync(client.listSchemas(registryName))
77+
const singleItem: registrySchemasMap = { registryName: registryName, schemaList: schemas }
78+
//wizard setup always calls getRegistries method prior to getSchemas, so this shouldn't be undefined
79+
if (!registrySchemasMapList) {
80+
this.pushRegionDataIntoCache(region, [], [singleItem], credentials)
81+
}
82+
83+
if (registrySchemasMapList) {
84+
registrySchemasMapList.push(singleItem)
85+
}
86+
}
87+
} catch (err) {
88+
const error = err as Error
89+
this.logger.error('Error retrieving schemas', error)
90+
91+
return undefined
92+
}
93+
94+
return schemas
95+
}
96+
97+
private pushRegionDataIntoCache(
98+
region: string,
99+
registryNames: string[],
100+
registrySchemasMapList: registrySchemasMap[],
101+
credentials?: Credentials
102+
): void {
103+
const regionData: regionRegistryMap = {
104+
region: region,
105+
registryNames: registryNames,
106+
registrySchemasMapList: registrySchemasMapList
107+
}
108+
109+
const cachedCredential = this.cache.credentialsRegionDataList.filter(x => x.credentials === credentials).shift()
110+
cachedCredential?.regionDataList.push(regionData)
111+
112+
if (!cachedCredential) {
113+
const regionDataWithCredentials: credentialsRegionDataListMap = {
114+
credentials: credentials!,
115+
regionDataList: [regionData]
116+
}
117+
this.cache.credentialsRegionDataList.push(regionDataWithCredentials)
118+
}
119+
}
120+
121+
public static getInstance(): SchemasDataProvider {
122+
if (!SchemasDataProvider.INSTANCE) {
123+
SchemasDataProvider.INSTANCE = new SchemasDataProvider(new Cache([]))
124+
}
125+
126+
return SchemasDataProvider.INSTANCE
127+
}
128+
}

0 commit comments

Comments
 (0)