change builder API to return imageID

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>
This commit is contained in:
Nicolas De Loof 2021-04-08 14:52:37 +02:00 committed by Nicolas De loof
parent ad770b272c
commit 3eee3e093a

View File

@ -1,6 +1,5 @@
import enum import enum
import itertools import itertools
import json
import logging import logging
import os import os
import re import re
@ -1125,8 +1124,9 @@ class Service:
'Impossible to perform platform-targeted builds for API version < 1.35' 'Impossible to perform platform-targeted builds for API version < 1.35'
) )
builder = self.client if not cli else _CLIBuilder(progress) builder = _ClientBuilder(self.client) if not cli else _CLIBuilder(progress)
build_output = builder.build( return builder.build(
service=self,
path=path, path=path,
tag=self.image_name, tag=self.image_name,
rm=rm, rm=rm,
@ -1147,30 +1147,7 @@ class Service:
gzip=gzip, gzip=gzip,
isolation=build_opts.get('isolation', self.options.get('isolation', None)), isolation=build_opts.get('isolation', self.options.get('isolation', None)),
platform=self.platform, platform=self.platform,
) output_stream=output_stream)
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
def get_cache_from(self, build_opts): def get_cache_from(self, build_opts):
cache_from = build_opts.get('cache_from', None) cache_from = build_opts.get('cache_from', None)
@ -1827,20 +1804,77 @@ def rewrite_build_path(path):
return path return path
class _CLIBuilder: class _ClientBuilder:
def __init__(self, progress): def __init__(self, client):
self._progress = progress self.client = client
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, nocache=False, rm=False, timeout=None,
custom_context=False, encoding=None, pull=False, custom_context=False, encoding=None, pull=False,
forcerm=False, dockerfile=None, container_limits=None, forcerm=False, dockerfile=None, container_limits=None,
decode=False, buildargs=None, gzip=False, shmsize=None, decode=False, buildargs=None, gzip=False, shmsize=None,
labels=None, cache_from=None, target=None, network_mode=None, labels=None, cache_from=None, target=None, network_mode=None,
squash=None, extra_hosts=None, platform=None, isolation=None, squash=None, extra_hosts=None, platform=None, isolation=None,
use_config_proxy=True): 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, 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):
""" """
Args: Args:
service (str): Service to be built
path (str): Path to the directory containing the Dockerfile path (str): Path to the directory containing the Dockerfile
buildargs (dict): A dictionary of build arguments buildargs (dict): A dictionary of build arguments
cache_from (:py:class:`list`): A list of images used for build cache_from (:py:class:`list`): A list of images used for build
@ -1889,6 +1923,7 @@ class _CLIBuilder:
configuration file (``~/.docker/config.json`` by default) configuration file (``~/.docker/config.json`` by default)
contains a proxy configuration, the corresponding environment contains a proxy configuration, the corresponding environment
variables will be set in the container being built. variables will be set in the container being built.
output_stream (writer): stream to use for build logs
Returns: Returns:
A generator for the build output. A generator for the build output.
""" """
@ -1921,33 +1956,18 @@ class _CLIBuilder:
args = command_builder.build([path]) args = command_builder.build([path])
magic_word = "Successfully built " with subprocess.Popen(args, stdout=output_stream, stderr=sys.stderr,
appear = False
with subprocess.Popen(args, stdout=subprocess.PIPE,
universal_newlines=True) as p: 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() p.communicate()
if p.returncode != 0: if p.returncode != 0:
raise StreamOutputError() raise BuildError(service, "Build failed")
with open(iidfile) as f: with open(iidfile) as f:
line = f.readline() line = f.readline()
image_id = line.split(":")[1].strip() image_id = line.split(":")[1].strip()
os.remove(iidfile) os.remove(iidfile)
# In case of `DOCKER_BUILDKIT=1` return image_id
# 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)})
class _CommandBuilder: class _CommandBuilder: