Skip to content

Commit

Permalink
change builder API to return imageID
Browse files Browse the repository at this point in the history
this allows more flexibility running CLI builder, especially we don't
have anymore to parse build output to get build status/imageID and can
pass the console FDs to buildkit

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
  • Loading branch information
ndeloof committed Apr 9, 2021
1 parent ad770b2 commit 3eee3e0
Showing 1 changed file with 67 additions and 47 deletions.
114 changes: 67 additions & 47 deletions compose/service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import enum
import itertools
import json
import logging
import os
import re
Expand Down Expand Up @@ -1125,8 +1124,9 @@ def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_a
'Impossible to perform platform-targeted builds for API version < 1.35'
)

builder = self.client if not cli else _CLIBuilder(progress)
build_output = builder.build(
builder = _ClientBuilder(self.client) if not cli else _CLIBuilder(progress)
return builder.build(
service=self,
path=path,
tag=self.image_name,
rm=rm,
Expand All @@ -1147,30 +1147,7 @@ def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_a
gzip=gzip,
isolation=build_opts.get('isolation', self.options.get('isolation', None)),
platform=self.platform,
)

try:
all_events = list(stream_output(build_output, output_stream))
except StreamOutputError as e:
raise BuildError(self, str(e))

# Ensure the HTTP connection is not reused for another
# streaming command, as the Docker daemon can sometimes
# complain about it
self.client.close()

image_id = None

for event in all_events:
if 'stream' in event:
match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', ''))
if match:
image_id = match.group(1)

if image_id is None:
raise BuildError(self, event if all_events else 'Unknown')

return image_id
output_stream=output_stream)

def get_cache_from(self, build_opts):
cache_from = build_opts.get('cache_from', None)
Expand Down Expand Up @@ -1827,20 +1804,77 @@ def rewrite_build_path(path):
return path


class _ClientBuilder:
def __init__(self, client):
self.client = client

def build(self, service, path, tag=None, quiet=False, fileobj=None,
nocache=False, rm=False, timeout=None,
custom_context=False, encoding=None, pull=False,
forcerm=False, dockerfile=None, container_limits=None,
decode=False, buildargs=None, gzip=False, shmsize=None,
labels=None, cache_from=None, target=None, network_mode=None,
squash=None, extra_hosts=None, platform=None, isolation=None,
use_config_proxy=True, output_stream=sys.stdout):
build_output = self.client.build(
path=path,
tag=tag,
nocache=nocache,
rm=rm,
pull=pull,
forcerm=forcerm,
dockerfile=dockerfile,
labels=labels,
cache_from=cache_from,
buildargs=buildargs,
network_mode=network_mode,
target=target,
shmsize=shmsize,
extra_hosts=extra_hosts,
container_limits=container_limits,
gzip=gzip,
isolation=isolation,
platform=platform)

try:
all_events = list(stream_output(build_output, output_stream))
except StreamOutputError as e:
raise BuildError(service, str(e))

# Ensure the HTTP connection is not reused for another
# streaming command, as the Docker daemon can sometimes
# complain about it
self.client.close()

image_id = None

for event in all_events:
if 'stream' in event:
match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', ''))
if match:
image_id = match.group(1)

if image_id is None:
raise BuildError(service, event if all_events else 'Unknown')

return image_id


class _CLIBuilder:
def __init__(self, progress):
self._progress = progress

def build(self, path, tag=None, quiet=False, fileobj=None,
def build(self, service, path, tag=None, quiet=False, fileobj=None,
nocache=False, rm=False, timeout=None,
custom_context=False, encoding=None, pull=False,
forcerm=False, dockerfile=None, container_limits=None,
decode=False, buildargs=None, gzip=False, shmsize=None,
labels=None, cache_from=None, target=None, network_mode=None,
squash=None, extra_hosts=None, platform=None, isolation=None,
use_config_proxy=True):
use_config_proxy=True, output_stream=sys.stdout):
"""
Args:
service (str): Service to be built
path (str): Path to the directory containing the Dockerfile
buildargs (dict): A dictionary of build arguments
cache_from (:py:class:`list`): A list of images used for build
Expand Down Expand Up @@ -1889,6 +1923,7 @@ def build(self, path, tag=None, quiet=False, fileobj=None,
configuration file (``~/.docker/config.json`` by default)
contains a proxy configuration, the corresponding environment
variables will be set in the container being built.
output_stream (writer): stream to use for build logs
Returns:
A generator for the build output.
"""
Expand Down Expand Up @@ -1921,33 +1956,18 @@ def build(self, path, tag=None, quiet=False, fileobj=None,

args = command_builder.build([path])

magic_word = "Successfully built "
appear = False
with subprocess.Popen(args, stdout=subprocess.PIPE,
with subprocess.Popen(args, stdout=output_stream, stderr=sys.stderr,
universal_newlines=True) as p:
while True:
line = p.stdout.readline()
if not line:
break
if line.startswith(magic_word):
appear = True
yield json.dumps({"stream": line})

p.communicate()
if p.returncode != 0:
raise StreamOutputError()
raise BuildError(service, "Build failed")

with open(iidfile) as f:
line = f.readline()
image_id = line.split(":")[1].strip()
os.remove(iidfile)

# In case of `DOCKER_BUILDKIT=1`
# there is no success message already present in the output.
# Since that's the way `Service::build` gets the `image_id`
# it has to be added `manually`
if not appear:
yield json.dumps({"stream": "{}{}\n".format(magic_word, image_id)})
return image_id


class _CommandBuilder:
Expand Down

0 comments on commit 3eee3e0

Please sign in to comment.