Skip to content

Commit

Permalink
Implement image building
Browse files Browse the repository at this point in the history
Pinching a few json stream utils from Compose.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
  • Loading branch information
bfirsh committed Sep 9, 2016
1 parent fce2389 commit bf71a46
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 4 deletions.
9 changes: 9 additions & 0 deletions docker/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,12 @@ def __init__(self, container, exit_status, command, image, stderr):
msg = ("Command '{}' in image '{}' returned non-zero exit status {}: "
"{}").format(command, image, exit_status, stderr)
super(ContainerError, self).__init__(msg)


class StreamParseError(RuntimeError):
def __init__(self, reason):
self.msg = reason


class BuildError(Exception):
pass
21 changes: 19 additions & 2 deletions docker/models/images.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import re

from ..errors import BuildError
from ..utils.json_stream import json_stream
from .resource import Collection, Model


Expand Down Expand Up @@ -42,8 +46,21 @@ def build(self, *args, **kwargs):
"""
Build an image and return it.
"""
image_id = self.client.api.build(*args, **kwargs)
return self.get(image_id)
resp = self.client.api.build(*args, **kwargs)
if isinstance(resp, basestring):
return self.get(resp)
events = list(json_stream(resp))
if not events:
return BuildError('Unknown')
event = events[-1]
if 'stream' in event:
match = re.search(r'Successfully built ([0-9a-f]+)',
event.get('stream', ''))
if match:
image_id = match.group(1)
return self.get(image_id)

raise BuildError(event.get('error') or event)

def get(self, name):
"""
Expand Down
79 changes: 79 additions & 0 deletions docker/utils/json_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from __future__ import absolute_import
from __future__ import unicode_literals

import json
import json.decoder

import six

from ..errors import StreamParseError


json_decoder = json.JSONDecoder()


def stream_as_text(stream):
"""Given a stream of bytes or text, if any of the items in the stream
are bytes convert them to text.
This function can be removed once docker-py returns text streams instead
of byte streams.
"""
for data in stream:
if not isinstance(data, six.text_type):
data = data.decode('utf-8', 'replace')
yield data


def json_splitter(buffer):
"""Attempt to parse a json object from a buffer. If there is at least one
object, return it and the rest of the buffer, otherwise return None.
"""
buffer = buffer.strip()
try:
obj, index = json_decoder.raw_decode(buffer)
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():]
return obj, rest
except ValueError:
return None


def json_stream(stream):
"""Given a stream of text, return a stream of json objects.
This handles streams which are inconsistently buffered (some entries may
be newline delimited, and others are not).
"""
return split_buffer(stream, json_splitter, json_decoder.decode)


def line_splitter(buffer, separator=u'\n'):
index = buffer.find(six.text_type(separator))
if index == -1:
return None
return buffer[:index + 1], buffer[index + 1:]


def split_buffer(stream, splitter=None, decoder=lambda a: a):
"""Given a generator which yields strings and a splitter function,
joins all input, splits on the separator and yields each chunk.
Unlike string.split(), each chunk includes the trailing
separator, except for the last one if none was found on the end
of the input.
"""
splitter = splitter or line_splitter
buffered = six.text_type('')

for data in stream_as_text(stream):
buffered += data
while True:
buffer_split = splitter(buffered)
if buffer_split is None:
break

item, buffered = buffer_split
yield item

if buffered:
try:
yield decoder(buffered)
except Exception as e:
raise StreamParseError(e)
20 changes: 18 additions & 2 deletions tests/integration/images_test.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import io
import unittest

import docker


class ImageTest(unittest.TestCase):
class ImageCollectionTest(unittest.TestCase):

def test_build(self):
pass
client = docker.from_env()
image = client.images.build(fileobj=io.BytesIO(
"FROM alpine\n"
"CMD echo hello world"
))
assert client.containers.run(image) == "hello world\n"

def test_build_with_error(self):
client = docker.from_env()
with self.assertRaises(docker.errors.BuildError) as cm:
client.images.build(fileobj=io.BytesIO(
"FROM alpine\n"
"NOTADOCKERFILECOMMAND"
))
assert str(cm.exception) == ("Unknown instruction: "
"NOTADOCKERFILECOMMAND")

def test_list(self):
client = docker.from_env()
Expand Down
62 changes: 62 additions & 0 deletions tests/unit/utils_json_stream_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# encoding: utf-8
from __future__ import absolute_import
from __future__ import unicode_literals

from docker.utils.json_stream import json_splitter, stream_as_text, json_stream


class TestJsonSplitter(object):

def test_json_splitter_no_object(self):
data = '{"foo": "bar'
assert json_splitter(data) is None

def test_json_splitter_with_object(self):
data = '{"foo": "bar"}\n \n{"next": "obj"}'
assert json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}')

def test_json_splitter_leading_whitespace(self):
data = '\n \r{"foo": "bar"}\n\n {"next": "obj"}'
assert json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}')


class TestStreamAsText(object):

def test_stream_with_non_utf_unicode_character(self):
stream = [b'\xed\xf3\xf3']
output, = stream_as_text(stream)
assert output == '���'

def test_stream_with_utf_character(self):
stream = ['ěĝ'.encode('utf-8')]
output, = stream_as_text(stream)
assert output == 'ěĝ'


class TestJsonStream(object):

def test_with_falsy_entries(self):
stream = [
'{"one": "two"}\n{}\n',
"[1, 2, 3]\n[]\n",
]
output = list(json_stream(stream))
assert output == [
{'one': 'two'},
{},
[1, 2, 3],
[],
]

def test_with_leading_whitespace(self):
stream = [
'\n \r\n {"one": "two"}{"x": 1}',
' {"three": "four"}\t\t{"x": 2}'
]
output = list(json_stream(stream))
assert output == [
{'one': 'two'},
{'x': 1},
{'three': 'four'},
{'x': 2}
]

0 comments on commit bf71a46

Please sign in to comment.