Repository: cekit/cekit

Issue:
When installing packages with --no-squash the cache and lists increases the size of final images
**Describe the solution you'd like**
Currently when you install packages it doesn't clean up the cache and lists after which leaves them left in the layers. They are cleaned up at the end with `apt-get clean` but without squashing this leaves the final image larger. Even with this increasing the build time a little due to no cache for `apt-get update` this will remove around 20MB from final images with a small change. 

**Additional context**
This is best practice from docker and reduces the foot guns when using the packages feature.
https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

Happy to look at adding a PR to move the clean up to after each package install if that fits with the project.

Hints from issue comments:
@rnc: Normally we would recommend using the squash functionality - is there a reason you didn't want to?

When using multiple modules a lot of layers can be created due to the included RUN commands - hence the tool defaults to squashing. 

While package 'management' code _could_ be moved closer together for clarity I am not sure how much this might affect overall build times especially with images that have a large number of modules.

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/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.


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_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..569425b 100644
--- a/tests/test_unit_tools.py
+++ b/tests/test_unit_tools.py
@@ -937,8 +937,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!"