12 Contributing #
Abstract
This document describes the development process of KIWI NG and how you can be part of it. This description applies to version 10.2.2.
12.1 Using KIWI NG in a Python Project #
Abstract
KIWI NG is provided as a Python module under the kiwi namespace. It is available for the Python 3 version. The following description applies for KIWI NG version 10.2.2.
KIWI NG can also function as a module for other Python projects. The following example demonstrates how to read an existing image description, add a new repository definition, and export the modified description on stdout.
import sys
import logging
from kiwi.xml_description import XMLDescription
from kiwi.xml_state import XMLState
description = XMLDescription('path/to/kiwi/XML/config.xml')
xml_data = description.load()
xml_state = XMLState(
xml_data=xml_data, profiles=[], build_type='iso'
)
xml_state.add_repository(
repo_source='http://repo',
repo_type='rpm-md',
repo_alias='myrepo',
repo_prio=99
)
xml_data.export(
outfile=sys.stdout, level=0
)
Each class in the example is responsible for a single tasks, so they can be reused in other user cases. Therefore it is possible to use KIWI NG beyond the main image building scope, for example to manage setup of loop devices, filesystems, partitions, etc.
This means KIWI NG offers a way to describe a system, but you can choose whether
you want to use the KIWI NG description format or not. The following example
shows how to use KIWI NG to create a simple filesystem image which contains your
host tmp
directory.
import logging
from kiwi.storage.loop_device import LoopDevice
from kiwi.filesystem import FileSystem
with LoopDevice(
filename='my_tmp.ext4', filesize_mbytes=100
) as loop_provider:
loop_provider.create()
filesystem = FileSystem.new(
'ext4', loop_provider, '/tmp/'
)
filesystem.create_on_device(
label='TMP'
)
filesystem.sync_data()
12.2 Plugin Architecture #
Each command provided by KIWI NG is written as a task plugin under the kiwi.tasks namespace. As a developer, you can extend KIWI NG with custom task plugins, following the conventions below.
12.2.1 Naming conventions #
- Task plugin file name
The file name of a task plugin must follow the pattern
<service>_<command>.py
. This allows you to invoke the task withkiwi-ng service command ...
- Task plugin option handling
KIWI NG uses the docopt module to handle options. Each task plugin must use docopt to allow option handling.
- Task plugin class
The implementation of the plugin must be a class that matches the naming convention
<Service><Command>Task
. The class must inherit from theCliTask
base class. On the plugin startup, KIWI NG expects an implementation of theprocess
method.- Task plugin entry point
Registration of the plugin must be done in
setup.py
using theentry_points
concept from Python’s setuptools.'packages': ['kiwi_plugin'], 'entry_points': { 'kiwi.tasks': [ 'service_command=kiwi_plugin.tasks.service_command' ] }
12.2.2 Example plugin #
The following example assumes an existing Python project which was set up according to the Python project rules and standards.
Assuming the project namespace is kiwi_relax_plugin, create the task plugin directory
kiwi_relax_plugin/tasks
Create the entry point in
setup.py
.Assuming we want to create the service named relax that has the command justdoit, this is the following entry point definition in
setup.py
:'packages': ['kiwi_relax_plugin'], 'entry_points': { 'kiwi.tasks': [ 'relax_justdoit=kiwi_relax_plugin.tasks.relax_justdoit' ] }
Create the plugin code in the file
kiwi_relax_plugin/tasks/relax_justdoit.py
with the following content:""" usage: kiwi-ng relax justdoit -h | --help kiwi-ng relax justdoit --now commands: justdoit time to relax options: --now right now. For more details about docopt see: http://docopt.org """ # These imports requires kiwi to be part of your environment # It can be either installed from pip into a virtual development # environment or from the distribution package manager from kiwi.tasks.base import CliTask from kiwi.help import Help class RelaxJustdoitTask(CliTask): def process(self): self.manual = Help() if self.command_args.get('help') is True: # The following will invoke man to show the man page # for the requested command. Thus for the call to # succeed a manual page needs to be written and # installed by the plugin return self.manual.show('kiwi::relax::justdoit') print( 'https://genius.com/Frankie-goes-to-hollywood-relax-lyrics' )
Test the plugin
$ ./setup.py develop $ kiwi-ng relax justdoit --now
12.3 Write Integration Tests for the Scripts #
KIWI NG comes with a set of helper functions that can be used in config.sh
(see
also: Section 7.6, “User-Defined Scripts”). These functions utilize containers
to run the individual tasks and verify the final result.
Ensure that you have either podman
or docker
installed and
configured on your system. With Podman, the integration tests use podman
in
rootless mode by default. You can select
docker
instead by setting the environment variable
CONTAINER_RUNTIME
to docker
. Then you can run the integration tests via
tox:
$ tox -e scripts -- -n NUMBER_OF_THREADS
The tests are written using the pytest-container plugin. If applicable,
use the utility functions and fixtures of the plugin. For example, the
auto_container
and auto_container_per_test
fixtures in conjunction with
testinfra.
12.3.1 Test setup #
The script tests can be run inside different containers specified in
test/scripts/conftest.py
. This file contains the CONTAINERS
list
with all currently present images. These images are pulled and built as needed,
and the functions.sh
is copied into /bin/
, so it is
available in PATH
.
To use any of these containers, you can either define the global variable
CONTAINER_IMAGES
in a test module and use the auto_container
fixture, or
parametrize the
container
fixture indirectly:
@pytest.mark.parametrize("container_per_test", (TUMBLEWEED, LEAP_15_3), indirect=True)
def test_RmWorks(container_per_test):
# create the file /root/foobar
container_per_test.connection.run_expect([0], "touch /root/foobar")
assert container_per_test.connection.file("/root/foobar").exists
# source the functions and execute our function under test
container_per_test.connection.run_expect([0], ". /bin/functions.sh && Rm /root/foobar")
# verify the result
assert not container_per_test.connection.file("/root/foobar").exists
The example above uses the _per_test
variant of the container
fixture.
It ensures that the container is used only in a single test function. Use this
variant for tests that mutate the system under test, because otherwise it may
lead race conditions that are difficult to debug. For tests that only perform
reads, you can omit the _per_test
suffix, so that the container environment can
shared with other tests. This improves execution speed, but comes at the
expense of safety in case of mutation.
For further information, refer to pytest-container.
12.4 Extending KIWI NG with Custom Operations #
Abstract
Extension plugins in KIWI NG offer a mechanism for adding information outside the standard KIWI NG schema.
This document describes how to create an extension plugin for the KIWI NG schema as well as how to add and validate additional information in the KIWI NG image description.
The described schema extension can be used in an additional KIWI NG task plugin to provide a new subcommand for KIWI NG. At the present moment, there is no other plugin interface except for providing additional KIWI NG commands.
Depending on the demand for custom plugins, the interface to hook in code into other parts of the KIWI NG processing needs to be extended.
This description applies for version 10.2.2.
12.4.1 The <extension> Section #
The main KIWI NG schema supports an extension section that allows you to specify any XML structure and attributes, as long as they are attached to a namespace. This means that any custom XML structure can be implemented similar to the the example below:
<image>
...
<extension xmlns:my_plugin="http://www.my_plugin.com">
<my_plugin:my_feature>
<my_plugin:title name="cool stuff"/>
</my_plugin:my_feature>
</extension>
</image>
Any toplevel namespace must be unique
Multiple different toplevel namespaces are allowed, for example: my_plugin_a, my_plugin_b
12.4.2 RELAX NG Schema for the Extension #
If an extension section is found, KIWI NG looks up its namespace and uses
the main XML catalog for the schema file to validate the extension data.
The schema file must be a RELAX NG schema in the .rng format. We recommend
to save the schema as /usr/share/xml/kiwi/my_plugin.rng
For the example above, the RELAX NG Schema in the compressed format
my_plugin.rnc
looks as follows:
namespace my_plugin = "http://www.my_plugin.com" start = k.my_feature div { k.my_feature.attlist = empty k.my_feature = element my_plugin:my_feature { k.my_feature.attlist & k.title } } div { k.title.name.attribute = attribute name { text } k.title.attlist = k.title.name.attribute k.title = element my_plugin:title { k.title.attlist } }
In order to convert this schema to the .rng format just call:
$ trang -I rnc -O rng my_plugin.rnc /usr/share/xml/kiwi/my_plugin.rng
12.4.3 Extension schema in XML catalog #
As mentioned above, the mapping from the extension namespace to the correct RELAX NG schema file is handled by a XML catalog file. The XML catalog for the example is as follows:
<?xml version="1.0"?>
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">
<system
systemId="http://www.my_plugin.com"
uri="file:////usr/share/xml/kiwi/my_plugin.rng"/>
</catalog>
For resolving the catalog, KIWI NG uses the xmlcatalog
command
and the main XML catalog from the system /etc/xml/catalog
.
How the main catalog is informed about the existence of the KIWI NG extension catalog file depends on the distribution and its version. Refer to the distribution documentation for information on adding XML catalogs.
If the following command provides the information to the correct RELAX NG schema file, you are ready for a first test:
$ xmlcatalog /etc/xml/catalog http://www.my_plugin.com
12.4.4 Using the extension #
In order to test the extension, insert the example extension into one of your
image description’s config.xml
file.
The following example reads the name attribute from the title section of the my_feature root element and prints it:
import logging
from kiwi.xml_description import XMLDescription
description = XMLDescription('path/to/kiwi/XML/config.xml')
description.load()
my_plugin = description.get_extension_xml_data('my_plugin')
print(my_plugin.getroot()[0].get('name'))
12.5 The Basics #
The core appliance builder is developed in Python and follows the test driven development rules.
If you want to implement a bigger feature, consider opening an issue on
GitHub first to discuss the changes. Or join the discussion in the
#kiwi
channel on Riot.im.
12.6 Fork the upstream repository #
On GitHub, navigate to: https://github.com/OSInside/kiwi
In the top-right corner of the page, click
Fork
.
12.7 Create a local clone of the forked repository #
$ git clone https://github.com/YOUR-USERNAME/kiwi $ git remote add upstream https://github.com/OSInside/kiwi.git
12.8 Install Required Operating System Packages #
KIWI NG requires additional packages at runtime which are not
provided by pip
. Those will be pulled in by installing
the following package:
kiwi-systemdeps
The package is provided on the Open Build Service in the
Virtualization:Appliances:Builder
project. For manual inspection of the packages
that are pulled in from the above kiwi-systemdeps package, please refer
to the package/python-kiwi-spec-template
spec file from the checked
out Git repository.
12.9 Create a Python Virtual Development Environment #
The following commands initializes and activates a development environment for Python 3:
KIWI NG uses tox to create a devel environment and to run tests, linters and other tasks in the tox generated environment. A tox version >= 3.3 is required for this setup process. On your host a python version >= 3.9 is required for tox to work.
$ poetry install
The command above automatically creates the application script
called kiwi-ng
, which allows you to run KIWI NG from the
Python sources inside the virtual environment using Poetry:
$ poetry run kiwi-ng --help
12.10 Running the Unit Tests #
We use tox
to run the unit tests. Tox sets up its own
virtualenvs inside the .tox
directory for multiple Python versions
and should thus not be invoked from inside your development virtualenv.
Before submitting your changes via a pull request, ensure that all tests pass and that the code has the required test coverage via the command:
$ tox
We also include pytest-xdist
in the development virtualenv which allows
to run the unit tests in parallel. It is turned off by default but can be
enabled via:
$ tox -- "-n NUMBER_OF_PROCESSES"
where you can insert an arbitrary number as NUMBER_OF_PROCESSES
(or a
shell command like $(nproc)
). Note that the double quotes around -n
NUMBER_OF_PROCESSES
are required (otherwise tox
will consume
this command line flag instead of forwarding it to pytest
).
The previous call would run the unit tests for different Python versions, check the source code for errors and build the documentation.
If you want to see the available targets, use the option -l
to let
tox
print a list of them:
$ tox -l
To only run a special target, use the -e
option. The following
example runs the test cases for the Python 3.11 interpreter only:
$ tox -e unit_py3_11
12.11 Create a Branch for each Feature or Bugfix #
Code changes should be done in an extra Git branch. This allows for creating GitHub pull requests in a clean way. See also: Collaborating with issues and pull requests
$ git checkout -b my-topic-branch
Make and commit your changes.
You can make multiple commits which is generally useful to give your changes a clear structure and to allow us to better review your work.
Your work is important and must be signed to ensure the integrity of the repository and the code. Thus we recommend to setup a signing key as documented in Signing Git Patches.
$ git commit -S -a
Run the tests and code style checks. All of these are also performed by GitLab CI when a pull request is created.
$ tox
Once everything is done, push your local branch to your forked repository and create a pull request into the upstream repository.
$ git push origin my-topic-branch
Thank you much for contributing to KIWI NG. Your time and work effort is very much appreciated!
12.12 Coding Style #
KIWI NG follows the general PEP8 guidelines with the following exceptions:
We do not use free functions at all. Even utility functions must be part of a class, but should be either prefixed with the
@classmethod
or@staticmethod
decorators (whichever is more appropriate).Do not set module and class level variables, put these into the classes’
__init__
method.The names of constants are not written in all capital letters.
12.12.1 Documentation #
KIWI NG uses Sphinx for the API, user documentation and manual pages
tox -e doc
Document all your classes, methods, their parameters and their types using the standard reStructuredText syntax as supported by Sphinx, an example class is documented as follows:
class Example:
"""
**Example class**
:param str param: A parameter
:param bool : Source file name to compress
:param list supported_zipper: List of supported compression tools
:attr Optional[str] attr: A class attribute
"""
def __init__(self, param, param_w_default=False):
self.attr = param if param_w_default else None
def method(self, param):
"""
A method that takes a parameter.
:param list param: a parameter
:return: whether param is very long
:rtype: bool
"""
return len(param) > 50
Try to stick to the following guidelines when documenting source code:
Classes should be documented directly in their main docstring and not in
__init__
.Document every function parameter and every public attribute including their types.
Only public methods should be documented, private methods don’t have to, unless they are complex and it is not easy to grasp what they do (which should be avoided anyway).
Please also document any user-facing changes that you implementing
(e.g. adding a new build type) in the user documentation, which can be
found in doc/source
. General documentation should be put into the
working_with_kiwi/
subfolder, whereas documentation about more
specialized topics would belong into the building/
subfolder.
Adhere to a line limit of 75 characters when writing the user facing documentation .
12.13 Additional Information #
The following sections provides further information about the repository integrity, version, package and documentation management.
12.13.1 Signing Git Patches #
To ensure the integrity of the repository and the code base, patches sent for inclusion should be signed with a GPG key.
To prepare Git to sign commits, follow these instructions:
Create a key suitable for signing (it is not recommended to use existing keys to not mix it with your email environment):
$ gpg2 --expert --full-gen-key
Either choose a RSA key for signing (option
(4)
) or an ECC key for signing (option(10)
). For a RSA key choose a key size of 4096 bits and for a ECC key choose Curve 25519 (option(1)
). Enter a reasonable validity period (we recommend 2 to 5 years). Complete the key generation by entering your name and email address.Add the key ID to your git configuration, by running the following
git config
commands:$ git config --local user.signingkey $YOUR_SIGN_KEY_ID $ git config --local commit.gpgSign true
Omitting the flag
--local
will make these settings global for all repositories (they will be added to~/.gitconfig
). You can find your signkey’s ID via:$ gpg2 --list-keys --keyid-format long $YOUR_EMAIL pub rsa4096/AABBCCDDEEFF0011 2019-04-26 [S] [expires: 2021-04-16] AAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBB uid [ultimate] YOU <$YOUR_EMAIL>
The key’s ID in this case would be
AABBCCDDEEFF0011
. Note that your signkey will have only a[S]
after the creation date, not a[SC]
(then you are looking at your ordinary GPG key that can also encrypt).
12.13.2 Bumping the Version #
The KIWI NG project follows the Semantic Versioning
scheme. We use the bumpversion
tool for consistent versioning.
Follow these instructions to bump the major, minor, or patch part of the
KIWI NG version. Ensure that your repository is clean (i.e. no modified and
unknown files exist) beforehand running bumpversion
.
For backwards-compatible bug fixes:
$ bumpversion patch
For additional functionality in a backwards-compatible manner. When changed, the patch level is reset to zero:
$ bumpversion minor
For incompatible API changes. When changed, the patch and minor levels are reset to zero:
$ bumpversion major
12.13.3 Creating a RPM Package #
We provide a template for a RPM spec file in
package/python-kiwi-spec-template
alongside with a rpmlint
configuration file and an automatically updated
python-kiwi.changes
.
To create the necessary files to build a RPM package via rpmbuild
, run:
$ make build
The sources are collected in the dist/
directory. These can be
directly build it with rpmbuild
, fedpkg
, or submitted
to the Open Build Service using osc
.