Repository: cekit/cekit

Issue:
Support disabling color in logs
**Describe the solution you'd like**

This would be useful for recording logs that don't support the colour codes. 
Internal ref : CPAAS-1520

**Additional context**
We should look into supporting https://no-color.org/

Discussion:

Patch:
diff --git a/cekit/cli.py b/cekit/cli.py
index 7ce327f..4878825 100644
--- a/cekit/cli.py
+++ b/cekit/cli.py
@@ -13,22 +13,22 @@ from cekit.log import setup_logging
 from cekit.tools import Map
 from cekit.version import __version__
 
-# FIXME we should try to move this to json
-setup_logging()
 LOGGER = logging.getLogger('cekit')
 CONFIG = Config()
 
 default_work_dir = "~/.cekit"
 
+
 @click.group(context_settings=dict(max_content_width=100))
 @click.option('--descriptor', metavar="PATH", help="Path to image descriptor file.", default="image.yaml", show_default=True)
 @click.option('-v', '--verbose', help="Enable verbose output.", is_flag=True)
+@click.option('--nocolor', help="Disable color output.", is_flag=True)
 @click.option('--work-dir', metavar="PATH", help="Location of the working directory.", default=default_work_dir, show_default=True)
 @click.option('--config', metavar="PATH", help="Path to configuration file.", default=default_work_dir + "/config", show_default=True)
 @click.option('--redhat', help="Set default options for Red Hat internal infrastructure.", is_flag=True)
 @click.option('--target', metavar="PATH", help="Path to directory where files should be generated", default="target", show_default=True)
 @click.version_option(message="%(version)s", version=__version__)
-def cli(descriptor, verbose, work_dir, config, redhat, target):  # pylint: disable=unused-argument,too-many-arguments
+def cli(descriptor, verbose, nocolor, work_dir, config, redhat, target):  # pylint: disable=unused-argument,too-many-arguments
     """
     ABOUT
 
@@ -285,6 +285,10 @@ class Cekit(object):
 
     def init(self):
         """ Initialize logging """
+        if self.params.nocolor:
+            setup_logging(False)
+        else:
+            setup_logging()
         if self.params.verbose:
             LOGGER.setLevel(logging.DEBUG)
         else:
diff --git a/cekit/log.py b/cekit/log.py
index 2ab3d8d..3bc2769 100644
--- a/cekit/log.py
+++ b/cekit/log.py
@@ -1,4 +1,5 @@
 import logging
+import os
 import sys
 
 import colorlog
@@ -18,15 +19,19 @@ class SingleLevelFilter(logging.Filter):
             return record.levelno <= self.passlevel
 
 
-def setup_logging():
+def setup_logging(color=True):
     handler_out = logging.StreamHandler(sys.stdout)
     handler_err = logging.StreamHandler(sys.stderr)
 
     handler_out.addFilter(SingleLevelFilter(logging.INFO, False))
     handler_err.addFilter(SingleLevelFilter(logging.INFO, True))
 
-    formatter = colorlog.ColoredFormatter(
-        '%(log_color)s%(asctime)s %(filename)s:%(lineno)-10s %(levelname)-5s %(message)s')
+    if not color or os.environ.get('NO_COLOR'):
+        formatter = logging.Formatter(
+            '%(asctime)s %(filename)s:%(lineno)-10s %(levelname)-5s %(message)s')
+    else:
+        formatter = colorlog.ColoredFormatter(
+            '%(log_color)s%(asctime)s %(filename)s:%(lineno)-10s %(levelname)-5s %(message)s')
 
     handler_out.setFormatter(formatter)
     handler_err.setFormatter(formatter)
diff --git a/docs/descriptor/includes/packages.rst b/docs/descriptor/includes/packages.rst
index 07f5349..c2929ae 100644
--- a/docs/descriptor/includes/packages.rst
+++ b/docs/descriptor/includes/packages.rst
@@ -153,7 +153,7 @@ RPM repository
 .. note::
     Available only on RPM-based distributions.
 
-This ways is using repository configuration files and related keys packaged as an RPM.
+This way is using repository configuration files and related keys packaged as an RPM.
 
 **Example**: To enable `CentOS SCL <https://wiki.centos.org/AdditionalResources/Repositories/SCL>`_ inside the
 image you should define repository in a following way:
@@ -175,6 +175,10 @@ image you should define repository in a following way:
                 - name: epel
                   rpm: https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
 
+.. warning::
+    Images with `microdnf` do not not support installing from a remote URL. To work around this, the current recommended option is
+    to use a module and use ``rpm -i`` to enable the new repo.
+
 URL repository
 *******************
 
diff --git a/docs/handbook/installation/instructions.rst b/docs/handbook/installation/instructions.rst
index 94e44ba..80028b7 100644
--- a/docs/handbook/installation/instructions.rst
+++ b/docs/handbook/installation/instructions.rst
@@ -10,9 +10,6 @@ CEKit installation on other platforms is still possible via ``pip``.
 RPM packages are distributed via regular repositories in case of Fedora
 and the EPEL repository for CentOS/RHEL.
 
-.. warning::
-    Currently packaged version is a snapshot release of the upcoming CEKit 3.0.
-
 .. tip::
     You can see latest submitted package updates `submitted in Bodhi <https://bodhi.fedoraproject.org/updates/?packages=cekit>`_.
 


Test patch:
diff --git a/tests/test_dockerfile.py b/tests/test_dockerfile.py
index 919fc8e..8234c5b 100644
--- a/tests/test_dockerfile.py
+++ b/tests/test_dockerfile.py
@@ -1,5 +1,6 @@
 import os
 import re
+import sys
 
 import pytest
 import yaml
@@ -256,7 +257,7 @@ def test_unsupported_package_manager(tmpdir, caplog):
 def test_default_package_manager(tmpdir):
     target = str(tmpdir.mkdir('target'))
 
-    generate(target, ['-v', 'build', '--dry-run', 'podman'],
+    generate(target, ['--nocolor', '-v', 'build', '--dry-run', 'podman'],
              descriptor={'packages': {
                  'repositories': [{'name': 'foo',
                                    'rpm': 'foo-repo.rpm'}],
@@ -484,6 +485,8 @@ def generate(image_dir, command, descriptor=None, exit_code=0):
 
     with Chdir(image_dir):
         result = CliRunner().invoke(cli, command, catch_exceptions=False)
+        sys.stdout.write("\n")
+        sys.stdout.write(result.output)
 
         assert result.exit_code == exit_code
 
diff --git a/tests/test_integ_addhelp.py b/tests/test_integ_addhelp.py
index 8f9eda2..ccd7ade 100644
--- a/tests/test_integ_addhelp.py
+++ b/tests/test_integ_addhelp.py
@@ -3,6 +3,8 @@
 from __future__ import print_function
 
 import os
+import sys
+
 import yaml
 import pytest
 
@@ -44,6 +46,9 @@ def run_cekit(image_dir, args=None, descriptor=None):
             yaml.dump(descriptor, fd, default_flow_style=False)
 
         result = CliRunner().invoke(cli, args, catch_exceptions=False)
+        sys.stdout.write("\n")
+        sys.stdout.write(result.output)
+
         assert result.exit_code == 0
         return result
 
@@ -67,7 +72,9 @@ def test_custom_help_template_path(tmpdir, path_type):
         with open('image.yaml', 'w') as fd:
             yaml.dump(my_image_descriptor, fd, default_flow_style=False)
 
-        CliRunner().invoke(cli, ['-v', 'build', '--dry-run', 'docker'], catch_exceptions=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()
diff --git a/tests/test_integ_builder_osbs.py b/tests/test_integ_builder_osbs.py
index c43c985..ee47e05 100644
--- a/tests/test_integ_builder_osbs.py
+++ b/tests/test_integ_builder_osbs.py
@@ -7,6 +7,8 @@ import os
 import re
 import shutil
 import subprocess
+import sys
+
 import yaml
 
 import pytest
@@ -24,8 +26,6 @@ config = Config()
 def reset_config():
     config.cfg['common'] = {}
 
-
-config = Config()
 config.cfg['common'] = {'redhat': True}
 
 image_descriptor = {
@@ -48,6 +48,8 @@ def run_cekit(cwd,
               message=None, return_code=0):
     with Chdir(cwd):
         result = CliRunner().invoke(cli, parameters, catch_exceptions=False)
+        sys.stdout.write("\n")
+        sys.stdout.write(result.output)
 
         assert result.exit_code == return_code
 
diff --git a/tests/test_integ_test_behave.py b/tests/test_integ_test_behave.py
index 0fa2042..bec376f 100644
--- a/tests/test_integ_test_behave.py
+++ b/tests/test_integ_test_behave.py
@@ -2,6 +2,7 @@
 
 import os
 import shutil
+import sys
 import tempfile
 import platform
 import pytest
@@ -42,6 +43,9 @@ def fixture_build_image():
         with Chdir(image_dir):
             result = CliRunner().invoke(cli, args, catch_exceptions=False)
 
+        sys.stdout.write("\n")
+        sys.stdout.write(result.output)
+
         assert result.exit_code == 0
 
         return image_dir
@@ -73,7 +77,8 @@ Feature: Basic tests
     with Chdir(test_image_dir):
         result = CliRunner().invoke(cli, ["-v", "test", "behave"], catch_exceptions=False)
 
-    print(result.output)
+    sys.stdout.write("\n")
+    sys.stdout.write(result.output)
 
     assert result.exit_code == 0
     assert "1 feature passed, 0 failed, 0 skipped" in result.output
@@ -110,7 +115,8 @@ Feature: Basic tests
         result = CliRunner().invoke(
             cli, ["-v", "test", "--overrides", overrides, "behave"], catch_exceptions=False)
 
-    print(result.output)
+    sys.stdout.write("\n")
+    sys.stdout.write(result.output)
 
     assert result.exit_code == 0
     assert "1 feature passed, 0 failed, 0 skipped" in result.output
diff --git a/tests/test_unit_args.py b/tests/test_unit_args.py
index 1c774a1..f90b979 100644
--- a/tests/test_unit_args.py
+++ b/tests/test_unit_args.py
@@ -1,4 +1,5 @@
 import importlib
+import sys
 
 import pytest
 from click.testing import CliRunner
@@ -21,7 +22,7 @@ def _get_class_by_name(clazz):
         ['--redhat', 'build', 'docker'],
         'cekit.builders.docker_builder.DockerBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config', 'redhat': True,
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config', 'redhat': True,
             'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'pull': False,
             'no_squash': False, 'tags': ()
         }
@@ -31,7 +32,7 @@ def _get_class_by_name(clazz):
         ['--target', 'custom-target', 'build', 'docker'],
         'cekit.builders.docker_builder.DockerBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config', 'redhat': False,
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config', 'redhat': False,
             'target': 'custom-target', 'validate': False, 'dry_run': False, 'overrides': (), 'pull': False,
             'no_squash': False, 'tags': ()
         }
@@ -41,7 +42,7 @@ def _get_class_by_name(clazz):
         ['--work-dir', 'custom-workdir', 'build', 'docker'],
         'cekit.builders.docker_builder.DockerBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': 'custom-workdir', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': 'custom-workdir', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'pull': False,
             'no_squash': False, 'tags': ()
         }
@@ -51,7 +52,7 @@ def _get_class_by_name(clazz):
         ['--config', 'custom-config', 'build', 'docker'],
         'cekit.builders.docker_builder.DockerBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': 'custom-config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': 'custom-config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (),  'pull': False,
             'no_squash': False, 'tags': ()
         }
@@ -61,7 +62,7 @@ def _get_class_by_name(clazz):
         ['build', 'docker'],
         'cekit.builders.docker_builder.DockerBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'pull': False,
             'no_squash': False, 'tags': ()
         }
@@ -71,7 +72,7 @@ def _get_class_by_name(clazz):
         ['build', '--overrides', 'foo', '--overrides-file', 'bar', 'docker'],
         'cekit.builders.docker_builder.DockerBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': ('foo', 'bar'),
             'pull': False, 'no_squash': False, 'tags': ()
         }
@@ -81,7 +82,7 @@ def _get_class_by_name(clazz):
         ['build', 'osbs'],
         'cekit.builders.osbs.OSBSBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'nowait': False,
             'release': False, 'user': None, 'stage': False, 'sync_only': False, 'commit_message': None, 'assume_yes': False
         }
@@ -91,7 +92,7 @@ def _get_class_by_name(clazz):
         ['build', 'osbs', '--user', 'SOMEUSER'],
         'cekit.builders.osbs.OSBSBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'nowait': False,
             'release': False, 'user': 'SOMEUSER', 'stage': False, 'sync_only': False,
             'commit_message': None, 'assume_yes': False
@@ -102,7 +103,7 @@ def _get_class_by_name(clazz):
         ['build', 'osbs', '--stage'],
         'cekit.builders.osbs.OSBSBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'nowait': False,
             'release': False, 'user': None, 'stage': True, 'sync_only': False, 'commit_message': None, 'assume_yes': False
         }
@@ -112,7 +113,7 @@ def _get_class_by_name(clazz):
         ['build', 'osbs', '--nowait'],
         'cekit.builders.osbs.OSBSBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'nowait': True,
             'release': False, 'user': None, 'stage': False, 'sync_only': False, 'commit_message': None, 'assume_yes': False
         }
@@ -121,7 +122,7 @@ def _get_class_by_name(clazz):
         ['test', '--image', 'image:1.0', 'behave'],
         'cekit.test.behave_tester.BehaveTester',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'overrides': (), 'image': 'image:1.0',
             'steps_url': 'https://github.com/cekit/behave-test-steps.git', 'wip': False, 'names': ()
         }
@@ -130,7 +131,7 @@ def _get_class_by_name(clazz):
         ['build', 'docker', '--pull'],
         'cekit.builders.docker_builder.DockerBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'pull': True,
             'no_squash': False, 'tags': ()
         }
@@ -139,7 +140,7 @@ def _get_class_by_name(clazz):
         ['build', 'osbs'],
         'cekit.builders.osbs.OSBSBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'release': False,
             'user': None, 'nowait': False, 'stage': False, 'sync_only': False, 'commit_message': None, 'assume_yes': False
         }),
@@ -147,7 +148,7 @@ def _get_class_by_name(clazz):
         ['build', 'docker'],
         'cekit.builders.docker_builder.DockerBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'pull': False,
             'no_squash': False, 'tags': ()
         }
@@ -156,7 +157,7 @@ def _get_class_by_name(clazz):
         ['build', 'buildah'],
         'cekit.builders.buildah.BuildahBuilder',
         {
-            'descriptor': 'image.yaml', 'verbose': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
+            'descriptor': 'image.yaml', 'verbose': False, 'nocolor': False, 'work_dir': '~/.cekit', 'config': '~/.cekit/config',
             'redhat': False, 'target': 'target', 'validate': False, 'dry_run': False, 'overrides': (), 'pull': False, 'tags': (), 'no_squash': False
         }
     )
@@ -165,7 +166,9 @@ def test_args_command(mocker, args, clazz, params):
     cekit_class = mocker.patch('cekit.cli.Cekit')
     cekit_object = mocker.Mock()
     cekit_class.return_value = cekit_object
-    CliRunner().invoke(cli, args, catch_exceptions=False)
+    result = CliRunner().invoke(cli, args, catch_exceptions=False)
+    sys.stdout.write("\n")
+    sys.stdout.write(result.output)
 
     cls = _get_class_by_name(clazz)
 
@@ -175,6 +178,8 @@ def test_args_command(mocker, args, clazz, params):
 
 def test_args_not_valid_command():
     result = CliRunner().invoke(cli, ['explode'], catch_exceptions=False)
+    sys.stdout.write("\n")
+    sys.stdout.write(result.output)
 
     assert isinstance(result.exception, SystemExit)
     assert "No such command 'explode'" in result.output
@@ -183,6 +188,8 @@ def test_args_not_valid_command():
 
 def test_args_invalid_build_engine():
     result = CliRunner().invoke(cli, ['build', 'rocketscience'], catch_exceptions=False)
+    sys.stdout.write("\n")
+    sys.stdout.write(result.output)
 
     assert isinstance(result.exception, SystemExit)
     assert "No such command 'rocketscience'" in result.output
diff --git a/tests/test_validate.py b/tests/test_validate.py
index 1fe3e23..49281f4 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -1346,6 +1346,37 @@ def test_validation_of_image_and_module_descriptors(tmpdir, mocker, caplog):
     assert "The --validate parameter was specified, generation will not be performed, exiting" in caplog.text
 
 
+def test_color(tmpdir, mocker, caplog):
+    image_dir = str(tmpdir.mkdir('source'))
+    copy_repos(image_dir)
+
+    with open(os.path.join(image_dir, 'image.yaml'), 'w') as fd:
+        yaml.dump(image_descriptor, fd, default_flow_style=False)
+
+    result = run_cekit(image_dir, ['-v',
+                                   '--nocolor',
+                                   'build',
+                                   '--validate',
+                                   'podman'])
+
+    assert "\033" not in result.output
+
+    result = run_cekit(image_dir, ['-v',
+                                   'build',
+                                   '--validate',
+                                   'podman'])
+
+    assert "\033" in result.output
+
+    result = run_cekit(image_dir, ['-v',
+                                   'build',
+                                   '--validate',
+                                   'podman'],
+                       env={'NO_COLOR': 'TRUE'})
+
+    assert "\033" not in result.output
+
+
 # https://github.com/cekit/cekit/issues/551
 def test_validation_of_image_and_module_descriptors_should_fail_on_invalid_descriptor(tmpdir, mocker, caplog):
     image_dir = str(tmpdir.mkdir('source'))
@@ -1377,6 +1408,9 @@ def run_cekit(cwd, parameters=None, message=None, env=None):
 
     with Chdir(cwd):
         result = CliRunner(env=env).invoke(cli, parameters, catch_exceptions=False)
+        sys.stdout.write("\n")
+        sys.stdout.write(result.output)
+
         assert result.exit_code == 0
 
         if message:
@@ -1392,6 +1426,8 @@ def run_cekit_exception(cwd,
                         message=None):
     with Chdir(cwd):
         result = CliRunner().invoke(cli, parameters, catch_exceptions=False)
+        sys.stdout.write("\n")
+        sys.stdout.write(result.output)
 
         assert isinstance(result.exception, exception)
         assert result.exit_code == exit_code
diff --git a/tests/test_validate_cache.py b/tests/test_validate_cache.py
index 4811f10..917f4cd 100644
--- a/tests/test_validate_cache.py
+++ b/tests/test_validate_cache.py
@@ -1,5 +1,7 @@
 import os
 import re
+import sys
+
 import pytest
 
 from click.testing import CliRunner
@@ -225,6 +227,9 @@ def test_cekit_cache_clear_with_error(mocker, tmpdir):
 
 def run_cekit_cache(args, return_code=0, i=None):
     result = CliRunner().invoke(cli, args, input=i, catch_exceptions=False)
+    sys.stdout.write("\n")
+    sys.stdout.write(result.output)
+
     assert result.exit_code == return_code
 
     return result