Repository: cekit/cekit

Issue:
help doesn´t work inside module
**Describe the solution you'd like**
I like to modify help template and share it with multiples images like a module, but help inside a module is ignored. Can I share help template between multiples repositories?

.

Discussion:
@jmtd: We include the docs for "help" in the module documentation : https://docs.cekit.io/en/latest/descriptor/module.html#help 

which at least implies we expect it to work in a module, and if it does not, then there's a bug: either in the  docs or the generator.@rnc: Documentation is only available for the final image I believe looking at the code ( https://github.com/cekit/cekit/blob/develop/cekit/generator/base.py#L199 )  and at the preceding issue that @goldmann worked on in #377. 

The image help is generated from the final image descriptor (which is generated as per https://docs.cekit.io/en/latest/handbook/modules/image_order.html ). 

From the look of the code it only checks the top level image descriptor, not included modules or overrides for any activation of help or configuration of the template. Another thing the documentation does not make clear is that the template path can actually be absolute or relative.

Solution patch:
diff --git a/cekit/builder.py b/cekit/builder.py
index 8a87c3a..5a3bac7 100644
--- a/cekit/builder.py
+++ b/cekit/builder.py
@@ -102,6 +102,7 @@ class Builder(Command):
             self.params.target,
             container_file,
             self.params.overrides,
+            self.params.no_squash,
         )
 
         if CONFIG.get("common", "redhat"):
diff --git a/cekit/builders/oci_builder.py b/cekit/builders/oci_builder.py
index 6d7b4d9..42bd6c1 100644
--- a/cekit/builders/oci_builder.py
+++ b/cekit/builders/oci_builder.py
@@ -48,7 +48,15 @@ class OCIBuilder(Builder):
 
         cmd.append(os.path.join(self.target, "image"))
 
-        run_wrapper(cmd, False, f"Could not run {build_type} {cmd}")
+        capture: bool = False
+        if LOGGER.isEnabledFor(logging.DEBUG):
+            # Normally we'll just let output print to stdout/stderr. If verbose(debug) is enabled
+            # then we'll capture it but log it as well. This is useful for tests verifying the output.
+            capture = True
+        result = run_wrapper(cmd, capture, f"Could not run {build_type} {cmd}")
+        if result.stdout is not None:
+            LOGGER.debug(result.stdout)
+            LOGGER.debug("\n")
 
         LOGGER.info(
             f"Image built and available under following tags: {', '.join(tags)}"
diff --git a/cekit/descriptor/image.py b/cekit/descriptor/image.py
index 0ee4519..104bfa1 100644
--- a/cekit/descriptor/image.py
+++ b/cekit/descriptor/image.py
@@ -1,5 +1,6 @@
 import copy
 import logging
+import os
 from collections import OrderedDict
 from typing import TYPE_CHECKING, Any, Dict, Iterable, List, NamedTuple, Union
 
@@ -243,6 +244,10 @@ class Image(Descriptor):
     def help(self) -> dict:
         return self.get("help", {})
 
+    @help.setter
+    def help(self, value):
+        self._descriptor["help"] = value
+
     def apply_image_overrides(self, overrides: List["Overrides"]):
         """
         Applies overrides to the image descriptor.
@@ -258,6 +263,8 @@ class Image(Descriptor):
                 self.base = override.base
             if override.description:
                 self.description = override.description
+            if override.help:
+                self.help = override.help
 
             labels = Image._to_dict(self.labels)
             for label in override.labels:
@@ -519,6 +526,20 @@ class Image(Descriptor):
                 module, module.modules.install, install_list, module_registry
             )
 
+            if module.help:
+                self.help = module.help
+                # This makes it easier for people to use helper templates in modules. It automatically prepends
+                # the appropriate path for modules so a relative path can be located.
+                if self.help.get("template", "") and not os.path.isabs(
+                    module.help.get("template")
+                ):
+                    self.help["template"] = (
+                        "target/image/modules/"
+                        + to_install.name
+                        + "/"
+                        + self.help.get("template")
+                    )
+
             # move this module to the end of the list.
             install_list.pop(to_install.name)
             install_list[to_install.name] = to_install
diff --git a/cekit/generator/base.py b/cekit/generator/base.py
index 1dce90a..24422a7 100644
--- a/cekit/generator/base.py
+++ b/cekit/generator/base.py
@@ -71,6 +71,7 @@ class Generator(object):
         target: PathType,
         container_file: str,
         overrides: List[str],
+        no_squash: bool,
     ):
         self._descriptor_path: PathType = descriptor_path
         self._overrides: List[Overrides] = []
@@ -81,6 +82,7 @@ class Generator(object):
         self.builder_images: List[Image] = []
         self.images: List[Image] = []
         self.container_file: str = container_file
+        self.no_squash: bool = no_squash
 
         # If descriptor has been passed in from standard input its not a path so use current working directory
         if "-" == descriptor_path:
@@ -383,6 +385,7 @@ class Generator(object):
         env.globals["helper"] = TemplateHelper(self._module_registry)
         env.globals["image"] = self.image
         env.globals["builders"] = self.builder_images
+        env.globals["no_squash"] = self.no_squash
 
         template = env.get_template(os.path.basename(template_file))
 
diff --git a/cekit/generator/behave.py b/cekit/generator/behave.py
index 20ae69d..55aee37 100644
--- a/cekit/generator/behave.py
+++ b/cekit/generator/behave.py
@@ -11,9 +11,10 @@ class BehaveGenerator(Generator):
         target: PathType,
         container_file: str,
         overrides: List[str],
+        no_squash: bool,
     ):
         super(BehaveGenerator, self).__init__(
-            descriptor_path, target, container_file, overrides
+            descriptor_path, target, container_file, overrides, no_squash
         )
 
     def prepare_artifacts(self):
diff --git a/cekit/generator/docker.py b/cekit/generator/docker.py
index ec0482f..45feafa 100644
--- a/cekit/generator/docker.py
+++ b/cekit/generator/docker.py
@@ -17,9 +17,10 @@ class DockerGenerator(Generator):
         target: PathType,
         container_file: str,
         overrides: List[str],
+        no_squash: bool,
     ):
         super(DockerGenerator, self).__init__(
-            descriptor_path, target, container_file, overrides
+            descriptor_path, target, container_file, overrides, no_squash
         )
         self._fetch_repos = True
 
diff --git a/cekit/generator/osbs.py b/cekit/generator/osbs.py
index df8138c..dc120cc 100644
--- a/cekit/generator/osbs.py
+++ b/cekit/generator/osbs.py
@@ -37,10 +37,11 @@ class OSBSGenerator(Generator):
         target: PathType,
         container_file: str,
         overrides: List[str],
+        no_squash: bool,
     ):
         self._wipe = True
         super(OSBSGenerator, self).__init__(
-            descriptor_path, target, container_file, overrides
+            descriptor_path, target, container_file, overrides, no_squash
         )
 
     def init(self):
diff --git a/cekit/template_helper.py b/cekit/template_helper.py
index 98b4f4f..a448975 100644
--- a/cekit/template_helper.py
+++ b/cekit/template_helper.py
@@ -176,20 +176,25 @@ class TemplateHelper(object):
         ):
             return True
 
-    def package_manager_clean_and_query(self, pkg_mgr):
+    def package_manager_query(self, pkg_mgr):
         if "apk" in pkg_mgr:
             return "apk info -e"
         elif "apt-get" in pkg_mgr:
             return "dpkg-query --list"
         else:
-            result = ""
-            if "microdnf" in pkg_mgr:
-                result += "microdnf clean all && "
-            result += "rpm -q"
-            return result
+            return "rpm -q"
 
     def package_manager_remove(self, pkg_mgr):
         if "apk" in pkg_mgr:
             return "del"
         else:
             return "remove -y"
+
+    def package_manager_clean(self, pkg_mgr):
+        if "apk" in pkg_mgr:
+            # Cache is not enabled by default so there is nothing to clean
+            return ":"
+        elif "apt-get" in pkg_mgr:
+            return pkg_mgr + " clean"
+        else:
+            return pkg_mgr + " clean all"
diff --git a/cekit/templates/template.jinja b/cekit/templates/template.jinja
index d166e7d..f8a622c 100644
--- a/cekit/templates/template.jinja
+++ b/cekit/templates/template.jinja
@@ -12,7 +12,7 @@ repos/{{ repo.filename }} \
  # Removes packages using specified package manager.
  #}
 {%- macro pkg_remove(pkg_manager, pkg_manager_flags, packages) -%}
-{{ pkg_manager }} {{ helper.package_manager_flags(pkg_manager, pkg_manager_flags) }} {{ helper.package_manager_remove(pkg_manager) }} {%- for package in packages %} {{ package }}{% endfor %}
+{{ pkg_manager }} {{ helper.package_manager_flags(pkg_manager, pkg_manager_flags) }} {{ helper.package_manager_remove(pkg_manager) }} {%- for package in packages %} {{ package }}{% endfor %} \
 {%- endmacro -%}
 
 {#
@@ -20,7 +20,7 @@ repos/{{ repo.filename }} \
  #}
 {%- macro pkg_install(pkg_manager, pkg_manager_flags, packages) -%}
 {{ pkg_manager }} {{ helper.package_manager_flags(pkg_manager, pkg_manager_flags) }} {{ helper.package_manager_install(pkg_manager) }} {%- for package in packages %} {{ package }}{% endfor %} \
-            && {{ helper.package_manager_clean_and_query(pkg_manager) }}{% for package in packages %} {{ package.split('=')[0] }}{% endfor %}
+            && {{ helper.package_manager_query(pkg_manager) }}{% for package in packages %} {{ package.split('=')[0] }}{% endfor %} \
 {%- endmacro -%}
 
 {#
@@ -28,10 +28,17 @@ repos/{{ repo.filename }} \
  #}
 {%- macro pkg_reinstall(pkg_manager, pkg_manager_flags, packages) -%}
 {{ pkg_manager }} {{ helper.package_manager_flags(pkg_manager, pkg_manager_flags) }} {{ helper.package_manager_reinstall(pkg_manager) }} {%- for package in packages %} {{ package }}{% endfor %} \
-            && {{ helper.package_manager_clean_and_query(pkg_manager) }}{% for package in packages %} {{ package.split('=')[0] }}{% endfor %}
+            && {{ helper.package_manager_query(pkg_manager) }}{% for package in packages %} {{ package.split('=')[0] }}{% endfor %} \
 {%- endmacro -%}
 
 
+{#
+ # Cleans up package manager.
+ #}
+{%- macro pkg_cleanup(pkg_manager) -%}
+    {{ helper.package_manager_clean(pkg_manager) }} \
+{%- endmacro -%}
+
 {#
  # Handle processing ARGS
  #}
@@ -99,18 +106,36 @@ repos/{{ repo.filename }} \
         {% if module.packages and module.packages.remove or module.packages.install or module.packages.reinstall %}
         # Switch to 'root' user for package management for '{{ module.name }}' {{ module_type }} defined packages
         USER root
+        {#
+         # Ideally we would use heredoc but its not available in all versions so use continuation lines.
+         #}
+        RUN : \
         {% if module.packages and module.packages.remove %}
         # Remove packages defined in the '{{ module.name }}' {{ module_type }}
-        RUN {{ pkg_remove(parent_image.packages.manager, parent_image.packages.manager_flags, module.packages.remove) }}
+        && {{ pkg_remove(parent_image.packages.manager, parent_image.packages.manager_flags, module.packages.remove) }}
         {% endif -%}
         {% if module.packages and module.packages.install %}
         # Install packages defined in the '{{ module.name }}' {{ module_type }}
-        RUN {{ pkg_install(parent_image.packages.manager, parent_image.packages.manager_flags, module.packages.install) }}
+        && {{ pkg_install(parent_image.packages.manager, parent_image.packages.manager_flags, module.packages.install) }}
         {% endif -%}
         {% if module.packages and module.packages.reinstall %}
         # Reinstall packages defined in the '{{ module.name }}' {{ module_type }}
-        RUN {{ pkg_reinstall(parent_image.packages.manager, parent_image.packages.manager_flags, module.packages.reinstall) }}
+        && {{ pkg_reinstall(parent_image.packages.manager, parent_image.packages.manager_flags, module.packages.reinstall) }}
         {% endif -%}
+        {#
+         # Only if we're not using CEKit squashing do we clear the metadata here.
+         #}
+        {% if no_squash %}
+        {% if helper.package_manager_cleanup(image) %}
+        # Clear package manager metadata
+        && {{ pkg_cleanup(parent_image.packages.manager) }}
+        && rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf" \
+        {% endif -%}
+        {% endif -%}
+        {#
+         # The : noop is only to complete any prior continuation lines.
+         #}
+        && :
 
         {% endif -%}
         {% if module_type == 'module' %}
@@ -239,9 +264,12 @@ repos/{{ repo.filename }} \
     {#
      # Clears repository cache.
      #}
+    {% if not no_squash %}
     {% if helper.package_manager_cleanup(image) %}
     # Clear package manager metadata
-    RUN rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf"
+    RUN {{ pkg_cleanup(image.packages.manager) }}
+    && rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf"
+    {% endif -%}
     {% endif -%}
 
     {#
diff --git a/cekit/tools.py b/cekit/tools.py
index 2f38214..686c112 100644
--- a/cekit/tools.py
+++ b/cekit/tools.py
@@ -362,7 +362,7 @@ def run_wrapper(
         stderr_capture = None
         if capture_output:
             stdout_capture = subprocess.PIPE
-            stderr_capture = subprocess.PIPE
+            stderr_capture = subprocess.STDOUT
         # subprocess.run uses Popen: https://docs.python.org/3/library/subprocess.html#subprocess.Popen
         # which uses https://docs.python.org/3/library/os.html#os.execvpe like behaviour
         # to locate the executable if its relative on POSIX.
diff --git a/docs/descriptor/includes/help.rst b/docs/descriptor/includes/help.rst
index 0575ff6..e3e44ac 100644
--- a/docs/descriptor/includes/help.rst
+++ b/docs/descriptor/includes/help.rst
@@ -21,8 +21,22 @@ a ``help.md`` is generated and added to the **dist-git** repository.
 The default help template is supplied within CEKit. You can override it for
 every image via image definition. The optional help sub-section defines
 a key ``template``, which can be used to define a filename to use for generating
-image documentation at build time. Note that if the ``template`` denotes a file *without*
-any substitution markers then effectively the file is copied to the dist-git repository as ``help.md`` verbatim.
+image documentation at build time.
+
+The filename may be absolute or a relative path. If the latter it is used as relative to the image descriptor.
+For modules a relative path will be relative to the module root.
+
+.. code-block:: sh
+
+   cat test_module/module.yaml
+   version: '1.0'
+   name: test_module
+   help:
+     add: true
+     template: help.jinja
+
+Note that if the ``template`` denotes a file *without* any substitution markers then effectively the file is copied
+to the dist-git repository as ``help.md`` verbatim.
 
 .. code-block:: yaml
 
@@ -72,3 +86,6 @@ will produce the following rendered Markdown file:
 For a more detailed template example, see the
 `default help.jinja supplied in the CEKit source code
 <https://github.com/cekit/cekit/blob/develop/cekit/templates/help.jinja>`__.
+
+.. warning::
+    If multiple help templates are used (e.g. in modules), as they cannot be merged the last one will be used.
\ No newline at end of file


Test patch:
diff --git a/cekit/test/behave_tester.py b/cekit/test/behave_tester.py
index d411626..56a302f 100644
--- a/cekit/test/behave_tester.py
+++ b/cekit/test/behave_tester.py
@@ -33,6 +33,7 @@ class BehaveTester(Command):
             self.params.target,
             "Dockerfile",
             self.params.overrides,
+            False,
         )
 
         # Handle dependencies for selected generator, if any
diff --git a/tests/test_builder.py b/tests/test_builder.py
index 31bfc41..d264353 100644
--- a/tests/test_builder.py
+++ b/tests/test_builder.py
@@ -224,14 +224,14 @@ def test_osbs_builder_run_brew_stage(mocker):
         [
             call(
                 ["git", "config", "--get", "remote.origin.url"],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
             ),
             call(
                 ["git", "rev-parse", "HEAD"],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
@@ -245,7 +245,7 @@ def test_osbs_builder_run_brew_stage(mocker):
                     "--kwargs",
                     "{'src': 'git+https://something.redhat.com/git/containers/openjdk#c5a0731b558c8a247dd7f85b5f54462cd5b68b23', 'target': 'some-branch-containers-candidate', 'opts': {'scratch': True, 'git_branch': 'some-branch', 'yum_repourls': []}}",
                 ],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
@@ -283,14 +283,14 @@ def test_osbs_builder_run_brew(mocker):
         [
             call(
                 ["git", "config", "--get", "remote.origin.url"],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
             ),
             call(
                 ["git", "rev-parse", "HEAD"],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
@@ -304,7 +304,7 @@ def test_osbs_builder_run_brew(mocker):
                     "--kwargs",
                     "{'src': 'git+https://something.redhat.com/git/containers/openjdk#c5a0731b558c8a247dd7f85b5f54462cd5b68b23', 'target': 'some-branch-containers-candidate', 'opts': {'scratch': True, 'git_branch': 'some-branch', 'yum_repourls': []}}",
                 ],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
@@ -342,14 +342,14 @@ def test_osbs_builder_run_koji(mocker):
         [
             call(
                 ["git", "config", "--get", "remote.origin.url"],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
             ),
             call(
                 ["git", "rev-parse", "HEAD"],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
@@ -363,7 +363,7 @@ def test_osbs_builder_run_koji(mocker):
                     "--kwargs",
                     "{'src': 'git+https://something.redhat.com/git/containers/openjdk#c5a0731b558c8a247dd7f85b5f54462cd5b68b23', 'target': 'some-branch-containers-candidate', 'opts': {'scratch': True, 'git_branch': 'some-branch', 'yum_repourls': []}}",
                 ],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
@@ -435,7 +435,7 @@ def test_osbs_builder_run_brew_user(mocker):
             "--kwargs",
             "{'src': 'git+https://something.redhat.com/git/containers/openjdk#c5a0731b558c8a247dd7f85b5f54462cd5b68b23', 'target': 'some-branch-containers-candidate', 'opts': {'scratch': True, 'git_branch': 'some-branch', 'yum_repourls': []}}",
         ],
-        stderr=-1,
+        stderr=-2,
         stdout=-1,
         check=True,
         universal_newlines=True,
@@ -476,7 +476,7 @@ def test_osbs_builder_run_brew_target_defined_in_descriptor(mocker):
             "--kwargs",
             "{'src': 'git+https://something.redhat.com/git/containers/openjdk#c5a0731b558c8a247dd7f85b5f54462cd5b68b23', 'target': 'some-target', 'opts': {'scratch': True, 'git_branch': 'some-branch', 'yum_repourls': []}}",
         ],
-        stderr=-1,
+        stderr=-2,
         stdout=-1,
         check=True,
         universal_newlines=True,
@@ -514,7 +514,7 @@ def test_osbs_wait_for_osbs_task_finished_successfully(mocker):
 
     run.assert_called_with(
         ["brew", "call", "--json-output", "getTaskInfo", "12345"],
-        stderr=-1,
+        stderr=-2,
         stdout=-1,
         check=True,
         universal_newlines=True,
@@ -570,14 +570,14 @@ def test_osbs_wait_for_osbs_task_in_progress(mocker):
         [
             call(
                 ["brew", "call", "--json-output", "getTaskInfo", "12345"],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
             ),
             call(
                 ["brew", "call", "--json-output", "getTaskInfo", "12345"],
-                stderr=-1,
+                stderr=-2,
                 stdout=-1,
                 check=True,
                 universal_newlines=True,
@@ -622,7 +622,7 @@ def test_osbs_wait_for_osbs_task_failed(mocker):
 
     run.assert_called_with(
         ["brew", "call", "--json-output", "getTaskInfo", "12345"],
-        stderr=-1,
+        stderr=-2,
         stdout=-1,
         check=True,
         universal_newlines=True,
@@ -1046,7 +1046,7 @@ def test_podman_builder_run_with_generator(mocker):
     params = Map({"tags": []})
     run = mocker.patch.object(subprocess, "run")
     builder = create_builder_object(mocker, "podman", params)
-    builder.generator = DockerGenerator("", "", "", {})
+    builder.generator = DockerGenerator("", "", "", {}, False)
     builder.generator.image = Image(
         yaml.safe_load(
             """
@@ -1088,7 +1088,7 @@ def test_buildah_builder_run_with_generator(mocker):
     params = Map({"tags": []})
     run = mocker.patch.object(subprocess, "run")
     builder = create_builder_object(mocker, "buildah", params)
-    builder.generator = DockerGenerator("", "", "", {})
+    builder.generator = DockerGenerator("", "", "", {}, False)
     builder.generator.image = Image(
         yaml.safe_load(
             """
@@ -1177,7 +1177,7 @@ def test_podman_builder_with_squashing_disabled(mocker):
     params = {"no_squash": True}
     run = mocker.patch.object(subprocess, "run")
     builder = create_builder_object(mocker, "podman", params)
-    builder.generator = DockerGenerator("", "", "", [])
+    builder.generator = DockerGenerator("", "", "", [], True)
     builder.generator.image = Image(
         yaml.safe_load(
             """
@@ -1210,7 +1210,7 @@ def test_podman_builder_with_build_arg(mocker):
     params = {"build_args": ["KEY=VALUE"]}
     run = mocker.patch.object(subprocess, "run")
     builder = create_builder_object(mocker, "podman", params)
-    builder.generator = DockerGenerator("", "", "", [])
+    builder.generator = DockerGenerator("", "", "", [], False)
     builder.generator.image = Image(
         yaml.safe_load(
             """
@@ -1245,7 +1245,7 @@ def test_podman_builder_with_build_flag(mocker):
     params = {"build_flag": ["--compress"]}
     run = mocker.patch.object(subprocess, "run")
     builder = create_builder_object(mocker, "podman", params)
-    builder.generator = DockerGenerator("", "", "", [])
+    builder.generator = DockerGenerator("", "", "", [], False)
     builder.generator.image = Image(
         yaml.safe_load(
             """
diff --git a/tests/test_dockerfile.py b/tests/test_dockerfile.py
index 3aa81a2..85ea637 100644
--- a/tests/test_dockerfile.py
+++ b/tests/test_dockerfile.py
@@ -169,7 +169,7 @@ def test_dockerfile_docker_odcs_rpm_microdnf(tmpdir, mocker):
     )
     regex_dockerfile(
         target,
-        "RUN microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y a b",
+        "&& microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y a b",
     )
     regex_dockerfile(target, "rpm -q a b")
 
@@ -197,7 +197,7 @@ def test_dockerfile_docker_odcs_rpm_microdnf_custom_flag_1(tmpdir, mocker):
         desc_part,
     )
     regex_dockerfile(target, "RUN microdnf  install -y foo-repo.rpm")
-    regex_dockerfile(target, "RUN microdnf  install -y a b")
+    regex_dockerfile(target, "&& microdnf  install -y a b")
     regex_dockerfile(target, "rpm -q a b")
 
 
@@ -226,7 +226,7 @@ def test_dockerfile_docker_odcs_rpm_microdnf_custom_flag_2(tmpdir, mocker):
     regex_dockerfile(
         target, "RUN microdnf --setopt=tsflags=nodocs install -y foo-repo.rpm"
     )
-    regex_dockerfile(target, "RUN microdnf --setopt=tsflags=nodocs install -y a b")
+    regex_dockerfile(target, "&& microdnf --setopt=tsflags=nodocs install -y a b")
     regex_dockerfile(target, "rpm -q a b")
 
 
@@ -412,7 +412,7 @@ def test_default_package_manager(tmpdir):
     )
 
     regex_dockerfile(target, "RUN yum --setopt=tsflags=nodocs install -y foo-repo.rpm")
-    regex_dockerfile(target, "RUN yum --setopt=tsflags=nodocs install -y a")
+    regex_dockerfile(target, "&& yum --setopt=tsflags=nodocs install -y a")
     regex_dockerfile(target, "rpm -q a")
 
 
@@ -447,7 +447,7 @@ def test_dockerfile_custom_package_manager_with_overrides(tmpdir):
     )
     regex_dockerfile(
         target,
-        "RUN microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y a b",
+        "&& microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y a b",
     )
     regex_dockerfile(target, "rpm -q a")
 
@@ -478,7 +478,7 @@ def test_dockerfile_custom_package_manager_with_overrides_overriden_again(tmpdir
         },
     )
     regex_dockerfile(target, "RUN dnf --setopt=tsflags=nodocs install -y foo-repo.rpm")
-    regex_dockerfile(target, "RUN dnf --setopt=tsflags=nodocs install -y a b")
+    regex_dockerfile(target, "&& dnf --setopt=tsflags=nodocs install -y a b")
     regex_dockerfile(target, "rpm -q a")
 
 
@@ -504,7 +504,7 @@ def test_dockerfile_osbs_odcs_rpm_microdnf(tmpdir):
     )
     regex_dockerfile(
         target,
-        "RUN microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y a",
+        "&& microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y a",
     )
     regex_dockerfile(target, "rpm -q a")
 
@@ -530,7 +530,7 @@ def test_supported_package_managers(tmpdir, manager):
     if "microdnf" in manager:
         flags = "--setopt=install_weak_deps=0 " + flags
     regex_dockerfile(target, f"RUN {manager} {flags} install -y foo-repo.rpm")
-    regex_dockerfile(target, f"RUN {manager} {flags} install -y a")
+    regex_dockerfile(target, f"&& {manager} {flags} install -y a")
     regex_dockerfile(target, "rpm -q a")
 
 
@@ -548,7 +548,7 @@ def test_supported_package_managers_apk(tmpdir, caplog):
             }
         },
     )
-    regex_dockerfile(target, "RUN apk  add a")
+    regex_dockerfile(target, "&& apk  add a")
     regex_dockerfile(target, "apk info -e a")
     assert (
         "Package manager apk does not support defining repositories, skipping all repositories"
@@ -572,7 +572,7 @@ def test_supported_package_managers_apt(tmpdir, caplog):
     )
     regex_dockerfile(
         target,
-        "RUN apt-get update && apt-get --no-install-recommends install -y a b=1.0.0",
+        "&& apt-get update && apt-get --no-install-recommends install -y a b=1.0.0",
     )
     regex_dockerfile(target, "dpkg-query --list a b")
     assert (
@@ -788,18 +788,20 @@ def test_package_removal_and_install_and_reinstall(tmpdir):
     assert (
         """# Switch to 'root' user for package management for 'testimage' image defined packages
         USER root
+        RUN : \\
         # Remove packages defined in the 'testimage' image
-        RUN dnf --setopt=tsflags=nodocs remove -y b
+        && dnf --setopt=tsflags=nodocs remove -y b \\
         # Install packages defined in the 'testimage' image
-        RUN dnf --setopt=tsflags=nodocs install -y a \\
-            && rpm -q a
+        && dnf --setopt=tsflags=nodocs install -y a \\
+            && rpm -q a \\
         # Reinstall packages defined in the 'testimage' image
-        RUN dnf --setopt=tsflags=nodocs reinstall -y t \\
+        && dnf --setopt=tsflags=nodocs reinstall -y t \\
             && rpm -q t"""
         in dockerfile
     )
     assert (
-        """RUN rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf"
+        """RUN dnf clean all \\
+    && rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf"
     """
         in dockerfile
     )
@@ -946,6 +948,7 @@ def regex_dockerfile(image_dir, exp_regex, container_file="Dockerfile"):
     with open(os.path.join(image_dir, "target", "image", container_file), "r") as fd:
         dockerfile_content = fd.read()
         regex = re.compile(exp_regex, re.MULTILINE)
+        print(f"Searching for {regex} in {dockerfile_content}")
         assert regex.search(dockerfile_content) is not None
 
 
diff --git a/tests/test_integ_addhelp.py b/tests/test_integ_addhelp.py
index a7e79f8..6a1f61d 100644
--- a/tests/test_integ_addhelp.py
+++ b/tests/test_integ_addhelp.py
@@ -157,3 +157,108 @@ And more text"""
         )
     assert os.path.exists(os.path.join(tmpdir, "target", "image", "help.md"))
     assert check_file_text(tmpdir, "Markdown Heading", "help.md") is True
+
+
+def test_should_generate_help_if_enabled_in_overrides(tmpdir):
+    """Uses default template"""
+    tmpdir = str(tmpdir)
+    my_image_descriptor = image_descriptor.copy()
+    overrides_descriptor = {"schema_version": 1, "help": {"add": True}}
+
+    with open(os.path.join(str(tmpdir), "overrides.yaml"), "w") as fd:
+        yaml.dump(overrides_descriptor, fd, default_flow_style=False)
+
+    print(
+        run_cekit(
+            tmpdir,
+            [
+                "-v",
+                "build",
+                "--overrides-file",
+                "overrides.yaml",
+                "--dry-run",
+                "docker",
+            ],
+            descriptor=my_image_descriptor,
+        ).output
+    )
+    assert os.path.exists(os.path.join(tmpdir, "target", "image", "help.md"))
+    assert check_file_text(tmpdir, "# `test/image:1.0`", "help.md") is True
+    assert (
+        check_file_text(tmpdir, "Container will run as `root` user.", "help.md") is True
+    )
+    assert check_file_text(tmpdir, "There are no defined ports.", "help.md") is True
+    assert check_file_text(tmpdir, "There are no volumes defined.", "help.md") is True
+    assert (
+        check_file_text(
+            tmpdir, "This image is based on the `centos:7` image.", "help.md"
+        )
+        is True
+    )
+    assert (
+        check_file_text(
+            tmpdir, "There is no entrypoint specified for the container.", "help.md"
+        )
+        is True
+    )
+
+
+@pytest.mark.parametrize("template_str", [template_teststr_1, template_teststr_2])
+@pytest.mark.parametrize("path_type", ["absolute", "relative"])
+def test_custom_help_template_path_with_modules(tmpdir, path_type, template_str):
+    module_dir = tmpdir / "modules"
+    module_dir.mkdir()
+
+    help_template_file = module_dir / "help.jinja"
+    help_template_file.write_text(template_str, encoding="utf-8")
+    help_template = str(help_template_file)
+
+    if path_type == "relative":
+        help_template = "help.jinja"
+
+    my_image_descriptor = image_descriptor.copy()
+    my_image_descriptor.update(
+        {
+            "modules": {
+                "repositories": [{"name": "modules", "path": "modules/"}],
+                "install": [{"name": "test_module"}],
+            }
+        }
+    )
+
+    module_descriptor_file = module_dir / "module.yaml"
+    module_descriptor_file.write_text(
+        yaml.dump(
+            {
+                "name": "test_module",
+                "version": "1.0",
+                "help": {"template": help_template, "add": True},
+            }
+        ),
+        encoding="utf-8",
+    )
+
+    with Chdir(str(tmpdir)):
+        with open("image.yaml", "w") as fd:
+            yaml.dump(my_image_descriptor, fd, default_flow_style=False)
+
+        result = CliRunner().invoke(
+            cli, ["-v", "build", "--dry-run", "docker"], catch_exceptions=False
+        )
+        sys.stdout.write("\n")
+        sys.stdout.write(result.output)
+
+        with open("target/image/help.md", "r") as fd:
+            contents = fd.read()
+            if (
+                "This string does not occur in the default help.md template"
+                in template_str
+            ):
+                assert template_str in contents
+            else:
+                assert (
+                    """
+centos:7
+A simple template with a substitution."""
+                    in contents
+                )
diff --git a/tests/test_integ_podman.py b/tests/test_integ_podman.py
index e319c98..bf529e7 100644
--- a/tests/test_integ_podman.py
+++ b/tests/test_integ_podman.py
@@ -2,30 +2,37 @@
 
 from __future__ import print_function
 
+import logging
 import os
 import shutil
+import sys
 
 from _pytest.capture import CaptureResult
 from click.testing import CliRunner
 
 from cekit.cli import cli
 from cekit.tools import Chdir
+from cekit.version import __version__
 
 
 def run_cekit(image_dir, args=None, env=None):
     if args is None:
-        args = ["build", "podman"]
+        args = ["-v", "build", "podman"]
 
     if env is None:
         env = {}
 
     with Chdir(image_dir):
+        print(f"Using image_dir={image_dir}")
         result = CliRunner(env=env).invoke(cli, args, catch_exceptions=False)
+        sys.stdout.write("\n")
+        sys.stdout.write(result.output)
         assert result.exit_code == 0
         return result
 
 
-def test_podman_builder_with_alpine_image(tmpdir):
+def test_podman_builder_with_alpine_image(tmpdir, caplog):
+    caplog.set_level(logging.DEBUG, logger="cekit")
     tmpdir = str(tmpdir)
 
     shutil.copytree(
@@ -37,6 +44,83 @@ def test_podman_builder_with_alpine_image(tmpdir):
     # See: https://bugzilla.redhat.com/show_bug.cgi?id=1746022
     run_cekit(os.path.join(tmpdir, "alpine"), env={"BUILDAH_LAYERS": "false"})
 
+    print(f"#### Caplog {caplog.text} ####")
+    assert (
+        """STEP 11/12: RUN :     && rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf"
+"""
+        in caplog.text
+    )
+
+
+def test_podman_builder_with_alpine_image_and_no_squash(tmpdir, caplog):
+    caplog.set_level(logging.DEBUG, logger="cekit")
+    tmpdir = str(tmpdir)
+
+    shutil.copytree(
+        os.path.join(os.path.dirname(__file__), "images", "alpine"),
+        os.path.join(tmpdir, "alpine"),
+    )
+
+    # The 'BUILDAH_LAYERS' environment variable is required to not cache intermediate layers
+    # See: https://bugzilla.redhat.com/show_bug.cgi?id=1746022
+    run_cekit(
+        os.path.join(tmpdir, "alpine"),
+        env={"BUILDAH_LAYERS": "false"},
+        args=["-v", "build", "podman", "--no-squash"],
+    )
+    assert (
+        """STEP 5/11: RUN :         && apk  add python3             && apk info -e python3         && :         && rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf"         && :"""
+        in caplog.text
+    )
+
+    with open(
+        os.path.join(str(tmpdir), "alpine/target/image/Containerfile"), "r"
+    ) as _file:
+        containerFile = _file.read()
+        assert (
+            """###### START module 'app:1.0'
+###### \\
+        # Copy 'app' module content
+        COPY modules/app /tmp/scripts/app
+        # Switch to 'root' user for package management for 'app' module defined packages
+        USER root
+        RUN : \\
+        # Install packages defined in the 'app' module
+        && apk  add python3 \\
+            && apk info -e python3 \\
+        # Clear package manager metadata
+        && : \\
+        && rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf" \\
+        && :
+
+        # Custom scripts from 'app' module
+        USER root
+        RUN [ "sh", "-x", "/tmp/scripts/app/install.sh" ]
+###### /
+###### END module 'app:1.0'
+
+###### START image 'cekit-alpine:1.0.0'
+###### \\
+        # Set 'cekit-alpine' image defined labels
+        LABEL \\
+            description="Some Alpine image" \\
+            io.cekit.version="VVVVV" \\
+            summary="Some Alpine image"
+###### /
+###### END image 'cekit-alpine:1.0.0'
+
+
+
+    # Switch to 'root' user and remove artifacts and modules
+    USER root
+    RUN rm -rf "/tmp/scripts" "/tmp/artifacts"
+    # Define the user
+    USER root""".replace(
+                "VVVVV", __version__
+            )
+            in containerFile
+        )
+
 
 def test_podman_from_scratch(tmpdir):
     tmpdir = str(tmpdir)
diff --git a/tests/test_unit_generator_docker.py b/tests/test_unit_generator_docker.py
index 9311103..72b6229 100644
--- a/tests/test_unit_generator_docker.py
+++ b/tests/test_unit_generator_docker.py
@@ -73,7 +73,7 @@ def docker_generator(tmpdir, overrides=None):
 
     image_dir = str(tmpdir)
     target_dir = os.path.join(image_dir, "target")
-    yield DockerGenerator(image_dir, target_dir, "", overrides)
+    yield DockerGenerator(image_dir, target_dir, "", overrides, False)
 
 
 def setup_function():
diff --git a/tests/test_unit_tools.py b/tests/test_unit_tools.py
index 0a80de9..b20d0eb 100644
--- a/tests/test_unit_tools.py
+++ b/tests/test_unit_tools.py
@@ -78,8 +78,6 @@ def test_merging_description_osbs():
     yaml.SafeDumper.add_multi_representer(Descriptor, Descriptor.to_yaml)
     desc1 = Osbs({"extra_dir_target": "foo"}, None)
     desc2 = Osbs({"repository": {"branch": "branch", "name": "repo"}}, None)
-    #    print("\n### Output is:\n" + yaml.dump(merged, None, default_flow_style=False, Dumper=yaml.SafeDumper))
-
     merged = _merge_descriptors(desc1, desc2)
     assert (
         yaml.dump(merged, None, default_flow_style=False, Dumper=yaml.SafeDumper)
@@ -937,8 +935,8 @@ def test_run_wrapper_capture_error(tmpdir) -> None:
         result = run_wrapper(
             ["git", "rev-parse", "--is-inside-work-tree"], True, check=False
         )
-        assert result.stdout == ""
-        assert not result.stderr.endswith("\n")
+        assert result.stderr is None
+        assert not result.stdout.endswith("\n")
         assert result.returncode == 128
 
 
diff --git a/tests/test_validate.py b/tests/test_validate.py
index ee945c6..e631102 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -533,7 +533,7 @@ def test_override_add_module_and_packages_in_overrides(tmpdir):
 
     assert check_dockerfile(
         image_dir,
-        "RUN yum --setopt=tsflags=nodocs install -y package1 package2 \\",
+        "&& yum --setopt=tsflags=nodocs install -y package1 package2 \\",
         "Containerfile",
     )
     assert check_dockerfile(
@@ -574,8 +574,9 @@ def test_microdnf_clean_all_cmd_present(tmpdir):
     )
 
     required_matches = [
-        "RUN microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y package1 package2 \\",
-        "&& microdnf clean all && rpm -q package1 package2",
+        "&& microdnf --setopt=install_weak_deps=0 --setopt=tsflags=nodocs install -y package1 package2 \\",
+        "RUN microdnf clean all \\",
+        '&& rm -rf "/var/cache/yum" "/var/lib/dnf" "/var/cache/apt" "/var/cache/dnf"',
     ]
 
     for match in required_matches:
@@ -1206,7 +1207,7 @@ def test_override_modules_flat(tmpdir, mocker):
     run_cekit(image_dir)
 
     assert check_dockerfile_text(image_dir, 'foo="mod_2"')
-    assert not check_dockerfile_text(image_dir, "RUN yum clean all")
+    assert not check_dockerfile_text(image_dir, "&& yum clean all")
 
 
 def test_execution_order_flat(tmpdir, mocker):
@@ -1293,7 +1294,7 @@ def test_execution_order_flat(tmpdir, mocker):
 ###### END module 'mod_4:1.0'
 """
     assert check_dockerfile_text(image_dir, expected_modules_order)
-    assert not check_dockerfile_text(image_dir, "RUN yum clean all")
+    assert not check_dockerfile_text(image_dir, "&& yum clean all")
 
 
 def test_package_related_commands_packages_in_module(tmpdir, mocker):
@@ -1319,9 +1320,12 @@ def test_package_related_commands_packages_in_module(tmpdir, mocker):
 ###### \\
         # Switch to 'root' user for package management for 'packages_module' module defined packages
         USER root
+        RUN : \\
         # Install packages defined in the 'packages_module' module
-        RUN yum --setopt=tsflags=nodocs install -y kernel java-1.8.0-openjdk \\
-            && rpm -q kernel java-1.8.0-openjdk
+        && yum --setopt=tsflags=nodocs install -y kernel java-1.8.0-openjdk \\
+            && rpm -q kernel java-1.8.0-openjdk \\
+        && :
+
 ###### /
 ###### END module 'packages_module:1.0'
 
@@ -1329,9 +1333,12 @@ def test_package_related_commands_packages_in_module(tmpdir, mocker):
 ###### \\
         # Switch to 'root' user for package management for 'packages_module_1' module defined packages
         USER root
+        RUN : \\
         # Install packages defined in the 'packages_module_1' module
-        RUN yum --setopt=tsflags=nodocs install -y wget mc \\
-            && rpm -q wget mc
+        && yum --setopt=tsflags=nodocs install -y wget mc \\
+            && rpm -q wget mc \\
+        && :
+
 ###### /
 ###### END module 'packages_module_1:1.0'
 """
@@ -1364,10 +1371,10 @@ def test_package_related_commands_packages_in_image(tmpdir, caplog):
 
     expected_packages_install = """
         USER root
+        RUN : \\
         # Install packages defined in the 'test/image' image
-        RUN yum --setopt=tsflags=nodocs install -y wget mc \\
-            && rpm -q wget mc
-"""
+        && yum --setopt=tsflags=nodocs install -y wget mc \\
+            && rpm -q wget mc"""
 
     assert (
         "Required CEKit library 'odcs-client' was found as a 'odcs' module!"