aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcin Juszkiewicz <marcin.juszkiewicz+kolla@linaro.org>2022-10-06 11:28:37 +0200
committerMarcin Juszkiewicz <marcin.juszkiewicz+kolla@linaro.org>2022-10-06 13:01:33 +0000
commita8ee3244bfd4f3ee388308da6ab7e9da60d4a6b7 (patch)
tree53acbfe43f828d8b004d2a86a2313d4f85f6f65c
parentaee19bd1c4b45807d7d246b3d06710cca93f137a (diff)
downloadkolla-a8ee3244bfd4f3ee388308da6ab7e9da60d4a6b7.tar.gz
kolla-a8ee3244bfd4f3ee388308da6ab7e9da60d4a6b7.zip
Move build related tasks to separate file
As part of kolla/image/ refactoring we move build related task classes into own file. Based on patch by Konstantin Yarovoy <konstantin.yarovoy@tietoevry.com> Change-Id: I093620679016b37e1664c9fe4cf7559433e744b7
-rwxr-xr-xkolla/image/build.py377
-rw-r--r--kolla/image/tasks.py397
-rw-r--r--kolla/tests/test_build.py29
3 files changed, 413 insertions, 390 deletions
diff --git a/kolla/image/build.py b/kolla/image/build.py
index ff045bbc6..4dad92428 100755
--- a/kolla/image/build.py
+++ b/kolla/image/build.py
@@ -14,25 +14,20 @@
import contextlib
import datetime
-import errno
import json
import logging
import os
import queue
import re
-import requests
import shutil
import sys
-import tarfile
import tempfile
import threading
import time
import docker
-import git
import jinja2
from oslo_config import cfg
-from requests import exceptions as requests_exc
# NOTE(SamYaple): Update the search path to prefer PROJECT_ROOT as the source
@@ -44,9 +39,9 @@ if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
from kolla.common import config as common_config # noqa
-from kolla.common import task # noqa
from kolla.common import utils # noqa
from kolla.image.unbuildable import UNBUILDABLE_IMAGES # noqa
+from kolla.image.tasks import BuildTask # noqa
from kolla.image.utils import LOG # noqa
from kolla.image.utils import Status # noqa
from kolla.image.utils import STATUS_ERRORS # noqa
@@ -56,10 +51,6 @@ from kolla.template import methods as jinja_methods # noqa
from kolla import version # noqa
-class ArchivingError(Exception):
- pass
-
-
@contextlib.contextmanager
def join_many(threads):
try:
@@ -81,23 +72,6 @@ def join_many(threads):
LOG.warning('Force exits')
-class DockerTask(task.Task):
-
- docker_kwargs = docker.utils.kwargs_from_env()
-
- def __init__(self):
- super(DockerTask, self).__init__()
- self._dc = None
-
- @property
- def dc(self):
- if self._dc is not None:
- return self._dc
- docker_kwargs = self.docker_kwargs.copy()
- self._dc = docker.APIClient(version='auto', **docker_kwargs)
- return self._dc
-
-
class Image(object):
def __init__(self, name, canonical_name, path, parent_name='',
status=Status.UNPROCESSED, parent=None,
@@ -141,355 +115,6 @@ class Image(object):
self.parent_name, self.status, self.parent, self.source)
-class PushIntoQueueTask(task.Task):
- """Task that pushes some other task into a queue."""
-
- def __init__(self, push_task, push_queue):
- super(PushIntoQueueTask, self).__init__()
- self.push_task = push_task
- self.push_queue = push_queue
-
- @property
- def name(self):
- return 'PushIntoQueueTask(%s)' % (self.push_task.name)
-
- def run(self):
- self.push_queue.put(self.push_task)
- self.success = True
-
-
-class PushError(Exception):
- """Raised when there is a problem with pushing image to repository."""
- pass
-
-
-class PushTask(DockerTask):
- """Task that pushes an image to a docker repository."""
-
- def __init__(self, conf, image):
- super(PushTask, self).__init__()
- self.conf = conf
- self.image = image
- self.logger = image.logger
-
- @property
- def name(self):
- return 'PushTask(%s)' % self.image.name
-
- def run(self):
- image = self.image
- self.logger.info('Trying to push the image')
- try:
- self.push_image(image)
- except requests_exc.ConnectionError:
- self.logger.exception('Make sure Docker is running and that you'
- ' have the correct privileges to run Docker'
- ' (root)')
- image.status = Status.CONNECTION_ERROR
- except PushError as exception:
- self.logger.error(exception)
- image.status = Status.PUSH_ERROR
- except Exception:
- self.logger.exception('Unknown error when pushing')
- image.status = Status.PUSH_ERROR
- finally:
- if (image.status not in STATUS_ERRORS and
- image.status != Status.UNPROCESSED):
- self.logger.info('Pushed successfully')
- self.success = True
- else:
- self.success = False
-
- def push_image(self, image):
- kwargs = dict(stream=True, decode=True)
-
- for response in self.dc.push(image.canonical_name, **kwargs):
- if 'stream' in response:
- self.logger.info(response['stream'])
- elif 'errorDetail' in response:
- raise PushError(response['errorDetail']['message'])
-
- # Reset any previous errors.
- image.status = Status.BUILT
-
-
-class BuildTask(DockerTask):
- """Task that builds out an image."""
-
- def __init__(self, conf, image, push_queue):
- super(BuildTask, self).__init__()
- self.conf = conf
- self.image = image
- self.push_queue = push_queue
- self.forcerm = not conf.keep
- self.logger = image.logger
-
- @property
- def name(self):
- return 'BuildTask(%s)' % self.image.name
-
- def run(self):
- self.builder(self.image)
- if self.image.status in (Status.BUILT, Status.SKIPPED):
- self.success = True
-
- @property
- def followups(self):
- followups = []
- if self.conf.push and self.success:
- followups.extend([
- # If we are supposed to push the image into a docker
- # repository, then make sure we do that...
- PushIntoQueueTask(
- PushTask(self.conf, self.image),
- self.push_queue),
- ])
- if self.image.children and self.success:
- for image in self.image.children:
- if image.status in (Status.UNMATCHED, Status.SKIPPED,
- Status.UNBUILDABLE):
- continue
- followups.append(BuildTask(self.conf, image, self.push_queue))
- return followups
-
- def process_source(self, image, source):
- if not source['enabled']:
- self.logger.debug("Skipping disabled source %s", source['name'])
- return
-
- dest_archive = os.path.join(image.path, source['name'] + '-archive')
-
- # NOTE(mgoddard): Change ownership of files to root:root. This
- # avoids an issue introduced by the fix for git CVE-2022-24765,
- # which breaks PBR when the source checkout is not owned by the
- # user installing it. LP#1969096
- def reset_userinfo(tarinfo):
- tarinfo.uid = tarinfo.gid = 0
- tarinfo.uname = tarinfo.gname = "root"
- return tarinfo
-
- if source.get('type') == 'url':
- self.logger.debug("Getting archive from %s", source['source'])
- try:
- r = requests.get(source['source'], timeout=self.conf.timeout)
- except requests_exc.Timeout:
- self.logger.exception(
- 'Request timed out while getting archive from %s',
- source['source'])
- image.status = Status.ERROR
- return
-
- if r.status_code == 200:
- with open(dest_archive, 'wb') as f:
- f.write(r.content)
- else:
- self.logger.error(
- 'Failed to download archive: status_code %s',
- r.status_code)
- image.status = Status.ERROR
- return
-
- elif source.get('type') == 'git':
- clone_dir = '{}-{}'.format(dest_archive,
- source['reference'].replace('/', '-'))
- if os.path.exists(clone_dir):
- self.logger.info("Clone dir %s exists. Removing it.",
- clone_dir)
- shutil.rmtree(clone_dir)
-
- try:
- self.logger.debug("Cloning from %s", source['source'])
- git.Git().clone(source['source'], clone_dir)
- git.Git(clone_dir).checkout(source['reference'])
- reference_sha = git.Git(clone_dir).rev_parse('HEAD')
- self.logger.debug("Git checkout by reference %s (%s)",
- source['reference'], reference_sha)
- except Exception as e:
- self.logger.error("Failed to get source from git: %s",
- source['source'])
- self.logger.error("Error: %s", e)
- # clean-up clone folder to retry
- shutil.rmtree(clone_dir)
- image.status = Status.ERROR
- return
-
- with tarfile.open(dest_archive, 'w') as tar:
- tar.add(clone_dir, arcname=os.path.basename(clone_dir),
- filter=reset_userinfo)
-
- elif source.get('type') == 'local':
- self.logger.debug("Getting local archive from %s",
- source['source'])
- if os.path.isdir(source['source']):
- with tarfile.open(dest_archive, 'w') as tar:
- tar.add(source['source'],
- arcname=os.path.basename(source['source']),
- filter=reset_userinfo)
- else:
- shutil.copyfile(source['source'], dest_archive)
-
- else:
- self.logger.error("Wrong source type '%s'", source.get('type'))
- image.status = Status.ERROR
- return
-
- # Set time on destination archive to epoch 0
- os.utime(dest_archive, (0, 0))
-
- return dest_archive
-
- def update_buildargs(self):
- buildargs = dict()
- if self.conf.build_args:
- buildargs = dict(self.conf.build_args)
-
- proxy_vars = ('HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY',
- 'https_proxy', 'FTP_PROXY', 'ftp_proxy',
- 'NO_PROXY', 'no_proxy')
-
- for proxy_var in proxy_vars:
- if proxy_var in os.environ and proxy_var not in buildargs:
- buildargs[proxy_var] = os.environ.get(proxy_var)
-
- if not buildargs:
- return None
- return buildargs
-
- def builder(self, image):
-
- def make_an_archive(items, arcname, item_child_path=None):
- if not item_child_path:
- item_child_path = arcname
- archives = list()
- items_path = os.path.join(image.path, item_child_path)
- for item in items:
- archive_path = self.process_source(image, item)
- if image.status in STATUS_ERRORS:
- raise ArchivingError
- if archive_path:
- archives.append(archive_path)
- if archives:
- for archive in archives:
- with tarfile.open(archive, 'r') as archive_tar:
- archive_tar.extractall(path=items_path)
- else:
- try:
- os.mkdir(items_path)
- except OSError as e:
- if e.errno == errno.EEXIST:
- self.logger.info(
- 'Directory %s already exist. Skipping.',
- items_path)
- else:
- self.logger.error('Failed to create directory %s: %s',
- items_path, e)
- image.status = Status.CONNECTION_ERROR
- raise ArchivingError
- arc_path = os.path.join(image.path, '%s-archive' % arcname)
- with tarfile.open(arc_path, 'w') as tar:
- tar.add(items_path, arcname=arcname)
- return len(os.listdir(items_path))
-
- self.logger.debug('Processing')
-
- if image.status in [Status.SKIPPED, Status.UNBUILDABLE]:
- self.logger.info('Skipping %s' % image.name)
- return
-
- if image.status == Status.UNMATCHED:
- return
-
- if (image.parent is not None and
- image.parent.status in STATUS_ERRORS):
- self.logger.error('Parent image error\'d with message "%s"',
- image.parent.status)
- image.status = Status.PARENT_ERROR
- return
-
- image.status = Status.BUILDING
- image.start = datetime.datetime.now()
- self.logger.info('Building started at %s' % image.start)
-
- if image.source and 'source' in image.source:
- self.process_source(image, image.source)
- if image.status in STATUS_ERRORS:
- return
-
- try:
- plugins_am = make_an_archive(image.plugins, 'plugins')
- except ArchivingError:
- self.logger.error(
- "Failed turning any plugins into a plugins archive")
- return
- else:
- self.logger.debug(
- "Turned %s plugins into plugins archive",
- plugins_am)
- try:
- additions_am = make_an_archive(image.additions, 'additions')
- except ArchivingError:
- self.logger.error(
- "Failed turning any additions into a additions archive")
- return
- else:
- self.logger.debug(
- "Turned %s additions into additions archive",
- additions_am)
-
- # Pull the latest image for the base distro only
- pull = self.conf.pull if image.parent is None else False
-
- buildargs = self.update_buildargs()
- try:
- for stream in self.dc.build(path=image.path,
- tag=image.canonical_name,
- nocache=not self.conf.cache,
- rm=True,
- decode=True,
- network_mode=self.conf.network_mode,
- pull=pull,
- forcerm=self.forcerm,
- buildargs=buildargs):
- if 'stream' in stream:
- for line in stream['stream'].split('\n'):
- if line:
- self.logger.info('%s', line)
- if 'errorDetail' in stream:
- image.status = Status.ERROR
- self.logger.error('Error\'d with the following message')
- for line in stream['errorDetail']['message'].split('\n'):
- if line:
- self.logger.error('%s', line)
- return
-
- if image.status != Status.ERROR and self.conf.squash:
- self.squash()
- except docker.errors.DockerException:
- image.status = Status.ERROR
- self.logger.exception('Unknown docker error when building')
- except Exception:
- image.status = Status.ERROR
- self.logger.exception('Unknown error when building')
- else:
- image.status = Status.BUILT
- now = datetime.datetime.now()
- self.logger.info('Built at %s (took %s)' %
- (now, now - image.start))
-
- def squash(self):
- image_tag = self.image.canonical_name
- image_id = self.dc.inspect_image(image_tag)['Id']
-
- parent_history = self.dc.history(self.image.parent_name)
- parent_last_layer = parent_history[0]['Id']
- self.logger.info('Parent lastest layer is: %s' % parent_last_layer)
-
- utils.squash(image_id, image_tag, from_layer=parent_last_layer,
- cleanup=self.conf.squash_cleanup,
- tmp_dir=self.conf.squash_tmp_dir)
- self.logger.info('Image is squashed successfully')
-
-
class WorkerThread(threading.Thread):
"""Thread that executes tasks until the queue provides a tombstone."""
diff --git a/kolla/image/tasks.py b/kolla/image/tasks.py
new file mode 100644
index 000000000..cbfecce09
--- /dev/null
+++ b/kolla/image/tasks.py
@@ -0,0 +1,397 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import datetime
+import docker
+import errno
+import os
+import shutil
+import tarfile
+
+import git
+import requests
+from requests import exceptions as requests_exc
+
+from kolla.common import task # noqa
+from kolla.common import utils # noqa
+from kolla.image.utils import Status
+from kolla.image.utils import STATUS_ERRORS
+
+
+class ArchivingError(Exception):
+ pass
+
+
+class DockerTask(task.Task):
+
+ docker_kwargs = docker.utils.kwargs_from_env()
+
+ def __init__(self):
+ super(DockerTask, self).__init__()
+ self._dc = None
+
+ @property
+ def dc(self):
+ if self._dc is not None:
+ return self._dc
+ docker_kwargs = self.docker_kwargs.copy()
+ self._dc = docker.APIClient(version='auto', **docker_kwargs)
+ return self._dc
+
+
+class PushIntoQueueTask(task.Task):
+ """Task that pushes some other task into a queue."""
+
+ def __init__(self, push_task, push_queue):
+ super(PushIntoQueueTask, self).__init__()
+ self.push_task = push_task
+ self.push_queue = push_queue
+
+ @property
+ def name(self):
+ return 'PushIntoQueueTask(%s)' % (self.push_task.name)
+
+ def run(self):
+ self.push_queue.put(self.push_task)
+ self.success = True
+
+
+class PushError(Exception):
+ """Raised when there is a problem with pushing image to repository."""
+ pass
+
+
+class PushTask(DockerTask):
+ """Task that pushes an image to a docker repository."""
+
+ def __init__(self, conf, image):
+ super(PushTask, self).__init__()
+ self.conf = conf
+ self.image = image
+ self.logger = image.logger
+
+ @property
+ def name(self):
+ return 'PushTask(%s)' % self.image.name
+
+ def run(self):
+ image = self.image
+ self.logger.info('Trying to push the image')
+ try:
+ self.push_image(image)
+ except requests_exc.ConnectionError:
+ self.logger.exception('Make sure Docker is running and that you'
+ ' have the correct privileges to run Docker'
+ ' (root)')
+ image.status = Status.CONNECTION_ERROR
+ except PushError as exception:
+ self.logger.error(exception)
+ image.status = Status.PUSH_ERROR
+ except Exception:
+ self.logger.exception('Unknown error when pushing')
+ image.status = Status.PUSH_ERROR
+ finally:
+ if (image.status not in STATUS_ERRORS and
+ image.status != Status.UNPROCESSED):
+ self.logger.info('Pushed successfully')
+ self.success = True
+ else:
+ self.success = False
+
+ def push_image(self, image):
+ kwargs = dict(stream=True, decode=True)
+
+ for response in self.dc.push(image.canonical_name, **kwargs):
+ if 'stream' in response:
+ self.logger.info(response['stream'])
+ elif 'errorDetail' in response:
+ raise PushError(response['errorDetail']['message'])
+
+ # Reset any previous errors.
+ image.status = Status.BUILT
+
+
+class BuildTask(DockerTask):
+ """Task that builds out an image."""
+
+ def __init__(self, conf, image, push_queue):
+ super(BuildTask, self).__init__()
+ self.conf = conf
+ self.image = image
+ self.push_queue = push_queue
+ self.forcerm = not conf.keep
+ self.logger = image.logger
+
+ @property
+ def name(self):
+ return 'BuildTask(%s)' % self.image.name
+
+ def run(self):
+ self.builder(self.image)
+ if self.image.status in (Status.BUILT, Status.SKIPPED):
+ self.success = True
+
+ @property
+ def followups(self):
+ followups = []
+ if self.conf.push and self.success:
+ followups.extend([
+ # If we are supposed to push the image into a docker
+ # repository, then make sure we do that...
+ PushIntoQueueTask(
+ PushTask(self.conf, self.image),
+ self.push_queue),
+ ])
+ if self.image.children and self.success:
+ for image in self.image.children:
+ if image.status in (Status.UNMATCHED, Status.SKIPPED,
+ Status.UNBUILDABLE):
+ continue
+ followups.append(BuildTask(self.conf, image, self.push_queue))
+ return followups
+
+ def process_source(self, image, source):
+ if not source['enabled']:
+ self.logger.debug("Skipping disabled source %s", source['name'])
+ return
+
+ dest_archive = os.path.join(image.path, source['name'] + '-archive')
+
+ # NOTE(mgoddard): Change ownership of files to root:root. This
+ # avoids an issue introduced by the fix for git CVE-2022-24765,
+ # which breaks PBR when the source checkout is not owned by the
+ # user installing it. LP#1969096
+ def reset_userinfo(tarinfo):
+ tarinfo.uid = tarinfo.gid = 0
+ tarinfo.uname = tarinfo.gname = "root"
+ return tarinfo
+
+ if source.get('type') == 'url':
+ self.logger.debug("Getting archive from %s", source['source'])
+ try:
+ r = requests.get(source['source'], timeout=self.conf.timeout)
+ except requests_exc.Timeout:
+ self.logger.exception(
+ 'Request timed out while getting archive from %s',
+ source['source'])
+ image.status = Status.ERROR
+ return
+
+ if r.status_code == 200:
+ with open(dest_archive, 'wb') as f:
+ f.write(r.content)
+ else:
+ self.logger.error(
+ 'Failed to download archive: status_code %s',
+ r.status_code)
+ image.status = Status.ERROR
+ return
+
+ elif source.get('type') == 'git':
+ clone_dir = '{}-{}'.format(dest_archive,
+ source['reference'].replace('/', '-'))
+ if os.path.exists(clone_dir):
+ self.logger.info("Clone dir %s exists. Removing it.",
+ clone_dir)
+ shutil.rmtree(clone_dir)
+
+ try:
+ self.logger.debug("Cloning from %s", source['source'])
+ git.Git().clone(source['source'], clone_dir)
+ git.Git(clone_dir).checkout(source['reference'])
+ reference_sha = git.Git(clone_dir).rev_parse('HEAD')
+ self.logger.debug("Git checkout by reference %s (%s)",
+ source['reference'], reference_sha)
+ except Exception as e:
+ self.logger.error("Failed to get source from git: %s",
+ source['source'])
+ self.logger.error("Error: %s", e)
+ # clean-up clone folder to retry
+ shutil.rmtree(clone_dir)
+ image.status = Status.ERROR
+ return
+
+ with tarfile.open(dest_archive, 'w') as tar:
+ tar.add(clone_dir, arcname=os.path.basename(clone_dir),
+ filter=reset_userinfo)
+
+ elif source.get('type') == 'local':
+ self.logger.debug("Getting local archive from %s",
+ source['source'])
+ if os.path.isdir(source['source']):
+ with tarfile.open(dest_archive, 'w') as tar:
+ tar.add(source['source'],
+ arcname=os.path.basename(source['source']),
+ filter=reset_userinfo)
+ else:
+ shutil.copyfile(source['source'], dest_archive)
+
+ else:
+ self.logger.error("Wrong source type '%s'", source.get('type'))
+ image.status = Status.ERROR
+ return
+
+ # Set time on destination archive to epoch 0
+ os.utime(dest_archive, (0, 0))
+
+ return dest_archive
+
+ def update_buildargs(self):
+ buildargs = dict()
+ if self.conf.build_args:
+ buildargs = dict(self.conf.build_args)
+
+ proxy_vars = ('HTTP_PROXY', 'http_proxy', 'HTTPS_PROXY',
+ 'https_proxy', 'FTP_PROXY', 'ftp_proxy',
+ 'NO_PROXY', 'no_proxy')
+
+ for proxy_var in proxy_vars:
+ if proxy_var in os.environ and proxy_var not in buildargs:
+ buildargs[proxy_var] = os.environ.get(proxy_var)
+
+ if not buildargs:
+ return None
+ return buildargs
+
+ def builder(self, image):
+
+ def make_an_archive(items, arcname, item_child_path=None):
+ if not item_child_path:
+ item_child_path = arcname
+ archives = list()
+ items_path = os.path.join(image.path, item_child_path)
+ for item in items:
+ archive_path = self.process_source(image, item)
+ if image.status in STATUS_ERRORS:
+ raise ArchivingError
+ if archive_path:
+ archives.append(archive_path)
+ if archives:
+ for archive in archives:
+ with tarfile.open(archive, 'r') as archive_tar:
+ archive_tar.extractall(path=items_path)
+ else:
+ try:
+ os.mkdir(items_path)
+ except OSError as e:
+ if e.errno == errno.EEXIST:
+ self.logger.info(
+ 'Directory %s already exist. Skipping.',
+ items_path)
+ else:
+ self.logger.error('Failed to create directory %s: %s',
+ items_path, e)
+ image.status = Status.CONNECTION_ERROR
+ raise ArchivingError
+ arc_path = os.path.join(image.path, '%s-archive' % arcname)
+ with tarfile.open(arc_path, 'w') as tar:
+ tar.add(items_path, arcname=arcname)
+ return len(os.listdir(items_path))
+
+ self.logger.debug('Processing')
+
+ if image.status in [Status.SKIPPED, Status.UNBUILDABLE]:
+ self.logger.info('Skipping %s' % image.name)
+ return
+
+ if image.status == Status.UNMATCHED:
+ return
+
+ if (image.parent is not None and
+ image.parent.status in STATUS_ERRORS):
+ self.logger.error('Parent image error\'d with message "%s"',
+ image.parent.status)
+ image.status = Status.PARENT_ERROR
+ return
+
+ image.status = Status.BUILDING
+ image.start = datetime.datetime.now()
+ self.logger.info('Building started at %s' % image.start)
+
+ if image.source and 'source' in image.source:
+ self.process_source(image, image.source)
+ if image.status in STATUS_ERRORS:
+ return
+
+ try:
+ plugins_am = make_an_archive(image.plugins, 'plugins')
+ except ArchivingError:
+ self.logger.error(
+ "Failed turning any plugins into a plugins archive")
+ return
+ else:
+ self.logger.debug(
+ "Turned %s plugins into plugins archive",
+ plugins_am)
+ try:
+ additions_am = make_an_archive(image.additions, 'additions')
+ except ArchivingError:
+ self.logger.error(
+ "Failed turning any additions into a additions archive")
+ return
+ else:
+ self.logger.debug(
+ "Turned %s additions into additions archive",
+ additions_am)
+
+ # Pull the latest image for the base distro only
+ pull = self.conf.pull if image.parent is None else False
+
+ buildargs = self.update_buildargs()
+ try:
+ for stream in self.dc.build(path=image.path,
+ tag=image.canonical_name,
+ nocache=not self.conf.cache,
+ rm=True,
+ decode=True,
+ network_mode=self.conf.network_mode,
+ pull=pull,
+ forcerm=self.forcerm,
+ buildargs=buildargs):
+ if 'stream' in stream:
+ for line in stream['stream'].split('\n'):
+ if line:
+ self.logger.info('%s', line)
+ if 'errorDetail' in stream:
+ image.status = Status.ERROR
+ self.logger.error('Error\'d with the following message')
+ for line in stream['errorDetail']['message'].split('\n'):
+ if line:
+ self.logger.error('%s', line)
+ return
+
+ if image.status != Status.ERROR and self.conf.squash:
+ self.squash()
+ except docker.errors.DockerException:
+ image.status = Status.ERROR
+ self.logger.exception('Unknown docker error when building')
+ except Exception:
+ image.status = Status.ERROR
+ self.logger.exception('Unknown error when building')
+ else:
+ image.status = Status.BUILT
+ now = datetime.datetime.now()
+ self.logger.info('Built at %s (took %s)' %
+ (now, now - image.start))
+
+ def squash(self):
+ image_tag = self.image.canonical_name
+ image_id = self.dc.inspect_image(image_tag)['Id']
+
+ parent_history = self.dc.history(self.image.parent_name)
+ parent_last_layer = parent_history[0]['Id']
+ self.logger.info('Parent lastest layer is: %s' % parent_last_layer)
+
+ utils.squash(image_id, image_tag, from_layer=parent_last_layer,
+ cleanup=self.conf.squash_cleanup,
+ tmp_dir=self.conf.squash_tmp_dir)
+ self.logger.info('Image is squashed successfully')
diff --git a/kolla/tests/test_build.py b/kolla/tests/test_build.py
index 89c406503..b466a6810 100644
--- a/kolla/tests/test_build.py
+++ b/kolla/tests/test_build.py
@@ -18,6 +18,7 @@ from unittest import mock
from kolla.cmd import build as build_cmd
from kolla.image import build
+from kolla.image import tasks
from kolla.image import utils
from kolla.tests import base
@@ -64,7 +65,7 @@ class TasksTest(base.TestCase):
@mock.patch('docker.APIClient')
def test_push_image(self, mock_client):
self.dc = mock_client
- pusher = build.PushTask(self.conf, self.image)
+ pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
@@ -76,7 +77,7 @@ class TasksTest(base.TestCase):
"""failure on connecting Docker API"""
self.dc = mock_client
mock_client().push.side_effect = Exception
- pusher = build.PushTask(self.conf, self.image)
+ pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
@@ -89,7 +90,7 @@ class TasksTest(base.TestCase):
"""failure on connecting Docker API, success on retry"""
self.dc = mock_client
mock_client().push.side_effect = [Exception, []]
- pusher = build.PushTask(self.conf, self.image)
+ pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
@@ -110,7 +111,7 @@ class TasksTest(base.TestCase):
self.dc = mock_client
mock_client().push.return_value = [{'errorDetail': {'message':
'mock push fail'}}]
- pusher = build.PushTask(self.conf, self.image)
+ pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
@@ -124,7 +125,7 @@ class TasksTest(base.TestCase):
self.dc = mock_client
mock_client().push.return_value = [{'errorDetail': {'message':
'mock push fail'}}]
- pusher = build.PushTask(self.conf, self.image)
+ pusher = tasks.PushTask(self.conf, self.image)
pusher.run()
mock_client().push.assert_called_once_with(
self.image.canonical_name, decode=True, stream=True)
@@ -144,7 +145,7 @@ class TasksTest(base.TestCase):
def test_build_image(self, mock_client):
self.dc = mock_client
push_queue = mock.Mock()
- builder = build.BuildTask(self.conf, self.image, push_queue)
+ builder = tasks.BuildTask(self.conf, self.image, push_queue)
builder.run()
mock_client().build.assert_called_once_with(
@@ -161,7 +162,7 @@ class TasksTest(base.TestCase):
push_queue = mock.Mock()
self.conf.set_override('network_mode', 'bridge')
- builder = build.BuildTask(self.conf, self.image, push_queue)
+ builder = tasks.BuildTask(self.conf, self.image, push_queue)
builder.run()
mock_client().build.assert_called_once_with(
@@ -181,7 +182,7 @@ class TasksTest(base.TestCase):
}
self.conf.set_override('build_args', build_args)
push_queue = mock.Mock()
- builder = build.BuildTask(self.conf, self.image, push_queue)
+ builder = tasks.BuildTask(self.conf, self.image, push_queue)
builder.run()
mock_client().build.assert_called_once_with(
@@ -200,7 +201,7 @@ class TasksTest(base.TestCase):
build_args = {
'http_proxy': 'http://FROM_ENV:8080',
}
- builder = build.BuildTask(self.conf, self.image, push_queue)
+ builder = tasks.BuildTask(self.conf, self.image, push_queue)
builder.run()
mock_client().build.assert_called_once_with(
@@ -221,7 +222,7 @@ class TasksTest(base.TestCase):
self.conf.set_override('build_args', build_args)
push_queue = mock.Mock()
- builder = build.BuildTask(self.conf, self.image, push_queue)
+ builder = tasks.BuildTask(self.conf, self.image, push_queue)
builder.run()
mock_client().build.assert_called_once_with(
@@ -242,7 +243,7 @@ class TasksTest(base.TestCase):
'enabled': True
}
push_queue = mock.Mock()
- builder = build.BuildTask(self.conf, self.image, push_queue)
+ builder = tasks.BuildTask(self.conf, self.image, push_queue)
mock_get.side_effect = requests.exceptions.Timeout
get_result = builder.process_source(self.image, self.image.source)
@@ -278,7 +279,7 @@ class TasksTest(base.TestCase):
'enabled': True}]:
self.image.source = source
push_queue = mock.Mock()
- builder = build.BuildTask(self.conf, self.image, push_queue)
+ builder = tasks.BuildTask(self.conf, self.image, push_queue)
get_result = builder.process_source(self.image, self.image.source)
self.assertEqual(self.image.status, utils.Status.ERROR)
self.assertFalse(builder.success)
@@ -301,7 +302,7 @@ class TasksTest(base.TestCase):
self.image.path = "fake_image_path"
mock_path_exists.return_value = True
push_queue = mock.Mock()
- builder = build.BuildTask(self.conf, self.image, push_queue)
+ builder = tasks.BuildTask(self.conf, self.image, push_queue)
get_result = builder.process_source(self.image, self.image.source)
mock_rmtree.assert_called_with(
@@ -319,7 +320,7 @@ class TasksTest(base.TestCase):
}
self.imageChild.children.append(FAKE_IMAGE_CHILD_UNMATCHED)
push_queue = mock.Mock()
- builder = build.BuildTask(self.conf, self.imageChild, push_queue)
+ builder = tasks.BuildTask(self.conf, self.imageChild, push_queue)
builder.success = True
self.conf.push = FAKE_IMAGE_CHILD_UNMATCHED
get_result = builder.followups