diff --git a/doc/source/image_description/elements.rst b/doc/source/image_description/elements.rst index eb5b96d3c9..0401cd817c 100644 --- a/doc/source/image_description/elements.rst +++ b/doc/source/image_description/elements.rst @@ -1233,6 +1233,76 @@ Used to customize the installation media images created for oem images deployment. For details see: :ref:`installmedia_customize` +.. _sec.registry: + + +------------ + +Setup containers to fetch from a registry assigned to one +of the supported container backends + +.. code:: xml + + + + + +The optional containers element specifies the location of one ore +more containers on a registry `source` server. {kiwi} will take +this information and fetch the containers as OCI archives to +the image. On first boot those container archives will be loaded +into the local container backend store for the selected +backend and the archive files get deleted. + +Supported `backend` values are `docker` and `podman`. +The `backend` attribute is mandatory and specifies for which +container backend the image should be available in the system. +The `containers` element has the following optional attributes: + +arch="arch" + The containers section can be configured to apply only for a certain + architecture. In this case specify the `arch` attribute with a + value as it is reported by :command:`uname -m`. + +profiles="name[,name]" + A list of profiles to which this containers selection applies + (see :ref:`image-profiles`). + + +----------------------- + +Details about the container + +.. code:: xml + + + + + +The `name` attributes is mandatory and specifies +the name of the container as it exists in the registry. +The `container` element has the following optional attributes: + +path="some/path" + The path to the container in the registry. If not specified + the value defaults to `/` + +fetch_only="true|false" + If set to `true` kiwi will only fetch the container but does not + setup the systemd unit for loading the container into + the local registry. In this mode the container archive file stays + in the system and can be handled in a custom way. By default + `fetch_only` is set to `false`. + +tag="tagname" + Specifies the container tag to fetch. If not set the tag name + defaults to `latest` + +arch="arch" + The container section can be configured to apply only for a certain + architecture. In this case specify the `arch` attribute with a + value as it is reported by :command:`uname -m`. + .. _sec.repository: diff --git a/kiwi/builder/template/container_import.py b/kiwi/builder/template/container_import.py new file mode 100644 index 0000000000..fbfba49f64 --- /dev/null +++ b/kiwi/builder/template/container_import.py @@ -0,0 +1,68 @@ +# Copyright (c) 2024 SUSE LLC. All rights reserved. +# +# This file is part of kiwi. +# +# kiwi is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kiwi is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kiwi. If not, see +# +import os +from typing import List +from string import Template +from textwrap import dedent + + +class BuilderTemplateSystemdUnit: + """ + **systemd unit file templates** + """ + def __init__(self): + self.unit = dedent(''' + # kiwi generated unit file + [Unit] + Description=Import Local Container(s) + ''').strip() + os.linesep + + self.service = dedent(''' + [Service] + Type=oneshot + ''').strip() + os.linesep + + self.install = dedent(''' + [Install] + WantedBy=multi-user.target + ''').strip() + os.linesep + + def get_container_import_template( + self, container_files: List[str], load_commands: List[List[str]], + after: List[str] + ): + template_data = self.unit + for container_file in container_files: + template_data += 'ConditionPathExists={0}{1}'.format( + container_file, os.linesep + ) + if after: + template_data += 'After={0}{1}'.format( + ' '.join(after), os.linesep + ) + template_data += self.service + for load_command in load_commands: + template_data += 'ExecStart={0}{1}'.format( + ' '.join(load_command), os.linesep + ) + for container_file in container_files: + template_data += 'ExecStartPost=/bin/rm -f {0}{1}'.format( + container_file, os.linesep + ) + template_data += self.install + return Template(template_data) diff --git a/kiwi/defaults.py b/kiwi/defaults.py index 7539f5c91a..202f2d9752 100644 --- a/kiwi/defaults.py +++ b/kiwi/defaults.py @@ -91,6 +91,7 @@ ) if MODULE_SPEC else 'unknown' TEMP_DIR = '/var/tmp' +LOCAL_CONTAINERS = '/var/tmp/kiwi_containers' CUSTOM_RUNTIME_CONFIG_FILE = None PLATFORM_MACHINE = platform.machine() EFI_FAT_IMAGE_SIZE = 20 diff --git a/kiwi/schema/kiwi.rnc b/kiwi/schema/kiwi.rnc index 38f629172e..8224862a7f 100644 --- a/kiwi/schema/kiwi.rnc +++ b/kiwi/schema/kiwi.rnc @@ -90,6 +90,7 @@ div { k.drivers* & k.strip* & k.repository* & + k.containers* & k.packages* & k.extension? } @@ -1047,6 +1048,61 @@ div { } } +#========================================== +# common element +# +div { + k.containers.profiles.attribute = k.profiles.attribute + k.containers.arch.attribute = k.arch.attribute + k.containers.source.attribute = + ## Name of registry source server + attribute source { text } + k.containers.backend.attribute = + ## Use container with specified container backend + attribute backend { "podman" | "docker" } + k.containers.attlist = + k.containers.profiles.attribute? & + k.containers.arch.attribute? & + k.containers.source.attribute & + k.containers.backend.attribute + k.containers = + element containers { + k.containers.attlist, + k.container+ + } +} + +#========================================== +# common element +# +div { + k.container.arch.attribute = k.arch.attribute + k.container.name.attribute = + ## Container name + attribute name { text } + k.container.path.attribute = + ## Container path, default to '/' if not specified + attribute path { text } + k.container.tag.attribute = + ## Container tag, defaults to 'latest' if not specified + attribute tag { text } + k.container.fetch_only.attribute = + ## Only fetch the container but do not activate the + ## loading of the container at first boot + attribute fetch_only { xsd:boolean } + k.container.attlist = + k.container.name.attribute & + k.container.arch.attribute? & + k.container.path.attribute? & + k.container.tag.attribute? & + k.container.fetch_only.attribute? + k.container = + element container { + k.container.attlist, + empty + } +} + #========================================== # common element # diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng index 85907f868e..89f9d2421d 100644 --- a/kiwi/schema/kiwi.rng +++ b/kiwi/schema/kiwi.rng @@ -208,6 +208,9 @@ named /etc/ImageID + + + @@ -1601,6 +1604,108 @@ definition can be composed by other existing profiles. + +
+ + + + + + + + + Name of registry source server + + + + + Use container with specified container backend + + podman + docker + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + Container name + + + + + Container path, default to '/' if not specified + + + + + Container tag, defaults to 'latest' if not specified + + + + + Only fetch the container but do not activate the +loading of the container at first boot + + + + + + + + + + + + + + + + + + + + + + + + + + +
Setup kiwi_containers.service import unit') + service = BuilderTemplateSystemdUnit() + unit_template = service.get_container_import_template( + container_files_to_load, container_execs_to_load, + list(after_services) + ) + unit = unit_template.substitute() + unit_file = '{0}/etc/systemd/system/{1}.service'.format( + self.root_dir, 'kiwi_containers' + ) + with open(unit_file, 'w') as systemd: + systemd.write(unit) + Command.run( + [ + 'chroot', self.root_dir, + 'systemctl', 'enable', 'kiwi_containers' + ] + ) + def import_description(self) -> None: """ Import XML descriptions, custom scripts, archives and diff --git a/kiwi/tasks/system_build.py b/kiwi/tasks/system_build.py index 8763e56de7..940ba9981a 100644 --- a/kiwi/tasks/system_build.py +++ b/kiwi/tasks/system_build.py @@ -307,6 +307,7 @@ def process(self): setup.setup_timezone() setup.setup_permissions() setup.import_files() + setup.setup_registry_import() # setup permanent image repositories after cleanup setup.import_repositories_marked_as_imageinclude() diff --git a/kiwi/tasks/system_prepare.py b/kiwi/tasks/system_prepare.py index 829a458a71..b17d496b24 100644 --- a/kiwi/tasks/system_prepare.py +++ b/kiwi/tasks/system_prepare.py @@ -293,6 +293,7 @@ def process(self): setup.setup_timezone() setup.setup_permissions() setup.import_files() + setup.setup_registry_import() # setup permanent image repositories after cleanup setup.import_repositories_marked_as_imageinclude() diff --git a/kiwi/xml_parse.py b/kiwi/xml_parse.py index cbf533eca4..396cfe0c93 100644 --- a/kiwi/xml_parse.py +++ b/kiwi/xml_parse.py @@ -812,7 +812,7 @@ class image(GeneratedsSuper): """The root element of the configuration file""" subclass = None superclass = None - def __init__(self, name=None, displayname=None, id=None, schemaversion=None, noNamespaceSchemaLocation=None, schemaLocation=None, include=None, description=None, preferences=None, profiles=None, users=None, drivers=None, strip=None, repository=None, packages=None, extension=None): + def __init__(self, name=None, displayname=None, id=None, schemaversion=None, noNamespaceSchemaLocation=None, schemaLocation=None, include=None, description=None, preferences=None, profiles=None, users=None, drivers=None, strip=None, repository=None, containers=None, packages=None, extension=None): self.original_tagname_ = None self.name = _cast(None, name) self.displayname = _cast(None, displayname) @@ -852,6 +852,10 @@ def __init__(self, name=None, displayname=None, id=None, schemaversion=None, noN self.repository = [] else: self.repository = repository + if containers is None: + self.containers = [] + else: + self.containers = containers if packages is None: self.packages = [] else: @@ -911,6 +915,11 @@ def set_repository(self, repository): self.repository = repository def add_repository(self, value): self.repository.append(value) def insert_repository_at(self, index, value): self.repository.insert(index, value) def replace_repository_at(self, index, value): self.repository[index] = value + def get_containers(self): return self.containers + def set_containers(self, containers): self.containers = containers + def add_containers(self, value): self.containers.append(value) + def insert_containers_at(self, index, value): self.containers.insert(index, value) + def replace_containers_at(self, index, value): self.containers[index] = value def get_packages(self): return self.packages def set_packages(self, packages): self.packages = packages def add_packages(self, value): self.packages.append(value) @@ -950,6 +959,7 @@ def hasContent_(self): self.drivers or self.strip or self.repository or + self.containers or self.packages or self.extension ): @@ -1017,6 +1027,8 @@ def exportChildren(self, outfile, level, namespaceprefix_='', name_='image', fro strip_.export(outfile, level, namespaceprefix_, name_='strip', pretty_print=pretty_print) for repository_ in self.repository: repository_.export(outfile, level, namespaceprefix_, name_='repository', pretty_print=pretty_print) + for containers_ in self.containers: + containers_.export(outfile, level, namespaceprefix_, name_='containers', pretty_print=pretty_print) for packages_ in self.packages: packages_.export(outfile, level, namespaceprefix_, name_='packages', pretty_print=pretty_print) for extension_ in self.extension: @@ -1097,6 +1109,11 @@ def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): obj_.build(child_) self.repository.append(obj_) obj_.original_tagname_ = 'repository' + elif nodeName_ == 'containers': + obj_ = containers.factory() + obj_.build(child_) + self.containers.append(obj_) + obj_.original_tagname_ = 'containers' elif nodeName_ == 'packages': obj_ = packages.factory() obj_.build(child_) @@ -2438,6 +2455,257 @@ def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): # end class requires +class containers(GeneratedsSuper): + subclass = None + superclass = None + def __init__(self, profiles=None, arch=None, source=None, backend=None, container=None): + self.original_tagname_ = None + self.profiles = _cast(None, profiles) + self.arch = _cast(None, arch) + self.source = _cast(None, source) + self.backend = _cast(None, backend) + if container is None: + self.container = [] + else: + self.container = container + def factory(*args_, **kwargs_): + if CurrentSubclassModule_ is not None: + subclass = getSubclassFromModule_( + CurrentSubclassModule_, containers) + if subclass is not None: + return subclass(*args_, **kwargs_) + if containers.subclass: + return containers.subclass(*args_, **kwargs_) + else: + return containers(*args_, **kwargs_) + factory = staticmethod(factory) + def get_container(self): return self.container + def set_container(self, container): self.container = container + def add_container(self, value): self.container.append(value) + def insert_container_at(self, index, value): self.container.insert(index, value) + def replace_container_at(self, index, value): self.container[index] = value + def get_profiles(self): return self.profiles + def set_profiles(self, profiles): self.profiles = profiles + def get_arch(self): return self.arch + def set_arch(self, arch): self.arch = arch + def get_source(self): return self.source + def set_source(self, source): self.source = source + def get_backend(self): return self.backend + def set_backend(self, backend): self.backend = backend + def validate_arch_name(self, value): + # Validate type arch-name, a restriction on xs:token. + if value is not None and Validate_simpletypes_: + if not self.gds_validate_simple_patterns( + self.validate_arch_name_patterns_, value): + warnings_.warn('Value "%s" does not match xsd pattern restrictions: %s' % (value.encode('utf-8'), self.validate_arch_name_patterns_, )) + validate_arch_name_patterns_ = [['^.*$']] + def hasContent_(self): + if ( + self.container + ): + return True + else: + return False + def export(self, outfile, level, namespaceprefix_='', name_='containers', namespacedef_='', pretty_print=True): + imported_ns_def_ = GenerateDSNamespaceDefs_.get('containers') + if imported_ns_def_ is not None: + namespacedef_ = imported_ns_def_ + if pretty_print: + eol_ = '\n' + else: + eol_ = '' + if self.original_tagname_ is not None: + name_ = self.original_tagname_ + showIndent(outfile, level, pretty_print) + outfile.write('<%s%s%s' % (namespaceprefix_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) + already_processed = set() + self.exportAttributes(outfile, level, already_processed, namespaceprefix_, name_='containers') + if self.hasContent_(): + outfile.write('>%s' % (eol_, )) + self.exportChildren(outfile, level + 1, namespaceprefix_='', name_='containers', pretty_print=pretty_print) + showIndent(outfile, level, pretty_print) + outfile.write('%s' % (namespaceprefix_, name_, eol_)) + else: + outfile.write('/>%s' % (eol_, )) + def exportAttributes(self, outfile, level, already_processed, namespaceprefix_='', name_='containers'): + if self.profiles is not None and 'profiles' not in already_processed: + already_processed.add('profiles') + outfile.write(' profiles=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.profiles), input_name='profiles')), )) + if self.arch is not None and 'arch' not in already_processed: + already_processed.add('arch') + outfile.write(' arch=%s' % (quote_attrib(self.arch), )) + if self.source is not None and 'source' not in already_processed: + already_processed.add('source') + outfile.write(' source=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.source), input_name='source')), )) + if self.backend is not None and 'backend' not in already_processed: + already_processed.add('backend') + outfile.write(' backend=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.backend), input_name='backend')), )) + def exportChildren(self, outfile, level, namespaceprefix_='', name_='containers', fromsubclass_=False, pretty_print=True): + if pretty_print: + eol_ = '\n' + else: + eol_ = '' + for container_ in self.container: + container_.export(outfile, level, namespaceprefix_, name_='container', pretty_print=pretty_print) + def build(self, node): + already_processed = set() + self.buildAttributes(node, node.attrib, already_processed) + for child in node: + nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] + self.buildChildren(child, node, nodeName_) + return self + def buildAttributes(self, node, attrs, already_processed): + value = find_attr_value_('profiles', node) + if value is not None and 'profiles' not in already_processed: + already_processed.add('profiles') + self.profiles = value + value = find_attr_value_('arch', node) + if value is not None and 'arch' not in already_processed: + already_processed.add('arch') + self.arch = value + self.arch = ' '.join(self.arch.split()) + self.validate_arch_name(self.arch) # validate type arch-name + value = find_attr_value_('source', node) + if value is not None and 'source' not in already_processed: + already_processed.add('source') + self.source = value + value = find_attr_value_('backend', node) + if value is not None and 'backend' not in already_processed: + already_processed.add('backend') + self.backend = value + self.backend = ' '.join(self.backend.split()) + def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): + if nodeName_ == 'container': + obj_ = container.factory() + obj_.build(child_) + self.container.append(obj_) + obj_.original_tagname_ = 'container' +# end class containers + + +class container(GeneratedsSuper): + subclass = None + superclass = None + def __init__(self, name=None, arch=None, path=None, tag=None, fetch_only=None): + self.original_tagname_ = None + self.name = _cast(None, name) + self.arch = _cast(None, arch) + self.path = _cast(None, path) + self.tag = _cast(None, tag) + self.fetch_only = _cast(bool, fetch_only) + def factory(*args_, **kwargs_): + if CurrentSubclassModule_ is not None: + subclass = getSubclassFromModule_( + CurrentSubclassModule_, container) + if subclass is not None: + return subclass(*args_, **kwargs_) + if container.subclass: + return container.subclass(*args_, **kwargs_) + else: + return container(*args_, **kwargs_) + factory = staticmethod(factory) + def get_name(self): return self.name + def set_name(self, name): self.name = name + def get_arch(self): return self.arch + def set_arch(self, arch): self.arch = arch + def get_path(self): return self.path + def set_path(self, path): self.path = path + def get_tag(self): return self.tag + def set_tag(self, tag): self.tag = tag + def get_fetch_only(self): return self.fetch_only + def set_fetch_only(self, fetch_only): self.fetch_only = fetch_only + def validate_arch_name(self, value): + # Validate type arch-name, a restriction on xs:token. + if value is not None and Validate_simpletypes_: + if not self.gds_validate_simple_patterns( + self.validate_arch_name_patterns_, value): + warnings_.warn('Value "%s" does not match xsd pattern restrictions: %s' % (value.encode('utf-8'), self.validate_arch_name_patterns_, )) + validate_arch_name_patterns_ = [['^.*$']] + def hasContent_(self): + if ( + + ): + return True + else: + return False + def export(self, outfile, level, namespaceprefix_='', name_='container', namespacedef_='', pretty_print=True): + imported_ns_def_ = GenerateDSNamespaceDefs_.get('container') + if imported_ns_def_ is not None: + namespacedef_ = imported_ns_def_ + if pretty_print: + eol_ = '\n' + else: + eol_ = '' + if self.original_tagname_ is not None: + name_ = self.original_tagname_ + showIndent(outfile, level, pretty_print) + outfile.write('<%s%s%s' % (namespaceprefix_, name_, namespacedef_ and ' ' + namespacedef_ or '', )) + already_processed = set() + self.exportAttributes(outfile, level, already_processed, namespaceprefix_, name_='container') + if self.hasContent_(): + outfile.write('>%s' % (eol_, )) + self.exportChildren(outfile, level + 1, namespaceprefix_='', name_='container', pretty_print=pretty_print) + outfile.write('%s' % (namespaceprefix_, name_, eol_)) + else: + outfile.write('/>%s' % (eol_, )) + def exportAttributes(self, outfile, level, already_processed, namespaceprefix_='', name_='container'): + if self.name is not None and 'name' not in already_processed: + already_processed.add('name') + outfile.write(' name=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.name), input_name='name')), )) + if self.arch is not None and 'arch' not in already_processed: + already_processed.add('arch') + outfile.write(' arch=%s' % (quote_attrib(self.arch), )) + if self.path is not None and 'path' not in already_processed: + already_processed.add('path') + outfile.write(' path=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.path), input_name='path')), )) + if self.tag is not None and 'tag' not in already_processed: + already_processed.add('tag') + outfile.write(' tag=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.tag), input_name='tag')), )) + if self.fetch_only is not None and 'fetch_only' not in already_processed: + already_processed.add('fetch_only') + outfile.write(' fetch_only="%s"' % self.gds_format_boolean(self.fetch_only, input_name='fetch_only')) + def exportChildren(self, outfile, level, namespaceprefix_='', name_='container', fromsubclass_=False, pretty_print=True): + pass + def build(self, node): + already_processed = set() + self.buildAttributes(node, node.attrib, already_processed) + for child in node: + nodeName_ = Tag_pattern_.match(child.tag).groups()[-1] + self.buildChildren(child, node, nodeName_) + return self + def buildAttributes(self, node, attrs, already_processed): + value = find_attr_value_('name', node) + if value is not None and 'name' not in already_processed: + already_processed.add('name') + self.name = value + value = find_attr_value_('arch', node) + if value is not None and 'arch' not in already_processed: + already_processed.add('arch') + self.arch = value + self.arch = ' '.join(self.arch.split()) + self.validate_arch_name(self.arch) # validate type arch-name + value = find_attr_value_('path', node) + if value is not None and 'path' not in already_processed: + already_processed.add('path') + self.path = value + value = find_attr_value_('tag', node) + if value is not None and 'tag' not in already_processed: + already_processed.add('tag') + self.tag = value + value = find_attr_value_('fetch_only', node) + if value is not None and 'fetch_only' not in already_processed: + already_processed.add('fetch_only') + if value in ('true', '1'): + self.fetch_only = True + elif value in ('false', '0'): + self.fetch_only = False + else: + raise_parse_error(node, 'Bad boolean attribute') + def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): + pass +# end class container + + class repository(k_source): """The Name of the Repository""" subclass = None @@ -9599,7 +9867,9 @@ def main(): "bootloadersettings", "collectionModule", "configoption", + "container", "containerconfig", + "containers", "description", "dracut", "drivers", diff --git a/kiwi/xml_state.py b/kiwi/xml_state.py index 0371d0e94c..5e5fdd0d64 100644 --- a/kiwi/xml_state.py +++ b/kiwi/xml_state.py @@ -85,6 +85,15 @@ class FileT(NamedTuple): permissions: str +class ContainerT(NamedTuple): + name: str + backend: str + container_file: str + fetch_only: bool + fetch_command: List[str] + load_command: List[str] + + class XMLState: """ **Implements methods to get stateful information from the XML data** @@ -461,6 +470,34 @@ def repository_matches_host_architecture(self, repository: Any) -> bool: """ return self._section_matches_host_architecture(repository) + def containers_matches_host_architecture(self, containers: Any) -> bool: + """ + Tests if the given containers section is applicable for the + current host architecture. If no arch attribute is provided in + the section it is considered as a match and returns: True. + + :param section: XML section object + + :return: True or False + + :rtype: bool + """ + return self._section_matches_host_architecture(containers) + + def container_matches_host_architecture(self, container: Any) -> bool: + """ + Tests if the given container section is applicable for the + current host architecture. If no arch attribute is provided in + the section it is considered as a match and returns: True. + + :param section: XML section object + + :return: True or False + + :rtype: bool + """ + return self._section_matches_host_architecture(container) + def get_package_sections( self, packages_sections: List ) -> List[package_type]: @@ -1711,6 +1748,50 @@ def get_partitions(self) -> Dict[str, ptable_entry_type]: ) return partitions + def get_containers(self) -> List[ContainerT]: + containers = [] + for containers_section in self.get_containers_sections(): + for container in containers_section.get_container(): + if self.container_matches_host_architecture(container): + fetch_command = [] + load_command = [] + container_tag = container.get_tag() or 'latest' + container_path = container.get_path() or '' + container_endpoint = os.path.normpath( + '{0}/{1}/{2}:{3}'.format( + containers_section.get_source(), container_path, + container.name, container_tag + ) + ) + container_file_name = '{0}/{1}_{2}'.format( + defaults.LOCAL_CONTAINERS, container.name, container_tag + ) + container_backend = containers_section.get_backend() or '' + if container_backend in ['podman', 'docker']: + fetch_command = [ + '/usr/bin/skopeo', 'copy', + 'docker://{0}'.format(container_endpoint), + 'oci-archive:{0}:{1}'.format( + container_file_name, container_endpoint + ) + ] + if not container.get_fetch_only(): + load_command = [ + f'/usr/bin/{container_backend}', + 'load', '-i', container_file_name + ] + containers.append( + ContainerT( + name=f'{container.name}_{container_tag}', + backend=container_backend, + container_file=container_file_name, + fetch_only=bool(container.get_fetch_only()), + fetch_command=fetch_command, + load_command=load_command + ) + ) + return containers + def get_volumes(self) -> List[volume_type]: """ List of configured systemdisk volumes. @@ -2014,6 +2095,21 @@ def get_repository_sections(self) -> List: repository_list.append(repository) return repository_list + def get_containers_sections(self) -> List: + """ + List of all containers sections for the selected profiles that + matches the host architecture + + :return: section reference(s) + + :rtype: list + """ + containers_list = [] + for containers in self._profiled(self.xml_data.get_containers()): + if self.containers_matches_host_architecture(containers): + containers_list.append(containers) + return containers_list + def get_repository_sections_used_for_build(self) -> List: """ List of all repositorys sections used to build the image and diff --git a/test/data/example_config.xml b/test/data/example_config.xml index 1fb6302c26..79b2a888b9 100644 --- a/test/data/example_config.xml +++ b/test/data/example_config.xml @@ -182,6 +182,15 @@ + + + + + + + + + diff --git a/test/unit/system/setup_test.py b/test/unit/system/setup_test.py index ab247f69a6..9dbc28cd11 100644 --- a/test/unit/system/setup_test.py +++ b/test/unit/system/setup_test.py @@ -1685,3 +1685,43 @@ def test_import_repositories_marked_as_imageinclude( def test_script_exists(self, mock_path_exists): assert self.setup.script_exists('some-script') == \ mock_path_exists.return_value + + @patch('pathlib.Path') + @patch('kiwi.system.setup.Command.run') + def test_setup_registry_import(self, mock_Command_run, mock_Path): + with patch('builtins.open'): + self.setup_with_real_xml.setup_registry_import() + assert mock_Command_run.call_args_list == [ + call( + [ + 'chroot', 'root_dir', '/usr/bin/skopeo', 'copy', + 'docker://registry.suse.com/home/mschaefer/images_pubcloud' + '/pct/rmtserver:latest', + 'oci-archive:/var/tmp/kiwi_containers/' + 'rmtserver_latest:registry.suse.com/home/mschaefer/' + 'images_pubcloud/pct/rmtserver:latest' + ] + ), + call( + [ + 'chroot', 'root_dir', '/usr/bin/skopeo', 'copy', + 'docker://registry.suse.com/some:latest', + 'oci-archive:/var/tmp/kiwi_containers/' + 'some_latest:registry.suse.com/some:latest' + ] + ), + call( + [ + 'chroot', 'root_dir', '/usr/bin/skopeo', 'copy', + 'docker://docker.io/foo:latest', + 'oci-archive:/var/tmp/kiwi_containers/' + 'foo_latest:docker.io/foo:latest' + ] + ), + call( + [ + 'chroot', 'root_dir', 'systemctl', + 'enable', 'kiwi_containers' + ] + ) + ] diff --git a/test/unit/tasks/system_build_test.py b/test/unit/tasks/system_build_test.py index 8697addbdf..d69d77bd65 100644 --- a/test/unit/tasks/system_build_test.py +++ b/test/unit/tasks/system_build_test.py @@ -186,6 +186,7 @@ def test_process_system_build( self.setup.setup_timezone.assert_called_once_with() self.setup.setup_permissions.assert_called_once_with() self.setup.import_files.assert_called_once_with() + self.setup.setup_registry_import.assert_called_once_with() self.setup.setup_selinux_file_contexts.assert_called_once_with() system_prepare.pinch_system.assert_has_calls( [call(force=False), call(force=True)] diff --git a/test/unit/tasks/system_prepare_test.py b/test/unit/tasks/system_prepare_test.py index 37e0366591..4b31e8501d 100644 --- a/test/unit/tasks/system_prepare_test.py +++ b/test/unit/tasks/system_prepare_test.py @@ -172,6 +172,7 @@ def test_process_system_prepare(self, mock_SystemPrepare, mock_keys): self.setup.setup_timezone.assert_called_once_with() self.setup.setup_permissions.assert_called_once_with() self.setup.import_files.assert_called_once_with() + self.setup.setup_registry_import.assert_called_once_with() self.setup.setup_selinux_file_contexts.assert_called_once_with() system_prepare.pinch_system.assert_has_calls( diff --git a/test/unit/xml_state_test.py b/test/unit/xml_state_test.py index 7335662081..7ca4a479fc 100644 --- a/test/unit/xml_state_test.py +++ b/test/unit/xml_state_test.py @@ -10,7 +10,9 @@ ) from kiwi.defaults import Defaults -from kiwi.xml_state import XMLState +from kiwi.xml_state import ( + XMLState, ContainerT +) from kiwi.storage.disk import ptable_entry_type from kiwi.xml_description import XMLDescription @@ -410,6 +412,56 @@ def test_get_partitions(self): ) } + def test_get_containers(self): + assert self.state.get_containers() == [ + ContainerT( + name='rmtserver_latest', + backend='podman', + container_file='/var/tmp/kiwi_containers/rmtserver_latest', + fetch_only=False, + fetch_command=[ + '/usr/bin/skopeo', 'copy', + 'docker://registry.suse.com/home/mschaefer/' + 'images_pubcloud/pct/rmtserver:latest', + 'oci-archive:/var/tmp/kiwi_containers/' + 'rmtserver_latest:registry.suse.com/home/mschaefer/' + 'images_pubcloud/pct/rmtserver:latest' + ], + load_command=[ + '/usr/bin/podman', 'load', '-i', + '/var/tmp/kiwi_containers/rmtserver_latest' + ] + ), + ContainerT( + name='some_latest', + backend='docker', + container_file='/var/tmp/kiwi_containers/some_latest', + fetch_only=False, + fetch_command=[ + '/usr/bin/skopeo', 'copy', + 'docker://registry.suse.com/some:latest', + 'oci-archive:/var/tmp/kiwi_containers/' + 'some_latest:registry.suse.com/some:latest' + ], + load_command=[ + '/usr/bin/docker', 'load', '-i', + '/var/tmp/kiwi_containers/some_latest' + ] + ), + ContainerT( + name='foo_latest', + backend='podman', + container_file='/var/tmp/kiwi_containers/foo_latest', + fetch_only=True, + fetch_command=[ + '/usr/bin/skopeo', 'copy', 'docker://docker.io/foo:latest', + 'oci-archive:/var/tmp/kiwi_containers/' + 'foo_latest:docker.io/foo:latest' + ], + load_command=[] + ) + ] + def test_get_volumes_custom_root_volume_name(self): description = XMLDescription( '../data/example_lvm_custom_rootvol_config.xml'