175 lines
7.2 KiB
Python
175 lines
7.2 KiB
Python
# 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 search_result is None:
|
|
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
|