Skip to content

Commit 1b39df8

Browse files
committed
fix(docker): bubble up docker errors from the internal docker client
Fixes #556
1 parent f1ee725 commit 1b39df8

4 files changed

Lines changed: 54 additions & 24 deletions

File tree

rootfs/api/models/release.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from django.conf import settings
44
from django.db import models
5+
from rest_framework.serializers import ValidationError
56

6-
from registry import publish_release
7+
from registry import publish_release, RegistryException
78
from api.utils import dict_diff
89

910
from api.models import UuidAuditedModel, log_event
@@ -77,6 +78,10 @@ def new(self, user, config, build, summary=None, source_version='latest'):
7778
# If we cannot publish this app, just log and carry on
7879
log_event(self.app, e)
7980
pass
81+
except RegistryException as e:
82+
log_event(self.app, e)
83+
# Uses ValidationError to get return of 400 up in views
84+
raise ValidationError({'detail': str(e)})
8085

8186
return release
8287

@@ -99,8 +104,8 @@ def publish(self, source_version='latest'):
99104
source_image = "{}:{}".format(source_image, source_tag)
100105

101106
# If the build has a SHA, assume it's from deis-builder and in the deis-registry already
102-
deis_registry = bool(self.build.sha)
103107
if not self.build.dockerfile and not self.build.sha:
108+
deis_registry = bool(self.build.sha)
104109
publish_release(source_image, self.image, deis_registry)
105110

106111
def previous(self):

rootfs/registry/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .dockerclient import publish_release # noqa
1+
from .dockerclient import publish_release, RegistryException # noqa

rootfs/registry/dockerclient.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@
77
from django.conf import settings
88
from rest_framework.exceptions import PermissionDenied
99
from simpleflock import SimpleFlock
10+
1011
import docker
1112
import docker.constants
13+
from docker.errors import APIError
1214

1315
logger = logging.getLogger(__name__)
1416

1517

18+
class RegistryException(Exception):
19+
pass
20+
21+
1622
class DockerClient(object):
1723
"""Use the Docker API to pull, tag, build, and push images to deis-registry."""
1824

@@ -40,35 +46,41 @@ def publish_release(self, source, target, deis_registry):
4046
repo = "{}/{}".format(self.registry, src_name)
4147
else:
4248
repo = src_name
43-
self.pull(repo, src_tag)
4449

45-
# tag the image locally without the repository URL
46-
image = "{}:{}".format(src_name, src_tag)
47-
self.tag(image, "{}/{}".format(self.registry, name), tag=tag)
50+
try:
51+
self.pull(repo, src_tag)
52+
53+
# tag the image locally without the repository URL
54+
image = "{}:{}".format(src_name, src_tag)
55+
self.tag(image, "{}/{}".format(self.registry, name), tag=tag)
4856

49-
# push the image to deis-registry
50-
self.push("{}/{}".format(self.registry, name), tag)
57+
# push the image to deis-registry
58+
self.push("{}/{}".format(self.registry, name), tag)
59+
except APIError as e:
60+
raise RegistryException(str(e))
5161

5262
def pull(self, repo, tag):
5363
"""Pull a Docker image into the local storage graph."""
5464
check_blacklist(repo)
5565
logger.info("Pulling Docker image {}:{}".format(repo, tag))
5666
with SimpleFlock(self.FLOCKFILE, timeout=1200):
57-
stream = self.client.pull(repo, tag=tag, stream=True, insecure_registry=True)
58-
log_output(stream)
67+
stream = self.client.pull(repo, tag=tag, stream=True,
68+
decode=True, insecure_registry=True)
69+
log_output(stream, 'pull', repo, tag)
5970

6071
def push(self, repo, tag):
6172
"""Push a local Docker image to a registry."""
6273
logger.info("Pushing Docker image {}:{}".format(repo, tag))
63-
stream = self.client.push(repo, tag=tag, stream=True, insecure_registry=True)
64-
log_output(stream)
74+
stream = self.client.push(repo, tag=tag, stream=True, decode=True,
75+
insecure_registry=True)
76+
log_output(stream, 'push', repo, tag)
6577

6678
def tag(self, image, repo, tag):
6779
"""Tag a local Docker image with a new name and tag."""
6880
check_blacklist(repo)
6981
logger.info("Tagging Docker image {} as {}:{}".format(image, repo, tag))
7082
if not self.client.tag(image, repo, tag=tag, force=True):
71-
raise docker.errors.DockerException("tagging failed")
83+
raise RegistryException('Tagging {} as {}:{} failed'.format(image, repo, tag))
7284

7385

7486
def check_blacklist(repo):
@@ -81,13 +93,26 @@ def check_blacklist(repo):
8193
raise PermissionDenied("Repository name {} is not allowed".format(repo))
8294

8395

84-
def log_output(stream):
85-
"""Log a stream at DEBUG level, and raise DockerException if it contains "error"."""
96+
def log_output(stream, operation, repo, tag):
97+
"""Log a stream at DEBUG level, and raise RegistryException if it contains an error"""
8698
for chunk in stream:
87-
logger.debug(chunk)
8899
# error handling requires looking at the response body
89-
if b'"error"' in chunk.lower():
90-
raise docker.errors.DockerException(chunk)
100+
if 'error' in chunk:
101+
stream_error(chunk, operation, repo, tag)
102+
103+
104+
def stream_error(chunk, operation, repo, tag):
105+
"""Translate docker stream errors into a more digestable format"""
106+
# not all errors provide the code
107+
if 'code' in chunk['errorDetail']:
108+
# permission denied on the repo
109+
if chunk['errorDetail']['code'] == 403:
110+
message = 'Permission Denied attempting to {} image {}:{}'.format(operation, repo, tag)
111+
else:
112+
# grab the generic error and strip the useless Error: portion
113+
message = chunk['error'].replace('Error: ', '')
114+
115+
raise RegistryException(message)
91116

92117

93118
def strip_prefix(name):
@@ -97,5 +122,4 @@ def strip_prefix(name):
97122

98123

99124
def publish_release(source, target, deis_registry):
100-
client = DockerClient()
101-
return client.publish_release(source, target, deis_registry)
125+
return DockerClient().publish_release(source, target, deis_registry)

rootfs/registry/tests.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ def test_publish_release(self, mock_client):
3333
self.client.publish_release('ozzy/embryo:git-f2a8020', 'quay.io/ozzy/embryo:v4', True)
3434
docker_push = self.client.client.push
3535
docker_push.assert_called_with(
36-
'localhost:5000/ozzy/embryo', tag='v4', insecure_registry=True, stream=True)
36+
'localhost:5000/ozzy/embryo', tag='v4', insecure_registry=True,
37+
decode=True, stream=True)
3738
# Test that blacklisted image names can't be published
3839
with self.assertRaises(PermissionDenied):
3940
self.client.publish_release(
@@ -47,7 +48,7 @@ def test_pull(self, mock_client):
4748
self.client.pull('alpine', '3.2')
4849
docker_pull = self.client.client.pull
4950
docker_pull.assert_called_once_with(
50-
'alpine', tag='3.2', insecure_registry=True, stream=True)
51+
'alpine', tag='3.2', insecure_registry=True, decode=True, stream=True)
5152
# Test that blacklisted image names can't be pulled
5253
with self.assertRaises(PermissionDenied):
5354
self.client.pull('deis/controller', 'v1.11.1')
@@ -59,7 +60,7 @@ def test_push(self, mock_client):
5960
self.client.push('ozzy/embryo', 'v4')
6061
docker_push = self.client.client.push
6162
docker_push.assert_called_once_with(
62-
'ozzy/embryo', tag='v4', insecure_registry=True, stream=True)
63+
'ozzy/embryo', tag='v4', insecure_registry=True, decode=True, stream=True)
6364

6465
def test_tag(self, mock_client):
6566
self.client = DockerClient()

0 commit comments

Comments
 (0)