From de330f53d6d9e05058c6157190a1c6f6cefb7536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?= Date: Fri, 18 Oct 2024 11:47:56 +0200 Subject: [PATCH] final schema update --- doc/source/image_description/elements.rst | 55 ++---- kiwi/schema/kiwi.rnc | 42 ++--- kiwi/schema/kiwi.rng | 67 +++---- kiwi/xml_parse.py | 210 ++++++++-------------- kiwi/xml_state.py | 52 ++++-- test/data/example_config.xml | 22 +-- 6 files changed, 180 insertions(+), 268 deletions(-) diff --git a/doc/source/image_description/elements.rst b/doc/source/image_description/elements.rst index 0270d55160..7c9868c495 100644 --- a/doc/source/image_description/elements.rst +++ b/doc/source/image_description/elements.rst @@ -1235,64 +1235,47 @@ For details see: :ref:`installmedia_customize` .. _sec.registry: - ----------- + +------------ -Setup containers to fetch from one or more registry elements +Setup containers to fetch from a registry assigned to one +of the supported container backends .. code:: xml - - - - - + + + -The optional registry element specifies the location of one ore +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 gets deleted. - ----------------------- - -Details about the container backend - -.. code:: xml - - - - - - - Supported `backend` values as of today are `docker` and `podman`. The `backend` attribute is mandatory and specifies for which container backend the image should be available in the system. -The `container` element has the following optional attribute: -path="some/path" - The path to the container in the registry. If not specified - the value defaults to `/` - - ---------------------------------- + +----------------------- -Details about the container to fetch from a given source registry +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 +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 diff --git a/kiwi/schema/kiwi.rnc b/kiwi/schema/kiwi.rnc index afb146a8ea..8224862a7f 100644 --- a/kiwi/schema/kiwi.rnc +++ b/kiwi/schema/kiwi.rnc @@ -90,7 +90,7 @@ div { k.drivers* & k.strip* & k.repository* & - k.registry* & + k.containers* & k.packages* & k.extension? } @@ -1049,42 +1049,26 @@ div { } #========================================== -# common element +# common element # div { - k.registry.profiles.attribute = k.profiles.attribute - k.registry.arch.attribute = k.arch.attribute - k.registry.source.attribute = + 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.registry.attlist = - k.registry.profiles.attribute? & - k.registry.arch.attribute? & - k.registry.source.attribute - k.registry = - element registry { - k.registry.attlist, - k.containers+ - } -} - -#========================================== -# common element -# -div { k.containers.backend.attribute = ## Use container with specified container backend attribute backend { "podman" | "docker" } - k.containers.path.attribute = - ## Container path, default to '/' if not specified - attribute path { text } k.containers.attlist = - k.containers.backend.attribute & - k.containers.path.attribute? + 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* + k.container+ } } @@ -1092,9 +1076,13 @@ div { # 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 } @@ -1104,6 +1092,8 @@ div { 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 = diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng index ce1b9608df..89f9d2421d 100644 --- a/kiwi/schema/kiwi.rng +++ b/kiwi/schema/kiwi.rng @@ -209,7 +209,7 @@ named /etc/ImageID - + @@ -1606,47 +1606,21 @@ definition can be composed by other existing profiles.
- + - + - + Name of registry source server - - - - - - - - - - - - - - - - - - - -
- -
Use container with specified container backend @@ -1656,25 +1630,24 @@ definition can be composed by other existing profiles. - - - Container path, default to '/' if not specified - - - - + + + + + + - + - +
@@ -1684,11 +1657,19 @@ definition can be composed by other existing profiles. -->
+ + + Container name + + + Container path, default to '/' if not specified + + Container tag, defaults to 'latest' if not specified @@ -1704,6 +1685,12 @@ loading of the container at first boot + + + + + + diff --git a/kiwi/xml_parse.py b/kiwi/xml_parse.py index 86dc23fbb6..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, registry=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,10 +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 = [] + if containers is None: + self.containers = [] else: - self.registry = registry + self.containers = containers if packages is None: self.packages = [] else: @@ -915,11 +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_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) @@ -959,7 +959,7 @@ def hasContent_(self): self.drivers or self.strip or self.repository or - self.registry or + self.containers or self.packages or self.extension ): @@ -1027,8 +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 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: @@ -1109,11 +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() + elif nodeName_ == 'containers': + obj_ = containers.factory() obj_.build(child_) - self.registry.append(obj_) - obj_.original_tagname_ = 'registry' + self.containers.append(obj_) + obj_.original_tagname_ = 'containers' elif nodeName_ == 'packages': obj_ = packages.factory() obj_.build(child_) @@ -2455,40 +2455,43 @@ def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): # end class requires -class registry(GeneratedsSuper): +class containers(GeneratedsSuper): subclass = None superclass = None - def __init__(self, profiles=None, arch=None, source=None, containers=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) - if containers is None: - self.containers = [] + self.backend = _cast(None, backend) + if container is None: + self.container = [] else: - self.containers = containers + self.container = container def factory(*args_, **kwargs_): if CurrentSubclassModule_ is not None: subclass = getSubclassFromModule_( - CurrentSubclassModule_, registry) + CurrentSubclassModule_, containers) if subclass is not None: return subclass(*args_, **kwargs_) - if registry.subclass: - return registry.subclass(*args_, **kwargs_) + if containers.subclass: + return containers.subclass(*args_, **kwargs_) else: - return registry(*args_, **kwargs_) + return containers(*args_, **kwargs_) factory = staticmethod(factory) - 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_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_: @@ -2498,13 +2501,13 @@ def validate_arch_name(self, value): validate_arch_name_patterns_ = [['^.*$']] def hasContent_(self): if ( - self.containers + 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') + 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: @@ -2516,15 +2519,15 @@ def export(self, outfile, level, namespaceprefix_='', name_='registry', namespac 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') + self.exportAttributes(outfile, level, already_processed, namespaceprefix_, name_='containers') if self.hasContent_(): outfile.write('>%s' % (eol_, )) - self.exportChildren(outfile, level + 1, namespaceprefix_='', name_='registry', pretty_print=pretty_print) + 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_='registry'): + 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')), )) @@ -2534,13 +2537,16 @@ def exportAttributes(self, outfile, level, already_processed, namespaceprefix_=' 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 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 containers_ in self.containers: - containers_.export(outfile, level, namespaceprefix_, name_='containers', pretty_print=pretty_print) + 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) @@ -2563,105 +2569,11 @@ def buildAttributes(self, node, attrs, already_processed): 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_ == 'containers': - obj_ = containers.factory() - obj_.build(child_) - self.containers.append(obj_) - obj_.original_tagname_ = 'containers' -# end class registry - - -class containers(GeneratedsSuper): - subclass = None - superclass = None - def __init__(self, backend=None, path=None, container=None): - self.original_tagname_ = None - self.backend = _cast(None, backend) - self.path = _cast(None, path) - 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_backend(self): return self.backend - def set_backend(self, backend): self.backend = backend - def get_path(self): return self.path - def set_path(self, path): self.path = path - 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.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')), )) - 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')), )) - 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_('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()) - value = find_attr_value_('path', node) - if value is not None and 'path' not in already_processed: - already_processed.add('path') - self.path = value def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): if nodeName_ == 'container': obj_ = container.factory() @@ -2674,9 +2586,11 @@ def buildChildren(self, child_, node, nodeName_, fromsubclass_=False): class container(GeneratedsSuper): subclass = None superclass = None - def __init__(self, name=None, tag=None, fetch_only=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_): @@ -2692,10 +2606,21 @@ def factory(*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 ( @@ -2727,6 +2652,12 @@ def exportAttributes(self, outfile, level, already_processed, namespaceprefix_=' 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')), )) @@ -2747,6 +2678,16 @@ def buildAttributes(self, node, attrs, already_processed): 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') @@ -9962,7 +9903,6 @@ def main(): "product", "profile", "profiles", - "registry", "repository", "requires", "shimoption", diff --git a/kiwi/xml_state.py b/kiwi/xml_state.py index 9ac759c3fe..5e5fdd0d64 100644 --- a/kiwi/xml_state.py +++ b/kiwi/xml_state.py @@ -470,9 +470,9 @@ 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: + def containers_matches_host_architecture(self, containers: Any) -> bool: """ - Tests if the given registry section is applicable for the + 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. @@ -482,7 +482,21 @@ def registry_matches_host_architecture(self, registry: Any) -> bool: :rtype: bool """ - return self._section_matches_host_architecture(registry) + 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 @@ -1736,16 +1750,16 @@ def get_partitions(self) -> Dict[str, ptable_entry_type]: def get_containers(self) -> List[ContainerT]: containers = [] - for registry_section in self.get_registry_sections(): - for containers_section in registry_section.get_containers(): - for container in containers_section.get_container(): + 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 = containers_section.get_path() or '' + container_path = container.get_path() or '' container_endpoint = os.path.normpath( '{0}/{1}/{2}:{3}'.format( - registry_section.get_source(), container_path, + containers_section.get_source(), container_path, container.name, container_tag ) ) @@ -1756,8 +1770,10 @@ def get_containers(self) -> List[ContainerT]: if container_backend in ['podman', 'docker']: fetch_command = [ '/usr/bin/skopeo', 'copy', - f'docker://{container_endpoint}', - f'oci-archive:{container_file_name}:{container_endpoint}' + 'docker://{0}'.format(container_endpoint), + 'oci-archive:{0}:{1}'.format( + container_file_name, container_endpoint + ) ] if not container.get_fetch_only(): load_command = [ @@ -2079,20 +2095,20 @@ def get_repository_sections(self) -> List: repository_list.append(repository) return repository_list - def get_registry_sections(self) -> List: + def get_containers_sections(self) -> List: """ - List of all registry sections for the selected profiles that + List of all containers sections for the selected profiles that matches the host architecture - :return: section reference(s) + :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 + 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: """ diff --git a/test/data/example_config.xml b/test/data/example_config.xml index d553fb0c5e..79b2a888b9 100644 --- a/test/data/example_config.xml +++ b/test/data/example_config.xml @@ -182,19 +182,15 @@ - - - - - - - - - - - - - + + + + + + + + +