Mitch Garnaat

A WIP commit on the new refactor for support of Python and other features.

1 -# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/ 1 +# Copyright (c) 2014, 2015 Mitch Garnaat
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");
4 -# may not use this file except in compliance with the License. A copy of 4 +# you may not use this file except in compliance with the License.
5 -# the License is located at 5 +# You may obtain a copy of the License at
6 # 6 #
7 -# http://aws.amazon.com/apache2.0/ 7 +# http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 -# or in the "license" file accompanying this file. This file is 9 +# Unless required by applicable law or agreed to in writing, software
10 -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 -# ANY KIND, either express or implied. See the License for the specific 11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 -# language governing permissions and limitations under the License. 12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
13 14
14 import os 15 import os
15 16
......
1 +# Copyright (c) 2015 Mitch Garnaat
2 +#
3 +# Licensed under the Apache License, Version 2.0 (the "License");
4 +# you may not use this file except in compliance with the License.
5 +# You may obtain a copy of the License at
6 +#
7 +# http://www.apache.org/licenses/LICENSE-2.0
8 +#
9 +# Unless required by applicable law or agreed to in writing, software
10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
14 +
15 +import logging
16 +import json
17 +import os
18 +
19 +import datetime
20 +import jmespath
21 +import boto3
22 +
23 +
24 +LOG = logging.getLogger(__name__)
25 +
26 +
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):
36 +
37 + def __init__(self, service_name, region_name, profile_name,
38 + record_path=None):
39 + self._service_name = service_name
40 + self._region_name = region_name
41 + self._profile_name = profile_name
42 + self._record_path = record_path
43 + self._client = self._create_client()
44 +
45 + @property
46 + def service_name(self):
47 + return self._service_name
48 +
49 + @property
50 + def region_name(self):
51 + return self._region_name
52 +
53 + @property
54 + def profile_name(self):
55 + return self._profile_name
56 +
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):
91 + session = boto3.session.Session(
92 + region_name=self._region_name, profile_name=self._profile_name)
93 + return session.client(self._service_name)
94 +
95 + def call(self, op_name, query=None, **kwargs):
96 + """
97 + Make a request to a method in this client. The response data is
98 + returned from this call as native Python data structures.
99 +
100 + This method differs from just calling the client method directly
101 + in the following ways:
102 +
103 + * It automatically handles the pagination rather than
104 + relying on a separate pagination method call.
105 + * You can pass an optional jmespath query and this query
106 + will be applied to the data returned from the low-level
107 + call. This allows you to tailor the returned data to be
108 + exactly what you want.
109 +
110 + :type op_name: str
111 + :param op_name: The name of the request you wish to make.
112 +
113 + :type query: str
114 + :param query: A jmespath query that will be applied to the
115 + data returned by the operation prior to returning
116 + it to the user.
117 +
118 + :type kwargs: keyword arguments
119 + :param kwargs: Additional keyword arguments you want to pass
120 + to the method when making the request.
121 + """
122 + LOG.debug(kwargs)
123 + if query:
124 + query = jmespath.compile(query)
125 + if self._client.can_paginate(op_name):
126 + paginator = self._client.get_paginator(op_name)
127 + results = paginator.paginate(**kwargs)
128 + data = results.build_full_result()
129 + else:
130 + op = getattr(self._client, op_name)
131 + data = op(**kwargs)
132 + if query:
133 + data = query.search(data)
134 + self._record(op_name, kwargs, data)
135 + return data
136 +
137 +
138 +_client_cache = {}
139 +
140 +
141 +def create_client(service_name, context):
142 + global _client_cache
143 + client_key = '{}:{}:{}'.format(service_name, context.region,
144 + context.profile)
145 + if client_key not in _client_cache:
146 + _client_cache[client_key] = AWSClient(service_name,
147 + context.region,
148 + context.profile,
149 + context.record_path)
150 + return _client_cache[client_key]
1 -# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/ 1 +# Copyright (c) 2014, 2015 Mitch Garnaat
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");
4 -# may not use this file except in compliance with the License. A copy of 4 +# you may not use this file except in compliance with the License.
5 -# the License is located at 5 +# You may obtain a copy of the License at
6 # 6 #
7 -# http://aws.amazon.com/apache2.0/ 7 +# http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 -# or in the "license" file accompanying this file. This file is 9 +# Unless required by applicable law or agreed to in writing, software
10 -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 -# ANY KIND, either express or implied. See the License for the specific 11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 -# language governing permissions and limitations under the License. 12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
13 14
14 import logging 15 import logging
15 import yaml 16 import yaml
16 import time 17 import time
18 +import os
17 19
18 import kappa.function 20 import kappa.function
19 import kappa.event_source 21 import kappa.event_source
...@@ -28,34 +30,53 @@ InfoFmtString = '\t%(message)s' ...@@ -28,34 +30,53 @@ InfoFmtString = '\t%(message)s'
28 30
29 class Context(object): 31 class Context(object):
30 32
31 - def __init__(self, config_file, debug=False): 33 + def __init__(self, config_file, environment=None, debug=False):
32 if debug: 34 if debug:
33 self.set_logger('kappa', logging.DEBUG) 35 self.set_logger('kappa', logging.DEBUG)
34 else: 36 else:
35 self.set_logger('kappa', logging.INFO) 37 self.set_logger('kappa', logging.INFO)
38 + self._load_cache()
36 self.config = yaml.load(config_file) 39 self.config = yaml.load(config_file)
37 - if 'policy' in self.config.get('iam', ''): 40 + self.environment = environment
38 - self.policy = kappa.policy.Policy( 41 + self.policy = kappa.policy.Policy(
39 - self, self.config['iam']['policy']) 42 + self, self.config['environments'][self.environment])
40 - else: 43 + self.role = kappa.role.Role(
41 - self.policy = None 44 + self, self.config['environments'][self.environment])
42 - if 'role' in self.config.get('iam', ''):
43 - self.role = kappa.role.Role(
44 - self, self.config['iam']['role'])
45 - else:
46 - self.role = None
47 self.function = kappa.function.Function( 45 self.function = kappa.function.Function(
48 self, self.config['lambda']) 46 self, self.config['lambda'])
49 self.event_sources = [] 47 self.event_sources = []
50 self._create_event_sources() 48 self._create_event_sources()
51 49
50 + def _load_cache(self):
51 + self.cache = {}
52 + if os.path.isdir('.kappa'):
53 + cache_file = os.path.join('.kappa', 'cache')
54 + if os.path.isfile(cache_file):
55 + with open(cache_file, 'rb') as fp:
56 + self.cache = yaml.load(fp)
57 +
58 + def save_cache(self):
59 + if not os.path.isdir('.kappa'):
60 + os.mkdir('.kappa')
61 + cache_file = os.path.join('.kappa', 'cache')
62 + with open(cache_file, 'wb') as fp:
63 + yaml.dump(self.cache, fp)
64 +
65 + @property
66 + def name(self):
67 + return self.config.get('name', None)
68 +
52 @property 69 @property
53 def profile(self): 70 def profile(self):
54 - return self.config.get('profile', None) 71 + return self.config['environments'][self.environment]['profile']
55 72
56 @property 73 @property
57 def region(self): 74 def region(self):
58 - return self.config.get('region', None) 75 + return self.config['environments'][self.environment]['region']
76 +
77 + @property
78 + def record_path(self):
79 + return self.config.get('record_path', None)
59 80
60 @property 81 @property
61 def lambda_config(self): 82 def lambda_config(self):
...@@ -134,6 +155,18 @@ class Context(object): ...@@ -134,6 +155,18 @@ class Context(object):
134 time.sleep(5) 155 time.sleep(5)
135 self.function.create() 156 self.function.create()
136 157
158 + def deploy(self):
159 + if self.policy:
160 + self.policy.deploy()
161 + if self.role:
162 + self.role.create()
163 + # There is a consistency problem here.
164 + # If you don't wait for a bit, the function.create call
165 + # will fail because the policy has not been attached to the role.
166 + LOG.debug('Waiting for policy/role propogation')
167 + time.sleep(5)
168 + self.function.deploy()
169 +
137 def update_code(self): 170 def update_code(self):
138 self.function.update() 171 self.function.update()
139 172
......
1 -# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/ 1 +# Copyright (c) 2014, 2015 Mitch Garnaat
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");
4 -# may not use this file except in compliance with the License. A copy of 4 +# you may not use this file except in compliance with the License.
5 -# the License is located at 5 +# You may obtain a copy of the License at
6 # 6 #
7 -# http://aws.amazon.com/apache2.0/ 7 +# http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 -# or in the "license" file accompanying this file. This file is 9 +# Unless required by applicable law or agreed to in writing, software
10 -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 -# ANY KIND, either express or implied. See the License for the specific 11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 -# language governing permissions and limitations under the License. 12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
13 14
14 import logging 15 import logging
15 16
16 from botocore.exceptions import ClientError 17 from botocore.exceptions import ClientError
17 18
18 -import kappa.aws 19 +import kappa.awsclient
19 20
20 LOG = logging.getLogger(__name__) 21 LOG = logging.getLogger(__name__)
21 22
...@@ -47,12 +48,12 @@ class KinesisEventSource(EventSource): ...@@ -47,12 +48,12 @@ class KinesisEventSource(EventSource):
47 48
48 def __init__(self, context, config): 49 def __init__(self, context, config):
49 super(KinesisEventSource, self).__init__(context, config) 50 super(KinesisEventSource, self).__init__(context, config)
50 - aws = kappa.aws.get_aws(context) 51 + self._lambda = kappa.awsclient.create_client('kinesis', context)
51 - self._lambda = aws.create_client('lambda')
52 52
53 def _get_uuid(self, function): 53 def _get_uuid(self, function):
54 uuid = None 54 uuid = None
55 - response = self._lambda.list_event_source_mappings( 55 + response = self._lambda.call(
56 + 'list_event_source_mappings',
56 FunctionName=function.name, 57 FunctionName=function.name,
57 EventSourceArn=self.arn) 58 EventSourceArn=self.arn)
58 LOG.debug(response) 59 LOG.debug(response)
...@@ -62,7 +63,8 @@ class KinesisEventSource(EventSource): ...@@ -62,7 +63,8 @@ class KinesisEventSource(EventSource):
62 63
63 def add(self, function): 64 def add(self, function):
64 try: 65 try:
65 - response = self._lambda.create_event_source_mapping( 66 + response = self._lambda.call(
67 + 'create_event_source_mapping',
66 FunctionName=function.name, 68 FunctionName=function.name,
67 EventSourceArn=self.arn, 69 EventSourceArn=self.arn,
68 BatchSize=self.batch_size, 70 BatchSize=self.batch_size,
...@@ -78,7 +80,8 @@ class KinesisEventSource(EventSource): ...@@ -78,7 +80,8 @@ class KinesisEventSource(EventSource):
78 uuid = self._get_uuid(function) 80 uuid = self._get_uuid(function)
79 if uuid: 81 if uuid:
80 try: 82 try:
81 - response = self._lambda.update_event_source_mapping( 83 + response = self._lambda.call(
84 + 'update_event_source_mapping',
82 BatchSize=self.batch_size, 85 BatchSize=self.batch_size,
83 Enabled=self.enabled, 86 Enabled=self.enabled,
84 FunctionName=function.arn) 87 FunctionName=function.arn)
...@@ -90,7 +93,8 @@ class KinesisEventSource(EventSource): ...@@ -90,7 +93,8 @@ class KinesisEventSource(EventSource):
90 response = None 93 response = None
91 uuid = self._get_uuid(function) 94 uuid = self._get_uuid(function)
92 if uuid: 95 if uuid:
93 - response = self._lambda.delete_event_source_mapping( 96 + response = self._lambda.call(
97 + 'delete_event_source_mapping',
94 UUID=uuid) 98 UUID=uuid)
95 LOG.debug(response) 99 LOG.debug(response)
96 return response 100 return response
...@@ -101,7 +105,8 @@ class KinesisEventSource(EventSource): ...@@ -101,7 +105,8 @@ class KinesisEventSource(EventSource):
101 uuid = self._get_uuid(function) 105 uuid = self._get_uuid(function)
102 if uuid: 106 if uuid:
103 try: 107 try:
104 - response = self._lambda.get_event_source_mapping( 108 + response = self._lambda.call(
109 + 'get_event_source_mapping',
105 UUID=self._get_uuid(function)) 110 UUID=self._get_uuid(function))
106 LOG.debug(response) 111 LOG.debug(response)
107 except ClientError: 112 except ClientError:
...@@ -121,8 +126,7 @@ class S3EventSource(EventSource): ...@@ -121,8 +126,7 @@ class S3EventSource(EventSource):
121 126
122 def __init__(self, context, config): 127 def __init__(self, context, config):
123 super(S3EventSource, self).__init__(context, config) 128 super(S3EventSource, self).__init__(context, config)
124 - aws = kappa.aws.get_aws(context) 129 + self._s3 = kappa.awsclient.create_client('s3', config)
125 - self._s3 = aws.create_client('s3')
126 130
127 def _make_notification_id(self, function_name): 131 def _make_notification_id(self, function_name):
128 return 'Kappa-%s-notification' % function_name 132 return 'Kappa-%s-notification' % function_name
...@@ -141,7 +145,8 @@ class S3EventSource(EventSource): ...@@ -141,7 +145,8 @@ class S3EventSource(EventSource):
141 ] 145 ]
142 } 146 }
143 try: 147 try:
144 - response = self._s3.put_bucket_notification_configuration( 148 + response = self._s3.call(
149 + 'put_bucket_notification_configuration',
145 Bucket=self._get_bucket_name(), 150 Bucket=self._get_bucket_name(),
146 NotificationConfiguration=notification_spec) 151 NotificationConfiguration=notification_spec)
147 LOG.debug(response) 152 LOG.debug(response)
...@@ -154,7 +159,8 @@ class S3EventSource(EventSource): ...@@ -154,7 +159,8 @@ class S3EventSource(EventSource):
154 159
155 def remove(self, function): 160 def remove(self, function):
156 LOG.debug('removing s3 notification') 161 LOG.debug('removing s3 notification')
157 - response = self._s3.get_bucket_notification( 162 + response = self._s3.call(
163 + 'get_bucket_notification',
158 Bucket=self._get_bucket_name()) 164 Bucket=self._get_bucket_name())
159 LOG.debug(response) 165 LOG.debug(response)
160 if 'CloudFunctionConfiguration' in response: 166 if 'CloudFunctionConfiguration' in response:
...@@ -162,14 +168,16 @@ class S3EventSource(EventSource): ...@@ -162,14 +168,16 @@ class S3EventSource(EventSource):
162 if fn_arn == function.arn: 168 if fn_arn == function.arn:
163 del response['CloudFunctionConfiguration'] 169 del response['CloudFunctionConfiguration']
164 del response['ResponseMetadata'] 170 del response['ResponseMetadata']
165 - response = self._s3.put_bucket_notification( 171 + response = self._s3.call(
172 + 'put_bucket_notification',
166 Bucket=self._get_bucket_name(), 173 Bucket=self._get_bucket_name(),
167 NotificationConfiguration=response) 174 NotificationConfiguration=response)
168 LOG.debug(response) 175 LOG.debug(response)
169 176
170 def status(self, function): 177 def status(self, function):
171 LOG.debug('status for s3 notification for %s', function.name) 178 LOG.debug('status for s3 notification for %s', function.name)
172 - response = self._s3.get_bucket_notification( 179 + response = self._s3.call(
180 + 'get_bucket_notification',
173 Bucket=self._get_bucket_name()) 181 Bucket=self._get_bucket_name())
174 LOG.debug(response) 182 LOG.debug(response)
175 if 'CloudFunctionConfiguration' not in response: 183 if 'CloudFunctionConfiguration' not in response:
...@@ -181,15 +189,15 @@ class SNSEventSource(EventSource): ...@@ -181,15 +189,15 @@ class SNSEventSource(EventSource):
181 189
182 def __init__(self, context, config): 190 def __init__(self, context, config):
183 super(SNSEventSource, self).__init__(context, config) 191 super(SNSEventSource, self).__init__(context, config)
184 - aws = kappa.aws.get_aws(context) 192 + self._sns = kappa.awsclient.create_client('sns', context)
185 - self._sns = aws.create_client('sns')
186 193
187 def _make_notification_id(self, function_name): 194 def _make_notification_id(self, function_name):
188 return 'Kappa-%s-notification' % function_name 195 return 'Kappa-%s-notification' % function_name
189 196
190 def exists(self, function): 197 def exists(self, function):
191 try: 198 try:
192 - response = self._sns.list_subscriptions_by_topic( 199 + response = self._sns.call(
200 + 'list_subscriptions_by_topic',
193 TopicArn=self.arn) 201 TopicArn=self.arn)
194 LOG.debug(response) 202 LOG.debug(response)
195 for subscription in response['Subscriptions']: 203 for subscription in response['Subscriptions']:
...@@ -201,7 +209,8 @@ class SNSEventSource(EventSource): ...@@ -201,7 +209,8 @@ class SNSEventSource(EventSource):
201 209
202 def add(self, function): 210 def add(self, function):
203 try: 211 try:
204 - response = self._sns.subscribe( 212 + response = self._sns.call(
213 + 'subscribe',
205 TopicArn=self.arn, Protocol='lambda', 214 TopicArn=self.arn, Protocol='lambda',
206 Endpoint=function.arn) 215 Endpoint=function.arn)
207 LOG.debug(response) 216 LOG.debug(response)
...@@ -216,7 +225,8 @@ class SNSEventSource(EventSource): ...@@ -216,7 +225,8 @@ class SNSEventSource(EventSource):
216 try: 225 try:
217 subscription = self.exists(function) 226 subscription = self.exists(function)
218 if subscription: 227 if subscription:
219 - response = self._sns.unsubscribe( 228 + response = self._sns.call(
229 + 'unsubscribe',
220 SubscriptionArn=subscription['SubscriptionArn']) 230 SubscriptionArn=subscription['SubscriptionArn'])
221 LOG.debug(response) 231 LOG.debug(response)
222 except Exception: 232 except Exception:
......
1 -# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/ 1 +# Copyright (c) 2014, 2015 Mitch Garnaat
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");
4 -# may not use this file except in compliance with the License. A copy of 4 +# you may not use this file except in compliance with the License.
5 -# the License is located at 5 +# You may obtain a copy of the License at
6 # 6 #
7 -# http://aws.amazon.com/apache2.0/ 7 +# http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 -# or in the "license" file accompanying this file. This file is 9 +# Unless required by applicable law or agreed to in writing, software
10 -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 -# ANY KIND, either express or implied. See the License for the specific 11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 -# language governing permissions and limitations under the License. 12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
13 14
14 import logging 15 import logging
15 import os 16 import os
16 import zipfile 17 import zipfile
18 +import time
17 19
18 from botocore.exceptions import ClientError 20 from botocore.exceptions import ClientError
19 21
20 -import kappa.aws 22 +import kappa.awsclient
21 import kappa.log 23 import kappa.log
22 24
23 LOG = logging.getLogger(__name__) 25 LOG = logging.getLogger(__name__)
...@@ -28,14 +30,14 @@ class Function(object): ...@@ -28,14 +30,14 @@ class Function(object):
28 def __init__(self, context, config): 30 def __init__(self, context, config):
29 self._context = context 31 self._context = context
30 self._config = config 32 self._config = config
31 - aws = kappa.aws.get_aws(context) 33 + self._lambda_client = kappa.awsclient.create_client(
32 - self._lambda_svc = aws.create_client('lambda') 34 + 'lambda', context)
33 - self._arn = None 35 + self._response = None
34 self._log = None 36 self._log = None
35 37
36 @property 38 @property
37 def name(self): 39 def name(self):
38 - return self._config['name'] 40 + return self._context.name
39 41
40 @property 42 @property
41 def runtime(self): 43 def runtime(self):
...@@ -73,17 +75,44 @@ class Function(object): ...@@ -73,17 +75,44 @@ class Function(object):
73 def permissions(self): 75 def permissions(self):
74 return self._config.get('permissions', list()) 76 return self._config.get('permissions', list())
75 77
76 - @property 78 + def _get_response(self):
77 - def arn(self): 79 + if self._response is None:
78 - if self._arn is None:
79 try: 80 try:
80 - response = self._lambda_svc.get_function( 81 + self._response = self._lambda_client.call(
82 + 'get_function',
81 FunctionName=self.name) 83 FunctionName=self.name)
82 - LOG.debug(response) 84 + LOG.debug(self._response)
83 - self._arn = response['Configuration']['FunctionArn']
84 except Exception: 85 except Exception:
85 LOG.debug('Unable to find ARN for function: %s', self.name) 86 LOG.debug('Unable to find ARN for function: %s', self.name)
86 - return self._arn 87 + return self._response
88 +
89 + @property
90 + def code_sha_256(self):
91 + response = self._get_response()
92 + return response['Configuration']['CodeSha256']
93 +
94 + @property
95 + def arn(self):
96 + response = self._get_response()
97 + return response['Configuration']['FunctionArn']
98 +
99 + @property
100 + def version(self):
101 + response = self._get_response()
102 + return response['Configuration']['Version']
103 +
104 + @property
105 + def repository_type(self):
106 + response = self._get_response()
107 + return response['Code']['RepositoryType']
108 +
109 + @property
110 + def location(self):
111 + response = self._get_response()
112 + return response['Code']['Location']
113 +
114 + def exists(self):
115 + return self._get_response()
87 116
88 @property 117 @property
89 def log(self): 118 def log(self):
...@@ -125,6 +154,8 @@ class Function(object): ...@@ -125,6 +154,8 @@ class Function(object):
125 self._zip_lambda_file(zipfile_name, lambda_fn) 154 self._zip_lambda_file(zipfile_name, lambda_fn)
126 155
127 def add_permissions(self): 156 def add_permissions(self):
157 + if self.permissions:
158 + time.sleep(5)
128 for permission in self.permissions: 159 for permission in self.permissions:
129 try: 160 try:
130 kwargs = { 161 kwargs = {
...@@ -138,7 +169,8 @@ class Function(object): ...@@ -138,7 +169,8 @@ class Function(object):
138 source_account = permission.get('source_account', None) 169 source_account = permission.get('source_account', None)
139 if source_account: 170 if source_account:
140 kwargs['SourceAccount'] = source_account 171 kwargs['SourceAccount'] = source_account
141 - response = self._lambda_svc.add_permission(**kwargs) 172 + response = self._lambda_client.call(
173 + 'add_permission', **kwargs)
142 LOG.debug(response) 174 LOG.debug(response)
143 except Exception: 175 except Exception:
144 LOG.exception('Unable to add permission') 176 LOG.exception('Unable to add permission')
...@@ -151,7 +183,8 @@ class Function(object): ...@@ -151,7 +183,8 @@ class Function(object):
151 LOG.debug('exec_role=%s', exec_role) 183 LOG.debug('exec_role=%s', exec_role)
152 try: 184 try:
153 zipdata = fp.read() 185 zipdata = fp.read()
154 - response = self._lambda_svc.create_function( 186 + response = self._lambda_client.call(
187 + 'create_function',
155 FunctionName=self.name, 188 FunctionName=self.name,
156 Code={'ZipFile': zipdata}, 189 Code={'ZipFile': zipdata},
157 Runtime=self.runtime, 190 Runtime=self.runtime,
...@@ -168,21 +201,90 @@ class Function(object): ...@@ -168,21 +201,90 @@ class Function(object):
168 def update(self): 201 def update(self):
169 LOG.debug('updating %s', self.zipfile_name) 202 LOG.debug('updating %s', self.zipfile_name)
170 self.zip_lambda_function(self.zipfile_name, self.path) 203 self.zip_lambda_function(self.zipfile_name, self.path)
171 - with open(self.zipfile_name, 'rb') as fp: 204 + stats = os.stat(self.zipfile_name)
172 - try: 205 + if self._context.cache.get('zipfile_size') != stats.st_size:
173 - zipdata = fp.read() 206 + self._context.cache['zipfile_size'] = stats.st_size
174 - response = self._lambda_svc.update_function_code( 207 + self._context.save_cache()
175 - FunctionName=self.name, 208 + with open(self.zipfile_name, 'rb') as fp:
176 - ZipFile=zipdata) 209 + try:
177 - LOG.debug(response) 210 + zipdata = fp.read()
178 - except Exception: 211 + response = self._lambda_client.call(
179 - LOG.exception('Unable to update zip file') 212 + 'update_function_code',
213 + FunctionName=self.name,
214 + ZipFile=zipdata)
215 + LOG.debug(response)
216 + except Exception:
217 + LOG.exception('Unable to update zip file')
218 + else:
219 + LOG.info('Code has not changed')
220 +
221 + def deploy(self):
222 + if self.exists():
223 + return self.update()
224 + return self.create()
225 +
226 + def publish_version(self, description):
227 + LOG.debug('publishing version of ', self.name)
228 + try:
229 + response = self._lambda_client.call(
230 + 'publish_version',
231 + FunctionName=self.name,
232 + CodeSha256=self.code_sha_256,
233 + Description=description)
234 + LOG.debug(response)
235 + except Exception:
236 + LOG.exception('Unable to publish version')
237 + return response['Version']
238 +
239 + def list_versions(self):
240 + LOG.debug('listing versions of ', self.name)
241 + try:
242 + response = self._lambda_client.call(
243 + 'list_versions_by_function',
244 + FunctionName=self.name)
245 + LOG.debug(response)
246 + except Exception:
247 + LOG.exception('Unable to list versions')
248 + return response['Versions']
249 +
250 + def create_alias(self, name, description, version=None):
251 + LOG.debug('creating alias of ', self.name)
252 + if version is None:
253 + version = self.version
254 + try:
255 + response = self._lambda_client.call(
256 + 'create_alias',
257 + FunctionName=self.name,
258 + Description=description,
259 + FunctionVersion=self.version,
260 + Name=name)
261 + LOG.debug(response)
262 + except Exception:
263 + LOG.exception('Unable to create alias')
264 +
265 + def list_aliases(self):
266 + LOG.debug('listing aliases of ', self.name)
267 + try:
268 + response = self._lambda_client.call(
269 + 'list_aliases',
270 + FunctionName=self.name,
271 + FunctionVersion=self.version)
272 + LOG.debug(response)
273 + except Exception:
274 + LOG.exception('Unable to list aliases')
275 + return response['Versions']
276 +
277 + def tag(self, name, description):
278 + version = self.publish_version(description)
279 + self.create_alias(name, description, version)
180 280
181 def delete(self): 281 def delete(self):
182 LOG.debug('deleting function %s', self.name) 282 LOG.debug('deleting function %s', self.name)
183 response = None 283 response = None
184 try: 284 try:
185 - response = self._lambda_svc.delete_function(FunctionName=self.name) 285 + response = self._lambda_client.call(
286 + 'delete_function',
287 + FunctionName=self.name)
186 LOG.debug(response) 288 LOG.debug(response)
187 except ClientError: 289 except ClientError:
188 LOG.debug('function %s: not found', self.name) 290 LOG.debug('function %s: not found', self.name)
...@@ -191,7 +293,8 @@ class Function(object): ...@@ -191,7 +293,8 @@ class Function(object):
191 def status(self): 293 def status(self):
192 LOG.debug('getting status for function %s', self.name) 294 LOG.debug('getting status for function %s', self.name)
193 try: 295 try:
194 - response = self._lambda_svc.get_function( 296 + response = self._lambda_client.call(
297 + 'get_function',
195 FunctionName=self.name) 298 FunctionName=self.name)
196 LOG.debug(response) 299 LOG.debug(response)
197 except ClientError: 300 except ClientError:
...@@ -202,7 +305,8 @@ class Function(object): ...@@ -202,7 +305,8 @@ class Function(object):
202 def invoke_asynch(self, data_file): 305 def invoke_asynch(self, data_file):
203 LOG.debug('_invoke_async %s', data_file) 306 LOG.debug('_invoke_async %s', data_file)
204 with open(data_file) as fp: 307 with open(data_file) as fp:
205 - response = self._lambda_svc.invoke_async( 308 + response = self._lambda_client.call(
309 + 'invoke_async',
206 FunctionName=self.name, 310 FunctionName=self.name,
207 InvokeArgs=fp) 311 InvokeArgs=fp)
208 LOG.debug(response) 312 LOG.debug(response)
...@@ -212,7 +316,8 @@ class Function(object): ...@@ -212,7 +316,8 @@ class Function(object):
212 test_data = self.test_data 316 test_data = self.test_data
213 LOG.debug('invoke %s', test_data) 317 LOG.debug('invoke %s', test_data)
214 with open(test_data) as fp: 318 with open(test_data) as fp:
215 - response = self._lambda_svc.invoke( 319 + response = self._lambda_client.call(
320 + 'invoke',
216 FunctionName=self.name, 321 FunctionName=self.name,
217 InvocationType=invocation_type, 322 InvocationType=invocation_type,
218 LogType='Tail', 323 LogType='Tail',
......
1 -# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/ 1 +# Copyright (c) 2014, 2015 Mitch Garnaat
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");
4 -# may not use this file except in compliance with the License. A copy of 4 +# you may not use this file except in compliance with the License.
5 -# the License is located at 5 +# You may obtain a copy of the License at
6 # 6 #
7 -# http://aws.amazon.com/apache2.0/ 7 +# http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 -# or in the "license" file accompanying this file. This file is 9 +# Unless required by applicable law or agreed to in writing, software
10 -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 -# ANY KIND, either express or implied. See the License for the specific 11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 -# language governing permissions and limitations under the License. 12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
13 14
14 import logging 15 import logging
15 16
16 from botocore.exceptions import ClientError 17 from botocore.exceptions import ClientError
17 18
18 -import kappa.aws 19 +import kappa.awsclient
19 20
20 LOG = logging.getLogger(__name__) 21 LOG = logging.getLogger(__name__)
21 22
...@@ -25,12 +26,11 @@ class Log(object): ...@@ -25,12 +26,11 @@ class Log(object):
25 def __init__(self, context, log_group_name): 26 def __init__(self, context, log_group_name):
26 self._context = context 27 self._context = context
27 self.log_group_name = log_group_name 28 self.log_group_name = log_group_name
28 - aws = kappa.aws.get_aws(self._context) 29 + self._log_client = kappa.awsclient.create_client('logs', context)
29 - self._log_svc = aws.create_client('logs')
30 30
31 def _check_for_log_group(self): 31 def _check_for_log_group(self):
32 LOG.debug('checking for log group') 32 LOG.debug('checking for log group')
33 - response = self._log_svc.describe_log_groups() 33 + response = self._log_client.call('describe_log_groups')
34 log_group_names = [lg['logGroupName'] for lg in response['logGroups']] 34 log_group_names = [lg['logGroupName'] for lg in response['logGroups']]
35 return self.log_group_name in log_group_names 35 return self.log_group_name in log_group_names
36 36
...@@ -40,7 +40,8 @@ class Log(object): ...@@ -40,7 +40,8 @@ class Log(object):
40 LOG.info( 40 LOG.info(
41 'log group %s has not been created yet', self.log_group_name) 41 'log group %s has not been created yet', self.log_group_name)
42 return [] 42 return []
43 - response = self._log_svc.describe_log_streams( 43 + response = self._log_client.call(
44 + 'describe_log_streams',
44 logGroupName=self.log_group_name) 45 logGroupName=self.log_group_name)
45 LOG.debug(response) 46 LOG.debug(response)
46 return response['logStreams'] 47 return response['logStreams']
...@@ -58,7 +59,8 @@ class Log(object): ...@@ -58,7 +59,8 @@ class Log(object):
58 latest_stream = stream 59 latest_stream = stream
59 elif stream['lastEventTimestamp'] > latest_stream['lastEventTimestamp']: 60 elif stream['lastEventTimestamp'] > latest_stream['lastEventTimestamp']:
60 latest_stream = stream 61 latest_stream = stream
61 - response = self._log_svc.get_log_events( 62 + response = self._log_client.call(
63 + 'get_log_events',
62 logGroupName=self.log_group_name, 64 logGroupName=self.log_group_name,
63 logStreamName=latest_stream['logStreamName']) 65 logStreamName=latest_stream['logStreamName'])
64 LOG.debug(response) 66 LOG.debug(response)
...@@ -66,7 +68,8 @@ class Log(object): ...@@ -66,7 +68,8 @@ class Log(object):
66 68
67 def delete(self): 69 def delete(self):
68 try: 70 try:
69 - response = self._log_svc.delete_log_group( 71 + response = self._log_client.call(
72 + 'delete_log_group',
70 logGroupName=self.log_group_name) 73 logGroupName=self.log_group_name)
71 LOG.debug(response) 74 LOG.debug(response)
72 except ClientError: 75 except ClientError:
......
1 -# Copyright (c) 2015 Mitch Garnaat http://garnaat.org/ 1 +# Copyright (c) 2014, 2015 Mitch Garnaat
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");
4 -# may not use this file except in compliance with the License. A copy of 4 +# you may not use this file except in compliance with the License.
5 -# the License is located at 5 +# You may obtain a copy of the License at
6 # 6 #
7 -# http://aws.amazon.com/apache2.0/ 7 +# http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 -# or in the "license" file accompanying this file. This file is 9 +# Unless required by applicable law or agreed to in writing, software
10 -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 -# ANY KIND, either express or implied. See the License for the specific 11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 -# language governing permissions and limitations under the License. 12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
13 14
14 import logging 15 import logging
16 +import json
17 +import hashlib
15 18
16 -import kappa.aws 19 +import kappa.awsclient
17 20
18 LOG = logging.getLogger(__name__) 21 LOG = logging.getLogger(__name__)
19 22
...@@ -23,21 +26,39 @@ class Policy(object): ...@@ -23,21 +26,39 @@ class Policy(object):
23 def __init__(self, context, config): 26 def __init__(self, context, config):
24 self._context = context 27 self._context = context
25 self._config = config 28 self._config = config
26 - aws = kappa.aws.get_aws(context) 29 + self._iam_client = kappa.awsclient.create_client('iam', self._context)
27 - self._iam_svc = aws.create_client('iam') 30 + self._arn = self._config['policy'].get('arn', None)
28 - self._arn = None 31 +
32 + @property
33 + def environment(self):
34 + return self._context.environment
29 35
30 @property 36 @property
31 def name(self): 37 def name(self):
32 - return self._config['name'] 38 + return '{}-{}-policy'.format(self._context.name, self.environment)
33 39
34 @property 40 @property
35 def description(self): 41 def description(self):
36 - return self._config.get('description', None) 42 + return 'A kappa policy to control access to {} resources'.format(
43 + self.environment)
37 44
38 - @property
39 def document(self): 45 def document(self):
40 - return self._config.get('document', None) 46 + if 'resources' not in self._config['policy']:
47 + return None
48 + document = {"Version": "2012-10-17"}
49 + statements = []
50 + document['Statement'] = statements
51 + for resource in self._config['policy']['resources']:
52 + arn = resource['arn']
53 + _, _, service, _ = arn.split(':', 3)
54 + statement = {"Effect": "Allow",
55 + "Resource": resource['arn']}
56 + actions = []
57 + for action in resource['actions']:
58 + actions.append("{}:{}".format(service, action))
59 + statement['Action'] = actions
60 + statements.append(statement)
61 + return json.dumps(document, indent=2, sort_keys=True)
41 62
42 @property 63 @property
43 def path(self): 64 def path(self):
...@@ -52,20 +73,21 @@ class Policy(object): ...@@ -52,20 +73,21 @@ class Policy(object):
52 return self._arn 73 return self._arn
53 74
54 def _find_all_policies(self): 75 def _find_all_policies(self):
55 - # boto3 does not currently do pagination
56 - # so we have to do it ourselves
57 - policies = []
58 try: 76 try:
59 - response = self._iam_svc.list_policies() 77 + response = self._iam_client.call(
60 - policies += response['Policies'] 78 + 'list_policies')
61 - while response['IsTruncated']:
62 - LOG.debug('getting another page of policies')
63 - response = self._iam_svc.list_policies(
64 - Marker=response['Marker'])
65 - policies += response['Policies']
66 except Exception: 79 except Exception:
67 LOG.exception('Error listing policies') 80 LOG.exception('Error listing policies')
68 - return policies 81 + return response['Policies']
82 +
83 + def _list_versions(self):
84 + try:
85 + response = self._iam_client.call(
86 + 'list_policy_versions',
87 + PolicyArn=self.arn)
88 + except Exception:
89 + LOG.exception('Error listing policy versions')
90 + return response['Versions']
69 91
70 def exists(self): 92 def exists(self):
71 for policy in self._find_all_policies(): 93 for policy in self._find_all_policies():
...@@ -73,27 +95,72 @@ class Policy(object): ...@@ -73,27 +95,72 @@ class Policy(object):
73 return policy 95 return policy
74 return None 96 return None
75 97
76 - def create(self): 98 + def _add_policy_version(self):
77 - LOG.debug('creating policy %s', self.name) 99 + document = self.document()
100 + if not document:
101 + LOG.debug('not a custom policy, no need to version it')
102 + return
103 + versions = self._list_versions()
104 + if len(versions) == 5:
105 + try:
106 + response = self._iam_client.call(
107 + 'delete_policy_version',
108 + PolicyArn=self.arn,
109 + VersionId=versions[-1]['VersionId'])
110 + except Exception:
111 + LOG.exception('Unable to delete policy version')
112 + # update policy with a new version here
113 + try:
114 + response = self._iam_client.call(
115 + 'create_policy_version',
116 + PolicyArn=self.arn,
117 + PolicyDocument=document,
118 + SetAsDefault=True)
119 + LOG.debug(response)
120 + except Exception:
121 + LOG.exception('Error creating new Policy version')
122 +
123 + def deploy(self):
124 + LOG.debug('deploying policy %s', self.name)
125 + document = self.document()
126 + if not document:
127 + LOG.debug('not a custom policy, no need to create it')
128 + return
78 policy = self.exists() 129 policy = self.exists()
79 - if not policy and self.document: 130 + if policy:
80 - with open(self.document, 'rb') as fp: 131 + m = hashlib.md5()
81 - try: 132 + m.update(document)
82 - response = self._iam_svc.create_policy( 133 + policy_md5 = m.hexdigest()
83 - Path=self.path, PolicyName=self.name, 134 + LOG.debug('policy_md5: {}'.format(policy_md5))
84 - PolicyDocument=fp.read(), 135 + LOG.debug('cache md5: {}'.format(
85 - Description=self.description) 136 + self._context.cache.get('policy_md5')))
86 - LOG.debug(response) 137 + if policy_md5 != self._context.cache.get('policy_md5'):
87 - except Exception: 138 + self._context.cache['policy_md5'] = policy_md5
88 - LOG.exception('Error creating Policy') 139 + self._context.save_cache()
140 + self._add_policy_version()
141 + else:
142 + LOG.info('Policy unchanged')
143 + else:
144 + # create a new policy
145 + try:
146 + response = self._iam_client.call(
147 + 'create_policy',
148 + Path=self.path, PolicyName=self.name,
149 + PolicyDocument=document,
150 + Description=self.description)
151 + LOG.debug(response)
152 + except Exception:
153 + LOG.exception('Error creating Policy')
89 154
90 def delete(self): 155 def delete(self):
91 response = None 156 response = None
92 # Only delete the policy if it has a document associated with it. 157 # Only delete the policy if it has a document associated with it.
93 # This indicates that it was a custom policy created by kappa. 158 # This indicates that it was a custom policy created by kappa.
94 - if self.arn and self.document: 159 + document = self.document()
160 + if self.arn and document:
95 LOG.debug('deleting policy %s', self.name) 161 LOG.debug('deleting policy %s', self.name)
96 - response = self._iam_svc.delete_policy(PolicyArn=self.arn) 162 + response = self._iam_client.call(
163 + 'delete_policy', PolicyArn=self.arn)
97 LOG.debug(response) 164 LOG.debug(response)
98 return response 165 return response
99 166
......
1 -# Copyright (c) 2015 Mitch Garnaat http://garnaat.org/ 1 +# Copyright (c) 2014, 2015 Mitch Garnaat
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");
4 -# may not use this file except in compliance with the License. A copy of 4 +# you may not use this file except in compliance with the License.
5 -# the License is located at 5 +# You may obtain a copy of the License at
6 # 6 #
7 -# http://aws.amazon.com/apache2.0/ 7 +# http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 -# or in the "license" file accompanying this file. This file is 9 +# Unless required by applicable law or agreed to in writing, software
10 -# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 -# ANY KIND, either express or implied. See the License for the specific 11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 -# language governing permissions and limitations under the License. 12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
13 14
14 import logging 15 import logging
15 16
16 from botocore.exceptions import ClientError 17 from botocore.exceptions import ClientError
17 18
18 -import kappa.aws 19 +import kappa.awsclient
19 20
20 LOG = logging.getLogger(__name__) 21 LOG = logging.getLogger(__name__)
21 22
...@@ -39,20 +40,20 @@ class Role(object): ...@@ -39,20 +40,20 @@ class Role(object):
39 def __init__(self, context, config): 40 def __init__(self, context, config):
40 self._context = context 41 self._context = context
41 self._config = config 42 self._config = config
42 - aws = kappa.aws.get_aws(context) 43 + self._iam_client = kappa.awsclient.create_client('iam', context)
43 - self._iam_svc = aws.create_client('iam')
44 self._arn = None 44 self._arn = None
45 45
46 @property 46 @property
47 def name(self): 47 def name(self):
48 - return self._config['name'] 48 + return '{}-{}-role'.format(
49 + self._context.name, self._context.environment)
49 50
50 @property 51 @property
51 def arn(self): 52 def arn(self):
52 if self._arn is None: 53 if self._arn is None:
53 try: 54 try:
54 - response = self._iam_svc.get_role( 55 + response = self._iam_client.call(
55 - RoleName=self.name) 56 + 'get_role', RoleName=self.name)
56 LOG.debug(response) 57 LOG.debug(response)
57 self._arn = response['Role']['Arn'] 58 self._arn = response['Role']['Arn']
58 except Exception: 59 except Exception:
...@@ -60,20 +61,11 @@ class Role(object): ...@@ -60,20 +61,11 @@ class Role(object):
60 return self._arn 61 return self._arn
61 62
62 def _find_all_roles(self): 63 def _find_all_roles(self):
63 - # boto3 does not currently do pagination
64 - # so we have to do it ourselves
65 - roles = []
66 try: 64 try:
67 - response = self._iam_svc.list_roles() 65 + response = self._iam_client.call('list_roles')
68 - roles += response['Roles']
69 - while response['IsTruncated']:
70 - LOG.debug('getting another page of roles')
71 - response = self._iam_svc.list_roles(
72 - Marker=response['Marker'])
73 - roles += response['Roles']
74 except Exception: 66 except Exception:
75 LOG.exception('Error listing roles') 67 LOG.exception('Error listing roles')
76 - return roles 68 + return response['Roles']
77 69
78 def exists(self): 70 def exists(self):
79 for role in self._find_all_roles(): 71 for role in self._find_all_roles():
...@@ -86,13 +78,15 @@ class Role(object): ...@@ -86,13 +78,15 @@ class Role(object):
86 role = self.exists() 78 role = self.exists()
87 if not role: 79 if not role:
88 try: 80 try:
89 - response = self._iam_svc.create_role( 81 + response = self._iam_client.call(
82 + 'create_role',
90 Path=self.Path, RoleName=self.name, 83 Path=self.Path, RoleName=self.name,
91 AssumeRolePolicyDocument=AssumeRolePolicyDocument) 84 AssumeRolePolicyDocument=AssumeRolePolicyDocument)
92 LOG.debug(response) 85 LOG.debug(response)
93 if self._context.policy: 86 if self._context.policy:
94 LOG.debug('attaching policy %s', self._context.policy.arn) 87 LOG.debug('attaching policy %s', self._context.policy.arn)
95 - response = self._iam_svc.attach_role_policy( 88 + response = self._iam_client.call(
89 + 'attach_role_policy',
96 RoleName=self.name, 90 RoleName=self.name,
97 PolicyArn=self._context.policy.arn) 91 PolicyArn=self._context.policy.arn)
98 LOG.debug(response) 92 LOG.debug(response)
...@@ -106,10 +100,12 @@ class Role(object): ...@@ -106,10 +100,12 @@ class Role(object):
106 LOG.debug('First detach the policy from the role') 100 LOG.debug('First detach the policy from the role')
107 policy_arn = self._context.policy.arn 101 policy_arn = self._context.policy.arn
108 if policy_arn: 102 if policy_arn:
109 - response = self._iam_svc.detach_role_policy( 103 + response = self._iam_client.call(
104 + 'detach_role_policy',
110 RoleName=self.name, PolicyArn=policy_arn) 105 RoleName=self.name, PolicyArn=policy_arn)
111 LOG.debug(response) 106 LOG.debug(response)
112 - response = self._iam_svc.delete_role(RoleName=self.name) 107 + response = self._iam_client.call(
108 + 'delete_role', RoleName=self.name)
113 LOG.debug(response) 109 LOG.debug(response)
114 except ClientError: 110 except ClientError:
115 LOG.exception('role %s not found', self.name) 111 LOG.exception('role %s not found', self.name)
...@@ -118,7 +114,8 @@ class Role(object): ...@@ -118,7 +114,8 @@ class Role(object):
118 def status(self): 114 def status(self):
119 LOG.debug('getting status for role %s', self.name) 115 LOG.debug('getting status for role %s', self.name)
120 try: 116 try:
121 - response = self._iam_svc.get_role(RoleName=self.name) 117 + response = self._iam_client.call(
118 + 'get_role', RoleName=self.name)
122 LOG.debug(response) 119 LOG.debug(response)
123 except ClientError: 120 except ClientError:
124 LOG.debug('role %s not found', self.name) 121 LOG.debug('role %s not found', self.name)
......
1 +# Copyright (c) 2014, 2015 Mitch Garnaat
2 +#
3 +# Licensed under the Apache License, Version 2.0 (the "License");
4 +# you may not use this file except in compliance with the License.
5 +# You may obtain a copy of the License at
6 +#
7 +# http://www.apache.org/licenses/LICENSE-2.0
8 +#
9 +# Unless required by applicable law or agreed to in writing, software
10 +# distributed under the License is distributed on an "AS IS" BASIS,
11 +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +# See the License for the specific language governing permissions and
13 +# limitations under the License.
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 -# Copyright (c) 2014 Mitch Garnaat http://garnaat.org/ 2 +# Copyright (c) 2014, 2015 Mitch Garnaat http://garnaat.org/
3 # 3 #
4 # Licensed under the Apache License, Version 2.0 (the "License"). You 4 # Licensed under the Apache License, Version 2.0 (the "License"). You
5 # may not use this file except in compliance with the License. A copy of 5 # may not use this file except in compliance with the License. A copy of
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
11 # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
12 # ANY KIND, either express or implied. See the License for the specific 12 # ANY KIND, either express or implied. See the License for the specific
13 # language governing permissions and limitations under the License. 13 # language governing permissions and limitations under the License.
14 +
14 from datetime import datetime 15 from datetime import datetime
15 -import logging
16 import base64 16 import base64
17 17
18 import click 18 import click
...@@ -31,70 +31,97 @@ from kappa.context import Context ...@@ -31,70 +31,97 @@ from kappa.context import Context
31 default=False, 31 default=False,
32 help='Turn on debugging output' 32 help='Turn on debugging output'
33 ) 33 )
34 +@click.option(
35 + '--environment',
36 + help='Specify which environment to work with'
37 +)
34 @click.pass_context 38 @click.pass_context
35 -def cli(ctx, config=None, debug=False): 39 +def cli(ctx, config=None, debug=False, environment=None):
36 config = config 40 config = config
37 ctx.obj['debug'] = debug 41 ctx.obj['debug'] = debug
38 ctx.obj['config'] = config 42 ctx.obj['config'] = config
43 + ctx.obj['environment'] = environment
44 +
39 45
40 @cli.command() 46 @cli.command()
41 @click.pass_context 47 @click.pass_context
42 -def create(ctx): 48 +def deploy(ctx):
43 - context = Context(ctx.obj['config'], ctx.obj['debug']) 49 + """Deploy the Lambda function and any policies and roles required"""
44 - click.echo('creating...') 50 + context = Context(ctx.obj['config'], ctx.obj['environment'],
45 - context.create() 51 + ctx.obj['debug'])
52 + click.echo('deploying...')
53 + context.deploy()
46 click.echo('...done') 54 click.echo('...done')
47 55
56 +
48 @cli.command() 57 @cli.command()
49 @click.pass_context 58 @click.pass_context
50 -def update_code(ctx): 59 +def tag(ctx):
51 - context = Context(ctx.obj['config'], ctx.obj['debug']) 60 + """Deploy the Lambda function and any policies and roles required"""
52 - click.echo('updating code...') 61 + context = Context(ctx.obj['config'], ctx.obj['environment'],
53 - context.update_code() 62 + ctx.obj['debug'])
63 + click.echo('deploying...')
64 + context.deploy()
54 click.echo('...done') 65 click.echo('...done')
55 66
67 +
56 @cli.command() 68 @cli.command()
57 @click.pass_context 69 @click.pass_context
58 def invoke(ctx): 70 def invoke(ctx):
59 - context = Context(ctx.obj['config'], ctx.obj['debug']) 71 + """Invoke the command synchronously"""
72 + context = Context(ctx.obj['config'], ctx.obj['environment'],
73 + ctx.obj['debug'])
60 click.echo('invoking...') 74 click.echo('invoking...')
61 response = context.invoke() 75 response = context.invoke()
62 log_data = base64.b64decode(response['LogResult']) 76 log_data = base64.b64decode(response['LogResult'])
63 click.echo(log_data) 77 click.echo(log_data)
78 + click.echo(response['Payload'].read())
64 click.echo('...done') 79 click.echo('...done')
65 80
81 +
66 @cli.command() 82 @cli.command()
67 @click.pass_context 83 @click.pass_context
68 def dryrun(ctx): 84 def dryrun(ctx):
69 - context = Context(ctx.obj['config'], ctx.obj['debug']) 85 + """Show you what would happen but don't actually do anything"""
86 + context = Context(ctx.obj['config'], ctx.obj['environment'],
87 + ctx.obj['debug'])
70 click.echo('invoking dryrun...') 88 click.echo('invoking dryrun...')
71 response = context.dryrun() 89 response = context.dryrun()
72 click.echo(response) 90 click.echo(response)
73 click.echo('...done') 91 click.echo('...done')
74 92
93 +
75 @cli.command() 94 @cli.command()
76 @click.pass_context 95 @click.pass_context
77 def invoke_async(ctx): 96 def invoke_async(ctx):
78 - context = Context(ctx.obj['config'], ctx.obj['debug']) 97 + """Invoke the Lambda function asynchronously"""
98 + context = Context(ctx.obj['config'], ctx.obj['environment'],
99 + ctx.obj['debug'])
79 click.echo('invoking async...') 100 click.echo('invoking async...')
80 response = context.invoke_async() 101 response = context.invoke_async()
81 click.echo(response) 102 click.echo(response)
82 click.echo('...done') 103 click.echo('...done')
83 104
105 +
84 @cli.command() 106 @cli.command()
85 @click.pass_context 107 @click.pass_context
86 def tail(ctx): 108 def tail(ctx):
87 - context = Context(ctx.obj['config'], ctx.obj['debug']) 109 + """Show the last 10 lines of the log file"""
110 + context = Context(ctx.obj['config'], ctx.obj['environment'],
111 + ctx.obj['debug'])
88 click.echo('tailing logs...') 112 click.echo('tailing logs...')
89 for e in context.tail()[-10:]: 113 for e in context.tail()[-10:]:
90 ts = datetime.utcfromtimestamp(e['timestamp']//1000).isoformat() 114 ts = datetime.utcfromtimestamp(e['timestamp']//1000).isoformat()
91 click.echo("{}: {}".format(ts, e['message'])) 115 click.echo("{}: {}".format(ts, e['message']))
92 click.echo('...done') 116 click.echo('...done')
93 117
118 +
94 @cli.command() 119 @cli.command()
95 @click.pass_context 120 @click.pass_context
96 def status(ctx): 121 def status(ctx):
97 - context = Context(ctx.obj['config'], ctx.obj['debug']) 122 + """Print a status of this Lambda function"""
123 + context = Context(ctx.obj['config'], ctx.obj['environment'],
124 + ctx.obj['debug'])
98 status = context.status() 125 status = context.status()
99 click.echo(click.style('Policy', bold=True)) 126 click.echo(click.style('Policy', bold=True))
100 if status['policy']: 127 if status['policy']:
...@@ -126,30 +153,38 @@ def status(ctx): ...@@ -126,30 +153,38 @@ def status(ctx):
126 else: 153 else:
127 click.echo(click.style(' None', fg='green')) 154 click.echo(click.style(' None', fg='green'))
128 155
156 +
129 @cli.command() 157 @cli.command()
130 @click.pass_context 158 @click.pass_context
131 def delete(ctx): 159 def delete(ctx):
132 - context = Context(ctx.obj['config'], ctx.obj['debug']) 160 + """Delete the Lambda function and related policies and roles"""
161 + context = Context(ctx.obj['config'], ctx.obj['environment'],
162 + ctx.obj['debug'])
133 click.echo('deleting...') 163 click.echo('deleting...')
134 context.delete() 164 context.delete()
135 click.echo('...done') 165 click.echo('...done')
136 166
167 +
137 @cli.command() 168 @cli.command()
138 @click.pass_context 169 @click.pass_context
139 def add_event_sources(ctx): 170 def add_event_sources(ctx):
140 - context = Context(ctx.obj['config'], ctx.obj['debug']) 171 + """Add any event sources specified in the config file"""
172 + context = Context(ctx.obj['config'], ctx.obj['environment'],
173 + ctx.obj['debug'])
141 click.echo('adding event sources...') 174 click.echo('adding event sources...')
142 context.add_event_sources() 175 context.add_event_sources()
143 click.echo('...done') 176 click.echo('...done')
144 177
178 +
145 @cli.command() 179 @cli.command()
146 @click.pass_context 180 @click.pass_context
147 def update_event_sources(ctx): 181 def update_event_sources(ctx):
148 - context = Context(ctx.obj['config'], ctx.obj['debug']) 182 + """Update event sources specified in the config file"""
183 + context = Context(ctx.obj['config'], ctx.obj['environment'],
184 + ctx.obj['debug'])
149 click.echo('updating event sources...') 185 click.echo('updating event sources...')
150 context.update_event_sources() 186 context.update_event_sources()
151 click.echo('...done') 187 click.echo('...done')
152 188
153 189
154 -if __name__ == '__main__': 190 +cli(obj={})
155 - cli(obj={})
......
1 -boto3==1.1.1 1 +boto3==1.2.0
2 -click==4.0 2 +click==5.1
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
......
...@@ -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 - 'boto3==1.1.1', 8 + 'boto3==1.2.0',
9 - 'click==4.0', 9 + 'click>=5.0',
10 'PyYAML>=3.11' 10 'PyYAML>=3.11'
11 ] 11 ]
12 12
...@@ -22,7 +22,10 @@ setup( ...@@ -22,7 +22,10 @@ setup(
22 packages=find_packages(exclude=['tests*']), 22 packages=find_packages(exclude=['tests*']),
23 package_data={'kappa': ['_version']}, 23 package_data={'kappa': ['_version']},
24 package_dir={'kappa': 'kappa'}, 24 package_dir={'kappa': 'kappa'},
25 - scripts=['bin/kappa'], 25 + entry_points="""
26 + [console_scripts]
27 + kappa=kappa.scripts.cli:cli
28 + """,
26 install_requires=requires, 29 install_requires=requires,
27 license=open("LICENSE").read(), 30 license=open("LICENSE").read(),
28 classifiers=( 31 classifiers=(
......