Showing
3 changed files
with
479 additions
and
1 deletions
... | @@ -26,6 +26,7 @@ import kappa.event_source.dynamodb_stream | ... | @@ -26,6 +26,7 @@ import kappa.event_source.dynamodb_stream |
26 | import kappa.event_source.kinesis | 26 | import kappa.event_source.kinesis |
27 | import kappa.event_source.s3 | 27 | import kappa.event_source.s3 |
28 | import kappa.event_source.sns | 28 | import kappa.event_source.sns |
29 | +import kappa.event_source.cloudwatch | ||
29 | import kappa.policy | 30 | import kappa.policy |
30 | import kappa.role | 31 | import kappa.role |
31 | import kappa.awsclient | 32 | import kappa.awsclient |
... | @@ -181,6 +182,7 @@ class Context(object): | ... | @@ -181,6 +182,7 @@ class Context(object): |
181 | 'kinesis': kappa.event_source.kinesis.KinesisEventSource, | 182 | 'kinesis': kappa.event_source.kinesis.KinesisEventSource, |
182 | 's3': kappa.event_source.s3.S3EventSource, | 183 | 's3': kappa.event_source.s3.S3EventSource, |
183 | 'sns': kappa.event_source.sns.SNSEventSource, | 184 | 'sns': kappa.event_source.sns.SNSEventSource, |
185 | + 'events': kappa.event_source.cloudwatch.CloudWatchEventSource | ||
184 | } | 186 | } |
185 | for event_source_cfg in event_sources: | 187 | for event_source_cfg in event_sources: |
186 | _, _, svc, _ = event_source_cfg['arn'].split(':', 3) | 188 | _, _, svc, _ = event_source_cfg['arn'].split(':', 3) |
... | @@ -226,7 +228,7 @@ class Context(object): | ... | @@ -226,7 +228,7 @@ class Context(object): |
226 | # There is a consistency problem here. | 228 | # There is a consistency problem here. |
227 | # If you don't wait for a bit, the function.create call | 229 | # If you don't wait for a bit, the function.create call |
228 | # will fail because the policy has not been attached to the role. | 230 | # will fail because the policy has not been attached to the role. |
229 | - LOG.debug('Waiting for policy/role propogation') | 231 | + LOG.debug('Waiting for policy/role propagation') |
230 | time.sleep(5) | 232 | time.sleep(5) |
231 | self.function.create() | 233 | self.function.create() |
232 | self.add_event_sources() | 234 | self.add_event_sources() |
... | @@ -239,6 +241,7 @@ class Context(object): | ... | @@ -239,6 +241,7 @@ class Context(object): |
239 | self.function.deploy() | 241 | self.function.deploy() |
240 | if self.restapi: | 242 | if self.restapi: |
241 | self.restapi.deploy() | 243 | self.restapi.deploy() |
244 | + self.add_event_sources() | ||
242 | 245 | ||
243 | def invoke(self, data): | 246 | def invoke(self, data): |
244 | return self.function.invoke(data) | 247 | return self.function.invoke(data) | ... | ... |
kappa/event_source.py
0 → 100644
1 | +# Copyright (c) 2014, 2015 Mitch Garnaat | ||
2 | +# | ||
3 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
4 | +# you may not use this file except in compliance with the License. | ||
5 | +# You may obtain a copy of the License at | ||
6 | +# | ||
7 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
8 | +# | ||
9 | +# Unless required by applicable law or agreed to in writing, software | ||
10 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
12 | +# See the License for the specific language governing permissions and | ||
13 | +# limitations under the License. | ||
14 | + | ||
15 | +import logging | ||
16 | +import uuid | ||
17 | + | ||
18 | +from botocore.exceptions import ClientError | ||
19 | + | ||
20 | +import kappa.awsclient | ||
21 | + | ||
22 | +LOG = logging.getLogger(__name__) | ||
23 | + | ||
24 | + | ||
25 | +class EventSource(object): | ||
26 | + | ||
27 | + def __init__(self, context, config): | ||
28 | + self._context = context | ||
29 | + self._config = config | ||
30 | + | ||
31 | + @property | ||
32 | + def arn(self): | ||
33 | + return self._config['arn'] | ||
34 | + | ||
35 | + @property | ||
36 | + def starting_position(self): | ||
37 | + return self._config.get('starting_position', 'LATEST') | ||
38 | + | ||
39 | + @property | ||
40 | + def batch_size(self): | ||
41 | + return self._config.get('batch_size', 100) | ||
42 | + | ||
43 | + @property | ||
44 | + def enabled(self): | ||
45 | + return self._config.get('enabled', False) | ||
46 | + | ||
47 | + | ||
48 | +class KinesisEventSource(EventSource): | ||
49 | + | ||
50 | + def __init__(self, context, config): | ||
51 | + super(KinesisEventSource, self).__init__(context, config) | ||
52 | + self._lambda = kappa.awsclient.create_client( | ||
53 | + 'lambda', context.session) | ||
54 | + | ||
55 | + def _get_uuid(self, function): | ||
56 | + uuid = None | ||
57 | + response = self._lambda.call( | ||
58 | + 'list_event_source_mappings', | ||
59 | + FunctionName=function.name, | ||
60 | + EventSourceArn=self.arn) | ||
61 | + LOG.debug(response) | ||
62 | + if len(response['EventSourceMappings']) > 0: | ||
63 | + uuid = response['EventSourceMappings'][0]['UUID'] | ||
64 | + return uuid | ||
65 | + | ||
66 | + def add(self, function): | ||
67 | + try: | ||
68 | + response = self._lambda.call( | ||
69 | + 'create_event_source_mapping', | ||
70 | + FunctionName=function.name, | ||
71 | + EventSourceArn=self.arn, | ||
72 | + BatchSize=self.batch_size, | ||
73 | + StartingPosition=self.starting_position, | ||
74 | + Enabled=self.enabled | ||
75 | + ) | ||
76 | + LOG.debug(response) | ||
77 | + except Exception: | ||
78 | + LOG.exception('Unable to add event source') | ||
79 | + | ||
80 | + def enable(self, function): | ||
81 | + self._config['enabled'] = True | ||
82 | + try: | ||
83 | + response = self._lambda.call( | ||
84 | + 'update_event_source_mapping', | ||
85 | + UUID=self._get_uuid(function), | ||
86 | + Enabled=self.enabled | ||
87 | + ) | ||
88 | + LOG.debug(response) | ||
89 | + except Exception: | ||
90 | + LOG.exception('Unable to enable event source') | ||
91 | + | ||
92 | + def disable(self, function): | ||
93 | + self._config['enabled'] = False | ||
94 | + try: | ||
95 | + response = self._lambda.call( | ||
96 | + 'update_event_source_mapping', | ||
97 | + FunctionName=function.name, | ||
98 | + Enabled=self.enabled | ||
99 | + ) | ||
100 | + LOG.debug(response) | ||
101 | + except Exception: | ||
102 | + LOG.exception('Unable to disable event source') | ||
103 | + | ||
104 | + def update(self, function): | ||
105 | + response = None | ||
106 | + uuid = self._get_uuid(function) | ||
107 | + if uuid: | ||
108 | + try: | ||
109 | + response = self._lambda.call( | ||
110 | + 'update_event_source_mapping', | ||
111 | + BatchSize=self.batch_size, | ||
112 | + Enabled=self.enabled, | ||
113 | + FunctionName=function.arn) | ||
114 | + LOG.debug(response) | ||
115 | + except Exception: | ||
116 | + LOG.exception('Unable to update event source') | ||
117 | + | ||
118 | + def remove(self, function): | ||
119 | + response = None | ||
120 | + uuid = self._get_uuid(function) | ||
121 | + if uuid: | ||
122 | + response = self._lambda.call( | ||
123 | + 'delete_event_source_mapping', | ||
124 | + UUID=uuid) | ||
125 | + LOG.debug(response) | ||
126 | + return response | ||
127 | + | ||
128 | + def status(self, function): | ||
129 | + response = None | ||
130 | + LOG.debug('getting status for event source %s', self.arn) | ||
131 | + uuid = self._get_uuid(function) | ||
132 | + if uuid: | ||
133 | + try: | ||
134 | + response = self._lambda.call( | ||
135 | + 'get_event_source_mapping', | ||
136 | + UUID=self._get_uuid(function)) | ||
137 | + LOG.debug(response) | ||
138 | + except ClientError: | ||
139 | + LOG.debug('event source %s does not exist', self.arn) | ||
140 | + response = None | ||
141 | + else: | ||
142 | + LOG.debug('No UUID for event source %s', self.arn) | ||
143 | + return response | ||
144 | + | ||
145 | + | ||
146 | +class DynamoDBStreamEventSource(KinesisEventSource): | ||
147 | + | ||
148 | + pass | ||
149 | + | ||
150 | + | ||
151 | +class S3EventSource(EventSource): | ||
152 | + | ||
153 | + def __init__(self, context, config): | ||
154 | + super(S3EventSource, self).__init__(context, config) | ||
155 | + self._s3 = kappa.awsclient.create_client('s3', context.session) | ||
156 | + | ||
157 | + def _make_notification_id(self, function_name): | ||
158 | + return 'Kappa-%s-notification' % function_name | ||
159 | + | ||
160 | + def _get_bucket_name(self): | ||
161 | + return self.arn.split(':')[-1] | ||
162 | + | ||
163 | + def add(self, function): | ||
164 | + notification_spec = { | ||
165 | + 'LambdaFunctionConfigurations': [ | ||
166 | + { | ||
167 | + 'Id': self._make_notification_id(function.name), | ||
168 | + 'Events': [e for e in self._config['events']], | ||
169 | + 'LambdaFunctionArn': function.arn, | ||
170 | + } | ||
171 | + ] | ||
172 | + } | ||
173 | + try: | ||
174 | + response = self._s3.call( | ||
175 | + 'put_bucket_notification_configuration', | ||
176 | + Bucket=self._get_bucket_name(), | ||
177 | + NotificationConfiguration=notification_spec) | ||
178 | + LOG.debug(response) | ||
179 | + except Exception as exc: | ||
180 | + LOG.debug(exc.response) | ||
181 | + LOG.exception('Unable to add S3 event source') | ||
182 | + | ||
183 | + enable = add | ||
184 | + | ||
185 | + def update(self, function): | ||
186 | + self.add(function) | ||
187 | + | ||
188 | + def remove(self, function): | ||
189 | + LOG.debug('removing s3 notification') | ||
190 | + response = self._s3.call( | ||
191 | + 'get_bucket_notification', | ||
192 | + Bucket=self._get_bucket_name()) | ||
193 | + LOG.debug(response) | ||
194 | + if 'CloudFunctionConfiguration' in response: | ||
195 | + fn_arn = response['CloudFunctionConfiguration']['CloudFunction'] | ||
196 | + if fn_arn == function.arn: | ||
197 | + del response['CloudFunctionConfiguration'] | ||
198 | + del response['ResponseMetadata'] | ||
199 | + response = self._s3.call( | ||
200 | + 'put_bucket_notification', | ||
201 | + Bucket=self._get_bucket_name(), | ||
202 | + NotificationConfiguration=response) | ||
203 | + LOG.debug(response) | ||
204 | + | ||
205 | + disable = remove | ||
206 | + | ||
207 | + def status(self, function): | ||
208 | + LOG.debug('status for s3 notification for %s', function.name) | ||
209 | + response = self._s3.call( | ||
210 | + 'get_bucket_notification', | ||
211 | + Bucket=self._get_bucket_name()) | ||
212 | + LOG.debug(response) | ||
213 | + if 'CloudFunctionConfiguration' not in response: | ||
214 | + response = None | ||
215 | + return response | ||
216 | + | ||
217 | + | ||
218 | +class SNSEventSource(EventSource): | ||
219 | + | ||
220 | + def __init__(self, context, config): | ||
221 | + super(SNSEventSource, self).__init__(context, config) | ||
222 | + self._sns = kappa.awsclient.create_client('sns', context.session) | ||
223 | + | ||
224 | + def _make_notification_id(self, function_name): | ||
225 | + return 'Kappa-%s-notification' % function_name | ||
226 | + | ||
227 | + def exists(self, function): | ||
228 | + try: | ||
229 | + response = self._sns.call( | ||
230 | + 'list_subscriptions_by_topic', | ||
231 | + TopicArn=self.arn) | ||
232 | + LOG.debug(response) | ||
233 | + for subscription in response['Subscriptions']: | ||
234 | + if subscription['Endpoint'] == function.arn: | ||
235 | + return subscription | ||
236 | + return None | ||
237 | + except Exception: | ||
238 | + LOG.exception('Unable to find event source %s', self.arn) | ||
239 | + | ||
240 | + def add(self, function): | ||
241 | + try: | ||
242 | + response = self._sns.call( | ||
243 | + 'subscribe', | ||
244 | + TopicArn=self.arn, Protocol='lambda', | ||
245 | + Endpoint=function.arn) | ||
246 | + LOG.debug(response) | ||
247 | + except Exception: | ||
248 | + LOG.exception('Unable to add SNS event source') | ||
249 | + | ||
250 | + enable = add | ||
251 | + | ||
252 | + def update(self, function): | ||
253 | + self.add(function) | ||
254 | + | ||
255 | + def remove(self, function): | ||
256 | + LOG.debug('removing SNS event source') | ||
257 | + try: | ||
258 | + subscription = self.exists(function) | ||
259 | + if subscription: | ||
260 | + response = self._sns.call( | ||
261 | + 'unsubscribe', | ||
262 | + SubscriptionArn=subscription['SubscriptionArn']) | ||
263 | + LOG.debug(response) | ||
264 | + except Exception: | ||
265 | + LOG.exception('Unable to remove event source %s', self.arn) | ||
266 | + | ||
267 | + disable = remove | ||
268 | + | ||
269 | + def status(self, function): | ||
270 | + LOG.debug('status for SNS notification for %s', function.name) | ||
271 | + status = self.exists(function) | ||
272 | + if status: | ||
273 | + status['EventSourceArn'] = status['TopicArn'] | ||
274 | + return status | ||
275 | + | ||
276 | +class CloudWatchEventSource(EventSource): | ||
277 | + | ||
278 | + def __init__(self, context, config): | ||
279 | + super(CloudWatchEventSource, self).__init__(context, config) | ||
280 | + self._events = kappa.awsclient.create_client('events', context.session) | ||
281 | + self._lambda = kappa.awsclient.create_client('lambda', context.session) | ||
282 | + self._name = config['name'] | ||
283 | + self._context = context | ||
284 | + self._config = config | ||
285 | + | ||
286 | + def find(self): | ||
287 | + response = self._events.call('list_rules', NamePrefix=self._name) | ||
288 | + LOG.debug(response) | ||
289 | + if 'Rules' in response: | ||
290 | + for r in response['Rules']: | ||
291 | + if r['Name'] == self._name: | ||
292 | + return r | ||
293 | + return None | ||
294 | + | ||
295 | + def add(self, function): | ||
296 | + kwargs = { | ||
297 | + 'Name': self._name, | ||
298 | + 'State': 'ENABLED' if self.enabled else 'DISABLED' | ||
299 | + } | ||
300 | + if 'schedule' in self._config: | ||
301 | + kwargs['ScheduleExpression'] = self._config['schedule'] | ||
302 | + if 'pattern' in self._config: | ||
303 | + kwargs['EventPattern'] = self._config['pattern'] | ||
304 | + if 'description' in self._config: | ||
305 | + kwargs['Description'] = self._config['description'] | ||
306 | + if 'role_arn' in self._config: | ||
307 | + kwargs['RoleArn'] = self._config['role_arn'] | ||
308 | + try: | ||
309 | + response = self._events.call('put_rule', **kwargs) | ||
310 | + LOG.debug(response) | ||
311 | + self._config['arn'] = response['RuleArn'] | ||
312 | + self._lambda.call('add_permission', | ||
313 | + FunctionName=function.name, | ||
314 | + StatementId=str(uuid.uuid4()), | ||
315 | + Action='lambda:InvokeFunction', | ||
316 | + Principal='events.amazonaws.com', | ||
317 | + SourceArn=response['RuleArn']) | ||
318 | + response = self._events.call('put_targets', | ||
319 | + Rule=self._name, | ||
320 | + Targets=[{ | ||
321 | + 'Id': '1', | ||
322 | + 'Arn': function.arn | ||
323 | + }]) | ||
324 | + LOG.debug(response) | ||
325 | + except Exception: | ||
326 | + LOG.exception('Unable to put CloudWatch event source') | ||
327 | + | ||
328 | + def update(self, function): | ||
329 | + self.add(function) | ||
330 | + | ||
331 | + def remove(self, function): | ||
332 | + LOG.debug('removing CloudWatch event source') | ||
333 | + try: | ||
334 | + rule = self.find() | ||
335 | + if rule: | ||
336 | + response = self._events.call( | ||
337 | + 'delete_rule', | ||
338 | + Name=self._name) | ||
339 | + LOG.debug(response) | ||
340 | + except Exception: | ||
341 | + LOG.exception('Unable to remove CloudWatch event source %s', self._name) | ||
342 | + | ||
343 | + def status(self, function): | ||
344 | + LOG.debug('status for CloudWatch event for %s', function.name) | ||
345 | + return self._to_status(self.find()) | ||
346 | + | ||
347 | + def enable(self, function): | ||
348 | + if self.find(): | ||
349 | + self._events.call('enable_rule', Name=self._name) | ||
350 | + | ||
351 | + def disable(self, function): | ||
352 | + if self.find(): | ||
353 | + self._events.call('disable_rule', Name=self._name) | ||
354 | + | ||
355 | + def _to_status(self, rule): | ||
356 | + if rule: | ||
357 | + return { | ||
358 | + 'EventSourceArn': rule['Arn'], | ||
359 | + 'State': rule['State'] | ||
360 | + } | ||
361 | + else: | ||
362 | + return None |
kappa/event_source/cloudwatch.py
0 → 100644
1 | +# -*- coding: utf-8 -*- | ||
2 | +# Copyright (c) 2014, 2015 Mitch Garnaat | ||
3 | +# | ||
4 | +# Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | +# you may not use this file except in compliance with the License. | ||
6 | +# You may obtain a copy of the License at | ||
7 | +# | ||
8 | +# http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | +# | ||
10 | +# Unless required by applicable law or agreed to in writing, software | ||
11 | +# distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | +# See the License for the specific language governing permissions and | ||
14 | +# limitations under the License. | ||
15 | + | ||
16 | +import kappa.event_source.base | ||
17 | +import logging | ||
18 | +import uuid | ||
19 | + | ||
20 | +LOG = logging.getLogger(__name__) | ||
21 | + | ||
22 | + | ||
23 | +class CloudWatchEventSource(kappa.event_source.base.EventSource): | ||
24 | + | ||
25 | + def __init__(self, context, config): | ||
26 | + super(CloudWatchEventSource, self).__init__(context, config) | ||
27 | + self._events = kappa.awsclient.create_client('events', context.session) | ||
28 | + self._lambda = kappa.awsclient.create_client('lambda', context.session) | ||
29 | + self._name = config['arn'].split('/')[-1] | ||
30 | + self._context = context | ||
31 | + self._config = config | ||
32 | + | ||
33 | + def get_rule(self): | ||
34 | + response = self._events.call('list_rules', NamePrefix=self._name) | ||
35 | + LOG.debug(response) | ||
36 | + if 'Rules' in response: | ||
37 | + for r in response['Rules']: | ||
38 | + if r['Name'] == self._name: | ||
39 | + return r | ||
40 | + return None | ||
41 | + | ||
42 | + def add(self, function): | ||
43 | + kwargs = { | ||
44 | + 'Name': self._name, | ||
45 | + 'State': 'ENABLED' if self.enabled else 'DISABLED' | ||
46 | + } | ||
47 | + if 'schedule' in self._config: | ||
48 | + kwargs['ScheduleExpression'] = self._config['schedule'] | ||
49 | + if 'pattern' in self._config: | ||
50 | + kwargs['EventPattern'] = self._config['pattern'] | ||
51 | + if 'description' in self._config: | ||
52 | + kwargs['Description'] = self._config['description'] | ||
53 | + if 'role_arn' in self._config: | ||
54 | + kwargs['RoleArn'] = self._config['role_arn'] | ||
55 | + try: | ||
56 | + response = self._events.call('put_rule', **kwargs) | ||
57 | + LOG.debug(response) | ||
58 | + self._config['arn'] = response['RuleArn'] | ||
59 | + response = self._lambda.call('add_permission', | ||
60 | + FunctionName=function.name, | ||
61 | + StatementId=str(uuid.uuid4()), | ||
62 | + Action='lambda:InvokeFunction', | ||
63 | + Principal='events.amazonaws.com', | ||
64 | + SourceArn=response['RuleArn']) | ||
65 | + LOG.debug(response) | ||
66 | + response = self._events.call('put_targets', | ||
67 | + Rule=self._name, | ||
68 | + Targets=[{ | ||
69 | + 'Id': function.name, | ||
70 | + 'Arn': function.arn | ||
71 | + }]) | ||
72 | + LOG.debug(response) | ||
73 | + except Exception: | ||
74 | + LOG.exception('Unable to put CloudWatch event source') | ||
75 | + | ||
76 | + def update(self, function): | ||
77 | + self.add(function) | ||
78 | + | ||
79 | + def remove(self, function): | ||
80 | + LOG.debug('removing CloudWatch event source') | ||
81 | + try: | ||
82 | + rule = self.get_rule() | ||
83 | + if rule: | ||
84 | + response = self._events.call('remove_targets', | ||
85 | + Rule=self._name, | ||
86 | + Ids=[function.name]) | ||
87 | + LOG.debug(response) | ||
88 | + response = self._events.call('delete_rule', | ||
89 | + Name=self._name) | ||
90 | + LOG.debug(response) | ||
91 | + except Exception: | ||
92 | + LOG.exception('Unable to remove CloudWatch event source %s', self._name) | ||
93 | + | ||
94 | + def status(self, function): | ||
95 | + LOG.debug('status for CloudWatch event for %s', function.name) | ||
96 | + return self._to_status(self.get_rule()) | ||
97 | + | ||
98 | + def enable(self, function): | ||
99 | + if self.get_rule(): | ||
100 | + self._events.call('enable_rule', Name=self._name) | ||
101 | + | ||
102 | + def disable(self, function): | ||
103 | + if self.get_rule(): | ||
104 | + self._events.call('disable_rule', Name=self._name) | ||
105 | + | ||
106 | + def _to_status(self, rule): | ||
107 | + if rule: | ||
108 | + return { | ||
109 | + 'EventSourceArn': rule['Arn'], | ||
110 | + 'State': rule['State'] | ||
111 | + } | ||
112 | + else: | ||
113 | + return None |
-
Please register or login to post a comment