From be5619cc9567b22850b06bd5b758b499ff13ede3 Mon Sep 17 00:00:00 2001 From: David Cassany Date: Wed, 8 Jan 2025 14:16:07 +0100 Subject: [PATCH] Adding support for a new Stackbuild specific schema Signed-off-by: David Cassany --- Makefile | 8 + kiwi_stackbuild_plugin/defaults.py | 30 ++ kiwi_stackbuild_plugin/exceptions.py | 7 + kiwi_stackbuild_plugin/kiwi-fake-schema.rnc | 9 + kiwi_stackbuild_plugin/kiwi-fake-schema.rng | 16 + kiwi_stackbuild_plugin/schema.rnc | 56 +++ kiwi_stackbuild_plugin/schema.rng | 98 ++++++ .../tasks/system_stackbuild.py | 79 +++-- kiwi_stackbuild_plugin/tasks/system_stash.py | 4 +- kiwi_stackbuild_plugin/xml_merge.py | 333 ++++++++++++++++++ .../kiwi-description/alternate-config.kiwi | 29 ++ test/data/kiwi-description/config.xml | 32 ++ test/data/stackbuild-description/config.kiwi | 40 +++ test/unit/tasks/system_stackbuild_test.py | 79 ++++- test/unit/xml_merge_test.py | 59 ++++ 15 files changed, 842 insertions(+), 37 deletions(-) create mode 100644 kiwi_stackbuild_plugin/kiwi-fake-schema.rnc create mode 100644 kiwi_stackbuild_plugin/kiwi-fake-schema.rng create mode 100644 kiwi_stackbuild_plugin/schema.rnc create mode 100644 kiwi_stackbuild_plugin/schema.rng create mode 100644 kiwi_stackbuild_plugin/xml_merge.py create mode 100644 test/data/kiwi-description/alternate-config.kiwi create mode 100644 test/data/kiwi-description/config.xml create mode 100644 test/data/stackbuild-description/config.kiwi create mode 100644 test/unit/xml_merge_test.py diff --git a/Makefile b/Makefile index 19ca58a..5e91d9f 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,14 @@ install: install -m 644 README.rst \ ${buildroot}${docdir}/python-kiwi_stackbuild_plugin/README +kiwi_stackbuild_plugin/schema.rng: kiwi_stackbuild_plugin/schema.rnc + # whenever the schema is changed this target will convert + # the short form of the RelaxNG schema to the format used + # in code and auto generates the python data structures + @type -p trang &>/dev/null || \ + (echo "ERROR: trang not found in path: $(PATH)"; exit 1) + trang -I rnc -O rng kiwi_stackbuild_plugin/schema.rnc kiwi_stackbuild_plugin/schema.rng + build: clean tox # create setup.py variant for rpm build. # delete module versions from setup.py for building an rpm diff --git a/kiwi_stackbuild_plugin/defaults.py b/kiwi_stackbuild_plugin/defaults.py index bdd8f7b..de3e608 100644 --- a/kiwi_stackbuild_plugin/defaults.py +++ b/kiwi_stackbuild_plugin/defaults.py @@ -16,6 +16,8 @@ # along with kiwi-stackbuild. If not, see # import re +import importlib +from importlib.resources import as_file from kiwi.defaults import Defaults from typing import ( @@ -95,3 +97,31 @@ def get_container_config( 'author': maintainer } } + + @staticmethod + def project_file(filename): + """ + Provides the python module base directory search path + + The method uses the importlib.resources.path method to identify + files and directories from the application + + :param string filename: relative project file + + :return: absolute file path name + + :rtype: str + """ + with as_file(importlib.resources.files('kiwi_stackbuild_plugin')) as path: + return f'{path}/{filename}' + + @staticmethod + def get_schema_file(): + """ + Provides file path to kiwi RelaxNG schema + + :return: file path + + :rtype: str + """ + return StackBuildDefaults.project_file('schema.rng') diff --git a/kiwi_stackbuild_plugin/exceptions.py b/kiwi_stackbuild_plugin/exceptions.py index 10db50c..3435695 100644 --- a/kiwi_stackbuild_plugin/exceptions.py +++ b/kiwi_stackbuild_plugin/exceptions.py @@ -41,3 +41,10 @@ class KiwiStackBuildPluginRootSyncFailed(KiwiError): Exception raised if the rsync process to sync the stash into the root-tree failed """ + + +class KiwiStackBuildPluginSchemaValidationFailed(KiwiError): + """ + Exception raised if the provided XML description is not compliant with + the stack build image schema + """ diff --git a/kiwi_stackbuild_plugin/kiwi-fake-schema.rnc b/kiwi_stackbuild_plugin/kiwi-fake-schema.rnc new file mode 100644 index 0000000..2a60ed4 --- /dev/null +++ b/kiwi_stackbuild_plugin/kiwi-fake-schema.rnc @@ -0,0 +1,9 @@ +#========================================== +# Fake rnc file used only to convert +# stackbuild schema.rnc file to schema.rng +# without requiring the actual KIWI schema +# file +# +start = + ## The start pattern of an image + k.image diff --git a/kiwi_stackbuild_plugin/kiwi-fake-schema.rng b/kiwi_stackbuild_plugin/kiwi-fake-schema.rng new file mode 100644 index 0000000..e336b14 --- /dev/null +++ b/kiwi_stackbuild_plugin/kiwi-fake-schema.rng @@ -0,0 +1,16 @@ + + + + + + The start pattern of an image + + + diff --git a/kiwi_stackbuild_plugin/schema.rnc b/kiwi_stackbuild_plugin/schema.rnc new file mode 100644 index 0000000..6955a6f --- /dev/null +++ b/kiwi_stackbuild_plugin/schema.rnc @@ -0,0 +1,56 @@ +#================ +# FILE : schema.rnc +#**************** +# PROJECT : KIWI - Stack Build Plugin +# COPYRIGHT : (c) 2021 SUSE LINUX Products GmbH +# : +# AUTHOR : David Cassany +# : +# BELONGS TO : Operating System images +# : +# DESCRIPTION : This is the RELAX NG Schema for KIWI Rebuild Images +# : plugin configuration files. The schema is maintained +# : in the relax compact syntax. Any changes should +# : made in !! *** schema.rnc *** !! +# : +# : +# STATUS : Development +#**************** + +namespace rng = "http://relaxng.org/ns/structure/1.0" + +# The real include value is computed and replaced in memory at runtime +# to match the actual KIWI schema of the KIWI module being loaded +include "kiwi-fake-schema.rnc" { + k.image.schemaversion.attribute = + ## The allowed Schema version (fixed value) + attribute schemaversion { "0.1" } + + k.image.attlist = k.image.name.attribute + & k.image.stackbuild.attribute + & k.image.displayname.attribute? + & k.image.id? + & k.image.schemaversion.attribute + & ( k.image.noNamespaceSchemaLocation.attribute? + | k.image.schemaLocation.attribute? )? + + k.image = + ## The root element of the configuration file + element image { + k.image.attlist & + k.description? & + k.preferences* & + k.profiles? & + k.users* & + k.drivers* & + k.strip* & + k.repository* & + k.packages* + } +} + +div{ + k.image.stackbuild.attribute = + ## Identifies description as a stackbuild XML + attribute stackbuild { "true" } +} diff --git a/kiwi_stackbuild_plugin/schema.rng b/kiwi_stackbuild_plugin/schema.rng new file mode 100644 index 0000000..d673451 --- /dev/null +++ b/kiwi_stackbuild_plugin/schema.rng @@ -0,0 +1,98 @@ + + + + + + + + The allowed Schema version (fixed value) + 0.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + The root element of the configuration file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + Identifies description as a stackbuild XML + true + + +
+
diff --git a/kiwi_stackbuild_plugin/tasks/system_stackbuild.py b/kiwi_stackbuild_plugin/tasks/system_stackbuild.py index ec3c7bc..82417ac 100644 --- a/kiwi_stackbuild_plugin/tasks/system_stackbuild.py +++ b/kiwi_stackbuild_plugin/tasks/system_stackbuild.py @@ -65,6 +65,7 @@ import logging from mock import patch from docopt import docopt +from tempfile import TemporaryDirectory from typing import ( Dict, List ) @@ -82,9 +83,10 @@ from kiwi.utils.sync import DataSync from kiwi.defaults import Defaults +from kiwi_stackbuild_plugin.xml_merge import XMLMerge from kiwi_stackbuild_plugin.exceptions import ( KiwiStackBuildPluginTargetDirExists, - KiwiStackBuildPluginRootSyncFailed + KiwiStackBuildPluginRootSyncFailed, ) log = logging.getLogger('kiwi') @@ -150,34 +152,57 @@ def process(self) -> None: ) if self.command_args.get('--description'): - with patch.object( - sys, 'argv', self._validate_kiwi_build_command( - [ - 'system', 'build', - '--description', self.command_args['--description'], - '--target-dir', self.command_args['--target-dir'], - '--allow-existing-root' - ] - ) - ): - kiwi_task = SystemBuildTask( - should_perform_task_setup=False - ) + merger = XMLMerge(self.command_args['--description']) + if merger.is_stackbuild_description(): + merger.validate_schema() + with TemporaryDirectory( + prefix='kiwi_description.' + ) as temp_desc: + merger.merge_description( + f'{image_root_dir}/image', temp_desc + ) + self._kiwi_build_task( + temp_desc, self.command_args['--target-dir'] + ).process() + + else: + self._kiwi_build_task( + self.command_args['--description'], + self.command_args['--target-dir'] + ).process() else: - with patch.object( - sys, 'argv', self._validate_kiwi_create_command( - [ - 'system', 'create', - '--root', image_root_dir, - '--target-dir', self.command_args['--target-dir'] - ] - ) - ): - kiwi_task = SystemCreateTask( - should_perform_task_setup=False - ) + self._kiwi_create_task( + image_root_dir, self.command_args['--target-dir'] + ).process() - kiwi_task.process() + def _kiwi_build_task(self, description: str, target_dir: str) -> SystemBuildTask: + with patch.object( + sys, 'argv', self._validate_kiwi_build_command( + [ + 'system', 'build', + '--description', description, + '--target-dir', target_dir, + '--allow-existing-root' + ] + ) + ): + return SystemBuildTask( + should_perform_task_setup=False + ) + + def _kiwi_create_task(self, root_dir: str, target_dir: str) -> SystemCreateTask: + with patch.object( + sys, 'argv', self._validate_kiwi_create_command( + [ + 'system', 'create', + '--root', root_dir, + '--target-dir', target_dir + ] + ) + ): + return SystemCreateTask( + should_perform_task_setup=False + ) def _validate_kiwi_create_command( self, kiwi_create_command: List[str] diff --git a/kiwi_stackbuild_plugin/tasks/system_stash.py b/kiwi_stackbuild_plugin/tasks/system_stash.py index 122de20..763265d 100644 --- a/kiwi_stackbuild_plugin/tasks/system_stash.py +++ b/kiwi_stackbuild_plugin/tasks/system_stash.py @@ -89,7 +89,9 @@ def process(self) -> None: ) description = XMLDescription(kiwi_description) xml_state = XMLState( - xml_data=description.load() + description.load(), + self.global_args['--profile'], + self.global_args['--type'] ) contact_info = xml_state.get_description_section() image_name = self.command_args['--container-name'] or \ diff --git a/kiwi_stackbuild_plugin/xml_merge.py b/kiwi_stackbuild_plugin/xml_merge.py new file mode 100644 index 0000000..0d82df2 --- /dev/null +++ b/kiwi_stackbuild_plugin/xml_merge.py @@ -0,0 +1,333 @@ +# Copyright (c) 2021 SUSE Linux GmbH. All rights reserved. +# +# This file is part of kiwi-stackbuild. +# +# kiwi is free software: you can redistribute it and/or modify +# it under the terms owf 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 +# +from lxml import etree +import os +import glob +import logging + +from kiwi.utils.sync import DataSync +from kiwi.defaults import Defaults +from kiwi.exceptions import ( + KiwiConfigFileNotFound +) + +from kiwi_stackbuild_plugin.defaults import StackBuildDefaults +from kiwi_stackbuild_plugin.exceptions import ( + KiwiStackBuildPluginSchemaValidationFailed +) + +log = logging.getLogger('kiwi') + + +class XMLMerge: + """ + ***Implements the class to handle stackbuild schema and descriptions. + Converts a given stackbuild description into a full KIWI description + based on the stashed KIWI description*** + + :param str description_dir: the folder path contianing the stackbuild + description XML + """ + def __init__(self, description_dir: str): + self.description = self._find_description_file(description_dir) + self.description_dir = description_dir + self.description_tree = etree.parse(self.description) + + def is_stackbuild_description(self) -> bool: + """ + Returns True if the root element includes the 'stackbuild="true"' + attribute + + :returns: True if the stackbuild attribute is found, False otherwise + + :rtype: bool + """ + root = self.description_tree.getroot() + if 'stackbuild' in root.attrib and root.attrib['stackbuild'] == 'true': + return True + return False + + def validate_schema(self) -> None: + """ + Runs the stackbuild schema validation, raises a + 'KiwiStackBuildPluginSchemaValidationFailed' exception + if the validation fails + """ + schema_file = StackBuildDefaults.get_schema_file() + log.info('Loading stack build schema') + schema_tree = etree.parse(schema_file) + root = schema_tree.getroot() + kiwi_schema = Defaults.get_schema_file() + rngns = root.nsmap['rng'] + for child in root.iter(): + if child.tag == f'{{{rngns}}}include': + log.debug(f'Setting stack build schema to include kiwi schema: {kiwi_schema}') + child.set('href', kiwi_schema) + break + + relaxng = etree.RelaxNG(schema_tree) + validation_rng = relaxng.validate(self.description_tree) + + if not validation_rng: + raise KiwiStackBuildPluginSchemaValidationFailed(f'Failed to validate schema: {relaxng.error_log}') + + def merge_description(self, derived_from_dir: str, target_dir: str) -> None: + """ + Merges a stackbuild description with the KIWI description found + in the given path. The original description is copied to the target + directory and then the stackbuild description is applied on top. + + :param str derived_from_dir: path of the KIWI description to update + with the stackbuild description + :param str target_dir: path where the merged description is stored + """ + sync = DataSync(os.path.join(derived_from_dir, ''), target_dir) + sync.sync_data(options=Defaults.get_sync_options()) + + derived_from = self._find_description_file(target_dir) + work_tree = etree.parse(derived_from) + + self._image_merge(work_tree) + self._description_replace(work_tree) + self._preferences_merge(work_tree) + self._profiles_merge(work_tree) + self._users_merge(work_tree) + self._drop_in_sections(work_tree) + + sync = DataSync(f'{self.description_dir}/', target_dir) + sync.sync_data(options=Defaults.get_sync_options()) + + work_tree.write(derived_from, pretty_print=True) + + def _description_replace(self, work_tree): + """ + Replaces the description section of the original KIWI description + with the one provided by the stackbuild description if any + + :param work_tree: parsed etree of the original KIWI description + to modify + """ + self._replace_unique_element_by_xpath( + work_tree, '/image/description[@type="system"]' + ) + + def _drop_in_sections(self, work_tree): + """ + Adds all sections in stackbuild description to the given etree + except for 'description', 'preferences' and 'profiles' which require + an specific merge logic + + :param work_tree: parsed etree of the original KIWI description + to modify + """ + root = self.description_tree.getroot() + w_root = work_tree.getroot() + exclude_list = ['description', 'preferences', 'profiles'] + for item in root: + if item.tag not in exclude_list: + w_root.append(item) + + def _replace_unique_element_by_xpath(self, work_tree, xpath): + """ + Replaces an element found in the current stackbuild etree to the + given etree. The element is refrenced and located with a given xpath + + :param work_tree: parsed etree of the original KIWI description + to modify + :param str xpath: the xpath to identify and locate the element to replace + + :return: true or false + + :rtype: bool + """ + replacement = self.description_tree.xpath(xpath) + if len(replacement): + obsolete = work_tree.xpath(xpath) + if len(obsolete): + parent = obsolete[0].getparent() + parent.replace(obsolete[0], replacement[0]) + return True + return False + + def _profiles_merge(self, work_tree): + """ + Appends or replaces the profiles included within the stackbuild description + to the given working elementTree based on the original KIWI description + + :param work_tree: parsed etree of the original KIWI description + to modify + """ + profiles = self.description_tree.xpath('/image/profiles') + if len(profiles): + w_profiles = work_tree.xpath('/image/profiles') + if len(w_profiles): + for profile in profiles[0]: + name = profile.attrib['name'] + if not self._replace_unique_element_by_xpath( + work_tree, + f'/image/profiles/profile[@name=\'{name}\']' + ): + w_profiles[0].append(profile) + else: + work_tree.getroot().append(profiles[0]) + + def _preferences_merge(self, work_tree): + """ + Combines the preferences available in the stackbuild description + with the originial KIWI description. Stackbuild preferences are simply + appended or combined to the original depending on attribures set + of each preferences element. If a stackbuild preferences element includes + the same set of attributes of another prefences element present in the + original description they are combined. Otherwise the stackbuild + element is simply appended to the original KIWI description. + + :param work_tree: parsed etree of the original KIWI description + to modify + """ + preferences = self.description_tree.xpath('/image/preferences') + for preferences_set in preferences: + pref = self._element_with_attributes_exists( + work_tree, '/image/preferences', preferences_set.attrib + ) + if pref is None: + work_tree.getroot().append(preferences_set) + else: + self._merge_preferences_set(preferences_set, pref) + + def _element_with_attributes_exists(self, tree, e_path, attributes): + """ + Check if it exists an elemental in the given tree matches the given + path and attributes. Returns the first matching element or None if + there is no match. + + :param tree: etree to evaluate + :param str e_path: the path used to match elements + :param attributes: the set of attributes to match + + :return: The first etree element matching with the given path + and attributes. Returns None if no match + """ + attr_list = [f'@{k}=\'{v}\'' for k, v in attributes.items()] + constraints = '[not(@*)]' + if attr_list: + constraints = '[' + ' and '.join(attr_list) + ']' + item = tree.xpath(e_path + constraints) + if len(item): + return item[0] + return None + + def _merge_preferences_set(self, pref_setA, pref_setB): + """ + Combines the two given preferences sets. A is applied over + B. Any child element in A that is missing B is appended to B. Any + child element in A that is already existing in B is replace in B + with the contents of A. The rule has a couple of exceptions: + + * 'type' elements are only replaced if they also match the image type + * 'showlicense' elements are only appended, they can't be replaced + + :param pref_setA: a preferences element tree object + :param pref_setB: a preferences element tree object + """ + for child in pref_setA: + if child.tag == 'showlicense': + pref_setB.append(child) + continue + if child.tag == 'type': + b_type = self._element_with_attributes_exists( + pref_setB, './type', {'image': child.attrib['image']} + ) + if b_type is None: + pref_setB.append(child) + else: + pref_setB.replace(b_type, child) + continue + b_child = pref_setB.xpath(f'./{child.tag}') + if len(b_child): + pref_setB.replace(b_child[0], child) + else: + pref_setB.append(child) + + def _find_description_file(self, description_directory): + """ + Finds a description XML file in given directory + + :param str description_directory: the directory path to evaluate + + :return: the found XML description file + + :rtype: str + """ + config_file = description_directory + '/config.xml' + log.debug(f'looking for XML description file {config_file}') + if os.path.exists(config_file): + return config_file + + log.debug(f'{config_file} not found...') + glob_match = description_directory + '/*.kiwi' + log.debug(f'looking for XML description file {glob_match}') + for config_file in sorted(glob.iglob(glob_match)): + return config_file + + raise KiwiConfigFileNotFound( + 'no XML description found in %s' % description_directory + ) + + def _image_merge(self, work_tree): + """ + Replaces or adds the stackbuild image attributes to the + original KIWI description image root element. + + :param work_tree: parsed etree of the original KIWI description + to modify + """ + img = self.description_tree.getroot() + w_root = work_tree.getroot() + if 'displayname' in img.attrib: + w_root.attrib['displayname'] = img.attrib['displayname'] + if 'id' in img.attrib: + w_root.attrib['id'] = img.attrib['id'] + if 'name' in img.attrib: + w_root.attrib['name'] = img.attrib['name'] + + def _users_merge(self, work_tree): + """ + Appends or replaces users from the stackbuild description to the + original KIWI description. The criteria to replace users is based on + users name. If a user name defined in stackbuild description already + exists in the KIWI description this gets replaced. + + :param work_tree: parsed etree of the original KIWI description + to modify + """ + users = self.description_tree.xpath('/image/users') + for users_sec in users: + usrs_match = self._element_with_attributes_exists( + work_tree, '/image/users', users_sec.attrib + ) + if usrs_match is None: + work_tree.getroot().append(users_sec) + else: + for user in users_sec: + usr_match = self._element_with_attributes_exists( + usrs_match, './user', {'name': user.attrib['name']} + ) + if usr_match is None: + usrs_match.append(user) + else: + usrs_match.replace(usr_match, user) diff --git a/test/data/kiwi-description/alternate-config.kiwi b/test/data/kiwi-description/alternate-config.kiwi new file mode 100644 index 0000000..28772f0 --- /dev/null +++ b/test/data/kiwi-description/alternate-config.kiwi @@ -0,0 +1,29 @@ + + + + Marcus Schaefer + ms@suse.com + Some Image + + + us + en_US + zypper + Europe/Berlin + 1.99.1 + + + + + + + + + + + + + + + + diff --git a/test/data/kiwi-description/config.xml b/test/data/kiwi-description/config.xml new file mode 100644 index 0000000..0c614ce --- /dev/null +++ b/test/data/kiwi-description/config.xml @@ -0,0 +1,32 @@ + + + + Marcus Schaefer + ms@suse.com + Some Image + + + + + + us + en_US + zypper + Europe/Berlin + 1.99.1 + + + + + + + + + + + + + + + + diff --git a/test/data/stackbuild-description/config.kiwi b/test/data/stackbuild-description/config.kiwi new file mode 100644 index 0000000..05926e0 --- /dev/null +++ b/test/data/stackbuild-description/config.kiwi @@ -0,0 +1,40 @@ + + + + David Cassany + dcassany@suse.com + Some variation of an stashed image + + + + + + us + en_US + zypper + Europe/Berlin + 1.99.1 + + + + us + en_US + zypper + Europe/Berlin + 1.99.1 + + + MY_LICENSE + 1.1 + + + + + + + + + + + + diff --git a/test/unit/tasks/system_stackbuild_test.py b/test/unit/tasks/system_stackbuild_test.py index 4179bc5..810f3a9 100644 --- a/test/unit/tasks/system_stackbuild_test.py +++ b/test/unit/tasks/system_stackbuild_test.py @@ -1,5 +1,7 @@ import sys +import shutil from pytest import raises +from tempfile import mkdtemp from mock import ( Mock, patch, call ) @@ -20,10 +22,14 @@ def setup(self): '--signing-key', 'some-key' ] self.task = SystemStackbuildTask() + self.target_dir = mkdtemp(prefix='kiwi_test_target.') def setup_method(self, cls): self.setup() + def teardown_method(self, cls): + shutil.rmtree(self.target_dir) + def _init_command_args(self): self.task.command_args = {} self.task.command_args['help'] = False @@ -143,20 +149,18 @@ def test_process_rebuild( @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.Path.create') @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.Command.run') @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.SystemBuildTask') - @patch('os.path.exists') @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.patch.object') - def test_process_new_build( - self, mock_patch_object, mock_os_path_exists, + def test_process_new_kiwi_build( + self, mock_patch_object, mock_SystemBuildTask, mock_Command_run, mock_Path_create, mock_Privileges ): self._init_command_args() self.task.command_args['stackbuild'] = True self.task.command_args['--stash'] = ['name'] - self.task.command_args['--target-dir'] = '/some/target-dir' - self.task.command_args['--description'] = '/path/to/kiwi/description' + self.task.command_args['--target-dir'] = self.target_dir + self.task.command_args['--description'] = '../data/kiwi-description' self.task.command_args['--from-registry'] = 'registry.uri' - mock_os_path_exists.return_value = False mock_Command_run.return_value.output = '/podman/mount/path' kiwi_task = Mock() mock_SystemBuildTask.return_value = kiwi_task @@ -169,7 +173,7 @@ def test_process_new_build( 'rsync', '--archive', '--hard-links', '--xattrs', '--acls', '--one-file-system', '--inplace', '/podman/mount/path/', - '/some/target-dir/build/image-root' + f'{self.target_dir}/build/image-root' ] ), call( @@ -186,8 +190,65 @@ def test_process_new_build( 'kiwi-ng', '--type', 'iso', '--profile', 'a', '--profile', 'b', 'system', 'build', - '--description', '/path/to/kiwi/description', - '--target-dir', '/some/target-dir', + '--description', '../data/kiwi-description', + '--target-dir', self.target_dir, + '--allow-existing-root', '--signing-key', 'some-key' + ] + ) + + @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.TemporaryDirectory') + @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.XMLMerge') + @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.Privileges') + @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.Path.create') + @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.Command.run') + @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.SystemBuildTask') + @patch('kiwi_stackbuild_plugin.tasks.system_stackbuild.patch.object') + def test_process_new_stackbuild_build( + self, mock_patch_object, mock_SystemBuildTask, + mock_Command_run, mock_Path_create, mock_Privileges, + mock_XMLMerge, mock_TemporaryDirectory + ): + self._init_command_args() + self.task.command_args['stackbuild'] = True + self.task.command_args['--stash'] = ['name'] + self.task.command_args['--target-dir'] = self.target_dir + self.task.command_args['--description'] = '../data/stackbuild-description' + self.task.command_args['--from-registry'] = 'registry.uri' + mock_Command_run.return_value.output = '/podman/mount/path' + stackbuild_check = Mock() + stackbuild_check.return_value = True + mock_XMLMerge.is_stackbuild_description = stackbuild_check + mock_TemporaryDirectory.return_value.__enter__.return_value = '/temporary/merged/description' + kiwi_task = Mock() + mock_SystemBuildTask.return_value = kiwi_task + self.task.process() + assert mock_Command_run.call_args_list == [ + call(['podman', 'pull', 'registry.uri/name']), + call(['podman', 'image', 'mount', 'name']), + call( + [ + 'rsync', '--archive', '--hard-links', '--xattrs', + '--acls', '--one-file-system', '--inplace', + '/podman/mount/path/', + f'{self.target_dir}/build/image-root' + ] + ), + call( + ['podman', 'image', 'umount', '--force', 'name'], + raise_on_error=False + ) + ] + mock_SystemBuildTask.assert_called_once_with( + should_perform_task_setup=False + ) + kiwi_task.process.assert_called_once_with() + mock_patch_object.assert_called_once_with( + sys, 'argv', [ + 'kiwi-ng', '--type', 'iso', + '--profile', 'a', '--profile', 'b', + 'system', 'build', + '--description', '/temporary/merged/description', + '--target-dir', self.target_dir, '--allow-existing-root', '--signing-key', 'some-key' ] ) diff --git a/test/unit/xml_merge_test.py b/test/unit/xml_merge_test.py new file mode 100644 index 0000000..a84faeb --- /dev/null +++ b/test/unit/xml_merge_test.py @@ -0,0 +1,59 @@ +import shutil +from pytest import raises +from tempfile import mkdtemp +from mock import patch + +from kiwi.exceptions import ( + KiwiConfigFileNotFound +) + +from kiwi_stackbuild_plugin.xml_merge import XMLMerge +from kiwi_stackbuild_plugin.exceptions import ( + KiwiStackBuildPluginSchemaValidationFailed +) + + +class TestXMLMerge: + def setup(self): + self.target_dir = mkdtemp(prefix='kiwi_desc_target.') + + def setup_method(self, cls): + self.setup() + + def teardown_method(self, cls): + shutil.rmtree(self.target_dir) + + def test_XMLMerge_no_description(self): + with raises(KiwiConfigFileNotFound): + XMLMerge(self.target_dir) + + def test_is_stackbuild_description(self): + xml_merge = XMLMerge('../data/stackbuild-description') + assert xml_merge.is_stackbuild_description() + + def test_is_not_stackbuild_description(self): + xml_merge = XMLMerge('../data/kiwi-description') + assert not xml_merge.is_stackbuild_description() + + def test_validate_schema(self): + xml_merge = XMLMerge('../data/stackbuild-description') + xml_merge.validate_schema() + + def test_validate_schema_fails(self): + xml_merge = XMLMerge('../data/kiwi-description') + with raises(KiwiStackBuildPluginSchemaValidationFailed): + xml_merge.validate_schema() + + @patch('kiwi_stackbuild_plugin.xml_merge.DataSync') + def test_merge_description(self, mock_DataSync): + shutil.copy('../data/kiwi-description/config.xml', self.target_dir) + xml_merge = XMLMerge('../data/stackbuild-description') + xml_merge.validate_schema() + xml_merge.merge_description('../data/kiwi-description', self.target_dir) + + @patch('kiwi_stackbuild_plugin.xml_merge.DataSync') + def test_merge_description_without_profiles(self, mock_DataSync): + shutil.copy('../data/kiwi-description/alternate-config.kiwi', self.target_dir) + xml_merge = XMLMerge('../data/stackbuild-description') + xml_merge.validate_schema() + xml_merge.merge_description('../data/kiwi-description', self.target_dir)