diff --git a/kolla/common/config.py b/kolla/common/config.py index 86bdcf2309..7f21d0c25f 100644 --- a/kolla/common/config.py +++ b/kolla/common/config.py @@ -246,8 +246,12 @@ help='Prefix prepended to image names'), cfg.StrOpt('repos-yaml', default='', help='Path to alternative repos.yaml file'), - cfg.StrOpt('engine', default='docker', choices=['docker'], - help='Container engine to build images on.') + cfg.StrOpt('engine', default='docker', choices=['docker','whales'], + help='Container engine to build images on.'), + cfg.StrOpt('cache_from_repo', default=None, help='repository to fetch image cache from'), + cfg.MultiOpt('cache_from_tags', types.String(), + help='additional tags to fetch image cache from, can be specified multiple times.'), + cfg.BoolOpt('push_inline_cache', default=False, help='whether to push layer cahe to repo inline'), ] _BASE_OPTS = [ diff --git a/kolla/engine_adapter/engine.py b/kolla/engine_adapter/engine.py index dc608204c7..c4486ea576 100644 --- a/kolla/engine_adapter/engine.py +++ b/kolla/engine_adapter/engine.py @@ -19,10 +19,16 @@ except (ImportError): LOG.debug("Docker python library was not found") +try: + import python_on_whales +except ImportError: + LOG.debug("python_on_whales library was not found") + class Engine(Enum): DOCKER = "docker" + WHALES = "whales" class UnsupportedEngineError(ValueError): @@ -38,6 +44,8 @@ def __str__(self): def getEngineException(conf): if conf.engine == Engine.DOCKER.value: return (docker.errors.DockerException) + elif conf.engine == Engine.WHALES.value: + return python_on_whales.exceptions.DockerException else: raise UnsupportedEngineError(conf.engine) @@ -46,6 +54,9 @@ def getEngineClient(conf): if conf.engine == Engine.DOCKER.value: kwargs_env = docker.utils.kwargs_from_env() return docker.APIClient(version='auto', **kwargs_env) + elif conf.engine == Engine.WHALES.value: + kwargs_env = docker.utils.kwargs_from_env() + return python_on_whales.DockerClient(**kwargs_env) else: raise UnsupportedEngineError(conf.engine) @@ -53,5 +64,7 @@ def getEngineClient(conf): def getEngineVersion(conf): if conf.engine == Engine.DOCKER.value: return StrictVersion(docker.__version__) + elif conf.engine == Engine.WHALES.value: + return StrictVersion(python_on_whales.__version__) else: raise UnsupportedEngineError(conf.engine) diff --git a/kolla/image/build.py b/kolla/image/build.py index 2c03884161..ba7f4c0d34 100644 --- a/kolla/image/build.py +++ b/kolla/image/build.py @@ -106,7 +106,7 @@ def run_build(): if conf.debug: LOG.setLevel(logging.DEBUG) - if conf.engine not in (engine.Engine.DOCKER.value,): + if conf.engine not in (engine.Engine.DOCKER.value, engine.Engine.WHALES.value): LOG.error(f'Unsupported engine name "{conf.engine}", exiting.') sys.exit(1) LOG.info(f'Using engine: {conf.engine}') diff --git a/kolla/image/kolla_worker.py b/kolla/image/kolla_worker.py index 1a665ddd44..4e8338d010 100644 --- a/kolla/image/kolla_worker.py +++ b/kolla/image/kolla_worker.py @@ -42,7 +42,8 @@ class Image(object): def __init__(self, name, canonical_name, path, parent_name='', status=Status.UNPROCESSED, parent=None, - source=None, logger=None, engine_client=None): + source=None, logger=None, engine_client=None, + cache_from=None): self.name = name self.canonical_name = canonical_name self.path = path @@ -57,11 +58,13 @@ def __init__(self, name, canonical_name, path, parent_name='', self.plugins = [] self.additions = [] self.engine_client = engine_client + self.cache_from = cache_from def copy(self): c = Image(self.name, self.canonical_name, self.path, logger=self.logger, parent_name=self.parent_name, - status=self.status, parent=self.parent) + status=self.status, parent=self.parent, + cache_from=self.cache_from) if self.source: c.source = self.source.copy() if self.children: @@ -636,10 +639,21 @@ def process_source_installation(image, section): else: parent_name = '' del match + + image_kwargs = {} + + if self.conf.cache_from_repo: + cache_from_repo = self.conf.cache_from_repo + cache_from_tags = [self.tag] + cache_from_tags.extend(self.conf.cache_from_tags) + cache_from = [f"{cache_from_repo}/{image_name}:{tag}" for tag in cache_from_tags] + image_kwargs["cache_from"]=cache_from + image = Image(image_name, canonical_name, path, parent_name=parent_name, logger=utils.make_a_logger(self.conf, image_name), - engine_client=self.engine_client) + engine_client=self.engine_client, + **image_kwargs) # NOTE(jeffrey4l): register the opts if the section didn't # register in the kolla/common/config.py file diff --git a/kolla/image/tasks.py b/kolla/image/tasks.py index 5c399869f3..723224a602 100644 --- a/kolla/image/tasks.py +++ b/kolla/image/tasks.py @@ -107,14 +107,20 @@ def run(self): self.success = False def push_image(self, image): - kwargs = dict(stream=True, decode=True) - - for response in self.engine_client.push( - image.canonical_name, **kwargs): - if 'stream' in response: - self.logger.info(response['stream']) - elif 'errorDetail' in response: - raise PushError(response['errorDetail']['message']) + if self.conf.engine == engine.Engine.DOCKER.value: + kwargs = dict(stream=True, decode=True) + for response in self.engine_client.push(image.canonical_name, **kwargs): + if "stream" in response: + self.logger.info(response["stream"]) + elif "errorDetail" in response: + raise PushError(response["errorDetail"]["message"]) + + if self.conf.engine == engine.Engine.WHALES.value: + kwargs = dict(quiet=True) + try: + self.engine_client.push(image.canonical_name, **kwargs) + except Exception as ex: + raise PushError(ex) # Reset any previous errors. image.status = Status.BUILT @@ -367,28 +373,54 @@ def reset_userinfo(tarinfo): pull = self.conf.pull if image.parent is None else False buildargs = self.update_buildargs() + + kwargs = {} + if self.conf.engine == engine.Engine.DOCKER.value: + kwargs["path"] = image.path + kwargs["tag"] = image.canonical_name + kwargs["nocache"] = not self.conf.cache + kwargs["rm"] = True + kwargs["decode"] = True + kwargs["network_mode"] = self.conf.network_mode + kwargs["pull"] = pull + kwargs["forcerm"] = self.forcerm + kwargs["buildargs"] = buildargs + + if self.conf.engine == engine.Engine.WHALES.value: + kwargs["context_path"] = image.path + kwargs["tags"] = [image.canonical_name] + kwargs["pull"] = pull + if buildargs: + kwargs["build_args"] = buildargs + kwargs["stream_logs"] = True + kwargs["cache"] = self.conf.cache + + # pull layer cahce from remote registries + if image.cache_from: + kwargs["cache_from"] = [{"type": "registry", "ref": ref} for ref in image.cache_from] + + # push inline layer cache + if self.conf.push_inline_cache: + kwargs["cache_to"] = {"type": "inline"} + try: - for stream in \ - self.engine_client.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'): + for stream in self.engine_client.build(**kwargs): + if isinstance(stream, dict): + 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 + else: + for line in stream.split("\n"): if line: - self.logger.error('%s', line) - return + self.logger.info("%s", line) if image.status != Status.ERROR and self.conf.squash and \ self.conf.engine == engine.Engine.DOCKER.value: