Mitch Garnaat

bunch of changes leading up to the merge to develop.

...@@ -31,7 +31,8 @@ deploy ...@@ -31,7 +31,8 @@ deploy
31 31
32 The ``deploy`` command does whatever is required to deploy the 32 The ``deploy`` command does whatever is required to deploy the
33 current version of your Lambda function such as creating/updating policies and 33 current version of your Lambda function such as creating/updating policies and
34 -roles and creating or updating the function itself. 34 +roles, creating or updating the function itself, and adding any event sources
35 +specified in your config file.
35 36
36 When the command is run the first time, it creates all of the relevant 37 When the command is run the first time, it creates all of the relevant
37 resources required. On subsequent invocations, it will attempt to determine 38 resources required. On subsequent invocations, it will attempt to determine
...@@ -49,6 +50,10 @@ invoke ...@@ -49,6 +50,10 @@ invoke
49 The ``invoke`` command makes a synchronous call to your Lambda function, 50 The ``invoke`` command makes a synchronous call to your Lambda function,
50 passing test data and display the resulting log data and any response returned 51 passing test data and display the resulting log data and any response returned
51 from your Lambda function. 52 from your Lambda function.
53 +
54 +The ``invoke`` command takes one positional argument, the ``data_file``. This
55 +should be the path to a JSON data file that will be sent to the function as
56 +data.
52 57
53 tag 58 tag
54 --- 59 ---
...@@ -84,9 +89,10 @@ event_sources ...@@ -84,9 +89,10 @@ event_sources
84 ------------- 89 -------------
85 90
86 The ``event_sources`` command provides access the commands available for 91 The ``event_sources`` command provides access the commands available for
87 -dealing with event sources. This command has an additional option: 92 +dealing with event sources. This command takes an additional positional
93 +argument, ``command``.
88 94
89 -* --command - the command to run (add|update|enable|disable) 95 +* command - the command to run (list|enable|disable)
90 96
91 status 97 status
92 ------ 98 ------
......
1 +The Config File
2 +===============
3 +
4 +The config file is at the heart of kappa. It is what describes your functions
5 +and drives your deployments. This section provides a reference for all of the
6 +elements of the kappa config file.
7 +
8 +
9 +Example
10 +-------
11 +
12 +Here is an example config file showing all possible sections.
13 +
14 +.. sourcecode:: yaml
15 + :linenos:
16 +
17 + ---
18 + name: kappa-python-sample
19 + environments:
20 + env1:
21 + profile: profile1
22 + region: us-west-2
23 + policy:
24 + resources:
25 + - arn: arn:aws:dynamodb:us-west-2:123456789012:table/foo
26 + actions:
27 + - "*"
28 + - arn: arn:aws:logs:*:*:*
29 + actions:
30 + - "*"
31 + event_sources:
32 + -
33 + arn: arn:aws:kinesis:us-west-2:123456789012:stream/foo
34 + starting_position: LATEST
35 + batch_size: 100
36 + env2:
37 + profile: profile2
38 + region: us-west-2
39 + policy_resources:
40 + - arn: arn:aws:dynamodb:us-west-2:234567890123:table/foo
41 + actions:
42 + - "*"
43 + - arn: arn:aws:logs:*:*:*
44 + actions:
45 + - "*"
46 + event_sources:
47 + -
48 + arn: arn:aws:kinesis:us-west-2:234567890123:stream/foo
49 + starting_position: LATEST
50 + batch_size: 100
51 + lambda:
52 + description: A simple Python sample
53 + handler: simple.handler
54 + runtime: python2.7
55 + memory_size: 256
56 + timeout: 3
57 + vpc_config:
58 + security_group_ids:
59 + - sg-12345678
60 + - sg-23456789
61 + subnet_ids:
62 + - subnet-12345678
63 + - subnet-23456789
64 +
65 +
66 +Explanations:
67 +
68 +=========== =============================================================
69 +Line Number Description
70 +=========== =============================================================
71 +2 This name will be used to name the function itself as well as
72 + any policies and roles created for use by the function.
73 +3 A map of environments. Each environment represents one
74 + possible deployment target. For example, you might have a
75 + dev and a prod. The names can be whatever you want but the
76 + environment names are specified using the --env option when
77 + you deploy.
78 +5 The profile name associated with this environment. This
79 + refers to a profile in your AWS credential file.
80 +6 The AWS region associated with this environment.
81 +7 This section defines the elements of the IAM policy that will
82 + be created for this function in this environment.
83 +9 Each resource your function needs access to needs to be
84 + listed here. Provide the ARN of the resource as well as
85 + a list of actions. This could be wildcarded to allow all
86 + actions but preferably should list the specific actions you
87 + want to allow.
88 +15 If your Lambda function has any event sources, this would be
89 + where you list them. Here, the example shows a Kinesis
90 + stream but this could also be a DynamoDB stream, an SNS
91 + topic, or an S3 bucket.
92 +18 For Kinesis streams and DynamoDB streams, you can specify
93 + the starting position (one of LATEST or TRIM_HORIZON) and
94 + the batch size.
95 +35 This section contains settings specify to your Lambda
96 + function. See the Lambda docs for details on these.
97 +=========== =============================================================
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
3 You can adapt this file completely to your liking, but it should at least 3 You can adapt this file completely to your liking, but it should at least
4 contain the root `toctree` directive. 4 contain the root `toctree` directive.
5 5
6 -Welcome to kappa's documentation! 6 +Welcome to kappa's documentation
7 -================================= 7 +================================
8 8
9 Contents: 9 Contents:
10 10
...@@ -12,7 +12,8 @@ Contents: ...@@ -12,7 +12,8 @@ Contents:
12 :maxdepth: 2 12 :maxdepth: 2
13 13
14 why 14 why
15 - config_file 15 + how
16 + config_file_example
16 commands 17 commands
17 18
18 19
......
...@@ -187,6 +187,15 @@ class Context(object): ...@@ -187,6 +187,15 @@ class Context(object):
187 for event_source in self.event_sources: 187 for event_source in self.event_sources:
188 event_source.update(self.function) 188 event_source.update(self.function)
189 189
190 + def list_event_sources(self):
191 + event_sources = []
192 + for event_source in self.event_sources:
193 + event_sources.append({'arn': event_source.arn,
194 + 'starting_position': event_source.starting_position,
195 + 'batch_size': event_source.batch_size,
196 + 'enabled': event_source.enabled})
197 + return event_sources
198 +
190 def enable_event_sources(self): 199 def enable_event_sources(self):
191 for event_source in self.event_sources: 200 for event_source in self.event_sources:
192 event_source.enable(self.function) 201 event_source.enable(self.function)
...@@ -206,6 +215,7 @@ class Context(object): ...@@ -206,6 +215,7 @@ class Context(object):
206 LOG.debug('Waiting for policy/role propogation') 215 LOG.debug('Waiting for policy/role propogation')
207 time.sleep(5) 216 time.sleep(5)
208 self.function.create() 217 self.function.create()
218 + self.add_event_sources()
209 219
210 def deploy(self): 220 def deploy(self):
211 if self.policy: 221 if self.policy:
......
...@@ -33,7 +33,7 @@ class EventSource(object): ...@@ -33,7 +33,7 @@ class EventSource(object):
33 33
34 @property 34 @property
35 def starting_position(self): 35 def starting_position(self):
36 - return self._config.get('starting_position', 'TRIM_HORIZON') 36 + return self._config.get('starting_position', 'LATEST')
37 37
38 @property 38 @property
39 def batch_size(self): 39 def batch_size(self):
...@@ -41,7 +41,7 @@ class EventSource(object): ...@@ -41,7 +41,7 @@ class EventSource(object):
41 41
42 @property 42 @property
43 def enabled(self): 43 def enabled(self):
44 - return self._config.get('enabled', True) 44 + return self._config.get('enabled', False)
45 45
46 46
47 class KinesisEventSource(EventSource): 47 class KinesisEventSource(EventSource):
...@@ -161,7 +161,7 @@ class S3EventSource(EventSource): ...@@ -161,7 +161,7 @@ class S3EventSource(EventSource):
161 161
162 def add(self, function): 162 def add(self, function):
163 notification_spec = { 163 notification_spec = {
164 - 'LambdaFunctionConfigurations':[ 164 + 'LambdaFunctionConfigurations': [
165 { 165 {
166 'Id': self._make_notification_id(function.name), 166 'Id': self._make_notification_id(function.name),
167 'Events': [e for e in self._config['events']], 167 'Events': [e for e in self._config['events']],
......
...@@ -57,7 +57,7 @@ class Function(object): ...@@ -57,7 +57,7 @@ class Function(object):
57 @property 57 @property
58 def dependencies(self): 58 def dependencies(self):
59 return self._config.get('dependencies', list()) 59 return self._config.get('dependencies', list())
60 - 60 +
61 @property 61 @property
62 def description(self): 62 def description(self):
63 return self._config['description'] 63 return self._config['description']
...@@ -71,6 +71,18 @@ class Function(object): ...@@ -71,6 +71,18 @@ class Function(object):
71 return self._config['memory_size'] 71 return self._config['memory_size']
72 72
73 @property 73 @property
74 + def vpc_config(self):
75 + vpc_config = {}
76 + if 'vpc_config' in self._config:
77 + if 'security_group_ids' in self._config['vpc_config']:
78 + sgids = self._config['vpc_config']['security_group_ids']
79 + vpc_config['SecurityGroupIds'] = ','.join(sgids)
80 + if 'subnet_ids' in self._config['vpc_config']:
81 + snids = self._config['vpc_config']['subnet_ids']
82 + vpc_config['SubnetIds'] = ','.join(snids)
83 + return vpc_config
84 +
85 + @property
74 def zipfile_name(self): 86 def zipfile_name(self):
75 return '{}.zip'.format(self._context.name) 87 return '{}.zip'.format(self._context.name)
76 88
...@@ -372,6 +384,7 @@ class Function(object): ...@@ -372,6 +384,7 @@ class Function(object):
372 Description=self.description, 384 Description=self.description,
373 Timeout=self.timeout, 385 Timeout=self.timeout,
374 MemorySize=self.memory_size, 386 MemorySize=self.memory_size,
387 + VpcConfig=self.vpc_config,
375 Publish=True) 388 Publish=True)
376 LOG.debug(response) 389 LOG.debug(response)
377 description = 'For stage {}'.format( 390 description = 'For stage {}'.format(
...@@ -418,6 +431,7 @@ class Function(object): ...@@ -418,6 +431,7 @@ class Function(object):
418 response = self._lambda_client.call( 431 response = self._lambda_client.call(
419 'update_function_configuration', 432 'update_function_configuration',
420 FunctionName=self.name, 433 FunctionName=self.name,
434 + VpcConfig=self.vpc_config,
421 Role=exec_role, 435 Role=exec_role,
422 Handler=self.handler, 436 Handler=self.handler,
423 Description=self.description, 437 Description=self.description,
......
...@@ -23,34 +23,32 @@ LOG = logging.getLogger(__name__) ...@@ -23,34 +23,32 @@ LOG = logging.getLogger(__name__)
23 23
24 class Policy(object): 24 class Policy(object):
25 25
26 + _path_prefix = '/kappa/'
27 +
26 def __init__(self, context, config): 28 def __init__(self, context, config):
27 - self._context = context 29 + self.context = context
28 - self._config = config 30 + self.config = config
29 self._iam_client = kappa.awsclient.create_client( 31 self._iam_client = kappa.awsclient.create_client(
30 - 'iam', self._context.session) 32 + 'iam', self.context.session)
31 - self._arn = self._config['policy'].get('arn', None) 33 + self._arn = self.config['policy'].get('arn', None)
32 -
33 - @property
34 - def environment(self):
35 - return self._context.environment
36 34
37 @property 35 @property
38 def name(self): 36 def name(self):
39 - return '{}_{}'.format(self._context.name, self.environment) 37 + return '{}_{}'.format(self.context.name, self.context.environment)
40 38
41 @property 39 @property
42 def description(self): 40 def description(self):
43 return 'A kappa policy to control access to {} resources'.format( 41 return 'A kappa policy to control access to {} resources'.format(
44 - self.environment) 42 + self.context.environment)
45 43
46 def document(self): 44 def document(self):
47 - if ('resources' not in self._config['policy'] and 45 + if ('resources' not in self.config['policy'] and
48 - 'statements' not in self._config['policy']): 46 + 'statements' not in self.config['policy']):
49 return None 47 return None
50 - document = {"Version": "2012-10-17"} 48 + document = {'Version': '2012-10-17'}
51 statements = [] 49 statements = []
52 document['Statement'] = statements 50 document['Statement'] = statements
53 - for resource in self._config['policy']['resources']: 51 + for resource in self.config['policy']['resources']:
54 arn = resource['arn'] 52 arn = resource['arn']
55 _, _, service, _ = arn.split(':', 3) 53 _, _, service, _ = arn.split(':', 3)
56 statement = {"Effect": "Allow", 54 statement = {"Effect": "Allow",
...@@ -60,15 +58,11 @@ class Policy(object): ...@@ -60,15 +58,11 @@ class Policy(object):
60 actions.append("{}:{}".format(service, action)) 58 actions.append("{}:{}".format(service, action))
61 statement['Action'] = actions 59 statement['Action'] = actions
62 statements.append(statement) 60 statements.append(statement)
63 - for statement in self._config['policy'].get('statements', []): 61 + for statement in self.config['policy'].get('statements', []):
64 statements.append(statement) 62 statements.append(statement)
65 return json.dumps(document, indent=2, sort_keys=True) 63 return json.dumps(document, indent=2, sort_keys=True)
66 64
67 @property 65 @property
68 - def path(self):
69 - return self._config.get('path', '/kappa/')
70 -
71 - @property
72 def arn(self): 66 def arn(self):
73 if self._arn is None: 67 if self._arn is None:
74 policy = self.exists() 68 policy = self.exists()
...@@ -79,7 +73,7 @@ class Policy(object): ...@@ -79,7 +73,7 @@ class Policy(object):
79 def _find_all_policies(self): 73 def _find_all_policies(self):
80 try: 74 try:
81 response = self._iam_client.call( 75 response = self._iam_client.call(
82 - 'list_policies', PathPrefix=self.path) 76 + 'list_policies', PathPrefix=self._path_prefix)
83 except Exception: 77 except Exception:
84 LOG.exception('Error listing policies') 78 LOG.exception('Error listing policies')
85 response = {} 79 response = {}
...@@ -130,11 +124,11 @@ class Policy(object): ...@@ -130,11 +124,11 @@ class Policy(object):
130 m = hashlib.md5() 124 m = hashlib.md5()
131 m.update(document.encode('utf-8')) 125 m.update(document.encode('utf-8'))
132 policy_md5 = m.hexdigest() 126 policy_md5 = m.hexdigest()
133 - cached_md5 = self._context.get_cache_value('policy_md5') 127 + cached_md5 = self.context.get_cache_value('policy_md5')
134 LOG.debug('policy_md5: %s', policy_md5) 128 LOG.debug('policy_md5: %s', policy_md5)
135 LOG.debug('cached md5: %s', cached_md5) 129 LOG.debug('cached md5: %s', cached_md5)
136 if policy_md5 != cached_md5: 130 if policy_md5 != cached_md5:
137 - self._context.set_cache_value('policy_md5', policy_md5) 131 + self.context.set_cache_value('policy_md5', policy_md5)
138 return True 132 return True
139 return False 133 return False
140 134
...@@ -156,7 +150,7 @@ class Policy(object): ...@@ -156,7 +150,7 @@ class Policy(object):
156 try: 150 try:
157 response = self._iam_client.call( 151 response = self._iam_client.call(
158 'create_policy', 152 'create_policy',
159 - Path=self.path, PolicyName=self.name, 153 + Path=self._path_prefix, PolicyName=self.name,
160 PolicyDocument=document, 154 PolicyDocument=document,
161 Description=self.description) 155 Description=self.description)
162 LOG.debug(response) 156 LOG.debug(response)
......
...@@ -139,20 +139,19 @@ def delete(ctx): ...@@ -139,20 +139,19 @@ def delete(ctx):
139 139
140 140
141 @cli.command() 141 @cli.command()
142 -@click.option( 142 +@click.argument('command',
143 - '--command', 143 + type=click.Choice(['list', 'enable', 'disable']))
144 - type=click.Choice(['add', 'update', 'enable', 'disable']),
145 - help='Operation to perform on event sources')
146 @pass_ctx 144 @pass_ctx
147 def event_sources(ctx, command): 145 def event_sources(ctx, command):
148 - """Add any event sources specified in the config file""" 146 + """List, enable, and disable event sources specified in the config file"""
149 - if command == 'add': 147 + if command == 'list':
150 - click.echo('adding event sources') 148 + click.echo('listing event sources')
151 - ctx.add_event_sources() 149 + event_sources = ctx.list_event_sources()
152 - click.echo('done') 150 + for es in event_sources:
153 - elif command == 'update': 151 + click.echo('arn: {}'.format(es['arn']))
154 - click.echo('updating event sources') 152 + click.echo('starting position: {}'.format(es['starting_position']))
155 - ctx.update_event_sources() 153 + click.echo('batch size: {}'.format(es['batch_size']))
154 + click.echo('enabled: {}'.format(es['enabled']))
156 click.echo('done') 155 click.echo('done')
157 elif command == 'enable': 156 elif command == 'enable':
158 click.echo('enabling event sources') 157 click.echo('enabling event sources')
......
1 -boto3>=1.2.2 1 +boto3>=1.2.3
2 -placebo>=0.4.1 2 +placebo>=0.7.0
3 click==5.1 3 click==5.1
4 PyYAML>=3.11 4 PyYAML>=3.11
5 mock>=1.0.1 5 mock>=1.0.1
......