Skip to content

Commit 489edf7

Browse files
committed
Error handling
jsonschema provides a rich error tree of info, by parsing each error we can pull out relevant info and re-write the error messages. This covers current error handling behaviour. This includes new error handling behaviour for types and formatting of the ports field. Signed-off-by: Mazz Mosley <mazz@houseofmnowster.com>
1 parent faf3078 commit 489edf7

File tree

2 files changed

+78
-4
lines changed

2 files changed

+78
-4
lines changed

compose/config.py

+74-2
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,77 @@ def _is_valid(port):
150150
return False
151151

152152

153+
def get_unsupported_config_msg(service_name, error_key):
154+
msg = "Unsupported config option for '{}' service: '{}'".format(service_name, error_key)
155+
if error_key in DOCKER_CONFIG_HINTS:
156+
msg += " (did you mean '{}'?)".format(DOCKER_CONFIG_HINTS[error_key])
157+
return msg
158+
159+
160+
def process_errors(errors):
161+
"""
162+
jsonschema gives us an error tree full of information to explain what has
163+
gone wrong. Process each error and pull out relevant information and re-write
164+
helpful error messages that are relevant.
165+
"""
166+
def _parse_key_from_error_msg(error):
167+
return error.message.split("'")[1]
168+
169+
root_msgs = []
170+
invalid_keys = []
171+
required = []
172+
type_errors = []
173+
174+
for error in errors:
175+
# handle root level errors
176+
if len(error.path) == 0:
177+
if error.validator == 'type':
178+
msg = "Top level object needs to be a dictionary. Check your .yml file that you have defined a service at the top level."
179+
root_msgs.append(msg)
180+
elif error.validator == 'additionalProperties':
181+
invalid_service_name = _parse_key_from_error_msg(error)
182+
msg = "Invalid service name '{}' - only {} characters are allowed".format(invalid_service_name, VALID_NAME_CHARS)
183+
root_msgs.append(msg)
184+
else:
185+
root_msgs.append(error.message)
186+
187+
else:
188+
# handle service level errors
189+
service_name = error.path[0]
190+
191+
if error.validator == 'additionalProperties':
192+
invalid_config_key = _parse_key_from_error_msg(error)
193+
invalid_keys.append(get_unsupported_config_msg(service_name, invalid_config_key))
194+
elif error.validator == 'anyOf':
195+
if 'image' in error.instance and 'build' in error.instance:
196+
required.append("Service '{}' has both an image and build path specified. A service can either be built to image or use an existing image, not both.".format(service_name))
197+
elif 'image' not in error.instance and 'build' not in error.instance:
198+
required.append("Service '{}' has neither an image nor a build path specified. Exactly one must be provided.".format(service_name))
199+
else:
200+
required.append(error.message)
201+
elif error.validator == 'type':
202+
msg = "a"
203+
if error.validator_value == "array":
204+
msg = "an"
205+
206+
try:
207+
config_key = error.path[1]
208+
type_errors.append("Service '{}' has an invalid value for '{}', it should be {} {}".format(service_name, config_key, msg, error.validator_value))
209+
except IndexError:
210+
config_key = error.path[0]
211+
root_msgs.append("Service '{}' doesn\'t have any configuration options. All top level keys in your docker-compose.yml must map to a dictionary of configuration options.'".format(config_key))
212+
elif error.validator == 'required':
213+
config_key = error.path[1]
214+
required.append("Service '{}' option '{}' is invalid, {}".format(service_name, config_key, error.message))
215+
elif error.validator == 'dependencies':
216+
dependency_key = error.validator_value.keys()[0]
217+
required_keys = ",".join(error.validator_value[dependency_key])
218+
required.append("Invalid '{}' configuration for '{}' service: when defining '{}' you must set '{}' as well".format(
219+
dependency_key, service_name, dependency_key, required_keys))
220+
221+
return "\n".join(root_msgs + invalid_keys + required + type_errors)
222+
223+
153224
def validate_against_schema(config):
154225
config_source_dir = os.path.dirname(os.path.abspath(__file__))
155226
schema_file = os.path.join(config_source_dir, "schema.json")
@@ -159,9 +230,10 @@ def validate_against_schema(config):
159230

160231
validation_output = Draft4Validator(schema, format_checker=FormatChecker(["ports"]))
161232

162-
errors = [error.message for error in sorted(validation_output.iter_errors(config), key=str)]
233+
errors = [error for error in sorted(validation_output.iter_errors(config), key=str)]
163234
if errors:
164-
raise ConfigurationError("Validation failed, reason(s): {}".format("\n".join(errors)))
235+
error_msg = process_errors(errors)
236+
raise ConfigurationError("Validation failed, reason(s):\n{}".format(error_msg))
165237

166238

167239
def load(config_details):

tests/unit/config_test.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,8 @@ def test_validation_fails_with_just_memswap_limit(self):
326326
When you set a 'memswap_limit' it is invalid config unless you also set
327327
a mem_limit
328328
"""
329-
with self.assertRaisesRegexp(config.ConfigurationError, "u'mem_limit' is a dependency of u'memswap_limit'"):
329+
expected_error_msg = "Invalid 'memswap_limit' configuration for 'foo' service: when defining 'memswap_limit' you must set 'mem_limit' as well"
330+
with self.assertRaisesRegexp(config.ConfigurationError, expected_error_msg):
330331
config.load(
331332
config.ConfigDetails(
332333
{
@@ -576,7 +577,8 @@ def test_extends_validation_missing_service_key(self):
576577
)
577578

578579
def test_extends_validation_invalid_key(self):
579-
with self.assertRaisesRegexp(config.ConfigurationError, "'rogue_key' was unexpected"):
580+
expected_error_msg = "Unsupported config option for 'web' service: 'rogue_key'"
581+
with self.assertRaisesRegexp(config.ConfigurationError, expected_error_msg):
580582
config.load(
581583
config.ConfigDetails(
582584
{

0 commit comments

Comments
 (0)