1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
|
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import jmespath
from jsonschema import Draft4Validator
import botocore.session
from botocore.exceptions import UnknownServiceError
from botocore.utils import ArgumentGenerator
WAITER_SCHEMA = {
"type": "object",
"properties": {
"version": {"type": "number"},
"waiters": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["api"]
},
"operation": {"type": "string"},
"description": {"type": "string"},
"delay": {
"type": "number",
"minimum": 0,
},
"maxAttempts": {
"type": "integer",
"minimum": 1
},
"acceptors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"state": {
"type": "string",
"enum": ["success", "retry", "failure"]
},
"matcher": {
"type": "string",
"enum": [
"path", "pathAll", "pathAny",
"status", "error"
]
},
"argument": {"type": "string"},
"expected": {
"oneOf": [
{"type": "string"},
{"type": "number"},
{"type": "boolean"}
]
}
},
"required": [
"state", "matcher", "expected"
],
"additionalProperties": False
}
}
},
"required": ["operation", "delay", "maxAttempts", "acceptors"],
"additionalProperties": False
}
}
},
"additionalProperties": False
}
def test_lint_waiter_configs():
session = botocore.session.get_session()
validator = Draft4Validator(WAITER_SCHEMA)
for service_name in session.get_available_services():
client = session.create_client(service_name, 'us-east-1')
service_model = client.meta.service_model
try:
# We use the loader directly here because we need the entire
# json document, not just the portions exposed (either
# internally or externally) by the WaiterModel class.
loader = session.get_component('data_loader')
waiter_model = loader.load_service_model(
service_name, 'waiters-2')
except UnknownServiceError:
# The service doesn't have waiters
continue
yield _validate_schema, validator, waiter_model
for waiter_name in client.waiter_names:
yield _lint_single_waiter, client, waiter_name, service_model
def _lint_single_waiter(client, waiter_name, service_model):
try:
waiter = client.get_waiter(waiter_name)
# The 'acceptors' property is dynamic and will create
# the acceptor configs when first accessed. This is still
# considered a failure to construct the waiter which is
# why it's in this try/except block.
# This catches things like:
# * jmespath expression compiles
# * matcher has a known value
acceptors = waiter.config.acceptors
except Exception as e:
raise AssertionError("Could not create waiter '%s': %s"
% (waiter_name, e))
operation_name = waiter.config.operation
# Needs to reference an existing operation name.
if operation_name not in service_model.operation_names:
raise AssertionError("Waiter config references unknown "
"operation: %s" % operation_name)
# Needs to have at least one acceptor.
if not waiter.config.acceptors:
raise AssertionError("Waiter config must have at least "
"one acceptor state: %s" % waiter.name)
op_model = service_model.operation_model(operation_name)
for acceptor in acceptors:
_validate_acceptor(acceptor, op_model, waiter.name)
if not waiter.name.isalnum():
raise AssertionError(
"Waiter name %s is not alphanumeric." % waiter_name
)
def _validate_schema(validator, waiter_json):
errors = list(e.message for e in validator.iter_errors(waiter_json))
if errors:
raise AssertionError('\n'.join(errors))
def _validate_acceptor(acceptor, op_model, waiter_name):
if acceptor.matcher.startswith('path'):
expression = acceptor.argument
# The JMESPath expression should have the potential to match something
# in the response shape.
output_shape = op_model.output_shape
assert output_shape is not None, (
"Waiter '%s' has JMESPath expression with no output shape: %s"
% (waiter_name, op_model))
# We want to check if the JMESPath expression makes sense.
# To do this, we'll generate sample output and evaluate the
# JMESPath expression against the output. We'll then
# check a few things about this returned search result.
search_result = _search_jmespath_expression(expression, op_model)
if not search_result:
raise AssertionError("JMESPath expression did not match "
"anything for waiter '%s': %s"
% (waiter_name, expression))
if acceptor.matcher in ['pathAll', 'pathAny']:
assert isinstance(search_result, list), \
("Attempted to use '%s' matcher in waiter '%s' "
"with non list result in JMESPath expression: %s"
% (acceptor.matcher, waiter_name, expression))
def _search_jmespath_expression(expression, op_model):
arg_gen = ArgumentGenerator(use_member_names=True)
sample_output = arg_gen.generate_skeleton(op_model.output_shape)
search_result = jmespath.search(expression, sample_output)
return search_result
|