Skip to content

Commit 387956e

Browse files
authored
Merge pull request #66 from chmeliik/error-reporting
Error reporting
2 parents c87219f + 64bb80f commit 387956e

File tree

7 files changed

+219
-20
lines changed

7 files changed

+219
-20
lines changed

operatorcourier/api.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from operatorcourier.push import PushCmd
1616
from operatorcourier.format import format_bundle
1717
from operatorcourier.nest import nest_bundles
18+
from operatorcourier.errors import OpCourierBadBundle
1819

1920
logger = logging.getLogger(__name__)
2021

@@ -31,6 +32,12 @@ def build_and_verify(source_dir=None, yamls=None, ui_validate_io=False,
3132
:param ui_validate_io: Optional flag to test operatorhub.io specific validation
3233
:param validation_output: Path to optional output file for validation logs
3334
:param repository: Repository name for the application
35+
36+
:raises TypeError: When called with both source_dir and yamls specified
37+
38+
:raises OpCourierBadYaml: When an invalid yaml file is encountered
39+
:raises OpCourierBadArtifact: When a file is not any of {CSV, CRD, Package}
40+
:raises OpCourierBadBundle: When the resulting bundle fails validation
3441
"""
3542

3643
if source_dir is not None and yamls is not None:
@@ -53,17 +60,19 @@ def build_and_verify(source_dir=None, yamls=None, ui_validate_io=False,
5360
valid, validation_results_dict = ValidateCmd(ui_validate_io).validate(bundle,
5461
repository)
5562

56-
if not valid:
57-
bundle = None
58-
logger.error("Bundle failed validation.")
59-
raise ValueError("Resulting bundle is invalid, input yaml is improperly defined.")
60-
else:
61-
bundle = format_bundle(bundle)
62-
6363
if validation_output is not None:
6464
with open(validation_output, 'w') as f:
6565
f.write(json.dumps(validation_results_dict) + "\n")
66-
return bundle
66+
67+
if valid:
68+
bundle = format_bundle(bundle)
69+
return bundle
70+
else:
71+
logger.error("Bundle failed validation.")
72+
raise OpCourierBadBundle(
73+
"Resulting bundle is invalid, input yaml is improperly defined.",
74+
validation_info=validation_results_dict
75+
)
6776

6877

6978
def build_verify_and_push(namespace, repository, revision, token,
@@ -82,6 +91,16 @@ def build_verify_and_push(namespace, repository, revision, token,
8291
:param source_dir: Path to local directory of yaml files to be read
8392
:param yamls: List of yaml strings to create bundle with
8493
:param validation_output: Path to optional output file for validation logs
94+
95+
:raises TypeError: When called with both source_dir and yamls specified
96+
97+
:raises OpCourierBadYaml: When an invalid yaml file is encountered
98+
:raises OpCourierBadArtifact: When a file is not any of {CSV, CRD, Package}
99+
:raises OpCourierBadBundle: When the resulting bundle fails validation
100+
101+
:raises OpCourierQuayCommunicationError: When communication with Quay fails
102+
:raises OpCourierQuayErrorResponse: When Quay responds with an error
103+
:raises OpCourierQuayError: When the request fails in an unexpected way
85104
"""
86105

87106
bundle = build_and_verify(source_dir, yamls, repository=repository,
@@ -102,6 +121,9 @@ def nest(source_dir, registry_dir):
102121
:param source_dir: Path to local directory of yaml files to be read
103122
:param output_dir: Path of your directory to be populated.
104123
If directory does not exist, it will be created.
124+
125+
:raises OpCourierBadYaml: When an invalid yaml file is encountered
126+
:raises OpCourierBadArtifact: When a file is not any of {CSV, CRD, Package}
105127
"""
106128

107129
yaml_files = []

operatorcourier/errors.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
operatorcourier.errors
3+
4+
Defines custom operator-courier exceptions.
5+
"""
6+
from requests import RequestException
7+
8+
9+
class OpCourierError(Exception):
10+
"""Base class for all operator-courier exceptions"""
11+
pass
12+
13+
14+
class OpCourierValueError(OpCourierError, ValueError):
15+
"""Base class for exceptions that need to inherit from ValueError
16+
for backwards compatibility
17+
"""
18+
pass
19+
20+
21+
class OpCourierBadYaml(OpCourierValueError):
22+
"""Invalid yaml file"""
23+
pass
24+
25+
26+
class OpCourierBadArtifact(OpCourierValueError):
27+
"""File is a valid yaml, but not a valid CSV, CRD or Package"""
28+
pass
29+
30+
31+
class OpCourierBadBundle(OpCourierValueError):
32+
"""Invalid bundle (e.g. missing CSV/CRD/Package).
33+
Contains the info collected during validation of the bundle.
34+
"""
35+
def __init__(self, msg, validation_info):
36+
"""
37+
:param msg: the message of this exception
38+
:param validation_info: info collected during validation of bundle
39+
"""
40+
super().__init__(msg)
41+
self.validation_info = validation_info
42+
43+
44+
class OpCourierQuayError(OpCourierError):
45+
"""An error occurred while pushing bundle to Quay.io"""
46+
pass
47+
48+
49+
class OpCourierQuayCommunicationError(OpCourierQuayError, RequestException):
50+
"""Communication with Quay.io failed"""
51+
pass
52+
53+
54+
class OpCourierQuayErrorResponse(OpCourierQuayError, OpCourierValueError):
55+
"""Quay.io responded with an error"""
56+
def __init__(self, msg, code, error_response):
57+
"""
58+
:param msg: the message of this exception
59+
:param code: http status code returned by response
60+
:param error_response: error json taken from response
61+
"""
62+
super().__init__(msg)
63+
self.code = code
64+
self.error_response = error_response

operatorcourier/identify.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from yaml import safe_load
22
from yaml import MarkedYAMLError
33
import logging
4+
from operatorcourier.errors import OpCourierBadYaml, OpCourierBadArtifact
45

56
logger = logging.getLogger(__name__)
67

@@ -17,7 +18,7 @@ def get_operator_artifact_type(operatorArtifactString):
1718
except MarkedYAMLError:
1819
msg = "Courier requires valid input YAML files"
1920
logger.error(msg)
20-
raise ValueError(msg)
21+
raise OpCourierBadYaml(msg)
2122
else:
2223
artifact_type, artifact_name = None, None
2324
if isinstance(operatorArtifact, dict):
@@ -34,4 +35,4 @@ def get_operator_artifact_type(operatorArtifactString):
3435

3536
msg = 'Courier requires valid CSV, CRD, and Package files'
3637
logger.error(msg)
37-
raise ValueError(msg)
38+
raise OpCourierBadArtifact(msg)

operatorcourier/push.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import tarfile
44
import logging
55
from tempfile import TemporaryDirectory
6+
from operatorcourier.errors import (
7+
OpCourierQuayCommunicationError,
8+
OpCourierQuayErrorResponse
9+
)
610

711
logger = logging.getLogger(__name__)
812

@@ -41,16 +45,23 @@ def _push_to_registry(self, namespace, repository, release, bundle, auth_token):
4145
logger.info('Pushing bundle to %s' % push_uri)
4246
headers = {'Content-Type': 'application/json', 'Authorization': auth_token}
4347
json = {'blob': bundle, 'release': release, "media_type": "helm"}
44-
r = requests.post(push_uri, json=json, headers=headers)
48+
49+
try:
50+
r = requests.post(push_uri, json=json, headers=headers)
51+
except requests.RequestException as e:
52+
msg = str(e)
53+
logger.error(msg)
54+
raise OpCourierQuayCommunicationError(msg)
55+
4556
if r.status_code != 200:
4657
logger.error(r.text)
4758

48-
msg = 'Failed to get error details'
4959
try:
50-
error = r.json()['error']
51-
msg = "Quay.io answered: '{}' ({})".format(
52-
error['code'], error['message'])
53-
except Exception:
54-
pass
60+
r_json = r.json()
61+
except ValueError:
62+
r_json = {}
5563

56-
raise ValueError("Failed to push to app registry: {}".format(msg))
64+
msg = r_json.get('error', {}).get(
65+
'message', 'Failed to get error details.'
66+
)
67+
raise OpCourierQuayErrorResponse(msg, r.status_code, r_json)

tests/test_api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import yaml
33
from operatorcourier import api
44
from operatorcourier.format import unformat_bundle
5+
from operatorcourier.errors import OpCourierBadBundle
56

67

78
@pytest.mark.parametrize('directory,expected', [
@@ -37,3 +38,29 @@ def test_make_bundle_with_yaml_list(yaml_files, expected):
3738
with open(expected, "r") as expected_file:
3839
expected_bundle = yaml.safe_load(expected_file)
3940
assert unformat_bundle(bundle) == unformat_bundle(expected_bundle)
41+
42+
43+
@pytest.mark.parametrize('yaml_files', [
44+
["tests/test_files/bundles/api/bundle1/crd.yml",
45+
"tests/test_files/bundles/api/bundle1/csv.yaml"]
46+
])
47+
def test_make_bundle_invalid(yaml_files):
48+
yamls = []
49+
for file in yaml_files:
50+
with open(file, "r") as yaml_file:
51+
yamls.append(yaml_file.read())
52+
53+
with pytest.raises(OpCourierBadBundle) as e_info:
54+
api.build_and_verify(yamls=yamls)
55+
56+
e = e_info.value
57+
e_msg = "Resulting bundle is invalid, input yaml is improperly defined."
58+
e_dict = {
59+
"warnings": [
60+
"csv metadata.annotations not defined.",
61+
"csv spec.icon not defined"
62+
],
63+
"errors": ["Bundle does not contain any packages."]
64+
}
65+
assert str(e) == e_msg
66+
assert e.validation_info == e_dict

tests/test_errors.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import pytest
2+
from requests import RequestException
3+
4+
from operatorcourier.errors import (
5+
OpCourierError,
6+
OpCourierValueError,
7+
8+
OpCourierBadYaml,
9+
OpCourierBadArtifact,
10+
OpCourierBadBundle,
11+
12+
OpCourierQuayError,
13+
OpCourierQuayCommunicationError,
14+
OpCourierQuayErrorResponse
15+
)
16+
17+
18+
@pytest.mark.parametrize('op_courier_exception', [
19+
OpCourierValueError,
20+
21+
OpCourierBadYaml,
22+
OpCourierBadArtifact,
23+
OpCourierBadBundle,
24+
25+
OpCourierQuayErrorResponse
26+
])
27+
def test_value_error_compatibility(op_courier_exception):
28+
"""Test that exceptions replacing ValueError are backwards compatible."""
29+
assert issubclass(op_courier_exception, ValueError)
30+
31+
32+
@pytest.mark.parametrize('op_courier_exception', [
33+
OpCourierQuayCommunicationError
34+
])
35+
def test_request_exception_compatibility(op_courier_exception):
36+
"""Test that exceptions replacing RequestException are backwards compatible"""
37+
assert (
38+
issubclass(op_courier_exception, RequestException)
39+
and not issubclass(op_courier_exception, ValueError)
40+
)
41+
42+
43+
@pytest.mark.parametrize('op_courier_exception', [
44+
OpCourierValueError,
45+
46+
OpCourierBadYaml,
47+
OpCourierBadArtifact,
48+
OpCourierBadBundle,
49+
50+
OpCourierQuayError,
51+
OpCourierQuayCommunicationError,
52+
OpCourierQuayErrorResponse
53+
])
54+
def test_base_exception_inheritance(op_courier_exception):
55+
"""Test that all exceptions are subclasses of the base exception"""
56+
assert issubclass(op_courier_exception, OpCourierError)
57+
58+
59+
@pytest.mark.parametrize('op_courier_exception', [
60+
OpCourierQuayCommunicationError,
61+
OpCourierQuayErrorResponse
62+
])
63+
def test_quay_exception_inheritance(op_courier_exception):
64+
"""Test that quay exceptions are subclasses of the base quay exception"""
65+
assert issubclass(op_courier_exception, OpCourierQuayError)
66+
67+
68+
def test_create_quay_error_response():
69+
"""Test that creation of quay error responses is working as intended"""
70+
e = OpCourierQuayErrorResponse('oh no', 500, {'error': 'uh oh'})
71+
assert str(e) == 'oh no'
72+
assert e.code == 500
73+
assert e.error_response == {'error': 'uh oh'}

tests/test_identify.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import pytest
22
import operatorcourier.identify as identify
3+
from operatorcourier.errors import OpCourierBadYaml, OpCourierBadArtifact
34
from testfixtures import LogCapture
45

56

@@ -21,7 +22,7 @@ def test_get_operator_artifact_type(fname, expected):
2122
def test_get_operator_artifact_type_assertions(fname):
2223
with open(fname) as f:
2324
yaml = f.read()
24-
with pytest.raises(ValueError) as e, LogCapture() as logs:
25+
with pytest.raises(OpCourierBadArtifact) as e, LogCapture() as logs:
2526
identify.get_operator_artifact_type(yaml)
2627

2728
logs.check(('operatorcourier.identify',
@@ -38,7 +39,7 @@ def test_get_operator_artifact_type_with_invalid_yaml(fname):
3839
with open(fname) as f:
3940
yaml = f.read()
4041

41-
with pytest.raises(ValueError) as e, LogCapture() as logs:
42+
with pytest.raises(OpCourierBadYaml) as e, LogCapture() as logs:
4243
identify.get_operator_artifact_type(yaml)
4344

4445
logs.check(('operatorcourier.identify',

0 commit comments

Comments
 (0)