Skip to content

Commit

Permalink
New PodResponse.get_failure_reason() method
Browse files Browse the repository at this point in the history
Signed-off-by: Tim Waugh <twaugh@redhat.com>
  • Loading branch information
twaugh committed Jul 7, 2017
1 parent adb21cb commit d20ae77
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 1 deletion.
59 changes: 58 additions & 1 deletion osbs/build/pod_response.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Copyright (c) 2015 Red Hat, Inc
Copyright (c) 2015, 2017 Red Hat, Inc
All rights reserved.
This software may be modified and distributed under the terms
Expand Down Expand Up @@ -50,3 +50,60 @@ def remove_prefix(image_id, prefix):
return dict([(status['image'], remove_prefix(status['imageID'],
'docker://'))
for status in statuses])

def get_failure_reason(self):
"""
Find the reason a pod failed
:return: dict, which will always have key 'reason':
reason: brief reason for state
containerID (if known): ID of container
exitCode (if known): numeric exit code
"""

REASON_KEY = 'reason'
CID_KEY = 'containerID'
EXIT_KEY = 'exitCode'

pod_status = self.json.get('status', {})
statuses = pod_status.get('containerStatuses', [])

# Find the first non-zero exit code from a container
# and return its 'message' or 'reason' value
for status in statuses:
try:
terminated = status['state']['terminated']
exit_code = terminated['exitCode']
if exit_code != 0:
reason_dict = {
EXIT_KEY: exit_code,
}

if 'containerID' in terminated:
reason_dict[CID_KEY] = terminated['containerID']

for key in ['message', 'reason']:
try:
reason_dict[REASON_KEY] = terminated[key]
break
except KeyError:
continue
else:
# Both 'message' and 'reason' are missing
reason_dict[REASON_KEY] = 'Exit code {code}'.format(
code=exit_code
)

return reason_dict
except KeyError:
continue

# Failing that, return the 'message' or 'reason' value for the
# pod
for key in ['message', 'reason']:
try:
return {REASON_KEY: pod_status[key]}
except KeyError:
continue

return {REASON_KEY: pod_status['phase']}
202 changes: 202 additions & 0 deletions tests/build_/test_pod_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""
Copyright (c) 2017 Red Hat, Inc
All rights reserved.
This software may be modified and distributed under the terms
of the BSD license. See the LICENSE file for details.
"""

from __future__ import unicode_literals

from copy import deepcopy
import pytest

from osbs.build.pod_response import PodResponse


class TestPodResponse(object):
GENERIC_POD_JSON = {
'apiVersion': 'v1',
'kind': 'Pod',
'metadata': {
'name': 'foo',
},
'spec': {
'containers': [
{
'image': 'foo',
'name': 'custom-build',
},
],
},
'status': {},
'phase': 'Succeeded',
}

@pytest.mark.parametrize('expect_image_ids,container_statuses', [
# No containerStatuses
({}, None),
# Empty containerStatuses
({}, []),
# No prefix
({'image': 'no-prefix'},
[{
'image': 'image',
'imageID': 'no-prefix',
}]),
# Normal case
({'image1': 'imageID1', 'image2': 'imageID2'},
[
{
'image': 'image1',
'imageID': 'docker://imageID1',
},
{
'image': 'image2',
'imageID': 'docker://imageID2',
},
]),
])
def test_container_image_ids(self, expect_image_ids, container_statuses):
pod_json = deepcopy(self.GENERIC_POD_JSON)
if container_statuses is not None:
pod_json['status']['containerStatuses'] = container_statuses

pod_response = PodResponse(pod_json)
image_ids = pod_response.get_container_image_ids()
assert image_ids == expect_image_ids

@pytest.mark.parametrize('expected_reason,pod_status', [
# No container statuses but a pod message
({'reason': 'too cold'},
{
'message': 'too cold',
'reason': 'too hot',
'phase': 'Failed',
'containerStatuses': [],
}),
# No non-zero exit code container statuses but a pod reason
({'reason': 'too hot'},
{
'reason': 'too hot',
'phase': 'Failed',
'containerStatuses': [
{
'state': {
'terminated': {
'exitCode': 0
},
},
},
],
}),
# No container statuses, only pod phase available
({'reason': 'Failed'}, {'phase': 'Failed'}),
# Non-zero exit code with message
(
{
'reason': 'Container cannot run',
'exitCode': 1,
},
{
'message': 'too cold',
'reason': 'too hot',
'phase': 'Failed',
'containerStatuses': [
{
'state': {
'terminated': {
# Should ignore this one
'exitCode': 0,
},
},
},
{
'state': {
'terminated': {
'exitCode': 1,
'message': 'Container cannot run',
'reason': 'ContainerCannotRun',
},
},
},
],
}
),
# Non-zero exit code with reason
(
{
'reason': 'ContainerCannotRun',
'exitCode': 1,
},
{
'message': 'too cold',
'reason': 'too hot',
'phase': 'Failed',
'containerStatuses': [
{
'state': {
'terminated': {
'exitCode': 1,
'reason': 'ContainerCannotRun',
},
},
},
{
'state': {
'terminated': {
# Should ignore this one
'exitCode': 2,
'message': 'on fire',
'reason': 'FanFailure',
},
},
},
],
}
),
# Non-zero exit code, no explanation
(
{
'reason': 'Exit code 1',
'exitCode': 1,
'containerID': 'docker://abcde',
},
{
'message': 'too cold',
'reason': 'too hot',
'phase': 'Failed',
'containerStatuses': [
{
'state': {
# Should ignore this one
'running': {},
},
},
{
'state': {
'terminated': {
'containerID': 'docker://abcde',
'exitCode': 1,
},
},
},
],
},
),
])
def test_failure_reason(self, expected_reason,
pod_status):
pod_json = deepcopy(self.GENERIC_POD_JSON)
pod_json['status'].update(pod_status)
pod_response = PodResponse(pod_json)
fail_reason = pod_response.get_failure_reason()
assert fail_reason == expected_reason

0 comments on commit d20ae77

Please sign in to comment.