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.
Showing
18 changed files
with
526 additions
and
229 deletions
... | @@ -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 | ... | ... |
kappa/policy.py
0 → 100644
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() |
kappa/role.py
0 → 100644
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 |
kappa/stack.py
deleted
100644 → 0
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 | -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 | } | ... | ... |
samples/sns/LambdaSNSSamplePolicy.json
0 → 100644
samples/sns/config.yml
0 → 100644
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 |
samples/sns/dynamodb_table.json
0 → 100644
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 | +} |
samples/sns/messageStore.js
0 → 100644
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 | +}; |
samples/sns/resources.json
0 → 100644
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 | ... | ... |
-
Please register or login to post a comment