Repository: cekit/cekit

Issue:
Enhance CI to build docs
As part of CI build (for PR) we should also build documentation. Files should be saved as artifacts to check them later. Additionally this will ensure that documentation is still building. 

Discussion:
goldmann: You can even see how it is run in the checks above, or result of the build as saved artifacts here: https://994-102005331-gh.circle-artifacts.com/0/home/circleci/project/docs/_build/html/index.html

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3ed99b9..f2bfe90 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,6 +1,6 @@
 version: 2
 jobs:
-  build:
+  test:
     parallelism: 4
     machine:
       image: circleci/classic:201808-01
@@ -28,3 +28,32 @@ jobs:
           if [[ "${CIRCLE_NODE_INDEX}" == 3 ]]; then
              make test-py37
           fi
+
+  docs-build:
+    machine:
+      image: circleci/classic:201808-01
+    steps:
+      - checkout
+      - run:
+          name: Install docs dependencies
+          command: |
+            cd docs
+            mkdir -p ./venv
+            virtualenv ./venv
+            . venv/bin/activate
+            pip install -r requirements.txt
+      - run:
+          name: Generate documentation
+          command: |
+            cd docs
+            . venv/bin/activate
+            make html
+      - store_artifacts:
+          path: docs/_build
+
+workflows:
+  version: 2
+  build:
+    jobs:
+      - test
+      - docs-build
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..2917776 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)
@@ -156,16 +156,19 @@ class Resource(Descriptor):
 
     def __substitute_cache_url(self, url):
         cache = config.get('common', 'cache_url')
+
         if not cache:
             return url
 
         for algorithm in SUPPORTED_HASH_ALGORITHMS:
             if algorithm in self:
-                logger.debug("Using %s to fetch artifacts from cacher."
-                             % algorithm)
-                return (cache.replace('#filename#', self.name)
-                        .replace('#algorithm#', algorithm)
-                        .replace('#hash#', self[algorithm]))
+                logger.debug("Using {} checksum to fetch artifacts from cacher".format(algorithm))
+
+                url = cache.replace('#filename#', self.name).replace(
+                    '#algorithm#', algorithm).replace('#hash#', self[algorithm])
+
+                logger.debug("Using cache url '{}'".format(url))
+
         return url
 
     def _download_file(self, url, destination, use_cache=True):
@@ -200,12 +203,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))
 
@@ -302,26 +315,30 @@ class _PlainResource(Resource):
         self.target = self.target_file_name()
 
     def _copy_impl(self, target):
-
+        # First of all try to download the file using cacher if specified
         if config.get('common', 'cache_url'):
-            logger.debug("Trying to download artifact %s from remote cache" % self.name)
-            # If cacher URL is set, use it
             try:
-                self._download_file(self.url, target)
+                self._download_file(None, target)
                 return target
-            except:
-                logger.warning("Could not download artifact %s from the remote cache" % self.name)
+            except Exception as e:
+                logger.debug(str(e))
+                logger.warning("Could not download '{}' artifact using cacher".format(self.name))
 
         md5 = self.get('md5')
 
+        # Next option is to download it from Brew directly but only if the md5 checkum
+        # is provided and we are running with the --redhat switch
         if md5 and config.get('common', 'redhat'):
-            logger.debug("Trying to download artifact %s from Brew directly" % self.name)
+            logger.debug("Trying to download artifact '{}' from Brew directly".format(self.name))
 
             try:
-                self.url = get_brew_url(md5)
-                self._download_file(self.url, target, use_cache=False)
+                # Generate the URL
+                url = get_brew_url(md5)
+                # Use the URL to download the file
+                self._download_file(url, target, use_cache=False)
                 return target
-            except:
-                logger.warning("Could not download artifact %s from Brew" % self.name)
+            except Exception as e:
+                logger.debug(str(e))
+                logger.warning("Could not download artifact '{}' from Brew".format(self.name))
 
-        raise CekitError("Artifact %s could not be found" % self.name)
+        raise CekitError("Artifact {} could not be found".format(self.name))
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..feba92b 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'}
 
@@ -68,6 +70,7 @@ def test_fetching_with_ssl_verify(mocker):
     mock_urlopen = get_mock_urlopen(mocker)
 
     res = Resource({'name': 'file', 'url': 'https:///dummy'})
+
     try:
         res.copy()
     except:
@@ -86,6 +89,7 @@ def test_fetching_disable_ssl_verify(mocker):
     get_mock_ssl(mocker, ctx)
 
     res = Resource({'name': 'file', 'url': 'https:///dummy'})
+
     try:
         res.copy()
     except:
@@ -151,7 +155,9 @@ def test_fetching_file_exists_no_hash_fetched_again(mocker):
 
     with open('file', 'w') as f:  # noqa: F841
         pass
+
     res = Resource({'name': 'file', 'url': 'http:///dummy'})
+
     with pytest.raises(CekitError):
         # url is not valid so we get error, but we are not interested
         # in it. We just need to check that we attempted to downlad.
@@ -192,6 +198,122 @@ 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_copy_plain_resource_with_cacher(mocker, tmpdir):
+    config.cfg['common']['cache_url'] = '#filename#,#algorithm#,#hash#'
+    config.cfg['common']['work_dir'] = str(tmpdir)
+
+    urlopen_class_mock = mocker.patch('cekit.descriptor.resource.urlopen')
+    mock_urlopen = urlopen_class_mock.return_value
+    mock_urlopen.getcode.return_value = 200
+    mock_urlopen.read.side_effect = [b"one", b"two", None]
+
+    ctx = get_ctx(mocker)
+    get_mock_ssl(mocker, ctx)
+
+    with open('file', 'w') as f:  # noqa: F841
+        pass
+
+    res = Resource({'name': 'foo',
+                    'md5': '5b9164ad6f496d9dee12ec7634ce253f'})
+
+    substitute_cache_url_mock = mocker.patch.object(
+        res, '_Resource__substitute_cache_url', return_value='http://cache/abc')
+
+    res.copy(str(tmpdir))
+
+    substitute_cache_url_mock.assert_called_once_with(None)
+    urlopen_class_mock.assert_called_with('http://cache/abc', context=ctx)
+
+
+def test_copy_plain_resource_from_brew(mocker, tmpdir):
+    config.cfg['common']['work_dir'] = str(tmpdir)
+    config.cfg['common']['redhat'] = True
+
+    urlopen_class_mock = mocker.patch('cekit.descriptor.resource.urlopen')
+    mock_urlopen = urlopen_class_mock.return_value
+    mock_urlopen.getcode.return_value = 200
+    mock_urlopen.read.side_effect = [b"one", b"two", None]
+
+    ctx = get_ctx(mocker)
+    get_mock_ssl(mocker, ctx)
+
+    with open('file', 'w') as f:  # noqa: F841
+        pass
+
+    res = Resource({'name': 'foo',
+                    'md5': '5b9164ad6f496d9dee12ec7634ce253f'})
+
+    mocker.spy(res, '_Resource__substitute_cache_url')
+
+    mock_get_brew_url = mocker.patch(
+        'cekit.descriptor.resource.get_brew_url', return_value='http://cache/abc')
+
+    res.copy(str(tmpdir))
+
+    mock_get_brew_url.assert_called_once_with('5b9164ad6f496d9dee12ec7634ce253f')
+    res._Resource__substitute_cache_url.call_count == 0
+    urlopen_class_mock.assert_called_with('http://cache/abc', context=ctx)
+
+
 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