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)