Repository: cekit/cekit

Issue:
Update bash and zsh completions for new CLI
Related to #412.

Discussion:

Patch:
diff --git a/.pylintrc b/.pylintrc
index a2c5df1..ad93845 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,5 +1,5 @@
 [MESSAGES CONTROL]
-disable=logging-format-interpolation,logging-not-lazy,useless-object-inheritance
+disable=logging-format-interpolation,logging-not-lazy,useless-object-inheritance,invalid-name,missing-docstring,no-self-use
 
 [FORMAT]
 ignore-long-lines=(^\s*(# )?<?https?://\S+>?$)|(^@click\.)
diff --git a/cekit/cache/artifact.py b/cekit/cache/artifact.py
index 614efc3..ffa2091 100644
--- a/cekit/cache/artifact.py
+++ b/cekit/cache/artifact.py
@@ -5,7 +5,7 @@ import uuid
 import yaml
 
 from cekit.config import Config
-from cekit.crypto import SUPPORTED_HASH_ALGORITHMS
+from cekit.crypto import SUPPORTED_HASH_ALGORITHMS, get_sum
 from cekit.errors import CekitError
 
 CONFIG = Config()
@@ -19,21 +19,21 @@ class ArtifactCache():
     """
 
     def __init__(self):
-        self._cache_dir = os.path.expanduser(
+        self.cache_dir = os.path.expanduser(
             os.path.join(CONFIG.get('common', 'work_dir'), 'cache'))
-        if not os.path.exists(self._cache_dir):
-            os.makedirs(self._cache_dir)
+        if not os.path.exists(self.cache_dir):
+            os.makedirs(self.cache_dir)
 
     def _get_cache(self):
         cache = {}
-        for index_file in glob.glob(os.path.join(self._cache_dir, '*.yaml')):
+        for index_file in glob.glob(os.path.join(self.cache_dir, '*.yaml')):
             with open(index_file, 'r') as file_:
                 cache[os.path.basename(index_file)] = yaml.safe_load(file_)
 
         return cache
 
     def _update_cache(self, cache_entry, artifact_id):
-        index_file = os.path.join(self._cache_dir, artifact_id + '.yaml')
+        index_file = os.path.join(self.cache_dir, artifact_id + '.yaml')
         tmp_cache_file = index_file + str(os.getpid())
         with open(tmp_cache_file, 'w') as file_:
             yaml.safe_dump(cache_entry, file_)
@@ -51,16 +51,16 @@ class ArtifactCache():
 
         artifact_id = str(uuid.uuid4())
 
-        artifact_file = os.path.expanduser(os.path.join(self._cache_dir, artifact_id))
+        artifact_file = os.path.expanduser(os.path.join(self.cache_dir, artifact_id))
         if not os.path.exists(artifact_file):
             artifact.guarded_copy(artifact_file)
 
         cache_entry = {'names': [artifact['name']],
                        'cached_path': artifact_file}
 
+        # We should populate the cache entry with checksums for all supported algorithms
         for alg in SUPPORTED_HASH_ALGORITHMS:
-            if alg in artifact:
-                cache_entry.update({alg: artifact[alg]})
+            cache_entry.update({alg: get_sum(artifact_file, alg)})
 
         self._update_cache(cache_entry, artifact_id)
         return artifact_id
@@ -68,8 +68,8 @@ class ArtifactCache():
     def delete(self, artifact_uuid):
         # check if artifact exists
         self._get_cache()
-        os.remove(os.path.join(self._cache_dir, artifact_uuid))
-        os.remove(os.path.join(self._cache_dir, artifact_uuid + '.yaml'))
+        os.remove(os.path.join(self.cache_dir, artifact_uuid))
+        os.remove(os.path.join(self.cache_dir, artifact_uuid + '.yaml'))
 
     def get(self, artifact):
         for alg in SUPPORTED_HASH_ALGORITHMS:
diff --git a/cekit/cache/cli.py b/cekit/cache/cli.py
index e202e9c..462e30c 100644
--- a/cekit/cache/cli.py
+++ b/cekit/cache/cli.py
@@ -1,4 +1,5 @@
 import logging
+import shutil
 import sys
 
 import click
@@ -12,8 +13,8 @@ from cekit.tools import Map
 from cekit.version import version
 
 setup_logging()
-logger = logging.getLogger('cekit')
-config = Config()
+LOGGER = logging.getLogger('cekit')
+CONFIG = Config()
 
 
 @click.group(context_settings=dict(max_content_width=100))
@@ -21,7 +22,7 @@ config = Config()
 @click.option('--config', metavar="PATH", help="Path to configuration file.", default="~/.cekit/config", show_default=True)
 @click.option('--work-dir', metavar="PATH", help="Location of the working directory.", default="~/.cekit", show_default=True)
 @click.version_option(message="%(version)s", version=version)
-def cli(config, verbose, work_dir):
+def cli(config, verbose, work_dir):  # pylint: disable=unused-argument
     pass
 
 
@@ -35,7 +36,7 @@ def ls():
 @click.option('--md5', metavar="CHECKSUM", help="The md5 checksum of the artifact.")
 @click.option('--sha1', metavar="CHECKSUM", help="The sha1 checksum of the artifact.")
 @click.option('--sha256', metavar="CHECKSUM", help="The sha256 checksum of the artifact.")
-def add(location, md5, sha1, sha256):
+def add(location, md5, sha1, sha256):  # pylint: disable=unused-argument
     if not (md5 or sha1 or sha256):
         raise click.UsageError("At least one checksum must be provided")
 
@@ -48,6 +49,11 @@ def rm(uuid):
     CacheCli.prepare().rm(uuid)
 
 
+@cli.command(name="clear", short_help="Remove all artifacts from the cache")
+def clear():
+    CacheCli.prepare().clear()
+
+
 class CacheCli():
     @staticmethod
     def prepare():
@@ -58,11 +64,11 @@ class CacheCli():
 
         # TODO: logging is used only when adding the artifact, we need to find out if it would be possible to do it better
         if args.verbose:
-            logger.setLevel(logging.DEBUG)
+            LOGGER.setLevel(logging.DEBUG)
         else:
-            logger.setLevel(logging.INFO)
+            LOGGER.setLevel(logging.INFO)
 
-        config.configure(args.config, {'work_dir': args.work_dir})
+        CONFIG.configure(args.config, {'work_dir': args.work_dir})
 
     def add(self, location, md5, sha1, sha256):
         artifact_cache = ArtifactCache()
@@ -90,7 +96,7 @@ class CacheCli():
         try:
             artifact_id = artifact_cache.add(artifact)
             click.echo("Artifact {} cached with UUID '{}'".format(location, artifact_id))
-        except Exception as ex:
+        except Exception as ex:  # pylint: disable=broad-except
             click.secho("Cannot cache artifact {}: {}".format(location, str(ex)), fg='red')
             sys.exit(1)
 
@@ -118,10 +124,29 @@ class CacheCli():
         try:
             artifact_cache.delete(uuid)
             click.echo("Artifact with UUID '{}' removed".format(uuid))
-        except Exception:
+        except Exception:  # pylint: disable=broad-except
             click.secho("Artifact with UUID '{}' doesn't exists in the cache".format(uuid), fg='yellow')
             sys.exit(1)
 
+    def clear(self):
+        """
+        Removes the artifact cache directory with all artifacts.
+
+        Use with caution!
+        """
+        artifact_cache = ArtifactCache()
+
+        if not click.confirm("Are you sure to remove all artifacts from cache?", show_default=True):
+            return
+
+        try:
+            shutil.rmtree(artifact_cache.cache_dir)
+            click.echo("Artifact cache cleared!")
+        except Exception:  # pylint: disable=broad-except
+            click.secho("An error occurred while removing the artifact cache directory '{}'".format(
+                artifact_cache.cache_dir), fg='red')
+            sys.exit(1)
+
 
 if __name__ == "__main__":
     cli()  # pylint: disable=no-value-for-parameter
diff --git a/cekit/crypto.py b/cekit/crypto.py
index 74c2969..ac30590 100644
--- a/cekit/crypto.py
+++ b/cekit/crypto.py
@@ -9,11 +9,13 @@ SUPPORTED_HASH_ALGORITHMS = ['sha256', 'sha1', 'md5']
 def get_sum(target, algorithm):
     hash_function = getattr(hashlib, algorithm)()
 
+    logger.debug("Computing {} checksum for '{}' file".format(algorithm, target))
+
     with open(target, "rb") as f:
         for chunk in iter(lambda: f.read(65536), b""):
             hash_function.update(chunk)
     return hash_function.hexdigest()
-    
+
 
 def check_sum(target, algorithm, expected, name=None):
     """ Check that file chksum is correct
diff --git a/cekit/descriptor/resource.py b/cekit/descriptor/resource.py
index ed76acd..b506057 100644
--- a/cekit/descriptor/resource.py
+++ b/cekit/descriptor/resource.py
@@ -121,7 +121,7 @@ class Resource(Descriptor):
             self._copy_impl(target)
         except Exception as ex:
             logger.warning("Cekit is not able to fetch resource '%s' automatically. "
-                        "Please use cekit-cache command to add this artifact manually." % self.name)
+                           "Please use cekit-cache command to add this artifact manually." % self.name)
 
             if self.description:
                 logger.info(self.description)
@@ -200,12 +200,22 @@ class Resource(Descriptor):
 
             if res.getcode() != 200:
                 raise CekitError("Could not download file from %s" % url)
-            with open(destination, 'wb') as f:
-                while True:
-                    chunk = res.read(1024)
-                    if not chunk:
-                        break
-                    f.write(chunk)
+
+            try:
+                with open(destination, 'wb') as f:
+                    while True:
+                        chunk = res.read(1024)
+                        if not chunk:
+                            break
+                        f.write(chunk)
+            except Exception:
+                try:
+                    logger.debug("Removing incompletely downloaded '{}' file".format(destination))
+                    os.remove(destination)
+                except OSError:
+                    logger.warning("An error occurred while removing file '{}'".format(destination))
+
+                raise
         else:
             raise CekitError("Unsupported URL scheme: %s" % (url))
 
diff --git a/cekit/generator/docker.py b/cekit/generator/docker.py
index 3e14240..9b00e5b 100644
--- a/cekit/generator/docker.py
+++ b/cekit/generator/docker.py
@@ -15,6 +15,8 @@ config = Config()
 
 class DockerGenerator(Generator):
 
+    ODCS_HIDDEN_REPOS_FLAG = 'include_unpublished_pulp_repos'
+
     def __init__(self, descriptor_path, target, overrides):
         super(DockerGenerator, self).__init__(descriptor_path, target, overrides)
         self._fetch_repos = True
@@ -26,33 +28,46 @@ class DockerGenerator(Generator):
         if config.get('common', 'redhat'):
             deps['odcs-client'] = {
                 'package': 'odcs-client',
-                'executable': 'odcs'
+                'executable': '/usr/bin/odcs'
             }
 
             deps['brew'] = {
                 'package': 'brewkoji',
-                'executable': 'brew'
+                'executable': '/usr/bin/brew'
             }
 
         return deps
 
     def _prepare_content_sets(self, content_sets):
+        if not content_sets:
+            return False
+
         if not config.get('common', 'redhat'):
             return False
 
         arch = platform.machine()
+
         if arch not in content_sets:
-            raise CekitError("There are no contet_sets defined for platform '%s'!")
+            raise CekitError("There are no contet_sets defined for platform '{}'!".format(arch))
 
         repos = ' '.join(content_sets[arch])
 
         try:
             # idealy this will be API for ODCS, but there is no python3 package for ODCS
-            cmd = ['odcs']
+            cmd = ['/usr/bin/odcs']
 
             if config.get('common', 'redhat'):
                 cmd.append('--redhat')
-            cmd.extend(['create', 'pulp', repos])
+
+            cmd.append('create')
+
+            compose = self.image.get('osbs', {}).get(
+                'configuration', {}).get('container', {}).get('compose', {})
+
+            if compose.get(DockerGenerator.ODCS_HIDDEN_REPOS_FLAG, False):
+                cmd.extend(['--flag', DockerGenerator.ODCS_HIDDEN_REPOS_FLAG])
+
+            cmd.extend(['pulp', repos])
 
             logger.debug("Creating ODCS content set via '%s'" % " ".join(cmd))
 
diff --git a/cekit/tools.py b/cekit/tools.py
index 3c4b9c2..04a779c 100644
--- a/cekit/tools.py
+++ b/cekit/tools.py
@@ -65,7 +65,7 @@ def decision(question):
 def get_brew_url(md5):
     try:
         LOGGER.debug("Getting brew details for an artifact with '%s' md5 sum" % md5)
-        list_archives_cmd = ['brew', 'call', '--json-output', 'listArchives',
+        list_archives_cmd = ['/usr/bin/brew', 'call', '--json-output', 'listArchives',
                              'checksum=%s' % md5, 'type=maven']
         LOGGER.debug("Executing '%s'." % " ".join(list_archives_cmd))
         archive_yaml = yaml.safe_load(subprocess.check_output(list_archives_cmd))
diff --git a/requirements.txt b/requirements.txt
index e45b8c4..6863ee4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,4 +10,5 @@
 PyYAML>=3.10
 Jinja2>=2.7
 pykwalify>=1.6.0
-colorlog>=2.10.0
\ No newline at end of file
+colorlog>=2.10.0
+click>=6.7
diff --git a/support/completion/bash/cekit b/support/completion/bash/cekit
index 2a17a92..88412e1 100644
--- a/support/completion/bash/cekit
+++ b/support/completion/bash/cekit
@@ -1,90 +1,43 @@
-_cekit_global_options()
-{
-    local options
-    options+='--tags '
-    options+='--overrides '
-    options+='--overrides-file '
-    options+='--target '
-    options+='--descriptor '
-    options+='--work-dir '
-    echo "$options"
-}
-
-_cekit_build_options()
-{
-    local options
-    options+='--build-engine '
-    options+='--build-pull '
-    options+='--build-osbs-release '
-    options+='--build-osbs-user '
-    options+='--build-osbs-nowait '
-    options+='--build-osbs-stage '
-    options+='--build-osbs-target '
-    options+='--build-osbs-commit-msg '
-    options+='--build-tech-preview '
-    echo "$options"
+_cekit_completion() {
+    local IFS=$'
+'
+    COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
+                   COMP_CWORD=$COMP_CWORD \
+                   _CEKIT_COMPLETE=complete $1 ) )
+    return 0
 }
 
+_cekit_completionetup() {
+    local COMPLETION_OPTIONS=""
+    local BASH_VERSION_ARR=(${BASH_VERSION//./ })
+    # Only BASH version 4.4 and later have the nosort option.
+    if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
+        COMPLETION_OPTIONS="-o nosort"
+    fi
 
-_cekit_test_options()
-{
-    local options
-    options+='--test-wip '
-    options+='--test-steps-url '
-    echo "$options"
+    complete $COMPLETION_OPTIONS -F _cekit_completion cekit
 }
 
-_cekit_complete()
-{
-    # save the current world
-    local cur="${COMP_WORDS[COMP_CWORD]}"
-    local prev="${COMP_WORDS[COMP_CWORD-1]}"
-    # process all valid options here
-
-    if [[ '--config' == $prev ]]; then
-        _filedir
-        return
-
-    elif [[ '--descriptor --overrides-file' =~ .*$prev.* ]]; then
-        _filedir yaml
-        return
+_cekit_completionetup;
 
-    elif [[ '--target' == $prev ]]; then
-        _filedir -d
-        return
-
-    elif [[ '--work-dir' == $prev ]]; then
-        _filedir -d
-        return
-
-    elif [[ 'build generate' =~ .*$prev.* ]]; then
-        local options
-        options+=$(_cekit_global_options)
-        options+=$(_cekit_build_options)
-        COMPREPLY=( $( compgen -W "$options" -- $cur ) )
-        return
-
-    elif [[ 'test' == $prev ]]; then
-        local options
-        options+=$(_cekit_global_options)
-        options+=$(_cekit_test_options)
-        COMPREPLY=( $( compgen -W "$options" -- $cur ) )
-        return
+_cekit_cache_completion() {
+    local IFS=$'
+'
+    COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
+                   COMP_CWORD=$COMP_CWORD \
+                   _CEKIT_CACHE_COMPLETE=complete $1 ) )
+    return 0
+}
 
-    elif [[ "$cur" == -* ]]; then
-        local options
-        options+=$(_cekit_global_options)
-        options+='--help '
-        options+='--verbose '
-        options+='--version '
-        options+='--config '
-        options+='--redhat '
-        options+='--package-manager '
-        COMPREPLY=( $( compgen -W "$options" -- $cur ) )
-        return
+_cekit_cache_completionetup() {
+    local COMPLETION_OPTIONS=""
+    local BASH_VERSION_ARR=(${BASH_VERSION//./ })
+    # Only BASH version 4.4 and later have the nosort option.
+    if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] && [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then
+        COMPLETION_OPTIONS="-o nosort"
     fi
 
-    COMPREPLY=( $(compgen -W "generate build test" -- $cur))
+    complete $COMPLETION_OPTIONS -F _cekit_cache_completion cekit-cache
 }
 
-complete -F _cekit_complete cekit
+_cekit_cache_completionetup;
\ No newline at end of file
diff --git a/support/completion/zsh/_cekit b/support/completion/zsh/_cekit
index b2b0db7..6e89d2c 100644
--- a/support/completion/zsh/_cekit
+++ b/support/completion/zsh/_cekit
@@ -1,83 +1,57 @@
-#compdef cekit
-
-(( $+functions[_cekit_commands] )) ||
-_cekit_commands () {
-  local -a cekit_commands
-
-  cekit_commands=(
-    'generate:Generate source'
-    'build:Build container image'
-    'test:Test container image'
-  )
-
-  integer ret=1
+_cekit_completion() {
+    local -a completions
+    local -a completions_with_descriptions
+    local -a response
+    response=("${(@f)$( env COMP_WORDS="${words[*]}" \
+                        COMP_CWORD=$((CURRENT-1)) \
+                        _CEKIT_COMPLETE="complete_zsh" \
+                        cekit )}")
+
+    for key descr in ${(kv)response}; do
+      if [[ "$descr" == "_" ]]; then
+          completions+=("$key")
+      else
+          completions_with_descriptions+=("$key":"$descr")
+      fi
+    done
 
-  _describe -t cekit-commands 'cekit command' cekit_commands && ret=0
+    if [ -n "$completions_with_descriptions" ]; then
+        _describe -V unsorted completions_with_descriptions -U -Q
+    fi
 
-  return ret
+    if [ -n "$completions" ]; then
+        compadd -U -V unsorted -Q -a completions
+    fi
+    compstate[insert]="automenu"
 }
 
-_cekit() {
-  integer ret=1
+compdef _cekit_completion cekit;
 
-  local curcontext=$curcontext state line
-  declare -A opt_args
+_cekit_cache_completion() {
+    local -a completions
+    local -a completions_with_descriptions
+    local -a response
+    response=("${(@f)$( env COMP_WORDS="${words[*]}" \
+                        COMP_CWORD=$((CURRENT-1)) \
+                        _CEKIT_CACHE_COMPLETE="complete_zsh" \
+                        cekit-cache )}")
 
-  _arguments -C -S \
-    "--help[show help message and exit]" \
-    "--verbose[verbose output]" \
-    "--version[show version and exit]" \
-    "--config[path for cekit config file (~/.cekit/config is default)]:config:_files" \
-    "--redhat[Set default options for Red Hat internal infrastructure]" \
-    "--work-dir[Location of cekit working directory, it's used to store dist-git repos]:workdir:_files -/" \
-    "--package-manager[Package  manager to use for installing dependencies. default: 'yum']:package_manager:" \
-    "*--tag[tag used to build/test the image, can be used multiple times]:tags:" \
-    "*--overrides[a YAML object to override image descriptor, can be used multiple times]:overrides:" \
-    "*--overrides-file[path to a file containing overrides, can be used multiple times]:overrides-files:_files" \
-    "--target[path to directory where to generate sources, default: 'target' directory in current working directory]:target:_files -/" \
-    "--descriptor[path to image descriptor file, default: image.yaml]:descriptor:_files -g \*.\(yml\|yaml\)" \
-    '(-): :->command' \
-    '(-)*:: :->option-or-argument' && return
-
-  case $state in
-    (command)
-      _cekit_commands && ret=0
-      ;;
-    (option-or-argument)
-      local cmd=$words[1]
-      curcontext=${curcontext%:*:*}:cekit_$cmd:
-
-      if (( $+functions[_cekit_$cmd] )); then
-        _call_function ret _cekit_$cmd
-      elif zstyle -T :completion:$curcontext: use-fallback; then
-        _files && ret=0
+    for key descr in ${(kv)response}; do
+      if [[ "$descr" == "_" ]]; then
+          completions+=("$key")
       else
-        _message 'unknown sub-command'
+          completions_with_descriptions+=("$key":"$descr")
       fi
-  esac
+    done
 
-  return ret
-}
-
-(( $+functions[_cekit_build] )) ||
-_cekit_build() {
-    _arguments \
-        "--build-engine[an engine used to build the image]" \
-        "--build-pull[Always fetch latest base image during build]" \
-        "--build-osbs-release[execute OSBS release build]" \
-        "--build-osbs-user[user for rphkg tool]" \
-        "--build-osbs-nowait[run rhpkg container build with --nowait option]" \
-        "--build-osbs-stage[use rhpkg-stage instead of rhpkg]" \
-        "--build-osbs-target[overrides the default rhpkg target]" \
-	"--build-osbs-commit-msg[commit message for dist-git]" \
-        "--build-tech-preview[perform tech preview build]"
-}
+    if [ -n "$completions_with_descriptions" ]; then
+        _describe -V unsorted completions_with_descriptions -U -Q
+    fi
 
-(( $+functions[_cekit_test] )) ||
-_cekit_test() {
-    _arguments \
-        "--test-wip[Run @wip tests only]" \
-        "--test-steps-url[contains url for cekit test steps]"
+    if [ -n "$completions" ]; then
+        compadd -U -V unsorted -Q -a completions
+    fi
+    compstate[insert]="automenu"
 }
 
-_cekit
+compdef _cekit_cache_completion cekit-cache;
\ No newline at end of file


Test patch:
diff --git a/tests/test_expose_services.py b/tests/test_expose_services.py
index dd1eb72..15c68dd 100644
--- a/tests/test_expose_services.py
+++ b/tests/test_expose_services.py
@@ -1,4 +1,3 @@
-# test the generation of io.openshift.expose-services label
 # -*- encoding: utf-8 -*-
 
 import os
diff --git a/tests/test_unit_generator_docker.py b/tests/test_unit_generator_docker.py
new file mode 100644
index 0000000..b7fc8fe
--- /dev/null
+++ b/tests/test_unit_generator_docker.py
@@ -0,0 +1,173 @@
+# -*- encoding: utf-8 -*-
+
+# pylint: disable=protected-access
+
+import os
+
+from contextlib import contextmanager
+
+import pytest
+
+from cekit.config import Config
+from cekit.errors import CekitError
+from cekit.generator.docker import DockerGenerator
+
+
+odcs_fake_resp = b"""Result:
+{u'arches': u'x86_64',
+ u'flags': [],
+ u'id': 2019,
+ u'koji_event': None,
+ u'koji_task_id': None,
+ u'owner': u'dbecvari',
+ u'packages': None,
+ u'removed_by': None,
+ u'result_repo': u'http://hidden/compose/Temporary',
+ u'result_repofile': u'http://hidden/Temporary/odcs-2019.repo',
+ u'results': [u'repository'],
+ u'sigkeys': u'FD431D51',
+ u'source': u'rhel-7-server-rpms',
+ u'source_type': 4,
+ u'state': 2,
+ u'state_name': u'done',
+ u'state_reason': u'Compose is generated successfully',
+ u'time_done': u'2018-05-02T14:11:19Z',
+ u'time_removed': None,
+ u'time_submitted': u'2018-05-02T14:11:16Z',
+ u'time_to_expire': u'2018-05-03T14:11:16Z'}"""
+
+odcs_fake_invalid_resp = b"""Result:
+{u'arches': u'x86_64',
+ u'results': [u'repository'],
+ u'sigkeys': u'FD431D51',
+ u'source': u'rhel-7-server-rpms',
+ u'source_type': 4,
+ u'state': 1,
+ u'state_name': u'fail',
+ u'state_reason': u'Compose failed',
+ u'time_to_expire': u'2018-05-03T14:11:16Z'}"""
+
+
+@contextmanager
+def cekit_config(redhat=False):
+    config = Config()
+    config.cfg.update({'common': {'redhat': redhat}})
+
+    try:
+        yield config
+    finally:
+        pass
+
+
+@contextmanager
+def docker_generator(tmpdir, overrides=None):
+    if overrides is None:
+        overrides = {}
+
+    image_dir = str(tmpdir)
+    target_dir = os.path.join(image_dir, 'target')
+    yield DockerGenerator(image_dir, target_dir, overrides)
+
+
+def setup_function():
+    # For each invocation, reset the config
+    Config.cfg = {'common': {}}
+
+
+def test_prepare_content_sets_should_return_early_when_redhat_is_not_set(tmpdir):
+    with docker_generator(tmpdir) as generator:
+        with cekit_config(redhat=False):
+            assert generator._prepare_content_sets({'something': []}) is False
+
+
+def test_prepare_content_sets_should_not_fail_when_cs_is_none(tmpdir):
+    with docker_generator(tmpdir) as generator:
+        with cekit_config(redhat=True):
+            assert generator._prepare_content_sets(None) is False
+
+
+def test_prepare_content_sets_should_not_fail_when_cs_is_empty(tmpdir):
+    with docker_generator(tmpdir) as generator:
+        with cekit_config(redhat=True):
+            assert generator._prepare_content_sets({}) is False
+
+
+def test_prepare_content_sets_should_fail_when_cs_are_note_defined_for_current_platform(tmpdir, mocker):
+    mocker.patch('cekit.generator.docker.platform.machine',
+                 return_value="current_platform")
+
+    with docker_generator(tmpdir) as generator:
+        with cekit_config(redhat=True):
+            with pytest.raises(CekitError, match="There are no contet_sets defined for platform 'current_platform'!"):
+                assert generator._prepare_content_sets(
+                    {'invalid_platform': ['ca1', 'cs2']}) is False
+
+
+def test_prepare_content_sets_should_request_odcs(tmpdir, mocker):
+    mocker.patch('cekit.generator.docker.platform.machine',
+                 return_value="current_platform")
+    mock_odcs = mocker.patch(
+        'cekit.generator.docker.subprocess.check_output', return_value=odcs_fake_resp)
+
+    with docker_generator(tmpdir) as generator:
+        generator.image = {'osbs': {'configuration': {'container': {'compose': {}}}}}
+
+        with cekit_config(redhat=True):
+            assert generator._prepare_content_sets(
+                {'current_platform': ['ca1', 'cs2']}) == "http://hidden/Temporary/odcs-2019.repo"
+
+    mock_odcs.assert_called_once_with(['/usr/bin/odcs', '--redhat', 'create', 'pulp', 'ca1 cs2'])
+
+
+def test_prepare_content_sets_should_request_odcs_with_hidden_repos_flag(tmpdir, mocker):
+    mocker.patch('cekit.generator.docker.platform.machine',
+                 return_value="current_platform")
+    mock_odcs = mocker.patch(
+        'cekit.generator.docker.subprocess.check_output', return_value=odcs_fake_resp)
+
+    with docker_generator(tmpdir) as generator:
+        generator.image = {'osbs': {'configuration': {'container': {
+            'compose': {'include_unpublished_pulp_repos': True}}}}}
+
+        with cekit_config(redhat=True):
+            assert generator._prepare_content_sets(
+                {'current_platform': ['ca1', 'cs2']}) == "http://hidden/Temporary/odcs-2019.repo"
+
+    mock_odcs.assert_called_once_with(
+        ['/usr/bin/odcs', '--redhat', 'create', '--flag', 'include_unpublished_pulp_repos', 'pulp', 'ca1 cs2'])
+
+
+def test_prepare_content_sets_should_handle_incorrect_state(tmpdir, mocker):
+    mocker.patch('cekit.generator.docker.platform.machine',
+                 return_value="current_platform")
+    mock_odcs = mocker.patch(
+        'cekit.generator.docker.subprocess.check_output', return_value=odcs_fake_invalid_resp)
+
+    with docker_generator(tmpdir) as generator:
+        generator.image = {'osbs': {'configuration': {'container': {
+            'compose': {}}}}}
+
+        with cekit_config(redhat=True):
+            with pytest.raises(CekitError, match="Cannot create content set: 'Compose failed'"):
+                generator._prepare_content_sets({'current_platform': ['ca1', 'cs2']})
+
+    mock_odcs.assert_called_once_with(
+        ['/usr/bin/odcs', '--redhat', 'create', 'pulp', 'ca1 cs2'])
+
+
+def test_prepare_content_sets_should_handle_no_odcs_command(tmpdir, mocker):
+    mocker.patch('cekit.generator.docker.platform.machine',
+                 return_value="current_platform")
+    mock_odcs = mocker.patch(
+        'cekit.generator.docker.subprocess.check_output', side_effect=OSError)
+
+    with docker_generator(tmpdir) as generator:
+        generator.image = {'osbs': {'configuration': {'container': {
+            'compose': {}}}}}
+
+        with cekit_config(redhat=True):
+            with pytest.raises(CekitError, match="ODCS is not installed, please install 'odcs-client' package"):
+                generator._prepare_content_sets({'current_platform': ['ca1', 'cs2']})
+
+    mock_odcs.assert_called_once_with(
+        ['/usr/bin/odcs', '--redhat', 'create', 'pulp', 'ca1 cs2'])
diff --git a/tests/test_unit_resource.py b/tests/test_unit_resource.py
index 7d301c6..b140573 100644
--- a/tests/test_unit_resource.py
+++ b/tests/test_unit_resource.py
@@ -1,3 +1,4 @@
+import logging
 import pytest
 import os
 import yaml
@@ -8,6 +9,7 @@ from cekit.errors import CekitError
 
 config = Config()
 
+
 def setup_function(function):
     config.cfg['common'] = {'work_dir': '/tmp'}
 
@@ -192,6 +194,65 @@ def test_path_resource_relative():
     assert res.path == '/foo/bar'
 
 
+def test_path_local_existing_resource_no_cacher_use(mocker):
+    config.cfg['common']['cache_url'] = '#filename#,#algorithm#,#hash#'
+    mocker.patch('os.path.exists', return_value=True)
+    shutil_mock = mocker.patch('shutil.copy')
+
+    res = Resource({'name': 'foo',
+                    'path': 'bar'}, directory='/foo')
+
+    mocker.spy(res, '_download_file')
+
+    res.guarded_copy('target')
+
+    shutil_mock.assert_called_with('/foo/bar', 'target')
+    assert res._download_file.call_count == 0
+
+
+def test_path_local_non_existing_resource_with_cacher_use(mocker):
+    config.cfg['common']['cache_url'] = '#filename#,#algorithm#,#hash#'
+    mocker.patch('os.path.exists', return_value=False)
+    mocker.patch('os.makedirs')
+
+    res = Resource({'name': 'foo',
+                    'path': 'bar'}, directory='/foo')
+
+    mocker.spy(res, '_download_file')
+    download_file_mock = mocker.patch.object(res, '_download_file')
+
+    res.guarded_copy('target')
+
+    download_file_mock.assert_called_with('/foo/bar', 'target')
+
+
+def test_url_resource_download_cleanup_after_failure(mocker, tmpdir, caplog):
+    caplog.set_level(logging.DEBUG, logger="cekit")
+
+    mocker.patch('os.path.exists', return_value=False)
+    mocker.patch('os.makedirs')
+    os_remove_mock = mocker.patch('os.remove')
+
+    urlopen_class_mock = mocker.patch('cekit.descriptor.resource.urlopen')
+    urlopen_mock = urlopen_class_mock.return_value
+    urlopen_mock.getcode.return_value = 200
+    urlopen_mock.read.side_effect = Exception
+
+    res = Resource({'url': 'http://server.org/dummy',
+                    'sha256': 'justamocksum'})
+
+    targetfile = os.path.join(str(tmpdir), 'targetfile')
+
+    with pytest.raises(CekitError) as excinfo:
+        res.guarded_copy(targetfile)
+
+    assert "Error copying resource: 'dummy'. See logs for more info" in str(excinfo.value)
+    assert "Removing incompletely downloaded '{}' file".format(targetfile) in caplog.text
+
+    urlopen_class_mock.assert_called_with('http://server.org/dummy', context=mocker.ANY)
+    os_remove_mock.assert_called_with(targetfile)
+
+
 def test_overide_resource_remove_chksum():
     image = Image(yaml.safe_load("""
     from: foo
diff --git a/tests/test_unit_tools.py b/tests/test_unit_tools.py
index faef21b..b146a0e 100644
--- a/tests/test_unit_tools.py
+++ b/tests/test_unit_tools.py
@@ -10,14 +10,14 @@ from cekit.errors import CekitError
 from cekit import tools
 
 
-class TestDescriptor(Descriptor):
+class MockedDescriptor(Descriptor):
     def __init__(self, descriptor):
         self.schemas = [yaml.safe_load("""type: any""")]
-        super(TestDescriptor, self).__init__(descriptor)
+        super(MockedDescriptor, self).__init__(descriptor)
 
         for key, val in descriptor.items():
             if isinstance(val, dict):
-                self._descriptor[key] = TestDescriptor(val)
+                self._descriptor[key] = MockedDescriptor(val)
 
 
 def test_merging_description_image():
@@ -51,15 +51,15 @@ def test_merging_description_override():
 
 
 def test_merging_plain_descriptors():
-    desc1 = TestDescriptor({'name': 'foo',
+    desc1 = MockedDescriptor({'name': 'foo',
                             'a': 1,
                             'b': 2})
 
-    desc2 = TestDescriptor({'name': 'foo',
+    desc2 = MockedDescriptor({'name': 'foo',
                             'b': 5,
                             'c': 3})
 
-    expected = TestDescriptor({'name': 'foo',
+    expected = MockedDescriptor({'name': 'foo',
                                'a': 1,
                                'b': 2,
                                'c': 3})
@@ -68,17 +68,17 @@ def test_merging_plain_descriptors():
 
 
 def test_merging_emdedded_descriptors():
-    desc1 = TestDescriptor({'name': 'a',
+    desc1 = MockedDescriptor({'name': 'a',
                             'a': 1,
                             'b': {'name': 'b',
                                   'b1': 10,
                                   'b2': 20}})
-    desc2 = TestDescriptor({'b': {'name': 'b',
+    desc2 = MockedDescriptor({'b': {'name': 'b',
                                   'b2': 50,
                                   'b3': 30},
                             'c': {'name': 'c'}})
 
-    expected = TestDescriptor({'name': 'a',
+    expected = MockedDescriptor({'name': 'a',
                                'a': 1,
                                'b': {'name': 'b',
                                      'b1': 10,
@@ -104,19 +104,19 @@ def test_merging_plain_list_of_list():
 
 
 def test_merging_list_of_descriptors():
-    desc1 = [TestDescriptor({'name': 1,
+    desc1 = [MockedDescriptor({'name': 1,
                              'a': 1,
                              'b': 2})]
 
-    desc2 = [TestDescriptor({'name': 2,
+    desc2 = [MockedDescriptor({'name': 2,
                              'a': 123}),
-             TestDescriptor({'name': 1,
+             MockedDescriptor({'name': 1,
                              'b': 3,
                              'c': 3})]
 
-    expected = [TestDescriptor({'name': 2,
+    expected = [MockedDescriptor({'name': 2,
                                 'a': 123}),
-                TestDescriptor({'name': 1,
+                MockedDescriptor({'name': 1,
                                 'a': 1,
                                 'b': 2,
                                 'c': 3})]
diff --git a/tests/test_validate.py b/tests/test_validate.py
index 4f2c193..de50189 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -127,7 +127,7 @@ def test_content_sets_file_container_file(tmpdir, mocker, caplog):
 
     run_cekit_cs_overrides(image_dir, mocker, overrides_descriptor)
 
-    assert "Creating ODCS content set via 'odcs --redhat create pulp aaa bbb'" in caplog.text
+    assert "Creating ODCS content set via '/usr/bin/odcs --redhat create pulp aaa bbb'" in caplog.text
     assert "The image has ContentSets repositories specified, all other repositories are removed!" in caplog.text
 
 
@@ -148,7 +148,7 @@ def test_content_sets_file_container_embedded(tmpdir, mocker, caplog):
 
     run_cekit_cs_overrides(image_dir, mocker, overrides_descriptor)
 
-    assert "Creating ODCS content set via 'odcs --redhat create pulp aaa bbb'" in caplog.text
+    assert "Creating ODCS content set via '/usr/bin/odcs --redhat create pulp aaa bbb'" in caplog.text
     assert "The image has ContentSets repositories specified, all other repositories are removed!" in caplog.text
 
 
@@ -164,7 +164,7 @@ def test_content_sets_embedded_container_embedded(tmpdir, mocker, caplog):
 
     run_cekit_cs_overrides(image_dir, mocker, overrides_descriptor)
 
-    assert "Creating ODCS content set via 'odcs --redhat create pulp aaa bbb'" in caplog.text
+    assert "Creating ODCS content set via '/usr/bin/odcs --redhat create pulp aaa bbb'" in caplog.text
     assert "The image has ContentSets repositories specified, all other repositories are removed!" in caplog.text
 
 
@@ -184,7 +184,7 @@ def test_content_sets_embedded_container_file(tmpdir, mocker, caplog):
 
     run_cekit_cs_overrides(image_dir, mocker, overrides_descriptor)
 
-    assert "Creating ODCS content set via 'odcs --redhat create pulp aaa bbb'" in caplog.text
+    assert "Creating ODCS content set via '/usr/bin/odcs --redhat create pulp aaa bbb'" in caplog.text
     assert "The image has ContentSets repositories specified, all other repositories are removed!" in caplog.text
 
 
diff --git a/tests/test_validate_cache.py b/tests/test_validate_cache.py
index 9b5e323..b7be767 100644
--- a/tests/test_validate_cache.py
+++ b/tests/test_validate_cache.py
@@ -1,15 +1,14 @@
 import os
 import re
 import pytest
-import sys
+
+from click.testing import CliRunner
 
 from cekit.cache.cli import cli
 from cekit.crypto import SUPPORTED_HASH_ALGORITHMS
 
-from click.testing import CliRunner
-
 
-def test_cekit_cache_ls(mocker, tmpdir):
+def test_cekit_cache_ls(tmpdir):
     work_dir = str(tmpdir.mkdir('work_dir'))
 
     result = run_cekit_cache(['-v',
@@ -20,7 +19,7 @@ def test_cekit_cache_ls(mocker, tmpdir):
     assert 'No artifacts cached!' in result.output
 
 
-def test_cekit_cache_rm_non_existing(mocker, tmpdir):
+def test_cekit_cache_rm_non_existing(tmpdir):
     work_dir = str(tmpdir.mkdir('work_dir'))
     run_cekit_cache(['-v',
                      '--work-dir',
@@ -29,7 +28,7 @@ def test_cekit_cache_rm_non_existing(mocker, tmpdir):
                      '12345'], 1)
 
 
-def test_cekit_cache_add_artifact(mocker, tmpdir):
+def test_cekit_cache_add_artifact(tmpdir):
     work_dir = str(tmpdir.mkdir('work_dir'))
 
     artifact = os.path.join(work_dir, 'artifact')
@@ -45,8 +44,21 @@ def test_cekit_cache_add_artifact(mocker, tmpdir):
 
     assert "cached with UUID" in result.output
 
+    # Artifact added to cache should have all supported checksums computed
+    result = run_cekit_cache(['-v',
+                              '--work-dir',
+                              work_dir,
+                              'ls'])
+
+    for alg in SUPPORTED_HASH_ALGORITHMS:
+        assert alg in result.output
+
+    assert "sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" in result.output
+    assert "sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709" in result.output
+    assert "md5: d41d8cd98f00b204e9800998ecf8427e" in result.output
 
-def test_cekit_cache_add_artifact_existing(mocker, tmpdir):
+
+def test_cekit_cache_add_artifact_existing(tmpdir):
     work_dir = str(tmpdir.mkdir('work_dir'))
 
     artifact = os.path.join(work_dir, 'artifact')
@@ -66,7 +78,7 @@ def test_cekit_cache_add_artifact_existing(mocker, tmpdir):
     assert 'is already cached!' in result.output
 
 
-def test_cekit_cache_delete_artifact(mocker, tmpdir):
+def test_cekit_cache_delete_artifact(tmpdir):
     work_dir = str(tmpdir.mkdir('work_dir'))
     artifact = os.path.join(work_dir, 'artifact')
     open(artifact, 'a').close()
@@ -88,7 +100,7 @@ def test_cekit_cache_delete_artifact(mocker, tmpdir):
                      artifact_uuid])
 
 
-def test_cekit_cache_delete_not_existing_artifact(mocker, tmpdir):
+def test_cekit_cache_delete_not_existing_artifact(tmpdir):
     work_dir = str(tmpdir.mkdir('work_dir'))
 
     result = run_cekit_cache(['-v',
@@ -99,7 +111,7 @@ def test_cekit_cache_delete_not_existing_artifact(mocker, tmpdir):
     assert "Artifact with UUID 'foo' doesn't exists in the cache" in result.output
 
 
-def test_cekit_cannot_add_artifact_without_checksum(mocker, tmpdir):
+def test_cekit_cannot_add_artifact_without_checksum(tmpdir):
     work_dir = str(tmpdir.mkdir('work_dir'))
     artifact = os.path.join(work_dir, 'artifact')
     open(artifact, 'a').close()
@@ -113,7 +125,7 @@ def test_cekit_cannot_add_artifact_without_checksum(mocker, tmpdir):
 
 
 @pytest.mark.parametrize('algorithm', SUPPORTED_HASH_ALGORITHMS)
-def test_cekit_supported_algorithms(mocker, tmpdir, algorithm):
+def test_cekit_supported_algorithms(tmpdir, algorithm):
     """
     This is a bit counter intuitive, but we try to cache an artifact
     with all known supported hash algorithms. In case when the algorithm
@@ -125,20 +137,93 @@ def test_cekit_supported_algorithms(mocker, tmpdir, algorithm):
     artifact = os.path.join(work_dir, 'artifact')
     open(artifact, 'a').close()
 
-    result = run_cekit_cache(['-v',
-                              '--work-dir',
-                              work_dir,
-                              'add',
-                              artifact,
-                              "--{}".format(algorithm),
-                              'WRONG!'], 1)
+    result = run_cekit_cache(
+        [
+            '-v',
+            '--work-dir',
+            work_dir,
+            'add',
+            artifact,
+            "--{}".format(algorithm),
+            'WRONG!'
+        ],
+        1)
 
     assert "Cannot cache artifact {}: Artifact checksum verification failed!".format(
         artifact) in result.output
 
 
-def run_cekit_cache(args, rc=0):
-    result = CliRunner().invoke(cli, args, catch_exceptions=False)
-    assert result.exit_code == rc
+def test_cekit_cache_clear(mocker, tmpdir):
+    work_dir = str(tmpdir.mkdir('work_dir'))
+    mock_rmtree = mocker.patch('shutil.rmtree')
+
+    result = run_cekit_cache(
+        [
+            '-v',
+            '--work-dir',
+            work_dir,
+            'clear'],
+        0,
+        'y\n')
+
+    mock_rmtree.assert_called_once_with(os.path.join(work_dir, 'cache'))
+    assert "Artifact cache cleared!" in result.output
+
+
+def test_cekit_cache_clear_no_confirm(mocker, tmpdir):
+    work_dir = str(tmpdir.mkdir('work_dir'))
+    mock_rmtree = mocker.patch('shutil.rmtree')
+
+    result = run_cekit_cache(
+        [
+            '-v',
+            '--work-dir',
+            work_dir,
+            'clear'],
+        0,
+        'n\n')
+
+    mock_rmtree.assert_not_called()
+    assert "Artifact cache cleared!" not in result.output
+
+
+def test_cekit_cache_clear_no_confirm_by_default(mocker, tmpdir):
+    work_dir = str(tmpdir.mkdir('work_dir'))
+    mock_rmtree = mocker.patch('shutil.rmtree')
+
+    result = run_cekit_cache(
+        [
+            '-v',
+            '--work-dir',
+            work_dir,
+            'clear'],
+        0,
+        '\n')
+
+    mock_rmtree.assert_not_called()
+    assert "Artifact cache cleared!" not in result.output
+
+
+def test_cekit_cache_clear_with_error(mocker, tmpdir):
+    work_dir = str(tmpdir.mkdir('work_dir'))
+
+    mocker.patch('shutil.rmtree', side_effect=Exception)
+
+    result = run_cekit_cache(
+        [
+            '-v',
+            '--work-dir',
+            work_dir,
+            'clear'],
+        1,
+        'y\n')
+
+    assert "An error occurred while removing the artifact cache directory '{}'".format(
+        os.path.join(work_dir, 'cache')) in result.output
+
+
+def run_cekit_cache(args, return_code=0, i=None):
+    result = CliRunner().invoke(cli, args, input=i, catch_exceptions=False)
+    assert result.exit_code == return_code
 
     return result