Skip to content

Commit cc5fd8f

Browse files
author
thomas
authored
Merge pull request #5 from pypyr/dev
waitfor step - wait for any aws client method
2 parents 73fe531 + 1e72a2f commit cc5fd8f

File tree

9 files changed

+1381
-158
lines changed

9 files changed

+1381
-158
lines changed

README.rst

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ steps
7676
| `pypyraws.steps.s3fetchyaml`_ | Fetch a yaml file from s3 into the pypyr | s3Fetch (dict) |
7777
| | context. | |
7878
+-------------------------------+-------------------------------------------------+------------------------------+
79-
| `pypyraws.steps.wait`_ | Wait for an aws client method to complete. | awsWaitIn (dict) |
79+
| `pypyraws.steps.wait`_ | Wait for an aws client waiter method to | awsWaitIn (dict) |
80+
| | complete. | |
81+
+-------------------------------+-------------------------------------------------+------------------------------+
82+
| `pypyraws.steps.waitfor`_ | Wait for any aws client method to complete, | awsWaitFor (dict) |
83+
| | even when it doesn't have an official waiter. | |
8084
+-------------------------------+-------------------------------------------------+------------------------------+
8185

8286
pypyraws.steps.client
@@ -98,6 +102,9 @@ support all the methods you need.
98102

99103
You can actually pretty much just grab the json as written from the excellent
100104
AWS help docs, paste it into some json that pypyr consumes and tadaaa!
105+
Alternatively, grab the samples from the boto3 python documentation to include
106+
in some yaml - the python dictionary structures map to yaml without too much
107+
faff.
101108

102109
Supported AWS services
103110
----------------------
@@ -361,6 +368,65 @@ The input context requires:
361368
362369
The *awsWaitIn* context supports text `Substitutions`_.
363370

371+
pypyraws.steps.waitfor
372+
======================
373+
Custom waiter for any aws client operation. Where `pypyraws.steps.wait`_ uses
374+
the official AWS waiters from the low-level client api, this step allows you to
375+
execute *any* aws low-level client method and wait for a specified field in
376+
the response to become the value you want it to be.
377+
378+
This is especially handy for things like Beanstalk, because Elastic Beanstalk
379+
does not have Waiters for environment creation.
380+
381+
The input context looks like this:
382+
383+
.. code-block:: yaml
384+
385+
awsWaitFor:
386+
awsClientIn: # required. awsClientIn allows the same arguments as pypyraws.steps.client.
387+
serviceName: elasticbeanstalk
388+
methodName: describe_environments
389+
methodArgs:
390+
ApplicationName: my wonderful beanstalk default application
391+
EnvironmentNames:
392+
- my-wonderful-environment
393+
VersionLabel: v0.1
394+
waitForField: '{Environments[0][Status]}' # required. format expression for field name to check in awsClient response
395+
toBe: Ready # required. Stop waiting when waitForField equals this value
396+
pollInterval: 30 # optional. Seconds to wait between polling attempts. Defaults to 30 if not specified.
397+
maxAttempts: 10 # optional. Defaults to 10 if not specified.
398+
errorOnWaitTimeout: True # optional. Defaults to True if not specified. Stop processing if maxAttempts exhausted without reaching toBe value.
399+
400+
See `pypyraws.steps.client`_ for a full listing of available arguments under
401+
*awsClientIn*.
402+
403+
If ``errorOnWaitTimeout`` is True and ``max_attempts`` exhaust before reaching
404+
the desired target state, pypyr will stop processing with a
405+
``pypyraws.errors.WaitTimeOut`` error.
406+
407+
Once this step completes it adds ``awsWaitForTimedOut`` to the pypyr context.
408+
This is a boolean value with values:
409+
410+
+--------------------------+---------------------------------------------------+
411+
| awsWaitForTimedOut | Description |
412+
+--------------------------+---------------------------------------------------+
413+
| True | ``errorOnWaitTimeout=False`` and ``max_attempts`` |
414+
| | exhausted without reaching ``toBe``. |
415+
+--------------------------+---------------------------------------------------+
416+
| False | ``waitForField``'s value becomes ``toBe`` within |
417+
| | ``max_attempts``. |
418+
+--------------------------+---------------------------------------------------+
419+
420+
421+
The *awsWaitFor* context supports text `Substitutions`_. Do note that while
422+
``waitForField`` uses substitution style format strings, the substitutions are
423+
made against the response object that returns from the aws client call specified
424+
in *awsClientIn*, and not from the pypyr context itself.
425+
426+
See a worked example for an `elastic beanstalk custom waiter for environmment
427+
creation here
428+
<https://github.com/pypyr/pypyr-example/blob/master/pipelines/aws-beanstalk-waitfor.yaml>`__.
429+
364430
*************
365431
Substitutions
366432
*************

pypyraws/contextargs.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Prepare aws arguments from the pypyr context."""
2+
from pypyr.errors import KeyInContextHasNoValueError, KeyNotInContextError
3+
4+
5+
def get_awsclient_args(context, calling_module_name):
6+
"""Get required args from context for awsClientIn type steps.
7+
8+
Args:
9+
context: pypyr.context.Context.
10+
calling_module_name: string. This is just to make a friendly error msg
11+
should something go wrong.
12+
13+
Returns:
14+
tuple(client_in, service_name, method_name)
15+
16+
Raises:
17+
pypyr.errors.KeyNotInContextError: Required key missing in context.
18+
pypyr.errors.KeyInContextHasNoValueError: Required key exists but is
19+
empty or None.
20+
"""
21+
try:
22+
client_in = context['awsClientIn']
23+
service_name = client_in['serviceName']
24+
method_name = client_in['methodName']
25+
except KeyError as err:
26+
raise KeyNotInContextError(
27+
f"awsClientIn missing required key for {calling_module_name}: "
28+
f"{err}"
29+
) from err
30+
31+
if not (service_name and service_name.strip()):
32+
raise KeyInContextHasNoValueError(
33+
f'serviceName required in awsClientIn for {calling_module_name}')
34+
35+
if not (method_name and method_name.strip()):
36+
raise KeyInContextHasNoValueError(
37+
f'methodName required in awsClientIn for {calling_module_name}')
38+
39+
return client_in, service_name, method_name
40+
41+
42+
def get_formatted_iterable(input_dict, field_name, context):
43+
"""Format inputdict's field_name field against context.
44+
45+
Args:
46+
input_dict: dict. Dictionary containing dict to format.
47+
field_name: str. Points at field in input_dict to format.
48+
context: pypyr.context.Context. Substitutes string expressions from
49+
this.
50+
51+
Returns:
52+
dict: Formatted dictionary that was at input_dict['field_name']
53+
Returns None if input_dict['field_name'] doesn't exist.
54+
"""
55+
output = input_dict.get(field_name, None)
56+
if output is not None:
57+
output = context.get_formatted_iterable(output)
58+
59+
return output

pypyraws/errors.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Custom exceptions for pypyraws.
2+
3+
All pypyraws specific exceptions derive from pypyr root Error.
4+
"""
5+
6+
from pypyr.errors import PlugInError
7+
8+
9+
class Error(PlugInError):
10+
"""Base class for all pypyraws exceptions."""
11+
12+
13+
class WaitTimeOut(Error):
14+
"""Aws resource that did not finish processing within wait limit."""

pypyraws/steps/client.py

Lines changed: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""pypyr step that runs any boto3 low-level client method."""
22
import logging
33
import pypyraws.aws.service
4-
from pypyr.errors import KeyInContextHasNoValueError, KeyNotInContextError
4+
import pypyraws.contextargs as contextargs
55

66

77
# pypyr logger means the log level will be set correctly and output formatted.
@@ -41,15 +41,16 @@ def run_step(context):
4141
None.
4242
"""
4343
logger.debug("started")
44-
client_in, service_name, method_name = get_service_args(context)
44+
client_in, service_name, method_name = contextargs.get_awsclient_args(
45+
context, __name__)
4546

46-
client_args = client_in.get('clientArgs', None)
47-
if client_args is not None:
48-
client_args = context.get_formatted_iterable(client_args)
47+
client_args = contextargs.get_formatted_iterable(input_dict=client_in,
48+
field_name='clientArgs',
49+
context=context)
4950

50-
method_args = client_in.get('methodArgs', None)
51-
if method_args is not None:
52-
method_args = context.get_formatted_iterable(method_args)
51+
method_args = contextargs.get_formatted_iterable(input_dict=client_in,
52+
field_name='methodArgs',
53+
context=context)
5354

5455
context['awsClientOut'] = pypyraws.aws.service.operation_exec(
5556
service_name=context.get_formatted_string(service_name),
@@ -61,39 +62,3 @@ def run_step(context):
6162
logger.info(f"Executed {method_name} on aws {service_name}.")
6263

6364
logger.debug("done")
64-
65-
66-
def get_service_args(context):
67-
"""Gets required args from context for this step.
68-
69-
Args:
70-
context - dict. context.
71-
72-
Returns:
73-
tuple(client_in, service_name, method_name)
74-
75-
Raises:
76-
pypyr.errors.KeyNotInContextError: Required key missing in context.
77-
pypyr.errors.KeyInContextHasNoValueError: Required key exists but is
78-
empty or None.
79-
"""
80-
try:
81-
client_in = context['awsClientIn']
82-
service_name = client_in['serviceName']
83-
method_name = client_in['methodName']
84-
except KeyError as err:
85-
raise KeyNotInContextError(
86-
"awsClientIn missing required key for pypyraws.steps.client: "
87-
f"{err}"
88-
) from err
89-
90-
# of course, if They went and made it a bool and True this will pass.
91-
if not (service_name and service_name.strip()):
92-
raise KeyInContextHasNoValueError(
93-
'serviceName required in awsClientIn for pypyraws.steps.client')
94-
95-
if not (method_name and method_name.strip()):
96-
raise KeyInContextHasNoValueError(
97-
'methodName required in awsClientIn for pypyraws.steps.client')
98-
99-
return client_in, service_name, method_name

0 commit comments

Comments
 (0)