Repository: cekit/cekit

Issue:
relative paths should be relative to the file specifying them
For example, relative paths for artifacts defined in overrides.yaml should be relative to the overrides.yaml file, not the image.yaml.  This makes it difficult to use a single overrides file for multiple image builds, since the relative paths may be different.  I haven't verified whether or not relative paths work correctly with modules (i.e. if a module defines a resource itself).

Discussion:

Patch:
diff --git a/cekit/builder.py b/cekit/builder.py
index 7b72823..3b37e0e 100644
--- a/cekit/builder.py
+++ b/cekit/builder.py
@@ -23,6 +23,9 @@ class Builder(object):
                 # import is delayed until here to prevent circular import error
                 from cekit.builders.osbs import OSBSBuilder as BuilderImpl
                 logger.info("Using OSBS builder to build the image.")
+            elif 'buildah' == build_engine:
+                from cekit.builders.buildah import BuildahBuilder as BuilderImpl
+                logger.info("Using Buildah builder to build the image.")
             else:
                 raise CekitError("Builder engine %s is not supported" % build_engine)
 
diff --git a/cekit/builders/buildah.py b/cekit/builders/buildah.py
new file mode 100644
index 0000000..3e9cb72
--- /dev/null
+++ b/cekit/builders/buildah.py
@@ -0,0 +1,57 @@
+import logging
+import subprocess
+import os
+
+from cekit.builder import Builder
+from cekit.errors import CekitError
+
+
+logger = logging.getLogger('cekit')
+
+
+class BuildahBuilder(Builder):
+    """This class representes buildah builder in build-using-dockerfile mode."""
+
+    def __init__(self, build_engine, target, params={}):
+        self._tags = params.get('tags')
+        self._pull = params.get('pull', False)  # --pull-always
+        super(BuildahBuilder, self).__init__(build_engine, target, params)
+
+    def check_prerequisities(self):
+        try:
+            subprocess.check_output(['sudo', 'buildah', 'version'], stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError as ex:
+            raise CekitError("Buildah build engine needs buildah"
+                             " installed and configured, error: %s"
+                             % ex.output)
+        except Exception as ex:
+            raise CekitError("Buildah build engine needs buildah installed and configured!", ex)
+
+    def build(self):
+        """Build container image using buildah."""
+        tags = self._tags
+        cmd = ["sudo", "buildah", "build-using-dockerfile"]
+
+        if self._pull:
+            cmd.append('--pull-awlays')
+
+        # Custom tags for the container image
+        logger.debug("Building image with tags: '%s'" %
+                     "', '".join(tags))
+
+        for tag in tags:
+            cmd.extend(["-t", tag])
+
+        logger.info("Building container image...")
+
+        cmd.append(os.path.join(self.target, 'image'))
+
+        logger.debug("Running Buildah build: '%s'" % " ".join(cmd))
+
+        try:
+            subprocess.check_call(cmd)
+
+            logger.info("Image built and available under following tags: %s"
+                        % ", ".join(tags))
+        except:
+            raise CekitError("Image build failed, see logs above.")
diff --git a/cekit/cli.py b/cekit/cli.py
index 24e0962..b421f2f 100644
--- a/cekit/cli.py
+++ b/cekit/cli.py
@@ -67,7 +67,7 @@ class Cekit(object):
 
         build_group.add_argument('--build-engine',
                                  default='docker',
-                                 choices=['docker', 'osbs'],
+                                 choices=['docker', 'osbs', 'buildah'],
                                  help='an engine used to build the image.')
 
         build_group.add_argument('--build-tag',
diff --git a/cekit/descriptor/base.py b/cekit/descriptor/base.py
index d1448e3..63b1d59 100644
--- a/cekit/descriptor/base.py
+++ b/cekit/descriptor/base.py
@@ -4,6 +4,7 @@ import logging
 import os
 import yaml
 
+
 from cekit.errors import CekitError
 from pykwalify.core import Core
 
diff --git a/cekit/descriptor/image.py b/cekit/descriptor/image.py
index 18107a5..f2eec1b 100644
--- a/cekit/descriptor/image.py
+++ b/cekit/descriptor/image.py
@@ -1,4 +1,5 @@
 import copy
+import os
 import yaml
 
 from cekit.descriptor import Descriptor, Label, Env, Port, Run, Modules, \
@@ -29,8 +30,9 @@ def get_image_schema():
 
 
 class Image(Descriptor):
-    def __init__(self, descriptor, directory):
-        self.directory = directory
+    def __init__(self, descriptor, artifact_dir):
+        self._artifact_dir = artifact_dir
+        self.path = artifact_dir
         self.schemas = [_image_schema.copy()]
         super(Image, self).__init__(descriptor)
         self.skip_merging = ['description',
@@ -76,9 +78,10 @@ class Image(Descriptor):
         self._descriptor['ports'] = [Port(x) for x in self._descriptor.get('ports', [])]
         if 'run' in self._descriptor:
             self._descriptor['run'] = Run(self._descriptor['run'])
-        self._descriptor['artifacts'] = [Resource(a) for a in self._descriptor.get('artifacts', [])]
+        self._descriptor['artifacts'] = [Resource(a, directory=self._artifact_dir)
+                                         for a in self._descriptor.get('artifacts', [])]
         if 'modules' in self._descriptor:
-            self._descriptor['modules'] = Modules(self._descriptor['modules'])
+            self._descriptor['modules'] = Modules(self._descriptor['modules'], self.path)
         if 'packages' in self._descriptor:
             self._descriptor['packages'] = Packages(self._descriptor['packages'])
         if 'osbs' in self._descriptor:
diff --git a/cekit/descriptor/module.py b/cekit/descriptor/module.py
index 18f32bd..ae7e126 100644
--- a/cekit/descriptor/module.py
+++ b/cekit/descriptor/module.py
@@ -16,7 +16,9 @@ class Module(Image):
     Constructor arguments:
     descriptor_path: A path to module descriptor file.
     """
-    def __init__(self, descriptor, path):
+    def __init__(self, descriptor, path, artifact_dir):
+        self._artifact_dir = artifact_dir
+        self.path = path
         schema = module_schema.copy()
         self.schemas = [schema]
         # calling Descriptor constructor only here (we dont wat Image() to mess with schema)
@@ -27,7 +29,6 @@ class Module(Image):
                              'release']
 
         self._prepare()
-        self.path = path
         self.name = self._descriptor['name']
         self._descriptor['execute'] = [Execute(x, self.name)
                                       for x in self._descriptor.get('execute', [])]
diff --git a/cekit/descriptor/modules.py b/cekit/descriptor/modules.py
index b033833..0a885a5 100644
--- a/cekit/descriptor/modules.py
+++ b/cekit/descriptor/modules.py
@@ -19,10 +19,10 @@ map:
 
 
 class Modules(Descriptor):
-    def __init__(self, descriptor):
+    def __init__(self, descriptor, path):
         self.schemas = modules_schema
         super(Modules, self).__init__(descriptor)
-        self._descriptor['repositories'] = [Resource(r)
+        self._descriptor['repositories'] = [Resource(r, directory=path)
                                             for r in self._descriptor.get('repositories', [])]
         self._descriptor['install'] = [Install(x) for x in self._descriptor.get('install', [])]
 
diff --git a/cekit/descriptor/overrides.py b/cekit/descriptor/overrides.py
index 265abfb..4378cfd 100644
--- a/cekit/descriptor/overrides.py
+++ b/cekit/descriptor/overrides.py
@@ -7,7 +7,9 @@ overrides_schema['map']['version'] = {'type': 'text'}
 
 
 class Overrides(Image):
-    def __init__(self, descriptor):
+    def __init__(self, descriptor, artifact_dir):
+        self._artifact_dir = artifact_dir
+        self.path = artifact_dir
         schema = overrides_schema.copy()
         self.schemas = [schema]
         # calling Descriptor constructor only here (we dont wat Image() to mess with schema)
diff --git a/cekit/descriptor/resource.py b/cekit/descriptor/resource.py
index 55a4d03..7e9b693 100644
--- a/cekit/descriptor/resource.py
+++ b/cekit/descriptor/resource.py
@@ -25,12 +25,9 @@ class Resource(Descriptor):
     SUPPORTED_HASH_ALGORITHMS = ['sha256', 'sha1', 'md5']
     CHECK_INTEGRITY = True
 
-    def __new__(cls, resource):
+    def __new__(cls, resource, **kwargs):
         if cls is Resource:
             if 'path' in resource:
-                directory = resource['path']
-                if not os.path.isabs(directory):
-                    resource['path'] = os.path.join(os.getcwd(), directory)
                 return super(Resource, cls).__new__(_PathResource)
             elif 'url' in resource:
                 return super(Resource, cls).__new__(_UrlResource)
@@ -67,13 +64,13 @@ class Resource(Descriptor):
                 self.checksums[algorithm] = descriptor[algorithm]
 
     def __eq__(self, other):
-        #All subclasses of Resource are considered same object type
+        # All subclasses of Resource are considered same object type
         if isinstance(other, Resource):
             return self['name'] == other['name']
         return NotImplemented
 
     def __ne__(self, other):
-        #All subclasses of Resource are considered same object type
+        # All subclasses of Resource are considered same object type
         if isinstance(other, Resource):
             return not self['name'] == other['name']
         return NotImplemented
@@ -113,7 +110,7 @@ class Resource(Descriptor):
 
             # exception is fatal we be logged before Cekit dies
             raise CekitError("Error copying resource: '%s'. See logs for more info."
-                                 % self.name, ex)
+                             % self.name, ex)
 
         self.__verify(target)
 
@@ -122,7 +119,8 @@ class Resource(Descriptor):
     def __verify(self, target):
         """ Checks all defined check_sums for an aritfact """
         if not self.checksums:
-            logger.debug("Artifact '%s' lacks any checksum definition, it will be replaced" % self.name)
+            logger.debug("Artifact '%s' lacks any checksum definition, it will be replaced"
+                         % self.name)
             return False
         if not Resource.CHECK_INTEGRITY:
             logger.info("Integrity checking disabled, skipping verification.")
@@ -211,7 +209,13 @@ class Resource(Descriptor):
 
 class _PathResource(Resource):
 
-    def __init__(self, descriptor):
+    def __init__(self, descriptor, directory, **kwargs):
+        # if the path si relative its considered relative to the directory parameter
+        # it defualts to CWD, but should be set for a descriptor dir if used for artifacts
+        if not os.path.isabs(descriptor['path']):
+            descriptor['path'] = os.path.join(directory,
+                                              descriptor['path'])
+
         if 'name' not in descriptor:
             descriptor['name'] = os.path.basename(descriptor['path'])
         super(_PathResource, self).__init__(descriptor)
@@ -233,8 +237,8 @@ class _PathResource(Resource):
                     raise CekitError("Could not download resource '%s' from cache" % self.name)
             else:
                 raise CekitError("Could not copy resource '%s', "
-                                     "source path does not exist. "
-                                     "Make sure you provided correct path" % self.name)
+                                 "source path does not exist. "
+                                 "Make sure you provided correct path" % self.name)
 
         logger.debug("Copying repository from '%s' to '%s'." % (self.path,
                                                                 target))
@@ -247,7 +251,7 @@ class _PathResource(Resource):
 
 class _UrlResource(Resource):
 
-    def __init__(self, descriptor):
+    def __init__(self, descriptor, **kwargs):
         if 'name' not in descriptor:
             descriptor['name'] = os.path.basename(descriptor['url'])
         super(_UrlResource, self).__init__(descriptor)
@@ -264,7 +268,7 @@ class _UrlResource(Resource):
 
 class _GitResource(Resource):
 
-    def __init__(self, descriptor):
+    def __init__(self, descriptor, **kwargs):
         if 'name' not in descriptor:
             descriptor['name'] = os.path.basename(descriptor['git']['url'])
         super(_GitResource, self).__init__(descriptor)
diff --git a/cekit/generator.py b/cekit/generator.py
index a19fe1b..8cabe1b 100644
--- a/cekit/generator.py
+++ b/cekit/generator.py
@@ -35,7 +35,7 @@ class Generator(object):
             if not modules.get('repositories'):
                 modules['repositories'] = [{'path': local_mod_path, 'name': 'modules'}]
 
-        self.image = Image(descriptor, os.path.dirname(descriptor_path))
+        self.image = Image(descriptor, os.path.dirname(os.path.abspath(descriptor_path)))
         self.target = target
 
         if overrides:
@@ -110,7 +110,8 @@ class Generator(object):
 
     def override(self, overrides_path):
         logger.info("Using overrides file from '%s'." % overrides_path)
-        descriptor = Overrides(tools.load_descriptor(overrides_path))
+        descriptor = Overrides(tools.load_descriptor(overrides_path),
+                               os.path.dirname(os.path.abspath(overrides_path)))
         descriptor.merge(self.image)
         return descriptor
 
diff --git a/cekit/module.py b/cekit/module.py
index a870485..dfad8fb 100644
--- a/cekit/module.py
+++ b/cekit/module.py
@@ -49,7 +49,8 @@ def copy_module_to_target(name, version, target):
 
 def check_module_version(path, version):
     descriptor = Module(tools.load_descriptor(os.path.join(path, 'module.yaml')),
-                        path)
+                        path,
+                        os.path.dirname(os.path.abspath(os.path.join(path, 'module.yaml'))))
     if descriptor.version != version:
         raise CekitError("Requested conflicting version '%s' of module '%s'" %
                              (version, descriptor['name']))
@@ -84,7 +85,9 @@ def discover_modules(repo_dir):
     for modules_dir, _, files in os.walk(repo_dir):
         if 'module.yaml' in files:
             module = Module(tools.load_descriptor(os.path.join(modules_dir, 'module.yaml')),
-                            modules_dir)
+                            modules_dir,
+                            os.path.dirname(os.path.abspath(os.path.join(modules_dir,
+                                                                         'module.yaml'))))
             module.fetch_dependencies(repo_dir)
             logger.debug("Adding module '%s', path: '%s'" % (module.name, module.path))
             modules.append(module)
diff --git a/docs/build.rst b/docs/build.rst
index 5d01c9d..0f3830e 100644
--- a/docs/build.rst
+++ b/docs/build.rst
@@ -5,6 +5,7 @@ Cekit supports following builder engines:
 
 * ``Docker`` -- build the container image using `docker build <https://docs.docker.com/engine/reference/commandline/build/>`_ command and it default option
 * ``OSBS`` -- build the container image using `OSBS service <https://osbs.readthedocs.io>`_
+* ``Buildah`` -- build the container image using `Buildah <https://github.com/projectatomic/buildah>`_
 
 Executing builds
 -----------------
@@ -18,7 +19,7 @@ You can execute an container image build by running:
 **Options affecting builder:**
 
 * ``--tag`` -- an image tag used to build image (can be specified multiple times)
-* ``--build-engine`` -- a builder engine to use ``osbs`` or ``docker`` [#f1]_
+* ``--build-engine`` -- a builder engine to use ``osbs``, ``buildah`` or ``docker`` [#f1]_
 * ``--build-pull`` -- ask a builder engine to check and fetch latest base image
 * ``--build-osbs-stage`` -- use ``rhpkg-stage`` tool instead of ``rhpkg``
 * ``--build-osbs-release`` [#f2]_ -- perform a OSBS release build
@@ -46,7 +47,7 @@ This is the default way to build an container image. The image is build using ``
 OSBS build
 ^^^^^^^^^^^^^^^
 
-This build is using ``rhpkg container-build`` to build the image using OSBS service. By default
+This build engine is using ``rhpkg container-build`` to build the image using OSBS service. By default
 it performs scratch build. If you need a release build you need to specify ``--build-osbs-release`` parameter.
 
 **Example:** Performing scratch build
@@ -61,3 +62,21 @@ it performs scratch build. If you need a release build you need to specify ``--b
 .. code:: bash
 
 	  $ cekit build --build-engine=osbs --build-osbs-release
+
+
+Buildah build
+^^^^^^^^^^^^^
+
+This build engine is based on `Buildah <https://github.com/projectatomic/buildah>`_. Buildah still doesn't
+support non-privileged builds so you need to have **sudo** configured to run `buildah` as a root user on
+your desktop.
+
+.. note::
+   If you need to use any non default registry, please update `/etc/containers/registry.conf` file.
+
+
+**Example:** Building image using Buildah
+
+.. code:: bash
+
+	  $ cekit build --build-engine=buildah
diff --git a/docs/descriptor.rst b/docs/descriptor.rst
index 52f998c..2abd4a0 100644
--- a/docs/descriptor.rst
+++ b/docs/descriptor.rst
@@ -28,6 +28,14 @@ for downloaded resources will match the ``name`` attribute, which defaults to
 the base name of the file/URL. Artifact locations may be specified as ``url``\s,
 ``path``\s or ``git`` references.
 
+.. note::
+
+   If you are using relative ``path`` to define an artifact, path is considered relative to an
+   image descriptor which introduced that artifact.
+   
+   **Example**: If an artifact is defined inside */foo/bar/image.yaml* with a path: *baz/1.zip*
+   the artifact will be resolved as */foo/bar/baz/1.zip*
+
 .. code:: yaml
 
     artifacts:


Test patch:
diff --git a/tests/test_builder.py b/tests/test_builder.py
index 2293306..a4521ca 100644
--- a/tests/test_builder.py
+++ b/tests/test_builder.py
@@ -116,3 +116,32 @@ def test_docker_builder_run(mocker):
     builder.build()
 
     check_call.assert_called_once_with(['docker', 'build', '-t', 'foo', '-t', 'bar', 'tmp/image'])
+
+
+def test_buildah_builder_run(mocker):
+    params = {'tags': ['foo', 'bar']}
+    check_call = mocker.patch.object(subprocess, 'check_call')
+    builder = create_osbs_build_object(mocker, 'buildah', params)
+    builder.build()
+
+    check_call.assert_called_once_with(['sudo',
+                                        'buildah',
+                                        'build-using-dockerfile',
+                                        '-t', 'foo',
+                                        '-t', 'bar',
+                                        'tmp/image'])
+
+
+def test_buildah_builder_run_pull(mocker):
+    params = {'tags': ['foo', 'bar'], 'pull': True}
+    check_call = mocker.patch.object(subprocess, 'check_call')
+    builder = create_osbs_build_object(mocker, 'buildah', params)
+    builder.build()
+
+    check_call.assert_called_once_with(['sudo',
+                                        'buildah',
+                                        'build-using-dockerfile',
+                                        '--pull-awlays',
+                                        '-t', 'foo',
+                                        '-t', 'bar',
+                                        'tmp/image'])
diff --git a/tests/test_dockerfile.py b/tests/test_dockerfile.py
index d9da5b5..c5f8e15 100644
--- a/tests/test_dockerfile.py
+++ b/tests/test_dockerfile.py
@@ -88,7 +88,7 @@ def prepare_generator(target, desc_part, desc_type="image"):
     desc = basic_config.copy()
     desc.update(desc_part)
 
-    image = Module(desc, '/tmp/')
+    image = Module(desc, '/tmp/', '/tmp')
 
     generator = Generator.__new__(Generator)
     generator.image = image
diff --git a/tests/test_unit_args.py b/tests/test_unit_args.py
index 43ce2b3..10eb5dc 100644
--- a/tests/test_unit_args.py
+++ b/tests/test_unit_args.py
@@ -36,7 +36,8 @@ def test_args_build_pull(mocker):
 
     assert Cekit().parse().args.build_pull
 
-@pytest.mark.parametrize('engine', ['osbs', 'docker'])
+
+@pytest.mark.parametrize('engine', ['osbs', 'docker', 'buildah'])
 def test_args_build_engine(mocker, engine):
     mocker.patch.object(sys, 'argv', ['cekit', 'build', '--build-engine', engine])
 
diff --git a/tests/test_unit_module.py b/tests/test_unit_module.py
index 6f9bae9..fa3dca3 100644
--- a/tests/test_unit_module.py
+++ b/tests/test_unit_module.py
@@ -1,5 +1,7 @@
 from cekit.descriptor import Module
 from cekit.module import modules
+import os
+
 
 module_desc = {
     'schema_version': 1,
@@ -16,6 +18,6 @@ module_desc = {
 
 def test_modules_repos(tmpdir):
     tmpdir = str(tmpdir.mkdir('target'))
-    module = Module(module_desc, tmpdir)
+    module = Module(module_desc, os.getcwd(), '/tmp')
     module.fetch_dependencies(tmpdir)
     assert 'foo' in [m['name'] for m in modules]
diff --git a/tests/test_unit_resource.py b/tests/test_unit_resource.py
index 4446e2d..3bf20c9 100644
--- a/tests/test_unit_resource.py
+++ b/tests/test_unit_resource.py
@@ -178,3 +178,15 @@ def test_generated_url_with_cacher():
     res.name = 'file'
     assert res._Resource__substitute_cache_url('file') == 'file,sha256,justamocksum'
     tools.cfg = {}
+
+
+def test_path_resource_absolute():
+    res = Resource({'name': 'foo',
+                    'path': '/bar'}, directory='/foo')
+    assert res.path == '/bar'
+
+
+def test_path_resource_relative():
+    res = Resource({'name': 'foo',
+                    'path': 'bar'}, directory='/foo')
+    assert res.path == '/foo/bar'
diff --git a/tests/test_unit_tools.py b/tests/test_unit_tools.py
index 9c13786..31c5352 100644
--- a/tests/test_unit_tools.py
+++ b/tests/test_unit_tools.py
@@ -20,17 +20,17 @@ def test_merging_description_image():
     desc1 = Image({'name': 'foo', 'version': 1}, None)
 
     desc2 = Module({'name': 'mod1',
-                    'description': 'mod_desc'}, None)
+                    'description': 'mod_desc'}, None, None)
 
     merged = _merge_descriptors(desc1, desc2)
     assert 'description' not in merged
 
 
 def test_merging_description_modules():
-    desc1 = Module({'name': 'foo'}, None)
+    desc1 = Module({'name': 'foo'}, None, None)
 
     desc2 = Module({'name': 'mod1',
-                    'description': 'mod_desc'}, None)
+                    'description': 'mod_desc'}, None, None)
 
     merged = _merge_descriptors(desc1, desc2)
     assert 'description' not in merged
@@ -40,7 +40,7 @@ def test_merging_description_override():
     desc1 = Image({'name': 'foo', 'version': 1}, None)
 
     desc2 = Overrides({'name': 'mod1',
-                       'description': 'mod_desc'})
+                       'description': 'mod_desc'}, None)
 
     merged = _merge_descriptors(desc2, desc1)
     assert 'description' in merged