Mitch Garnaat

WIP Commit. Updating to use new GA version of the Lambda API. Also moving from…

… botocore to boto3.  Also adding SNS example.  No longer using CloudFormation for policies since we only need one and CloudFormation does not yet support managed policies.  Haven't updated any tests yet so they will all be failing for now.  Also need to update README.
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
13 # language governing permissions and limitations under the License. 13 # language governing permissions and limitations under the License.
14 from datetime import datetime 14 from datetime import datetime
15 import logging 15 import logging
16 +import base64
16 17
17 import click 18 import click
18 19
...@@ -38,18 +39,20 @@ def cli(ctx, config=None, debug=False): ...@@ -38,18 +39,20 @@ def cli(ctx, config=None, debug=False):
38 39
39 @cli.command() 40 @cli.command()
40 @click.pass_context 41 @click.pass_context
41 -def deploy(ctx): 42 +def create(ctx):
42 context = Context(ctx.obj['config'], ctx.obj['debug']) 43 context = Context(ctx.obj['config'], ctx.obj['debug'])
43 click.echo('deploying...') 44 click.echo('deploying...')
44 - context.deploy() 45 + context.create()
45 click.echo('...done') 46 click.echo('...done')
46 47
47 @cli.command() 48 @cli.command()
48 @click.pass_context 49 @click.pass_context
49 -def test(ctx): 50 +def invoke(ctx):
50 context = Context(ctx.obj['config'], ctx.obj['debug']) 51 context = Context(ctx.obj['config'], ctx.obj['debug'])
51 - click.echo('testing...') 52 + click.echo('invoking...')
52 - context.test() 53 + response = context.invoke()
54 + log_data = base64.b64decode(response['LogResult'])
55 + click.echo(log_data)
53 click.echo('...done') 56 click.echo('...done')
54 57
55 @cli.command() 58 @cli.command()
...@@ -67,30 +70,31 @@ def tail(ctx): ...@@ -67,30 +70,31 @@ def tail(ctx):
67 def status(ctx): 70 def status(ctx):
68 context = Context(ctx.obj['config'], ctx.obj['debug']) 71 context = Context(ctx.obj['config'], ctx.obj['debug'])
69 status = context.status() 72 status = context.status()
70 - click.echo(click.style('Stack', bold=True)) 73 + click.echo(click.style('Policy', bold=True))
71 - if status['stack']: 74 + if status['policy']:
72 - for stack in status['stack']['Stacks']: 75 + line = ' {} ({})'.format(
73 - line = ' {}: {}'.format(stack['StackId'], stack['StackStatus']) 76 + status['policy']['PolicyName'],
77 + status['policy']['Arn'])
78 + click.echo(click.style(line, fg='green'))
79 + click.echo(click.style('Role', bold=True))
80 + if status['role']:
81 + line = ' {} ({})'.format(
82 + status['role']['Role']['RoleName'],
83 + status['role']['Role']['Arn'])
74 click.echo(click.style(line, fg='green')) 84 click.echo(click.style(line, fg='green'))
75 - else:
76 - click.echo(click.style(' None', fg='green'))
77 click.echo(click.style('Function', bold=True)) 85 click.echo(click.style('Function', bold=True))
78 if status['function']: 86 if status['function']:
79 - line = ' {}'.format( 87 + line = ' {} ({})'.format(
80 - status['function']['Configuration']['FunctionName']) 88 + status['function']['Configuration']['FunctionName'],
89 + status['function']['Configuration']['FunctionArn'])
81 click.echo(click.style(line, fg='green')) 90 click.echo(click.style(line, fg='green'))
82 else: 91 else:
83 click.echo(click.style(' None', fg='green')) 92 click.echo(click.style(' None', fg='green'))
84 click.echo(click.style('Event Sources', bold=True)) 93 click.echo(click.style('Event Sources', bold=True))
85 if status['event_sources']: 94 if status['event_sources']:
86 for event_source in status['event_sources']: 95 for event_source in status['event_sources']:
87 - if 'EventSource' in event_source:
88 line = ' {}: {}'.format( 96 line = ' {}: {}'.format(
89 - event_source['EventSource'], event_source['IsActive']) 97 + event_source['EventSourceArn'], event_source['State'])
90 - click.echo(click.style(line, fg='green'))
91 - else:
92 - line = ' {}'.format(
93 - event_source['CloudFunctionConfiguration']['Id'])
94 click.echo(click.style(line, fg='green')) 98 click.echo(click.style(line, fg='green'))
95 else: 99 else:
96 click.echo(click.style(' None', fg='green')) 100 click.echo(click.style(' None', fg='green'))
......
1 -# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/ 1 +# Copyright (c) 2014,2015 Mitch Garnaat http://garnaat.org/
2 # 2 #
3 # Licensed under the Apache License, Version 2.0 (the "License"). You 3 # Licensed under the Apache License, Version 2.0 (the "License"). You
4 # may not use this file except in compliance with the License. A copy of 4 # may not use this file except in compliance with the License. A copy of
...@@ -11,21 +11,20 @@ ...@@ -11,21 +11,20 @@
11 # ANY KIND, either express or implied. See the License for the specific 11 # ANY KIND, either express or implied. See the License for the specific
12 # language governing permissions and limitations under the License. 12 # language governing permissions and limitations under the License.
13 13
14 -import botocore.session 14 +import boto3
15 15
16 16
17 class __AWS(object): 17 class __AWS(object):
18 18
19 - def __init__(self, profile=None, region=None): 19 + def __init__(self, profile_name=None, region_name=None):
20 self._client_cache = {} 20 self._client_cache = {}
21 - self._session = botocore.session.get_session() 21 + self._session = boto3.session.Session(
22 - self._session.profile = profile 22 + region_name=region_name, profile_name=profile_name)
23 - self._region = region
24 23
25 def create_client(self, client_name): 24 def create_client(self, client_name):
26 if client_name not in self._client_cache: 25 if client_name not in self._client_cache:
27 - self._client_cache[client_name] = self._session.create_client( 26 + self._client_cache[client_name] = self._session.client(
28 - client_name, self._region) 27 + client_name)
29 return self._client_cache[client_name] 28 return self._client_cache[client_name]
30 29
31 30
......
...@@ -16,7 +16,8 @@ import yaml ...@@ -16,7 +16,8 @@ import yaml
16 16
17 import kappa.function 17 import kappa.function
18 import kappa.event_source 18 import kappa.event_source
19 -import kappa.stack 19 +import kappa.policy
20 +import kappa.role
20 21
21 LOG = logging.getLogger(__name__) 22 LOG = logging.getLogger(__name__)
22 23
...@@ -32,8 +33,16 @@ class Context(object): ...@@ -32,8 +33,16 @@ class Context(object):
32 else: 33 else:
33 self.set_logger('kappa', logging.INFO) 34 self.set_logger('kappa', logging.INFO)
34 self.config = yaml.load(config_file) 35 self.config = yaml.load(config_file)
35 - self._stack = kappa.stack.Stack( 36 + if 'policy' in self.config.get('iam', ''):
36 - self, self.config['cloudformation']) 37 + self.policy = kappa.policy.Policy(
38 + self, self.config['iam']['policy'])
39 + else:
40 + self.policy = None
41 + if 'role' in self.config.get('iam', ''):
42 + self.role = kappa.role.Role(
43 + self, self.config['iam']['role'])
44 + else:
45 + self.role = None
37 self.function = kappa.function.Function( 46 self.function = kappa.function.Function(
38 self, self.config['lambda']) 47 self, self.config['lambda'])
39 self.event_sources = [] 48 self.event_sources = []
...@@ -57,11 +66,7 @@ class Context(object): ...@@ -57,11 +66,7 @@ class Context(object):
57 66
58 @property 67 @property
59 def exec_role_arn(self): 68 def exec_role_arn(self):
60 - return self._stack.exec_role_arn 69 + return self.role.arn
61 -
62 - @property
63 - def invoke_role_arn(self):
64 - return self._stack.invoke_role_arn
65 70
66 def debug(self): 71 def debug(self):
67 self.set_logger('kappa', logging.DEBUG) 72 self.set_logger('kappa', logging.DEBUG)
...@@ -90,6 +95,7 @@ class Context(object): ...@@ -90,6 +95,7 @@ class Context(object):
90 log.addHandler(ch) 95 log.addHandler(ch)
91 96
92 def _create_event_sources(self): 97 def _create_event_sources(self):
98 + if 'event_sources' in self.config['lambda']:
93 for event_source_cfg in self.config['lambda']['event_sources']: 99 for event_source_cfg in self.config['lambda']['event_sources']:
94 _, _, svc, _ = event_source_cfg['arn'].split(':', 3) 100 _, _, svc, _ = event_source_cfg['arn'].split(':', 3)
95 if svc == 'kinesis': 101 if svc == 'kinesis':
...@@ -99,35 +105,54 @@ class Context(object): ...@@ -99,35 +105,54 @@ class Context(object):
99 elif svc == 's3': 105 elif svc == 's3':
100 self.event_sources.append(kappa.event_source.S3EventSource( 106 self.event_sources.append(kappa.event_source.S3EventSource(
101 self, event_source_cfg)) 107 self, event_source_cfg))
108 + elif svc == 'sns':
109 + self.event_sources.append(
110 + kappa.event_source.SNSEventSource(self,
111 + event_source_cfg))
102 else: 112 else:
103 - msg = 'Unsupported event source: %s' % event_source_cfg['arn'] 113 + msg = 'Unknown event source: %s' % event_source_cfg['arn']
104 raise ValueError(msg) 114 raise ValueError(msg)
105 115
106 def add_event_sources(self): 116 def add_event_sources(self):
107 for event_source in self.event_sources: 117 for event_source in self.event_sources:
108 event_source.add(self.function) 118 event_source.add(self.function)
109 119
110 - def deploy(self): 120 + def create(self):
111 - self._stack.update() 121 + if self.policy:
112 - self.function.upload() 122 + self.policy.create()
123 + if self.role:
124 + self.role.create()
125 + self.function.create()
113 126
114 - def test(self): 127 + def invoke(self):
115 - self.function.test() 128 + return self.function.invoke()
116 129
117 def tail(self): 130 def tail(self):
118 return self.function.tail() 131 return self.function.tail()
119 132
120 def delete(self): 133 def delete(self):
121 - self._stack.delete() 134 + if self.policy:
135 + self.policy.delete()
136 + if self.role:
137 + self.role.delete()
122 self.function.delete() 138 self.function.delete()
123 for event_source in self.event_sources: 139 for event_source in self.event_sources:
124 event_source.remove(self.function) 140 event_source.remove(self.function)
125 141
126 def status(self): 142 def status(self):
127 status = {} 143 status = {}
128 - status['stack'] = self._stack.status() 144 + if self.policy:
145 + status['policy'] = self.policy.status()
146 + else:
147 + status['policy'] = None
148 + if self.role:
149 + status['role'] = self.role.status()
150 + else:
151 + status['role'] = None
129 status['function'] = self.function.status() 152 status['function'] = self.function.status()
130 status['event_sources'] = [] 153 status['event_sources'] = []
154 + if self.event_sources:
131 for event_source in self.event_sources: 155 for event_source in self.event_sources:
132 - status['event_sources'].append(event_source.status(self.function)) 156 + status['event_sources'].append(
157 + event_source.status(self.function))
133 return status 158 return status
......
...@@ -31,6 +31,10 @@ class EventSource(object): ...@@ -31,6 +31,10 @@ class EventSource(object):
31 return self._config['arn'] 31 return self._config['arn']
32 32
33 @property 33 @property
34 + def starting_position(self):
35 + return self._config.get('starting_position', 'TRIM_HORIZON')
36 +
37 + @property
34 def batch_size(self): 38 def batch_size(self):
35 return self._config.get('batch_size', 100) 39 return self._config.get('batch_size', 100)
36 40
...@@ -44,21 +48,21 @@ class KinesisEventSource(EventSource): ...@@ -44,21 +48,21 @@ class KinesisEventSource(EventSource):
44 48
45 def _get_uuid(self, function): 49 def _get_uuid(self, function):
46 uuid = None 50 uuid = None
47 - response = self._lambda.list_event_sources( 51 + response = self._lambda.list_event_source_mappings(
48 FunctionName=function.name, 52 FunctionName=function.name,
49 EventSourceArn=self.arn) 53 EventSourceArn=self.arn)
50 LOG.debug(response) 54 LOG.debug(response)
51 - if len(response['EventSources']) > 0: 55 + if len(response['EventSourceMappings']) > 0:
52 - uuid = response['EventSources'][0]['UUID'] 56 + uuid = response['EventSourceMappings'][0]['UUID']
53 return uuid 57 return uuid
54 58
55 def add(self, function): 59 def add(self, function):
56 try: 60 try:
57 - response = self._lambda.add_event_source( 61 + response = self._lambda.create_event_source_mapping(
58 FunctionName=function.name, 62 FunctionName=function.name,
59 - Role=self._context.invoke_role_arn, 63 + EventSourceArn=self.arn,
60 - EventSource=self.arn, 64 + BatchSize=self.batch_size,
61 - BatchSize=self.batch_size) 65 + StartingPosition=self.starting_position)
62 LOG.debug(response) 66 LOG.debug(response)
63 except Exception: 67 except Exception:
64 LOG.exception('Unable to add Kinesis event source') 68 LOG.exception('Unable to add Kinesis event source')
...@@ -67,7 +71,7 @@ class KinesisEventSource(EventSource): ...@@ -67,7 +71,7 @@ class KinesisEventSource(EventSource):
67 response = None 71 response = None
68 uuid = self._get_uuid(function) 72 uuid = self._get_uuid(function)
69 if uuid: 73 if uuid:
70 - response = self._lambda.remove_event_source( 74 + response = self._lambda.delete_event_source_mapping(
71 UUID=uuid) 75 UUID=uuid)
72 LOG.debug(response) 76 LOG.debug(response)
73 return response 77 return response
...@@ -75,7 +79,7 @@ class KinesisEventSource(EventSource): ...@@ -75,7 +79,7 @@ class KinesisEventSource(EventSource):
75 def status(self, function): 79 def status(self, function):
76 LOG.debug('getting status for event source %s', self.arn) 80 LOG.debug('getting status for event source %s', self.arn)
77 try: 81 try:
78 - response = self._lambda.get_event_source( 82 + response = self._lambda.get_event_source_mapping(
79 UUID=self._get_uuid(function)) 83 UUID=self._get_uuid(function))
80 LOG.debug(response) 84 LOG.debug(response)
81 except ClientError: 85 except ClientError:
...@@ -134,3 +138,50 @@ class S3EventSource(EventSource): ...@@ -134,3 +138,50 @@ class S3EventSource(EventSource):
134 if 'CloudFunctionConfiguration' not in response: 138 if 'CloudFunctionConfiguration' not in response:
135 response = None 139 response = None
136 return response 140 return response
141 +
142 +
143 +class SNSEventSource(EventSource):
144 +
145 + def __init__(self, context, config):
146 + super(SNSEventSource, self).__init__(context, config)
147 + aws = kappa.aws.get_aws(context)
148 + self._sns = aws.create_client('sns')
149 +
150 + def _make_notification_id(self, function_name):
151 + return 'Kappa-%s-notification' % function_name
152 +
153 + def exists(self, function):
154 + try:
155 + response = self._sns.list_subscriptions_by_topic(
156 + TopicArn=self.arn)
157 + LOG.debug(response)
158 + for subscription in response['Subscriptions']:
159 + if subscription['Endpoint'] == function.arn:
160 + return subscription
161 + return None
162 + except Exception:
163 + LOG.exception('Unable to find event source %s', self.arn)
164 +
165 + def add(self, function):
166 + try:
167 + response = self._sns.subscribe(
168 + TopicArn=self.arn, Protocol='lambda',
169 + Endpoint=function.arn)
170 + LOG.debug(response)
171 + except Exception:
172 + LOG.exception('Unable to add SNS event source')
173 +
174 + def remove(self, function):
175 + LOG.debug('removing SNS event source')
176 + try:
177 + subscription = self.exists(function)
178 + if subscription:
179 + response = self._sns.unsubscribe(
180 + SubscriptionArn=subscription['SubscriptionArn'])
181 + LOG.debug(response)
182 + except Exception:
183 + LOG.exception('Unable to remove event source %s', self.arn)
184 +
185 + def status(self, function):
186 + LOG.debug('status for SNS notification for %s', function.name)
187 + return self.exist(function)
......
...@@ -46,10 +46,6 @@ class Function(object): ...@@ -46,10 +46,6 @@ class Function(object):
46 return self._config['handler'] 46 return self._config['handler']
47 47
48 @property 48 @property
49 - def mode(self):
50 - return self._config['mode']
51 -
52 - @property
53 def description(self): 49 def description(self):
54 return self._config['description'] 50 return self._config['description']
55 51
...@@ -74,13 +70,17 @@ class Function(object): ...@@ -74,13 +70,17 @@ class Function(object):
74 return self._config['test_data'] 70 return self._config['test_data']
75 71
76 @property 72 @property
73 + def permissions(self):
74 + return self._config.get('permissions', list())
75 +
76 + @property
77 def arn(self): 77 def arn(self):
78 if self._arn is None: 78 if self._arn is None:
79 try: 79 try:
80 - response = self._lambda_svc.get_function_configuration( 80 + response = self._lambda_svc.get_function(
81 FunctionName=self.name) 81 FunctionName=self.name)
82 LOG.debug(response) 82 LOG.debug(response)
83 - self._arn = response['FunctionARN'] 83 + self._arn = response['Configuration']['FunctionArn']
84 except Exception: 84 except Exception:
85 LOG.debug('Unable to find ARN for function: %s', self.name) 85 LOG.debug('Unable to find ARN for function: %s', self.name)
86 return self._arn 86 return self._arn
...@@ -124,25 +124,45 @@ class Function(object): ...@@ -124,25 +124,45 @@ class Function(object):
124 else: 124 else:
125 self._zip_lambda_file(zipfile_name, lambda_fn) 125 self._zip_lambda_file(zipfile_name, lambda_fn)
126 126
127 - def upload(self): 127 + def add_permissions(self):
128 - LOG.debug('uploading %s', self.zipfile_name) 128 + for permission in self.permissions:
129 + try:
130 + kwargs = {
131 + 'FunctionName': self.name,
132 + 'StatementId': permission['statement_id'],
133 + 'Action': permission['action'],
134 + 'Principal': permission['principal']}
135 + source_arn = permission.get('source_arn', None)
136 + if source_arn:
137 + kwargs['SourceArn'] = source_arn
138 + source_account = permission.get('source_account', None)
139 + if source_account:
140 + kwargs['SourceAccount'] = source_account
141 + response = self._lambda_svc.add_permission(**kwargs)
142 + LOG.debug(response)
143 + except Exception:
144 + LOG.exception('Unable to add permission')
145 +
146 + def create(self):
147 + LOG.debug('creating %s', self.zipfile_name)
129 self.zip_lambda_function(self.zipfile_name, self.path) 148 self.zip_lambda_function(self.zipfile_name, self.path)
130 with open(self.zipfile_name, 'rb') as fp: 149 with open(self.zipfile_name, 'rb') as fp:
131 exec_role = self._context.exec_role_arn 150 exec_role = self._context.exec_role_arn
132 try: 151 try:
133 - response = self._lambda_svc.upload_function( 152 + zipdata = fp.read()
153 + response = self._lambda_svc.create_function(
134 FunctionName=self.name, 154 FunctionName=self.name,
135 - FunctionZip=fp, 155 + Code={'ZipFile': zipdata},
136 Runtime=self.runtime, 156 Runtime=self.runtime,
137 Role=exec_role, 157 Role=exec_role,
138 Handler=self.handler, 158 Handler=self.handler,
139 - Mode=self.mode,
140 Description=self.description, 159 Description=self.description,
141 Timeout=self.timeout, 160 Timeout=self.timeout,
142 MemorySize=self.memory_size) 161 MemorySize=self.memory_size)
143 LOG.debug(response) 162 LOG.debug(response)
144 except Exception: 163 except Exception:
145 LOG.exception('Unable to upload zip file') 164 LOG.exception('Unable to upload zip file')
165 + self.add_permissions()
146 166
147 def delete(self): 167 def delete(self):
148 LOG.debug('deleting function %s', self.name) 168 LOG.debug('deleting function %s', self.name)
...@@ -169,5 +189,14 @@ class Function(object): ...@@ -169,5 +189,14 @@ class Function(object):
169 InvokeArgs=fp) 189 InvokeArgs=fp)
170 LOG.debug(response) 190 LOG.debug(response)
171 191
172 - def test(self): 192 + def invoke(self, test_data=None):
173 - self.invoke_asynch(self.test_data) 193 + if test_data is None:
194 + test_data = self.test_data
195 + LOG.debug('invoke %s', test_data)
196 + with open(test_data) as fp:
197 + response = self._lambda_svc.invoke(
198 + FunctionName=self.name,
199 + LogType='Tail',
200 + Payload=fp.read())
201 + LOG.debug(response)
202 + return response
......
1 +# Copyright (c) 2015 Mitch Garnaat http://garnaat.org/
2 +#
3 +# Licensed under the Apache License, Version 2.0 (the "License"). You
4 +# may not use this file except in compliance with the License. A copy of
5 +# the License is located at
6 +#
7 +# http://aws.amazon.com/apache2.0/
8 +#
9 +# or in the "license" file accompanying this file. This file is
10 +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11 +# ANY KIND, either express or implied. See the License for the specific
12 +# language governing permissions and limitations under the License.
13 +
14 +import logging
15 +
16 +from botocore.exceptions import ClientError
17 +
18 +import kappa.aws
19 +
20 +LOG = logging.getLogger(__name__)
21 +
22 +
23 +class Policy(object):
24 +
25 + Path = '/kappa/'
26 +
27 + def __init__(self, context, config):
28 + self._context = context
29 + self._config = config
30 + aws = kappa.aws.get_aws(context)
31 + self._iam_svc = aws.create_client('iam')
32 + self._arn = None
33 +
34 + @property
35 + def name(self):
36 + return self._config['name']
37 +
38 + @property
39 + def description(self):
40 + return self._config.get('description', None)
41 +
42 + @property
43 + def document(self):
44 + return self._config['document']
45 +
46 + @property
47 + def arn(self):
48 + if self._arn is None:
49 + policy = self.exists()
50 + if policy:
51 + self._arn = policy.get('Arn', None)
52 + return self._arn
53 +
54 + def exists(self):
55 + try:
56 + response = self._iam_svc.list_policies(PathPrefix=self.Path)
57 + LOG.debug(response)
58 + for policy in response['Policies']:
59 + if policy['PolicyName'] == self.name:
60 + return policy
61 + except Exception:
62 + LOG.exception('Error listing policies')
63 + return None
64 +
65 + def create(self):
66 + LOG.debug('creating policy %s', self.name)
67 + policy = self.exists()
68 + if not policy:
69 + with open(self.document, 'rb') as fp:
70 + try:
71 + response = self._iam_svc.create_policy(
72 + Path=self.Path, PolicyName=self.name,
73 + PolicyDocument=fp.read(),
74 + Description=self.description)
75 + LOG.debug(response)
76 + except Exception:
77 + LOG.exception('Error creating Policy')
78 +
79 + def delete(self):
80 + LOG.debug('deleting policy %s', self.name)
81 + response = self._iam_svc.delete_policy(PolicyArn=self.arn)
82 + LOG.debug(response)
83 + return response
84 +
85 + def status(self):
86 + LOG.debug('getting status for policy %s', self.name)
87 + return self.exists()
1 +# Copyright (c) 2015 Mitch Garnaat http://garnaat.org/
2 +#
3 +# Licensed under the Apache License, Version 2.0 (the "License"). You
4 +# may not use this file except in compliance with the License. A copy of
5 +# the License is located at
6 +#
7 +# http://aws.amazon.com/apache2.0/
8 +#
9 +# or in the "license" file accompanying this file. This file is
10 +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11 +# ANY KIND, either express or implied. See the License for the specific
12 +# language governing permissions and limitations under the License.
13 +
14 +import logging
15 +
16 +from botocore.exceptions import ClientError
17 +
18 +import kappa.aws
19 +
20 +LOG = logging.getLogger(__name__)
21 +
22 +
23 +AssumeRolePolicyDocument = """{
24 + "Version" : "2012-10-17",
25 + "Statement": [ {
26 + "Effect": "Allow",
27 + "Principal": {
28 + "Service": [ "lambda.amazonaws.com" ]
29 + },
30 + "Action": [ "sts:AssumeRole" ]
31 + } ]
32 +}"""
33 +
34 +
35 +class Role(object):
36 +
37 + Path = '/kappa/'
38 +
39 + def __init__(self, context, config):
40 + self._context = context
41 + self._config = config
42 + aws = kappa.aws.get_aws(context)
43 + self._iam_svc = aws.create_client('iam')
44 + self._arn = None
45 +
46 + @property
47 + def name(self):
48 + return self._config['name']
49 +
50 + @property
51 + def arn(self):
52 + if self._arn is None:
53 + try:
54 + response = self._iam_svc.get_role(
55 + RoleName=self.name)
56 + LOG.debug(response)
57 + self._arn = response['Role']['Arn']
58 + except Exception:
59 + LOG.debug('Unable to find ARN for role: %s', self.name)
60 + return self._arn
61 +
62 + def exists(self):
63 + try:
64 + response = self._iam_svc.list_roles(PathPrefix=self.Path)
65 + LOG.debug(response)
66 + for role in response['Roles']:
67 + if role['RoleName'] == self.name:
68 + return role
69 + except Exception:
70 + LOG.exception('Error listing roles')
71 + return None
72 +
73 + def create(self):
74 + LOG.debug('creating role %s', self.name)
75 + role = self.exists()
76 + if not role:
77 + try:
78 + response = self._iam_svc.create_role(
79 + Path=self.Path, RoleName=self.name,
80 + AssumeRolePolicyDocument=AssumeRolePolicyDocument)
81 + LOG.debug(response)
82 + except Exception:
83 + LOG.exception('Error creating Role')
84 +
85 + def delete(self):
86 + LOG.debug('deleting role %s', self.name)
87 + response = self._iam_svc.delete_role(RoleName=self.name)
88 + LOG.debug(response)
89 + return response
90 +
91 + def status(self):
92 + LOG.debug('getting status for role %s', self.name)
93 + try:
94 + response = self._iam_svc.get_role(RoleName=self.name)
95 + LOG.debug(response)
96 + except ClientError:
97 + LOG.debug('role %s not found', self.name)
98 + response = None
99 + return response
1 -# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/
2 -#
3 -# Licensed under the Apache License, Version 2.0 (the "License"). You
4 -# may not use this file except in compliance with the License. A copy of
5 -# the License is located at
6 -#
7 -# http://aws.amazon.com/apache2.0/
8 -#
9 -# or in the "license" file accompanying this file. This file is
10 -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11 -# ANY KIND, either express or implied. See the License for the specific
12 -# language governing permissions and limitations under the License.
13 -
14 -import logging
15 -import time
16 -
17 -import kappa.aws
18 -
19 -LOG = logging.getLogger(__name__)
20 -
21 -
22 -class Stack(object):
23 -
24 - completed_states = ('CREATE_COMPLETE', 'UPDATE_COMPLETE')
25 - failed_states = ('UPDATE_ROLLBACK_COMPLETE', 'ROLLBACK_COMPLETE')
26 -
27 - def __init__(self, context, config):
28 - self._context = context
29 - self._config = config
30 - aws = kappa.aws.get_aws(self._context)
31 - self._cfn = aws.create_client('cloudformation')
32 - self._iam = aws.create_client('iam')
33 -
34 - @property
35 - def name(self):
36 - return self._config['stack_name']
37 -
38 - @property
39 - def template_path(self):
40 - return self._config['template']
41 -
42 - @property
43 - def exec_role(self):
44 - return self._config['exec_role']
45 -
46 - @property
47 - def exec_role_arn(self):
48 - return self._get_role_arn(self.exec_role)
49 -
50 - @property
51 - def invoke_role(self):
52 - return self._config['invoke_role']
53 -
54 - @property
55 - def invoke_role_arn(self):
56 - return self._get_role_arn(self.invoke_role)
57 -
58 - def _get_role_arn(self, role_name):
59 - role_arn = None
60 - try:
61 - resources = self._cfn.list_stack_resources(
62 - StackName=self.name)
63 - LOG.debug(resources)
64 - except Exception:
65 - LOG.exception('Unable to find role ARN: %s', role_name)
66 - for resource in resources['StackResourceSummaries']:
67 - if resource['LogicalResourceId'] == role_name:
68 - role = self._iam.get_role(
69 - RoleName=resource['PhysicalResourceId'])
70 - LOG.debug(role)
71 - role_arn = role['Role']['Arn']
72 - LOG.debug('role_arn: %s', role_arn)
73 - return role_arn
74 -
75 - def exists(self):
76 - """
77 - Does Cloudformation Stack already exist?
78 - """
79 - try:
80 - response = self._cfn.describe_stacks(StackName=self.name)
81 - LOG.debug('Stack %s exists', self.name)
82 - except Exception:
83 - LOG.debug('Stack %s does not exist', self.name)
84 - response = None
85 - return response
86 -
87 - def wait(self):
88 - done = False
89 - while not done:
90 - time.sleep(1)
91 - response = self._cfn.describe_stacks(StackName=self.name)
92 - LOG.debug(response)
93 - status = response['Stacks'][0]['StackStatus']
94 - LOG.debug('Stack status is: %s', status)
95 - if status in self.completed_states:
96 - done = True
97 - if status in self.failed_states:
98 - msg = 'Could not create stack %s: %s' % (self.name, status)
99 - raise ValueError(msg)
100 -
101 - def _create(self):
102 - LOG.debug('create_stack: stack_name=%s', self.name)
103 - template_body = open(self.template_path).read()
104 - try:
105 - response = self._cfn.create_stack(
106 - StackName=self.name, TemplateBody=template_body,
107 - Capabilities=['CAPABILITY_IAM'])
108 - LOG.debug(response)
109 - except Exception:
110 - LOG.exception('Unable to create stack')
111 - self.wait()
112 -
113 - def _update(self):
114 - LOG.debug('create_stack: stack_name=%s', self.name)
115 - template_body = open(self.template_path).read()
116 - try:
117 - response = self._cfn.update_stack(
118 - StackName=self.name, TemplateBody=template_body,
119 - Capabilities=['CAPABILITY_IAM'])
120 - LOG.debug(response)
121 - except Exception as e:
122 - if 'ValidationError' in str(e):
123 - LOG.info('No Updates Required')
124 - else:
125 - LOG.exception('Unable to update stack')
126 - self.wait()
127 -
128 - def update(self):
129 - if self.exists():
130 - self._update()
131 - else:
132 - self._create()
133 -
134 - def status(self):
135 - return self.exists()
136 -
137 - def delete(self):
138 - LOG.debug('delete_stack: stack_name=%s', self.name)
139 - try:
140 - response = self._cfn.delete_stack(StackName=self.name)
141 - LOG.debug(response)
142 - except Exception:
143 - LOG.exception('Unable to delete stack: %s', self.name)
1 -botocore==0.94.0 1 +boto3==0.0.15
2 -click==3.3 2 +click==4.0
3 PyYAML>=3.11 3 PyYAML>=3.11
4 mock>=1.0.1 4 mock>=1.0.1
5 nose==1.3.1 5 nose==1.3.1
......
1 -console.log('Loading event'); 1 +console.log('Loading function');
2 +
2 exports.handler = function(event, context) { 3 exports.handler = function(event, context) {
3 - console.log(JSON.stringify(event, null, ' ')); 4 + console.log(JSON.stringify(event, null, 2));
4 - for(i = 0; i < event.Records.length; ++i) { 5 + event.Records.forEach(function(record) {
5 - encodedPayload = event.Records[i].kinesis.data; 6 + // Kinesis data is base64 encoded so decode here
6 - payload = new Buffer(encodedPayload, 'base64').toString('ascii'); 7 + payload = new Buffer(record.kinesis.data, 'base64').toString('ascii');
7 - console.log("Decoded payload: " + payload); 8 + console.log('Decoded payload:', payload);
8 - } 9 + });
9 - context.done(null, "Hello World"); // SUCCESS with message 10 + context.succeed();
10 }; 11 };
......
1 --- 1 ---
2 profile: personal 2 profile: personal
3 region: us-east-1 3 region: us-east-1
4 -cloudformation: 4 +iam:
5 - template: roles.cf 5 + role_name: KinesisSampleRole
6 - stack_name: TestKinesis 6 + role_policy: AWSLambdaKinesisExecutionRole
7 - exec_role: ExecRole
8 - invoke_role: InvokeRole
9 lambda: 7 lambda:
10 name: KinesisSample 8 name: KinesisSample
11 zipfile_name: KinesisSample.zip 9 zipfile_name: KinesisSample.zip
...@@ -15,9 +13,10 @@ lambda: ...@@ -15,9 +13,10 @@ lambda:
15 runtime: nodejs 13 runtime: nodejs
16 memory_size: 128 14 memory_size: 128
17 timeout: 3 15 timeout: 3
18 - mode: event
19 event_sources: 16 event_sources:
20 - 17 -
21 arn: arn:aws:kinesis:us-east-1:084307701560:stream/lambdastream 18 arn: arn:aws:kinesis:us-east-1:084307701560:stream/lambdastream
19 + starting_position: TRIM_HORIZON
20 + batch_size: 100
22 test_data: input.json 21 test_data: input.json
23 22
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
12 "invokeIdentityArn": "arn:aws:iam::059493405231:role/testLEBRole", 12 "invokeIdentityArn": "arn:aws:iam::059493405231:role/testLEBRole",
13 "eventVersion": "1.0", 13 "eventVersion": "1.0",
14 "eventName": "aws:kinesis:record", 14 "eventName": "aws:kinesis:record",
15 - "eventSourceARN": "arn:aws:kinesis:us-east-1:35667example:stream/examplestream", 15 + "eventSourceARN": "arn:aws:kinesis:us-west-2:35667example:stream/examplestream",
16 - "awsRegion": "us-east-1" 16 + "awsRegion": "us-west-2"
17 } 17 }
18 ] 18 ]
19 } 19 }
......
1 +{
2 + "Version": "2012-10-17",
3 + "Statement":[
4 + {
5 + "Sid":"Stmt1428510662000",
6 + "Effect":"Allow",
7 + "Action":["dynamodb:*"],
8 + "Resource":["arn:aws:dynamodb:us-east-1:084307701560:table/snslambda"]
9 + }
10 + ]
11 +}
1 +---
2 +profile: personal
3 +region: us-east-1
4 +resources: resources.json
5 +iam:
6 + policy:
7 + description: A policy used with the Kappa SNS->DynamoDB example
8 + name: LambdaSNSSamplePolicy
9 + document: LambdaSNSSamplePolicy.json
10 + role:
11 + name: SNSSampleRole
12 + policy: LambdaSNSSamplePolicy
13 +lambda:
14 + name: SNSSample
15 + zipfile_name: SNSSample.zip
16 + description: Testing SNS -> DynamoDB Lambda handler
17 + path: messageStore.js
18 + handler: messageStore.handler
19 + runtime: nodejs
20 + memory_size: 128
21 + timeout: 3
22 + permissions:
23 + -
24 + statement_id: sns_invoke
25 + action: lambda:invokeFunction
26 + principal: sns.amazonaws.com
27 + source_arn: arn:aws:sns:us-east-1:084307701560:lambda_topic
28 + event_sources:
29 + -
30 + arn: arn:aws:sns:us-east-1:084307701560:lambda_topic
31 + test_data: input.json
32 +
...\ No newline at end of file ...\ No newline at end of file
1 +{
2 + "TableName": "snslambda",
3 + "AttributeDefinitions": [
4 + {
5 + "AttributeName": "SnsTopicArn",
6 + "AttributeType": "S"
7 + },
8 + {
9 + "AttributeName": "SnsPublishTime",
10 + "AttributeType": "S"
11 + },
12 + {
13 + "AttributeName": "SnsMessageId",
14 + "AttributeType": "S"
15 + }
16 + ],
17 + "KeySchema": [
18 + {
19 + "AttributeName": "SnsTopicArn",
20 + "KeyType": "HASH"
21 + },
22 + {
23 + "AttributeName": "SnsPublishTime",
24 + "KeyType": "RANGE"
25 + }
26 + ],
27 + "GlobalSecondaryIndexes": [
28 + {
29 + "IndexName": "MesssageIndex",
30 + "KeySchema": [
31 + {
32 + "AttributeName": "SnsMessageId",
33 + "KeyType": "HASH"
34 + }
35 + ],
36 + "Projection": {
37 + "ProjectionType": "ALL"
38 + },
39 + "ProvisionedThroughput": {
40 + "ReadCapacityUnits": 5,
41 + "WriteCapacityUnits": 1
42 + }
43 + }
44 + ],
45 + "ProvisionedThroughput": {
46 + "ReadCapacityUnits": 5,
47 + "WriteCapacityUnits": 5
48 + }
49 +}
1 +console.log('Loading event');
2 +var aws = require('aws-sdk');
3 +var ddb = new aws.DynamoDB({params: {TableName: 'snslambda'}});
4 +
5 +exports.handler = function(event, context) {
6 + var SnsMessageId = event.Records[0].Sns.MessageId;
7 + var SnsPublishTime = event.Records[0].Sns.Timestamp;
8 + var SnsTopicArn = event.Records[0].Sns.TopicArn;
9 + var LambdaReceiveTime = new Date().toString();
10 + var itemParams = {Item: {SnsTopicArn: {S: SnsTopicArn},
11 + SnsPublishTime: {S: SnsPublishTime}, SnsMessageId: {S: SnsMessageId},
12 + LambdaReceiveTime: {S: LambdaReceiveTime} }};
13 + ddb.putItem(itemParams, function() {
14 + context.done(null,'');
15 + });
16 +};
1 +{
2 + "AWSTemplateFormatVersion" : "2010-09-09",
3 +
4 + "Description" : "Creates the DynamoDB Table needed for the example",
5 +
6 + "Resources" : {
7 + "snslambda" : {
8 + "Type" : "AWS::DynamoDB::Table",
9 + "Properties" : {
10 + "AttributeDefinitions": [
11 + {
12 + "AttributeName" : "SnsTopicArn",
13 + "AttributeType" : "S"
14 + },
15 + {
16 + "AttributeName" : "SnsPublishTime",
17 + "AttributeType" : "S"
18 + }
19 + ],
20 + "KeySchema": [
21 + { "AttributeName": "SnsTopicArn", "KeyType": "HASH" },
22 + { "AttributeName": "SnsPublishTime", "KeyType": "RANGE" }
23 + ],
24 + "ProvisionedThroughput" : {
25 + "ReadCapacityUnits" : 5,
26 + "WriteCapacityUnits" : 5
27 + }
28 + }
29 + }
30 + },
31 +
32 + "Outputs" : {
33 + "TableName" : {
34 + "Value" : {"Ref" : "snslambda"},
35 + "Description" : "Table name of the newly created DynamoDB table"
36 + }
37 + }
38 +}
...@@ -5,8 +5,8 @@ from setuptools import setup, find_packages ...@@ -5,8 +5,8 @@ from setuptools import setup, find_packages
5 import os 5 import os
6 6
7 requires = [ 7 requires = [
8 - 'botocore==0.94.0', 8 + 'boto3==0.0.15',
9 - 'click==3.3', 9 + 'click==4.0',
10 'PyYAML>=3.11' 10 'PyYAML>=3.11'
11 ] 11 ]
12 12
......