Another WIP commit. Major changes in the CLI. Also much better detection of ch…
…anges (or no changes) in the code, configuration, policies, etc. when deploying. An attempt to incorporate a test runner that will run unit tests associated with the Lambda function.
Showing
19 changed files
with
435 additions
and
289 deletions
kappa/aws.py
deleted
100644 → 0
1 | -# Copyright (c) 2014,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 boto3 | ||
15 | - | ||
16 | - | ||
17 | -class __AWS(object): | ||
18 | - | ||
19 | - def __init__(self, profile_name=None, region_name=None): | ||
20 | - self._client_cache = {} | ||
21 | - self._session = boto3.session.Session( | ||
22 | - region_name=region_name, profile_name=profile_name) | ||
23 | - | ||
24 | - def create_client(self, client_name): | ||
25 | - if client_name not in self._client_cache: | ||
26 | - self._client_cache[client_name] = self._session.client( | ||
27 | - client_name) | ||
28 | - return self._client_cache[client_name] | ||
29 | - | ||
30 | - | ||
31 | -__Singleton_AWS = None | ||
32 | - | ||
33 | - | ||
34 | -def get_aws(context): | ||
35 | - global __Singleton_AWS | ||
36 | - if __Singleton_AWS is None: | ||
37 | - __Singleton_AWS = __AWS(context.profile, context.region) | ||
38 | - return __Singleton_AWS |
... | @@ -13,33 +13,22 @@ | ... | @@ -13,33 +13,22 @@ |
13 | # limitations under the License. | 13 | # limitations under the License. |
14 | 14 | ||
15 | import logging | 15 | import logging |
16 | -import json | ||
17 | import os | 16 | import os |
18 | 17 | ||
19 | -import datetime | ||
20 | import jmespath | 18 | import jmespath |
21 | import boto3 | 19 | import boto3 |
20 | +import placebo | ||
22 | 21 | ||
23 | 22 | ||
24 | LOG = logging.getLogger(__name__) | 23 | LOG = logging.getLogger(__name__) |
25 | 24 | ||
26 | 25 | ||
27 | -def json_encoder(obj): | ||
28 | - """JSON encoder that formats datetimes as ISO8601 format.""" | ||
29 | - if isinstance(obj, datetime.datetime): | ||
30 | - return obj.isoformat() | ||
31 | - else: | ||
32 | - return obj | ||
33 | - | ||
34 | - | ||
35 | class AWSClient(object): | 26 | class AWSClient(object): |
36 | 27 | ||
37 | - def __init__(self, service_name, region_name, profile_name, | 28 | + def __init__(self, service_name, region_name, profile_name): |
38 | - record_path=None): | ||
39 | self._service_name = service_name | 29 | self._service_name = service_name |
40 | self._region_name = region_name | 30 | self._region_name = region_name |
41 | self._profile_name = profile_name | 31 | self._profile_name = profile_name |
42 | - self._record_path = record_path | ||
43 | self._client = self._create_client() | 32 | self._client = self._create_client() |
44 | 33 | ||
45 | @property | 34 | @property |
... | @@ -54,43 +43,12 @@ class AWSClient(object): | ... | @@ -54,43 +43,12 @@ class AWSClient(object): |
54 | def profile_name(self): | 43 | def profile_name(self): |
55 | return self._profile_name | 44 | return self._profile_name |
56 | 45 | ||
57 | - def _record(self, op_name, kwargs, data): | ||
58 | - """ | ||
59 | - This is a little hack to enable easier unit testing of the code. | ||
60 | - Since botocore/boto3 has its own set of tests, I'm not interested in | ||
61 | - trying to test it again here. So, this recording capability allows | ||
62 | - us to save the data coming back from botocore as JSON files which | ||
63 | - can then be used by the mocked awsclient in the unit test directory. | ||
64 | - To enable this, pass in a record_path to the contructor and the JSON | ||
65 | - data files will get stored in this path. | ||
66 | - """ | ||
67 | - if self._record_path: | ||
68 | - path = os.path.expanduser(self._record_path) | ||
69 | - path = os.path.expandvars(path) | ||
70 | - path = os.path.join(path, self.service_name) | ||
71 | - if not os.path.isdir(path): | ||
72 | - os.mkdir(path) | ||
73 | - path = os.path.join(path, self.region_name) | ||
74 | - if not os.path.isdir(path): | ||
75 | - os.mkdir(path) | ||
76 | - path = os.path.join(path, self.account_id) | ||
77 | - if not os.path.isdir(path): | ||
78 | - os.mkdir(path) | ||
79 | - filename = op_name | ||
80 | - if kwargs: | ||
81 | - for k, v in kwargs.items(): | ||
82 | - if k != 'query': | ||
83 | - filename += '_{}_{}'.format(k, v) | ||
84 | - filename += '.json' | ||
85 | - path = os.path.join(path, filename) | ||
86 | - with open(path, 'wb') as fp: | ||
87 | - json.dump(data, fp, indent=4, default=json_encoder, | ||
88 | - ensure_ascii=False) | ||
89 | - | ||
90 | def _create_client(self): | 46 | def _create_client(self): |
91 | session = boto3.session.Session( | 47 | session = boto3.session.Session( |
92 | region_name=self._region_name, profile_name=self._profile_name) | 48 | region_name=self._region_name, profile_name=self._profile_name) |
93 | - return session.client(self._service_name) | 49 | + placebo.attach(session) |
50 | + client = session.client(self._service_name) | ||
51 | + return client | ||
94 | 52 | ||
95 | def call(self, op_name, query=None, **kwargs): | 53 | def call(self, op_name, query=None, **kwargs): |
96 | """ | 54 | """ |
... | @@ -131,20 +89,36 @@ class AWSClient(object): | ... | @@ -131,20 +89,36 @@ class AWSClient(object): |
131 | data = op(**kwargs) | 89 | data = op(**kwargs) |
132 | if query: | 90 | if query: |
133 | data = query.search(data) | 91 | data = query.search(data) |
134 | - self._record(op_name, kwargs, data) | ||
135 | return data | 92 | return data |
136 | 93 | ||
137 | 94 | ||
138 | _client_cache = {} | 95 | _client_cache = {} |
139 | 96 | ||
140 | 97 | ||
98 | +def save_recordings(recording_path): | ||
99 | + for key in _client_cache: | ||
100 | + client = _client_cache[key] | ||
101 | + full_path = os.path.join(recording_path, '{}.json'.format(key)) | ||
102 | + client._client.meta.placebo.save(full_path) | ||
103 | + | ||
104 | + | ||
141 | def create_client(service_name, context): | 105 | def create_client(service_name, context): |
142 | global _client_cache | 106 | global _client_cache |
143 | client_key = '{}:{}:{}'.format(service_name, context.region, | 107 | client_key = '{}:{}:{}'.format(service_name, context.region, |
144 | context.profile) | 108 | context.profile) |
145 | if client_key not in _client_cache: | 109 | if client_key not in _client_cache: |
146 | - _client_cache[client_key] = AWSClient(service_name, | 110 | + client = AWSClient(service_name, context.region, |
147 | - context.region, | 111 | + context.profile) |
148 | - context.profile, | 112 | + if 'placebo' in context.config: |
149 | - context.record_path) | 113 | + placebo_cfg = context.config['placebo'] |
114 | + if placebo_cfg.get('mode') == 'play': | ||
115 | + full_path = os.path.join( | ||
116 | + placebo_cfg['recording_path'], | ||
117 | + '{}.json'.format(client_key)) | ||
118 | + if os.path.exists(full_path): | ||
119 | + client._client.meta.placebo.load(full_path) | ||
120 | + client._client.meta.placebo.start() | ||
121 | + elif placebo_cfg['mode'] == 'record': | ||
122 | + client._client.meta.placebo.record() | ||
123 | + _client_cache[client_key] = client | ||
150 | return _client_cache[client_key] | 124 | return _client_cache[client_key] | ... | ... |
... | @@ -16,6 +16,7 @@ import logging | ... | @@ -16,6 +16,7 @@ import logging |
16 | import yaml | 16 | import yaml |
17 | import time | 17 | import time |
18 | import os | 18 | import os |
19 | +import shutil | ||
19 | 20 | ||
20 | import kappa.function | 21 | import kappa.function |
21 | import kappa.event_source | 22 | import kappa.event_source |
... | @@ -57,26 +58,28 @@ class Context(object): | ... | @@ -57,26 +58,28 @@ class Context(object): |
57 | with open(cache_file, 'rb') as fp: | 58 | with open(cache_file, 'rb') as fp: |
58 | self.cache = yaml.load(fp) | 59 | self.cache = yaml.load(fp) |
59 | 60 | ||
60 | - def save_cache(self): | 61 | + def _delete_cache(self): |
62 | + if os.path.isdir('.kappa'): | ||
63 | + shutil.rmtree('.kappa') | ||
64 | + self.cache = {} | ||
65 | + | ||
66 | + def _save_cache(self): | ||
61 | if not os.path.isdir('.kappa'): | 67 | if not os.path.isdir('.kappa'): |
62 | os.mkdir('.kappa') | 68 | os.mkdir('.kappa') |
63 | cache_file = os.path.join('.kappa', 'cache') | 69 | cache_file = os.path.join('.kappa', 'cache') |
64 | with open(cache_file, 'wb') as fp: | 70 | with open(cache_file, 'wb') as fp: |
65 | yaml.dump(self.cache, fp) | 71 | yaml.dump(self.cache, fp) |
66 | 72 | ||
67 | - @property | 73 | + def get_cache_value(self, key): |
68 | - def name(self): | 74 | + return self.cache.setdefault(self.environment, dict()).get(key) |
69 | - return '{}-{}-v{}'.format(self.base_name, | ||
70 | - self.environment, | ||
71 | - self.version) | ||
72 | 75 | ||
73 | - @property | 76 | + def set_cache_value(self, key, value): |
74 | - def base_name(self): | 77 | + self.cache.setdefault(self.environment, dict())[key] = value |
75 | - return self.config.get('base_name') | 78 | + self._save_cache() |
76 | 79 | ||
77 | @property | 80 | @property |
78 | - def version(self): | 81 | + def name(self): |
79 | - return self.config.get('version') | 82 | + return '{}-{}'.format(self.config['name'], self.environment) |
80 | 83 | ||
81 | @property | 84 | @property |
82 | def profile(self): | 85 | def profile(self): |
... | @@ -87,14 +90,23 @@ class Context(object): | ... | @@ -87,14 +90,23 @@ class Context(object): |
87 | return self.config['environments'][self.environment]['region'] | 90 | return self.config['environments'][self.environment]['region'] |
88 | 91 | ||
89 | @property | 92 | @property |
90 | - def record_path(self): | 93 | + def record(self): |
91 | - return self.config.get('record_path') | 94 | + return self.config.get('record', False) |
92 | 95 | ||
93 | @property | 96 | @property |
94 | def lambda_config(self): | 97 | def lambda_config(self): |
95 | return self.config.get('lambda') | 98 | return self.config.get('lambda') |
96 | 99 | ||
97 | @property | 100 | @property |
101 | + def test_dir(self): | ||
102 | + return self.config.get('tests', '_tests') | ||
103 | + | ||
104 | + @property | ||
105 | + def unit_test_runner(self): | ||
106 | + return self.config.get('unit_test_runner', | ||
107 | + 'nosetests . ../_tests/unit/') | ||
108 | + | ||
109 | + @property | ||
98 | def exec_role_arn(self): | 110 | def exec_role_arn(self): |
99 | return self.role.arn | 111 | return self.role.arn |
100 | 112 | ||
... | @@ -179,14 +191,20 @@ class Context(object): | ... | @@ -179,14 +191,20 @@ class Context(object): |
179 | time.sleep(5) | 191 | time.sleep(5) |
180 | self.function.deploy() | 192 | self.function.deploy() |
181 | 193 | ||
182 | - def update_code(self): | 194 | + def invoke(self, data): |
183 | - self.function.update() | 195 | + return self.function.invoke(data) |
184 | 196 | ||
185 | - def invoke(self): | 197 | + def unit_tests(self): |
186 | - return self.function.invoke() | 198 | + # run any unit tests |
199 | + unit_test_path = os.path.join(self.test_dir, 'unit') | ||
200 | + if os.path.exists(unit_test_path): | ||
201 | + os.chdir(self.function.path) | ||
202 | + print('running unit tests') | ||
203 | + pipe = os.popen(self.unit_test_runner, 'r') | ||
204 | + print(pipe.read()) | ||
187 | 205 | ||
188 | def test(self): | 206 | def test(self): |
189 | - return self.function.invoke() | 207 | + return self.unit_tests() |
190 | 208 | ||
191 | def dryrun(self): | 209 | def dryrun(self): |
192 | return self.function.dryrun() | 210 | return self.function.dryrun() |
... | @@ -197,6 +215,9 @@ class Context(object): | ... | @@ -197,6 +215,9 @@ class Context(object): |
197 | def tail(self): | 215 | def tail(self): |
198 | return self.function.tail() | 216 | return self.function.tail() |
199 | 217 | ||
218 | + def tag(self, name, description): | ||
219 | + return self.function.tag(name, description) | ||
220 | + | ||
200 | def delete(self): | 221 | def delete(self): |
201 | for event_source in self.event_sources: | 222 | for event_source in self.event_sources: |
202 | event_source.remove(self.function) | 223 | event_source.remove(self.function) |
... | @@ -208,6 +229,7 @@ class Context(object): | ... | @@ -208,6 +229,7 @@ class Context(object): |
208 | time.sleep(5) | 229 | time.sleep(5) |
209 | if self.policy: | 230 | if self.policy: |
210 | self.policy.delete() | 231 | self.policy.delete() |
232 | + self._delete_cache() | ||
211 | 233 | ||
212 | def status(self): | 234 | def status(self): |
213 | status = {} | 235 | status = {} | ... | ... |
... | @@ -17,6 +17,7 @@ import os | ... | @@ -17,6 +17,7 @@ import os |
17 | import zipfile | 17 | import zipfile |
18 | import time | 18 | import time |
19 | import shutil | 19 | import shutil |
20 | +import hashlib | ||
20 | 21 | ||
21 | from botocore.exceptions import ClientError | 22 | from botocore.exceptions import ClientError |
22 | 23 | ||
... | @@ -66,11 +67,11 @@ class Function(object): | ... | @@ -66,11 +67,11 @@ class Function(object): |
66 | 67 | ||
67 | @property | 68 | @property |
68 | def path(self): | 69 | def path(self): |
69 | - return self._config['path'] | 70 | + return self._config.get('path', '_src') |
70 | 71 | ||
71 | @property | 72 | @property |
72 | - def test_data(self): | 73 | + def tests(self): |
73 | - return self._config['test_data'] | 74 | + return self._config.get('tests', '_tests') |
74 | 75 | ||
75 | @property | 76 | @property |
76 | def permissions(self): | 77 | def permissions(self): |
... | @@ -87,34 +88,79 @@ class Function(object): | ... | @@ -87,34 +88,79 @@ class Function(object): |
87 | LOG.debug('Unable to find ARN for function: %s', self.name) | 88 | LOG.debug('Unable to find ARN for function: %s', self.name) |
88 | return self._response | 89 | return self._response |
89 | 90 | ||
90 | - @property | 91 | + def _get_response_configuration(self, key, default=None): |
91 | - def code_sha_256(self): | 92 | + value = None |
92 | response = self._get_response() | 93 | response = self._get_response() |
93 | - return response['Configuration']['CodeSha256'] | 94 | + if response: |
95 | + if 'Configuration' in response: | ||
96 | + value = response['Configuration'].get(key, default) | ||
97 | + return value | ||
98 | + | ||
99 | + def _get_response_code(self, key, default=None): | ||
100 | + value = None | ||
101 | + response = self._get_response | ||
102 | + if response: | ||
103 | + if 'Configuration' in response: | ||
104 | + value = response['Configuration'].get(key, default) | ||
105 | + return value | ||
94 | 106 | ||
95 | @property | 107 | @property |
96 | - def arn(self): | 108 | + def code_sha_256(self): |
97 | - response = self._get_response() | 109 | + return self._get_response_configuration('CodeSha256') |
98 | - return response['Configuration']['FunctionArn'] | ||
99 | 110 | ||
100 | @property | 111 | @property |
101 | - def version(self): | 112 | + def arn(self): |
102 | - response = self._get_response() | 113 | + return self._get_response_configuration('FunctionArn') |
103 | - return response['Configuration']['Version'] | ||
104 | 114 | ||
105 | @property | 115 | @property |
106 | def repository_type(self): | 116 | def repository_type(self): |
107 | - response = self._get_response() | 117 | + return self._get_response_code('RepositoryType') |
108 | - return response['Code']['RepositoryType'] | ||
109 | 118 | ||
110 | @property | 119 | @property |
111 | def location(self): | 120 | def location(self): |
112 | - response = self._get_response() | 121 | + return self._get_response_code('Location') |
113 | - return response['Code']['Location'] | 122 | + |
123 | + @property | ||
124 | + def version(self): | ||
125 | + return self._get_response_configuration('Version') | ||
114 | 126 | ||
115 | def exists(self): | 127 | def exists(self): |
116 | return self._get_response() | 128 | return self._get_response() |
117 | 129 | ||
130 | + def _check_function_md5(self): | ||
131 | + changed = True | ||
132 | + self._copy_config_file() | ||
133 | + self.zip_lambda_function(self.zipfile_name, self.path) | ||
134 | + m = hashlib.md5() | ||
135 | + with open(self.zipfile_name, 'rb') as fp: | ||
136 | + m.update(fp.read()) | ||
137 | + zip_md5 = m.hexdigest() | ||
138 | + cached_md5 = self._context.get_cache_value('zip_md5') | ||
139 | + LOG.debug('zip_md5: %s', zip_md5) | ||
140 | + LOG.debug('cached md5: %s', cached_md5) | ||
141 | + if zip_md5 != cached_md5: | ||
142 | + self._context.set_cache_value('zip_md5', zip_md5) | ||
143 | + else: | ||
144 | + changed = False | ||
145 | + LOG.info('function unchanged') | ||
146 | + return changed | ||
147 | + | ||
148 | + def _check_config_md5(self): | ||
149 | + m = hashlib.md5() | ||
150 | + m.update(self.description) | ||
151 | + m.update(self.handler) | ||
152 | + m.update(str(self.memory_size)) | ||
153 | + m.update(self._context.exec_role_arn) | ||
154 | + m.update(str(self.timeout)) | ||
155 | + config_md5 = m.hexdigest() | ||
156 | + cached_md5 = self._context.get_cache_value('config_md5') | ||
157 | + if config_md5 != cached_md5: | ||
158 | + self._context.set_cache_value('config_md5', config_md5) | ||
159 | + changed = True | ||
160 | + else: | ||
161 | + changed = False | ||
162 | + return changed | ||
163 | + | ||
118 | @property | 164 | @property |
119 | def log(self): | 165 | def log(self): |
120 | if self._log is None: | 166 | if self._log is None: |
... | @@ -181,13 +227,13 @@ class Function(object): | ... | @@ -181,13 +227,13 @@ class Function(object): |
181 | config_path = os.path.join(self.path, config_name) | 227 | config_path = os.path.join(self.path, config_name) |
182 | if os.path.exists(config_path): | 228 | if os.path.exists(config_path): |
183 | dest_path = os.path.join(self.path, 'config.json') | 229 | dest_path = os.path.join(self.path, 'config.json') |
184 | - LOG.info('copy %s to %s', config_path, dest_path) | 230 | + LOG.debug('copy %s to %s', config_path, dest_path) |
185 | - shutil.copyfile(config_path, dest_path) | 231 | + shutil.copy2(config_path, dest_path) |
186 | 232 | ||
187 | def create(self): | 233 | def create(self): |
188 | LOG.info('creating function %s', self.name) | 234 | LOG.info('creating function %s', self.name) |
189 | - self._copy_config_file() | 235 | + self._check_function_md5() |
190 | - self.zip_lambda_function(self.zipfile_name, self.path) | 236 | + self._check_config_md5() |
191 | with open(self.zipfile_name, 'rb') as fp: | 237 | with open(self.zipfile_name, 'rb') as fp: |
192 | exec_role = self._context.exec_role_arn | 238 | exec_role = self._context.exec_role_arn |
193 | LOG.debug('exec_role=%s', exec_role) | 239 | LOG.debug('exec_role=%s', exec_role) |
... | @@ -202,29 +248,17 @@ class Function(object): | ... | @@ -202,29 +248,17 @@ class Function(object): |
202 | Handler=self.handler, | 248 | Handler=self.handler, |
203 | Description=self.description, | 249 | Description=self.description, |
204 | Timeout=self.timeout, | 250 | Timeout=self.timeout, |
205 | - MemorySize=self.memory_size) | 251 | + MemorySize=self.memory_size, |
252 | + Publish=True) | ||
206 | LOG.debug(response) | 253 | LOG.debug(response) |
207 | except Exception: | 254 | except Exception: |
208 | LOG.exception('Unable to upload zip file') | 255 | LOG.exception('Unable to upload zip file') |
209 | self.add_permissions() | 256 | self.add_permissions() |
210 | 257 | ||
211 | - def _do_update(self): | ||
212 | - do_update = False | ||
213 | - if self._context.force: | ||
214 | - do_update = True | ||
215 | - else: | ||
216 | - stats = os.stat(self.zipfile_name) | ||
217 | - if self._context.cache.get('zipfile_size') != stats.st_size: | ||
218 | - self._context.cache['zipfile_size'] = stats.st_size | ||
219 | - do_update = True | ||
220 | - return do_update | ||
221 | - | ||
222 | def update(self): | 258 | def update(self): |
223 | LOG.info('updating %s', self.name) | 259 | LOG.info('updating %s', self.name) |
224 | - self._copy_config_file() | 260 | + if self._check_function_md5(): |
225 | - self.zip_lambda_function(self.zipfile_name, self.path) | 261 | + self._response = None |
226 | - if self._do_update(): | ||
227 | - self._context.save_cache() | ||
228 | with open(self.zipfile_name, 'rb') as fp: | 262 | with open(self.zipfile_name, 'rb') as fp: |
229 | try: | 263 | try: |
230 | LOG.info('uploading new function zipfile %s', | 264 | LOG.info('uploading new function zipfile %s', |
... | @@ -233,33 +267,40 @@ class Function(object): | ... | @@ -233,33 +267,40 @@ class Function(object): |
233 | response = self._lambda_client.call( | 267 | response = self._lambda_client.call( |
234 | 'update_function_code', | 268 | 'update_function_code', |
235 | FunctionName=self.name, | 269 | FunctionName=self.name, |
236 | - ZipFile=zipdata) | 270 | + ZipFile=zipdata, |
271 | + Publish=True) | ||
237 | LOG.debug(response) | 272 | LOG.debug(response) |
238 | except Exception: | 273 | except Exception: |
239 | LOG.exception('unable to update zip file') | 274 | LOG.exception('unable to update zip file') |
240 | - else: | ||
241 | - LOG.info('function has not changed') | ||
242 | - | ||
243 | - def deploy(self): | ||
244 | - if self.exists(): | ||
245 | - return self.update() | ||
246 | - return self.create() | ||
247 | 275 | ||
248 | - def publish_version(self, description): | 276 | + def update_configuration(self): |
249 | - LOG.info('publishing version of %s', self.name) | 277 | + if self._check_config_md5(): |
278 | + self._response = None | ||
279 | + LOG.info('updating configuration for %s', self.name) | ||
280 | + exec_role = self._context.exec_role_arn | ||
281 | + LOG.debug('exec_role=%s', exec_role) | ||
250 | try: | 282 | try: |
251 | response = self._lambda_client.call( | 283 | response = self._lambda_client.call( |
252 | - 'publish_version', | 284 | + 'update_function_configuration', |
253 | FunctionName=self.name, | 285 | FunctionName=self.name, |
254 | - CodeSha256=self.code_sha_256, | 286 | + Role=exec_role, |
255 | - Description=description) | 287 | + Handler=self.handler, |
288 | + Description=self.description, | ||
289 | + Timeout=self.timeout, | ||
290 | + MemorySize=self.memory_size) | ||
256 | LOG.debug(response) | 291 | LOG.debug(response) |
257 | except Exception: | 292 | except Exception: |
258 | - LOG.exception('Unable to publish version') | 293 | + LOG.exception('unable to update function configuration') |
259 | - return response['Version'] | 294 | + else: |
295 | + LOG.info('function configuration has not changed') | ||
296 | + | ||
297 | + def deploy(self): | ||
298 | + if self.exists(): | ||
299 | + self.update_configuration() | ||
300 | + return self.update() | ||
301 | + return self.create() | ||
260 | 302 | ||
261 | def list_versions(self): | 303 | def list_versions(self): |
262 | - LOG.info('listing versions of %s', self.name) | ||
263 | try: | 304 | try: |
264 | response = self._lambda_client.call( | 305 | response = self._lambda_client.call( |
265 | 'list_versions_by_function', | 306 | 'list_versions_by_function', |
... | @@ -270,15 +311,26 @@ class Function(object): | ... | @@ -270,15 +311,26 @@ class Function(object): |
270 | return response['Versions'] | 311 | return response['Versions'] |
271 | 312 | ||
272 | def create_alias(self, name, description, version=None): | 313 | def create_alias(self, name, description, version=None): |
273 | - LOG.info('creating alias of %s', self.name) | 314 | + # Find the current (latest) version by version number |
274 | - if version is None: | 315 | + # First find the SHA256 of $LATEST |
275 | - version = self.version | 316 | + if not version: |
317 | + versions = self.list_versions() | ||
318 | + for version in versions: | ||
319 | + if version['Version'] == '$LATEST': | ||
320 | + latest_sha256 = version['CodeSha256'] | ||
321 | + break | ||
322 | + for version in versions: | ||
323 | + if version['Version'] != '$LATEST': | ||
324 | + if version['CodeSha256'] == latest_sha256: | ||
325 | + version = version['Version'] | ||
326 | + break | ||
276 | try: | 327 | try: |
328 | + LOG.info('creating alias %s=%s', name, version) | ||
277 | response = self._lambda_client.call( | 329 | response = self._lambda_client.call( |
278 | 'create_alias', | 330 | 'create_alias', |
279 | FunctionName=self.name, | 331 | FunctionName=self.name, |
280 | Description=description, | 332 | Description=description, |
281 | - FunctionVersion=self.version, | 333 | + FunctionVersion=version, |
282 | Name=name) | 334 | Name=name) |
283 | LOG.debug(response) | 335 | LOG.debug(response) |
284 | except Exception: | 336 | except Exception: |
... | @@ -297,8 +349,7 @@ class Function(object): | ... | @@ -297,8 +349,7 @@ class Function(object): |
297 | return response['Versions'] | 349 | return response['Versions'] |
298 | 350 | ||
299 | def tag(self, name, description): | 351 | def tag(self, name, description): |
300 | - version = self.publish_version(description) | 352 | + self.create_alias(name, description) |
301 | - self.create_alias(name, description, version) | ||
302 | 353 | ||
303 | def delete(self): | 354 | def delete(self): |
304 | LOG.info('deleting function %s', self.name) | 355 | LOG.info('deleting function %s', self.name) |
... | @@ -332,17 +383,14 @@ class Function(object): | ... | @@ -332,17 +383,14 @@ class Function(object): |
332 | InvokeArgs=fp) | 383 | InvokeArgs=fp) |
333 | LOG.debug(response) | 384 | LOG.debug(response) |
334 | 385 | ||
335 | - def _invoke(self, test_data, invocation_type): | 386 | + def _invoke(self, data, invocation_type): |
336 | - if test_data is None: | 387 | + LOG.debug('invoke %s as %s', self.name, invocation_type) |
337 | - test_data = self.test_data | ||
338 | - LOG.debug('invoke %s', test_data) | ||
339 | - with open(test_data) as fp: | ||
340 | response = self._lambda_client.call( | 388 | response = self._lambda_client.call( |
341 | 'invoke', | 389 | 'invoke', |
342 | FunctionName=self.name, | 390 | FunctionName=self.name, |
343 | InvocationType=invocation_type, | 391 | InvocationType=invocation_type, |
344 | LogType='Tail', | 392 | LogType='Tail', |
345 | - Payload=fp.read()) | 393 | + Payload=data) |
346 | LOG.debug(response) | 394 | LOG.debug(response) |
347 | return response | 395 | return response |
348 | 396 | ... | ... |
... | @@ -131,11 +131,11 @@ class Policy(object): | ... | @@ -131,11 +131,11 @@ class Policy(object): |
131 | m = hashlib.md5() | 131 | m = hashlib.md5() |
132 | m.update(document) | 132 | m.update(document) |
133 | policy_md5 = m.hexdigest() | 133 | policy_md5 = m.hexdigest() |
134 | + cached_md5 = self._context.get_cache_value('policy_md5') | ||
134 | LOG.debug('policy_md5: %s', policy_md5) | 135 | LOG.debug('policy_md5: %s', policy_md5) |
135 | - LOG.debug('cache md5: %s', self._context.cache.get('policy_md5')) | 136 | + LOG.debug('cached md5: %s', cached_md5) |
136 | - if policy_md5 != self._context.cache.get('policy_md5'): | 137 | + if policy_md5 != cached_md5: |
137 | - self._context.cache['policy_md5'] = policy_md5 | 138 | + self._context.set_cache_value('policy_md5', policy_md5) |
138 | - self._context.save_cache() | ||
139 | self._add_policy_version() | 139 | self._add_policy_version() |
140 | else: | 140 | else: |
141 | LOG.info('policy unchanged') | 141 | LOG.info('policy unchanged') |
... | @@ -158,6 +158,20 @@ class Policy(object): | ... | @@ -158,6 +158,20 @@ class Policy(object): |
158 | document = self.document() | 158 | document = self.document() |
159 | if self.arn and document: | 159 | if self.arn and document: |
160 | LOG.info('deleting policy %s', self.name) | 160 | LOG.info('deleting policy %s', self.name) |
161 | + LOG.info('deleting all policy versions for %s', self.name) | ||
162 | + versions = self._list_versions() | ||
163 | + for version in versions: | ||
164 | + LOG.debug('deleting version %s', version['VersionId']) | ||
165 | + if not version['IsDefaultVersion']: | ||
166 | + try: | ||
167 | + response = self._iam_client.call( | ||
168 | + 'delete_policy_version', | ||
169 | + PolicyArn=self.arn, | ||
170 | + VersionId=version['VersionId']) | ||
171 | + except Exception: | ||
172 | + LOG.exception('Unable to delete policy version %s', | ||
173 | + version['VersionId']) | ||
174 | + LOG.debug('now delete policy') | ||
161 | response = self._iam_client.call( | 175 | response = self._iam_client.call( |
162 | 'delete_policy', PolicyArn=self.arn) | 176 | 'delete_policy', PolicyArn=self.arn) |
163 | LOG.debug(response) | 177 | LOG.debug(response) | ... | ... |
... | @@ -19,12 +19,16 @@ import click | ... | @@ -19,12 +19,16 @@ import click |
19 | 19 | ||
20 | from kappa.context import Context | 20 | from kappa.context import Context |
21 | 21 | ||
22 | +pass_ctx = click.make_pass_decorator(Context) | ||
23 | + | ||
22 | 24 | ||
23 | @click.group() | 25 | @click.group() |
24 | -@click.argument( | 26 | +@click.option( |
25 | - 'config', | 27 | + '--config', |
28 | + default='kappa.yml', | ||
26 | type=click.File('rb'), | 29 | type=click.File('rb'), |
27 | envvar='KAPPA_CONFIG', | 30 | envvar='KAPPA_CONFIG', |
31 | + help='Name of config file (default is kappa.yml)' | ||
28 | ) | 32 | ) |
29 | @click.option( | 33 | @click.option( |
30 | '--debug/--no-debug', | 34 | '--debug/--no-debug', |
... | @@ -32,117 +36,63 @@ from kappa.context import Context | ... | @@ -32,117 +36,63 @@ from kappa.context import Context |
32 | help='Turn on debugging output' | 36 | help='Turn on debugging output' |
33 | ) | 37 | ) |
34 | @click.option( | 38 | @click.option( |
35 | - '--environment', | 39 | + '--env', |
36 | - help='Specify which environment to work with' | 40 | + default='dev', |
37 | -) | 41 | + help='Specify which environment to work with (default dev)' |
38 | -@click.option( | ||
39 | - '--force/--no-force', | ||
40 | - default=False, | ||
41 | - help='Force an update of the Lambda function' | ||
42 | ) | 42 | ) |
43 | @click.pass_context | 43 | @click.pass_context |
44 | -def cli(ctx, config=None, debug=False, environment=None, force=None): | 44 | +def cli(ctx, config=None, debug=False, env=None): |
45 | - config = config | 45 | + ctx.obj = Context(config, env, debug) |
46 | - ctx.obj['debug'] = debug | ||
47 | - ctx.obj['config'] = config | ||
48 | - ctx.obj['environment'] = environment | ||
49 | - ctx.obj['force'] = force | ||
50 | 46 | ||
51 | 47 | ||
52 | @cli.command() | 48 | @cli.command() |
53 | -@click.pass_context | 49 | +@pass_ctx |
54 | def deploy(ctx): | 50 | def deploy(ctx): |
55 | """Deploy the Lambda function and any policies and roles required""" | 51 | """Deploy the Lambda function and any policies and roles required""" |
56 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
57 | - ctx.obj['debug'], ctx.obj['force']) | ||
58 | click.echo('deploying') | 52 | click.echo('deploying') |
59 | - context.deploy() | 53 | + ctx.deploy() |
60 | - click.echo('done') | ||
61 | - | ||
62 | - | ||
63 | -@cli.command() | ||
64 | -@click.pass_context | ||
65 | -def tag(ctx): | ||
66 | - """Deploy the Lambda function and any policies and roles required""" | ||
67 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
68 | - ctx.obj['debug'], ctx.obj['force']) | ||
69 | - click.echo('tagging') | ||
70 | - context.deploy() | ||
71 | click.echo('done') | 54 | click.echo('done') |
72 | 55 | ||
73 | 56 | ||
74 | @cli.command() | 57 | @cli.command() |
75 | -@click.pass_context | 58 | +@click.argument('data_file', type=click.File('r')) |
76 | -def invoke(ctx): | 59 | +@pass_ctx |
60 | +def invoke(ctx, data_file): | ||
77 | """Invoke the command synchronously""" | 61 | """Invoke the command synchronously""" |
78 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
79 | - ctx.obj['debug'], ctx.obj['force']) | ||
80 | click.echo('invoking') | 62 | click.echo('invoking') |
81 | - response = context.invoke() | 63 | + response = ctx.invoke(data_file.read()) |
82 | log_data = base64.b64decode(response['LogResult']) | 64 | log_data = base64.b64decode(response['LogResult']) |
83 | click.echo(log_data) | 65 | click.echo(log_data) |
66 | + click.echo('Response:') | ||
84 | click.echo(response['Payload'].read()) | 67 | click.echo(response['Payload'].read()) |
85 | click.echo('done') | 68 | click.echo('done') |
86 | 69 | ||
87 | 70 | ||
88 | @cli.command() | 71 | @cli.command() |
89 | -@click.pass_context | 72 | +@pass_ctx |
90 | def test(ctx): | 73 | def test(ctx): |
91 | """Test the command synchronously""" | 74 | """Test the command synchronously""" |
92 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
93 | - ctx.obj['debug'], ctx.obj['force']) | ||
94 | click.echo('testing') | 75 | click.echo('testing') |
95 | - response = context.test() | 76 | + ctx.test() |
96 | - log_data = base64.b64decode(response['LogResult']) | ||
97 | - click.echo(log_data) | ||
98 | - click.echo(response['Payload'].read()) | ||
99 | - click.echo('done') | ||
100 | - | ||
101 | - | ||
102 | -@cli.command() | ||
103 | -@click.pass_context | ||
104 | -def dryrun(ctx): | ||
105 | - """Show you what would happen but don't actually do anything""" | ||
106 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
107 | - ctx.obj['debug'], ctx.obj['force']) | ||
108 | - click.echo('invoking dryrun') | ||
109 | - response = context.dryrun() | ||
110 | - click.echo(response) | ||
111 | - click.echo('done') | ||
112 | - | ||
113 | - | ||
114 | -@cli.command() | ||
115 | -@click.pass_context | ||
116 | -def invoke_async(ctx): | ||
117 | - """Invoke the Lambda function asynchronously""" | ||
118 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
119 | - ctx.obj['debug'], ctx.obj['force']) | ||
120 | - click.echo('invoking async') | ||
121 | - response = context.invoke_async() | ||
122 | - click.echo(response) | ||
123 | click.echo('done') | 77 | click.echo('done') |
124 | 78 | ||
125 | 79 | ||
126 | @cli.command() | 80 | @cli.command() |
127 | -@click.pass_context | 81 | +@pass_ctx |
128 | def tail(ctx): | 82 | def tail(ctx): |
129 | """Show the last 10 lines of the log file""" | 83 | """Show the last 10 lines of the log file""" |
130 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
131 | - ctx.obj['debug'], ctx.obj['force']) | ||
132 | click.echo('tailing logs') | 84 | click.echo('tailing logs') |
133 | - for e in context.tail()[-10:]: | 85 | + for e in ctx.tail()[-10:]: |
134 | ts = datetime.utcfromtimestamp(e['timestamp']//1000).isoformat() | 86 | ts = datetime.utcfromtimestamp(e['timestamp']//1000).isoformat() |
135 | click.echo("{}: {}".format(ts, e['message'])) | 87 | click.echo("{}: {}".format(ts, e['message'])) |
136 | click.echo('done') | 88 | click.echo('done') |
137 | 89 | ||
138 | 90 | ||
139 | @cli.command() | 91 | @cli.command() |
140 | -@click.pass_context | 92 | +@pass_ctx |
141 | def status(ctx): | 93 | def status(ctx): |
142 | """Print a status of this Lambda function""" | 94 | """Print a status of this Lambda function""" |
143 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | 95 | + status = ctx.status() |
144 | - ctx.obj['debug']) | ||
145 | - status = context.status() | ||
146 | click.echo(click.style('Policy', bold=True)) | 96 | click.echo(click.style('Policy', bold=True)) |
147 | if status['policy']: | 97 | if status['policy']: |
148 | line = ' {} ({})'.format( | 98 | line = ' {} ({})'.format( |
... | @@ -175,58 +125,46 @@ def status(ctx): | ... | @@ -175,58 +125,46 @@ def status(ctx): |
175 | 125 | ||
176 | 126 | ||
177 | @cli.command() | 127 | @cli.command() |
178 | -@click.pass_context | 128 | +@pass_ctx |
179 | def delete(ctx): | 129 | def delete(ctx): |
180 | """Delete the Lambda function and related policies and roles""" | 130 | """Delete the Lambda function and related policies and roles""" |
181 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
182 | - ctx.obj['debug'], ctx.obj['force']) | ||
183 | click.echo('deleting') | 131 | click.echo('deleting') |
184 | - context.delete() | 132 | + ctx.delete() |
185 | click.echo('done') | 133 | click.echo('done') |
186 | 134 | ||
187 | 135 | ||
188 | @cli.command() | 136 | @cli.command() |
189 | -@click.pass_context | 137 | +@click.option( |
190 | -def add_event_sources(ctx): | 138 | + '--command', |
139 | + type=click.Choice(['add', 'update', 'enable', 'disable']), | ||
140 | + help='Operation to perform on event sources') | ||
141 | +@pass_ctx | ||
142 | +def event_sources(ctx, command): | ||
191 | """Add any event sources specified in the config file""" | 143 | """Add any event sources specified in the config file""" |
192 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | 144 | + if command == 'add': |
193 | - ctx.obj['debug'], ctx.obj['force']) | ||
194 | click.echo('adding event sources') | 145 | click.echo('adding event sources') |
195 | - context.add_event_sources() | 146 | + ctx.add_event_sources() |
196 | click.echo('done') | 147 | click.echo('done') |
197 | - | 148 | + elif command == 'update': |
198 | - | ||
199 | -@cli.command() | ||
200 | -@click.pass_context | ||
201 | -def update_event_sources(ctx): | ||
202 | - """Update event sources specified in the config file""" | ||
203 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
204 | - ctx.obj['debug'], ctx.obj['force']) | ||
205 | click.echo('updating event sources') | 149 | click.echo('updating event sources') |
206 | - context.update_event_sources() | 150 | + ctx.update_event_sources() |
207 | click.echo('done') | 151 | click.echo('done') |
208 | - | 152 | + elif command == 'enable': |
209 | - | ||
210 | -@cli.command() | ||
211 | -@click.pass_context | ||
212 | -def enable_event_sources(ctx): | ||
213 | - """Enable event sources specified in the config file""" | ||
214 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
215 | - ctx.obj['debug'], ctx.obj['force']) | ||
216 | click.echo('enabling event sources') | 153 | click.echo('enabling event sources') |
217 | - context.enable_event_sources() | 154 | + ctx.enable_event_sources() |
218 | click.echo('done') | 155 | click.echo('done') |
219 | - | 156 | + elif command == 'disable': |
220 | - | ||
221 | -@cli.command() | ||
222 | -@click.pass_context | ||
223 | -def disable_event_sources(ctx): | ||
224 | - """Disable event sources specified in the config file""" | ||
225 | - context = Context(ctx.obj['config'], ctx.obj['environment'], | ||
226 | - ctx.obj['debug'], ctx.obj['force']) | ||
227 | click.echo('enabling event sources') | 157 | click.echo('enabling event sources') |
228 | - context.disable_event_sources() | 158 | + ctx.disable_event_sources() |
229 | click.echo('done') | 159 | click.echo('done') |
230 | 160 | ||
231 | 161 | ||
232 | -cli(obj={}) | 162 | +@cli.command() |
163 | +@click.argument('name') | ||
164 | +@click.argument('description') | ||
165 | +@pass_ctx | ||
166 | +def tag(ctx, name, description): | ||
167 | + """Tag the current function version with a symbolic name""" | ||
168 | + click.echo('creating tag for function') | ||
169 | + ctx.tag(name, description) | ||
170 | + click.echo('done') | ... | ... |
samples/python/README.md
0 → 100644
1 | +A Simple Python Example | ||
2 | +======================= | ||
3 | + | ||
4 | +In this Python example, we will build a Lambda function that can be hooked up | ||
5 | +to methods in API Gateway to provide a simple CRUD REST API that persists JSON | ||
6 | +objects in DynamoDB. | ||
7 | + | ||
8 | +To implement this, we will create a single Lambda function that will be | ||
9 | +associated with the GET, POST, PUT, and DELETE HTTP methods of a single API | ||
10 | +Gateway resource. We will show the API Gateway connections later. For now, we | ||
11 | +will focus on our Lambda function. | ||
12 | + | ||
13 | + | ||
14 | + | ||
15 | +Installing Dependencies | ||
16 | +----------------------- | ||
17 | + | ||
18 | +Put all dependencies in the `requirements.txt` file in this directory and then | ||
19 | +run the following command to install them in this directory prior to uploading | ||
20 | +the code. | ||
21 | + | ||
22 | + $ pip install -r requirements.txt -t /full/path/to/this/code | ||
23 | + | ||
24 | +This will install all of the dependencies inside the code directory so they can | ||
25 | +be bundled with your own code and deployed to Lambda. | ||
26 | + | ||
27 | +The ``setup.cfg`` file in this directory is required if you are running on | ||
28 | +MacOS and are using brew. It may not be needed on other platforms. | ||
29 | + |
samples/python/_src/README.md
0 → 100644
1 | +The Code Is Here! | ||
2 | +================= | ||
3 | + | ||
4 | +At the moment, the contents of this directory are created by hand but when | ||
5 | +LambdaPI is complete, the basic framework would be created for you. You would | ||
6 | +have a Python source file that works but doesn't actually do anything. And the | ||
7 | +config.json file here would be created on the fly at deployment time. The | ||
8 | +correct resource names and other variables would be written into the config | ||
9 | +file and then then config file would get bundled up with the code. You can | ||
10 | +then load the config file at run time in the Lambda Python code so you don't | ||
11 | +have to hardcode resource names in your code. | ||
12 | + | ||
13 | + | ||
14 | +Installing Dependencies | ||
15 | +----------------------- | ||
16 | + | ||
17 | +Put all dependencies in the `requirements.txt` file in this directory and then | ||
18 | +run the following command to install them in this directory prior to uploading | ||
19 | +the code. | ||
20 | + | ||
21 | + $ pip install -r requirements.txt -t /full/path/to/this/code | ||
22 | + | ||
23 | +This will install all of the dependencies inside the code directory so they can | ||
24 | +be bundled with your own code and deployed to Lambda. | ||
25 | + | ||
26 | +The ``setup.cfg`` file in this directory is required if you are running on | ||
27 | +MacOS and are using brew. It may not be needed on other platforms. | ||
28 | + |
samples/python/_src/dev_config.json
0 → 100644
samples/python/_src/prod_config.json
0 → 100644
samples/python/_src/requirements.txt
0 → 100644
samples/python/_src/simple.py
0 → 100644
1 | +import logging | ||
2 | +import json | ||
3 | +import uuid | ||
4 | + | ||
5 | +import boto3 | ||
6 | + | ||
7 | +LOG = logging.getLogger() | ||
8 | +LOG.setLevel(logging.INFO) | ||
9 | + | ||
10 | +# The kappa deploy command will make sure that the right config file | ||
11 | +# for this environment is available in the local directory. | ||
12 | +config = json.load(open('config.json')) | ||
13 | + | ||
14 | +session = boto3.Session(region_name=config['region_name']) | ||
15 | +ddb_client = session.resource('dynamodb') | ||
16 | +table = ddb_client.Table(config['sample_table']) | ||
17 | + | ||
18 | + | ||
19 | +def foobar(): | ||
20 | + return 42 | ||
21 | + | ||
22 | + | ||
23 | +def _get(event, context): | ||
24 | + customer_id = event.get('id') | ||
25 | + if customer_id is None: | ||
26 | + raise Exception('No id provided for GET operation') | ||
27 | + response = table.get_item(Key={'id': customer_id}) | ||
28 | + item = response.get('Item') | ||
29 | + if item is None: | ||
30 | + raise Exception('id: {} not found'.format(customer_id)) | ||
31 | + return response['Item'] | ||
32 | + | ||
33 | + | ||
34 | +def _post(event, context): | ||
35 | + item = event['json_body'] | ||
36 | + if item is None: | ||
37 | + raise Exception('No json_body found in event') | ||
38 | + item['id'] = str(uuid.uuid4()) | ||
39 | + table.put_item(Item=item) | ||
40 | + return item | ||
41 | + | ||
42 | + | ||
43 | +def _put(event, context): | ||
44 | + data = _get(event, context) | ||
45 | + id = data.get('id') | ||
46 | + data.update(event['json_body']) | ||
47 | + # don't allow the id to be changed | ||
48 | + data['id'] = id | ||
49 | + table.put_item(Item=data) | ||
50 | + return data | ||
51 | + | ||
52 | + | ||
53 | +def handler(event, context): | ||
54 | + LOG.info(event) | ||
55 | + http_method = event.get('http_method') | ||
56 | + if not http_method: | ||
57 | + return 'NoHttpMethodSupplied' | ||
58 | + if http_method == 'GET': | ||
59 | + return _get(event, context) | ||
60 | + elif http_method == 'POST': | ||
61 | + return _post(event, context) | ||
62 | + elif http_method == 'PUT': | ||
63 | + return _put(event, context) | ||
64 | + elif http_method == 'DELETE': | ||
65 | + return _put(event, context) | ||
66 | + else: | ||
67 | + raise Exception('UnsupportedMethod: {}'.format(http_method)) |
samples/python/_tests/test_get.json
0 → 100644
samples/python/_tests/test_post.json
0 → 100644
samples/python/_tests/unit/__init__.py
0 → 100644
File mode changed
samples/python/_tests/unit/test_simple.py
0 → 100644
samples/python/kappa.yml.sample
0 → 100644
1 | +--- | ||
2 | +name: kappa-python-sample | ||
3 | +environments: | ||
4 | + dev: | ||
5 | + profile: <your dev profile> | ||
6 | + region: <your dev region e.g. us-west-2> | ||
7 | + policy: | ||
8 | + resources: | ||
9 | + - arn: arn:aws:dynamodb:us-west-2:123456789012:table/kappa-python-sample | ||
10 | + | ||
11 | + actions: | ||
12 | + - "*" | ||
13 | + - arn: arn:aws:logs:*:*:* | ||
14 | + actions: | ||
15 | + - "*" | ||
16 | + prod: | ||
17 | + profile: <your prod profile> | ||
18 | + region: <your prod region e.g. us-west-2> | ||
19 | + policy_resources: | ||
20 | + - arn: arn:aws:dynamodb:us-west-2:234567890123:table/kappa-python-sample | ||
21 | + actions: | ||
22 | + - "*" | ||
23 | + - arn: arn:aws:logs:*:*:* | ||
24 | + actions: | ||
25 | + - "*" | ||
26 | +lambda: | ||
27 | + description: A simple Python sample | ||
28 | + handler: simple.handler | ||
29 | + runtime: python2.7 | ||
30 | + memory_size: 256 | ||
31 | + timeout: 3 | ||
32 | + | ||
... | \ No newline at end of file | ... | \ No newline at end of file |
... | @@ -5,7 +5,7 @@ from setuptools import setup, find_packages | ... | @@ -5,7 +5,7 @@ from setuptools import setup, find_packages |
5 | import os | 5 | import os |
6 | 6 | ||
7 | requires = [ | 7 | requires = [ |
8 | - 'boto3>=1.2.0', | 8 | + 'boto3>=1.2.2', |
9 | 'click>=5.0', | 9 | 'click>=5.0', |
10 | 'PyYAML>=3.11' | 10 | 'PyYAML>=3.11' |
11 | ] | 11 | ] | ... | ... |
-
Please register or login to post a comment