diff --git a/doc/source/conf.py b/doc/source/conf.py index 6c49246cf2..96495a6c93 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -199,8 +199,7 @@ def setup(app): html_theme = "sphinx_rtd_theme" html_theme_options = { - 'collapse_navigation': False, - 'display_version': False + 'collapse_navigation': False } # -- Options for manual page output --------------------------------------- diff --git a/doc/source/image_description/elements.rst b/doc/source/image_description/elements.rst index eb5b96d3c9..978a523bd8 100644 --- a/doc/source/image_description/elements.rst +++ b/doc/source/image_description/elements.rst @@ -1233,6 +1233,58 @@ 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 one ore more registry elements + +.. code:: xml + + + + + +The optional registry 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 gets deleted. + + +--------------------- + +Details about a container to fetch from a given source registry + +.. code:: xml + + + + + +The `name` and `use_with` attributes are mandatory and specifies +the name of the container and for which backend it should be used. +So far `docker` and `podman` are supported backend values for +the `use_with` attribute. 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 actually 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` + .. _sec.repository: diff --git a/kiwi/builder/template/container_import.py b/kiwi/builder/template/container_import.py new file mode 100644 index 0000000000..29f78181a4 --- /dev/null +++ b/kiwi/builder/template/container_import.py @@ -0,0 +1,42 @@ +# Copyright (c) 2024 SUSE Software Solutions Germany GmbH. 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 string import Template +from textwrap import dedent + + +class BuilderTemplateSystemdUnit: + """ + **systemd unit file templates** + """ + def get_container_import_template(self) -> Template: + template_data = dedent(''' + # kiwi generated unit file + [Unit] + Description=Import Local Container: ${container_name} + ConditionPathExists=${container_file} + + [Service] + Type=oneshot + ExecStart=${load_command} + ExecStartPost=/bin/rm ${container_file} + + [Install] + WantedBy=multi-user.target + ''').strip() + os.linesep + 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..25ff2288ac 100644 --- a/kiwi/schema/kiwi.rnc +++ b/kiwi/schema/kiwi.rnc @@ -90,6 +90,7 @@ div { k.drivers* & k.strip* & k.repository* & + k.registry* & k.packages* & k.extension? } @@ -1047,6 +1048,58 @@ div { } } +#========================================== +# common element +# +div { + k.registry.profiles.attribute = k.profiles.attribute + k.registry.arch.attribute = k.arch.attribute + k.registry.source.attribute = + ## Name of registry source server + attribute source { text } + k.registry.attlist = + k.registry.profiles.attribute? & + k.registry.arch.attribute? & + k.registry.source.attribute + k.registry = + element registry { + k.registry.attlist, + k.container+ + } +} + +#========================================== +# common element +# +div { + k.container.name.attribute = + ## Container name + attribute name { text } + k.container.tag.attribute = + ## Container tag, defaults to 'latest' if not specified + attribute tag { text } + k.container.path.attribute = + ## Container path, default to '/' if not specified + attribute path { 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.use_with.attribute = + ## Use container with specified container backend + attribute use_with { "podman" | "docker" } + k.container.attlist = + k.container.name.attribute & + k.container.use_with.attribute & + k.container.tag.attribute? & + k.container.path.attribute? & + k.container.fetch_only.attribute? + k.container = + element container { + k.container.attlist + } +} + #========================================== # common element # diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng index 85907f868e..fa889fc749 100644 --- a/kiwi/schema/kiwi.rng +++ b/kiwi/schema/kiwi.rng @@ -208,6 +208,9 @@ named /etc/ImageID + + + @@ -1601,6 +1604,101 @@ definition can be composed by other existing profiles. + +
+ + + + + + + + + Name of registry source server + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + Container name + + + + + Container tag, defaults to 'latest' if not specified + + + + + Container path, default to '/' if not specified + + + + + Only fetch the container but do not activate the +loading of the container at first boot + + + + + + Use container with specified container backend + + podman + docker + + + + + + + + + + + + + + + + + + + + + + + +
Setup import unit') + service = BuilderTemplateSystemdUnit() + unit_template = service.get_container_import_template() + unit = unit_template.substitute( + { + 'container_name': container.name, + 'container_file': container.container_file, + 'load_command': ' '.join(container.load_command) + } + ) + unit_file = '{0}/usr/lib/systemd/system/{1}.service'.format( + self.root_dir, container.name + ) + with open(unit_file, 'w') as systemd: + systemd.write(unit) + Command.run( + [ + 'chroot', self.root_dir, + 'systemctl', 'enable', container.name + ] + ) + 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..b1b95e9d19 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, registry=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 registry is None: + self.registry = [] + else: + self.registry = registry 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_registry(self): return self.registry + def set_registry(self, registry): self.registry = registry + def add_registry(self, value): self.registry.append(value) + def insert_registry_at(self, index, value): self.registry.insert(index, value) + def replace_registry_at(self, index, value): self.registry[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.registry 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 registry_ in self.registry: + registry_.export(outfile, level, namespaceprefix_, name_='registry', 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_ == 'registry': + obj_ = registry.factory() + obj_.build(child_) + self.registry.append(obj_) + obj_.original_tagname_ = 'registry' elif nodeName_ == 'packages': obj_ = packages.factory() obj_.build(child_) @@ -2438,6 +2455,238 @@ def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): # end class requires +class registry(GeneratedsSuper): + subclass = None + superclass = None + def __init__(self, profiles=None, arch=None, source=None, container=None): + self.original_tagname_ = None + self.profiles = _cast(None, profiles) + self.arch = _cast(None, arch) + self.source = _cast(None, source) + if container is None: + self.container = [] + else: + self.container = container + def factory(*args_, **kwargs_): + if CurrentSubclassModule_ is not None: + subclass = getSubclassFromModule_( + CurrentSubclassModule_, registry) + if subclass is not None: + return subclass(*args_, **kwargs_) + if registry.subclass: + return registry.subclass(*args_, **kwargs_) + else: + return registry(*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 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_='registry', namespacedef_='', pretty_print=True): + imported_ns_def_ = GenerateDSNamespaceDefs_.get('registry') + 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_='registry') + if self.hasContent_(): + outfile.write('>%s' % (eol_, )) + self.exportChildren(outfile, level + 1, namespaceprefix_='', name_='registry', 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_='registry'): + 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')), )) + def exportChildren(self, outfile, level, namespaceprefix_='', name_='registry', 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 + 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 registry + + +class container(GeneratedsSuper): + subclass = None + superclass = None + def __init__(self, name=None, use_with=None, tag=None, path=None, fetch_only=None): + self.original_tagname_ = None + self.name = _cast(None, name) + self.use_with = _cast(None, use_with) + self.tag = _cast(None, tag) + self.path = _cast(None, path) + 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_use_with(self): return self.use_with + def set_use_with(self, use_with): self.use_with = use_with + def get_tag(self): return self.tag + def set_tag(self, tag): self.tag = tag + def get_path(self): return self.path + def set_path(self, path): self.path = path + def get_fetch_only(self): return self.fetch_only + def set_fetch_only(self, fetch_only): self.fetch_only = fetch_only + 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.use_with is not None and 'use_with' not in already_processed: + already_processed.add('use_with') + outfile.write(' use_with=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.use_with), input_name='use_with')), )) + 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.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.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_('use_with', node) + if value is not None and 'use_with' not in already_processed: + already_processed.add('use_with') + self.use_with = value + self.use_with = ' '.join(self.use_with.split()) + 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_('path', node) + if value is not None and 'path' not in already_processed: + already_processed.add('path') + self.path = 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,6 +9848,7 @@ def main(): "bootloadersettings", "collectionModule", "configoption", + "container", "containerconfig", "description", "dracut", @@ -9633,6 +9883,7 @@ def main(): "product", "profile", "profiles", + "registry", "repository", "requires", "shimoption", diff --git a/kiwi/xml_state.py b/kiwi/xml_state.py index 0371d0e94c..2c6451e3f2 100644 --- a/kiwi/xml_state.py +++ b/kiwi/xml_state.py @@ -85,6 +85,14 @@ class FileT(NamedTuple): permissions: str +class ContainerT(NamedTuple): + name: 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 +469,20 @@ def repository_matches_host_architecture(self, repository: Any) -> bool: """ return self._section_matches_host_architecture(repository) + def registry_matches_host_architecture(self, registry: Any) -> bool: + """ + Tests if the given registry 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(registry) + def get_package_sections( self, packages_sections: List ) -> List[package_type]: @@ -1711,6 +1733,46 @@ def get_partitions(self) -> Dict[str, ptable_entry_type]: ) return partitions + def get_containers(self) -> List[ContainerT]: + containers = [] + for registry_section in self.get_registry_sections(): + for container in registry_section.get_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( + registry_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 = container.get_use_with() or '' + if container_backend in ['podman', 'docker']: + fetch_command = [ + '/usr/bin/skopeo', 'copy', + f'docker://{container_endpoint}', + f'oci-archive:{container_file_name}:{container.name}' + ] + 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}', + 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 +2076,21 @@ def get_repository_sections(self) -> List: repository_list.append(repository) return repository_list + def get_registry_sections(self) -> List: + """ + List of all registry sections for the selected profiles that + matches the host architecture + + :return: section reference(s) + + :rtype: list + """ + registry_list = [] + for registry in self._profiled(self.xml_data.get_registry()): + if self.registry_matches_host_architecture(registry): + registry_list.append(registry) + return registry_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..1149c87762 100644 --- a/test/data/example_config.xml +++ b/test/data/example_config.xml @@ -182,6 +182,13 @@ + + + + + + + diff --git a/test/unit/system/setup_test.py b/test/unit/system/setup_test.py index ab247f69a6..b25b5f3690 100644 --- a/test/unit/system/setup_test.py +++ b/test/unit/system/setup_test.py @@ -1685,3 +1685,45 @@ 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:rmtserver' + ] + ), + call( + [ + 'chroot', 'root_dir', 'systemctl', 'enable', + 'rmtserver_latest' + ] + ), + call( + [ + 'chroot', 'root_dir', '/usr/bin/skopeo', 'copy', + 'docker://registry.suse.com/some:latest', + 'oci-archive:/var/tmp/kiwi_containers/some_latest:some' + ] + ), + call( + [ + 'chroot', 'root_dir', 'systemctl', 'enable', 'some_latest' + ] + ), + call( + [ + 'chroot', 'root_dir', '/usr/bin/skopeo', 'copy', + 'docker://docker.io/foo:latest', + 'oci-archive:/var/tmp/kiwi_containers/foo_latest:foo' + ] + ) + ] 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..cc55eca236 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,50 @@ def test_get_partitions(self): ) } + def test_get_containers(self): + assert self.state.get_containers() == [ + ContainerT( + name='rmtserver_latest', + 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:rmtserver' + ], + load_command=[ + '/usr/bin/podman', 'load', '-i', + '/var/tmp/kiwi_containers/rmtserver_latest' + ] + ), + ContainerT( + name='some_latest', + 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:some' + ], + load_command=[ + '/usr/bin/docker', 'load', '-i', + '/var/tmp/kiwi_containers/some_latest' + ] + ), + ContainerT( + name='foo_latest', + 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:foo' + ], + load_command=[] + ) + ] + def test_get_volumes_custom_root_volume_name(self): description = XMLDescription( '../data/example_lvm_custom_rootvol_config.xml'