Skip to content

Commit f74e2c8

Browse files
author
Connor Robertson
authored
Merge branch 'develop' into tmp/1698101531/main
2 parents d3b1823 + a2be62d commit f74e2c8

25 files changed

+4070
-493
lines changed

integration/combination/test_connectors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def tearDown(self):
6262
("combination/connector_appsync_api_to_lambda",),
6363
("combination/connector_appsync_to_lambda",),
6464
("combination/connector_appsync_to_table",),
65+
("combination/connector_appsync_to_eventbus",),
6566
("combination/connector_function_to_function",),
6667
("combination/connector_restapi_to_function",),
6768
("combination/connector_httpapi_to_function",),
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[
2+
{
3+
"LogicalResourceId": "ApiKey",
4+
"ResourceType": "AWS::AppSync::ApiKey"
5+
},
6+
{
7+
"LogicalResourceId": "ApiSchema",
8+
"ResourceType": "AWS::AppSync::GraphQLSchema"
9+
},
10+
{
11+
"LogicalResourceId": "AppSyncApi",
12+
"ResourceType": "AWS::AppSync::GraphQLApi"
13+
},
14+
{
15+
"LogicalResourceId": "AppSyncEventBusDataSource",
16+
"ResourceType": "AWS::AppSync::DataSource"
17+
},
18+
{
19+
"LogicalResourceId": "AppSyncSayHelloResolver",
20+
"ResourceType": "AWS::AppSync::Resolver"
21+
},
22+
{
23+
"LogicalResourceId": "ConnectorPolicy",
24+
"ResourceType": "AWS::IAM::ManagedPolicy"
25+
},
26+
{
27+
"LogicalResourceId": "EventBridgeRole",
28+
"ResourceType": "AWS::IAM::Role"
29+
},
30+
{
31+
"LogicalResourceId": "EventBus",
32+
"ResourceType": "AWS::Events::EventBus"
33+
},
34+
{
35+
"LogicalResourceId": "TriggerFunction",
36+
"ResourceType": "AWS::Lambda::Function"
37+
}
38+
]
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
Resources:
2+
EventBus:
3+
Type: AWS::Events::EventBus
4+
Properties:
5+
Name: !Sub "${AWS::StackName}-EventBus"
6+
7+
EventBridgeRole:
8+
Type: AWS::IAM::Role
9+
Properties:
10+
AssumeRolePolicyDocument:
11+
Version: '2012-10-17'
12+
Statement:
13+
- Effect: Allow
14+
Action:
15+
- sts:AssumeRole
16+
Principal:
17+
Service:
18+
- appsync.amazonaws.com
19+
- lambda.amazonaws.com
20+
21+
AppSyncApi:
22+
Type: AWS::AppSync::GraphQLApi
23+
Properties:
24+
Name: AppSyncApi
25+
AuthenticationType: API_KEY
26+
27+
ApiSchema:
28+
Type: AWS::AppSync::GraphQLSchema
29+
Properties:
30+
ApiId: !GetAtt AppSyncApi.ApiId
31+
Definition: |
32+
type EntryDetails {
33+
ErrorCode: String
34+
ErrorMessage: String
35+
EventId: String!
36+
}
37+
38+
type PutEventsResult {
39+
Entries: [EntryDetails!]!
40+
FailedEntry: Int
41+
}
42+
43+
type Query {
44+
sayHello: PutEventsResult!
45+
}
46+
47+
schema {
48+
query: Query
49+
}
50+
51+
AppSyncEventBusDataSource:
52+
Type: AWS::AppSync::DataSource
53+
Properties:
54+
ApiId: !GetAtt AppSyncApi.ApiId
55+
Name: AppSyncEventBusDataSource
56+
Type: AMAZON_EVENTBRIDGE
57+
ServiceRoleArn: !GetAtt EventBridgeRole.Arn
58+
EventBridgeConfig:
59+
EventBusArn: !GetAtt 'EventBus.Arn'
60+
61+
AppSyncSayHelloResolver:
62+
DependsOn: ApiSchema
63+
Type: AWS::AppSync::Resolver
64+
Properties:
65+
ApiId: !GetAtt AppSyncApi.ApiId
66+
TypeName: Query
67+
FieldName: sayHello
68+
DataSourceName: !GetAtt AppSyncEventBusDataSource.Name
69+
Runtime:
70+
Name: APPSYNC_JS
71+
RuntimeVersion: 1.0.0
72+
Code: |
73+
import { util } from '@aws-appsync/utils';
74+
export function request(ctx) {
75+
return {
76+
"operation" : "PutEvents",
77+
"events" : [{
78+
"source": "com.mycompany.myapp",
79+
"detail": {
80+
"key1" : "value1",
81+
"key2" : "value2"
82+
},
83+
"resources": ["Resource1", "Resource2"],
84+
"detailType": "myDetailType"
85+
}]
86+
}
87+
}
88+
89+
export function response(ctx) {
90+
if(ctx.error)
91+
util.error(ctx.error.message, ctx.error.type, ctx.result)
92+
else
93+
return ctx.result
94+
}
95+
96+
Connector:
97+
Type: AWS::Serverless::Connector
98+
Properties:
99+
Source:
100+
Id: AppSyncEventBusDataSource
101+
Destination:
102+
Id: EventBus
103+
Permissions:
104+
- Write
105+
106+
ApiKey:
107+
Type: AWS::AppSync::ApiKey
108+
Properties:
109+
ApiId: !GetAtt AppSyncApi.ApiId
110+
111+
TriggerFunction:
112+
Type: AWS::Serverless::Function
113+
Properties:
114+
Role: !GetAtt EventBridgeRole.Arn
115+
Environment:
116+
Variables:
117+
API_KEY: !GetAtt ApiKey.ApiKey
118+
GRAPHQL_URL: !GetAtt AppSyncApi.GraphQLUrl
119+
EventBusName: !Ref EventBus
120+
Runtime: nodejs16.x
121+
Handler: index.handler
122+
InlineCode: |
123+
const https = require("https");
124+
125+
exports.handler = async () => {
126+
const queries = {
127+
sayHello: /* GraphQL */ `
128+
query {
129+
sayHello {
130+
Entries {
131+
ErrorCode
132+
EventId
133+
ErrorMessage
134+
}
135+
FailedEntry
136+
}
137+
}
138+
`,
139+
};
140+
141+
const fetch = async (url, options) =>
142+
new Promise((resolve, reject) => {
143+
const req = https.request(url, options, (res) => {
144+
const body = [];
145+
res.on("data", (chunk) => body.push(chunk));
146+
res.on("end", () => {
147+
const resString = Buffer.concat(body).toString();
148+
resolve(resString);
149+
});
150+
});
151+
req.on("error", (err) => {
152+
reject(err);
153+
});
154+
req.on("timeout", () => {
155+
req.destroy();
156+
reject(new Error("Request time out"));
157+
});
158+
req.write(options.body);
159+
req.end();
160+
});
161+
162+
const makeRequest = async (queryName) => {
163+
const options = {
164+
method: "POST",
165+
headers: {
166+
"x-api-key": process.env.API_KEY,
167+
},
168+
body: JSON.stringify({ query: queries[queryName] }),
169+
timeout: 600000, // ms
170+
};
171+
172+
const response = await fetch(process.env.GRAPHQL_URL, options);
173+
let body = JSON.parse(response);
174+
const data = body.data?.[queryName];
175+
176+
if (body.errors !== undefined) {
177+
throw JSON.stringify(body.errors);
178+
}
179+
180+
if (data.FailedEntry != null || data.ErrorCode != null ) {
181+
throw new Error(
182+
`${queryName} error: failed to send event to eventbus ${process.env.EventBusName}`);
183+
}
184+
185+
return body.data;
186+
};
187+
188+
await makeRequest("sayHello");
189+
};
190+
191+
Metadata:
192+
SamTransformTest: true

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ filterwarnings =
2020
ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning
2121
# Pytest warnings
2222
ignore::pytest.PytestUnraisableExceptionWarning
23+
# https://github.com/urllib3/urllib3/blob/main/src/urllib3/poolmanager.py#L313
24+
ignore::DeprecationWarning:urllib3.*:

samtranslator/internal/schema_source/aws_serverless_function.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ class Properties(BaseModel):
626626
SnapStart: Optional[SnapStart] = prop("SnapStart")
627627
RuntimeManagementConfig: Optional[RuntimeManagementConfig] = prop("RuntimeManagementConfig")
628628
Tags: Optional[Tags] = prop("Tags")
629-
PropagateTags: Optional[bool] # TODO: add docs
629+
PropagateTags: Optional[bool] = prop("PropagateTags")
630630
Timeout: Optional[Timeout] = prop("Timeout")
631631
Tracing: Optional[Tracing] = prop("Tracing")
632632
VersionDescription: Optional[PassThroughProp] = prop("VersionDescription")
@@ -656,7 +656,7 @@ class Globals(BaseModel):
656656
["AWS::Lambda::Function", "Properties", "Environment"],
657657
)
658658
Tags: Optional[Tags] = prop("Tags")
659-
PropagateTags: Optional[bool] # TODO: add docs
659+
PropagateTags: Optional[bool] = prop("PropagateTags")
660660
Tracing: Optional[Tracing] = prop("Tracing")
661661
KmsKeyArn: Optional[KmsKeyArn] = prop("KmsKeyArn")
662662
Layers: Optional[Layers] = prop("Layers")

samtranslator/internal/schema_source/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,4 @@ class ResourceAttributes(BaseModel):
9999
Metadata: Optional[PassThroughProp]
100100
UpdateReplacePolicy: Optional[PassThroughProp]
101101
Condition: Optional[PassThroughProp]
102+
IgnoreGlobals: Optional[Union[str, List[str]]]

samtranslator/model/connector_profiles/profiles.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,23 @@
790790
}
791791
}
792792
}
793+
},
794+
"AWS::Events::EventBus": {
795+
"Type": "AWS_IAM_ROLE_MANAGED_POLICY",
796+
"Properties": {
797+
"SourcePolicy": true,
798+
"AccessCategories": {
799+
"Write": {
800+
"Statement": [
801+
{
802+
"Effect": "Allow",
803+
"Action": ["events:PutEvents"],
804+
"Resource": ["%{Destination.Arn}"]
805+
}
806+
]
807+
}
808+
}
809+
}
793810
}
794811
},
795812
"AWS::AppSync::GraphQLApi": {

samtranslator/open_api/open_api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,10 @@ def openapi(self) -> Dict[str, Any]:
518518

519519
if self.security_schemes:
520520
self._doc.setdefault("components", Py27Dict())
521+
if not self._doc["components"]:
522+
# explicitly set to dict to account for scenario where
523+
# 'components' is explicitly set to None
524+
self._doc["components"] = Py27Dict()
521525
self._doc["components"]["securitySchemes"] = self.security_schemes
522526

523527
if self.info:

samtranslator/plugins/globals/globals.py

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import Any, Dict, List
1+
import copy
2+
from typing import Any, Dict, List, Optional, Union
23

3-
from samtranslator.model.exceptions import ExceptionWithMessage
4+
from samtranslator.model.exceptions import ExceptionWithMessage, InvalidResourceAttributeTypeException
45
from samtranslator.public.intrinsics import is_intrinsics
56
from samtranslator.public.sdk.resource import SamResourceType
67
from samtranslator.swagger.swagger import SwaggerEditor
@@ -99,7 +100,7 @@ class Globals:
99100
SamResourceType.Function.value: ["RuntimeManagementConfig"],
100101
}
101102

102-
def __init__(self, template) -> None: # type: ignore[no-untyped-def]
103+
def __init__(self, template: Dict[str, Any]) -> None:
103104
"""
104105
Constructs an instance of this object
105106
@@ -111,12 +112,57 @@ def __init__(self, template) -> None: # type: ignore[no-untyped-def]
111112
# Sort the names for stability in list ordering
112113
self.supported_resource_section_names.sort()
113114

114-
self.template_globals = {}
115+
self.template_globals: Dict[str, GlobalProperties] = {}
115116

116117
if self._KEYWORD in template:
117118
self.template_globals = self._parse(template[self._KEYWORD]) # type: ignore[no-untyped-call]
118119

119-
def merge(self, resource_type, resource_properties): # type: ignore[no-untyped-def]
120+
def get_template_globals(
121+
self, logical_id: str, resource_type: str, ignore_globals: Optional[Union[str, List[str]]]
122+
) -> "GlobalProperties":
123+
"""
124+
Get template globals but remove globals based on IgnoreGlobals attribute.
125+
126+
:param string logical_id: LogicalId of the resource
127+
:param string resource_type: Type of the resource (Ex: AWS::Serverless::Function)
128+
:param dict ignore_globals: IgnoreGlobals resource attribute. It can be either 1) "*" string value
129+
or list of string value, each value should be a valid property in Globals section
130+
:return dict: processed template globals
131+
"""
132+
if not ignore_globals:
133+
return self.template_globals[resource_type]
134+
135+
if isinstance(ignore_globals, str) and ignore_globals == "*":
136+
return GlobalProperties({})
137+
138+
if isinstance(ignore_globals, list):
139+
global_props: GlobalProperties = copy.deepcopy(self.template_globals[resource_type])
140+
for key in ignore_globals:
141+
if key not in global_props.global_properties:
142+
raise InvalidResourceAttributeTypeException(
143+
logical_id,
144+
"IgnoreGlobals",
145+
None,
146+
f"Resource {logical_id} has invalid resource attribute 'IgnoreGlobals' on item '{key}'.",
147+
)
148+
del global_props.global_properties[key]
149+
return global_props
150+
151+
# We raise exception for any non "*" or non-list input
152+
raise InvalidResourceAttributeTypeException(
153+
logical_id,
154+
"IgnoreGlobals",
155+
None,
156+
f"Resource {logical_id} has invalid resource attribute 'IgnoreGlobals'.",
157+
)
158+
159+
def merge(
160+
self,
161+
resource_type: str,
162+
resource_properties: Dict[str, Any],
163+
logical_id: str = "",
164+
ignore_globals: Optional[Union[str, List[str]]] = None,
165+
) -> Any:
120166
"""
121167
Adds global properties to the resource, if necessary. This method is a no-op if there are no global properties
122168
for this resource type
@@ -130,12 +176,12 @@ def merge(self, resource_type, resource_properties): # type: ignore[no-untyped-
130176
# Nothing to do. Return the template unmodified
131177
return resource_properties
132178

133-
global_props = self.template_globals[resource_type]
179+
global_props = self.get_template_globals(logical_id, str(resource_type), ignore_globals)
134180

135-
return global_props.merge(resource_properties)
181+
return global_props.merge(resource_properties) # type: ignore[no-untyped-call]
136182

137183
@classmethod
138-
def del_section(cls, template): # type: ignore[no-untyped-def]
184+
def del_section(cls, template: Dict[str, Any]) -> None:
139185
"""
140186
Helper method to delete the Globals section altogether from the template
141187

0 commit comments

Comments
 (0)